# Staking contract ###### tags: `smart contract` `staking` [![built-with openzeppelin](https://img.shields.io/badge/built%20with-OpenZeppelin-3677FF)](https://docs.openzeppelin.com/) **Staking** provides a Scalable Reward Distribution option to all stakers. Users can deposit CenX tokens deployed on the Ethereum mainnet and receive a reward depending on the user share in total staking amount while tokens are locked in the contract. Staking gives users an additional choice for CenX token usage. There are no minimum deposit requirements and several withdrawal options available for users. Reward will be accrued depending on the user deposit percentage. Staking serves to reduce the overall amount of CenX in active circulation, provides options for CenX holders on Ethereum, and acts as a mechanism to limit available liquidity and supply. ## How to run ### Setup Clone the repo and then install dependencies: ``` $ npm i $ npm i -g npx ``` ### Testing To run the entire test suite: ``` $ npm run test ``` This will cause Mocha to stop immediately on the first failing test ```shell npm run test -- --bail ``` ### Compiling This will create `build/contracts` directory with contract's artifacts: ``` $ npm run compile ``` ### Deployment To run deployment in the local environment: ``` $ npm run migrate ``` To run deployment in rinkeby faucet ``` $ npm run migrate_rinkeby ``` ### Testing ``` $ npm run test ``` ### Linting ``` $ npm run solhint ``` ``` $ npm run solium ``` ## How it works Users can deposit CenX tokens to the contract and withdraw them along with accrued emission at any time. There are 2 types of withdrawal: 1. _Timed Withdrawal:_ user submits a withdrawal request, and after a specified time period, tokens and accrued emission may be withdrawn with no fee. 2. _Instant Withdrawal:_ user requests an instant withdrawal and pays a small fee to withdraw tokens and accrued emission. ## Scalable Reward Distribution ### Reward model We start by noting that absolute instants of the deposits, withdrawals and distribution events are not relevant, as the final outcome is only determined by the relative order of these events. Without loss of generality **we consider only one deposit** action per address and only full withdrawals. Additional deposits to an existing address can be modeled as two separate addresses while partial withdrawals can be modeled by two simpler operations: a full withdrawal followed by a new deposit. Let’s consider the chronological order of all the deposit, withdraw and distribute events. At a given instant t on this timeline, let Tt be the sum of all active stake deposits. On a distribute event for rewardt, a participant j with an associated stakej will get a reward of: ![](https://i.imgur.com/IBXyHEe.png) A simple implementation would iterate over all active stake deposits and increase their associated reward. But such a loop requires more gas per contract call as more deposits are created, making it a costly approach. A more efficient implementation is possible, in O(1) time. ### Factor out reward computation The total reward earned by a participant j for its stake deposit stake(j) is the sum of proportional rewards it extracted from all distribution events that occurred while the deposit was active: ![](https://i.imgur.com/I9B6Ops.png) where t iterates over all reward distribution events that occurred while stakej was active. Let’s note this sum, from the beginning of timeline until instant t: ![](https://i.imgur.com/YQft7ku.png) Assuming stake(j) is deposited at moment t1 and then withdrawn at moment t2 > t1, we can use the array St to compute the total reward for participant j: ![](https://i.imgur.com/tF6Juaj.png) This allows us to compute the reward for each withdraw event in constant time, at the expense of storing the entire S(t) array in the contract memory. ### Optimizing memory usage The memory usage can be further optimized by noting that St is monotonic and we can simply track the current (latest) value of S and snapshot this value only when we expect it to be required for a later computation. We will use a map S0[j] to save the value of S at the time the participant j makes a deposit. When j will later withdraw the stake, its total reward can be computed by using the latest value of S (at the time of the withdrawal) and the snapshot S0[j]: ![](https://i.imgur.com/1CpFFyd.png) This strategy makes it possible to achieve both time optimality and memory optimality, as for N participants it takes O(N) memory to keep track of both the S0 value map and the stake registry. As memory usage no longer depends on the number of distribute events, the algorithm is now suitable for very fine grained distribution: daily, hourly or even at every mined block. ### The algorithm will expose three methods: - deposit to add a new participant stake. - distribute to fan out reward to all participants. - withdraw to return the participant’s entire stake deposit plus the accumulated reward. ![](https://i.imgur.com/KSgYFFE.png) ### NOTES AND FUTURE WORK The stake and reward may be units of the same token (e.g. PoS pools) or they may be different tokens. In practice, most organizations will stake ERC20 tokens and distribute either ether rewards (dividends) or other tokens (utility or loyalty points). When different tokens are used, the withdrawal action will execute two different transfers, instead of returning deposited + reward. If the same token is used for quantifying both the stake and the reward, the algorithm will not compound the reward. An active user may effectively compound the reward by executing a withdraw followed by a new deposit. We will examine the possibility to automatically compound for all participants. ### Comments 1. When user deposits, user is assigned a personal reward factor `S` which exists for the current state. And the total amount of stakes is incremented 2. When owner distributes a reward, the personal reward factor `S` is recalculated according to formula: ``` S = S + r/T ``` where: S - Current reward factor r - Reward distributed at this instant of time T - Total staked amount from all stakeholders 3. When user withdraws his reward is calculated by formula: ``` reward = deposited * (S_curr - S_user) ``` where: deposited - amount deposited at this user account S_curr - current contract reward factor S_user - user reward factor assigned when user made deposit ### Making a deposit In order to make a deposit, a user can call `deposit(uint256 _amount)` function of the `Staking` contract. The contract will generate a unique ID of the new deposit and accept tokens. The `deposit(uint256 _amount)` function requires tokens to be approved by the user first (using `approve` ERC20 function of the CenX token). To replenish an existing deposit, the user can call `deposit(uint256 _depositId, uint256 _amount)` function specifying the ID of the existing deposit. In this case, the Staking contract will withdraw all staked amount with reward, add the specified `_amount` to the deposit, and reset the deposit's timestamp to the current one. But this time users reward coefficient will be updated in the proportion of personal deposit devided by the total staked amount which exists for the current moment. So this means that instead of replenishing the account it's more profitable to deposit to a new account. User can keep several accounts and each of this will be entitle it's personal proportion depending on the personal input in the staking. ### Making a timed withdrawal To withdraw tokens from the `Staking` contract without a fee, a user needs to submit a withdrawal request using `requestWithdrawal(uint256 _depositId)` function. After `withdrawalLockDuration` time has elapsed, the user must call `makeRequestedWithdrawal(uint256 _depositId, uint256 _amount)` within the withdrawal window defined in `withdrawalUnlockDuration`. If the user misses the withdrawal window time period, they can repeat the steps (calling `requestWithdrawal` again and then wait for the `withdrawalLockDuration` time before calling `makeRequestedWithdrawal`). The `_amount` parameter allows the user to define the amount of tokens to withdraw from their deposit. The balance can be obtained using the `balances(address _holder, uint256 _depositId)` public getter. The `_amount` can be passed as `0` which means the user wants to withdraw all of their tokens with accrued emission. When withdrawing a deposit (fully or partly) the user will receive the specified amount of tokens and accrued emission. ### Making an instant withdrawal To withdraw tokens from the `Staking` contract immediately, a user needs to call `makeForcedWithdrawal(uint256 _depositId, uint256 _amount)`. In this case, the fee will be subtracted from the deposit. ### Examples of making rewards There is one way that makes up the reward for user: - Personal (coefficient based) Reward distributed among owner and stakeholder in proportion set in the contract according to whitepaper, i.e. 25% of rewards during the first year are spend for stakeholders rewards. Remaing part of the income is coming to the owner wallet or liquidity provider address. Data for examples: 1. instant withdrawal fee: 3% 2. total staked: `1500000 CenX` **1st example:** User deposits `1000 tokens` then makes an instant withdrawal near this block. In this case, a fee will be `30 tokens`. User receives about `970 tokens` back. Remaining 30 tokens will be distributed as a reward. **2nd example:** User deposits `1000 tokens`. After `2 weeks` (14 days). Pesonal reward factor is equal 1000/1500000. _Note: The user could have instead chosen to create a 2nd deposit, which would have created a new deposit id and not reset the deposit date or generated accrued emissions for the initial 1000 token deposit._ - User receives `...`. - LP receives `...`. **3rd example** User deposits `1000 tokens`. Then they make a timed withdrawal for half after `6 months` (180 days). - User receives `521.37 tokens`, the new balance is `500 tokens` and the deposit date is not reset (that is, the personal APR remains equal to 7.35% and continues to grow). - On withdrawal, the LP receives `500 * (15-(7.35 + 1.32)) / 100 * 180 / 365 = 15.6 tokens`. ### Withdrawal Window When a user requests a timed withdrawal, they must wait to withdraw their tokens within a set window of time. There is a lock period (e.g., 12 hours) before they can withdraw, then there is a set withdrawal window during which they can execute their withdrawal (e.g., 12 hours as well). If a user requests a timed withdrawal but fails to execute within the allotted time, their CenX tokens are relocked into the contract. This does not update their deposit date. Tokens are relocked and accrue emission according to the initial deposit timestamp. ## Roles and methods available to each role ### Anyone 1. `deposit(uint256)` 2. `deposit(uint256,uint256)` 3. `requestWithdrawal(uint256)` 4. `makeRequestedWithdrawal(uint256,uint256)` 5. `makeForcedWithdrawal(uint256,uint256)` ### Owner The owner can change the contract parameters and claim unsupported tokens accidentally sent to the contract. For the `set*` functions listed below the changed parameter values will only take effect `7 days` after the function is called. 1. `setFee(uint256)` allows the owner to set a fee percentage for an instant withdrawal. 2% by default. 2. `setWithdrawalLockDuration(uint256)` allows the owner to change time period from the withdrawal request after which a timed withdrawal is available. 12 hours by default. Cannot exceed 30 days. 3. `setWithdrawalUnlockDuration(uint256)` allows the owner to change time period during which a timed withdrawal is available from the moment of unlocking. 12 hours by default. Cannot be less than 1 hour. 4. `transferOwnership(address)` allows the owner to transfer the ownership to another address. 5. `renounceOwnership()` allows the owner to resign forever. 6. `ownerWithdraw()` transfers all owner balance into the owner token address 7. `setRewardMaturityDuration(uint256)` allows to set reward maturity duration. It means that 100% of the owner rewards will be available only after some period of time defined by this parameter. Default is 2 weeks. This can help to mitigate the case when user can stake some amount to get immediate reward. 8. `setRewardSharePercent(uint256)` allows to setup a share percent by which users get 25% during first year with following decreasing specifed in whitepaper. ### Proxy Admin The Proxy Admin can upgrade the contract logic. This role was abolished by calling `renounceOwnership` in the ProxyAdmin contract. *`Note: All methods are described in code.`* ## Security Audit ### Issues found at previous audit and measures taken to remove those issues > Tokens are minted both on staking deposit and withdrawal, however, they should be minted only once. **Solution**: this was working as designed to mint the reward and deposited amount. In fact, this solution implied that there were 2 tokens: Main Token and Staking Token. The solution of reward calculation was replaced by more simple algorithm and the Staking is not a token anymore. > Anyone can change governanceStatus for the governance smart contract. It may cause harm to the voting process. **Solution**: Fixed. > require(proposal.proposalStatus == ProposalStatus.Active, "Proposal must be active to make votes"); in unlockTokens and calculateVotes functions should be changed to other status, because it should be done for proposal not in active state, or error message should be rewritten **Solution**: Fixed. > Lock and unlock functionality are view functions that don't change any data. They will not lock/unlock tokens for governance. Lock fully duplicates isEligable functionality. **Solution:** Replaced by `totalUserBalance(uint256)` function > It’s recommended to have an event emit in _mint function of staking contract **Solution:** Implemeted event for each signnificant state mutation > It’s recommended to have events in unlockTokens and calculateVotes functions. **Solution:** Fixed. > Owner can overwrite existing proposals via addProposal function. It’s recommended to handle proposal ids in smart contract, not to receive them as a parameter **Solution:** All ID for proposals will be generated offchain. > votesKeyArray.push(proposalId); in vote function is not needed because it was pushed during addProposal. **Solution:** Removed. And the whole algorithm of voting and vote calculation was simplified. ## Updated from the last audit review ### Reward accrual algorithm - It was replaced by compound algorithms described in the attached document - Sigmoid function was removed ### Added events to log every state mutation as per audit recommendation ### Updated the reward calculation algorithm #### Parameters required for this to work: - _rewardMaturityDuration: - _rewardSharePercent: reward distribution ### Removed liquidity providers functionality ### Removed claimTokens function ### Added function to withdraw to owner account all the accrued reward ### Future improvement 1. Complience with https://eips.ethereum.org/EIPS/eip-900 of staking contract 2. Complience if main token with ERC677 3. Currently, if user stakes right before the reward, in a week he can withdraw the reward. Some users may have 1 month staked before getting reward. To make this scheme more fair it needs defining coefficient pegged on time elapse since last deposit 4. Consider making the Staking to be ERC20 token to allow to sell it's tokens 5. Consider making the CenXToken inherit from Capped classes. ### Audit #### Storage Gaps https://docs.openzeppelin.com/contracts/3.x/upgradeable#storage_gaps #### dangerous strict equality ```solidity Governance.vote(uint256) (Governance.sol:340-364) uses a dangerous strict equality: • require(bool,string)(proposals[_proposalId].proposalStatus == ProposalStatus.Active,vote:Proposal must be active to vote for it) (Governance.sol#351) ``` Doesn't have anything to do with the balance checking https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities