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

ACL: remove ACLOracle canPerform() gas limit #565

Merged
merged 18 commits into from
Feb 6, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c53f160
Remove ACLOracle canPerform() call gas limit. Add test to check ACL c…
willjgriff Jan 10, 2020
74b0c42
Added SafeMath to prevent underflow. Added comment regarding oog iter…
willjgriff Jan 10, 2020
1785c3a
Reduced Error Allowance. Simplified OverGasLimitOracle.
willjgriff Jan 20, 2020
79628be
Revert when detected ACL CheckOracle `staticcall` is OOG.
willjgriff Jan 21, 2020
2771428
Remove SafeMath from ACL. Remove Solidity test for ACL Oracle. Add JS…
willjgriff Jan 21, 2020
38c9e04
Formatting changes.
willjgriff Jan 21, 2020
0606ebe
Added memoryVar setting to OverGasLimitOracle. Stored current gasleft…
willjgriff Jan 22, 2020
fed5fac
Moved ACL param creation test util function to a separate file. Updat…
willjgriff Jan 22, 2020
a3f94ef
Add JS test for successful ACL oracle.
willjgriff Jan 23, 2020
7169d41
Merge branch 'next' into acl-remove-oracle-limit
sohkai Jan 31, 2020
c72b9f9
test: cosmetic updates, more exhaustive oracle test cases
sohkai Jan 31, 2020
7eae34d
ACL: send all gas to oracle
sohkai Feb 1, 2020
18800ad
ACL: add comments for forwarding all gas to oracle
sohkai Feb 1, 2020
a18faf1
Added alternative gas amount tests for failing OOG oracle. Disabled b…
willjgriff Feb 3, 2020
b11da3c
test: use skipCoverage() in ACLOracle test
sohkai Feb 5, 2020
4d4279a
Added gas checks to ACL params tests. Replaced for loop with `revert`…
willjgriff Feb 5, 2020
70b19fb
test: update ACLOracle tests, fix skipping coverage of entire suite
sohkai Feb 5, 2020
3882538
test: add more comments to ACL oracle gas tests
sohkai Feb 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions contracts/acl/ACL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ contract ACL is IACL, TimeHelpers, AragonApp, ACLHelpers {
address public constant ANY_ENTITY = address(-1);
address public constant BURN_ENTITY = address(1); // address(0) is already used as "no permission manager"

uint256 internal constant ORACLE_CHECK_GAS = 30000;
uint256 internal constant ERROR_GAS_ALLOWANCE = 50000;

string private constant ERROR_AUTH_INIT_KERNEL = "ACL_AUTH_INIT_KERNEL";
string private constant ERROR_AUTH_NO_MANAGER = "ACL_AUTH_NO_MANAGER";
string private constant ERROR_EXISTENT_MANAGER = "ACL_EXISTENT_MANAGER";
string private constant ERROR_ORACLE_OOG = "ACL_ORACLE_OOG";

// Whether someone has a permission
mapping (bytes32 => bytes32) internal permissions; // permissions hash => params hash
Expand Down Expand Up @@ -421,14 +422,18 @@ contract ACL is IACL, TimeHelpers, AragonApp, ACLHelpers {

// a raw call is required so we can return false if the call reverts, rather than reverting
bytes memory checkCalldata = abi.encodeWithSelector(sig, _who, _where, _what, _how);
uint256 oracleCheckGas = ORACLE_CHECK_GAS;

bool ok;
assembly {
ok := staticcall(oracleCheckGas, _oracleAddr, add(checkCalldata, 0x20), mload(checkCalldata), 0, 0)

if (gasleft() > ERROR_GAS_ALLOWANCE) {
Copy link
Contributor

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.

uint256 oracleCheckGas = gasleft() - ERROR_GAS_ALLOWANCE;
assembly {
ok := staticcall(oracleCheckGas, _oracleAddr, add(checkCalldata, 0x20), mload(checkCalldata), 0, 0)
}
}

if (!ok) {
require(gasleft() > ERROR_GAS_ALLOWANCE, ERROR_ORACLE_OOG);
sohkai marked this conversation as resolved.
Show resolved Hide resolved
return false;
Copy link
Contributor

@bingen bingen Jan 23, 2020

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.

Copy link
Contributor Author

@willjgriff willjgriff Jan 24, 2020

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

}

Expand Down
7 changes: 7 additions & 0 deletions contracts/test/helpers/ACLHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,10 @@ contract ConditionalOracle is IACLOracle {
return how[0] > 0;
}
}

contract OverGasLimitOracle is IACLOracle {
function canPerform(address, address, bytes32, uint256[]) external view returns (bool) {
while (true) {}
return true;
sohkai marked this conversation as resolved.
Show resolved Hide resolved
}
}
14 changes: 10 additions & 4 deletions contracts/test/tests/TestACLInterpreter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,6 @@ contract TestACLInterpreter is ACL, ACLHelper {
assertEval(arr(), ORACLE_PARAM_ID, Op.EQ, uint256(new RejectOracle()), false);
assertEval(arr(), ORACLE_PARAM_ID, Op.NEQ, uint256(new RejectOracle()), true);

// doesn't revert even if oracle reverts
assertEval(arr(), ORACLE_PARAM_ID, Op.EQ, uint256(new RevertOracle()), false);
// the staticcall will error as the oracle tries to modify state, so a no is returned
assertEval(arr(), ORACLE_PARAM_ID, Op.EQ, uint256(new StateModifyingOracle()), false);
// if returned data size is not correct, returns false
assertEval(arr(), ORACLE_PARAM_ID, Op.EQ, uint256(new EmptyDataReturnOracle()), false);

Expand All @@ -94,6 +90,16 @@ contract TestACLInterpreter is ACL, ACLHelper {
assertEval(arr(uint256(0), uint256(1)), ORACLE_PARAM_ID, Op.EQ, uint256(conditionalOracle), false);
}

function testRevertOracle() public {
// doesn't revert even if oracle reverts
assertEval(arr(), ORACLE_PARAM_ID, Op.EQ, uint256(new RevertOracle()), false);
}

function testStateModifyingOracle() public {
// the staticcall will error as the oracle tries to modify state, so a no is returned
assertEval(arr(), ORACLE_PARAM_ID, Op.EQ, uint256(new StateModifyingOracle()), false);
}

function testReturn() public {
assertEval(arr(), PARAM_VALUE_PARAM_ID, Op.RET, uint256(1), true);
assertEval(arr(), PARAM_VALUE_PARAM_ID, Op.RET, uint256(0), false);
Expand Down
42 changes: 42 additions & 0 deletions test/contracts/acl/acl_params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { assertRevert } = require('../../helpers/assertThrow')

const ACL = artifacts.require('ACL')
const Kernel = artifacts.require('Kernel')
const KernelProxy = artifacts.require('KernelProxy')
const OverGasLimitOracle = artifacts.require('OverGasLimitOracle')

const ANY_ADDR = '0xffffffffffffffffffffffffffffffffffffffff'

contract('ACL', ([permissionsRoot, mockAppAddress]) => {
let aclBase, kernelBase, acl, kernel, overGasLimitOracle
const MOCK_APP_ROLE = "0xAB"

before(async () => {
kernelBase = await Kernel.new(true) // petrify immediately
aclBase = await ACL.new()
})

beforeEach(async () => {
kernel = Kernel.at((await KernelProxy.new(kernelBase.address)).address)
await kernel.initialize(aclBase.address, permissionsRoot)
acl = ACL.at(await kernel.acl())
overGasLimitOracle = await OverGasLimitOracle.new()
})

context('> ACLOracle Permission', () => {

it('fails when oracle canPerform goes OOG', async () => {
// Set role such that the Oracle canPerform() function is used to determine the permission
const argId = '0xCB' // arg 203 - Oracle ID
const op = '01' // equal
const value = `00000000000000000000${overGasLimitOracle.address.slice(2)}` // oracle address
const param = new web3.BigNumber(`${argId}${op}${value}`)

await acl.createPermission(permissionsRoot, mockAppAddress, MOCK_APP_ROLE, permissionsRoot)
await acl.grantPermissionP(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE, [param])

await assertRevert(acl.hasPermission(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE), "ACL_ORACLE_OOG")
})

})
})