const { expectEvent, expectRevert, time: { duration }, } = require('@openzeppelin/test-helpers'); const { AccessMode } = require('../../helpers/enums'); const AccessManager = artifacts.require('AccessManager'); const AccessManagerAdapter = artifacts.require('AccessManagerAdapter'); const AccessManaged = artifacts.require('$AccessManagedMock'); const Ownable = artifacts.require('$Ownable'); const AccessControl = artifacts.require('$AccessControl'); const groupUtils = { mask: group => 1n << BigInt(group), decodeBitmap: hexBitmap => { const m = BigInt(hexBitmap); const allGroups = new Array(256).fill().map((_, i) => i.toString()); return allGroups.filter(i => (m & groupUtils.mask(i)) !== 0n); }, role: group => web3.utils.asciiToHex('group:').padEnd(64, '0') + group.toString(16).padStart(2, '0'), }; const PUBLIC_GROUP = '255'; contract('AccessManager', function (accounts) { const [admin, nonAdmin, user1, user2, otherAuthority] = accounts; beforeEach('deploy', async function () { this.delay = duration.days(1); this.manager = await AccessManager.new(this.delay, admin); }); it('configures default admin rules', async function () { expect(await this.manager.defaultAdmin()).to.equal(admin); expect(await this.manager.defaultAdminDelay()).to.be.bignumber.equal(this.delay); }); describe('groups', function () { const group = '0'; const name = 'dao'; const otherGroup = '1'; const otherName = 'council'; describe('public group', function () { it('is created automatically', async function () { await expectEvent.inConstruction(this.manager, 'GroupUpdated', { group: PUBLIC_GROUP, name: 'public', }); }); it('includes all users automatically', async function () { const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1)); expect(groups).to.include(PUBLIC_GROUP); }); }); describe('creating', function () { it('admin can create groups', async function () { const created = await this.manager.createGroup(group, name, { from: admin }); expectEvent(created, 'GroupUpdated', { group, name }); expect(await this.manager.hasGroup(group)).to.equal(true); expect(await this.manager.hasGroup(otherGroup)).to.equal(false); }); it('non-admin cannot create groups', async function () { await expectRevert(this.manager.createGroup(group, name, { from: nonAdmin }), 'missing role'); }); it('cannot recreate a group', async function () { await this.manager.createGroup(group, name, { from: admin }); await expectRevert(this.manager.createGroup(group, name, { from: admin }), 'AccessManager: existing group'); }); }); describe('updating', function () { beforeEach('create group', async function () { await this.manager.createGroup(group, name, { from: admin }); }); it('admin can update group', async function () { const updated = await this.manager.updateGroupName(group, otherName, { from: admin }); expectEvent(updated, 'GroupUpdated', { group, name: otherName }); }); it('non-admin cannot update group', async function () { await expectRevert(this.manager.updateGroupName(group, name, { from: nonAdmin }), 'missing role'); }); it('cannot update built in group', async function () { await expectRevert( this.manager.updateGroupName(PUBLIC_GROUP, name, { from: admin }), 'AccessManager: built-in group', ); }); it('cannot update nonexistent group', async function () { await expectRevert( this.manager.updateGroupName(otherGroup, name, { from: admin }), 'AccessManager: unknown group', ); }); }); describe('granting', function () { beforeEach('create group', async function () { await this.manager.createGroup(group, name, { from: admin }); }); it('admin can grant group', async function () { const granted = await this.manager.grantGroup(group, user1, { from: admin }); expectEvent(granted, 'RoleGranted', { account: user1, role: groupUtils.role(group) }); const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1)); expect(groups).to.include(group); }); it('non-admin cannot grant group', async function () { await expectRevert(this.manager.grantGroup(group, user1, { from: nonAdmin }), 'missing role'); }); it('cannot grant nonexistent group', async function () { await expectRevert(this.manager.grantGroup(otherGroup, user1, { from: admin }), 'AccessManager: unknown group'); }); }); describe('revoking & renouncing', function () { beforeEach('create and grant group', async function () { await this.manager.createGroup(group, name, { from: admin }); await this.manager.grantGroup(group, user1, { from: admin }); }); it('admin can revoke group', async function () { await this.manager.revokeGroup(group, user1, { from: admin }); const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1)); expect(groups).to.not.include(group); }); it('non-admin cannot revoke group', async function () { await expectRevert(this.manager.revokeGroup(group, user1, { from: nonAdmin }), 'missing role'); }); it('user can renounce group', async function () { await this.manager.renounceGroup(group, user1, { from: user1 }); const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1)); expect(groups).to.not.include(group); }); it(`user cannot renounce other user's groups`, async function () { await expectRevert( this.manager.renounceGroup(group, user1, { from: user2 }), 'can only renounce roles for self', ); await expectRevert( this.manager.renounceGroup(group, user2, { from: user1 }), 'can only renounce roles for self', ); }); it('cannot revoke public group', async function () { await expectRevert( this.manager.revokeGroup(PUBLIC_GROUP, user1, { from: admin }), 'AccessManager: irrevocable group', ); }); it('cannot revoke nonexistent group', async function () { await expectRevert( this.manager.revokeGroup(otherGroup, user1, { from: admin }), 'AccessManager: unknown group', ); await expectRevert( this.manager.renounceGroup(otherGroup, user1, { from: user1 }), 'AccessManager: unknown group', ); }); }); describe('querying', function () { it('returns expected groups', async function () { const getGroups = () => this.manager.getUserGroups(user1); // only public group initially expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000000000'); await this.manager.createGroup('0', '0', { from: admin }); await this.manager.grantGroup('0', user1, { from: admin }); expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000000001'); await this.manager.createGroup('1', '1', { from: admin }); await this.manager.grantGroup('1', user1, { from: admin }); expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000000003'); await this.manager.createGroup('16', '16', { from: admin }); await this.manager.grantGroup('16', user1, { from: admin }); expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000010003'); }); }); }); describe('allowing', function () { const group = '1'; const groupMember = user1; const selector = web3.eth.abi.encodeFunctionSignature('restrictedFunction()'); const otherSelector = web3.eth.abi.encodeFunctionSignature('otherRestrictedFunction()'); beforeEach('deploying managed contract', async function () { await this.manager.createGroup(group, '', { from: admin }); await this.manager.grantGroup(group, groupMember, { from: admin }); this.managed = await AccessManaged.new(this.manager.address); }); it('non-admin cannot change allowed groups', async function () { await expectRevert( this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, true, { from: nonAdmin }), 'missing role', ); }); it('single selector', async function () { const receipt = await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, true, { from: admin, }); expectEvent(receipt, 'GroupAllowed', { target: this.managed.address, selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4 group, allowed: true, }); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]); const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector); expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([]); const restricted = await this.managed.restrictedFunction({ from: groupMember }); expectEvent(restricted, 'RestrictedRan'); await expectRevert( this.managed.otherRestrictedFunction({ from: groupMember }), 'AccessManaged: authority rejected', ); }); it('multiple selectors', async function () { const receipt = await this.manager.setFunctionAllowedGroup( this.managed.address, [selector, otherSelector], group, true, { from: admin }, ); expectEvent(receipt, 'GroupAllowed', { target: this.managed.address, selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4 group, allowed: true, }); expectEvent(receipt, 'GroupAllowed', { target: this.managed.address, selector: otherSelector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4 group, allowed: true, }); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]); const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector); expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([group]); const restricted = await this.managed.restrictedFunction({ from: groupMember }); expectEvent(restricted, 'RestrictedRan'); await this.managed.otherRestrictedFunction({ from: groupMember }); expectEvent(restricted, 'RestrictedRan'); }); it('works on open target', async function () { await this.manager.setContractModeOpen(this.managed.address, { from: admin }); await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin }); }); it('works on closed target', async function () { await this.manager.setContractModeClosed(this.managed.address, { from: admin }); await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin }); }); }); describe('disallowing', function () { const group = '1'; const groupMember = user1; const selector = web3.eth.abi.encodeFunctionSignature('restrictedFunction()'); const otherSelector = web3.eth.abi.encodeFunctionSignature('otherRestrictedFunction()'); beforeEach('deploying managed contract', async function () { await this.manager.createGroup(group, '', { from: admin }); await this.manager.grantGroup(group, groupMember, { from: admin }); this.managed = await AccessManaged.new(this.manager.address); await this.manager.setFunctionAllowedGroup(this.managed.address, [selector, otherSelector], group, true, { from: admin, }); }); it('non-admin cannot change disallowed groups', async function () { await expectRevert( this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: nonAdmin }), 'missing role', ); }); it('single selector', async function () { const receipt = await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin, }); expectEvent(receipt, 'GroupAllowed', { target: this.managed.address, selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4, group, allowed: false, }); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([]); const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector); expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([group]); await expectRevert(this.managed.restrictedFunction({ from: groupMember }), 'AccessManaged: authority rejected'); const otherRestricted = await this.managed.otherRestrictedFunction({ from: groupMember }); expectEvent(otherRestricted, 'RestrictedRan'); }); it('multiple selectors', async function () { const receipt = await this.manager.setFunctionAllowedGroup( this.managed.address, [selector, otherSelector], group, false, { from: admin }, ); expectEvent(receipt, 'GroupAllowed', { target: this.managed.address, selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4 group, allowed: false, }); expectEvent(receipt, 'GroupAllowed', { target: this.managed.address, selector: otherSelector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4 group, allowed: false, }); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([]); const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector); expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([]); await expectRevert(this.managed.restrictedFunction({ from: groupMember }), 'AccessManaged: authority rejected'); await expectRevert( this.managed.otherRestrictedFunction({ from: groupMember }), 'AccessManaged: authority rejected', ); }); it('works on open target', async function () { await this.manager.setContractModeOpen(this.managed.address, { from: admin }); await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin }); }); it('works on closed target', async function () { await this.manager.setContractModeClosed(this.managed.address, { from: admin }); await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin }); }); }); describe('modes', function () { const group = '1'; const selector = web3.eth.abi.encodeFunctionSignature('restrictedFunction()'); beforeEach('deploying managed contract', async function () { this.managed = await AccessManaged.new(this.manager.address); await this.manager.createGroup('1', 'a group', { from: admin }); await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, true, { from: admin }); }); it('custom mode is default', async function () { expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Custom); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]); }); it('open mode', async function () { const receipt = await this.manager.setContractModeOpen(this.managed.address, { from: admin }); expectEvent(receipt, 'AccessModeUpdated', { target: this.managed.address, mode: AccessMode.Open, }); expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Open); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([PUBLIC_GROUP]); }); it('closed mode', async function () { const receipt = await this.manager.setContractModeClosed(this.managed.address, { from: admin }); expectEvent(receipt, 'AccessModeUpdated', { target: this.managed.address, mode: AccessMode.Closed, }); expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Closed); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([]); }); it('mode cycle', async function () { await this.manager.setContractModeOpen(this.managed.address, { from: admin }); await this.manager.setContractModeClosed(this.managed.address, { from: admin }); await this.manager.setContractModeCustom(this.managed.address, { from: admin }); expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Custom); const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector); expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]); }); it('non-admin cannot change mode', async function () { await expectRevert(this.manager.setContractModeCustom(this.managed.address), 'missing role'); await expectRevert(this.manager.setContractModeOpen(this.managed.address), 'missing role'); await expectRevert(this.manager.setContractModeClosed(this.managed.address), 'missing role'); }); }); describe('transfering authority', function () { beforeEach('deploying managed contract', async function () { this.managed = await AccessManaged.new(this.manager.address); }); it('admin can transfer authority', async function () { await this.manager.transferContractAuthority(this.managed.address, otherAuthority, { from: admin }); expect(await this.managed.authority()).to.equal(otherAuthority); }); it('non-admin cannot transfer authority', async function () { await expectRevert( this.manager.transferContractAuthority(this.managed.address, otherAuthority, { from: nonAdmin }), 'missing role', ); }); }); describe('adapter', function () { const group = '0'; beforeEach('deploying adapter', async function () { await this.manager.createGroup(group, 'a group', { from: admin }); await this.manager.grantGroup(group, user1, { from: admin }); this.adapter = await AccessManagerAdapter.new(this.manager.address); }); it('with ownable', async function () { const target = await Ownable.new(); await target.transferOwnership(this.adapter.address); const { data } = await target.$_checkOwner.request(); const selector = data.slice(0, 10); await expectRevert( this.adapter.relay(target.address, data, { from: user1 }), 'AccessManagerAdapter: caller not allowed', ); await this.manager.setFunctionAllowedGroup(target.address, [selector], group, true, { from: admin }); await this.adapter.relay(target.address, data, { from: user1 }); }); it('with access control', async function () { const ROLE = web3.utils.soliditySha3('ROLE'); const target = await AccessControl.new(); await target.$_grantRole(ROLE, this.adapter.address); const { data } = await target.$_checkRole.request(ROLE); const selector = data.slice(0, 10); await expectRevert( this.adapter.relay(target.address, data, { from: user1 }), 'AccessManagerAdapter: caller not allowed', ); await this.manager.setFunctionAllowedGroup(target.address, [selector], group, true, { from: admin }); await this.adapter.relay(target.address, data, { from: user1 }); }); it('transfer authority', async function () { await this.manager.transferContractAuthority(this.adapter.address, otherAuthority, { from: admin }); expect(await this.adapter.authority()).to.equal(otherAuthority); }); }); });