-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Staking code, deployment scripts, tests, and thread model (#251)
Adds: * Staking code * Deployment scripts * Tests * Readme.md documentation * Thread model * Test plan
- Loading branch information
1 parent
249a3be
commit f4eb3e3
Showing
11 changed files
with
1,232 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
# Stake Holder Threat Model | ||
|
||
## Introduction | ||
|
||
This threat model document for the [Stake Holder](../../contracts/staking/README.md) contract has been created in preparation for external audit. | ||
|
||
## Rationale | ||
|
||
Immutable operates a system whereby people can place native IMX in a holding contract, do some actions (which are outside of the scope of this threat model), and then are paid a reward. The people, known as stakers, have full custody of their tokens they place in the holding contract; they can withdraw deposited IMX at any time. Anyone can choose to distribute rewards to stakers at any time. | ||
|
||
## Threat Model Scope | ||
|
||
The threat model is limited to the following Solidity file at GitHash [`fd982abc49884af41e05f18349b13edc9eefbc1e`](https://github.com/immutable/contracts/blob/fd982abc49884af41e05f18349b13edc9eefbc1e/contracts/staking/README.md): | ||
|
||
* [StakeHolder.sol](https://github.com/immutable/contracts/blob/fd982abc49884af41e05f18349b13edc9eefbc1e/contracts/staking/StakeHolder.sol) | ||
|
||
## Background | ||
|
||
See the [README](https://github.com/immutable/contracts/blob/fd982abc49884af41e05f18349b13edc9eefbc1e/contracts/staking/README.md) file for information about the usage and design of the `StakeHolder` contract. | ||
|
||
### Other Information | ||
|
||
This section provides links to test plans and test code. | ||
|
||
#### Test Plans and Test Code | ||
|
||
The test plan is available here: [Test Plan](../test/staking/README.md). The test code is contained in the same directory at the test plan. | ||
|
||
#### Continuous Integration | ||
|
||
Each time a commit is pushed to a pull request, the [continuous integration loop executes](https://github.com/immutable/contracts/actions). | ||
|
||
#### Building, Testing, Coverage and Static Code Analysis | ||
|
||
For instructions on building the code, running tests, coverage, and Slither, see the [BUILD.md](https://github.com/immutable/contracts/blob/main/BUILD.md). | ||
|
||
## Attack Surfaces | ||
|
||
The following sections list attack surfaces evaluated as part of this threat modelling exercise. | ||
|
||
### Externally Visible Functions | ||
|
||
An attacker could formulate an attack in which they send one or more transactions that execute one or more of the externally visible functions. | ||
|
||
The list of functions and their function selectors was determined by the following command. The additional information was obtained by reviewing the code. | ||
|
||
``` | ||
forge inspect StakeHolder --pretty methods | ||
``` | ||
|
||
Functions that *change* state: | ||
|
||
| Name | Function Selector | Access Control | | ||
| --------------------------------------- | ----------------- | ------------------- | | ||
| `distributeRewards((address,uint256)[])`| 00cfb539 | Permissionless | | ||
| `grantRole(bytes32,address)` | 2f2ff15d | Role admin | | ||
| `initialize(address,address)` | 485cc955 | Can only be called once during deployment | | ||
| `renounceRole(bytes32,address)` | 36568abe | `msg.sender` | | ||
| `revokeRole(bytes32,address)` | d547741f | Role admin | | ||
| `stake()` | 3a4b66f1 | Operations based on msg.sender | | ||
| `unstake(uint256)` | 2e17de78 | Operations based on msg.sender | | ||
| `upgradeStorage(bytes)` | ffd0016f | Can only be called once during upgrade | | ||
| `upgradeTo(address)` | 3659cfe6 | Upgrade role only | | ||
| `upgradeToAndCall(address,bytes)` | 4f1ef286 | Upgrade role only | | ||
|
||
|
||
Functions that *do not change* state: | ||
|
||
| Name | Function Selector | | ||
| -------------------------------- | ----------------- | | ||
| `DEFAULT_ADMIN_ROLE()` | a217fddf | | ||
| `UPGRADE_ROLE()` | b908afa8 | | ||
| `getBalance(address)` | f8b2cb4f | | ||
| `getNumStakers()` | bc788d46 | | ||
| `getRoleAdmin(bytes32)` | 248a9ca3 | | ||
| `getRoleMember(bytes32,uint256)` | 9010d07c | | ||
| `getRoleMemberCount(bytes32)` | ca15c873 | | ||
| `getStakers(uint256,uint256)` | ad71bd36 | | ||
| `hasRole(bytes32,address)` | 91d14854 | | ||
| `hasStaked(address)` | c93c8f34 | | ||
| `proxiableUUID()` | 52d1902d | | ||
| `supportsInterface(bytes4)` | 01ffc9a7 | | ||
| `version()` | 54fd4d50 | | ||
|
||
|
||
### Admin Roles | ||
|
||
Accounts with administrative privileges could be used by attackers to facilitate attacks. This section analyses what each role can do. | ||
|
||
#### Accounts with `DEFAULT_ADMIN` role on StakeHolder contract | ||
|
||
This role is granted to the `roleAdmin` specified in the `initialize` function of the contract. Accounts with the `DEFAULT_ADMIN` account can: | ||
|
||
* Grant administrator roles to any account, including the `DEFAULT_ADMIN` role | ||
* Revoke administrator roles from any account, including the `DEFAULT_ADMIN` role | ||
* The `DEFAULT_ADMIN` role cannot be revoked from an account if it the only account with the `DEFAULT_ADMIN` role | ||
* Renounce the `DEFAULT_ADMIN` role for itself, unless it is the only account with the `DEFAULT_ADMIN` role | ||
|
||
Exploiting this attack surface requires compromising an account with `DEFAULT_ADMIN` role. | ||
|
||
#### Accounts with `UPGRADE` role on StakeHolder contract | ||
|
||
An account with `UPGRADE` role can: | ||
|
||
* Upgrade the implementation contract. | ||
|
||
Exploiting this attack surface requires compromising an account with `UPGRADE` role. | ||
|
||
|
||
### Upgrade and Storage Slots | ||
|
||
The table was constructed by using the command described below, and analysing the source code. | ||
|
||
``` | ||
forge inspect StakeHolder --pretty storage | ||
``` | ||
|
||
| Name | Type | Slot | Offset | Bytes | Source File | | ||
| --------------------------------- | -------------------------------------------------------------- | ---- | ------ | ----- | ----------- | | ||
| \_initialized | uint8 | 0 | 0 | 1 | OpenZeppelin Contracts v4.9.3: proxy/utils/Initializable.sol | | ||
| \_initializing | bool | 0 | 1 | 1 | OpenZeppelin Contracts v4.9.3: proxy/utils/Initializable.sol | | ||
| \_\_gap | uint256[50] | 1 | 0 | 1600 | OpenZeppelin Contracts v4.9.3: utils/Context.sol | | ||
| \_\_gap | uint256[50] | 51 | 0 | 1600 | OpenZeppelin Contracts v4.9.3: utils/introspection/ERC165.sol | | ||
| \_roles | mapping(bytes32 => struct AccessControlUpgradeable.RoleData) | 101 | 0 | 32 | OpenZeppelin Contracts v4.9.3: access/AccessControlUpgradeable.sol | | ||
| \_\_gap | uint256[49] | 102 | 0 | 1568 | OpenZeppelin Contracts v4.9.3: access/AccessControlUpgradeable.sol | | ||
| \_roleMembers | mapping(bytes32 => struct EnumerableSetUpgradeable.AddressSet) | 151 | 0 | 32 | OpenZeppelin Contracts v4.9.3: access/AccessControlEnumerableUpgradeable.sol | | ||
| \_\_gap | uint256[49] | 152 | 0 | 1568 | OpenZeppelin Contracts v4.9.3: access/AccessControlEnumerableUpgradeable.sol | | ||
| \_\_gap | uint256[50] | 201 | 0 | 1600 | OpenZeppelin Contracts v4.9.3: proxy/ERC1967/ERC1967Upgrade.sol | | ||
| \_\_gap | uint256[50] | 251 | 0 | 1600 | OpenZeppelin Contracts v4.9.3: proxy/utils/UUPSUpgradeable.sol | | ||
| balances | mapping(address => struct StakeHolder.StakeInfo) | 301 | 0 | 32 | StakeHolder.sol | | ||
| stakers | address[] | 302 | 0 | 32 | StakeHolder.sol | | ||
| version | uint256 | 303 | 0 | 32 | StakeHolder.sol | | ||
| \_\_StakeHolderGap | uint256[50] | 304 | 0 | 640 | StakeHolder.sol | | ||
|
||
|
||
## Perceived Attackers | ||
|
||
This section lists the attackers that could attack the `StakeHolder` contract. | ||
|
||
It is assumed that all attackers have access to all documentation and source code of all systems related to the Immutable zkEVM, irrespective of whether the information resides in a public or private GitHub repository, email, Slack, Confluence, or any other information system. | ||
|
||
### Spear Phisher | ||
|
||
This attacker compromises accounts of people by using Spear Phishing attacks. For example they send a malicious PDF file to a user, which the user opens, the PDF file then installs malware on the user's computer. At this point, it is assumed that the Spear Phisher Attacker can detect all key strokes, mouse clicks, see all information retrieved, see any file in the user's file system, and execute any program on the user's computer. | ||
|
||
### Immutable zkEVM Block Proposer | ||
|
||
An operator of an Immutable zkEVM Block Proposer could, within narrow limits, alter the block timestamp of the block they produce. | ||
|
||
### Insider | ||
|
||
This attacker works for a company helping operate the Immutable zkEVM. This attacker could be being bribed or blackmailed. They can access the keys that they as an individual employee have access to. For instance, they might be one of the signers of the multi-signer administrative role. | ||
|
||
### General Public | ||
|
||
This attacker targets the public API of the `StakeHolder` contract. | ||
|
||
## Attack Mitigation | ||
|
||
This section outlines possible attacks against the attack surfaces by the attackers, and how those attacks are mitigated. | ||
|
||
### Public API Attack | ||
|
||
**Detection**: Staker funds are stolen. | ||
|
||
An attacker could target the public API in an attempt to steal funds. As shown in the `Externally Visible Functions` section, all functions that update state are protected by access control methods (`grantRole`, `revokeRole`, `upgradeTo`, `upgradeToAndCall`), operate on value owned by msg.sender (`distributeRewards`, `stake`, `unstake`), operate on state related to msg.sender (`renounceRole`), or are protected by state machine logic (`initialize`, `upgradeStorage`). As such, there is no mechanism by which an attacker could attack the contract using the public API. | ||
|
||
|
||
### `DEFAULT_ADMIN` Role Account Compromise | ||
|
||
**Detection**: Monitoring role change events. | ||
|
||
The mitigation is to assume that the role will be operated by multi-signature addresses such that an attacker would need to compromise multiple signers simultaneously. As such, even if some keys are compromised due to the Spear Phishing Attacker or the Insider Attacker, the administrative actions will not be able to be executed as a threshold number of keys will not be available. | ||
|
||
### `UPGRADE` Role Account Compromise | ||
|
||
**Detection**: Monitoring upgrade events. | ||
|
||
The mitigation is to assume that the role will be operated by multi-signature addresses such that an attacker would need to compromise multiple signers simultaneously. As such, even if some keys are compromised due to the Spear Phishing Attacker or the Insider Attacker, the administrative actions will not be able to be executed as a threshold number of keys will not be available. | ||
|
||
### Immutable zkEVM Block Proposer Censoring Transactions | ||
|
||
**Detection**: A staker could attempt to unstake some or all of their IMX. The block proposer could refuse to include this transaction. | ||
|
||
The mitigation for this attack is that Immutable zkEVM Block Proposers software is written such that no transactions are censored unless the transaction has been signed by an account on [OFAC's Sanctions List](https://ofac.treasury.gov/sanctions-list-service). | ||
|
||
|
||
## Conclusion | ||
|
||
This threat model has presented the architecture of the system, determined attack surfaces, and identified possible attackers and their capabilities. It has walked through each attack surface and based on the attackers, determined how the attacks are mitigated. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# Staking | ||
|
||
The Immutable zkEVM staking system consists of the Staking Holder contract. This contract holds staked native IMX. Any account (EOA or contract) can stake any amount 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. | ||
|
||
## Immutable Contract Addresses | ||
|
||
| Environment/Network | Deployment Address | Commit Hash | | ||
|--------------------------|--------------------|-------------| | ||
| Immutable zkEVM Testnet | Not deployed yet | -| | ||
| Immutable zkEVM Mainnet | Not deployed yet | -| | ||
|
||
# Status | ||
|
||
Contract threat models and audits: | ||
|
||
| Description | Date |Version Audited | Link to Report | | ||
|---------------------------|------------------|-----------------|----------------| | ||
| Threat model | Oct 21, 2024 | [`fd982abc49884af41e05f18349b13edc9eefbc1e`](https://github.com/immutable/contracts/blob/fd982abc49884af41e05f18349b13edc9eefbc1e/contracts/staking/README.md) | [202410-threat-model-stake-holder.md](../../audits/staking/202410-threat-model-stake-holder.md) | | ||
|
||
|
||
|
||
# Deployment | ||
|
||
**Deploy and verify using CREATE3 factory contract:** | ||
|
||
This repo includes a script for deploying via a CREATE3 factory contract. The script is defined as a test contract as per the examples [here](https://book.getfoundry.sh/reference/forge/forge-script#examples) and can be found in `./script/staking/DeployStakeHolder.sol`. | ||
|
||
See the `.env.example` for required environment variables. | ||
|
||
```sh | ||
forge script script/stake/DeployStakeHolder.sol --tc DeployStakeHolder --sig "deploy()" -vvv --rpc-url {rpc-url} --broadcast --verifier-url https://explorer.immutable.com/api --verifier blockscout --verify --gas-price 10gwei | ||
``` | ||
|
||
Optionally, you can also specify `--ledger` or `--trezor` for hardware deployments. See docs [here](https://book.getfoundry.sh/reference/forge/forge-script#wallet-options---hardware-wallet). | ||
|
||
|
||
# Usage | ||
|
||
To stake, any account should call `stake()`, passing in the amount to be staked as the msg.value. | ||
|
||
To unstake, the account that previously staked should call, `unstake(uint256 _amountToUnstake)`. | ||
|
||
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. | ||
|
||
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: | ||
|
||
* `getNumStakers() returns (uint256 _len)`: Return the length of the stakers array. This is the number of accounts that ever staked using the contract. | ||
* `getStakers(uint256 _startOffset, uint256 _numberToReturn) returns (address[] memory _stakers)`: Return all or a subset of the stakers array. The stakers array never changes order. As such, off-chain systems can cache previous results. | ||
* `getBalance(address _account) returns (uint256 _balance)`: Return the amount staked by an account. | ||
* `hasStaked(address _account) returns (bool _everStaked)`: Returns true if the account has ever staked. | ||
|
||
# Administration Notes | ||
|
||
The `StakeHolder` contract is `AccessControlEnumerableUpgradeable`, with the following minor modification: | ||
|
||
* `_revokeRole(bytes32 _role, address _account)` has been overridden to prevent the last DEFAULT_ADMIN_ROLE (the last role admin) from either being revoked or renounced. | ||
|
||
The `StakeHolder` contract is `UUPSUpgradeable`. Only accounts with `UPGRADE_ROLE` are authorised to upgrade the contract. | ||
|
||
## Upgrade Concept | ||
|
||
The `upgradeStorage` function should be updated each new contract version. It should do the following: | ||
|
||
* Check the value returned by `version`. This is the version of the code and associated storage format from the previous version prior to the upgrade. Three alternatives are possible: | ||
* The value is less than the new version: Upgrade as per the bullet points below. | ||
* The value is higher than the new version: This probably indicates that an attempt to upgrade the contract has mistakenly downgraded the contract to a previous version. The function should revert. | ||
* The value is the same as the newt version: Someone (an attacker) has called the `upgradeStorage` function after the code has been upgraded. The function should revert. | ||
* Based on the old code version and storage format indicated by the `version`, update the storage variables. Typically, upgrades only involve code changes, and require no storage variable changes. However, in some circumstances storage variables should also be updated. | ||
* Update the `version` storage variable to indicate the new code version. |
Oops, something went wrong.