Skip to content

Commit a834938

Browse files
author
cedephrase
authored
feat: Add generic RBAC for GHO token (aave#332)
* feat: initial generic RBAC for GHO token * fix: use new roles exclusively, update tests for roles * fix: change certora patch * fix: bug in previous certora fix * fix: add admin param to GhoToken constructor * fix: modify Certora GhoToken harness for new admin param * fix: add role grant verification
1 parent 4c22747 commit a834938

File tree

13 files changed

+188
-34
lines changed

13 files changed

+188
-34
lines changed

certora/applyHarness.patch

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ diff -ruN ../src/.gitignore .gitignore
77
diff -ruN ../src/contracts/gho/GhoToken.sol contracts/gho/GhoToken.sol
88
--- ../src/contracts/gho/GhoToken.sol 2023-02-26 10:23:14.000000000 +0200
99
+++ contracts/gho/GhoToken.sol 2023-02-26 13:26:13.000000000 +0200
10-
@@ -71,11 +71,16 @@
10+
@@ -75,11 +75,16 @@
1111
uint128 bucketCapacity
12-
) external onlyOwner {
12+
) external onlyRole(FACILITATOR_MANAGER) {
1313
Facilitator storage facilitator = _facilitators[facilitatorAddress];
1414
+ require(
15-
+ !facilitator.isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved
15+
+ !facilitator.isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved
1616
+ 'FACILITATOR_ALREADY_EXISTS'
1717
+ );
1818
require(bytes(facilitator.label).length == 0, 'FACILITATOR_ALREADY_EXISTS');
@@ -24,29 +24,29 @@ diff -ruN ../src/contracts/gho/GhoToken.sol contracts/gho/GhoToken.sol
2424

2525
_facilitatorsList.add(facilitatorAddress);
2626

27-
@@ -89,6 +94,10 @@
27+
@@ -93,6 +98,10 @@
2828
/// @inheritdoc IGhoToken
29-
function removeFacilitator(address facilitatorAddress) external onlyOwner {
29+
function removeFacilitator(address facilitatorAddress) external onlyRole(FACILITATOR_MANAGER) {
3030
require(
31-
+ _facilitators[facilitatorAddress].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved
31+
+ _facilitators[facilitatorAddress].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved
3232
+ 'FACILITATOR_DOES_NOT_EXIST'
3333
+ );
3434
+ require(
3535
bytes(_facilitators[facilitatorAddress].label).length > 0,
3636
'FACILITATOR_DOES_NOT_EXIST'
3737
);
38-
@@ -108,6 +117,10 @@
38+
@@ -112,6 +121,10 @@
3939
address facilitator,
4040
uint128 newCapacity
41-
) external onlyOwner {
41+
) external onlyRole(BUCKET_MANAGER) {
4242
+ require(
43-
+ _facilitators[facilitator].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved
43+
+ _facilitators[facilitator].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved
4444
+ 'FACILITATOR_DOES_NOT_EXIST'
4545
+ );
4646
require(bytes(_facilitators[facilitator].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST');
4747

4848
uint256 oldCapacity = _facilitators[facilitator].bucketCapacity;
49-
@@ -122,12 +135,12 @@
49+
@@ -126,12 +139,12 @@
5050
}
5151

5252
/// @inheritdoc IGhoToken

certora/harness/GhoTokenHarness.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {GhoToken} from '../munged/contracts/gho/GhoToken.sol';
77
contract GhoTokenHarness is GhoToken {
88
using EnumerableSet for EnumerableSet.AddressSet;
99

10+
constructor() GhoToken(msg.sender) {}
11+
1012
/**
1113
* @notice Returns the backet capacity
1214
* @param facilitator The address of the facilitator

deploy/00_deploy_gho_token.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const func: DeployFunction = async function ({
1616

1717
const ghoResult = await deploy('GhoToken', {
1818
from: deployer,
19-
args: [],
19+
args: [deployer],
2020
log: true,
2121
});
2222
console.log(`GHO Address: ${ghoResult.address}`);

src/contracts/gho/GhoToken.sol

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@
22
pragma solidity ^0.8.0;
33

44
import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol';
5-
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
5+
import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol';
66
import {ERC20} from './ERC20.sol';
77
import {IGhoToken} from './interfaces/IGhoToken.sol';
88

99
/**
1010
* @title GHO Token
1111
* @author Aave
1212
*/
13-
contract GhoToken is ERC20, Ownable, IGhoToken {
13+
contract GhoToken is ERC20, AccessControl, IGhoToken {
1414
using EnumerableSet for EnumerableSet.AddressSet;
1515

1616
mapping(address => Facilitator) internal _facilitators;
1717
EnumerableSet.AddressSet internal _facilitatorsList;
1818

19+
bytes32 public constant FACILITATOR_MANAGER = keccak256('FACILITATOR_MANAGER');
20+
bytes32 public constant BUCKET_MANAGER = keccak256('BUCKET_MANAGER');
21+
1922
/**
2023
* @dev Constructor
24+
* @param admin This is the initial holder of the default admin role
2125
*/
22-
constructor() ERC20('Gho Token', 'GHO', 18) {
23-
// Intentionally left blank
26+
constructor(address admin) ERC20('Gho Token', 'GHO', 18) {
27+
_setupRole(DEFAULT_ADMIN_ROLE, admin);
2428
}
2529

2630
/**
@@ -69,7 +73,7 @@ contract GhoToken is ERC20, Ownable, IGhoToken {
6973
address facilitatorAddress,
7074
string calldata facilitatorLabel,
7175
uint128 bucketCapacity
72-
) external onlyOwner {
76+
) external onlyRole(FACILITATOR_MANAGER) {
7377
Facilitator storage facilitator = _facilitators[facilitatorAddress];
7478
require(bytes(facilitator.label).length == 0, 'FACILITATOR_ALREADY_EXISTS');
7579
require(bytes(facilitatorLabel).length > 0, 'INVALID_LABEL');
@@ -87,7 +91,7 @@ contract GhoToken is ERC20, Ownable, IGhoToken {
8791
}
8892

8993
/// @inheritdoc IGhoToken
90-
function removeFacilitator(address facilitatorAddress) external onlyOwner {
94+
function removeFacilitator(address facilitatorAddress) external onlyRole(FACILITATOR_MANAGER) {
9195
require(
9296
bytes(_facilitators[facilitatorAddress].label).length > 0,
9397
'FACILITATOR_DOES_NOT_EXIST'
@@ -107,7 +111,7 @@ contract GhoToken is ERC20, Ownable, IGhoToken {
107111
function setFacilitatorBucketCapacity(
108112
address facilitator,
109113
uint128 newCapacity
110-
) external onlyOwner {
114+
) external onlyRole(BUCKET_MANAGER) {
111115
require(bytes(_facilitators[facilitator].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST');
112116

113117
uint256 oldCapacity = _facilitators[facilitator].bucketCapacity;

src/test/TestGhoBase.t.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ contract TestGhoBase is Test, Constants, Events {
104104
CONFIGURATOR = new MockedConfigurator(IPool(POOL));
105105
GHO_ORACLE = new GhoOracle();
106106
GHO_MANAGER = new GhoManager();
107-
GHO_TOKEN = new GhoToken();
107+
GHO_TOKEN = new GhoToken(address(this));
108+
GHO_TOKEN.grantRole(FACILITATOR_MANAGER, address(this));
109+
GHO_TOKEN.grantRole(BUCKET_MANAGER, address(this));
108110
AAVE_TOKEN = new TestnetERC20('AAVE', 'AAVE', 18, FAUCET);
109111
StakedAaveV3 stkAave = new StakedAaveV3(
110112
IERC20(address(AAVE_TOKEN)),

src/test/TestGhoToken.t.sol

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
pragma solidity ^0.8.0;
33

44
import './TestGhoBase.t.sol';
5+
import '@openzeppelin/contracts/utils/Strings.sol';
56

67
contract TestGhoToken is TestGhoBase {
78
function testConstructor() public {
8-
GhoToken ghoToken = new GhoToken();
9+
GhoToken ghoToken = new GhoToken(address(this));
10+
vm.expectEmit(true, true, true, true, address(GHO_TOKEN));
11+
emit RoleGranted(GHO_TOKEN.DEFAULT_ADMIN_ROLE(), msg.sender, address(this));
12+
GHO_TOKEN.grantRole(GHO_TOKEN.DEFAULT_ADMIN_ROLE(), msg.sender);
913
assertEq(ghoToken.name(), 'Gho Token', 'Wrong default ERC20 name');
1014
assertEq(ghoToken.symbol(), 'GHO', 'Wrong default ERC20 symbol');
1115
assertEq(ghoToken.decimals(), 18, 'Wrong default ERC20 decimals');
@@ -61,6 +65,16 @@ contract TestGhoToken is TestGhoBase {
6165
GHO_TOKEN.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY);
6266
}
6367

68+
function testAddFacilitatorWithRole() public {
69+
vm.expectEmit(true, true, true, true, address(GHO_TOKEN));
70+
emit RoleGranted(GHO_TOKEN.FACILITATOR_MANAGER(), ALICE, address(this));
71+
GHO_TOKEN.grantRole(GHO_TOKEN.FACILITATOR_MANAGER(), ALICE);
72+
vm.prank(ALICE);
73+
vm.expectEmit(true, true, false, true, address(GHO_TOKEN));
74+
emit FacilitatorAdded(ALICE, keccak256(abi.encodePacked('Alice')), DEFAULT_CAPACITY);
75+
GHO_TOKEN.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY);
76+
}
77+
6478
function testRevertAddExistingFacilitator() public {
6579
vm.expectRevert('FACILITATOR_ALREADY_EXISTS');
6680
GHO_TOKEN.addFacilitator(address(GHO_ATOKEN), 'Aave V3 Pool', DEFAULT_CAPACITY);
@@ -71,6 +85,18 @@ contract TestGhoToken is TestGhoBase {
7185
GHO_TOKEN.addFacilitator(ALICE, '', DEFAULT_CAPACITY);
7286
}
7387

88+
function testRevertAddFacilitatorNoRole() public {
89+
bytes memory revertMsg = abi.encodePacked(
90+
'AccessControl: account ',
91+
Strings.toHexString(ALICE),
92+
' is missing role ',
93+
Strings.toHexString(uint256(FACILITATOR_MANAGER), 32)
94+
);
95+
vm.prank(ALICE);
96+
vm.expectRevert(revertMsg);
97+
GHO_TOKEN.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY);
98+
}
99+
74100
function testRevertSetBucketCapacityNonFacilitator() public {
75101
vm.expectRevert('FACILITATOR_DOES_NOT_EXIST');
76102
GHO_TOKEN.setFacilitatorBucketCapacity(ALICE, DEFAULT_CAPACITY);
@@ -82,6 +108,28 @@ contract TestGhoToken is TestGhoBase {
82108
GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0);
83109
}
84110

111+
function testSetNewBucketCapacityAsManager() public {
112+
vm.expectEmit(true, true, true, true, address(GHO_TOKEN));
113+
emit RoleGranted(GHO_TOKEN.BUCKET_MANAGER(), ALICE, address(this));
114+
GHO_TOKEN.grantRole(GHO_TOKEN.BUCKET_MANAGER(), ALICE);
115+
vm.prank(ALICE);
116+
vm.expectEmit(true, false, false, true, address(GHO_TOKEN));
117+
emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), DEFAULT_CAPACITY, 0);
118+
GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0);
119+
}
120+
121+
function testRevertSetNewBucketCapacityNoRole() public {
122+
bytes memory revertMsg = abi.encodePacked(
123+
'AccessControl: account ',
124+
Strings.toHexString(ALICE),
125+
' is missing role ',
126+
Strings.toHexString(uint256(BUCKET_MANAGER), 32)
127+
);
128+
vm.prank(ALICE);
129+
vm.expectRevert(revertMsg);
130+
GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0);
131+
}
132+
85133
function testRevertRemoveNonFacilitator() public {
86134
vm.expectRevert('FACILITATOR_DOES_NOT_EXIST');
87135
GHO_TOKEN.removeFacilitator(ALICE);
@@ -99,6 +147,28 @@ contract TestGhoToken is TestGhoBase {
99147
GHO_TOKEN.removeFacilitator(address(GHO_ATOKEN));
100148
}
101149

150+
function testRemoveFacilitatorWithRole() public {
151+
vm.expectEmit(true, true, true, true, address(GHO_TOKEN));
152+
emit RoleGranted(GHO_TOKEN.FACILITATOR_MANAGER(), ALICE, address(this));
153+
GHO_TOKEN.grantRole(GHO_TOKEN.FACILITATOR_MANAGER(), ALICE);
154+
vm.prank(ALICE);
155+
vm.expectEmit(true, false, false, true, address(GHO_TOKEN));
156+
emit FacilitatorRemoved(address(GHO_ATOKEN));
157+
GHO_TOKEN.removeFacilitator(address(GHO_ATOKEN));
158+
}
159+
160+
function testRevertRemoveFacilitatorNoRole() public {
161+
bytes memory revertMsg = abi.encodePacked(
162+
'AccessControl: account ',
163+
Strings.toHexString(ALICE),
164+
' is missing role ',
165+
Strings.toHexString(uint256(FACILITATOR_MANAGER), 32)
166+
);
167+
vm.prank(ALICE);
168+
vm.expectRevert(revertMsg);
169+
GHO_TOKEN.removeFacilitator(address(GHO_ATOKEN));
170+
}
171+
102172
function testRevertMintBadFacilitator() public {
103173
vm.prank(ALICE);
104174
vm.expectRevert('INVALID_FACILITATOR');

src/test/helpers/Constants.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ contract Constants {
66
address constant SHORT_EXECUTOR = 0xEE56e2B3D491590B5b31738cC34d5232F378a8D5;
77
address constant STKAAVE_PROXY_ADMIN = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF;
88

9+
// admin roles for GhoToken
10+
bytes32 public constant FACILITATOR_MANAGER = keccak256('FACILITATOR_MANAGER');
11+
bytes32 public constant BUCKET_MANAGER = keccak256('BUCKET_MANAGER');
12+
913
// defaults used in test environment
1014
uint256 constant DEFAULT_FLASH_FEE = 0.0009e4; // 0.09%
1115
uint128 constant DEFAULT_CAPACITY = 100_000_000e18;

src/test/helpers/Events.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,7 @@ interface Events {
7272
address indexed asset,
7373
uint256 amount
7474
);
75+
76+
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
77+
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
7578
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import { expect } from 'chai';
12
import { GhoToken } from './../../../types/src/contracts/gho/GhoToken';
23
import { task } from 'hardhat/config';
34

45
task('gho-transfer-ownership', 'Transfer Ownership of Gho')
56
.addParam('newOwner')
67
.setAction(async ({ newOwner }, hre) => {
8+
const DEFAULT_ADMIN_ROLE = hre.ethers.utils.hexZeroPad('0x00', 32);
79
const gho = (await hre.ethers.getContract('GhoToken')) as GhoToken;
8-
const transferOwnershipTx = await gho.transferOwnership(newOwner);
9-
await transferOwnershipTx.wait();
10+
const grantAdminRoleTx = await gho.grantRole(DEFAULT_ADMIN_ROLE, newOwner);
11+
await expect(grantAdminRoleTx).to.emit(gho, 'RoleGranted');
12+
13+
const removeAdminRoleTx = await gho.renounceRole(DEFAULT_ADMIN_ROLE, users[0].address);
1014

1115
console.log(`GHO ownership transferred to: ${newOwner}`);
1216
});

tasks/testnet-setup/00_initialize-gho-reserve.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ task('initialize-gho-reserve', 'Initialize Gho Reserve').setAction(async (_, hre
2424
const ghoVariableDebtTokenImplementation = await ethers.getContract('GhoVariableDebtToken');
2525
const ghoInterestRateStrategy = await ethers.getContract('GhoInterestRateStrategy');
2626
const ghoToken = await ethers.getContract('GhoToken');
27+
ghoToken.grantRole(ghoToken.FACILITATOR_MANAGER(), _deployer.address);
28+
ghoToken.grantRole(ghoToken.BUCKET_MANAGER(), _deployer.address);
2729
const poolConfigurator = await getPoolConfiguratorProxy();
2830
const treasuryAddress = (await hre.deployments.get(TREASURY_PROXY_ID)).address;
2931
const incentivesControllerAddress = (await hre.deployments.get(INCENTIVES_PROXY_ID)).address;

0 commit comments

Comments
 (0)