-
Notifications
You must be signed in to change notification settings - Fork 246
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
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
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.
Overall looks good to me. Thanks @willjgriff for all this work!
I left a slight modification proposal inline.
contracts/acl/ACL.sol
Outdated
} | ||
|
||
if (!ok) { | ||
require(gasleft() > max(gasLeftBeforeCall / 64, ERROR_GAS_ALLOWANCE), ERROR_ORACLE_OOG); | ||
return false; |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()
staticcall
gas limit of30000
withgasleft()
minus some amount for returning an error (preventing a revert if thestaticcall
runs out of gas). As suggested by @sohkai.I've observed that if the
staticcall
reverts with an oog after being given all ofgasleft()
it still seems to reserve about100000
gas, 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
testRevertOracle
andtestStateModifyingOracle
from thetestOracle
test because when thestaticcall
reverts it consumes all the available gas, leaving very little to none for the remaining tests in the same transaction.