Skip to content

Commit 688f056

Browse files
authored
Added DISTRIBUTE_ADMIN role to be responsible for distributing rewards (#267)
1 parent 5b2fe8e commit 688f056

File tree

7 files changed

+55
-20
lines changed

7 files changed

+55
-20
lines changed

contracts/staking/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ To stake, any account should call `stake()`, passing in the amount to be staked
4040

4141
To unstake, the account that previously staked should call, `unstake(uint256 _amountToUnstake)`.
4242

43-
Accounts that wish to distribute rewards should call, `distributeRewards(AccountAmount[] calldata _recipientsAndAmounts)`. The `AccountAmount` structure consists of recipient address and amount to distribute pairs. Distributions can only be made to accounts that have previously or are currently staking. The amount to be distributed must be passed in as msg.value and must equal to the sum of the amounts specified in the `_recipientsAndAmounts` array.
43+
Accounts that have DISTRIBUTE_ROLE that wish to distribute rewards should call, `distributeRewards(AccountAmount[] calldata _recipientsAndAmounts)`. The `AccountAmount` structure consists of recipient address and amount to distribute pairs. Distributions can only be made to accounts that have previously or are currently staking. The amount to be distributed must be passed in as msg.value and must equal to the sum of the amounts specified in the `_recipientsAndAmounts` array.
4444

4545
The `stakers` array needs to be analysed to determine which accounts have staked and how much. The following functions provide access to this data structure:
4646

contracts/staking/StakeHolder.sol

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Immutable Pty Ltd 2018 - 2024
1+
// Copyright (c) Immutable Pty Ltd 2018 - 2025
22
// SPDX-License-Identifier: Apache 2
33
pragma solidity >=0.8.19 <0.8.29;
44

@@ -49,6 +49,9 @@ contract StakeHolder is AccessControlEnumerableUpgradeable, UUPSUpgradeable {
4949
/// @notice Only UPGRADE_ROLE can upgrade the contract
5050
bytes32 public constant UPGRADE_ROLE = bytes32("UPGRADE_ROLE");
5151

52+
/// @notice Only DISTRIBUTE_ROLE can call the distribute function
53+
bytes32 public constant DISTRIBUTE_ROLE = bytes32("DISTRIBUTE_ROLE");
54+
5255
/// @notice Version 0 version number
5356
uint256 private constant _VERSION0 = 0;
5457

@@ -78,12 +81,14 @@ contract StakeHolder is AccessControlEnumerableUpgradeable, UUPSUpgradeable {
7881
* @notice Initialises the upgradeable contract, setting up admin accounts.
7982
* @param _roleAdmin the address to grant `DEFAULT_ADMIN_ROLE` to
8083
* @param _upgradeAdmin the address to grant `UPGRADE_ROLE` to
84+
* @param _distributeAdmin the address to grant `DISTRIBUTE_ROLE` to
8185
*/
82-
function initialize(address _roleAdmin, address _upgradeAdmin) public initializer {
86+
function initialize(address _roleAdmin, address _upgradeAdmin, address _distributeAdmin) public initializer {
8387
__UUPSUpgradeable_init();
8488
__AccessControl_init();
8589
_grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin);
8690
_grantRole(UPGRADE_ROLE, _upgradeAdmin);
91+
_grantRole(DISTRIBUTE_ROLE, _distributeAdmin);
8792
version = _VERSION0;
8893
}
8994

@@ -136,14 +141,16 @@ contract StakeHolder is AccessControlEnumerableUpgradeable, UUPSUpgradeable {
136141
}
137142

138143
/**
139-
* @notice Any account can distribute tokens to any set of accounts.
144+
* @notice Accounts with DISTRIBUTE_ROLE can distribute tokens to any set of accounts.
140145
* @dev The total amount to distribute must match msg.value.
141146
* This function does not need re-entrancy guard as the distribution mechanism
142147
* does not call out to another contract.
143148
* @param _recipientsAndAmounts An array of recipients to distribute value to and
144149
* amounts to be distributed to each recipient.
145150
*/
146-
function distributeRewards(AccountAmount[] calldata _recipientsAndAmounts) external payable {
151+
function distributeRewards(
152+
AccountAmount[] calldata _recipientsAndAmounts
153+
) external payable onlyRole(DISTRIBUTE_ROLE) {
147154
// Initial validity checks
148155
if (msg.value == 0) {
149156
revert MustDistributeMoreThanZero();

script/staking/DeployStakeHolder.sol

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ struct DeploymentArgs {
3434
struct StakeHolderContractArgs {
3535
address roleAdmin;
3636
address upgradeAdmin;
37+
address distributeAdmin;
3738
}
3839

3940
/**
@@ -58,7 +59,8 @@ contract DeployStakeHolder is Test {
5859

5960
StakeHolderContractArgs memory stakeHolderContractArgs = StakeHolderContractArgs({
6061
roleAdmin: makeAddr("role"),
61-
upgradeAdmin: makeAddr("upgrade")
62+
upgradeAdmin: makeAddr("upgrade"),
63+
distributeAdmin: makeAddr("distribute")
6264
});
6365

6466
// Run deployment against forked testnet
@@ -75,13 +77,14 @@ contract DeployStakeHolder is Test {
7577
address factory = vm.envAddress("OWNABLE_CREATE3_FACTORY_ADDRESS");
7678
address roleAdmin = vm.envAddress("ROLE_ADMIN");
7779
address upgradeAdmin = vm.envAddress("UPGRADE_ADMIN");
80+
address distributeAdmin = vm.envAddress("DISTRIBUTE_ADMIN");
7881
string memory salt1 = vm.envString("IMPL_SALT");
7982
string memory salt2 = vm.envString("PROXY_SALT");
8083

8184
DeploymentArgs memory deploymentArgs = DeploymentArgs({signer: signer, factory: factory, salt1: salt1, salt2: salt2});
8285

8386
StakeHolderContractArgs memory stakeHolderContractArgs =
84-
StakeHolderContractArgs({roleAdmin: roleAdmin, upgradeAdmin: upgradeAdmin});
87+
StakeHolderContractArgs({roleAdmin: roleAdmin, upgradeAdmin: upgradeAdmin, distributeAdmin: distributeAdmin});
8588

8689
_deploy(deploymentArgs, stakeHolderContractArgs);
8790
}
@@ -107,7 +110,8 @@ contract DeployStakeHolder is Test {
107110

108111
// Create init data for teh ERC1967 Proxy
109112
bytes memory initData = abi.encodeWithSelector(
110-
StakeHolder.initialize.selector, stakeHolderContractArgs.roleAdmin, stakeHolderContractArgs.upgradeAdmin
113+
StakeHolder.initialize.selector, stakeHolderContractArgs.roleAdmin,
114+
stakeHolderContractArgs.upgradeAdmin, stakeHolderContractArgs.distributeAdmin
111115
);
112116

113117
// Deploy ERC1967Proxy via the Ownable Create3 factory.

test/staking/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ Operational tests (in [StakeHolderOperational.t.sol](../../contracts/staking/Sta
4848
| testDistributeMismatch | Fail if the total to distribute does not equal msg.value. | No | Yes |
4949
| testDistributeToEmptyAccount | Stake, unstake, distribute rewards. | Yes | Yes |
5050
| testDistributeToUnusedAccount | Attempt to distribute rewards to an account that has never staked. | No | Yes |
51+
| testDistributeBadAuth | Attempt to distribute rewards using an unauthorised account. | No | Yes |
5152

test/staking/StakeHolderBase.t.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ contract StakeHolderBaseTest is Test {
1515

1616
bytes32 public defaultAdminRole;
1717
bytes32 public upgradeRole;
18+
bytes32 public distributeRole;
1819

1920
ERC1967Proxy public proxy;
2021
StakeHolder public stakeHolder;
2122

2223
address public roleAdmin;
2324
address public upgradeAdmin;
25+
address public distributeAdmin;
2426

2527
address public staker1;
2628
address public staker2;
@@ -30,6 +32,7 @@ contract StakeHolderBaseTest is Test {
3032
function setUp() public {
3133
roleAdmin = makeAddr("RoleAdmin");
3234
upgradeAdmin = makeAddr("UpgradeAdmin");
35+
distributeAdmin = makeAddr("DistributeAdmin");
3336

3437
staker1 = makeAddr("Staker1");
3538
staker2 = makeAddr("Staker2");
@@ -39,13 +42,14 @@ contract StakeHolderBaseTest is Test {
3942
StakeHolder impl = new StakeHolder();
4043

4144
bytes memory initData = abi.encodeWithSelector(
42-
StakeHolder.initialize.selector, roleAdmin, upgradeAdmin
45+
StakeHolder.initialize.selector, roleAdmin, upgradeAdmin, distributeAdmin
4346
);
4447

4548
proxy = new ERC1967Proxy(address(impl), initData);
4649
stakeHolder = StakeHolder(address(proxy));
4750

4851
defaultAdminRole = stakeHolder.DEFAULT_ADMIN_ROLE();
4952
upgradeRole = stakeHolder.UPGRADE_ROLE();
53+
distributeRole = stakeHolder.DISTRIBUTE_ROLE();
5054
}
5155
}

test/staking/StakeHolderInit.t.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ contract StakeHolderInitTest is StakeHolderBaseTest {
2020
function testAdmins() public {
2121
assertEq(stakeHolder.getRoleMemberCount(defaultAdminRole), 1, "Expect one role admin");
2222
assertEq(stakeHolder.getRoleMemberCount(upgradeRole), 1, "Expect one upgrade admin");
23+
assertEq(stakeHolder.getRoleMemberCount(distributeRole), 1, "Expect one distribute admin");
2324
assertTrue(stakeHolder.hasRole(defaultAdminRole, roleAdmin), "Expect roleAdmin is role admin");
2425
assertTrue(stakeHolder.hasRole(upgradeRole, upgradeAdmin), "Expect upgradeAdmin is upgrade admin");
26+
assertTrue(stakeHolder.hasRole(distributeRole, distributeAdmin), "Expect distributeAdmin is distribute admin");
2527
}
2628
}

test/staking/StakeHolderOperational.t.sol

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
199199
vm.deal(staker1, 100 ether);
200200
vm.deal(staker2, 100 ether);
201201
vm.deal(staker3, 100 ether);
202-
vm.deal(bank, 100 ether);
202+
vm.deal(distributeAdmin, 100 ether);
203203

204204
vm.prank(staker1);
205205
stakeHolder.stake{value: 10 ether}();
@@ -211,7 +211,7 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
211211
// Distribute rewards to staker2 only.
212212
AccountAmount[] memory accountsAmounts = new AccountAmount[](1);
213213
accountsAmounts[0] = AccountAmount(staker2, 0.5 ether);
214-
vm.prank(bank);
214+
vm.prank(distributeAdmin);
215215
stakeHolder.distributeRewards{value: 0.5 ether}(accountsAmounts);
216216

217217
assertEq(stakeHolder.getBalance(staker1), 10 ether, "Incorrect balance1");
@@ -223,7 +223,7 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
223223
vm.deal(staker1, 100 ether);
224224
vm.deal(staker2, 100 ether);
225225
vm.deal(staker3, 100 ether);
226-
vm.deal(bank, 100 ether);
226+
vm.deal(distributeAdmin, 100 ether);
227227

228228
vm.prank(staker1);
229229
stakeHolder.stake{value: 10 ether}();
@@ -236,7 +236,7 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
236236
AccountAmount[] memory accountsAmounts = new AccountAmount[](2);
237237
accountsAmounts[0] = AccountAmount(staker2, 0.5 ether);
238238
accountsAmounts[1] = AccountAmount(staker3, 1 ether);
239-
vm.prank(bank);
239+
vm.prank(distributeAdmin);
240240
stakeHolder.distributeRewards{value: 1.5 ether}(accountsAmounts);
241241

242242
assertEq(stakeHolder.getBalance(staker1), 10 ether, "Incorrect balance1");
@@ -246,7 +246,7 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
246246

247247
function testDistributeZeroReward() public {
248248
vm.deal(staker1, 100 ether);
249-
vm.deal(bank, 100 ether);
249+
vm.deal(distributeAdmin, 100 ether);
250250

251251
vm.prank(staker1);
252252
stakeHolder.stake{value: 10 ether}();
@@ -255,15 +255,15 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
255255
AccountAmount[] memory accountsAmounts = new AccountAmount[](1);
256256
accountsAmounts[0] = AccountAmount(staker2, 0 ether);
257257
vm.expectRevert(abi.encodeWithSelector(StakeHolder.MustDistributeMoreThanZero.selector));
258-
vm.prank(bank);
258+
vm.prank(distributeAdmin);
259259
stakeHolder.distributeRewards{value: 0 ether}(accountsAmounts);
260260
}
261261

262262
function testDistributeMismatch() public {
263263
vm.deal(staker1, 100 ether);
264264
vm.deal(staker2, 100 ether);
265265
vm.deal(staker3, 100 ether);
266-
vm.deal(bank, 100 ether);
266+
vm.deal(distributeAdmin, 100 ether);
267267

268268
vm.prank(staker1);
269269
stakeHolder.stake{value: 10 ether}();
@@ -277,13 +277,13 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
277277
accountsAmounts[0] = AccountAmount(staker2, 0.5 ether);
278278
accountsAmounts[1] = AccountAmount(staker3, 1 ether);
279279
vm.expectRevert(abi.encodeWithSelector(StakeHolder.DistributionAmountsDoNotMatchTotal.selector, 1 ether, 1.5 ether));
280-
vm.prank(bank);
280+
vm.prank(distributeAdmin);
281281
stakeHolder.distributeRewards{value: 1 ether}(accountsAmounts);
282282
}
283283

284284
function testDistributeToEmptyAccount() public {
285285
vm.deal(staker1, 100 ether);
286-
vm.deal(bank, 100 ether);
286+
vm.deal(distributeAdmin, 100 ether);
287287

288288
vm.prank(staker1);
289289
stakeHolder.stake{value: 10 ether}();
@@ -293,7 +293,7 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
293293
// Distribute rewards to staker2 only.
294294
AccountAmount[] memory accountsAmounts = new AccountAmount[](1);
295295
accountsAmounts[0] = AccountAmount(staker1, 0.5 ether);
296-
vm.prank(bank);
296+
vm.prank(distributeAdmin);
297297
stakeHolder.distributeRewards{value: 0.5 ether}(accountsAmounts);
298298

299299
assertEq(stakeHolder.getBalance(staker1), 0.5 ether, "Incorrect balance1");
@@ -302,13 +302,30 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
302302
}
303303

304304
function testDistributeToUnusedAccount() public {
305-
vm.deal(bank, 100 ether);
305+
vm.deal(distributeAdmin, 100 ether);
306306

307307
// Distribute rewards to staker2 only.
308308
AccountAmount[] memory accountsAmounts = new AccountAmount[](1);
309309
accountsAmounts[0] = AccountAmount(staker1, 0.5 ether);
310310
vm.expectRevert(abi.encodeWithSelector(StakeHolder.AttemptToDistributeToNewAccount.selector, staker1, 0.5 ether));
311+
vm.prank(distributeAdmin);
312+
stakeHolder.distributeRewards{value: 0.5 ether}(accountsAmounts);
313+
}
314+
315+
function testDistributeBadAuth() public {
316+
vm.deal(staker1, 100 ether);
317+
vm.deal(bank, 100 ether);
318+
319+
vm.prank(staker1);
320+
stakeHolder.stake{value: 10 ether}();
321+
322+
// Distribute rewards to staker1 only, but not from distributeAdmin
323+
AccountAmount[] memory accountsAmounts = new AccountAmount[](1);
324+
accountsAmounts[0] = AccountAmount(staker1, 0.5 ether);
311325
vm.prank(bank);
326+
// Error will be of the form:
327+
// AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0x555047524144455f524f4c450000000000000000000000000000000000000000
328+
vm.expectRevert();
312329
stakeHolder.distributeRewards{value: 0.5 ether}(accountsAmounts);
313330
}
314331
}

0 commit comments

Comments
 (0)