-
Couldn't load subscription status.
- Fork 0
Implement new gas tracking contracts #874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: uups
Are you sure you want to change the base?
Conversation
| fallback() external { | ||
| meterCall(msg.data); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed, but I kinda think its a cool way for SeqChains contracts to call GasMeter for any of its functions! If people hate it, happy to drop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the MyContract(gasMeterAddress).fooBar syntax, but think it's not worth it since using a fallback function can lead to function name collisions and unexpected behavior if the gas aggregator and sequencing contracts define a function with the same name and parameters. This is especially dangerous if the gas aggregator is updated later on to add new functions to it - the contract upgrade can break existing sequencing contracts that will end up calling the newly added function instead of fallback().
Instead of abi.encodeWithSelector, you can use abi.encodeCall to get the same type-checking benefits as the fallback approach, even if the syntax is slightly uglier.
| uint256 version; | ||
| /// @notice The address of the GasMeter contract | ||
| /// @dev This is set during initialization and never changes | ||
| address gasMeter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it makes sense to move this into a different struct, since its not related to the CORE attributes of the chain and we could probably drop it once emissions are done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make this an immutable variable instead to save gas - if we really want to modify this post-deployment, we can just redeploy the implementation contract with a new gasMeter value and upgrade the proxy to the new implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work - I love this design pattern & think it's a really clean and extensible way to track gas and run pre/post tx hooks! I left a couple small comments/questions, but it looks great overall
| fallback() external { | ||
| meterCall(msg.data); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the MyContract(gasMeterAddress).fooBar syntax, but think it's not worth it since using a fallback function can lead to function name collisions and unexpected behavior if the gas aggregator and sequencing contracts define a function with the same name and parameters. This is especially dangerous if the gas aggregator is updated later on to add new functions to it - the contract upgrade can break existing sequencing contracts that will end up calling the newly added function instead of fallback().
Instead of abi.encodeWithSelector, you can use abi.encodeCall to get the same type-checking benefits as the fallback approach, even if the syntax is slightly uglier.
| uint256 version; | ||
| /// @notice The address of the GasMeter contract | ||
| /// @dev This is set during initialization and never changes | ||
| address gasMeter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make this an immutable variable instead to save gas - if we really want to modify this post-deployment, we can just redeploy the implementation contract with a new gasMeter value and upgrade the proxy to the new implementation.
| uint256 startGas = gasleft(); | ||
| (bool success, bytes memory result) = address(msg.sender).call(meteredCall); | ||
| if (!success) { | ||
| revert(string(result)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
think we should use
assembly { revert(result, mload(result)) }
instead to perfectly forward the revert reason. I think revert(string) will double abi-encode the error since result should already be abi-encoded, but not 100% sure if that's how it works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried updating to that assembly code, but a bunch of tests started failing due to lack of revert reasons, so I think the current way is correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yep it should be assembly { revert(add(result, 32), mload(result)) } instead. I guess revert(string(result)) works too though, apparently the compiler is smart enough to figure out that it's already encoded and doesn't double encode the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some digging and it turns out the error is being double abi encoded, just the foundry vm.expectRevert function automatically strips the "Error(string)" selector that revert(string) adds to errors if it is present. So I think we should definitely use the assembly code to prevent that.
| if (!success) { | ||
| revert(string(result)); | ||
| } | ||
| _getGasMeterStorage().gasUsed[getCurrentEpoch()][msg.sender] += startGas - gasleft(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we may want to add
assembly { return(result, mload(result)) }
to perfectly forward the return value. I doubt any code will use this return value, but it's probably worth forwarding it regardless just in case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also think this should be (startGas - gasleft()) * tx.gasprice instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the assembly should be
assembly { return(add(result, 32), mload(result)) }
instead
|
|
||
| /// @notice Disable gas tracking if needed | ||
| /// @dev Only callable by the contract owner | ||
| function disableGasTracking() external onlyOwner { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one nice property of this design is the chain owner can disable gas tracking at any point in time by upgrading the sequencing contract to remove the calls to the gas meter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. It would be nice to keep it.
| /// @dev Allows for contracts to call the gas meter address like they would call a function on their own contract to track gas used | ||
| /// Allows for usage like: MyContract(gasMeterAddress).fooBar(arg1, arg2, arg3); | ||
| /// Instead of: gasMeterAddress.meterCall(abi.encodeWithSelector(MyContract.fooBar.selector, arg1, arg2, arg3)); | ||
| fallback() external { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think we need some sort of reentrancy guard, to prevent a contract doing:
gas meter -> myImpl -> gas meter -> myImpl -> ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch. alternatively, I think we can allow reentrancy and change the code to something like
uint256 startGas = gasleft() + _getGasMeterStorage().gasUsed[getCurrentEpoch()][msg.sender];
address(msg.sender).call(meteredCall);
_getGasMeterStorage().gasUsed[getCurrentEpoch()][msg.sender] = startGas - gasleft();
instead
| /// @notice Meter a call and track the gas used | ||
| /// @dev Meters the gas used for a call and tracks it in the gas used mapping | ||
| /// @param meteredCall The call to track gas for | ||
| function meterCall(bytes calldata meteredCall) public { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we make this internal, so all calls come via the fallback method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this solution is really elegant and a step up from what we had before, great thinking! 🧠
0ccbde1 to
c61f2d8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm % immutable gasMeter variable, return value forwarding, and ReentrancyGuardTransientUpgradeable
| import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
| import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; | ||
| import {EpochTracker} from "./EpochTracker.sol"; | ||
| import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use ReentrancyGuardTransientUpgradeable instead? that uses transient instead of regular storage and should be a lot cheaper in terms of gas cost
| abi.encodeCall(SyndicateSequencingChain.initialize, (admin, address(permissionModule), chainId)); | ||
| (bool upgradeSuccess,) = sequencingChain.call( | ||
| abi.encodeWithSignature("upgradeToAndCall(address,bytes)", $.syndicateChainImpl, initData) | ||
| abi.encodeWithSignature("upgradeToAndCall(address,bytes)", syndicateChainImpl, initData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, ...) should work here for explicit type checking
| } | ||
| } | ||
|
|
||
| uint256 gasPrice = tx.gasprice == 0 ? 1 : tx.gasprice; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably leave a comment that explains why this workaround is needed for gas estimation.
The necessity of 0 ? 1 might be lost to the reader in the future
What does this PR do?
Summary:
Updates upgradable contracts to use an external contract for tracking gas called GasMeter. Any appchain can call this contract directly with the function they want tracked (through the fallback function on GasMeter) or explicitly with
.meterCall(...). GasMeter will then call back to the msg.sender with the passed in calldata (or exact calldata if through the fallback function) while keeping track of how much gas it uses and stores it per epoch.How can this PR be tested?
forge test