Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
22 changes: 15 additions & 7 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 @@ -417,18 +418,21 @@ contract ACL is IACL, TimeHelpers, AragonApp, ACLHelpers {
}

function checkOracle(IACLOracle _oracleAddr, address _who, address _where, bytes32 _what, uint256[] _how) internal view returns (bool) {
bytes4 sig = _oracleAddr.canPerform.selector;

// 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;
bytes memory checkCalldata = abi.encodeWithSelector(_oracleAddr.canPerform.selector, _who, _where, _what, _how);

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

if (gasLeftBeforeCall > ERROR_GAS_ALLOWANCE) {
uint256 oracleCanPerformGas = gasLeftBeforeCall - ERROR_GAS_ALLOWANCE;
assembly {
ok := staticcall(oracleCanPerformGas, _oracleAddr, add(checkCalldata, 0x20), mload(checkCalldata), 0, 0)
}
}

if (!ok) {
require(gasleft() > max(gasLeftBeforeCall / 64, ERROR_GAS_ALLOWANCE), ERROR_ORACLE_OOG);
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 All @@ -449,6 +453,10 @@ contract ACL is IACL, TimeHelpers, AragonApp, ACLHelpers {
return result;
}

function max(uint256 _a, uint256 _b) internal pure returns (uint256) {
return _a > _b ? _a : _b;
}

/**
* @dev Internal function that sets management
*/
Expand Down
11 changes: 11 additions & 0 deletions contracts/test/helpers/ACLHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ contract ConditionalOracle is IACLOracle {
return how[0] > 0;
}
}

contract OverGasLimitOracle is IACLOracle {
uint256 storageVar;

function canPerform(address, address, bytes32, uint256[]) external view returns (bool) {
while (true) {
uint256 memoryVar = storageVar;
}
return true;
}
}
2 changes: 0 additions & 2 deletions contracts/test/tests/TestACLInterpreter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ contract TestACLInterpreter is ACL, ACLHelper {

// 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 Down
63 changes: 63 additions & 0 deletions test/contracts/acl/acl_params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const { assertRevert } = require('../../helpers/assertThrow')
const { paramForOracle } = require('../../helpers/permissionParams')

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

const ANY_ADDR = '0xffffffffffffffffffffffffffffffffffffffff'

contract('ACL', ([permissionsRoot, mockAppAddress]) => {
let aclBase, kernelBase, acl, kernel
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())
await acl.createPermission(permissionsRoot, mockAppAddress, MOCK_APP_ROLE, permissionsRoot)
})

it('ACLOracle succeeds when oracle canPerform returns true', async () => {
const acceptOracle = await AcceptOracle.new()
const param = paramForOracle(acceptOracle.address)
await acl.grantPermissionP(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE, [param])
assert.isTrue(await acl.hasPermission(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE))
})

it('ACLOracle fails when oracle canPerform modifies state', async () => {
const stateModifyingOracle = await StateModifyingOracle.new()
const param = paramForOracle(stateModifyingOracle.address)
await acl.grantPermissionP(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE, [param])
await assertRevert(acl.hasPermission(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE), "ACL_ORACLE_OOG")
})

context('> ACLOracle OverGasLimitOracle', () => {
let overGasLimitOracle, param

beforeEach(async () => {
overGasLimitOracle = await OverGasLimitOracle.new()
param = paramForOracle(overGasLimitOracle.address)
})

// Note `evalParams()` is called twice when calling `hasPermission` for `ANY_ADDR`
it('fails when oracle canPerform goes OOG', async () => {
await acl.grantPermissionP(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE, [param])
await assertRevert(acl.hasPermission(ANY_ADDR, mockAppAddress, MOCK_APP_ROLE), "ACL_ORACLE_OOG")
})

// Note `evalParams()` is only called once when calling `hasPermission` for a specific address
it('fails when oracle canPerform goes OOG with specified permission owner', async () => {
await acl.grantPermissionP(permissionsRoot, mockAppAddress, MOCK_APP_ROLE, [param])
await assertRevert(acl.hasPermission(permissionsRoot, mockAppAddress, MOCK_APP_ROLE), "ACL_ORACLE_OOG")
})
})
})
12 changes: 12 additions & 0 deletions test/helpers/permissionParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

const paramForOracle = (oracleAddress) => {
// 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${oracleAddress.slice(2)}`
return new web3.BigNumber(`${argId}${op}${value}`)
}

module.exports = {
paramForOracle
}