This project is a smart contract system built for secure token staking and reward management on the Ethereum blockchain. It provides essential components for handling staking operations, managing rewards, and ensuring the security of staked assets. The system is designed to be robust, leveraging mathematical precision for reward calculations and secure vaults for asset protection.
To install the dependencies for this project, run the following command:
pnpm install
To install the system on the blockchain, follow these steps:
StakeManager
contract.VaultManager
contract on any chain (this can be on a development network or a testnet).VaultManager
is deployed, retrieve its codehash and configure it in the StakeManager
using the setTrustedCodehash(bytes32, bool)
function.VaultFactory
contract can be optionally used to optimize gas costs during deployments and to facilitate the creation of valid vaults.To stake tokens, the StakeVault
contract interacts with the StakeManager
. You can call the function StakeVault.stake(uint256 _amount, uint256 _secondsToLock)
with the desired amount and lock duration. Before staking, it is required to call approve
on the StakeToken
to authorize the StakeVault
address to manage your tokens.
The minimum _secondsToLock
is defined by the contract settings, and the minimum _amount
required to stake is set to ensure it generates Multiplier Points in every epoch.
Tokens are never deposited directly or transferred to the StakeManager
. Additionally, tokens should not be sent directly to the StakeVault
address. Always use the approve
method followed by the stake
function to properly stake tokens.
Note that staking will automatically process epochs. Refer to the Manually Processing Epochs section for more information.tion.
Rewards are claimed directly in the StakeManager
contract. To claim them, call StakeManager.executeAccount(address _vault)
and pass the address of the StakeVault
. If many epochs have passed without executeAccount
being called, the function may become expensive. In such cases, use StakeManager.executeAccount(address _vault, uint256 _limitEpoch)
with a limit on the epoch to process rewards in smaller transactions, allowing continuation in later transactions.
Both functions process rewards similarly, but the latter allows incremental processing. Rewards are sent exclusively to the owner of the StakeVault
.
Any address can execute the claim of rewards on behalf of the vault owner.
Note that claiming rewards will automatically process epochs. Refer to the Manually Processing Epochs section for more information.
To unstake tokens, users call StakeVault.unstake(uint256 amount)
. Unstaking reduces the user’s balance on the StakeManager
in proportion to the staked amount and time spent staking. Users can only unstake if their balance is not locked.
Note that unstaking will automatically process epochs. Refer to the Manually Processing Epochs section for more information.
The owner of the StakeManager
can initiate migration at any time. To do this, deploy a new StakeManager
, passing the address of the old StakeManager
in the constructor. Then, call the active StakeManager.startMigration(newStakeManager)
. Users with locked balances can either withdraw their funds or continue under the new contract specifications.
Rewards reserved for past epochs will remain in the old contract, and users can claim them before opting in or out of the migration. Only rewards that have not yet been reserved to any epoch will be transferred to the new StakeManager
.
Note that starting migration will automatically process epochs. Refer to the Manually Processing Epochs section for more information.
Users can accept or reject the new contract by calling StakeVault.acceptMigration()
or StakeVault.leave()
. If users have pending rewards in the old contract, those rewards will be claimed before opting in or out. Users with locked balances will have the option to leave, even if their balance is still locked.
Note that opting in or out of migration will automatically claim rewards. Refer to the Claiming Rewards section for more information.
Epoch processing is required for any contract operation. If the contract becomes obsolete and users stop interacting with it, epoch processing may become expensive to perform in a single transaction. Although processing epochs is cheaper than processing accounts, this has been considered.
The functions StakeManager.executeEpoch()
and StakeManager.executeEpoch(uint256 _limitEpoch)
can be called to manually process epochs before calling executeAccount
.
Note that StakeManager.executeAccount(address _vault, uint256 _limitEpoch)
will internally call StakeManager.executeEpoch(uint256 _limitEpoch)
with the same _limitEpoch
. You can check the latest epoch that can be processed by calling the view function StakeManager.newEpoch()
. Functions without the _limitEpoch
parameter internally use StakeManager.newEpoch()
as the limit, so calls like stakeManager.executeAccount(userVault)
and stakeManager.executeEpoch()
are equivalent to stakeManager.executeAccount(userVault, stakeManager.newEpoch())
and stakeManager.executeEpoch(stakeManager.newEpoch())
.
This is a list of the most frequently needed commands.
Build the contracts:
$ forge build
Delete the build artifacts and cache directories:
$ forge clean
Compile the contracts:
$ forge build
Get a test coverage report:
$ forge coverage
Deploy to Anvil:
$ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545
For this script to work, you need to have a MNEMONIC
environment variable set to a valid
BIP39 mnemonic.
For instructions on how to deploy to a testnet or mainnet, check out the Solidity Scripting tutorial.
Format the contracts:
$ forge fmt
Get a gas report:
pnpm gas-report
Get a gas snapshot:
forge snapshot
Lint the contracts:
$ pnpm lint
Run the tests:
$ forge test
Formats, generates snapshot and gas report:
pnpm adorno
This project is licensed under MIT.