parent
645e2a6c4a
commit
9efe429a18
@ -0,0 +1,69 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {IAccessManaged} from "../access/manager/IAccessManaged.sol"; |
||||
import {IAuthority} from "../access/manager/IAuthority.sol"; |
||||
|
||||
contract NotAuthorityMock is IAuthority { |
||||
function canCall(address /* caller */, address /* target */, bytes4 /* selector */) external pure returns (bool) { |
||||
revert("AuthorityNoDelayMock: not implemented"); |
||||
} |
||||
} |
||||
|
||||
contract AuthorityNoDelayMock is IAuthority { |
||||
bool _immediate; |
||||
|
||||
function canCall( |
||||
address /* caller */, |
||||
address /* target */, |
||||
bytes4 /* selector */ |
||||
) external view returns (bool immediate) { |
||||
return _immediate; |
||||
} |
||||
|
||||
function _setImmediate(bool immediate) external { |
||||
_immediate = immediate; |
||||
} |
||||
} |
||||
|
||||
contract AuthorityDelayMock { |
||||
bool _immediate; |
||||
uint32 _delay; |
||||
|
||||
function canCall( |
||||
address /* caller */, |
||||
address /* target */, |
||||
bytes4 /* selector */ |
||||
) external view returns (bool immediate, uint32 delay) { |
||||
return (_immediate, _delay); |
||||
} |
||||
|
||||
function _setImmediate(bool immediate) external { |
||||
_immediate = immediate; |
||||
} |
||||
|
||||
function _setDelay(uint32 delay) external { |
||||
_delay = delay; |
||||
} |
||||
} |
||||
|
||||
contract AuthorityNoResponse { |
||||
function canCall(address /* caller */, address /* target */, bytes4 /* selector */) external view {} |
||||
} |
||||
|
||||
contract AuthoritiyObserveIsConsuming { |
||||
event ConsumeScheduledOpCalled(address caller, bytes data, bytes4 isConsuming); |
||||
|
||||
function canCall( |
||||
address /* caller */, |
||||
address /* target */, |
||||
bytes4 /* selector */ |
||||
) external pure returns (bool immediate, uint32 delay) { |
||||
return (false, 1); |
||||
} |
||||
|
||||
function consumeScheduledOp(address caller, bytes memory data) public { |
||||
emit ConsumeScheduledOpCalled(caller, data, IAccessManaged(msg.sender).isConsumingScheduledOp()); |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {ERC20} from "../../token/ERC20/ERC20.sol"; |
||||
|
||||
contract ERC20WithAutoMinerReward is ERC20 { |
||||
constructor() ERC20("Reward", "RWD") { |
||||
_mintMinerReward(); |
||||
} |
||||
|
||||
function _mintMinerReward() internal { |
||||
_mint(block.coinbase, 1000); |
||||
} |
||||
|
||||
function _update(address from, address to, uint256 value) internal virtual override { |
||||
if (!(from == address(0) && to == block.coinbase)) { |
||||
_mintMinerReward(); |
||||
} |
||||
super._update(from, to, value); |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {Ownable} from "../../access/Ownable.sol"; |
||||
|
||||
contract MyContract is Ownable { |
||||
constructor(address initialOwner) Ownable(initialOwner) {} |
||||
|
||||
function normalThing() public { |
||||
// anyone can call this normalThing() |
||||
} |
||||
|
||||
function specialThing() public onlyOwner { |
||||
// only the owner can call specialThing()! |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
||||
|
||||
contract ERC20WithAutoMinerReward is ERC20 { |
||||
constructor() ERC20("Reward", "RWD") { |
||||
_mintMinerReward(); |
||||
} |
||||
|
||||
function _mintMinerReward() internal { |
||||
_mint(block.coinbase, 1000); |
||||
} |
||||
|
||||
function _update(address from, address to, uint256 value) internal virtual override { |
||||
if (!(from == address(0) && to == block.coinbase)) { |
||||
_mintMinerReward(); |
||||
} |
||||
super._update(from, to, value); |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; |
||||
|
||||
contract MyContract is Ownable { |
||||
constructor(address initialOwner) Ownable(initialOwner) {} |
||||
|
||||
function normalThing() public { |
||||
// anyone can call this normalThing() |
||||
} |
||||
|
||||
function specialThing() public onlyOwner { |
||||
// only the owner can call specialThing()! |
||||
} |
||||
} |
@ -0,0 +1,142 @@ |
||||
const { expectEvent, time, expectRevert } = require('@openzeppelin/test-helpers'); |
||||
const { selector } = require('../../helpers/methods'); |
||||
const { expectRevertCustomError } = require('../../helpers/customError'); |
||||
const { |
||||
time: { setNextBlockTimestamp }, |
||||
} = require('@nomicfoundation/hardhat-network-helpers'); |
||||
const { impersonate } = require('../../helpers/account'); |
||||
|
||||
const AccessManaged = artifacts.require('$AccessManagedTarget'); |
||||
const AccessManager = artifacts.require('$AccessManager'); |
||||
|
||||
const AuthoritiyObserveIsConsuming = artifacts.require('$AuthoritiyObserveIsConsuming'); |
||||
|
||||
contract('AccessManaged', function (accounts) { |
||||
const [admin, roleMember, other] = accounts; |
||||
|
||||
beforeEach(async function () { |
||||
this.authority = await AccessManager.new(admin); |
||||
this.managed = await AccessManaged.new(this.authority.address); |
||||
}); |
||||
|
||||
it('sets authority and emits AuthorityUpdated event during construction', async function () { |
||||
await expectEvent.inConstruction(this.managed, 'AuthorityUpdated', { |
||||
authority: this.authority.address, |
||||
}); |
||||
expect(await this.managed.authority()).to.eq(this.authority.address); |
||||
}); |
||||
|
||||
describe('restricted modifier', function () { |
||||
const method = 'fnRestricted()'; |
||||
|
||||
beforeEach(async function () { |
||||
this.selector = selector(method); |
||||
this.role = web3.utils.toBN(42); |
||||
await this.authority.$_setTargetFunctionRole(this.managed.address, this.selector, this.role); |
||||
await this.authority.$_grantRole(this.role, roleMember, 0, 0); |
||||
}); |
||||
|
||||
it('succeeds when role is granted without execution delay', async function () { |
||||
await this.managed.methods[method]({ from: roleMember }); |
||||
}); |
||||
|
||||
it('reverts when role is not granted', async function () { |
||||
await expectRevertCustomError(this.managed.methods[method]({ from: other }), 'AccessManagedUnauthorized', [ |
||||
other, |
||||
]); |
||||
}); |
||||
|
||||
it('panics in short calldata', async function () { |
||||
// We avoid adding the `restricted` modifier to the fallback function because other tests may depend on it
|
||||
// being accessible without restrictions. We check for the internal `_checkCanCall` instead.
|
||||
await expectRevert.unspecified(this.managed.$_checkCanCall(other, '0x1234')); |
||||
}); |
||||
|
||||
describe('when role is granted with execution delay', function () { |
||||
beforeEach(async function () { |
||||
const executionDelay = web3.utils.toBN(911); |
||||
await this.authority.$_grantRole(this.role, roleMember, 0, executionDelay); |
||||
}); |
||||
|
||||
it('reverts if the operation is not scheduled', async function () { |
||||
const calldata = await this.managed.contract.methods[method]().encodeABI(); |
||||
const opId = await this.authority.hashOperation(roleMember, this.managed.address, calldata); |
||||
|
||||
await expectRevertCustomError(this.managed.methods[method]({ from: roleMember }), 'AccessManagerNotScheduled', [ |
||||
opId, |
||||
]); |
||||
}); |
||||
|
||||
it('succeeds if the operation is scheduled', async function () { |
||||
// Arguments
|
||||
const delay = time.duration.hours(12); |
||||
const calldata = await this.managed.contract.methods[method]().encodeABI(); |
||||
|
||||
// Schedule
|
||||
const timestamp = await time.latest(); |
||||
const scheduledAt = timestamp.addn(1); |
||||
const when = scheduledAt.add(delay); |
||||
await setNextBlockTimestamp(scheduledAt); |
||||
await this.authority.schedule(this.managed.address, calldata, when, { |
||||
from: roleMember, |
||||
}); |
||||
|
||||
// Set execution date
|
||||
await setNextBlockTimestamp(when); |
||||
|
||||
// Shouldn't revert
|
||||
await this.managed.methods[method]({ from: roleMember }); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('setAuthority', function () { |
||||
beforeEach(async function () { |
||||
this.newAuthority = await AccessManager.new(admin); |
||||
}); |
||||
|
||||
it('reverts if the caller is not the authority', async function () { |
||||
await expectRevertCustomError(this.managed.setAuthority(other, { from: other }), 'AccessManagedUnauthorized', [ |
||||
other, |
||||
]); |
||||
}); |
||||
|
||||
it('reverts if the new authority is not a valid authority', async function () { |
||||
await impersonate(this.authority.address); |
||||
await expectRevertCustomError( |
||||
this.managed.setAuthority(other, { from: this.authority.address }), |
||||
'AccessManagedInvalidAuthority', |
||||
[other], |
||||
); |
||||
}); |
||||
|
||||
it('sets authority and emits AuthorityUpdated event', async function () { |
||||
await impersonate(this.authority.address); |
||||
const { receipt } = await this.managed.setAuthority(this.newAuthority.address, { from: this.authority.address }); |
||||
await expectEvent(receipt, 'AuthorityUpdated', { |
||||
authority: this.newAuthority.address, |
||||
}); |
||||
expect(await this.managed.authority()).to.eq(this.newAuthority.address); |
||||
}); |
||||
}); |
||||
|
||||
describe('isConsumingScheduledOp', function () { |
||||
beforeEach(async function () { |
||||
this.authority = await AuthoritiyObserveIsConsuming.new(); |
||||
this.managed = await AccessManaged.new(this.authority.address); |
||||
}); |
||||
|
||||
it('returns bytes4(0) when not consuming operation', async function () { |
||||
expect(await this.managed.isConsumingScheduledOp()).to.eq('0x00000000'); |
||||
}); |
||||
|
||||
it('returns isConsumingScheduledOp selector when consuming operation', async function () { |
||||
const receipt = await this.managed.fnRestricted({ from: other }); |
||||
await expectEvent.inTransaction(receipt.tx, this.authority, 'ConsumeScheduledOpCalled', { |
||||
caller: other, |
||||
data: this.managed.contract.methods.fnRestricted().encodeABI(), |
||||
isConsuming: selector('isConsumingScheduledOp()'), |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,711 @@ |
||||
const { time } = require('@openzeppelin/test-helpers'); |
||||
const { |
||||
time: { setNextBlockTimestamp }, |
||||
setStorageAt, |
||||
mine, |
||||
} = require('@nomicfoundation/hardhat-network-helpers'); |
||||
const { impersonate } = require('../../helpers/account'); |
||||
const { expectRevertCustomError } = require('../../helpers/customError'); |
||||
const { EXPIRATION, EXECUTION_ID_STORAGE_SLOT } = require('../../helpers/access-manager'); |
||||
|
||||
// ============ COMMON PATHS ============
|
||||
|
||||
const COMMON_IS_EXECUTING_PATH = { |
||||
executing() { |
||||
it('succeeds', async function () { |
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); |
||||
}); |
||||
}, |
||||
notExecuting() { |
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerUnauthorizedAccount', |
||||
[this.caller, this.role.id], |
||||
); |
||||
}); |
||||
}, |
||||
}; |
||||
|
||||
const COMMON_GET_ACCESS_PATH = { |
||||
requiredRoleIsGranted: { |
||||
roleGrantingIsDelayed: { |
||||
callerHasAnExecutionDelay: { |
||||
beforeGrantDelay() { |
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerUnauthorizedAccount', |
||||
[this.caller, this.role.id], |
||||
); |
||||
}); |
||||
}, |
||||
afterGrantDelay: undefined, // Diverges if there's an operation delay or not
|
||||
}, |
||||
callerHasNoExecutionDelay: { |
||||
beforeGrantDelay() { |
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerUnauthorizedAccount', |
||||
[this.caller, this.role.id], |
||||
); |
||||
}); |
||||
}, |
||||
afterGrantDelay() { |
||||
it('succeeds called directly', async function () { |
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); |
||||
}); |
||||
|
||||
it('succeeds via execute', async function () { |
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); |
||||
}); |
||||
}, |
||||
}, |
||||
}, |
||||
roleGrantingIsNotDelayed: { |
||||
callerHasAnExecutionDelay: undefined, // Diverges if there's an operation to schedule or not
|
||||
callerHasNoExecutionDelay() { |
||||
it('succeeds called directly', async function () { |
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); |
||||
}); |
||||
|
||||
it('succeeds via execute', async function () { |
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); |
||||
}); |
||||
}, |
||||
}, |
||||
}, |
||||
requiredRoleIsNotGranted() { |
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerUnauthorizedAccount', |
||||
[this.caller, this.role.id], |
||||
); |
||||
}); |
||||
}, |
||||
}; |
||||
|
||||
const COMMON_SCHEDULABLE_PATH = { |
||||
scheduled: { |
||||
before() { |
||||
it('reverts as AccessManagerNotReady', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerNotReady', |
||||
[this.operationId], |
||||
); |
||||
}); |
||||
}, |
||||
after() { |
||||
it('succeeds called directly', async function () { |
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); |
||||
}); |
||||
|
||||
it('succeeds via execute', async function () { |
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); |
||||
}); |
||||
}, |
||||
expired() { |
||||
it('reverts as AccessManagerExpired', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerExpired', |
||||
[this.operationId], |
||||
); |
||||
}); |
||||
}, |
||||
}, |
||||
notScheduled() { |
||||
it('reverts as AccessManagerNotScheduled', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerNotScheduled', |
||||
[this.operationId], |
||||
); |
||||
}); |
||||
}, |
||||
}; |
||||
|
||||
const COMMON_SCHEDULABLE_PATH_IF_ZERO_DELAY = { |
||||
scheduled: { |
||||
before() { |
||||
it.skip('is not reachable without a delay'); |
||||
}, |
||||
after() { |
||||
it.skip('is not reachable without a delay'); |
||||
}, |
||||
expired() { |
||||
it.skip('is not reachable without a delay'); |
||||
}, |
||||
}, |
||||
notScheduled() { |
||||
it('succeeds', async function () { |
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); |
||||
}); |
||||
}, |
||||
}; |
||||
|
||||
// ============ MODE HELPERS ============
|
||||
|
||||
/** |
||||
* @requires this.{manager,target} |
||||
*/ |
||||
function shouldBehaveLikeClosable({ closed, open }) { |
||||
describe('when the manager is closed', function () { |
||||
beforeEach('close', async function () { |
||||
await this.manager.$_setTargetClosed(this.target.address, true); |
||||
}); |
||||
|
||||
closed(); |
||||
}); |
||||
|
||||
describe('when the manager is open', function () { |
||||
beforeEach('open', async function () { |
||||
await this.manager.$_setTargetClosed(this.target.address, false); |
||||
}); |
||||
|
||||
open(); |
||||
}); |
||||
} |
||||
|
||||
// ============ DELAY HELPERS ============
|
||||
|
||||
/** |
||||
* @requires this.{delay} |
||||
*/ |
||||
function shouldBehaveLikeDelay(type, { before, after }) { |
||||
beforeEach('define timestamp when delay takes effect', async function () { |
||||
const timestamp = await time.latest(); |
||||
this.delayEffect = timestamp.add(this.delay); |
||||
}); |
||||
|
||||
describe(`when ${type} delay has not taken effect yet`, function () { |
||||
beforeEach(`set next block timestamp before ${type} takes effect`, async function () { |
||||
await setNextBlockTimestamp(this.delayEffect.subn(1)); |
||||
}); |
||||
|
||||
before(); |
||||
}); |
||||
|
||||
describe(`when ${type} delay has taken effect`, function () { |
||||
beforeEach(`set next block timestamp when ${type} takes effect`, async function () { |
||||
await setNextBlockTimestamp(this.delayEffect); |
||||
}); |
||||
|
||||
after(); |
||||
}); |
||||
} |
||||
|
||||
// ============ OPERATION HELPERS ============
|
||||
|
||||
/** |
||||
* @requires this.{manager,scheduleIn,caller,target,calldata} |
||||
*/ |
||||
function shouldBehaveLikeSchedulableOperation({ scheduled: { before, after, expired }, notScheduled }) { |
||||
describe('when operation is scheduled', function () { |
||||
beforeEach('schedule operation', async function () { |
||||
await impersonate(this.caller); // May be a contract
|
||||
const { operationId } = await scheduleOperation(this.manager, { |
||||
caller: this.caller, |
||||
target: this.target.address, |
||||
calldata: this.calldata, |
||||
delay: this.scheduleIn, |
||||
}); |
||||
this.operationId = operationId; |
||||
}); |
||||
|
||||
describe('when operation is not ready for execution', function () { |
||||
beforeEach('set next block time before operation is ready', async function () { |
||||
this.scheduledAt = await time.latest(); |
||||
const schedule = await this.manager.getSchedule(this.operationId); |
||||
await setNextBlockTimestamp(schedule.subn(1)); |
||||
}); |
||||
|
||||
before(); |
||||
}); |
||||
|
||||
describe('when operation is ready for execution', function () { |
||||
beforeEach('set next block time when operation is ready for execution', async function () { |
||||
this.scheduledAt = await time.latest(); |
||||
const schedule = await this.manager.getSchedule(this.operationId); |
||||
await setNextBlockTimestamp(schedule); |
||||
}); |
||||
|
||||
after(); |
||||
}); |
||||
|
||||
describe('when operation has expired', function () { |
||||
beforeEach('set next block time when operation expired', async function () { |
||||
this.scheduledAt = await time.latest(); |
||||
const schedule = await this.manager.getSchedule(this.operationId); |
||||
await setNextBlockTimestamp(schedule.add(EXPIRATION)); |
||||
}); |
||||
|
||||
expired(); |
||||
}); |
||||
}); |
||||
|
||||
describe('when operation is not scheduled', function () { |
||||
beforeEach('set expected operationId', async function () { |
||||
this.operationId = await this.manager.hashOperation(this.caller, this.target.address, this.calldata); |
||||
|
||||
// Assert operation is not scheduled
|
||||
expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal(web3.utils.toBN(0)); |
||||
}); |
||||
|
||||
notScheduled(); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @requires this.{manager,roles,target,calldata} |
||||
*/ |
||||
function shouldBehaveLikeARestrictedOperation({ callerIsNotTheManager, callerIsTheManager }) { |
||||
describe('when the call comes from the manager (msg.sender == manager)', function () { |
||||
beforeEach('define caller as manager', async function () { |
||||
this.caller = this.manager.address; |
||||
await impersonate(this.caller); |
||||
}); |
||||
|
||||
shouldBehaveLikeCanCallExecuting(callerIsTheManager); |
||||
}); |
||||
|
||||
describe('when the call does not come from the manager (msg.sender != manager)', function () { |
||||
beforeEach('define non manager caller', function () { |
||||
this.caller = this.roles.SOME.members[0]; |
||||
}); |
||||
|
||||
callerIsNotTheManager(); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @requires this.{manager,roles,executionDelay,operationDelay,target} |
||||
*/ |
||||
function shouldBehaveLikeDelayedOperation() { |
||||
describe('with operation delay', function () { |
||||
describe('when operation delay is greater than execution delay', function () { |
||||
beforeEach('set operation delay', async function () { |
||||
this.operationDelay = this.executionDelay.add(time.duration.hours(1)); |
||||
await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay); |
||||
this.scheduleIn = this.operationDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}); |
||||
|
||||
describe('when operation delay is shorter than execution delay', function () { |
||||
beforeEach('set operation delay', async function () { |
||||
this.operationDelay = this.executionDelay.sub(time.duration.hours(1)); |
||||
await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay); |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}); |
||||
}); |
||||
|
||||
describe('without operation delay', function () { |
||||
beforeEach('set operation delay', async function () { |
||||
this.operationDelay = web3.utils.toBN(0); |
||||
await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay); |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}); |
||||
} |
||||
|
||||
// ============ METHOD HELPERS ============
|
||||
|
||||
/** |
||||
* @requires this.{manager,roles,role,target,calldata} |
||||
*/ |
||||
function shouldBehaveLikeCanCall({ |
||||
closed, |
||||
open: { |
||||
callerIsTheManager, |
||||
callerIsNotTheManager: { publicRoleIsRequired, specificRoleIsRequired }, |
||||
}, |
||||
}) { |
||||
shouldBehaveLikeClosable({ |
||||
closed, |
||||
open() { |
||||
shouldBehaveLikeARestrictedOperation({ |
||||
callerIsTheManager, |
||||
callerIsNotTheManager() { |
||||
shouldBehaveLikeHasRole({ |
||||
publicRoleIsRequired, |
||||
specificRoleIsRequired, |
||||
}); |
||||
}, |
||||
}); |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @requires this.{target,calldata} |
||||
*/ |
||||
function shouldBehaveLikeCanCallExecuting({ executing, notExecuting }) { |
||||
describe('when _executionId is in storage for target and selector', function () { |
||||
beforeEach('set _executionId flag from calldata and target', async function () { |
||||
const executionId = await web3.utils.keccak256( |
||||
web3.eth.abi.encodeParameters(['address', 'bytes4'], [this.target.address, this.calldata.substring(0, 10)]), |
||||
); |
||||
await setStorageAt(this.manager.address, EXECUTION_ID_STORAGE_SLOT, executionId); |
||||
}); |
||||
|
||||
executing(); |
||||
}); |
||||
|
||||
describe('when _executionId does not match target and selector', notExecuting); |
||||
} |
||||
|
||||
/** |
||||
* @requires this.{target,calldata,roles,role} |
||||
*/ |
||||
function shouldBehaveLikeHasRole({ publicRoleIsRequired, specificRoleIsRequired }) { |
||||
describe('when the function requires the caller to be granted with the PUBLIC_ROLE', function () { |
||||
beforeEach('set target function role as PUBLIC_ROLE', async function () { |
||||
this.role = this.roles.PUBLIC; |
||||
await this.manager.$_setTargetFunctionRole(this.target.address, this.calldata.substring(0, 10), this.role.id, { |
||||
from: this.roles.ADMIN.members[0], |
||||
}); |
||||
}); |
||||
|
||||
publicRoleIsRequired(); |
||||
}); |
||||
|
||||
describe('when the function requires the caller to be granted with a role other than PUBLIC_ROLE', function () { |
||||
beforeEach('set target function role as PUBLIC_ROLE', async function () { |
||||
await this.manager.$_setTargetFunctionRole(this.target.address, this.calldata.substring(0, 10), this.role.id, { |
||||
from: this.roles.ADMIN.members[0], |
||||
}); |
||||
}); |
||||
|
||||
shouldBehaveLikeGetAccess(specificRoleIsRequired); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @requires this.{manager,role,caller} |
||||
*/ |
||||
function shouldBehaveLikeGetAccess({ |
||||
requiredRoleIsGranted: { |
||||
roleGrantingIsDelayed: { |
||||
// Because both grant and execution delay are set within the same $_grantRole call
|
||||
// it's not possible to create a set of tests that diverge between grant and execution delay.
|
||||
// Therefore, the shouldBehaveLikeDelay arguments are renamed for clarity:
|
||||
// before => beforeGrantDelay
|
||||
// after => afterGrantDelay
|
||||
callerHasAnExecutionDelay: { beforeGrantDelay: case1, afterGrantDelay: case2 }, |
||||
callerHasNoExecutionDelay: { beforeGrantDelay: case3, afterGrantDelay: case4 }, |
||||
}, |
||||
roleGrantingIsNotDelayed: { callerHasAnExecutionDelay: case5, callerHasNoExecutionDelay: case6 }, |
||||
}, |
||||
requiredRoleIsNotGranted, |
||||
}) { |
||||
describe('when the required role is granted to the caller', function () { |
||||
describe('when role granting is delayed', function () { |
||||
beforeEach('define delay', function () { |
||||
this.grantDelay = time.duration.minutes(3); |
||||
this.delay = this.grantDelay; // For shouldBehaveLikeDelay
|
||||
}); |
||||
|
||||
describe('when caller has an execution delay', function () { |
||||
beforeEach('set role and delay', async function () { |
||||
this.executionDelay = time.duration.hours(10); |
||||
this.delay = this.grantDelay; |
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay); |
||||
}); |
||||
|
||||
shouldBehaveLikeDelay('grant', { before: case1, after: case2 }); |
||||
}); |
||||
|
||||
describe('when caller has no execution delay', function () { |
||||
beforeEach('set role and delay', async function () { |
||||
this.executionDelay = web3.utils.toBN(0); |
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay); |
||||
}); |
||||
|
||||
shouldBehaveLikeDelay('grant', { before: case3, after: case4 }); |
||||
}); |
||||
}); |
||||
|
||||
describe('when role granting is not delayed', function () { |
||||
beforeEach('define delay', function () { |
||||
this.grantDelay = web3.utils.toBN(0); |
||||
}); |
||||
|
||||
describe('when caller has an execution delay', function () { |
||||
beforeEach('set role and delay', async function () { |
||||
this.executionDelay = time.duration.hours(10); |
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay); |
||||
}); |
||||
|
||||
case5(); |
||||
}); |
||||
|
||||
describe('when caller has no execution delay', function () { |
||||
beforeEach('set role and delay', async function () { |
||||
this.executionDelay = web3.utils.toBN(0); |
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay); |
||||
}); |
||||
|
||||
case6(); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when role is not granted', function () { |
||||
// Because this helper can be composed with other helpers, it's possible
|
||||
// that role has been set already by another helper.
|
||||
// Although this is highly unlikely, we check for it here to avoid false positives.
|
||||
beforeEach('assert role is unset', async function () { |
||||
const { since } = await this.manager.getAccess(this.role.id, this.caller); |
||||
expect(since).to.be.bignumber.equal(web3.utils.toBN(0)); |
||||
}); |
||||
|
||||
requiredRoleIsNotGranted(); |
||||
}); |
||||
} |
||||
|
||||
// ============ ADMIN OPERATION HELPERS ============
|
||||
|
||||
/** |
||||
* @requires this.{manager,roles,calldata,role} |
||||
*/ |
||||
function shouldBehaveLikeDelayedAdminOperation() { |
||||
const getAccessPath = COMMON_GET_ACCESS_PATH; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { |
||||
beforeEach('consume previously set grant delay', async function () { |
||||
// Consume previously set delay
|
||||
await mine(); |
||||
}); |
||||
shouldBehaveLikeDelayedOperation(); |
||||
}; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { |
||||
beforeEach('set execution delay', async function () { |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeDelayedOperation
|
||||
}); |
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}; |
||||
|
||||
beforeEach('set target as manager', function () { |
||||
this.target = this.manager; |
||||
}); |
||||
|
||||
shouldBehaveLikeARestrictedOperation({ |
||||
callerIsTheManager: COMMON_IS_EXECUTING_PATH, |
||||
callerIsNotTheManager() { |
||||
shouldBehaveLikeHasRole({ |
||||
publicRoleIsRequired() { |
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerUnauthorizedAccount', |
||||
[ |
||||
this.caller, |
||||
this.roles.ADMIN.id, // Although PUBLIC is required, target function role doesn't apply to admin ops
|
||||
], |
||||
); |
||||
}); |
||||
}, |
||||
specificRoleIsRequired: getAccessPath, |
||||
}); |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @requires this.{manager,roles,calldata,role} |
||||
*/ |
||||
function shouldBehaveLikeNotDelayedAdminOperation() { |
||||
const getAccessPath = COMMON_GET_ACCESS_PATH; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { |
||||
beforeEach('set execution delay', async function () { |
||||
await mine(); |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { |
||||
beforeEach('set execution delay', async function () { |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}; |
||||
|
||||
beforeEach('set target as manager', function () { |
||||
this.target = this.manager; |
||||
}); |
||||
|
||||
shouldBehaveLikeARestrictedOperation({ |
||||
callerIsTheManager: COMMON_IS_EXECUTING_PATH, |
||||
callerIsNotTheManager() { |
||||
shouldBehaveLikeHasRole({ |
||||
publicRoleIsRequired() { |
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerUnauthorizedAccount', |
||||
[this.caller, this.roles.ADMIN.id], // Although PUBLIC_ROLE is required, admin ops are not subject to target function roles
|
||||
); |
||||
}); |
||||
}, |
||||
specificRoleIsRequired: getAccessPath, |
||||
}); |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @requires this.{manager,roles,calldata,role} |
||||
*/ |
||||
function shouldBehaveLikeRoleAdminOperation(roleAdmin) { |
||||
const getAccessPath = COMMON_GET_ACCESS_PATH; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { |
||||
beforeEach('set operation delay', async function () { |
||||
await mine(); |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { |
||||
beforeEach('set execution delay', async function () { |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}; |
||||
|
||||
beforeEach('set target as manager', function () { |
||||
this.target = this.manager; |
||||
}); |
||||
|
||||
shouldBehaveLikeARestrictedOperation({ |
||||
callerIsTheManager: COMMON_IS_EXECUTING_PATH, |
||||
callerIsNotTheManager() { |
||||
shouldBehaveLikeHasRole({ |
||||
publicRoleIsRequired() { |
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagerUnauthorizedAccount', |
||||
[this.caller, roleAdmin], // Role admin ops require the role's admin
|
||||
); |
||||
}); |
||||
}, |
||||
specificRoleIsRequired: getAccessPath, |
||||
}); |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
// ============ RESTRICTED OPERATION HELPERS ============
|
||||
|
||||
/** |
||||
* @requires this.{manager,roles,calldata,role} |
||||
*/ |
||||
function shouldBehaveLikeAManagedRestrictedOperation() { |
||||
function revertUnauthorized() { |
||||
it('reverts as AccessManagedUnauthorized', async function () { |
||||
await expectRevertCustomError( |
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), |
||||
'AccessManagedUnauthorized', |
||||
[this.caller], |
||||
); |
||||
}); |
||||
} |
||||
|
||||
const getAccessPath = COMMON_GET_ACCESS_PATH; |
||||
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.beforeGrantDelay = |
||||
revertUnauthorized; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasNoExecutionDelay.beforeGrantDelay = |
||||
revertUnauthorized; |
||||
getAccessPath.requiredRoleIsNotGranted = revertUnauthorized; |
||||
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { |
||||
beforeEach('consume previously set grant delay', async function () { |
||||
// Consume previously set delay
|
||||
await mine(); |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}; |
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { |
||||
beforeEach('consume previously set grant delay', async function () { |
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
}); |
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); |
||||
}; |
||||
|
||||
const isExecutingPath = COMMON_IS_EXECUTING_PATH; |
||||
isExecutingPath.notExecuting = revertUnauthorized; |
||||
|
||||
shouldBehaveLikeCanCall({ |
||||
closed: revertUnauthorized, |
||||
open: { |
||||
callerIsTheManager: isExecutingPath, |
||||
callerIsNotTheManager: { |
||||
publicRoleIsRequired() { |
||||
it('succeeds called directly', async function () { |
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); |
||||
}); |
||||
|
||||
it('succeeds via execute', async function () { |
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); |
||||
}); |
||||
}, |
||||
specificRoleIsRequired: getAccessPath, |
||||
}, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
// ============ HELPERS ============
|
||||
|
||||
/** |
||||
* @requires this.{manager, caller, target, calldata} |
||||
*/ |
||||
async function scheduleOperation(manager, { caller, target, calldata, delay }) { |
||||
const timestamp = await time.latest(); |
||||
const scheduledAt = timestamp.addn(1); |
||||
await setNextBlockTimestamp(scheduledAt); // Fix next block timestamp for predictability
|
||||
const { receipt } = await manager.schedule(target, calldata, scheduledAt.add(delay), { |
||||
from: caller, |
||||
}); |
||||
|
||||
return { |
||||
receipt, |
||||
scheduledAt, |
||||
operationId: await manager.hashOperation(caller, target, calldata), |
||||
}; |
||||
} |
||||
|
||||
module.exports = { |
||||
// COMMON PATHS
|
||||
COMMON_SCHEDULABLE_PATH, |
||||
COMMON_SCHEDULABLE_PATH_IF_ZERO_DELAY, |
||||
// MODE HELPERS
|
||||
shouldBehaveLikeClosable, |
||||
// DELAY HELPERS
|
||||
shouldBehaveLikeDelay, |
||||
// OPERATION HELPERS
|
||||
shouldBehaveLikeSchedulableOperation, |
||||
// METHOD HELPERS
|
||||
shouldBehaveLikeCanCall, |
||||
shouldBehaveLikeGetAccess, |
||||
shouldBehaveLikeHasRole, |
||||
// ADMIN OPERATION HELPERS
|
||||
shouldBehaveLikeDelayedAdminOperation, |
||||
shouldBehaveLikeNotDelayedAdminOperation, |
||||
shouldBehaveLikeRoleAdminOperation, |
||||
// RESTRICTED OPERATION HELPERS
|
||||
shouldBehaveLikeAManagedRestrictedOperation, |
||||
// HELPERS
|
||||
scheduleOperation, |
||||
}; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,91 @@ |
||||
require('@openzeppelin/test-helpers'); |
||||
|
||||
const AuthorityUtils = artifacts.require('$AuthorityUtils'); |
||||
const NotAuthorityMock = artifacts.require('NotAuthorityMock'); |
||||
const AuthorityNoDelayMock = artifacts.require('AuthorityNoDelayMock'); |
||||
const AuthorityDelayMock = artifacts.require('AuthorityDelayMock'); |
||||
const AuthorityNoResponse = artifacts.require('AuthorityNoResponse'); |
||||
|
||||
contract('AuthorityUtils', function (accounts) { |
||||
const [user, other] = accounts; |
||||
|
||||
beforeEach(async function () { |
||||
this.mock = await AuthorityUtils.new(); |
||||
}); |
||||
|
||||
describe('canCallWithDelay', function () { |
||||
describe('when authority does not have a canCall function', function () { |
||||
beforeEach(async function () { |
||||
this.authority = await NotAuthorityMock.new(); |
||||
}); |
||||
|
||||
it('returns (immediate = 0, delay = 0)', async function () { |
||||
const { immediate, delay } = await this.mock.$canCallWithDelay( |
||||
this.authority.address, |
||||
user, |
||||
other, |
||||
'0x12345678', |
||||
); |
||||
expect(immediate).to.equal(false); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
}); |
||||
}); |
||||
|
||||
describe('when authority has no delay', function () { |
||||
beforeEach(async function () { |
||||
this.authority = await AuthorityNoDelayMock.new(); |
||||
this.immediate = true; |
||||
await this.authority._setImmediate(this.immediate); |
||||
}); |
||||
|
||||
it('returns (immediate, delay = 0)', async function () { |
||||
const { immediate, delay } = await this.mock.$canCallWithDelay( |
||||
this.authority.address, |
||||
user, |
||||
other, |
||||
'0x12345678', |
||||
); |
||||
expect(immediate).to.equal(this.immediate); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
}); |
||||
}); |
||||
|
||||
describe('when authority replies with a delay', function () { |
||||
beforeEach(async function () { |
||||
this.authority = await AuthorityDelayMock.new(); |
||||
this.immediate = true; |
||||
this.delay = web3.utils.toBN(42); |
||||
await this.authority._setImmediate(this.immediate); |
||||
await this.authority._setDelay(this.delay); |
||||
}); |
||||
|
||||
it('returns (immediate, delay)', async function () { |
||||
const { immediate, delay } = await this.mock.$canCallWithDelay( |
||||
this.authority.address, |
||||
user, |
||||
other, |
||||
'0x12345678', |
||||
); |
||||
expect(immediate).to.equal(this.immediate); |
||||
expect(delay).to.be.bignumber.equal(this.delay); |
||||
}); |
||||
}); |
||||
|
||||
describe('when authority replies with empty data', function () { |
||||
beforeEach(async function () { |
||||
this.authority = await AuthorityNoResponse.new(); |
||||
}); |
||||
|
||||
it('returns (immediate = 0, delay = 0)', async function () { |
||||
const { immediate, delay } = await this.mock.$canCallWithDelay( |
||||
this.authority.address, |
||||
user, |
||||
other, |
||||
'0x12345678', |
||||
); |
||||
expect(immediate).to.equal(false); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,69 @@ |
||||
const { time } = require('@openzeppelin/test-helpers'); |
||||
const { MAX_UINT64 } = require('./constants'); |
||||
const { artifacts } = require('hardhat'); |
||||
|
||||
function buildBaseRoles() { |
||||
const roles = { |
||||
ADMIN: { |
||||
id: web3.utils.toBN(0), |
||||
}, |
||||
SOME_ADMIN: { |
||||
id: web3.utils.toBN(17), |
||||
}, |
||||
SOME_GUARDIAN: { |
||||
id: web3.utils.toBN(35), |
||||
}, |
||||
SOME: { |
||||
id: web3.utils.toBN(42), |
||||
}, |
||||
PUBLIC: { |
||||
id: MAX_UINT64, |
||||
}, |
||||
}; |
||||
|
||||
// Names
|
||||
Object.entries(roles).forEach(([name, role]) => (role.name = name)); |
||||
|
||||
// Defaults
|
||||
for (const role of Object.keys(roles)) { |
||||
roles[role].admin = roles.ADMIN; |
||||
roles[role].guardian = roles.ADMIN; |
||||
} |
||||
|
||||
// Admins
|
||||
roles.SOME.admin = roles.SOME_ADMIN; |
||||
|
||||
// Guardians
|
||||
roles.SOME.guardian = roles.SOME_GUARDIAN; |
||||
|
||||
return roles; |
||||
} |
||||
|
||||
const formatAccess = access => [access[0], access[1].toString()]; |
||||
|
||||
const MINSETBACK = time.duration.days(5); |
||||
const EXPIRATION = time.duration.weeks(1); |
||||
|
||||
let EXECUTION_ID_STORAGE_SLOT = 3n; |
||||
let CONSUMING_SCHEDULE_STORAGE_SLOT = 0n; |
||||
try { |
||||
// Try to get the artifact paths, will throw if it doesn't exist
|
||||
artifacts._getArtifactPathSync('AccessManagerUpgradeable'); |
||||
artifacts._getArtifactPathSync('AccessManagedUpgradeable'); |
||||
|
||||
// ERC-7201 namespace location for AccessManager
|
||||
EXECUTION_ID_STORAGE_SLOT += 0x40c6c8c28789853c7efd823ab20824bbd71718a8a5915e855f6f288c9a26ad00n; |
||||
// ERC-7201 namespace location for AccessManaged
|
||||
CONSUMING_SCHEDULE_STORAGE_SLOT += 0xf3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a00n; |
||||
} catch (_) { |
||||
// eslint-disable-next-line no-empty
|
||||
} |
||||
|
||||
module.exports = { |
||||
buildBaseRoles, |
||||
formatAccess, |
||||
MINSETBACK, |
||||
EXPIRATION, |
||||
EXECUTION_ID_STORAGE_SLOT, |
||||
CONSUMING_SCHEDULE_STORAGE_SLOT, |
||||
}; |
Loading…
Reference in new issue