Skip to content

Commit a777b2b

Browse files
authored
StakeFor upgrade to staking system (#280)
This upgrade to the staking system adds the function stakeFor, that allows value to be staked on behalf of accounts. The contract deployed by deploySimple and deployComplex scripts is now the version 2 contract.
1 parent 8ff3fc9 commit a777b2b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1723
-221
lines changed

,,.x.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Compiling 118 files with Solc 0.8.17
2+
Compiling 55 files with Solc 0.8.20
3+
Compiling 305 files with Solc 0.8.28

audits/staking/202506-threat-model-stake-holder.md

Lines changed: 321 additions & 0 deletions
Large diffs are not rendered by default.

contracts/staking/IStakeHolder.sol

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ pragma solidity >=0.8.19 <0.8.29;
55
import {IAccessControlEnumerableUpgradeable} from "openzeppelin-contracts-upgradeable-4.9.3/access/IAccessControlEnumerableUpgradeable.sol";
66

77
/**
8-
* @title StakeHolderBase: allows anyone to stake any amount of an ERC20 token and to then remove all or part of that stake.
9-
* @dev The StakeHolderERC20 contract is designed to be upgradeable.
8+
* @title IStakeHolder: Interface for staking system.
109
*/
1110
interface IStakeHolder is IAccessControlEnumerableUpgradeable {
1211
/// @notice implementation does not accept native tokens.
@@ -33,13 +32,18 @@ interface IStakeHolder is IAccessControlEnumerableUpgradeable {
3332
/// @notice Error: Call to stake for implementations that accept value require value and parameter to match.
3433
error MismatchMsgValueAmount(uint256 _msgValue, uint256 _amount);
3534

35+
/// @notice Error: Unstake native value transfer failed with revert with no revert information.
36+
/// @dev An error was detected by the EVM. For example a function call to an address with no contract associated with it.
37+
error UnstakeTransferFailed();
38+
3639
/// @notice Event when an amount has been staked or when an amount is distributed to an account.
3740
event StakeAdded(address _staker, uint256 _amountAdded, uint256 _newBalance);
3841

3942
/// @notice Event when an amount has been unstaked.
4043
event StakeRemoved(address _staker, uint256 _amountRemoved, uint256 _newBalance);
4144

42-
/// @notice Event summarising a distribution. There will also be one StakeAdded event for each recipient.
45+
/// @notice Event summarising a distribution.
46+
/// @dev There will also be one StakeAdded event for each recipient.
4347
event Distributed(address _distributor, uint256 _totalDistribution, uint256 _numRecipients);
4448

4549
/// @notice Struct to combine an account and an amount.
@@ -61,7 +65,9 @@ interface IStakeHolder is IAccessControlEnumerableUpgradeable {
6165
function unstake(uint256 _amountToUnstake) external;
6266

6367
/**
64-
* @notice Accounts with DISTRIBUTE_ROLE can distribute tokens to any set of accounts.
68+
* @notice Distribute rewards to stakers.
69+
* @dev Only callable by accounts with DISTRIBUTE_ROLE.
70+
* @dev Recipients must have staked value prior to this function call.
6571
* @param _recipientsAndAmounts An array of recipients to distribute value to and
6672
* amounts to be distributed to each recipient.
6773
*/
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Immutable Pty Ltd 2018 - 2025
2+
// SPDX-License-Identifier: Apache 2
3+
pragma solidity >=0.8.19 <0.8.29;
4+
5+
import {IStakeHolder} from "./IStakeHolder.sol";
6+
7+
/**
8+
* @title IStakeHolderV2: Interface for V2 staking system.
9+
*/
10+
interface IStakeHolderV2 is IStakeHolder {
11+
/// @notice Native IMX was received from an account other than the WIMX contract.
12+
error ImxNotFromWimxContract(address _from);
13+
14+
/// @notice Event summarising a distribution via the stakeFor function.
15+
/// @dev There will be one StakeAdded event for each recipient.
16+
event StakedFor(address _distributor, uint256 _totalDistribution, uint256 _numRecipients);
17+
18+
/**
19+
* @notice Stake on behalf of others.
20+
* @dev Only callable by accounts with DISTRIBUTE_ROLE.
21+
* @dev Unlike the distributeRewards function, there is no requirement that recipients are existing stakers.
22+
* @param _recipientsAndAmounts An array of recipients to distribute value to and
23+
* amounts to be distributed to each recipient.
24+
*/
25+
function stakeFor(AccountAmount[] calldata _recipientsAndAmounts) external payable;
26+
}

contracts/staking/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Staking
22

3-
The Immutable zkEVM staking system allows any account (EOA or contract) to stake any amount of a token at any time. An account can remove all or some of their stake at any time. The contract has the facility to distribute rewards to stakers.
3+
The Immutable zkEVM staking system allows any account (EOA or contract) to stake any amount of a token at any time. An account can remove all or some of their stake at any time. The contracts have the facility to distribute rewards to stakers and to stake on behalf of accounts
44

55
The staking contracts are upgradeable and operate via a proxy contract. They use the [Universal Upgradeable Proxy Standard (UUPS)](https://eips.ethereum.org/EIPS/eip-1822) upgrade pattern, where the access control for upgrade resides within the application contract (the staking contract).
66

@@ -24,6 +24,10 @@ The system consists of a set of contracts show in the diagram below.
2424

2525
`OwnableCreate3Deployer.sol` ensures contracts are deployed to the same addresses across chains. The use of this contract is optional. See [deployment scripts](../../script/staking/README.md) for more information.
2626

27+
## Staking System V2
28+
29+
Files, contracts, and interfaced suffixed with `V2` form a part of the version two staking system. Version two introduces the ability for an admin account to stake on behalf of other accounts using the `stakeFor` function.
30+
2731
## Immutable Contract Addresses
2832

2933
TimelockController.sol:

contracts/staking/StakeHolderBase.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable-4.9
88
import {IStakeHolder} from "./IStakeHolder.sol";
99

1010
/**
11-
* @title StakeHolderBase: allows anyone to stake any amount of an ERC20 token and to then remove all or part of that stake.
12-
* @dev The StakeHolderERC20 contract is designed to be upgradeable.
11+
* @title StakeHolderBase: allows anyone to stake and unstake value.
12+
* @dev This contract is designed to be upgradeable.
1313
*/
1414
abstract contract StakeHolderBase is
1515
IStakeHolder,
@@ -58,7 +58,7 @@ abstract contract StakeHolderBase is
5858
address _roleAdmin,
5959
address _upgradeAdmin,
6060
address _distributeAdmin
61-
) internal onlyInitializing {
61+
) internal virtual onlyInitializing {
6262
__UUPSUpgradeable_init();
6363
__AccessControl_init();
6464
__ReentrancyGuard_init();
@@ -117,7 +117,7 @@ abstract contract StakeHolderBase is
117117
*/
118118
function distributeRewards(
119119
AccountAmount[] calldata _recipientsAndAmounts
120-
) external payable nonReentrant onlyRole(DISTRIBUTE_ROLE) {
120+
) external payable virtual nonReentrant onlyRole(DISTRIBUTE_ROLE) {
121121
// Distribute the value.
122122
uint256 total = 0;
123123
uint256 len = _recipientsAndAmounts.length;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) Immutable Pty Ltd 2018 - 2025
2+
// SPDX-License-Identifier: Apache 2
3+
pragma solidity >=0.8.19 <0.8.29;
4+
5+
import {StakeHolderBase} from "./StakeHolderBase.sol";
6+
import {IStakeHolderV2, IStakeHolder} from "./IStakeHolderV2.sol";
7+
8+
/**
9+
* @title StakeHolderBase: allows anyone to stake any amount of an ERC20 token and to then remove all or part of that stake.
10+
* @dev This contract is designed to be upgradeable.
11+
*/
12+
abstract contract StakeHolderBaseV2 is IStakeHolderV2, StakeHolderBase {
13+
/// @notice Version 2 version number
14+
uint256 internal constant _VERSION2 = 2;
15+
16+
/**
17+
* @notice Initialises the upgradeable contract, setting up admin accounts.
18+
* @param _roleAdmin the address to grant `DEFAULT_ADMIN_ROLE` to
19+
* @param _upgradeAdmin the address to grant `UPGRADE_ROLE` to
20+
* @param _distributeAdmin the address to grant `DISTRIBUTE_ROLE` to
21+
*/
22+
function __StakeHolderBase_init(
23+
address _roleAdmin,
24+
address _upgradeAdmin,
25+
address _distributeAdmin
26+
) internal virtual override {
27+
// NOTE: onlyInitializing is called in super.
28+
super.__StakeHolderBase_init(_roleAdmin, _upgradeAdmin, _distributeAdmin);
29+
version = _VERSION2;
30+
}
31+
32+
/**
33+
* @notice Function to be called when upgrading this contract.
34+
* @dev Call this function as part of upgradeToAndCall().
35+
* This initial version of this function reverts. There is no situation
36+
* in which it makes sense to upgrade to the V0 storage layout.
37+
* Note that this function is permissionless. Future versions must
38+
* compare the code version and the storage version and upgrade
39+
* appropriately. As such, the code will revert if an attacker calls
40+
* this function attempting a malicious upgrade.
41+
* @ param _data ABI encoded data to be used as part of the contract storage upgrade.
42+
*/
43+
function upgradeStorage(bytes memory /* _data */) external virtual override {
44+
if (version == _VERSION0) {
45+
// Upgrading from version 0 to 2 involves only code changes and
46+
// changing the storage version number.
47+
version = _VERSION2;
48+
} else {
49+
// Don't allow downgrade or re-initialising.
50+
revert CanNotUpgradeToLowerOrSameVersion(version);
51+
}
52+
}
53+
54+
/**
55+
* @inheritdoc IStakeHolder
56+
*/
57+
function distributeRewards(
58+
AccountAmount[] calldata _recipientsAndAmounts
59+
) external payable override(IStakeHolder, StakeHolderBase) nonReentrant onlyRole(DISTRIBUTE_ROLE) {
60+
uint256 total = _distributeRewards(_recipientsAndAmounts, true);
61+
uint256 len = _recipientsAndAmounts.length;
62+
emit Distributed(msg.sender, total, len);
63+
}
64+
65+
/**
66+
* @inheritdoc IStakeHolderV2
67+
*/
68+
function stakeFor(
69+
AccountAmount[] calldata _recipientsAndAmounts
70+
) external payable nonReentrant onlyRole(DISTRIBUTE_ROLE) {
71+
uint256 total = _distributeRewards(_recipientsAndAmounts, false);
72+
uint256 len = _recipientsAndAmounts.length;
73+
emit StakedFor(msg.sender, total, len);
74+
}
75+
76+
/**
77+
* @notice Distribute tokens to a set of accounts.
78+
* @param _recipientsAndAmounts An array of recipients to distribute value to and
79+
* amounts to be distributed to each recipient.
80+
* @param _existingAccountsOnly If true, revert if the account has never been used.
81+
* @return _total Value distributed.
82+
*/
83+
function _distributeRewards(
84+
AccountAmount[] calldata _recipientsAndAmounts,
85+
bool _existingAccountsOnly
86+
) private returns (uint256 _total) {
87+
// Distribute the value.
88+
_total = 0;
89+
uint256 len = _recipientsAndAmounts.length;
90+
for (uint256 i = 0; i < len; i++) {
91+
AccountAmount calldata accountAmount = _recipientsAndAmounts[i];
92+
uint256 amount = accountAmount.amount;
93+
_addStake(accountAmount.account, amount, _existingAccountsOnly);
94+
_total += amount;
95+
}
96+
if (_total == 0) {
97+
revert MustDistributeMoreThanZero();
98+
}
99+
_checksAndTransfer(_total);
100+
}
101+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Immutable Pty Ltd 2018 - 2025
2+
// SPDX-License-Identifier: Apache 2
3+
pragma solidity >=0.8.19 <0.8.29;
4+
5+
import {IERC20Upgradeable} from "openzeppelin-contracts-upgradeable-4.9.3/token/ERC20/IERC20Upgradeable.sol";
6+
import {SafeERC20Upgradeable} from "openzeppelin-contracts-upgradeable-4.9.3/token/ERC20/utils/SafeERC20Upgradeable.sol";
7+
import {IStakeHolder, StakeHolderBase, StakeHolderBaseV2} from "./StakeHolderBaseV2.sol";
8+
9+
/**
10+
* @title StakeHolderERC20V2: allows anyone to stake any amount of an ERC20 token and to then remove all or part of that stake.
11+
* @dev The StakeHolderERC20 contract is designed to be upgradeable.
12+
* @dev This contract is the same as StakeHolderERC20, with the exception that it derives from StakeHolderBaseV2.
13+
*/
14+
contract StakeHolderERC20V2 is StakeHolderBaseV2 {
15+
using SafeERC20Upgradeable for IERC20Upgradeable;
16+
17+
/// @notice The token used for staking.
18+
IERC20Upgradeable internal token;
19+
20+
/**
21+
* @notice Initialises the upgradeable contract, setting up admin accounts.
22+
* @param _roleAdmin the address to grant `DEFAULT_ADMIN_ROLE` to
23+
* @param _upgradeAdmin the address to grant `UPGRADE_ROLE` to
24+
* @param _distributeAdmin the address to grant `DISTRIBUTE_ROLE` to.
25+
* @param _token the token to use for staking.
26+
*/
27+
function initialize(
28+
address _roleAdmin,
29+
address _upgradeAdmin,
30+
address _distributeAdmin,
31+
address _token
32+
) public initializer {
33+
__StakeHolderERC20_init(_roleAdmin, _upgradeAdmin, _distributeAdmin, _token);
34+
}
35+
36+
function __StakeHolderERC20_init(
37+
address _roleAdmin,
38+
address _upgradeAdmin,
39+
address _distributeAdmin,
40+
address _token
41+
) internal onlyInitializing {
42+
__StakeHolderBase_init(_roleAdmin, _upgradeAdmin, _distributeAdmin);
43+
token = IERC20Upgradeable(_token);
44+
}
45+
46+
/**
47+
* @inheritdoc IStakeHolder
48+
*/
49+
function getToken() external view returns (address) {
50+
return address(token);
51+
}
52+
53+
/**
54+
* @inheritdoc StakeHolderBase
55+
*/
56+
function _sendValue(address _to, uint256 _amount) internal override {
57+
token.safeTransfer(_to, _amount);
58+
}
59+
60+
/**
61+
* @inheritdoc StakeHolderBase
62+
*/
63+
function _checksAndTransfer(uint256 _amount) internal override {
64+
if (msg.value != 0) {
65+
revert NonPayable();
66+
}
67+
token.safeTransferFrom(msg.sender, address(this), _amount);
68+
}
69+
70+
/// @notice storage gap for additional variables for upgrades
71+
// slither-disable-start unused-state
72+
// solhint-disable-next-line var-name-mixedcase
73+
uint256[50] private __StakeHolderERC20Gap;
74+
// slither-disable-end unused-state
75+
}

contracts/staking/StakeHolderNative.sol

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ import {IStakeHolder, StakeHolderBase} from "./StakeHolderBase.sol";
99
* @dev The StakeHolder contract is designed to be upgradeable.
1010
*/
1111
contract StakeHolderNative is StakeHolderBase {
12-
/// @notice Error: Unstake transfer failed.
13-
error UnstakeTransferFailed();
14-
1512
/**
1613
* @notice Initialises the upgradeable contract, setting up admin accounts.
1714
* @param _roleAdmin the address to grant `DEFAULT_ADMIN_ROLE` to
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Immutable Pty Ltd 2018 - 2025
2+
// SPDX-License-Identifier: Apache 2
3+
pragma solidity >=0.8.19 <0.8.29;
4+
5+
import {IStakeHolder, StakeHolderBase, StakeHolderBaseV2} from "./StakeHolderBaseV2.sol";
6+
7+
/**
8+
* @title StakeHolderNativeV2: allows anyone to stake any amount of native IMX and to then remove all or part of that stake.
9+
* @dev The StakeHolder contract is designed to be upgradeable.
10+
* @dev This contract is the same as StakeHolderNative, with the exception that it derives from StakeHolderBaseV2.
11+
*/
12+
contract StakeHolderNativeV2 is StakeHolderBaseV2 {
13+
/**
14+
* @notice Initialises the upgradeable contract, setting up admin accounts.
15+
* @param _roleAdmin the address to grant `DEFAULT_ADMIN_ROLE` to
16+
* @param _upgradeAdmin the address to grant `UPGRADE_ROLE` to
17+
* @param _distributeAdmin the address to grant `DISTRIBUTE_ROLE` to
18+
*/
19+
function initialize(address _roleAdmin, address _upgradeAdmin, address _distributeAdmin) public initializer {
20+
__StakeHolderBase_init(_roleAdmin, _upgradeAdmin, _distributeAdmin);
21+
}
22+
23+
/**
24+
* @inheritdoc IStakeHolder
25+
*/
26+
function getToken() external view virtual returns (address) {
27+
return address(0);
28+
}
29+
30+
/**
31+
* @inheritdoc StakeHolderBase
32+
*/
33+
function _sendValue(address _to, uint256 _amount) internal virtual override {
34+
// slither-disable-next-line low-level-calls,arbitrary-send-eth
35+
(bool success, bytes memory returndata) = payable(_to).call{value: _amount}("");
36+
if (!success) {
37+
// Look for revert reason and bubble it up if present.
38+
// Revert reasons should contain an error selector, which is four bytes long.
39+
if (returndata.length >= 4) {
40+
// solhint-disable-next-line no-inline-assembly
41+
assembly {
42+
let returndata_size := mload(returndata)
43+
revert(add(32, returndata), returndata_size)
44+
}
45+
} else {
46+
revert UnstakeTransferFailed();
47+
}
48+
}
49+
}
50+
51+
/**
52+
* @inheritdoc StakeHolderBase
53+
*/
54+
function _checksAndTransfer(uint256 _amount) internal virtual override {
55+
// Check that the amount matches the msg.value.
56+
if (_amount != msg.value) {
57+
revert MismatchMsgValueAmount(msg.value, _amount);
58+
}
59+
}
60+
61+
/// @notice storage gap for additional variables for upgrades
62+
// slither-disable-start unused-state
63+
// solhint-disable-next-line var-name-mixedcase
64+
uint256[50] private __StakeHolderNativeGap;
65+
// slither-disable-end unused-state
66+
}

0 commit comments

Comments
 (0)