Skip to content
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

add tests for more sufficient gas consumption testing in callWithExactGas library #13217

Closed
wants to merge 15 commits into from

Conversation

jhweintraub
Copy link
Collaborator

@jhweintraub jhweintraub commented May 15, 2024

The helper library CallWithExactGas.sol did not have any tests that ensured that all of the gas provided is properly consumed in the event that an incorrect amount of gas is passed in. CCIP makes extensive use of this library and so this tests attempts to ensure that all gas is properly utilized when contracts are called with the helper functions.

Added two tests to callWithExactGas.t.sol

  1. test_callWithExactGasAndConsumeAll which creates a mock contract and ensures that the amount of gas consumed is the same as the amount passed in.
  2. test_callWithExactGasAndConsumeMoreThanProvided which attempts to consume more gas than is provided in the event that gasLimit > (63/64) of available gas passed to the child call in conjunction with EIP-150

@@ -16,9 +16,9 @@
"compile": "hardhat compile",
"coverage": "hardhat coverage",
"prepublishOnly": "pnpm compile && ./scripts/prepublish_generate_abi_folder",
"publish-beta": "pnpm publish --tag beta",
"publish- beta": "pnpm publish --tag beta",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have to assume this is unintentional? it will likely break github release workflow

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea that's definitely unintentional. Idk how that got changed but i'll revert it

contracts/package.json Outdated Show resolved Hide resolved

function throwOutOfGasError() external pure {
while (true) {
//Intentionally consume all gas and throw an OOG error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
//[space]comment

@@ -216,6 +220,41 @@ contract CallWithExactGas__callWithExactGasSafeReturnData is CallWithExactGasSet
assertGt(gasUsed, 500);
}

function test_CallWithExactGasSafeReturnData_ConsumeAllGas_Success() external {
uint16 maxRetBytes = 0;
uint256 miscGasOverhead = 128;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls document how the overhead is calculated as 128; also it should be a constant right?

uint256 internal constant DEFAULT_GAS_LIMIT = 20_000;
uint16 internal constant DEFAULT_GAS_FOR_CALL_EXACT_CHECK = 5000;
uint256 internal constant EXTCODESIZE_GAS_COST = 2600;

// Calculated as the difference between gasUsed as returned from function and gas passed in to the function
uint256 internal constant CALL_WITH_EXACT_GAS_SAFE_RETURN_DATA_GAS_OVERHEAD = 128;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain why the difference between gasUsed as returned from function and gas passed in to the function turns out to be 128, btw is this overhead constant regardless of receiver logic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not able to say that with complete certainty due to optimizations in the compiler that may insert additional unexpected bytecode. The only way to get that level of absolute certainty would be to write a minimal huff-contract to ensure that every opcode is expected. 128 gas was determined as overhead because that was the difference in value returned from SafeCallWithExactGas. That function was called with 20k gas, but the gasUsed returned was 20,128. Since its impossible for the contract to use more than the gas it was assigned, the only logical explanation was that there's additional gas overhead in the call that isn't being properly accounted for.

If you want more certainty than I can attempt that level of granular bytecode construction with huff but it seems kinda like overkill.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, looking at the code, it's likely constant, can you:

  1. pseudo-verify it is constant regardless of gas limit/payload/receiver by trying out a few different inputs
  2. succinctly explain in comments the heuristics used to arrive at 18, similar to context above

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do my best. Without inspecting the bytecode manually or writing it in Huff it's very difficult to know exactly how much it takes cause of the compiler optimizations. I will write out the comments and see what I can do.

It looks like the actual amount is somewhere between 122 and 140 gas depending on the gas limit and where the loop ends.

} {
// If 30 gas is remaining, then exit the loop by returning
if lt(gas(), 30) {
pop(add(0, 0)) // Add two numbers but don't push result onto the stack. Safely consume any residual gas from an odd number remaining.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good job on going granular, please explain how the values are calculated, the important thing is the why, e.g:

  1. why terminate at 30, but not like 40, or 20
  2. why consume residual gas via pop(add(0, 0)) as opposed more loop iterations

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That value was calculated via manual binary search to determine the least amount of gas possible which can be returned. 30 was the least amount of gas that could remain before an OOG error would be thrown on this call.

Based on what I was able to deduce, one more iteration of the loop would cost more gas than remains, but you need to consume the remaining gas. I.E you leave 30 gas because 1 more loop would cost >30, but you still need to consume the gas. Add is one of the cheapest non-state-changing opcode you can perform, but yul requires you to pop it off the stack of assign it to a variable, and using pop ensures we're not charged the extraneous cost for storing in memory/stack. I will add all of these to comments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not be reliable right? for example if you need 20 gas to return, with current setup it might reach gas() with 28 gas, and it is able to return, however if someone changes the gas limit passed in, and if it happens to be 22 gas remaining when reaching gas(), then the test might fail.

Copy link
Contributor

@matYang matYang May 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe consider a range of input gases, eg. 20k, 20k+1, 20k+2,... 20k+30
and locate the minimum gas cutoff number that make all of them pass

contracts/package.json Outdated Show resolved Hide resolved
matYang
matYang previously approved these changes May 23, 2024
);

assertFalse(success, "Error: External Call Succeeded where it should not");
assertEq(retData.length, 0, "retData should be zero for OOG error");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since maxRetBytes is zero, this doesn't actually test anything. If anything was returned it would have been cut off

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gas consumer contract which is invoked explicitly does not return anything when called so it doesn't make sense to include a max return bytes for this contract.

        if lt(gas(), 60) {
          return(0x0, 0x0) // Return with no return data
        }

@cl-sonarqube-production
Copy link

Quality Gate passed Quality Gate passed

Issues
0 New issues
0 Fixed issues
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
No data about Duplication

See analysis details on SonarQube

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants