ACL: remove ACLOracle canPerform() gas limit#565
Conversation
…heck doesn't revert when Oracle canPerform() is over gas limit.
|
Thanks for opening this pull request! Someone will review it soon 🔍 |
I think this is due to the 63/64 gas forward change from EIP-150; if you test with a lower starting gas amount, you most likely will get a different value. I'm not super sure what value we should use here, but perhaps we should also decrease the saved amount to ~50k (save at least 1-2 SSTOREs after the ACL call)? |
There was a problem hiding this comment.
A few notes from our call with CD.
The goal of this change is to minimize the changeset between the currently deployed and new ACL contracts. In the future, we will want to evaluate whether hard set amounts make sense and whether we should move them to be dynamic (both the gas buffer as well as the 1/64 assumption).
contracts/acl/ACL.sol
Outdated
| assembly { | ||
| ok := staticcall(oracleCheckGas, _oracleAddr, add(checkCalldata, 0x20), mload(checkCalldata), 0, 0) | ||
|
|
||
| if (gasleft() > ERROR_GAS_ALLOWANCE) { |
There was a problem hiding this comment.
As mentioned in the call, we should use gasleft() once and then do operations on the same cached value afterwards to avoid any potential underflows.
… for use in all ACL CheckOracle calculations. Use max of gas values for require statement. Move sig var inline to prevent stack too deep error. Move StateModifyingOracle test to JS. Add test for specific permission owner.
|
In the most recent commit, as well as doing the changes requested, I have had to remove the |
Good catch I forgot about this! Trying to recall it I was reading that EIP and got confused about this: If that if condition holds, then |
bingen
left a comment
There was a problem hiding this comment.
Overall looks good to me. Thanks @willjgriff for all this work!
I left a slight modification proposal inline.
|
|
||
| if (!ok) { | ||
| require(gasleft() > max(gasLeftBeforeCall / 64, ERROR_GAS_ALLOWANCE), ERROR_ORACLE_OOG); | ||
| return false; |
There was a problem hiding this comment.
I would refactor all this as:
uint256 gasLeftBeforeCall = gasleft();
// I don't think we even need this, but if we do I would convert it to a require:
// require(gasLeftBeforeCall > ERROR_GAS_ALLOWANCE, ERROR_ORACLE_OOG);
assembly {
ok := staticcall(gasLeftBeforeCall, _oracleAddr, add(checkCalldata, 0x20), mload(checkCalldata), 0, 0)
}
if (!ok) {
uint256 gasLeftAfterCall = gasLeft();
require(gasLeftAfterCall > gasLeftBeforeCall / 64 && gasLeftAfterCall > ERROR_GAS_ALLOWANCE), ERROR_ORACLE_OOG);
return false;
}
And we can get rid of the max function below. I haven't checked, but as Solidity doesn't have inline functions we may save some gas this way. Also, I think if we split the require in 2 (instead oh using && inside) it's slightly cheaper.
There was a problem hiding this comment.
If we use all the available gas in the staticcall is there a chance there won't be enough left over for returning the error (even with the 1/64th rule)? After testing I can't find a gas limit where that's the case (eg just a revert without the error message) but it wasn't exhaustive. Also, due to a stack too deep error I can't add any more variables eg gasLeftAfterCall unless I remove something eg oracleCanPerformGas. So I can do this if you think the second require statement can always be reached. Eg the answer to my question is no.
There was a problem hiding this comment.
If we use all the available gas in the
staticcallis there a chance there won't be enough left over for returning the error (even with the 1/64th rule)?
In that case we could leave the commented require(gasLeftBeforeCall > ... check, with the proper amount to make sure that 1/64 of that amount is enough.
Anyway, even if that happen, what would be the problem? It would just revert, right? So compared to what would happen later in require(gasLeftAfterCall >... we would just miss the error message? Or am I missing something?
There was a problem hiding this comment.
Yes we would just miss the error message. @sohkai do you have a preference?
I would like to remove the max() function but I can't unless I remove the oracleCanPerformGas variable (or some unrelated variable somewhere) which would mean passing all of the gas to the function.
|
Not necessarily affecting the plans, but interesting to know that We use |
aabf83c to
66304cc
Compare
|
@willjgriff I've pushed a few commits. You'll notice I've reverted most of the logic around reserving gas, and just opted to send all the remaining gas to the oracle. First of all, sorry for making you run around in a few circles in hopes that we could make this mechanism work and provide a better debugging message. I've thought about it for a while now, and came to the conclusion that trying to save gas is relatively futile. While executing the revert statement is light on gas—usually lower than 1000 gas, placing the call gas requirement to safely execute the revert to be 40-60k with the 63/64 gas forward rule (it's only really in trouble inside the 2300 gas stipends)—in actual debugging we are likely to suspect out of gas issues through the gas used in a transaction as well as specific traces (parity's is much more helpful than geth's in this manner). In particular, tools like Tenderly also help us debug which parts of a transaction may have used up a lot of gas, and it should be fairly obvious if the oracle took up all the required gas. I started thinking about just leaving the 1/64 check, but then also started suspecting if this was really required (as I mention above, we have other heuristics to suspect out of gas scenarios). Hardcoding |
66304cc to
7f1b041
Compare
7f1b041 to
7eae34d
Compare
test/contracts/acl/acl_params.js
Outdated
| // Note `evalParams()` is only called once when calling `hasPermission` for a specific address | ||
| it('ACL disallows actions', async () => { | ||
| await acl.grantPermissionP(permissionsRoot, mockAppAddress, MOCK_APP_ROLE, [param]) | ||
| assert.isFalse(await acl.hasPermission(permissionsRoot, mockAppAddress, MOCK_APP_ROLE)) |
There was a problem hiding this comment.
Note that in these cases that are now not throwing due to sending all gas (truffle by default is using 6.9M gas), it may be interesting to experiment with sending a transaction with lower gas values to see if they start failing and adding some tests around this.
We could also add some assertions for the gas used in these transactions to double check that the oracle is indeed eating up 63/64th of the gas available.
…locking tests for coverage.
sohkai
left a comment
There was a problem hiding this comment.
Just updated the tests a bit, reorganizing them and adding a few more checks :).
Got caught up with us giving account[0] permissions and re-using it in tests. Generally, for anything permissions-based, you want to avoid testing with account[0] since it's the default account sending transactions.
| // Assuming we incur 40-60k gas overhead for this, we only have ~140,000 gas left. | ||
| // After the oracle call, we only have 140,000 / 64 ~= 2000 gas left, which begins to | ||
| // quick run out with SLOADs. | ||
| const LOW_GAS = 180000 |
There was a problem hiding this comment.
This seems like quite a lot of gas, since I initially only assumed that we would be reverting under very low gas constraints.
However, in these tests, we are using a full organization, with proxies and all, and this adds a lot of overhead. We revert here because we run out of gas after the SLOAD for the ANY_ADDR's permission params (if we flip the order that hasPermission() checks entities such that it checks the ANY_ADDR before the specific who, we can lower this LOW_GAS value).

This PR will enable the 1Hive Oracles to function as expected since the Istanbul hard fork. Unfortunately our Oracles
canPerform()functions can not be guaranteed to execute under the gas limit of30000.Relevant functions in the Oracles can be seen here:
https://github.com/1Hive/token-oracle/blob/master/contracts/TokenBalanceOracle.sol#L59
https://github.com/1Hive/dandelion-voting-app/blob/master/contracts/DandelionVoting.sol#L252
I've replaced the Oracle
canPerform()staticcallgas limit of30000withgasleft()minus some amount for returning an error (preventing a revert if thestaticcallruns out of gas). As suggested by @sohkai.I've observed that if the
staticcallreverts with an oog after being given all ofgasleft()it still seems to reserve about100000gas, as can be seen with agasleft()directly after the oogstaticcall(tested on remix). The function doesn't revert and returns false. I've still included the error gas allowance but it may not be necessary.I had to extract
testRevertOracleandtestStateModifyingOraclefrom thetestOracletest because when thestaticcallreverts it consumes all the available gas, leaving very little to none for the remaining tests in the same transaction.