From f8dd268a55ceb4df9fb9019b6dd9990a1728716f Mon Sep 17 00:00:00 2001 From: Alex Scott <5690430+alsco77@users.noreply.github.com> Date: Tue, 14 Sep 2021 14:59:28 +0100 Subject: [PATCH] Staking tests (#228) Staking tests Co-authored-by: Nicholas Addison --- .../governance/staking/GamifiedToken.sol | 6 +- contracts/governance/staking/QuestManager.sol | 17 +- contracts/governance/staking/StakedToken.sol | 15 +- .../governance/staking/StakedTokenBPT.sol | 25 +- contracts/legacy/v-BUSD.sol | 1994 --------------- contracts/legacy/v-GUSD.sol | 1990 --------------- contracts/legacy/v-HBTC.sol | 1994 --------------- contracts/legacy/v-TBTC.sol | 1994 --------------- contracts/legacy/v-alUSD.sol | 2139 ----------------- contracts/legacy/v-mBTC.sol | 1994 --------------- contracts/legacy/v-mUSD.sol | 1861 -------------- contracts/rewards/BoostDirector.sol | 2 +- contracts/rewards/BoostDirectorV2.sol | 2 +- contracts/rewards/staking/StakingRewards.sol | 2 +- .../StakingRewardsWithPlatformToken.sol | 2 +- .../@openzeppelin/InstantProxyAdmin.sol | 7 + contracts/z_mocks/governance/MockBPT.sol | 16 + contracts/z_mocks/governance/MockBVault.sol | 100 + .../MockRewardsDistributionRecipient.sol | 2 +- .../z_mocks/shared/IMStableVoterProxy.sol | 14 + hardhat.config.ts | 31 +- tasks-fork.config.ts | 1 - tasks.config.ts | 1 - tasks/deployLegacy.ts | 246 -- tasks/deployRewards.ts | 177 +- tasks/utils/deploy-utils.ts | 5 +- tasks/utils/networkAddressFactory.ts | 22 +- tasks/utils/rewardsUtils.ts | 210 ++ tasks/utils/tokens.ts | 20 +- .../feeders/feeders-musd-alchemix.spec.ts | 2 +- .../governance/staked-token-deploy.spec.ts | 604 +++++ test-utils/machines/standardAccounts.ts | 9 + .../staking/boost-director-v2.spec.ts | 487 ++++ .../staking/signature-verifier.spec.ts | 0 .../staking/staked-token-bpt.spec.ts | 336 ++- .../staking/staked-token-mta.spec.ts | 10 +- test/governance/staking/staked-token.spec.ts | 1890 +++++++++++---- types/index.ts | 1 + types/stakedToken.ts | 17 +- 39 files changed, 3213 insertions(+), 15032 deletions(-) delete mode 100644 contracts/legacy/v-BUSD.sol delete mode 100644 contracts/legacy/v-GUSD.sol delete mode 100644 contracts/legacy/v-HBTC.sol delete mode 100644 contracts/legacy/v-TBTC.sol delete mode 100644 contracts/legacy/v-alUSD.sol delete mode 100644 contracts/legacy/v-mBTC.sol delete mode 100644 contracts/legacy/v-mUSD.sol create mode 100644 contracts/shared/@openzeppelin/InstantProxyAdmin.sol create mode 100644 contracts/z_mocks/governance/MockBPT.sol create mode 100644 contracts/z_mocks/governance/MockBVault.sol create mode 100644 contracts/z_mocks/shared/IMStableVoterProxy.sol delete mode 100644 tasks/deployLegacy.ts create mode 100644 tasks/utils/rewardsUtils.ts create mode 100644 test-fork/governance/staked-token-deploy.spec.ts create mode 100644 test/governance/staking/boost-director-v2.spec.ts delete mode 100644 test/governance/staking/signature-verifier.spec.ts diff --git a/contracts/governance/staking/GamifiedToken.sol b/contracts/governance/staking/GamifiedToken.sol index 39813aeb..463b509b 100644 --- a/contracts/governance/staking/GamifiedToken.sol +++ b/contracts/governance/staking/GamifiedToken.sol @@ -52,6 +52,8 @@ abstract contract GamifiedToken is /** * @param _nexus System nexus * @param _rewardsToken Token that is being distributed as a reward. eg MTA + * @param _questManager Centralised manager of quests + * @param _hasPriceCoeff true if raw staked amount is multiplied by price coeff to get staked amount. eg BPT Staked Token */ constructor( address _nexus, @@ -248,7 +250,7 @@ abstract contract GamifiedToken is Balance memory _oldBalance, uint256 _oldScaledBalance, uint8 _newMultiplier - ) internal updateReward(_account) { + ) private updateReward(_account) { // 1. Set the questMultiplier _balances[_account].questMultiplier = _newMultiplier; @@ -551,5 +553,5 @@ abstract contract GamifiedToken is return string(bytesArray); } - uint256[45] private __gap; + uint256[46] private __gap; } diff --git a/contracts/governance/staking/QuestManager.sol b/contracts/governance/staking/QuestManager.sol index 417578ae..c107eb2f 100644 --- a/contracts/governance/staking/QuestManager.sol +++ b/contracts/governance/staking/QuestManager.sol @@ -28,6 +28,8 @@ contract QuestManager is IQuestManager, Initializable, ContextUpgradeable, Immut Quest[] private _quests; /// @notice Timestamp at which the current season started uint32 public override seasonEpoch; + /// @notice Timestamp at which the contract was created + uint32 public startTime; /// @notice A whitelisted questMaster who can administer quests including signing user quests are completed. address public override questMaster; @@ -47,7 +49,7 @@ contract QuestManager is IQuestManager, Initializable, ContextUpgradeable, Immut * @param _questSignerArg account that can sign user quests as completed */ function initialize(address _questMaster, address _questSignerArg) external initializer { - seasonEpoch = SafeCast.toUint32(block.timestamp); + startTime = SafeCast.toUint32(block.timestamp); questMaster = _questMaster; _questSigner = _questSignerArg; } @@ -118,6 +120,8 @@ contract QuestManager is IQuestManager, Initializable, ContextUpgradeable, Immut * @dev Adds a new stakedToken */ function addStakedToken(address _stakedToken) external override onlyGovernor { + require(_stakedToken != address(0), "Invalid StakedToken"); + _stakedTokens.push(_stakedToken); emit StakedTokenAdded(_stakedToken); @@ -185,6 +189,7 @@ contract QuestManager is IQuestManager, Initializable, ContextUpgradeable, Immut * A new season can only begin after 9 months has passed. */ function startNewQuestSeason() external override questMasterOrGovernor { + require(block.timestamp > (startTime + 39 weeks), "First season has not elapsed"); require(block.timestamp > (seasonEpoch + 39 weeks), "Season has not elapsed"); uint256 len = _quests.length; @@ -220,14 +225,14 @@ contract QuestManager is IQuestManager, Initializable, ContextUpgradeable, Immut bytes calldata _signature ) external override { uint256 len = _ids.length; - require(len > 0, "No quest ids"); + require(len > 0, "No quest IDs"); uint8 questMultiplier = checkForSeasonFinish(_account); // For each quest for (uint256 i = 0; i < len; i++) { - require(_validQuest(_ids[i]), "Err: Invalid Quest"); - require(!hasCompleted(_account, _ids[i]), "Err: Already Completed"); + require(_validQuest(_ids[i]), "Invalid Quest ID"); + require(!hasCompleted(_account, _ids[i]), "Quest already completed"); require( SignatureVerifier.verify(_questSigner, _account, _ids, _signature), "Invalid Quest Signer Signature" @@ -295,8 +300,8 @@ contract QuestManager is IQuestManager, Initializable, ContextUpgradeable, Immut questMultiplier += quest.multiplier; uint256 len2 = _stakedTokens.length; - for (uint256 i = 0; i < len2; i++) { - IStakedToken(_stakedTokens[i]).applyQuestMultiplier(_accounts[i], questMultiplier); + for (uint256 j = 0; j < len2; j++) { + IStakedToken(_stakedTokens[j]).applyQuestMultiplier(_accounts[j], questMultiplier); } } diff --git a/contracts/governance/staking/StakedToken.sol b/contracts/governance/staking/StakedToken.sol index f5a1216a..5aa8d63f 100644 --- a/contracts/governance/staking/StakedToken.sol +++ b/contracts/governance/staking/StakedToken.sol @@ -70,6 +70,7 @@ contract StakedToken is GamifiedVotingToken { * @param _stakedToken Core token that is staked and tracked (e.g. MTA) * @param _cooldownSeconds Seconds a user must wait after she initiates her cooldown before withdrawal is possible * @param _unstakeWindow Window in which it is possible to withdraw, following the cooldown period + * @param _hasPriceCoeff true if raw staked amount is multiplied by price coeff to get staked amount. eg BPT Staked Token */ constructor( address _nexus, @@ -205,8 +206,9 @@ contract StakedToken is GamifiedVotingToken { Balance memory oldBalance = _balances[_msgSender()]; // If we have missed the unstake window, or the user has chosen to exit the cooldown, // then reset the timestamp to 0 - bool exitCooldown = _exitCooldown || - block.timestamp > (oldBalance.cooldownTimestamp + COOLDOWN_SECONDS + UNSTAKE_WINDOW); + bool exitCooldown = _exitCooldown || ( + oldBalance.cooldownTimestamp > 0 && + block.timestamp > (oldBalance.cooldownTimestamp + COOLDOWN_SECONDS + UNSTAKE_WINDOW) ); if (exitCooldown) { emit CooldownExited(_msgSender()); } @@ -431,15 +433,6 @@ contract StakedToken is GamifiedVotingToken { _transferAndStake(_value, address(0), false); } - /** - * @dev Does nothing, because there is no lockup here. - **/ - function increaseLockLength( - uint256 /* _unlockTime */ - ) external virtual { - return; - } - /** * @dev Backwards compatibility. Previously a lock would run out and a user would call this. Now, it will take 2 calls * to exit in order to leave. The first will initiate the cooldown period, and the second will execute a full withdrawal. diff --git a/contracts/governance/staking/StakedTokenBPT.sol b/contracts/governance/staking/StakedTokenBPT.sol index 264c7fdb..aa7b2b9d 100644 --- a/contracts/governance/staking/StakedTokenBPT.sol +++ b/contracts/governance/staking/StakedTokenBPT.sol @@ -43,6 +43,7 @@ contract StakedTokenBPT is StakedToken { event BalClaimed(); event BalRecipientChanged(address newRecipient); event PriceCoefficientUpdated(uint256 newPriceCoeff); + event FeesConverted(uint256 bpt, uint256 mta); /*************************************** INIT @@ -144,18 +145,15 @@ contract StakedTokenBPT is StakedToken { // 1. Sell the BPT uint256 stakingBalBefore = STAKED_TOKEN.balanceOf(address(this)); uint256 mtaBalBefore = REWARDS_TOKEN.balanceOf(address(this)); - (address[] memory tokens, uint256[] memory balances, ) = balancerVault.getPoolTokens( - poolId - ); + (address[] memory tokens, , ) = balancerVault.getPoolTokens(poolId); require(tokens[0] == address(REWARDS_TOKEN), "MTA in wrong place"); - // 1.1. Calculate minimum output amount, assuming bpt 80/20 gives ~4% max slippage - uint256[] memory minOut = new uint256[](1); - address[] memory exitToken = new address[](1); + // 1.1. Calculate minimum output amount + uint256[] memory minOut = new uint256[](2); { - uint256 unitsPerToken = (balances[0] * 12e17) / STAKED_TOKEN.totalSupply(); - minOut[0] = (pendingBPT * unitsPerToken) / 1e18; - exitToken[0] = address(REWARDS_TOKEN); + // 10% discount from the latest pcoeff + // e.g. 1e18 * 42000 / 11000 = 3.81e18 + minOut[0] = (pendingBPT * priceCoefficient) / 11000; } // 1.2. Exits to here, from here. Assumes token is in position 0 @@ -163,7 +161,7 @@ contract StakedTokenBPT is StakedToken { poolId, address(this), payable(address(this)), - ExitPoolRequest(exitToken, minOut, bytes(abi.encode(0, pendingBPT - 1, 0)), false) + ExitPoolRequest(tokens, minOut, bytes(abi.encode(0, pendingBPT - 1, 0)), false) ); // 2. Verify and update state @@ -174,8 +172,11 @@ contract StakedTokenBPT is StakedToken { ); // 3. Inform HeadlessRewards about the new rewards - uint256 mtaBalAfter = REWARDS_TOKEN.balanceOf(address(this)); - pendingAdditionalReward += (mtaBalAfter - mtaBalBefore); + uint256 received = REWARDS_TOKEN.balanceOf(address(this)) - mtaBalBefore; + require(received >= minOut[0], "Must receive tokens"); + super._notifyAdditionalReward(received); + + emit FeesConverted(pendingBPT, received); } /** diff --git a/contracts/legacy/v-BUSD.sol b/contracts/legacy/v-BUSD.sol deleted file mode 100644 index bb539ba4..00000000 --- a/contracts/legacy/v-BUSD.sol +++ /dev/null @@ -1,1994 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.2; - -interface IERC20 { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); -} - -interface IBoostedVaultWithLockup { - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external; - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) external; - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external; - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external; - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) external; - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external; - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() external view returns (uint256); - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() external view returns (uint256); - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) external view returns (uint256); - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last - ); -} - -contract ModuleKeys { - // Governance - // =========== - // keccak256("Governance"); - bytes32 internal constant KEY_GOVERNANCE = - 0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d; - //keccak256("Staking"); - bytes32 internal constant KEY_STAKING = - 0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034; - //keccak256("ProxyAdmin"); - bytes32 internal constant KEY_PROXY_ADMIN = - 0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1; - - // mStable - // ======= - // keccak256("OracleHub"); - bytes32 internal constant KEY_ORACLE_HUB = - 0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040; - // keccak256("Manager"); - bytes32 internal constant KEY_MANAGER = - 0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f; - //keccak256("Recollateraliser"); - bytes32 internal constant KEY_RECOLLATERALISER = - 0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f; - //keccak256("MetaToken"); - bytes32 internal constant KEY_META_TOKEN = - 0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2; - // keccak256("SavingsManager"); - bytes32 internal constant KEY_SAVINGS_MANAGER = - 0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1; - // keccak256("Liquidator"); - bytes32 internal constant KEY_LIQUIDATOR = - 0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4; - // keccak256("InterestValidator"); - bytes32 internal constant KEY_INTEREST_VALIDATOR = - 0xc10a28f028c7f7282a03c90608e38a4a646e136e614e4b07d119280c5f7f839f; -} - -interface INexus { - function governor() external view returns (address); - - function getModule(bytes32 key) external view returns (address); - - function proposeModule(bytes32 _key, address _addr) external; - - function cancelProposedModule(bytes32 _key) external; - - function acceptProposedModule(bytes32 _key) external; - - function acceptProposedModules(bytes32[] calldata _keys) external; - - function requestLockModule(bytes32 _key) external; - - function cancelLockModule(bytes32 _key) external; - - function lockModule(bytes32 _key) external; -} - -abstract contract ImmutableModule is ModuleKeys { - INexus public immutable nexus; - - /** - * @dev Initialization function for upgradable proxy contracts - * @param _nexus Nexus contract address - */ - constructor(address _nexus) { - require(_nexus != address(0), "Nexus address is zero"); - nexus = INexus(_nexus); - } - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - _onlyGovernor(); - _; - } - - function _onlyGovernor() internal view { - require(msg.sender == _governor(), "Only governor can execute"); - } - - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - - /** - * @dev Modifier to allow function calls only from the ProxyAdmin. - */ - modifier onlyProxyAdmin() { - require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Manager. - */ - modifier onlyManager() { - require(msg.sender == _manager(), "Only manager can execute"); - _; - } - - /** - * @dev Returns Governor address from the Nexus - * @return Address of Governor Contract - */ - function _governor() internal view returns (address) { - return nexus.governor(); - } - - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - - /** - * @dev Return Staking Module address from the Nexus - * @return Address of the Staking Module contract - */ - function _staking() internal view returns (address) { - return nexus.getModule(KEY_STAKING); - } - - /** - * @dev Return ProxyAdmin Module address from the Nexus - * @return Address of the ProxyAdmin Module contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } - - /** - * @dev Return MetaToken Module address from the Nexus - * @return Address of the MetaToken Module contract - */ - function _metaToken() internal view returns (address) { - return nexus.getModule(KEY_META_TOKEN); - } - - /** - * @dev Return OracleHub Module address from the Nexus - * @return Address of the OracleHub Module contract - */ - function _oracleHub() internal view returns (address) { - return nexus.getModule(KEY_ORACLE_HUB); - } - - /** - * @dev Return Manager Module address from the Nexus - * @return Address of the Manager Module contract - */ - function _manager() internal view returns (address) { - return nexus.getModule(KEY_MANAGER); - } - - /** - * @dev Return SavingsManager Module address from the Nexus - * @return Address of the SavingsManager Module contract - */ - function _savingsManager() internal view returns (address) { - return nexus.getModule(KEY_SAVINGS_MANAGER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _recollateraliser() internal view returns (address) { - return nexus.getModule(KEY_RECOLLATERALISER); - } -} - -interface IRewardsDistributionRecipient { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); -} - -abstract contract InitializableRewardsDistributionRecipient is - IRewardsDistributionRecipient, - ImmutableModule -{ - // This address has the ability to distribute the rewards - address public rewardsDistributor; - - constructor(address _nexus) ImmutableModule(_nexus) {} - - /** @dev Recipient is a module, governed by mStable governance */ - function _initialize(address _rewardsDistributor) internal { - rewardsDistributor = _rewardsDistributor; - } - - /** - * @dev Only the rewards distributor can notify about rewards - */ - modifier onlyRewardsDistributor() { - require(msg.sender == rewardsDistributor, "Caller is not reward distributor"); - _; - } - - /** - * @dev Change the rewardsDistributor - only called by mStable governor - * @param _rewardsDistributor Address of the new distributor - */ - function setRewardsDistribution(address _rewardsDistributor) external onlyGovernor { - rewardsDistributor = _rewardsDistributor; - } -} - -interface IBoostDirector { - function getBalance(address _user) external returns (uint256); - - function setDirection( - address _old, - address _new, - bool _pokeNew - ) external; - - function whitelistVaults(address[] calldata _vaults) external; -} - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. - */ - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { - size := extcodesize(account) - } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return - functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) - internal - view - returns (bytes memory) - { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) - internal - returns (bytes memory) - { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) private pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} - -library SafeERC20 { - using Address for address; - - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { - _callOptionalReturn( - token, - abi.encodeWithSelector(token.transferFrom.selector, from, to, value) - ); - } - - /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. - */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function _callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that - // the target address contains contract code and also asserts for success in the low-level call. - - bytes memory returndata = address(token).functionCall( - data, - "SafeERC20: low-level call failed" - ); - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} - -contract InitializableReentrancyGuard { - bool private _notEntered; - - function _initializeReentrancyGuard() internal { - // Storing an initial non-zero value makes deployment a bit more - // expensive, but in exchange the refund on every call to nonReentrant - // will be lower in amount. Since refunds are capped to a percetange of - // the total transaction's gas, it is best to keep them low in cases - // like this one, to increase the likelihood of the full refund coming - // into effect. - _notEntered = true; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_notEntered, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _notEntered = false; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _notEntered = true; - } -} - -library StableMath { - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @dev Token Ratios are used when converting between units of bAsset, mAsset and MTA - * Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold) - * bAsset ratio unit for use in exact calculations, - * where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit - */ - uint256 private constant RATIO_SCALE = 1e8; - - /** - * @dev Provides an interface to the scaling unit - * @return Scaling unit (1e18 or 1 * 10**18) - */ - function getFullScale() internal pure returns (uint256) { - return FULL_SCALE; - } - - /** - * @dev Provides an interface to the ratio unit - * @return Ratio scale unit (1e8 or 1 * 10**8) - */ - function getRatioScale() internal pure returns (uint256) { - return RATIO_SCALE; - } - - /** - * @dev Scales a given integer to the power of the full scale. - * @param x Simple uint256 to scale - * @return Scaled value a to an exact number - */ - function scaleInteger(uint256 x) internal pure returns (uint256) { - return x * FULL_SCALE; - } - - /*************************************** - PRECISE ARITHMETIC - ****************************************/ - - /** - * @dev Multiplies two precise units, and then truncates by the full scale - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) { - return mulTruncateScale(x, y, FULL_SCALE); - } - - /** - * @dev Multiplies two precise units, and then truncates by the given scale. For example, - * when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18 - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @param scale Scale unit - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncateScale( - uint256 x, - uint256 y, - uint256 scale - ) internal pure returns (uint256) { - // e.g. assume scale = fullScale - // z = 10e18 * 9e17 = 9e36 - // return 9e38 / 1e18 = 9e18 - return (x * y) / scale; - } - - /** - * @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit, rounded up to the closest base unit. - */ - function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e17 * 17268172638 = 138145381104e17 - uint256 scaled = x * y; - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled + FULL_SCALE - 1; - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil / FULL_SCALE; - } - - /** - * @dev Precisely divides two units, by first scaling the left hand operand. Useful - * for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17) - * @param x Left hand input to division - * @param y Right hand input to division - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e18 * 1e18 = 8e36 - // e.g. 8e36 / 10e18 = 8e17 - return (x * FULL_SCALE) / y; - } - - /*************************************** - RATIO FUNCS - ****************************************/ - - /** - * @dev Multiplies and truncates a token ratio, essentially flooring the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand operand to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return c Result after multiplying the two inputs and then dividing by the ratio scale - */ - function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - return mulTruncateScale(x, ratio, RATIO_SCALE); - } - - /** - * @dev Multiplies and truncates a token ratio, rounding up the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand input to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the shared - * ratio scale, rounded up to the closest base unit. - */ - function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) { - // e.g. How much mAsset should I burn for this bAsset (x)? - // 1e18 * 1e8 = 1e26 - uint256 scaled = x * ratio; - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled + RATIO_SCALE - 1; - // return 100..00.999e8 / 1e8 = 1e18 - return ceil / RATIO_SCALE; - } - - /** - * @dev Precisely divides two ratioed units, by first scaling the left hand operand - * i.e. How much bAsset is this mAsset worth? - * @param x Left hand operand in division - * @param ratio bAsset ratio - * @return c Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - // e.g. 1e14 * 1e8 = 1e22 - // return 1e22 / 1e12 = 1e10 - return (x * RATIO_SCALE) / ratio; - } - - /*************************************** - HELPERS - ****************************************/ - - /** - * @dev Calculates minimum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Minimum of the two inputs - */ - function min(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? y : x; - } - - /** - * @dev Calculated maximum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Maximum of the two inputs - */ - function max(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? x : y; - } - - /** - * @dev Clamps a value to an upper bound - * @param x Left hand input - * @param upperBound Maximum possible value to return - * @return Input x clamped to a maximum value, upperBound - */ - function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) { - return x > upperBound ? upperBound : x; - } -} - -library Root { - /** - * @dev Returns the square root of a given number - * @param x Input - * @return y Square root of Input - */ - function sqrt(uint256 x) internal pure returns (uint256 y) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; // Seven iterations should be enough - uint256 r1 = x / r; - return uint256(r < r1 ? r : r1); - } - } -} - -contract BoostedTokenWrapper is InitializableReentrancyGuard { - using StableMath for uint256; - using SafeERC20 for IERC20; - - event Transfer(address indexed from, address indexed to, uint256 value); - - string private _name; - string private _symbol; - - IERC20 public immutable stakingToken; - IBoostDirector public immutable boostDirector; - - uint256 private _totalBoostedSupply; - mapping(address => uint256) private _boostedBalances; - mapping(address => uint256) private _rawBalances; - - // Vars for use in the boost calculations - uint256 private constant MIN_DEPOSIT = 1e18; - uint256 private constant MAX_VMTA = 600000e18; - uint256 private constant MAX_BOOST = 3e18; - uint256 private constant MIN_BOOST = 1e18; - uint256 private constant FLOOR = 98e16; - uint256 public immutable boostCoeff; // scaled by 10 - uint256 public immutable priceCoeff; - - /** - * @dev TokenWrapper constructor - * @param _stakingToken Wrapped token to be staked - * @param _boostDirector vMTA boost director - * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 - */ - constructor( - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _boostCoeff - ) { - stakingToken = IERC20(_stakingToken); - boostDirector = IBoostDirector(_boostDirector); - priceCoeff = _priceCoeff; - boostCoeff = _boostCoeff; - } - - function _initialize(string memory _nameArg, string memory _symbolArg) internal { - _initializeReentrancyGuard(); - _name = _nameArg; - _symbol = _symbolArg; - } - - function name() public view virtual returns (string memory) { - return _name; - } - - function symbol() public view virtual returns (string memory) { - return _symbol; - } - - function decimals() public view virtual returns (uint8) { - return 18; - } - - /** - * @dev Get the total boosted amount - * @return uint256 total supply - */ - function totalSupply() public view returns (uint256) { - return _totalBoostedSupply; - } - - /** - * @dev Get the boosted balance of a given account - * @param _account User for which to retrieve balance - */ - function balanceOf(address _account) public view returns (uint256) { - return _boostedBalances[_account]; - } - - /** - * @dev Get the RAW balance of a given account - * @param _account User for which to retrieve balance - */ - function rawBalanceOf(address _account) public view returns (uint256) { - return _rawBalances[_account]; - } - - /** - * @dev Read the boost for the given address - * @param _account User for which to return the boost - * @return boost where 1x == 1e18 - */ - function getBoost(address _account) public view returns (uint256) { - return balanceOf(_account).divPrecisely(rawBalanceOf(_account)); - } - - /** - * @dev Deposits a given amount of StakingToken from sender - * @param _amount Units of StakingToken - */ - function _stakeRaw(address _beneficiary, uint256 _amount) internal nonReentrant { - _rawBalances[_beneficiary] += _amount; - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - } - - /** - * @dev Withdraws a given stake from sender - * @param _amount Units of StakingToken - */ - function _withdrawRaw(uint256 _amount) internal nonReentrant { - _rawBalances[msg.sender] -= _amount; - stakingToken.safeTransfer(msg.sender, _amount); - } - - /** - * @dev Updates the boost for the given address according to the formula - * boost = min(0.5 + c * vMTA_balance / imUSD_locked^(7/8), 1.5) - * If rawBalance <= MIN_DEPOSIT, boost is 0 - * @param _account User for which to update the boost - */ - function _setBoost(address _account) internal { - uint256 rawBalance = _rawBalances[_account]; - uint256 boostedBalance = _boostedBalances[_account]; - uint256 boost = MIN_BOOST; - - // Check whether balance is sufficient - // is_boosted is used to minimize gas usage - uint256 scaledBalance = (rawBalance * priceCoeff) / 1e18; - if (scaledBalance >= MIN_DEPOSIT) { - uint256 votingWeight = boostDirector.getBalance(_account); - boost = _computeBoost(scaledBalance, votingWeight); - } - - uint256 newBoostedBalance = rawBalance.mulTruncate(boost); - - if (newBoostedBalance != boostedBalance) { - _totalBoostedSupply = _totalBoostedSupply - boostedBalance + newBoostedBalance; - _boostedBalances[_account] = newBoostedBalance; - - if (newBoostedBalance > boostedBalance) { - emit Transfer(address(0), _account, newBoostedBalance - boostedBalance); - } else { - emit Transfer(_account, address(0), boostedBalance - newBoostedBalance); - } - } - } - - /** - * @dev Computes the boost for - * boost = min(m, max(1, 0.95 + c * min(voting_weight, f) / deposit^(3/4))) - * @param _scaledDeposit deposit amount in terms of USD - */ - function _computeBoost(uint256 _scaledDeposit, uint256 _votingWeight) - private - view - returns (uint256 boost) - { - if (_votingWeight == 0) return MIN_BOOST; - - // Compute balance to the power 3/4 - uint256 sqrt1 = Root.sqrt(_scaledDeposit * 1e6); - uint256 sqrt2 = Root.sqrt(sqrt1); - uint256 denominator = sqrt1 * sqrt2; - boost = - (((StableMath.min(_votingWeight, MAX_VMTA) * boostCoeff) / 10) * 1e18) / - denominator; - boost = StableMath.min(MAX_BOOST, StableMath.max(MIN_BOOST, FLOOR + boost)); - } -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library SafeCast { - /** - * @dev Returns the downcasted uint128 from uint256, reverting on - * overflow (when the input is greater than largest uint128). - * - * Counterpart to Solidity's `uint128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - */ - function toUint128(uint256 value) internal pure returns (uint128) { - require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); - return uint128(value); - } - - /** - * @dev Returns the downcasted uint64 from uint256, reverting on - * overflow (when the input is greater than largest uint64). - * - * Counterpart to Solidity's `uint64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - */ - function toUint64(uint256 value) internal pure returns (uint64) { - require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); - return uint64(value); - } - - /** - * @dev Returns the downcasted uint32 from uint256, reverting on - * overflow (when the input is greater than largest uint32). - * - * Counterpart to Solidity's `uint32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - */ - function toUint32(uint256 value) internal pure returns (uint32) { - require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); - return uint32(value); - } - - /** - * @dev Returns the downcasted uint16 from uint256, reverting on - * overflow (when the input is greater than largest uint16). - * - * Counterpart to Solidity's `uint16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - */ - function toUint16(uint256 value) internal pure returns (uint16) { - require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); - return uint16(value); - } - - /** - * @dev Returns the downcasted uint8 from uint256, reverting on - * overflow (when the input is greater than largest uint8). - * - * Counterpart to Solidity's `uint8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - */ - function toUint8(uint256 value) internal pure returns (uint8) { - require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); - return uint8(value); - } - - /** - * @dev Converts a signed int256 into an unsigned uint256. - * - * Requirements: - * - * - input must be greater than or equal to 0. - */ - function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); - return uint256(value); - } - - /** - * @dev Returns the downcasted int128 from int256, reverting on - * overflow (when the input is less than smallest int128 or - * greater than largest int128). - * - * Counterpart to Solidity's `int128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - * - * _Available since v3.1._ - */ - function toInt128(int256 value) internal pure returns (int128) { - require(value >= -2**127 && value < 2**127, "SafeCast: value doesn't fit in 128 bits"); - return int128(value); - } - - /** - * @dev Returns the downcasted int64 from int256, reverting on - * overflow (when the input is less than smallest int64 or - * greater than largest int64). - * - * Counterpart to Solidity's `int64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - * - * _Available since v3.1._ - */ - function toInt64(int256 value) internal pure returns (int64) { - require(value >= -2**63 && value < 2**63, "SafeCast: value doesn't fit in 64 bits"); - return int64(value); - } - - /** - * @dev Returns the downcasted int32 from int256, reverting on - * overflow (when the input is less than smallest int32 or - * greater than largest int32). - * - * Counterpart to Solidity's `int32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - * - * _Available since v3.1._ - */ - function toInt32(int256 value) internal pure returns (int32) { - require(value >= -2**31 && value < 2**31, "SafeCast: value doesn't fit in 32 bits"); - return int32(value); - } - - /** - * @dev Returns the downcasted int16 from int256, reverting on - * overflow (when the input is less than smallest int16 or - * greater than largest int16). - * - * Counterpart to Solidity's `int16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - * - * _Available since v3.1._ - */ - function toInt16(int256 value) internal pure returns (int16) { - require(value >= -2**15 && value < 2**15, "SafeCast: value doesn't fit in 16 bits"); - return int16(value); - } - - /** - * @dev Returns the downcasted int8 from int256, reverting on - * overflow (when the input is less than smallest int8 or - * greater than largest int8). - * - * Counterpart to Solidity's `int8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - * - * _Available since v3.1._ - */ - function toInt8(int256 value) internal pure returns (int8) { - require(value >= -2**7 && value < 2**7, "SafeCast: value doesn't fit in 8 bits"); - return int8(value); - } - - /** - * @dev Converts an unsigned uint256 into a signed int256. - * - * Requirements: - * - * - input must be less than or equal to maxInt256. - */ - function toInt256(uint256 value) internal pure returns (int256) { - require(value < 2**255, "SafeCast: value doesn't fit in an int256"); - return int256(value); - } -} - -// Internal -// Libs -/** - * @title BoostedSavingsVault - * @author mStable - * @notice Accrues rewards second by second, based on a users boosted balance - * @dev Forked from rewards/staking/StakingRewards.sol - * Changes: - * - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months) - * - `updateBoost` hook called after every external action to reset a users boost - * - Struct packing of common data - * - Searching for and claiming of unlocked rewards - */ -contract BoostedSavingsVault is - IBoostedVaultWithLockup, - Initializable, - InitializableRewardsDistributionRecipient, - BoostedTokenWrapper -{ - using SafeERC20 for IERC20; - using StableMath for uint256; - using SafeCast for uint256; - - event RewardAdded(uint256 reward); - event Staked(address indexed user, uint256 amount, address payer); - event Withdrawn(address indexed user, uint256 amount); - event Poked(address indexed user); - event RewardPaid(address indexed user, uint256 reward); - - IERC20 public immutable rewardsToken; - - uint64 public constant DURATION = 7 days; - // Length of token lockup, after rewards are earned - uint256 public constant LOCKUP = 26 weeks; - // Percentage of earned tokens unlocked immediately - uint64 public constant UNLOCK = 33e16; - - // Timestamp for current period finish - uint256 public periodFinish; - // RewardRate for the rest of the PERIOD - uint256 public rewardRate; - // Last time any user took action - uint256 public lastUpdateTime; - // Ever increasing rewardPerToken rate, based on % of total supply - uint256 public rewardPerTokenStored; - mapping(address => UserData) public userData; - // Locked reward tracking - mapping(address => Reward[]) public userRewards; - mapping(address => uint64) public userClaim; - - struct UserData { - uint128 rewardPerTokenPaid; - uint128 rewards; - uint64 lastAction; - uint64 rewardCount; - } - - struct Reward { - uint64 start; - uint64 finish; - uint128 rate; - } - - constructor( - address _nexus, - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _coeff, - address _rewardsToken - ) - InitializableRewardsDistributionRecipient(_nexus) - BoostedTokenWrapper(_stakingToken, _boostDirector, _priceCoeff, _coeff) - { - rewardsToken = IERC20(_rewardsToken); - } - - /** - * @dev StakingRewards is a TokenWrapper and RewardRecipient - * Constants added to bytecode at deployTime to reduce SLOAD cost - */ - function initialize( - address _rewardsDistributor, - string calldata _nameArg, - string calldata _symbolArg - ) external initializer { - InitializableRewardsDistributionRecipient._initialize(_rewardsDistributor); - BoostedTokenWrapper._initialize(_nameArg, _symbolArg); - } - - /** - * @dev Updates the reward for a given address, before executing function. - * Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to - * (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed - * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). - */ - modifier updateReward(address _account) { - uint256 currentTime = block.timestamp; - uint64 currentTime64 = SafeCast.toUint64(currentTime); - - // Setting of global vars - (uint256 newRewardPerToken, uint256 lastApplicableTime) = _rewardPerToken(); - // If statement protects against loss in initialisation case - if (newRewardPerToken > 0) { - rewardPerTokenStored = newRewardPerToken; - lastUpdateTime = lastApplicableTime; - - // Setting of personal vars based on new globals - if (_account != address(0)) { - UserData memory data = userData[_account]; - uint256 earned_ = _earned(_account, data.rewardPerTokenPaid, newRewardPerToken); - - // If earned == 0, then it must either be the initial stake, or an action in the - // same block, since new rewards unlock after each block. - if (earned_ > 0) { - uint256 unlocked = earned_.mulTruncate(UNLOCK); - uint256 locked = earned_ - unlocked; - - userRewards[_account].push( - Reward({ - start: SafeCast.toUint64(LOCKUP + data.lastAction), - finish: SafeCast.toUint64(LOCKUP + currentTime), - rate: SafeCast.toUint128(locked / (currentTime - data.lastAction)) - }) - ); - - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: SafeCast.toUint128(unlocked + data.rewards), - lastAction: currentTime64, - rewardCount: data.rewardCount + 1 - }); - } else { - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: data.rewards, - lastAction: currentTime64, - rewardCount: data.rewardCount - }); - } - } - } else if (_account != address(0)) { - // This should only be hit once, for first staker in initialisation case - userData[_account].lastAction = currentTime64; - } - _; - } - - /** @dev Updates the boost for a given address, after the rest of the function has executed */ - modifier updateBoost(address _account) { - _; - _setBoost(_account); - } - - /*************************************** - ACTIONS - EXTERNAL - ****************************************/ - - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _stake(msg.sender, _amount); - } - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) - external - override - updateReward(_beneficiary) - updateBoost(_beneficiary) - { - _stake(_beneficiary, _amount); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external override updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(rawBalanceOf(msg.sender)); - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - _claimRewards(first, last); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(rawBalanceOf(msg.sender)); - _claimRewards(_first, _last); - } - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(_amount); - } - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external override updateReward(msg.sender) updateBoost(msg.sender) { - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - if (unlocked > 0) { - rewardsToken.safeTransfer(msg.sender, unlocked); - emit RewardPaid(msg.sender, unlocked); - } - } - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external override updateReward(msg.sender) updateBoost(msg.sender) { - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - - _claimRewards(first, last); - } - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _claimRewards(_first, _last); - } - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) - external - override - updateReward(_account) - updateBoost(_account) - { - emit Poked(_account); - } - - /*************************************** - ACTIONS - INTERNAL - ****************************************/ - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function _claimRewards(uint256 _first, uint256 _last) internal { - (uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last); - userClaim[msg.sender] = uint64(lastTimestamp); - - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - uint256 total = unclaimed + unlocked; - - if (total > 0) { - rewardsToken.safeTransfer(msg.sender, total); - - emit RewardPaid(msg.sender, total); - } - } - - /** - * @dev Internally stakes an amount by depositing from sender, - * and crediting to the specified beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function _stake(address _beneficiary, uint256 _amount) internal { - require(_amount > 0, "Cannot stake 0"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - _stakeRaw(_beneficiary, _amount); - emit Staked(_beneficiary, _amount, msg.sender); - } - - /** - * @dev Withdraws raw units from the sender - * @param _amount Units of StakingToken - */ - function _withdraw(uint256 _amount) internal { - require(_amount > 0, "Cannot withdraw 0"); - _withdrawRaw(_amount); - emit Withdrawn(msg.sender, _amount); - } - - /*************************************** - GETTERS - ****************************************/ - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() external view override returns (IERC20) { - return rewardsToken; - } - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() public view override returns (uint256) { - return StableMath.min(block.timestamp, periodFinish); - } - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() public view override returns (uint256) { - (uint256 rewardPerToken_, ) = _rewardPerToken(); - return rewardPerToken_; - } - - function _rewardPerToken() - internal - view - returns (uint256 rewardPerToken_, uint256 lastTimeRewardApplicable_) - { - uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD - uint256 timeDelta = lastApplicableTime - lastUpdateTime; // + 1 SLOAD - // If this has been called twice in the same block, shortcircuit to reduce gas - if (timeDelta == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units to distribute = rewardRate * timeSinceLastUpdate - uint256 rewardUnitsToDistribute = rewardRate * timeDelta; // + 1 SLOAD - uint256 supply = totalSupply(); // + 1 SLOAD - // If there is no StakingToken liquidity, avoid div(0) - // If there is nothing to distribute, short circuit - if (supply == 0 || rewardUnitsToDistribute == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens - uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(supply); - // return summed rate - return (rewardPerTokenStored + unitsToDistributePerToken, lastApplicableTime); // + 1 SLOAD - } - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) public view override returns (uint256) { - uint256 newEarned = _earned( - _account, - userData[_account].rewardPerTokenPaid, - rewardPerToken() - ); - uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK); - return immediatelyUnlocked + userData[_account].rewards; - } - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - override - returns ( - uint256 amount, - uint256 first, - uint256 last - ) - { - (first, last) = _unclaimedEpochs(_account); - (uint256 unlocked, ) = _unclaimedRewards(_account, first, last); - amount = unlocked + earned(_account); - } - - /** @dev Returns only the most recently earned rewards */ - function _earned( - address _account, - uint256 _userRewardPerTokenPaid, - uint256 _currentRewardPerToken - ) internal view returns (uint256) { - // current rate per token - rate user previously received - uint256 userRewardDelta = _currentRewardPerToken - _userRewardPerTokenPaid; // + 1 SLOAD - // Short circuit if there is nothing new to distribute - if (userRewardDelta == 0) { - return 0; - } - // new reward = staked tokens * difference in rate - uint256 userNewReward = balanceOf(_account).mulTruncate(userRewardDelta); // + 1 SLOAD - // add to previous rewards - return userNewReward; - } - - /** - * @dev Gets the first and last indexes of array elements containing unclaimed rewards - */ - function _unclaimedEpochs(address _account) - internal - view - returns (uint256 first, uint256 last) - { - uint64 lastClaim = userClaim[_account]; - - uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account); - uint256 lastUnclaimed = _findLastUnclaimed(_account); - - return (firstUnclaimed, lastUnclaimed); - } - - /** - * @dev Sums the cumulative rewards from a valid range - */ - function _unclaimedRewards( - address _account, - uint256 _first, - uint256 _last - ) internal view returns (uint256 amount, uint256 latestTimestamp) { - uint256 currentTime = block.timestamp; - uint64 lastClaim = userClaim[_account]; - - // Check for no rewards unlocked - uint256 totalLen = userRewards[_account].length; - if (_first == 0 && _last == 0) { - if (totalLen == 0 || currentTime <= userRewards[_account][0].start) { - return (0, currentTime); - } - } - // If there are previous unlocks, check for claims that would leave them untouchable - if (_first > 0) { - require( - lastClaim >= userRewards[_account][_first - 1].finish, - "Invalid _first arg: Must claim earlier entries" - ); - } - - uint256 count = _last - _first + 1; - for (uint256 i = 0; i < count; i++) { - uint256 id = _first + i; - Reward memory rwd = userRewards[_account][id]; - - require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch"); - - uint256 endTime = StableMath.min(rwd.finish, currentTime); - uint256 startTime = StableMath.max(rwd.start, lastClaim); - uint256 unclaimed = (endTime - startTime) * rwd.rate; - - amount += unclaimed; - } - - // Calculate last relevant timestamp here to allow users to avoid issue of OOG errors - // by claiming rewards in batches. - latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish); - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findFirstUnclaimed(uint64 _lastClaim, address _account) - internal - view - returns (uint256 first) - { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (_lastClaim > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findLastUnclaimed(address _account) internal view returns (uint256 first) { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (block.timestamp > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /*************************************** - ADMIN - ****************************************/ - - /** - * @dev Notifies the contract that new rewards have been added. - * Calculates an updated rewardRate based on the rewards in period. - * @param _reward Units of RewardToken that have been added to the pool - */ - function notifyRewardAmount(uint256 _reward) - external - override - onlyRewardsDistributor - updateReward(address(0)) - { - require(_reward < 1e24, "Cannot notify with more than a million units"); - - uint256 currentTime = block.timestamp; - // If previous period over, reset rewardRate - if (currentTime >= periodFinish) { - rewardRate = _reward / DURATION; - } - // If additional reward to existing period, calc sum - else { - uint256 remaining = periodFinish - currentTime; - uint256 leftover = remaining * rewardRate; - rewardRate = (_reward + leftover) / DURATION; - } - - lastUpdateTime = currentTime; - periodFinish = currentTime + DURATION; - - emit RewardAdded(_reward); - } -} diff --git a/contracts/legacy/v-GUSD.sol b/contracts/legacy/v-GUSD.sol deleted file mode 100644 index 41e04515..00000000 --- a/contracts/legacy/v-GUSD.sol +++ /dev/null @@ -1,1990 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.2; - -interface IERC20 { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); -} - -interface IBoostedVaultWithLockup { - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external; - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) external; - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external; - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external; - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) external; - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external; - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() external view returns (uint256); - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() external view returns (uint256); - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) external view returns (uint256); - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last - ); -} - -contract ModuleKeys { - // Governance - // =========== - // keccak256("Governance"); - bytes32 internal constant KEY_GOVERNANCE = - 0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d; - //keccak256("Staking"); - bytes32 internal constant KEY_STAKING = - 0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034; - //keccak256("ProxyAdmin"); - bytes32 internal constant KEY_PROXY_ADMIN = - 0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1; - - // mStable - // ======= - // keccak256("OracleHub"); - bytes32 internal constant KEY_ORACLE_HUB = - 0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040; - // keccak256("Manager"); - bytes32 internal constant KEY_MANAGER = - 0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f; - //keccak256("Recollateraliser"); - bytes32 internal constant KEY_RECOLLATERALISER = - 0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f; - //keccak256("MetaToken"); - bytes32 internal constant KEY_META_TOKEN = - 0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2; - // keccak256("SavingsManager"); - bytes32 internal constant KEY_SAVINGS_MANAGER = - 0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1; - // keccak256("Liquidator"); - bytes32 internal constant KEY_LIQUIDATOR = - 0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4; - // keccak256("InterestValidator"); - bytes32 internal constant KEY_INTEREST_VALIDATOR = - 0xc10a28f028c7f7282a03c90608e38a4a646e136e614e4b07d119280c5f7f839f; -} - -interface INexus { - function governor() external view returns (address); - - function getModule(bytes32 key) external view returns (address); - - function proposeModule(bytes32 _key, address _addr) external; - - function cancelProposedModule(bytes32 _key) external; - - function acceptProposedModule(bytes32 _key) external; - - function acceptProposedModules(bytes32[] calldata _keys) external; - - function requestLockModule(bytes32 _key) external; - - function cancelLockModule(bytes32 _key) external; - - function lockModule(bytes32 _key) external; -} - -abstract contract ImmutableModule is ModuleKeys { - INexus public immutable nexus; - - /** - * @dev Initialization function for upgradable proxy contracts - * @param _nexus Nexus contract address - */ - constructor(address _nexus) { - require(_nexus != address(0), "Nexus address is zero"); - nexus = INexus(_nexus); - } - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - _onlyGovernor(); - _; - } - - function _onlyGovernor() internal view { - require(msg.sender == _governor(), "Only governor can execute"); - } - - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - - /** - * @dev Modifier to allow function calls only from the ProxyAdmin. - */ - modifier onlyProxyAdmin() { - require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Manager. - */ - modifier onlyManager() { - require(msg.sender == _manager(), "Only manager can execute"); - _; - } - - /** - * @dev Returns Governor address from the Nexus - * @return Address of Governor Contract - */ - function _governor() internal view returns (address) { - return nexus.governor(); - } - - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - - /** - * @dev Return Staking Module address from the Nexus - * @return Address of the Staking Module contract - */ - function _staking() internal view returns (address) { - return nexus.getModule(KEY_STAKING); - } - - /** - * @dev Return ProxyAdmin Module address from the Nexus - * @return Address of the ProxyAdmin Module contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } - - /** - * @dev Return MetaToken Module address from the Nexus - * @return Address of the MetaToken Module contract - */ - function _metaToken() internal view returns (address) { - return nexus.getModule(KEY_META_TOKEN); - } - - /** - * @dev Return OracleHub Module address from the Nexus - * @return Address of the OracleHub Module contract - */ - function _oracleHub() internal view returns (address) { - return nexus.getModule(KEY_ORACLE_HUB); - } - - /** - * @dev Return Manager Module address from the Nexus - * @return Address of the Manager Module contract - */ - function _manager() internal view returns (address) { - return nexus.getModule(KEY_MANAGER); - } - - /** - * @dev Return SavingsManager Module address from the Nexus - * @return Address of the SavingsManager Module contract - */ - function _savingsManager() internal view returns (address) { - return nexus.getModule(KEY_SAVINGS_MANAGER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _recollateraliser() internal view returns (address) { - return nexus.getModule(KEY_RECOLLATERALISER); - } -} - -interface IRewardsDistributionRecipient { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); -} - -abstract contract InitializableRewardsDistributionRecipient is - IRewardsDistributionRecipient, - ImmutableModule -{ - // This address has the ability to distribute the rewards - address public rewardsDistributor; - - constructor(address _nexus) ImmutableModule(_nexus) {} - - /** @dev Recipient is a module, governed by mStable governance */ - function _initialize(address _rewardsDistributor) internal { - rewardsDistributor = _rewardsDistributor; - } - - /** - * @dev Only the rewards distributor can notify about rewards - */ - modifier onlyRewardsDistributor() { - require(msg.sender == rewardsDistributor, "Caller is not reward distributor"); - _; - } - - /** - * @dev Change the rewardsDistributor - only called by mStable governor - * @param _rewardsDistributor Address of the new distributor - */ - function setRewardsDistribution(address _rewardsDistributor) external onlyGovernor { - rewardsDistributor = _rewardsDistributor; - } -} - -interface IBoostDirector { - function getBalance(address _user) external returns (uint256); - - function setDirection( - address _old, - address _new, - bool _pokeNew - ) external; - - function whitelistVaults(address[] calldata _vaults) external; -} - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { - size := extcodesize(account) - } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return - functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) - internal - view - returns (bytes memory) - { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) - internal - returns (bytes memory) - { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) private pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} - -library SafeERC20 { - using Address for address; - - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { - _callOptionalReturn( - token, - abi.encodeWithSelector(token.transferFrom.selector, from, to, value) - ); - } - - /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. - */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function _callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that - // the target address contains contract code and also asserts for success in the low-level call. - - bytes memory returndata = address(token).functionCall( - data, - "SafeERC20: low-level call failed" - ); - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} - -contract InitializableReentrancyGuard { - bool private _notEntered; - - function _initializeReentrancyGuard() internal { - // Storing an initial non-zero value makes deployment a bit more - // expensive, but in exchange the refund on every call to nonReentrant - // will be lower in amount. Since refunds are capped to a percetange of - // the total transaction's gas, it is best to keep them low in cases - // like this one, to increase the likelihood of the full refund coming - // into effect. - _notEntered = true; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_notEntered, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _notEntered = false; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _notEntered = true; - } -} - -library StableMath { - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @dev Token Ratios are used when converting between units of bAsset, mAsset and MTA - * Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold) - * bAsset ratio unit for use in exact calculations, - * where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit - */ - uint256 private constant RATIO_SCALE = 1e8; - - /** - * @dev Provides an interface to the scaling unit - * @return Scaling unit (1e18 or 1 * 10**18) - */ - function getFullScale() internal pure returns (uint256) { - return FULL_SCALE; - } - - /** - * @dev Provides an interface to the ratio unit - * @return Ratio scale unit (1e8 or 1 * 10**8) - */ - function getRatioScale() internal pure returns (uint256) { - return RATIO_SCALE; - } - - /** - * @dev Scales a given integer to the power of the full scale. - * @param x Simple uint256 to scale - * @return Scaled value a to an exact number - */ - function scaleInteger(uint256 x) internal pure returns (uint256) { - return x * FULL_SCALE; - } - - /*************************************** - PRECISE ARITHMETIC - ****************************************/ - - /** - * @dev Multiplies two precise units, and then truncates by the full scale - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) { - return mulTruncateScale(x, y, FULL_SCALE); - } - - /** - * @dev Multiplies two precise units, and then truncates by the given scale. For example, - * when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18 - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @param scale Scale unit - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncateScale( - uint256 x, - uint256 y, - uint256 scale - ) internal pure returns (uint256) { - // e.g. assume scale = fullScale - // z = 10e18 * 9e17 = 9e36 - // return 9e38 / 1e18 = 9e18 - return (x * y) / scale; - } - - /** - * @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit, rounded up to the closest base unit. - */ - function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e17 * 17268172638 = 138145381104e17 - uint256 scaled = x * y; - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled + FULL_SCALE - 1; - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil / FULL_SCALE; - } - - /** - * @dev Precisely divides two units, by first scaling the left hand operand. Useful - * for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17) - * @param x Left hand input to division - * @param y Right hand input to division - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e18 * 1e18 = 8e36 - // e.g. 8e36 / 10e18 = 8e17 - return (x * FULL_SCALE) / y; - } - - /*************************************** - RATIO FUNCS - ****************************************/ - - /** - * @dev Multiplies and truncates a token ratio, essentially flooring the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand operand to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return c Result after multiplying the two inputs and then dividing by the ratio scale - */ - function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - return mulTruncateScale(x, ratio, RATIO_SCALE); - } - - /** - * @dev Multiplies and truncates a token ratio, rounding up the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand input to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the shared - * ratio scale, rounded up to the closest base unit. - */ - function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) { - // e.g. How much mAsset should I burn for this bAsset (x)? - // 1e18 * 1e8 = 1e26 - uint256 scaled = x * ratio; - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled + RATIO_SCALE - 1; - // return 100..00.999e8 / 1e8 = 1e18 - return ceil / RATIO_SCALE; - } - - /** - * @dev Precisely divides two ratioed units, by first scaling the left hand operand - * i.e. How much bAsset is this mAsset worth? - * @param x Left hand operand in division - * @param ratio bAsset ratio - * @return c Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - // e.g. 1e14 * 1e8 = 1e22 - // return 1e22 / 1e12 = 1e10 - return (x * RATIO_SCALE) / ratio; - } - - /*************************************** - HELPERS - ****************************************/ - - /** - * @dev Calculates minimum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Minimum of the two inputs - */ - function min(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? y : x; - } - - /** - * @dev Calculated maximum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Maximum of the two inputs - */ - function max(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? x : y; - } - - /** - * @dev Clamps a value to an upper bound - * @param x Left hand input - * @param upperBound Maximum possible value to return - * @return Input x clamped to a maximum value, upperBound - */ - function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) { - return x > upperBound ? upperBound : x; - } -} - -library Root { - /** - * @dev Returns the square root of a given number - * @param x Input - * @return y Square root of Input - */ - function sqrt(uint256 x) internal pure returns (uint256 y) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; // Seven iterations should be enough - uint256 r1 = x / r; - return uint256(r < r1 ? r : r1); - } - } -} - -contract BoostedTokenWrapper is InitializableReentrancyGuard { - using StableMath for uint256; - using SafeERC20 for IERC20; - - event Transfer(address indexed from, address indexed to, uint256 value); - - string private _name; - string private _symbol; - - IERC20 public immutable stakingToken; - IBoostDirector public immutable boostDirector; - - uint256 private _totalBoostedSupply; - mapping(address => uint256) private _boostedBalances; - mapping(address => uint256) private _rawBalances; - - // Vars for use in the boost calculations - uint256 private constant MIN_DEPOSIT = 1e18; - uint256 private constant MAX_VMTA = 600000e18; - uint256 private constant MAX_BOOST = 3e18; - uint256 private constant MIN_BOOST = 1e18; - uint256 private constant FLOOR = 98e16; - uint256 public immutable boostCoeff; // scaled by 10 - uint256 public immutable priceCoeff; - - /** - * @dev TokenWrapper constructor - * @param _stakingToken Wrapped token to be staked - * @param _boostDirector vMTA boost director - * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 - */ - constructor( - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _boostCoeff - ) { - stakingToken = IERC20(_stakingToken); - boostDirector = IBoostDirector(_boostDirector); - priceCoeff = _priceCoeff; - boostCoeff = _boostCoeff; - } - - function _initialize(string memory _nameArg, string memory _symbolArg) internal { - _initializeReentrancyGuard(); - _name = _nameArg; - _symbol = _symbolArg; - } - - function name() public view virtual returns (string memory) { - return _name; - } - - function symbol() public view virtual returns (string memory) { - return _symbol; - } - - function decimals() public view virtual returns (uint8) { - return 18; - } - - /** - * @dev Get the total boosted amount - * @return uint256 total supply - */ - function totalSupply() public view returns (uint256) { - return _totalBoostedSupply; - } - - /** - * @dev Get the boosted balance of a given account - * @param _account User for which to retrieve balance - */ - function balanceOf(address _account) public view returns (uint256) { - return _boostedBalances[_account]; - } - - /** - * @dev Get the RAW balance of a given account - * @param _account User for which to retrieve balance - */ - function rawBalanceOf(address _account) public view returns (uint256) { - return _rawBalances[_account]; - } - - /** - * @dev Read the boost for the given address - * @param _account User for which to return the boost - * @return boost where 1x == 1e18 - */ - function getBoost(address _account) public view returns (uint256) { - return balanceOf(_account).divPrecisely(rawBalanceOf(_account)); - } - - /** - * @dev Deposits a given amount of StakingToken from sender - * @param _amount Units of StakingToken - */ - function _stakeRaw(address _beneficiary, uint256 _amount) internal nonReentrant { - _rawBalances[_beneficiary] += _amount; - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - } - - /** - * @dev Withdraws a given stake from sender - * @param _amount Units of StakingToken - */ - function _withdrawRaw(uint256 _amount) internal nonReentrant { - _rawBalances[msg.sender] -= _amount; - stakingToken.safeTransfer(msg.sender, _amount); - } - - /** - * @dev Updates the boost for the given address according to the formula - * boost = min(0.5 + c * vMTA_balance / imUSD_locked^(7/8), 1.5) - * If rawBalance <= MIN_DEPOSIT, boost is 0 - * @param _account User for which to update the boost - */ - function _setBoost(address _account) internal { - uint256 rawBalance = _rawBalances[_account]; - uint256 boostedBalance = _boostedBalances[_account]; - uint256 boost = MIN_BOOST; - - // Check whether balance is sufficient - // is_boosted is used to minimize gas usage - uint256 scaledBalance = (rawBalance * priceCoeff) / 1e18; - if (scaledBalance >= MIN_DEPOSIT) { - uint256 votingWeight = boostDirector.getBalance(_account); - boost = _computeBoost(scaledBalance, votingWeight); - } - - uint256 newBoostedBalance = rawBalance.mulTruncate(boost); - - if (newBoostedBalance != boostedBalance) { - _totalBoostedSupply = _totalBoostedSupply - boostedBalance + newBoostedBalance; - _boostedBalances[_account] = newBoostedBalance; - - if (newBoostedBalance > boostedBalance) { - emit Transfer(address(0), _account, newBoostedBalance - boostedBalance); - } else { - emit Transfer(_account, address(0), boostedBalance - newBoostedBalance); - } - } - } - - /** - * @dev Computes the boost for - * boost = min(m, max(1, 0.95 + c * min(voting_weight, f) / deposit^(3/4))) - * @param _scaledDeposit deposit amount in terms of USD - */ - function _computeBoost(uint256 _scaledDeposit, uint256 _votingWeight) - private - view - returns (uint256 boost) - { - if (_votingWeight == 0) return MIN_BOOST; - - // Compute balance to the power 3/4 - uint256 sqrt1 = Root.sqrt(_scaledDeposit * 1e6); - uint256 sqrt2 = Root.sqrt(sqrt1); - uint256 denominator = sqrt1 * sqrt2; - boost = - (((StableMath.min(_votingWeight, MAX_VMTA) * boostCoeff) / 10) * 1e18) / - denominator; - boost = StableMath.min(MAX_BOOST, StableMath.max(MIN_BOOST, FLOOR + boost)); - } -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library SafeCast { - /** - * @dev Returns the downcasted uint128 from uint256, reverting on - * overflow (when the input is greater than largest uint128). - * - * Counterpart to Solidity's `uint128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - */ - function toUint128(uint256 value) internal pure returns (uint128) { - require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); - return uint128(value); - } - - /** - * @dev Returns the downcasted uint64 from uint256, reverting on - * overflow (when the input is greater than largest uint64). - * - * Counterpart to Solidity's `uint64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - */ - function toUint64(uint256 value) internal pure returns (uint64) { - require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); - return uint64(value); - } - - /** - * @dev Returns the downcasted uint32 from uint256, reverting on - * overflow (when the input is greater than largest uint32). - * - * Counterpart to Solidity's `uint32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - */ - function toUint32(uint256 value) internal pure returns (uint32) { - require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); - return uint32(value); - } - - /** - * @dev Returns the downcasted uint16 from uint256, reverting on - * overflow (when the input is greater than largest uint16). - * - * Counterpart to Solidity's `uint16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - */ - function toUint16(uint256 value) internal pure returns (uint16) { - require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); - return uint16(value); - } - - /** - * @dev Returns the downcasted uint8 from uint256, reverting on - * overflow (when the input is greater than largest uint8). - * - * Counterpart to Solidity's `uint8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - */ - function toUint8(uint256 value) internal pure returns (uint8) { - require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); - return uint8(value); - } - - /** - * @dev Converts a signed int256 into an unsigned uint256. - * - * Requirements: - * - * - input must be greater than or equal to 0. - */ - function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); - return uint256(value); - } - - /** - * @dev Returns the downcasted int128 from int256, reverting on - * overflow (when the input is less than smallest int128 or - * greater than largest int128). - * - * Counterpart to Solidity's `int128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - * - * _Available since v3.1._ - */ - function toInt128(int256 value) internal pure returns (int128) { - require(value >= -2**127 && value < 2**127, "SafeCast: value doesn't fit in 128 bits"); - return int128(value); - } - - /** - * @dev Returns the downcasted int64 from int256, reverting on - * overflow (when the input is less than smallest int64 or - * greater than largest int64). - * - * Counterpart to Solidity's `int64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - * - * _Available since v3.1._ - */ - function toInt64(int256 value) internal pure returns (int64) { - require(value >= -2**63 && value < 2**63, "SafeCast: value doesn't fit in 64 bits"); - return int64(value); - } - - /** - * @dev Returns the downcasted int32 from int256, reverting on - * overflow (when the input is less than smallest int32 or - * greater than largest int32). - * - * Counterpart to Solidity's `int32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - * - * _Available since v3.1._ - */ - function toInt32(int256 value) internal pure returns (int32) { - require(value >= -2**31 && value < 2**31, "SafeCast: value doesn't fit in 32 bits"); - return int32(value); - } - - /** - * @dev Returns the downcasted int16 from int256, reverting on - * overflow (when the input is less than smallest int16 or - * greater than largest int16). - * - * Counterpart to Solidity's `int16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - * - * _Available since v3.1._ - */ - function toInt16(int256 value) internal pure returns (int16) { - require(value >= -2**15 && value < 2**15, "SafeCast: value doesn't fit in 16 bits"); - return int16(value); - } - - /** - * @dev Returns the downcasted int8 from int256, reverting on - * overflow (when the input is less than smallest int8 or - * greater than largest int8). - * - * Counterpart to Solidity's `int8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - * - * _Available since v3.1._ - */ - function toInt8(int256 value) internal pure returns (int8) { - require(value >= -2**7 && value < 2**7, "SafeCast: value doesn't fit in 8 bits"); - return int8(value); - } - - /** - * @dev Converts an unsigned uint256 into a signed int256. - * - * Requirements: - * - * - input must be less than or equal to maxInt256. - */ - function toInt256(uint256 value) internal pure returns (int256) { - require(value < 2**255, "SafeCast: value doesn't fit in an int256"); - return int256(value); - } -} - -// Internal -// Libs -/** - * @title BoostedSavingsVault - * @author mStable - * @notice Accrues rewards second by second, based on a users boosted balance - * @dev Forked from rewards/staking/StakingRewards.sol - * Changes: - * - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months) - * - `updateBoost` hook called after every external action to reset a users boost - * - Struct packing of common data - * - Searching for and claiming of unlocked rewards - */ -contract BoostedSavingsVault is - IBoostedVaultWithLockup, - Initializable, - InitializableRewardsDistributionRecipient, - BoostedTokenWrapper -{ - using SafeERC20 for IERC20; - using StableMath for uint256; - using SafeCast for uint256; - - event RewardAdded(uint256 reward); - event Staked(address indexed user, uint256 amount, address payer); - event Withdrawn(address indexed user, uint256 amount); - event Poked(address indexed user); - event RewardPaid(address indexed user, uint256 reward); - - IERC20 public immutable rewardsToken; - - uint64 public constant DURATION = 7 days; - // Length of token lockup, after rewards are earned - uint256 public constant LOCKUP = 26 weeks; - // Percentage of earned tokens unlocked immediately - uint64 public constant UNLOCK = 33e16; - - // Timestamp for current period finish - uint256 public periodFinish; - // RewardRate for the rest of the PERIOD - uint256 public rewardRate; - // Last time any user took action - uint256 public lastUpdateTime; - // Ever increasing rewardPerToken rate, based on % of total supply - uint256 public rewardPerTokenStored; - mapping(address => UserData) public userData; - // Locked reward tracking - mapping(address => Reward[]) public userRewards; - mapping(address => uint64) public userClaim; - - struct UserData { - uint128 rewardPerTokenPaid; - uint128 rewards; - uint64 lastAction; - uint64 rewardCount; - } - - struct Reward { - uint64 start; - uint64 finish; - uint128 rate; - } - - constructor( - address _nexus, - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _coeff, - address _rewardsToken - ) - InitializableRewardsDistributionRecipient(_nexus) - BoostedTokenWrapper(_stakingToken, _boostDirector, _priceCoeff, _coeff) - { - rewardsToken = IERC20(_rewardsToken); - } - - /** - * @dev StakingRewards is a TokenWrapper and RewardRecipient - * Constants added to bytecode at deployTime to reduce SLOAD cost - */ - function initialize( - address _rewardsDistributor, - string calldata _nameArg, - string calldata _symbolArg - ) external initializer { - InitializableRewardsDistributionRecipient._initialize(_rewardsDistributor); - BoostedTokenWrapper._initialize(_nameArg, _symbolArg); - } - - /** - * @dev Updates the reward for a given address, before executing function. - * Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to - * (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed - * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). - */ - modifier updateReward(address _account) { - uint256 currentTime = block.timestamp; - uint64 currentTime64 = SafeCast.toUint64(currentTime); - - // Setting of global vars - (uint256 newRewardPerToken, uint256 lastApplicableTime) = _rewardPerToken(); - // If statement protects against loss in initialisation case - if (newRewardPerToken > 0) { - rewardPerTokenStored = newRewardPerToken; - lastUpdateTime = lastApplicableTime; - - // Setting of personal vars based on new globals - if (_account != address(0)) { - UserData memory data = userData[_account]; - uint256 earned_ = _earned(_account, data.rewardPerTokenPaid, newRewardPerToken); - - // If earned == 0, then it must either be the initial stake, or an action in the - // same block, since new rewards unlock after each block. - if (earned_ > 0) { - uint256 unlocked = earned_.mulTruncate(UNLOCK); - uint256 locked = earned_ - unlocked; - - userRewards[_account].push( - Reward({ - start: SafeCast.toUint64(LOCKUP + data.lastAction), - finish: SafeCast.toUint64(LOCKUP + currentTime), - rate: SafeCast.toUint128(locked / (currentTime - data.lastAction)) - }) - ); - - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: SafeCast.toUint128(unlocked + data.rewards), - lastAction: currentTime64, - rewardCount: data.rewardCount + 1 - }); - } else { - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: data.rewards, - lastAction: currentTime64, - rewardCount: data.rewardCount - }); - } - } - } else if (_account != address(0)) { - // This should only be hit once, for first staker in initialisation case - userData[_account].lastAction = currentTime64; - } - _; - } - - /** @dev Updates the boost for a given address, after the rest of the function has executed */ - modifier updateBoost(address _account) { - _; - _setBoost(_account); - } - - /*************************************** - ACTIONS - EXTERNAL - ****************************************/ - - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _stake(msg.sender, _amount); - } - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) - external - override - updateReward(_beneficiary) - updateBoost(_beneficiary) - { - _stake(_beneficiary, _amount); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external override updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(rawBalanceOf(msg.sender)); - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - _claimRewards(first, last); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(rawBalanceOf(msg.sender)); - _claimRewards(_first, _last); - } - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(_amount); - } - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external override updateReward(msg.sender) updateBoost(msg.sender) { - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - if (unlocked > 0) { - rewardsToken.safeTransfer(msg.sender, unlocked); - emit RewardPaid(msg.sender, unlocked); - } - } - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external override updateReward(msg.sender) updateBoost(msg.sender) { - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - - _claimRewards(first, last); - } - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _claimRewards(_first, _last); - } - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) - external - override - updateReward(_account) - updateBoost(_account) - { - emit Poked(_account); - } - - /*************************************** - ACTIONS - INTERNAL - ****************************************/ - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function _claimRewards(uint256 _first, uint256 _last) internal { - (uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last); - userClaim[msg.sender] = uint64(lastTimestamp); - - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - uint256 total = unclaimed + unlocked; - - if (total > 0) { - rewardsToken.safeTransfer(msg.sender, total); - - emit RewardPaid(msg.sender, total); - } - } - - /** - * @dev Internally stakes an amount by depositing from sender, - * and crediting to the specified beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function _stake(address _beneficiary, uint256 _amount) internal { - require(_amount > 0, "Cannot stake 0"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - _stakeRaw(_beneficiary, _amount); - emit Staked(_beneficiary, _amount, msg.sender); - } - - /** - * @dev Withdraws raw units from the sender - * @param _amount Units of StakingToken - */ - function _withdraw(uint256 _amount) internal { - require(_amount > 0, "Cannot withdraw 0"); - _withdrawRaw(_amount); - emit Withdrawn(msg.sender, _amount); - } - - /*************************************** - GETTERS - ****************************************/ - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() external view override returns (IERC20) { - return rewardsToken; - } - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() public view override returns (uint256) { - return StableMath.min(block.timestamp, periodFinish); - } - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() public view override returns (uint256) { - (uint256 rewardPerToken_, ) = _rewardPerToken(); - return rewardPerToken_; - } - - function _rewardPerToken() - internal - view - returns (uint256 rewardPerToken_, uint256 lastTimeRewardApplicable_) - { - uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD - uint256 timeDelta = lastApplicableTime - lastUpdateTime; // + 1 SLOAD - // If this has been called twice in the same block, shortcircuit to reduce gas - if (timeDelta == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units to distribute = rewardRate * timeSinceLastUpdate - uint256 rewardUnitsToDistribute = rewardRate * timeDelta; // + 1 SLOAD - uint256 supply = totalSupply(); // + 1 SLOAD - // If there is no StakingToken liquidity, avoid div(0) - // If there is nothing to distribute, short circuit - if (supply == 0 || rewardUnitsToDistribute == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens - uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(supply); - // return summed rate - return (rewardPerTokenStored + unitsToDistributePerToken, lastApplicableTime); // + 1 SLOAD - } - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) public view override returns (uint256) { - uint256 newEarned = _earned( - _account, - userData[_account].rewardPerTokenPaid, - rewardPerToken() - ); - uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK); - return immediatelyUnlocked + userData[_account].rewards; - } - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - override - returns ( - uint256 amount, - uint256 first, - uint256 last - ) - { - (first, last) = _unclaimedEpochs(_account); - (uint256 unlocked, ) = _unclaimedRewards(_account, first, last); - amount = unlocked + earned(_account); - } - - /** @dev Returns only the most recently earned rewards */ - function _earned( - address _account, - uint256 _userRewardPerTokenPaid, - uint256 _currentRewardPerToken - ) internal view returns (uint256) { - // current rate per token - rate user previously received - uint256 userRewardDelta = _currentRewardPerToken - _userRewardPerTokenPaid; // + 1 SLOAD - // Short circuit if there is nothing new to distribute - if (userRewardDelta == 0) { - return 0; - } - // new reward = staked tokens * difference in rate - uint256 userNewReward = balanceOf(_account).mulTruncate(userRewardDelta); // + 1 SLOAD - // add to previous rewards - return userNewReward; - } - - /** - * @dev Gets the first and last indexes of array elements containing unclaimed rewards - */ - function _unclaimedEpochs(address _account) - internal - view - returns (uint256 first, uint256 last) - { - uint64 lastClaim = userClaim[_account]; - - uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account); - uint256 lastUnclaimed = _findLastUnclaimed(_account); - - return (firstUnclaimed, lastUnclaimed); - } - - /** - * @dev Sums the cumulative rewards from a valid range - */ - function _unclaimedRewards( - address _account, - uint256 _first, - uint256 _last - ) internal view returns (uint256 amount, uint256 latestTimestamp) { - uint256 currentTime = block.timestamp; - uint64 lastClaim = userClaim[_account]; - - // Check for no rewards unlocked - uint256 totalLen = userRewards[_account].length; - if (_first == 0 && _last == 0) { - if (totalLen == 0 || currentTime <= userRewards[_account][0].start) { - return (0, currentTime); - } - } - // If there are previous unlocks, check for claims that would leave them untouchable - if (_first > 0) { - require( - lastClaim >= userRewards[_account][_first - 1].finish, - "Invalid _first arg: Must claim earlier entries" - ); - } - - uint256 count = _last - _first + 1; - for (uint256 i = 0; i < count; i++) { - uint256 id = _first + i; - Reward memory rwd = userRewards[_account][id]; - - require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch"); - - uint256 endTime = StableMath.min(rwd.finish, currentTime); - uint256 startTime = StableMath.max(rwd.start, lastClaim); - uint256 unclaimed = (endTime - startTime) * rwd.rate; - - amount += unclaimed; - } - - // Calculate last relevant timestamp here to allow users to avoid issue of OOG errors - // by claiming rewards in batches. - latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish); - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findFirstUnclaimed(uint64 _lastClaim, address _account) - internal - view - returns (uint256 first) - { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (_lastClaim > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findLastUnclaimed(address _account) internal view returns (uint256 first) { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (block.timestamp > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /*************************************** - ADMIN - ****************************************/ - - /** - * @dev Notifies the contract that new rewards have been added. - * Calculates an updated rewardRate based on the rewards in period. - * @param _reward Units of RewardToken that have been added to the pool - */ - function notifyRewardAmount(uint256 _reward) - external - override - onlyRewardsDistributor - updateReward(address(0)) - { - require(_reward < 1e24, "Cannot notify with more than a million units"); - - uint256 currentTime = block.timestamp; - // If previous period over, reset rewardRate - if (currentTime >= periodFinish) { - rewardRate = _reward / DURATION; - } - // If additional reward to existing period, calc sum - else { - uint256 remaining = periodFinish - currentTime; - uint256 leftover = remaining * rewardRate; - rewardRate = (_reward + leftover) / DURATION; - } - - lastUpdateTime = currentTime; - periodFinish = currentTime + DURATION; - - emit RewardAdded(_reward); - } -} diff --git a/contracts/legacy/v-HBTC.sol b/contracts/legacy/v-HBTC.sol deleted file mode 100644 index bb539ba4..00000000 --- a/contracts/legacy/v-HBTC.sol +++ /dev/null @@ -1,1994 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.2; - -interface IERC20 { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); -} - -interface IBoostedVaultWithLockup { - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external; - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) external; - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external; - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external; - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) external; - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external; - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() external view returns (uint256); - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() external view returns (uint256); - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) external view returns (uint256); - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last - ); -} - -contract ModuleKeys { - // Governance - // =========== - // keccak256("Governance"); - bytes32 internal constant KEY_GOVERNANCE = - 0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d; - //keccak256("Staking"); - bytes32 internal constant KEY_STAKING = - 0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034; - //keccak256("ProxyAdmin"); - bytes32 internal constant KEY_PROXY_ADMIN = - 0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1; - - // mStable - // ======= - // keccak256("OracleHub"); - bytes32 internal constant KEY_ORACLE_HUB = - 0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040; - // keccak256("Manager"); - bytes32 internal constant KEY_MANAGER = - 0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f; - //keccak256("Recollateraliser"); - bytes32 internal constant KEY_RECOLLATERALISER = - 0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f; - //keccak256("MetaToken"); - bytes32 internal constant KEY_META_TOKEN = - 0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2; - // keccak256("SavingsManager"); - bytes32 internal constant KEY_SAVINGS_MANAGER = - 0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1; - // keccak256("Liquidator"); - bytes32 internal constant KEY_LIQUIDATOR = - 0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4; - // keccak256("InterestValidator"); - bytes32 internal constant KEY_INTEREST_VALIDATOR = - 0xc10a28f028c7f7282a03c90608e38a4a646e136e614e4b07d119280c5f7f839f; -} - -interface INexus { - function governor() external view returns (address); - - function getModule(bytes32 key) external view returns (address); - - function proposeModule(bytes32 _key, address _addr) external; - - function cancelProposedModule(bytes32 _key) external; - - function acceptProposedModule(bytes32 _key) external; - - function acceptProposedModules(bytes32[] calldata _keys) external; - - function requestLockModule(bytes32 _key) external; - - function cancelLockModule(bytes32 _key) external; - - function lockModule(bytes32 _key) external; -} - -abstract contract ImmutableModule is ModuleKeys { - INexus public immutable nexus; - - /** - * @dev Initialization function for upgradable proxy contracts - * @param _nexus Nexus contract address - */ - constructor(address _nexus) { - require(_nexus != address(0), "Nexus address is zero"); - nexus = INexus(_nexus); - } - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - _onlyGovernor(); - _; - } - - function _onlyGovernor() internal view { - require(msg.sender == _governor(), "Only governor can execute"); - } - - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - - /** - * @dev Modifier to allow function calls only from the ProxyAdmin. - */ - modifier onlyProxyAdmin() { - require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Manager. - */ - modifier onlyManager() { - require(msg.sender == _manager(), "Only manager can execute"); - _; - } - - /** - * @dev Returns Governor address from the Nexus - * @return Address of Governor Contract - */ - function _governor() internal view returns (address) { - return nexus.governor(); - } - - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - - /** - * @dev Return Staking Module address from the Nexus - * @return Address of the Staking Module contract - */ - function _staking() internal view returns (address) { - return nexus.getModule(KEY_STAKING); - } - - /** - * @dev Return ProxyAdmin Module address from the Nexus - * @return Address of the ProxyAdmin Module contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } - - /** - * @dev Return MetaToken Module address from the Nexus - * @return Address of the MetaToken Module contract - */ - function _metaToken() internal view returns (address) { - return nexus.getModule(KEY_META_TOKEN); - } - - /** - * @dev Return OracleHub Module address from the Nexus - * @return Address of the OracleHub Module contract - */ - function _oracleHub() internal view returns (address) { - return nexus.getModule(KEY_ORACLE_HUB); - } - - /** - * @dev Return Manager Module address from the Nexus - * @return Address of the Manager Module contract - */ - function _manager() internal view returns (address) { - return nexus.getModule(KEY_MANAGER); - } - - /** - * @dev Return SavingsManager Module address from the Nexus - * @return Address of the SavingsManager Module contract - */ - function _savingsManager() internal view returns (address) { - return nexus.getModule(KEY_SAVINGS_MANAGER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _recollateraliser() internal view returns (address) { - return nexus.getModule(KEY_RECOLLATERALISER); - } -} - -interface IRewardsDistributionRecipient { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); -} - -abstract contract InitializableRewardsDistributionRecipient is - IRewardsDistributionRecipient, - ImmutableModule -{ - // This address has the ability to distribute the rewards - address public rewardsDistributor; - - constructor(address _nexus) ImmutableModule(_nexus) {} - - /** @dev Recipient is a module, governed by mStable governance */ - function _initialize(address _rewardsDistributor) internal { - rewardsDistributor = _rewardsDistributor; - } - - /** - * @dev Only the rewards distributor can notify about rewards - */ - modifier onlyRewardsDistributor() { - require(msg.sender == rewardsDistributor, "Caller is not reward distributor"); - _; - } - - /** - * @dev Change the rewardsDistributor - only called by mStable governor - * @param _rewardsDistributor Address of the new distributor - */ - function setRewardsDistribution(address _rewardsDistributor) external onlyGovernor { - rewardsDistributor = _rewardsDistributor; - } -} - -interface IBoostDirector { - function getBalance(address _user) external returns (uint256); - - function setDirection( - address _old, - address _new, - bool _pokeNew - ) external; - - function whitelistVaults(address[] calldata _vaults) external; -} - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. - */ - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { - size := extcodesize(account) - } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return - functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) - internal - view - returns (bytes memory) - { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) - internal - returns (bytes memory) - { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) private pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} - -library SafeERC20 { - using Address for address; - - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { - _callOptionalReturn( - token, - abi.encodeWithSelector(token.transferFrom.selector, from, to, value) - ); - } - - /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. - */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function _callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that - // the target address contains contract code and also asserts for success in the low-level call. - - bytes memory returndata = address(token).functionCall( - data, - "SafeERC20: low-level call failed" - ); - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} - -contract InitializableReentrancyGuard { - bool private _notEntered; - - function _initializeReentrancyGuard() internal { - // Storing an initial non-zero value makes deployment a bit more - // expensive, but in exchange the refund on every call to nonReentrant - // will be lower in amount. Since refunds are capped to a percetange of - // the total transaction's gas, it is best to keep them low in cases - // like this one, to increase the likelihood of the full refund coming - // into effect. - _notEntered = true; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_notEntered, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _notEntered = false; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _notEntered = true; - } -} - -library StableMath { - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @dev Token Ratios are used when converting between units of bAsset, mAsset and MTA - * Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold) - * bAsset ratio unit for use in exact calculations, - * where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit - */ - uint256 private constant RATIO_SCALE = 1e8; - - /** - * @dev Provides an interface to the scaling unit - * @return Scaling unit (1e18 or 1 * 10**18) - */ - function getFullScale() internal pure returns (uint256) { - return FULL_SCALE; - } - - /** - * @dev Provides an interface to the ratio unit - * @return Ratio scale unit (1e8 or 1 * 10**8) - */ - function getRatioScale() internal pure returns (uint256) { - return RATIO_SCALE; - } - - /** - * @dev Scales a given integer to the power of the full scale. - * @param x Simple uint256 to scale - * @return Scaled value a to an exact number - */ - function scaleInteger(uint256 x) internal pure returns (uint256) { - return x * FULL_SCALE; - } - - /*************************************** - PRECISE ARITHMETIC - ****************************************/ - - /** - * @dev Multiplies two precise units, and then truncates by the full scale - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) { - return mulTruncateScale(x, y, FULL_SCALE); - } - - /** - * @dev Multiplies two precise units, and then truncates by the given scale. For example, - * when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18 - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @param scale Scale unit - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncateScale( - uint256 x, - uint256 y, - uint256 scale - ) internal pure returns (uint256) { - // e.g. assume scale = fullScale - // z = 10e18 * 9e17 = 9e36 - // return 9e38 / 1e18 = 9e18 - return (x * y) / scale; - } - - /** - * @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit, rounded up to the closest base unit. - */ - function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e17 * 17268172638 = 138145381104e17 - uint256 scaled = x * y; - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled + FULL_SCALE - 1; - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil / FULL_SCALE; - } - - /** - * @dev Precisely divides two units, by first scaling the left hand operand. Useful - * for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17) - * @param x Left hand input to division - * @param y Right hand input to division - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e18 * 1e18 = 8e36 - // e.g. 8e36 / 10e18 = 8e17 - return (x * FULL_SCALE) / y; - } - - /*************************************** - RATIO FUNCS - ****************************************/ - - /** - * @dev Multiplies and truncates a token ratio, essentially flooring the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand operand to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return c Result after multiplying the two inputs and then dividing by the ratio scale - */ - function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - return mulTruncateScale(x, ratio, RATIO_SCALE); - } - - /** - * @dev Multiplies and truncates a token ratio, rounding up the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand input to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the shared - * ratio scale, rounded up to the closest base unit. - */ - function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) { - // e.g. How much mAsset should I burn for this bAsset (x)? - // 1e18 * 1e8 = 1e26 - uint256 scaled = x * ratio; - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled + RATIO_SCALE - 1; - // return 100..00.999e8 / 1e8 = 1e18 - return ceil / RATIO_SCALE; - } - - /** - * @dev Precisely divides two ratioed units, by first scaling the left hand operand - * i.e. How much bAsset is this mAsset worth? - * @param x Left hand operand in division - * @param ratio bAsset ratio - * @return c Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - // e.g. 1e14 * 1e8 = 1e22 - // return 1e22 / 1e12 = 1e10 - return (x * RATIO_SCALE) / ratio; - } - - /*************************************** - HELPERS - ****************************************/ - - /** - * @dev Calculates minimum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Minimum of the two inputs - */ - function min(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? y : x; - } - - /** - * @dev Calculated maximum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Maximum of the two inputs - */ - function max(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? x : y; - } - - /** - * @dev Clamps a value to an upper bound - * @param x Left hand input - * @param upperBound Maximum possible value to return - * @return Input x clamped to a maximum value, upperBound - */ - function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) { - return x > upperBound ? upperBound : x; - } -} - -library Root { - /** - * @dev Returns the square root of a given number - * @param x Input - * @return y Square root of Input - */ - function sqrt(uint256 x) internal pure returns (uint256 y) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; // Seven iterations should be enough - uint256 r1 = x / r; - return uint256(r < r1 ? r : r1); - } - } -} - -contract BoostedTokenWrapper is InitializableReentrancyGuard { - using StableMath for uint256; - using SafeERC20 for IERC20; - - event Transfer(address indexed from, address indexed to, uint256 value); - - string private _name; - string private _symbol; - - IERC20 public immutable stakingToken; - IBoostDirector public immutable boostDirector; - - uint256 private _totalBoostedSupply; - mapping(address => uint256) private _boostedBalances; - mapping(address => uint256) private _rawBalances; - - // Vars for use in the boost calculations - uint256 private constant MIN_DEPOSIT = 1e18; - uint256 private constant MAX_VMTA = 600000e18; - uint256 private constant MAX_BOOST = 3e18; - uint256 private constant MIN_BOOST = 1e18; - uint256 private constant FLOOR = 98e16; - uint256 public immutable boostCoeff; // scaled by 10 - uint256 public immutable priceCoeff; - - /** - * @dev TokenWrapper constructor - * @param _stakingToken Wrapped token to be staked - * @param _boostDirector vMTA boost director - * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 - */ - constructor( - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _boostCoeff - ) { - stakingToken = IERC20(_stakingToken); - boostDirector = IBoostDirector(_boostDirector); - priceCoeff = _priceCoeff; - boostCoeff = _boostCoeff; - } - - function _initialize(string memory _nameArg, string memory _symbolArg) internal { - _initializeReentrancyGuard(); - _name = _nameArg; - _symbol = _symbolArg; - } - - function name() public view virtual returns (string memory) { - return _name; - } - - function symbol() public view virtual returns (string memory) { - return _symbol; - } - - function decimals() public view virtual returns (uint8) { - return 18; - } - - /** - * @dev Get the total boosted amount - * @return uint256 total supply - */ - function totalSupply() public view returns (uint256) { - return _totalBoostedSupply; - } - - /** - * @dev Get the boosted balance of a given account - * @param _account User for which to retrieve balance - */ - function balanceOf(address _account) public view returns (uint256) { - return _boostedBalances[_account]; - } - - /** - * @dev Get the RAW balance of a given account - * @param _account User for which to retrieve balance - */ - function rawBalanceOf(address _account) public view returns (uint256) { - return _rawBalances[_account]; - } - - /** - * @dev Read the boost for the given address - * @param _account User for which to return the boost - * @return boost where 1x == 1e18 - */ - function getBoost(address _account) public view returns (uint256) { - return balanceOf(_account).divPrecisely(rawBalanceOf(_account)); - } - - /** - * @dev Deposits a given amount of StakingToken from sender - * @param _amount Units of StakingToken - */ - function _stakeRaw(address _beneficiary, uint256 _amount) internal nonReentrant { - _rawBalances[_beneficiary] += _amount; - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - } - - /** - * @dev Withdraws a given stake from sender - * @param _amount Units of StakingToken - */ - function _withdrawRaw(uint256 _amount) internal nonReentrant { - _rawBalances[msg.sender] -= _amount; - stakingToken.safeTransfer(msg.sender, _amount); - } - - /** - * @dev Updates the boost for the given address according to the formula - * boost = min(0.5 + c * vMTA_balance / imUSD_locked^(7/8), 1.5) - * If rawBalance <= MIN_DEPOSIT, boost is 0 - * @param _account User for which to update the boost - */ - function _setBoost(address _account) internal { - uint256 rawBalance = _rawBalances[_account]; - uint256 boostedBalance = _boostedBalances[_account]; - uint256 boost = MIN_BOOST; - - // Check whether balance is sufficient - // is_boosted is used to minimize gas usage - uint256 scaledBalance = (rawBalance * priceCoeff) / 1e18; - if (scaledBalance >= MIN_DEPOSIT) { - uint256 votingWeight = boostDirector.getBalance(_account); - boost = _computeBoost(scaledBalance, votingWeight); - } - - uint256 newBoostedBalance = rawBalance.mulTruncate(boost); - - if (newBoostedBalance != boostedBalance) { - _totalBoostedSupply = _totalBoostedSupply - boostedBalance + newBoostedBalance; - _boostedBalances[_account] = newBoostedBalance; - - if (newBoostedBalance > boostedBalance) { - emit Transfer(address(0), _account, newBoostedBalance - boostedBalance); - } else { - emit Transfer(_account, address(0), boostedBalance - newBoostedBalance); - } - } - } - - /** - * @dev Computes the boost for - * boost = min(m, max(1, 0.95 + c * min(voting_weight, f) / deposit^(3/4))) - * @param _scaledDeposit deposit amount in terms of USD - */ - function _computeBoost(uint256 _scaledDeposit, uint256 _votingWeight) - private - view - returns (uint256 boost) - { - if (_votingWeight == 0) return MIN_BOOST; - - // Compute balance to the power 3/4 - uint256 sqrt1 = Root.sqrt(_scaledDeposit * 1e6); - uint256 sqrt2 = Root.sqrt(sqrt1); - uint256 denominator = sqrt1 * sqrt2; - boost = - (((StableMath.min(_votingWeight, MAX_VMTA) * boostCoeff) / 10) * 1e18) / - denominator; - boost = StableMath.min(MAX_BOOST, StableMath.max(MIN_BOOST, FLOOR + boost)); - } -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library SafeCast { - /** - * @dev Returns the downcasted uint128 from uint256, reverting on - * overflow (when the input is greater than largest uint128). - * - * Counterpart to Solidity's `uint128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - */ - function toUint128(uint256 value) internal pure returns (uint128) { - require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); - return uint128(value); - } - - /** - * @dev Returns the downcasted uint64 from uint256, reverting on - * overflow (when the input is greater than largest uint64). - * - * Counterpart to Solidity's `uint64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - */ - function toUint64(uint256 value) internal pure returns (uint64) { - require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); - return uint64(value); - } - - /** - * @dev Returns the downcasted uint32 from uint256, reverting on - * overflow (when the input is greater than largest uint32). - * - * Counterpart to Solidity's `uint32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - */ - function toUint32(uint256 value) internal pure returns (uint32) { - require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); - return uint32(value); - } - - /** - * @dev Returns the downcasted uint16 from uint256, reverting on - * overflow (when the input is greater than largest uint16). - * - * Counterpart to Solidity's `uint16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - */ - function toUint16(uint256 value) internal pure returns (uint16) { - require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); - return uint16(value); - } - - /** - * @dev Returns the downcasted uint8 from uint256, reverting on - * overflow (when the input is greater than largest uint8). - * - * Counterpart to Solidity's `uint8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - */ - function toUint8(uint256 value) internal pure returns (uint8) { - require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); - return uint8(value); - } - - /** - * @dev Converts a signed int256 into an unsigned uint256. - * - * Requirements: - * - * - input must be greater than or equal to 0. - */ - function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); - return uint256(value); - } - - /** - * @dev Returns the downcasted int128 from int256, reverting on - * overflow (when the input is less than smallest int128 or - * greater than largest int128). - * - * Counterpart to Solidity's `int128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - * - * _Available since v3.1._ - */ - function toInt128(int256 value) internal pure returns (int128) { - require(value >= -2**127 && value < 2**127, "SafeCast: value doesn't fit in 128 bits"); - return int128(value); - } - - /** - * @dev Returns the downcasted int64 from int256, reverting on - * overflow (when the input is less than smallest int64 or - * greater than largest int64). - * - * Counterpart to Solidity's `int64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - * - * _Available since v3.1._ - */ - function toInt64(int256 value) internal pure returns (int64) { - require(value >= -2**63 && value < 2**63, "SafeCast: value doesn't fit in 64 bits"); - return int64(value); - } - - /** - * @dev Returns the downcasted int32 from int256, reverting on - * overflow (when the input is less than smallest int32 or - * greater than largest int32). - * - * Counterpart to Solidity's `int32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - * - * _Available since v3.1._ - */ - function toInt32(int256 value) internal pure returns (int32) { - require(value >= -2**31 && value < 2**31, "SafeCast: value doesn't fit in 32 bits"); - return int32(value); - } - - /** - * @dev Returns the downcasted int16 from int256, reverting on - * overflow (when the input is less than smallest int16 or - * greater than largest int16). - * - * Counterpart to Solidity's `int16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - * - * _Available since v3.1._ - */ - function toInt16(int256 value) internal pure returns (int16) { - require(value >= -2**15 && value < 2**15, "SafeCast: value doesn't fit in 16 bits"); - return int16(value); - } - - /** - * @dev Returns the downcasted int8 from int256, reverting on - * overflow (when the input is less than smallest int8 or - * greater than largest int8). - * - * Counterpart to Solidity's `int8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - * - * _Available since v3.1._ - */ - function toInt8(int256 value) internal pure returns (int8) { - require(value >= -2**7 && value < 2**7, "SafeCast: value doesn't fit in 8 bits"); - return int8(value); - } - - /** - * @dev Converts an unsigned uint256 into a signed int256. - * - * Requirements: - * - * - input must be less than or equal to maxInt256. - */ - function toInt256(uint256 value) internal pure returns (int256) { - require(value < 2**255, "SafeCast: value doesn't fit in an int256"); - return int256(value); - } -} - -// Internal -// Libs -/** - * @title BoostedSavingsVault - * @author mStable - * @notice Accrues rewards second by second, based on a users boosted balance - * @dev Forked from rewards/staking/StakingRewards.sol - * Changes: - * - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months) - * - `updateBoost` hook called after every external action to reset a users boost - * - Struct packing of common data - * - Searching for and claiming of unlocked rewards - */ -contract BoostedSavingsVault is - IBoostedVaultWithLockup, - Initializable, - InitializableRewardsDistributionRecipient, - BoostedTokenWrapper -{ - using SafeERC20 for IERC20; - using StableMath for uint256; - using SafeCast for uint256; - - event RewardAdded(uint256 reward); - event Staked(address indexed user, uint256 amount, address payer); - event Withdrawn(address indexed user, uint256 amount); - event Poked(address indexed user); - event RewardPaid(address indexed user, uint256 reward); - - IERC20 public immutable rewardsToken; - - uint64 public constant DURATION = 7 days; - // Length of token lockup, after rewards are earned - uint256 public constant LOCKUP = 26 weeks; - // Percentage of earned tokens unlocked immediately - uint64 public constant UNLOCK = 33e16; - - // Timestamp for current period finish - uint256 public periodFinish; - // RewardRate for the rest of the PERIOD - uint256 public rewardRate; - // Last time any user took action - uint256 public lastUpdateTime; - // Ever increasing rewardPerToken rate, based on % of total supply - uint256 public rewardPerTokenStored; - mapping(address => UserData) public userData; - // Locked reward tracking - mapping(address => Reward[]) public userRewards; - mapping(address => uint64) public userClaim; - - struct UserData { - uint128 rewardPerTokenPaid; - uint128 rewards; - uint64 lastAction; - uint64 rewardCount; - } - - struct Reward { - uint64 start; - uint64 finish; - uint128 rate; - } - - constructor( - address _nexus, - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _coeff, - address _rewardsToken - ) - InitializableRewardsDistributionRecipient(_nexus) - BoostedTokenWrapper(_stakingToken, _boostDirector, _priceCoeff, _coeff) - { - rewardsToken = IERC20(_rewardsToken); - } - - /** - * @dev StakingRewards is a TokenWrapper and RewardRecipient - * Constants added to bytecode at deployTime to reduce SLOAD cost - */ - function initialize( - address _rewardsDistributor, - string calldata _nameArg, - string calldata _symbolArg - ) external initializer { - InitializableRewardsDistributionRecipient._initialize(_rewardsDistributor); - BoostedTokenWrapper._initialize(_nameArg, _symbolArg); - } - - /** - * @dev Updates the reward for a given address, before executing function. - * Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to - * (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed - * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). - */ - modifier updateReward(address _account) { - uint256 currentTime = block.timestamp; - uint64 currentTime64 = SafeCast.toUint64(currentTime); - - // Setting of global vars - (uint256 newRewardPerToken, uint256 lastApplicableTime) = _rewardPerToken(); - // If statement protects against loss in initialisation case - if (newRewardPerToken > 0) { - rewardPerTokenStored = newRewardPerToken; - lastUpdateTime = lastApplicableTime; - - // Setting of personal vars based on new globals - if (_account != address(0)) { - UserData memory data = userData[_account]; - uint256 earned_ = _earned(_account, data.rewardPerTokenPaid, newRewardPerToken); - - // If earned == 0, then it must either be the initial stake, or an action in the - // same block, since new rewards unlock after each block. - if (earned_ > 0) { - uint256 unlocked = earned_.mulTruncate(UNLOCK); - uint256 locked = earned_ - unlocked; - - userRewards[_account].push( - Reward({ - start: SafeCast.toUint64(LOCKUP + data.lastAction), - finish: SafeCast.toUint64(LOCKUP + currentTime), - rate: SafeCast.toUint128(locked / (currentTime - data.lastAction)) - }) - ); - - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: SafeCast.toUint128(unlocked + data.rewards), - lastAction: currentTime64, - rewardCount: data.rewardCount + 1 - }); - } else { - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: data.rewards, - lastAction: currentTime64, - rewardCount: data.rewardCount - }); - } - } - } else if (_account != address(0)) { - // This should only be hit once, for first staker in initialisation case - userData[_account].lastAction = currentTime64; - } - _; - } - - /** @dev Updates the boost for a given address, after the rest of the function has executed */ - modifier updateBoost(address _account) { - _; - _setBoost(_account); - } - - /*************************************** - ACTIONS - EXTERNAL - ****************************************/ - - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _stake(msg.sender, _amount); - } - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) - external - override - updateReward(_beneficiary) - updateBoost(_beneficiary) - { - _stake(_beneficiary, _amount); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external override updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(rawBalanceOf(msg.sender)); - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - _claimRewards(first, last); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(rawBalanceOf(msg.sender)); - _claimRewards(_first, _last); - } - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(_amount); - } - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external override updateReward(msg.sender) updateBoost(msg.sender) { - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - if (unlocked > 0) { - rewardsToken.safeTransfer(msg.sender, unlocked); - emit RewardPaid(msg.sender, unlocked); - } - } - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external override updateReward(msg.sender) updateBoost(msg.sender) { - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - - _claimRewards(first, last); - } - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _claimRewards(_first, _last); - } - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) - external - override - updateReward(_account) - updateBoost(_account) - { - emit Poked(_account); - } - - /*************************************** - ACTIONS - INTERNAL - ****************************************/ - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function _claimRewards(uint256 _first, uint256 _last) internal { - (uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last); - userClaim[msg.sender] = uint64(lastTimestamp); - - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - uint256 total = unclaimed + unlocked; - - if (total > 0) { - rewardsToken.safeTransfer(msg.sender, total); - - emit RewardPaid(msg.sender, total); - } - } - - /** - * @dev Internally stakes an amount by depositing from sender, - * and crediting to the specified beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function _stake(address _beneficiary, uint256 _amount) internal { - require(_amount > 0, "Cannot stake 0"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - _stakeRaw(_beneficiary, _amount); - emit Staked(_beneficiary, _amount, msg.sender); - } - - /** - * @dev Withdraws raw units from the sender - * @param _amount Units of StakingToken - */ - function _withdraw(uint256 _amount) internal { - require(_amount > 0, "Cannot withdraw 0"); - _withdrawRaw(_amount); - emit Withdrawn(msg.sender, _amount); - } - - /*************************************** - GETTERS - ****************************************/ - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() external view override returns (IERC20) { - return rewardsToken; - } - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() public view override returns (uint256) { - return StableMath.min(block.timestamp, periodFinish); - } - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() public view override returns (uint256) { - (uint256 rewardPerToken_, ) = _rewardPerToken(); - return rewardPerToken_; - } - - function _rewardPerToken() - internal - view - returns (uint256 rewardPerToken_, uint256 lastTimeRewardApplicable_) - { - uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD - uint256 timeDelta = lastApplicableTime - lastUpdateTime; // + 1 SLOAD - // If this has been called twice in the same block, shortcircuit to reduce gas - if (timeDelta == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units to distribute = rewardRate * timeSinceLastUpdate - uint256 rewardUnitsToDistribute = rewardRate * timeDelta; // + 1 SLOAD - uint256 supply = totalSupply(); // + 1 SLOAD - // If there is no StakingToken liquidity, avoid div(0) - // If there is nothing to distribute, short circuit - if (supply == 0 || rewardUnitsToDistribute == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens - uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(supply); - // return summed rate - return (rewardPerTokenStored + unitsToDistributePerToken, lastApplicableTime); // + 1 SLOAD - } - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) public view override returns (uint256) { - uint256 newEarned = _earned( - _account, - userData[_account].rewardPerTokenPaid, - rewardPerToken() - ); - uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK); - return immediatelyUnlocked + userData[_account].rewards; - } - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - override - returns ( - uint256 amount, - uint256 first, - uint256 last - ) - { - (first, last) = _unclaimedEpochs(_account); - (uint256 unlocked, ) = _unclaimedRewards(_account, first, last); - amount = unlocked + earned(_account); - } - - /** @dev Returns only the most recently earned rewards */ - function _earned( - address _account, - uint256 _userRewardPerTokenPaid, - uint256 _currentRewardPerToken - ) internal view returns (uint256) { - // current rate per token - rate user previously received - uint256 userRewardDelta = _currentRewardPerToken - _userRewardPerTokenPaid; // + 1 SLOAD - // Short circuit if there is nothing new to distribute - if (userRewardDelta == 0) { - return 0; - } - // new reward = staked tokens * difference in rate - uint256 userNewReward = balanceOf(_account).mulTruncate(userRewardDelta); // + 1 SLOAD - // add to previous rewards - return userNewReward; - } - - /** - * @dev Gets the first and last indexes of array elements containing unclaimed rewards - */ - function _unclaimedEpochs(address _account) - internal - view - returns (uint256 first, uint256 last) - { - uint64 lastClaim = userClaim[_account]; - - uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account); - uint256 lastUnclaimed = _findLastUnclaimed(_account); - - return (firstUnclaimed, lastUnclaimed); - } - - /** - * @dev Sums the cumulative rewards from a valid range - */ - function _unclaimedRewards( - address _account, - uint256 _first, - uint256 _last - ) internal view returns (uint256 amount, uint256 latestTimestamp) { - uint256 currentTime = block.timestamp; - uint64 lastClaim = userClaim[_account]; - - // Check for no rewards unlocked - uint256 totalLen = userRewards[_account].length; - if (_first == 0 && _last == 0) { - if (totalLen == 0 || currentTime <= userRewards[_account][0].start) { - return (0, currentTime); - } - } - // If there are previous unlocks, check for claims that would leave them untouchable - if (_first > 0) { - require( - lastClaim >= userRewards[_account][_first - 1].finish, - "Invalid _first arg: Must claim earlier entries" - ); - } - - uint256 count = _last - _first + 1; - for (uint256 i = 0; i < count; i++) { - uint256 id = _first + i; - Reward memory rwd = userRewards[_account][id]; - - require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch"); - - uint256 endTime = StableMath.min(rwd.finish, currentTime); - uint256 startTime = StableMath.max(rwd.start, lastClaim); - uint256 unclaimed = (endTime - startTime) * rwd.rate; - - amount += unclaimed; - } - - // Calculate last relevant timestamp here to allow users to avoid issue of OOG errors - // by claiming rewards in batches. - latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish); - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findFirstUnclaimed(uint64 _lastClaim, address _account) - internal - view - returns (uint256 first) - { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (_lastClaim > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findLastUnclaimed(address _account) internal view returns (uint256 first) { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (block.timestamp > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /*************************************** - ADMIN - ****************************************/ - - /** - * @dev Notifies the contract that new rewards have been added. - * Calculates an updated rewardRate based on the rewards in period. - * @param _reward Units of RewardToken that have been added to the pool - */ - function notifyRewardAmount(uint256 _reward) - external - override - onlyRewardsDistributor - updateReward(address(0)) - { - require(_reward < 1e24, "Cannot notify with more than a million units"); - - uint256 currentTime = block.timestamp; - // If previous period over, reset rewardRate - if (currentTime >= periodFinish) { - rewardRate = _reward / DURATION; - } - // If additional reward to existing period, calc sum - else { - uint256 remaining = periodFinish - currentTime; - uint256 leftover = remaining * rewardRate; - rewardRate = (_reward + leftover) / DURATION; - } - - lastUpdateTime = currentTime; - periodFinish = currentTime + DURATION; - - emit RewardAdded(_reward); - } -} diff --git a/contracts/legacy/v-TBTC.sol b/contracts/legacy/v-TBTC.sol deleted file mode 100644 index bb539ba4..00000000 --- a/contracts/legacy/v-TBTC.sol +++ /dev/null @@ -1,1994 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.2; - -interface IERC20 { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); -} - -interface IBoostedVaultWithLockup { - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external; - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) external; - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external; - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external; - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) external; - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external; - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() external view returns (uint256); - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() external view returns (uint256); - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) external view returns (uint256); - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last - ); -} - -contract ModuleKeys { - // Governance - // =========== - // keccak256("Governance"); - bytes32 internal constant KEY_GOVERNANCE = - 0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d; - //keccak256("Staking"); - bytes32 internal constant KEY_STAKING = - 0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034; - //keccak256("ProxyAdmin"); - bytes32 internal constant KEY_PROXY_ADMIN = - 0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1; - - // mStable - // ======= - // keccak256("OracleHub"); - bytes32 internal constant KEY_ORACLE_HUB = - 0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040; - // keccak256("Manager"); - bytes32 internal constant KEY_MANAGER = - 0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f; - //keccak256("Recollateraliser"); - bytes32 internal constant KEY_RECOLLATERALISER = - 0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f; - //keccak256("MetaToken"); - bytes32 internal constant KEY_META_TOKEN = - 0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2; - // keccak256("SavingsManager"); - bytes32 internal constant KEY_SAVINGS_MANAGER = - 0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1; - // keccak256("Liquidator"); - bytes32 internal constant KEY_LIQUIDATOR = - 0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4; - // keccak256("InterestValidator"); - bytes32 internal constant KEY_INTEREST_VALIDATOR = - 0xc10a28f028c7f7282a03c90608e38a4a646e136e614e4b07d119280c5f7f839f; -} - -interface INexus { - function governor() external view returns (address); - - function getModule(bytes32 key) external view returns (address); - - function proposeModule(bytes32 _key, address _addr) external; - - function cancelProposedModule(bytes32 _key) external; - - function acceptProposedModule(bytes32 _key) external; - - function acceptProposedModules(bytes32[] calldata _keys) external; - - function requestLockModule(bytes32 _key) external; - - function cancelLockModule(bytes32 _key) external; - - function lockModule(bytes32 _key) external; -} - -abstract contract ImmutableModule is ModuleKeys { - INexus public immutable nexus; - - /** - * @dev Initialization function for upgradable proxy contracts - * @param _nexus Nexus contract address - */ - constructor(address _nexus) { - require(_nexus != address(0), "Nexus address is zero"); - nexus = INexus(_nexus); - } - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - _onlyGovernor(); - _; - } - - function _onlyGovernor() internal view { - require(msg.sender == _governor(), "Only governor can execute"); - } - - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - - /** - * @dev Modifier to allow function calls only from the ProxyAdmin. - */ - modifier onlyProxyAdmin() { - require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Manager. - */ - modifier onlyManager() { - require(msg.sender == _manager(), "Only manager can execute"); - _; - } - - /** - * @dev Returns Governor address from the Nexus - * @return Address of Governor Contract - */ - function _governor() internal view returns (address) { - return nexus.governor(); - } - - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - - /** - * @dev Return Staking Module address from the Nexus - * @return Address of the Staking Module contract - */ - function _staking() internal view returns (address) { - return nexus.getModule(KEY_STAKING); - } - - /** - * @dev Return ProxyAdmin Module address from the Nexus - * @return Address of the ProxyAdmin Module contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } - - /** - * @dev Return MetaToken Module address from the Nexus - * @return Address of the MetaToken Module contract - */ - function _metaToken() internal view returns (address) { - return nexus.getModule(KEY_META_TOKEN); - } - - /** - * @dev Return OracleHub Module address from the Nexus - * @return Address of the OracleHub Module contract - */ - function _oracleHub() internal view returns (address) { - return nexus.getModule(KEY_ORACLE_HUB); - } - - /** - * @dev Return Manager Module address from the Nexus - * @return Address of the Manager Module contract - */ - function _manager() internal view returns (address) { - return nexus.getModule(KEY_MANAGER); - } - - /** - * @dev Return SavingsManager Module address from the Nexus - * @return Address of the SavingsManager Module contract - */ - function _savingsManager() internal view returns (address) { - return nexus.getModule(KEY_SAVINGS_MANAGER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _recollateraliser() internal view returns (address) { - return nexus.getModule(KEY_RECOLLATERALISER); - } -} - -interface IRewardsDistributionRecipient { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); -} - -abstract contract InitializableRewardsDistributionRecipient is - IRewardsDistributionRecipient, - ImmutableModule -{ - // This address has the ability to distribute the rewards - address public rewardsDistributor; - - constructor(address _nexus) ImmutableModule(_nexus) {} - - /** @dev Recipient is a module, governed by mStable governance */ - function _initialize(address _rewardsDistributor) internal { - rewardsDistributor = _rewardsDistributor; - } - - /** - * @dev Only the rewards distributor can notify about rewards - */ - modifier onlyRewardsDistributor() { - require(msg.sender == rewardsDistributor, "Caller is not reward distributor"); - _; - } - - /** - * @dev Change the rewardsDistributor - only called by mStable governor - * @param _rewardsDistributor Address of the new distributor - */ - function setRewardsDistribution(address _rewardsDistributor) external onlyGovernor { - rewardsDistributor = _rewardsDistributor; - } -} - -interface IBoostDirector { - function getBalance(address _user) external returns (uint256); - - function setDirection( - address _old, - address _new, - bool _pokeNew - ) external; - - function whitelistVaults(address[] calldata _vaults) external; -} - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. - */ - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { - size := extcodesize(account) - } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return - functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) - internal - view - returns (bytes memory) - { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) - internal - returns (bytes memory) - { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) private pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} - -library SafeERC20 { - using Address for address; - - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { - _callOptionalReturn( - token, - abi.encodeWithSelector(token.transferFrom.selector, from, to, value) - ); - } - - /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. - */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function _callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that - // the target address contains contract code and also asserts for success in the low-level call. - - bytes memory returndata = address(token).functionCall( - data, - "SafeERC20: low-level call failed" - ); - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} - -contract InitializableReentrancyGuard { - bool private _notEntered; - - function _initializeReentrancyGuard() internal { - // Storing an initial non-zero value makes deployment a bit more - // expensive, but in exchange the refund on every call to nonReentrant - // will be lower in amount. Since refunds are capped to a percetange of - // the total transaction's gas, it is best to keep them low in cases - // like this one, to increase the likelihood of the full refund coming - // into effect. - _notEntered = true; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_notEntered, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _notEntered = false; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _notEntered = true; - } -} - -library StableMath { - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @dev Token Ratios are used when converting between units of bAsset, mAsset and MTA - * Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold) - * bAsset ratio unit for use in exact calculations, - * where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit - */ - uint256 private constant RATIO_SCALE = 1e8; - - /** - * @dev Provides an interface to the scaling unit - * @return Scaling unit (1e18 or 1 * 10**18) - */ - function getFullScale() internal pure returns (uint256) { - return FULL_SCALE; - } - - /** - * @dev Provides an interface to the ratio unit - * @return Ratio scale unit (1e8 or 1 * 10**8) - */ - function getRatioScale() internal pure returns (uint256) { - return RATIO_SCALE; - } - - /** - * @dev Scales a given integer to the power of the full scale. - * @param x Simple uint256 to scale - * @return Scaled value a to an exact number - */ - function scaleInteger(uint256 x) internal pure returns (uint256) { - return x * FULL_SCALE; - } - - /*************************************** - PRECISE ARITHMETIC - ****************************************/ - - /** - * @dev Multiplies two precise units, and then truncates by the full scale - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) { - return mulTruncateScale(x, y, FULL_SCALE); - } - - /** - * @dev Multiplies two precise units, and then truncates by the given scale. For example, - * when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18 - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @param scale Scale unit - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncateScale( - uint256 x, - uint256 y, - uint256 scale - ) internal pure returns (uint256) { - // e.g. assume scale = fullScale - // z = 10e18 * 9e17 = 9e36 - // return 9e38 / 1e18 = 9e18 - return (x * y) / scale; - } - - /** - * @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit, rounded up to the closest base unit. - */ - function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e17 * 17268172638 = 138145381104e17 - uint256 scaled = x * y; - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled + FULL_SCALE - 1; - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil / FULL_SCALE; - } - - /** - * @dev Precisely divides two units, by first scaling the left hand operand. Useful - * for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17) - * @param x Left hand input to division - * @param y Right hand input to division - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e18 * 1e18 = 8e36 - // e.g. 8e36 / 10e18 = 8e17 - return (x * FULL_SCALE) / y; - } - - /*************************************** - RATIO FUNCS - ****************************************/ - - /** - * @dev Multiplies and truncates a token ratio, essentially flooring the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand operand to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return c Result after multiplying the two inputs and then dividing by the ratio scale - */ - function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - return mulTruncateScale(x, ratio, RATIO_SCALE); - } - - /** - * @dev Multiplies and truncates a token ratio, rounding up the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand input to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the shared - * ratio scale, rounded up to the closest base unit. - */ - function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) { - // e.g. How much mAsset should I burn for this bAsset (x)? - // 1e18 * 1e8 = 1e26 - uint256 scaled = x * ratio; - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled + RATIO_SCALE - 1; - // return 100..00.999e8 / 1e8 = 1e18 - return ceil / RATIO_SCALE; - } - - /** - * @dev Precisely divides two ratioed units, by first scaling the left hand operand - * i.e. How much bAsset is this mAsset worth? - * @param x Left hand operand in division - * @param ratio bAsset ratio - * @return c Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - // e.g. 1e14 * 1e8 = 1e22 - // return 1e22 / 1e12 = 1e10 - return (x * RATIO_SCALE) / ratio; - } - - /*************************************** - HELPERS - ****************************************/ - - /** - * @dev Calculates minimum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Minimum of the two inputs - */ - function min(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? y : x; - } - - /** - * @dev Calculated maximum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Maximum of the two inputs - */ - function max(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? x : y; - } - - /** - * @dev Clamps a value to an upper bound - * @param x Left hand input - * @param upperBound Maximum possible value to return - * @return Input x clamped to a maximum value, upperBound - */ - function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) { - return x > upperBound ? upperBound : x; - } -} - -library Root { - /** - * @dev Returns the square root of a given number - * @param x Input - * @return y Square root of Input - */ - function sqrt(uint256 x) internal pure returns (uint256 y) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; // Seven iterations should be enough - uint256 r1 = x / r; - return uint256(r < r1 ? r : r1); - } - } -} - -contract BoostedTokenWrapper is InitializableReentrancyGuard { - using StableMath for uint256; - using SafeERC20 for IERC20; - - event Transfer(address indexed from, address indexed to, uint256 value); - - string private _name; - string private _symbol; - - IERC20 public immutable stakingToken; - IBoostDirector public immutable boostDirector; - - uint256 private _totalBoostedSupply; - mapping(address => uint256) private _boostedBalances; - mapping(address => uint256) private _rawBalances; - - // Vars for use in the boost calculations - uint256 private constant MIN_DEPOSIT = 1e18; - uint256 private constant MAX_VMTA = 600000e18; - uint256 private constant MAX_BOOST = 3e18; - uint256 private constant MIN_BOOST = 1e18; - uint256 private constant FLOOR = 98e16; - uint256 public immutable boostCoeff; // scaled by 10 - uint256 public immutable priceCoeff; - - /** - * @dev TokenWrapper constructor - * @param _stakingToken Wrapped token to be staked - * @param _boostDirector vMTA boost director - * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 - */ - constructor( - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _boostCoeff - ) { - stakingToken = IERC20(_stakingToken); - boostDirector = IBoostDirector(_boostDirector); - priceCoeff = _priceCoeff; - boostCoeff = _boostCoeff; - } - - function _initialize(string memory _nameArg, string memory _symbolArg) internal { - _initializeReentrancyGuard(); - _name = _nameArg; - _symbol = _symbolArg; - } - - function name() public view virtual returns (string memory) { - return _name; - } - - function symbol() public view virtual returns (string memory) { - return _symbol; - } - - function decimals() public view virtual returns (uint8) { - return 18; - } - - /** - * @dev Get the total boosted amount - * @return uint256 total supply - */ - function totalSupply() public view returns (uint256) { - return _totalBoostedSupply; - } - - /** - * @dev Get the boosted balance of a given account - * @param _account User for which to retrieve balance - */ - function balanceOf(address _account) public view returns (uint256) { - return _boostedBalances[_account]; - } - - /** - * @dev Get the RAW balance of a given account - * @param _account User for which to retrieve balance - */ - function rawBalanceOf(address _account) public view returns (uint256) { - return _rawBalances[_account]; - } - - /** - * @dev Read the boost for the given address - * @param _account User for which to return the boost - * @return boost where 1x == 1e18 - */ - function getBoost(address _account) public view returns (uint256) { - return balanceOf(_account).divPrecisely(rawBalanceOf(_account)); - } - - /** - * @dev Deposits a given amount of StakingToken from sender - * @param _amount Units of StakingToken - */ - function _stakeRaw(address _beneficiary, uint256 _amount) internal nonReentrant { - _rawBalances[_beneficiary] += _amount; - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - } - - /** - * @dev Withdraws a given stake from sender - * @param _amount Units of StakingToken - */ - function _withdrawRaw(uint256 _amount) internal nonReentrant { - _rawBalances[msg.sender] -= _amount; - stakingToken.safeTransfer(msg.sender, _amount); - } - - /** - * @dev Updates the boost for the given address according to the formula - * boost = min(0.5 + c * vMTA_balance / imUSD_locked^(7/8), 1.5) - * If rawBalance <= MIN_DEPOSIT, boost is 0 - * @param _account User for which to update the boost - */ - function _setBoost(address _account) internal { - uint256 rawBalance = _rawBalances[_account]; - uint256 boostedBalance = _boostedBalances[_account]; - uint256 boost = MIN_BOOST; - - // Check whether balance is sufficient - // is_boosted is used to minimize gas usage - uint256 scaledBalance = (rawBalance * priceCoeff) / 1e18; - if (scaledBalance >= MIN_DEPOSIT) { - uint256 votingWeight = boostDirector.getBalance(_account); - boost = _computeBoost(scaledBalance, votingWeight); - } - - uint256 newBoostedBalance = rawBalance.mulTruncate(boost); - - if (newBoostedBalance != boostedBalance) { - _totalBoostedSupply = _totalBoostedSupply - boostedBalance + newBoostedBalance; - _boostedBalances[_account] = newBoostedBalance; - - if (newBoostedBalance > boostedBalance) { - emit Transfer(address(0), _account, newBoostedBalance - boostedBalance); - } else { - emit Transfer(_account, address(0), boostedBalance - newBoostedBalance); - } - } - } - - /** - * @dev Computes the boost for - * boost = min(m, max(1, 0.95 + c * min(voting_weight, f) / deposit^(3/4))) - * @param _scaledDeposit deposit amount in terms of USD - */ - function _computeBoost(uint256 _scaledDeposit, uint256 _votingWeight) - private - view - returns (uint256 boost) - { - if (_votingWeight == 0) return MIN_BOOST; - - // Compute balance to the power 3/4 - uint256 sqrt1 = Root.sqrt(_scaledDeposit * 1e6); - uint256 sqrt2 = Root.sqrt(sqrt1); - uint256 denominator = sqrt1 * sqrt2; - boost = - (((StableMath.min(_votingWeight, MAX_VMTA) * boostCoeff) / 10) * 1e18) / - denominator; - boost = StableMath.min(MAX_BOOST, StableMath.max(MIN_BOOST, FLOOR + boost)); - } -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library SafeCast { - /** - * @dev Returns the downcasted uint128 from uint256, reverting on - * overflow (when the input is greater than largest uint128). - * - * Counterpart to Solidity's `uint128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - */ - function toUint128(uint256 value) internal pure returns (uint128) { - require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); - return uint128(value); - } - - /** - * @dev Returns the downcasted uint64 from uint256, reverting on - * overflow (when the input is greater than largest uint64). - * - * Counterpart to Solidity's `uint64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - */ - function toUint64(uint256 value) internal pure returns (uint64) { - require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); - return uint64(value); - } - - /** - * @dev Returns the downcasted uint32 from uint256, reverting on - * overflow (when the input is greater than largest uint32). - * - * Counterpart to Solidity's `uint32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - */ - function toUint32(uint256 value) internal pure returns (uint32) { - require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); - return uint32(value); - } - - /** - * @dev Returns the downcasted uint16 from uint256, reverting on - * overflow (when the input is greater than largest uint16). - * - * Counterpart to Solidity's `uint16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - */ - function toUint16(uint256 value) internal pure returns (uint16) { - require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); - return uint16(value); - } - - /** - * @dev Returns the downcasted uint8 from uint256, reverting on - * overflow (when the input is greater than largest uint8). - * - * Counterpart to Solidity's `uint8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - */ - function toUint8(uint256 value) internal pure returns (uint8) { - require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); - return uint8(value); - } - - /** - * @dev Converts a signed int256 into an unsigned uint256. - * - * Requirements: - * - * - input must be greater than or equal to 0. - */ - function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); - return uint256(value); - } - - /** - * @dev Returns the downcasted int128 from int256, reverting on - * overflow (when the input is less than smallest int128 or - * greater than largest int128). - * - * Counterpart to Solidity's `int128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - * - * _Available since v3.1._ - */ - function toInt128(int256 value) internal pure returns (int128) { - require(value >= -2**127 && value < 2**127, "SafeCast: value doesn't fit in 128 bits"); - return int128(value); - } - - /** - * @dev Returns the downcasted int64 from int256, reverting on - * overflow (when the input is less than smallest int64 or - * greater than largest int64). - * - * Counterpart to Solidity's `int64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - * - * _Available since v3.1._ - */ - function toInt64(int256 value) internal pure returns (int64) { - require(value >= -2**63 && value < 2**63, "SafeCast: value doesn't fit in 64 bits"); - return int64(value); - } - - /** - * @dev Returns the downcasted int32 from int256, reverting on - * overflow (when the input is less than smallest int32 or - * greater than largest int32). - * - * Counterpart to Solidity's `int32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - * - * _Available since v3.1._ - */ - function toInt32(int256 value) internal pure returns (int32) { - require(value >= -2**31 && value < 2**31, "SafeCast: value doesn't fit in 32 bits"); - return int32(value); - } - - /** - * @dev Returns the downcasted int16 from int256, reverting on - * overflow (when the input is less than smallest int16 or - * greater than largest int16). - * - * Counterpart to Solidity's `int16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - * - * _Available since v3.1._ - */ - function toInt16(int256 value) internal pure returns (int16) { - require(value >= -2**15 && value < 2**15, "SafeCast: value doesn't fit in 16 bits"); - return int16(value); - } - - /** - * @dev Returns the downcasted int8 from int256, reverting on - * overflow (when the input is less than smallest int8 or - * greater than largest int8). - * - * Counterpart to Solidity's `int8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - * - * _Available since v3.1._ - */ - function toInt8(int256 value) internal pure returns (int8) { - require(value >= -2**7 && value < 2**7, "SafeCast: value doesn't fit in 8 bits"); - return int8(value); - } - - /** - * @dev Converts an unsigned uint256 into a signed int256. - * - * Requirements: - * - * - input must be less than or equal to maxInt256. - */ - function toInt256(uint256 value) internal pure returns (int256) { - require(value < 2**255, "SafeCast: value doesn't fit in an int256"); - return int256(value); - } -} - -// Internal -// Libs -/** - * @title BoostedSavingsVault - * @author mStable - * @notice Accrues rewards second by second, based on a users boosted balance - * @dev Forked from rewards/staking/StakingRewards.sol - * Changes: - * - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months) - * - `updateBoost` hook called after every external action to reset a users boost - * - Struct packing of common data - * - Searching for and claiming of unlocked rewards - */ -contract BoostedSavingsVault is - IBoostedVaultWithLockup, - Initializable, - InitializableRewardsDistributionRecipient, - BoostedTokenWrapper -{ - using SafeERC20 for IERC20; - using StableMath for uint256; - using SafeCast for uint256; - - event RewardAdded(uint256 reward); - event Staked(address indexed user, uint256 amount, address payer); - event Withdrawn(address indexed user, uint256 amount); - event Poked(address indexed user); - event RewardPaid(address indexed user, uint256 reward); - - IERC20 public immutable rewardsToken; - - uint64 public constant DURATION = 7 days; - // Length of token lockup, after rewards are earned - uint256 public constant LOCKUP = 26 weeks; - // Percentage of earned tokens unlocked immediately - uint64 public constant UNLOCK = 33e16; - - // Timestamp for current period finish - uint256 public periodFinish; - // RewardRate for the rest of the PERIOD - uint256 public rewardRate; - // Last time any user took action - uint256 public lastUpdateTime; - // Ever increasing rewardPerToken rate, based on % of total supply - uint256 public rewardPerTokenStored; - mapping(address => UserData) public userData; - // Locked reward tracking - mapping(address => Reward[]) public userRewards; - mapping(address => uint64) public userClaim; - - struct UserData { - uint128 rewardPerTokenPaid; - uint128 rewards; - uint64 lastAction; - uint64 rewardCount; - } - - struct Reward { - uint64 start; - uint64 finish; - uint128 rate; - } - - constructor( - address _nexus, - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _coeff, - address _rewardsToken - ) - InitializableRewardsDistributionRecipient(_nexus) - BoostedTokenWrapper(_stakingToken, _boostDirector, _priceCoeff, _coeff) - { - rewardsToken = IERC20(_rewardsToken); - } - - /** - * @dev StakingRewards is a TokenWrapper and RewardRecipient - * Constants added to bytecode at deployTime to reduce SLOAD cost - */ - function initialize( - address _rewardsDistributor, - string calldata _nameArg, - string calldata _symbolArg - ) external initializer { - InitializableRewardsDistributionRecipient._initialize(_rewardsDistributor); - BoostedTokenWrapper._initialize(_nameArg, _symbolArg); - } - - /** - * @dev Updates the reward for a given address, before executing function. - * Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to - * (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed - * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). - */ - modifier updateReward(address _account) { - uint256 currentTime = block.timestamp; - uint64 currentTime64 = SafeCast.toUint64(currentTime); - - // Setting of global vars - (uint256 newRewardPerToken, uint256 lastApplicableTime) = _rewardPerToken(); - // If statement protects against loss in initialisation case - if (newRewardPerToken > 0) { - rewardPerTokenStored = newRewardPerToken; - lastUpdateTime = lastApplicableTime; - - // Setting of personal vars based on new globals - if (_account != address(0)) { - UserData memory data = userData[_account]; - uint256 earned_ = _earned(_account, data.rewardPerTokenPaid, newRewardPerToken); - - // If earned == 0, then it must either be the initial stake, or an action in the - // same block, since new rewards unlock after each block. - if (earned_ > 0) { - uint256 unlocked = earned_.mulTruncate(UNLOCK); - uint256 locked = earned_ - unlocked; - - userRewards[_account].push( - Reward({ - start: SafeCast.toUint64(LOCKUP + data.lastAction), - finish: SafeCast.toUint64(LOCKUP + currentTime), - rate: SafeCast.toUint128(locked / (currentTime - data.lastAction)) - }) - ); - - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: SafeCast.toUint128(unlocked + data.rewards), - lastAction: currentTime64, - rewardCount: data.rewardCount + 1 - }); - } else { - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: data.rewards, - lastAction: currentTime64, - rewardCount: data.rewardCount - }); - } - } - } else if (_account != address(0)) { - // This should only be hit once, for first staker in initialisation case - userData[_account].lastAction = currentTime64; - } - _; - } - - /** @dev Updates the boost for a given address, after the rest of the function has executed */ - modifier updateBoost(address _account) { - _; - _setBoost(_account); - } - - /*************************************** - ACTIONS - EXTERNAL - ****************************************/ - - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _stake(msg.sender, _amount); - } - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) - external - override - updateReward(_beneficiary) - updateBoost(_beneficiary) - { - _stake(_beneficiary, _amount); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external override updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(rawBalanceOf(msg.sender)); - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - _claimRewards(first, last); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(rawBalanceOf(msg.sender)); - _claimRewards(_first, _last); - } - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(_amount); - } - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external override updateReward(msg.sender) updateBoost(msg.sender) { - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - if (unlocked > 0) { - rewardsToken.safeTransfer(msg.sender, unlocked); - emit RewardPaid(msg.sender, unlocked); - } - } - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external override updateReward(msg.sender) updateBoost(msg.sender) { - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - - _claimRewards(first, last); - } - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _claimRewards(_first, _last); - } - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) - external - override - updateReward(_account) - updateBoost(_account) - { - emit Poked(_account); - } - - /*************************************** - ACTIONS - INTERNAL - ****************************************/ - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function _claimRewards(uint256 _first, uint256 _last) internal { - (uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last); - userClaim[msg.sender] = uint64(lastTimestamp); - - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - uint256 total = unclaimed + unlocked; - - if (total > 0) { - rewardsToken.safeTransfer(msg.sender, total); - - emit RewardPaid(msg.sender, total); - } - } - - /** - * @dev Internally stakes an amount by depositing from sender, - * and crediting to the specified beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function _stake(address _beneficiary, uint256 _amount) internal { - require(_amount > 0, "Cannot stake 0"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - _stakeRaw(_beneficiary, _amount); - emit Staked(_beneficiary, _amount, msg.sender); - } - - /** - * @dev Withdraws raw units from the sender - * @param _amount Units of StakingToken - */ - function _withdraw(uint256 _amount) internal { - require(_amount > 0, "Cannot withdraw 0"); - _withdrawRaw(_amount); - emit Withdrawn(msg.sender, _amount); - } - - /*************************************** - GETTERS - ****************************************/ - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() external view override returns (IERC20) { - return rewardsToken; - } - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() public view override returns (uint256) { - return StableMath.min(block.timestamp, periodFinish); - } - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() public view override returns (uint256) { - (uint256 rewardPerToken_, ) = _rewardPerToken(); - return rewardPerToken_; - } - - function _rewardPerToken() - internal - view - returns (uint256 rewardPerToken_, uint256 lastTimeRewardApplicable_) - { - uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD - uint256 timeDelta = lastApplicableTime - lastUpdateTime; // + 1 SLOAD - // If this has been called twice in the same block, shortcircuit to reduce gas - if (timeDelta == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units to distribute = rewardRate * timeSinceLastUpdate - uint256 rewardUnitsToDistribute = rewardRate * timeDelta; // + 1 SLOAD - uint256 supply = totalSupply(); // + 1 SLOAD - // If there is no StakingToken liquidity, avoid div(0) - // If there is nothing to distribute, short circuit - if (supply == 0 || rewardUnitsToDistribute == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens - uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(supply); - // return summed rate - return (rewardPerTokenStored + unitsToDistributePerToken, lastApplicableTime); // + 1 SLOAD - } - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) public view override returns (uint256) { - uint256 newEarned = _earned( - _account, - userData[_account].rewardPerTokenPaid, - rewardPerToken() - ); - uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK); - return immediatelyUnlocked + userData[_account].rewards; - } - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - override - returns ( - uint256 amount, - uint256 first, - uint256 last - ) - { - (first, last) = _unclaimedEpochs(_account); - (uint256 unlocked, ) = _unclaimedRewards(_account, first, last); - amount = unlocked + earned(_account); - } - - /** @dev Returns only the most recently earned rewards */ - function _earned( - address _account, - uint256 _userRewardPerTokenPaid, - uint256 _currentRewardPerToken - ) internal view returns (uint256) { - // current rate per token - rate user previously received - uint256 userRewardDelta = _currentRewardPerToken - _userRewardPerTokenPaid; // + 1 SLOAD - // Short circuit if there is nothing new to distribute - if (userRewardDelta == 0) { - return 0; - } - // new reward = staked tokens * difference in rate - uint256 userNewReward = balanceOf(_account).mulTruncate(userRewardDelta); // + 1 SLOAD - // add to previous rewards - return userNewReward; - } - - /** - * @dev Gets the first and last indexes of array elements containing unclaimed rewards - */ - function _unclaimedEpochs(address _account) - internal - view - returns (uint256 first, uint256 last) - { - uint64 lastClaim = userClaim[_account]; - - uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account); - uint256 lastUnclaimed = _findLastUnclaimed(_account); - - return (firstUnclaimed, lastUnclaimed); - } - - /** - * @dev Sums the cumulative rewards from a valid range - */ - function _unclaimedRewards( - address _account, - uint256 _first, - uint256 _last - ) internal view returns (uint256 amount, uint256 latestTimestamp) { - uint256 currentTime = block.timestamp; - uint64 lastClaim = userClaim[_account]; - - // Check for no rewards unlocked - uint256 totalLen = userRewards[_account].length; - if (_first == 0 && _last == 0) { - if (totalLen == 0 || currentTime <= userRewards[_account][0].start) { - return (0, currentTime); - } - } - // If there are previous unlocks, check for claims that would leave them untouchable - if (_first > 0) { - require( - lastClaim >= userRewards[_account][_first - 1].finish, - "Invalid _first arg: Must claim earlier entries" - ); - } - - uint256 count = _last - _first + 1; - for (uint256 i = 0; i < count; i++) { - uint256 id = _first + i; - Reward memory rwd = userRewards[_account][id]; - - require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch"); - - uint256 endTime = StableMath.min(rwd.finish, currentTime); - uint256 startTime = StableMath.max(rwd.start, lastClaim); - uint256 unclaimed = (endTime - startTime) * rwd.rate; - - amount += unclaimed; - } - - // Calculate last relevant timestamp here to allow users to avoid issue of OOG errors - // by claiming rewards in batches. - latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish); - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findFirstUnclaimed(uint64 _lastClaim, address _account) - internal - view - returns (uint256 first) - { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (_lastClaim > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findLastUnclaimed(address _account) internal view returns (uint256 first) { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (block.timestamp > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /*************************************** - ADMIN - ****************************************/ - - /** - * @dev Notifies the contract that new rewards have been added. - * Calculates an updated rewardRate based on the rewards in period. - * @param _reward Units of RewardToken that have been added to the pool - */ - function notifyRewardAmount(uint256 _reward) - external - override - onlyRewardsDistributor - updateReward(address(0)) - { - require(_reward < 1e24, "Cannot notify with more than a million units"); - - uint256 currentTime = block.timestamp; - // If previous period over, reset rewardRate - if (currentTime >= periodFinish) { - rewardRate = _reward / DURATION; - } - // If additional reward to existing period, calc sum - else { - uint256 remaining = periodFinish - currentTime; - uint256 leftover = remaining * rewardRate; - rewardRate = (_reward + leftover) / DURATION; - } - - lastUpdateTime = currentTime; - periodFinish = currentTime + DURATION; - - emit RewardAdded(_reward); - } -} diff --git a/contracts/legacy/v-alUSD.sol b/contracts/legacy/v-alUSD.sol deleted file mode 100644 index 38aa64de..00000000 --- a/contracts/legacy/v-alUSD.sol +++ /dev/null @@ -1,2139 +0,0 @@ -pragma solidity 0.8.2; - -interface IERC20 { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); -} - -interface IRewardsRecipientWithPlatformToken { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); - - function getPlatformToken() external view returns (IERC20); -} - -interface IBoostedDualVaultWithLockup { - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external; - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) external; - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external; - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external; - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) external; - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external; - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() external view returns (uint256); - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() external view returns (uint256, uint256); - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) external view returns (uint256, uint256); - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last, - uint256 platformAmount - ); -} - -contract ModuleKeys { - // Governance - // =========== - // keccak256("Governance"); - bytes32 internal constant KEY_GOVERNANCE = - 0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d; - //keccak256("Staking"); - bytes32 internal constant KEY_STAKING = - 0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034; - //keccak256("ProxyAdmin"); - bytes32 internal constant KEY_PROXY_ADMIN = - 0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1; - - // mStable - // ======= - // keccak256("OracleHub"); - bytes32 internal constant KEY_ORACLE_HUB = - 0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040; - // keccak256("Manager"); - bytes32 internal constant KEY_MANAGER = - 0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f; - //keccak256("Recollateraliser"); - bytes32 internal constant KEY_RECOLLATERALISER = - 0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f; - //keccak256("MetaToken"); - bytes32 internal constant KEY_META_TOKEN = - 0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2; - // keccak256("SavingsManager"); - bytes32 internal constant KEY_SAVINGS_MANAGER = - 0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1; - // keccak256("Liquidator"); - bytes32 internal constant KEY_LIQUIDATOR = - 0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4; - // keccak256("InterestValidator"); - bytes32 internal constant KEY_INTEREST_VALIDATOR = - 0xc10a28f028c7f7282a03c90608e38a4a646e136e614e4b07d119280c5f7f839f; -} - -interface INexus { - function governor() external view returns (address); - - function getModule(bytes32 key) external view returns (address); - - function proposeModule(bytes32 _key, address _addr) external; - - function cancelProposedModule(bytes32 _key) external; - - function acceptProposedModule(bytes32 _key) external; - - function acceptProposedModules(bytes32[] calldata _keys) external; - - function requestLockModule(bytes32 _key) external; - - function cancelLockModule(bytes32 _key) external; - - function lockModule(bytes32 _key) external; -} - -abstract contract ImmutableModule is ModuleKeys { - INexus public immutable nexus; - - /** - * @dev Initialization function for upgradable proxy contracts - * @param _nexus Nexus contract address - */ - constructor(address _nexus) { - require(_nexus != address(0), "Nexus address is zero"); - nexus = INexus(_nexus); - } - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - _onlyGovernor(); - _; - } - - function _onlyGovernor() internal view { - require(msg.sender == _governor(), "Only governor can execute"); - } - - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - - /** - * @dev Returns Governor address from the Nexus - * @return Address of Governor Contract - */ - function _governor() internal view returns (address) { - return nexus.governor(); - } - - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - - /** - * @dev Return SavingsManager Module address from the Nexus - * @return Address of the SavingsManager Module contract - */ - function _savingsManager() internal view returns (address) { - return nexus.getModule(KEY_SAVINGS_MANAGER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _recollateraliser() internal view returns (address) { - return nexus.getModule(KEY_RECOLLATERALISER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _liquidator() internal view returns (address) { - return nexus.getModule(KEY_LIQUIDATOR); - } - - /** - * @dev Return ProxyAdmin Module address from the Nexus - * @return Address of the ProxyAdmin Module contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } -} - -interface IRewardsDistributionRecipient { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); -} - -abstract contract InitializableRewardsDistributionRecipient is - IRewardsDistributionRecipient, - ImmutableModule -{ - // This address has the ability to distribute the rewards - address public rewardsDistributor; - - constructor(address _nexus) ImmutableModule(_nexus) {} - - /** @dev Recipient is a module, governed by mStable governance */ - function _initialize(address _rewardsDistributor) internal { - rewardsDistributor = _rewardsDistributor; - } - - /** - * @dev Only the rewards distributor can notify about rewards - */ - modifier onlyRewardsDistributor() { - require(msg.sender == rewardsDistributor, "Caller is not reward distributor"); - _; - } - - /** - * @dev Change the rewardsDistributor - only called by mStable governor - * @param _rewardsDistributor Address of the new distributor - */ - function setRewardsDistribution(address _rewardsDistributor) external onlyGovernor { - rewardsDistributor = _rewardsDistributor; - } -} - -interface IBoostDirector { - function getBalance(address _user) external returns (uint256); - - function setDirection( - address _old, - address _new, - bool _pokeNew - ) external; - - function whitelistVaults(address[] calldata _vaults) external; -} - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. - */ - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { - size := extcodesize(account) - } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return - functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) - internal - view - returns (bytes memory) - { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) - internal - returns (bytes memory) - { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) private pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} - -library SafeERC20 { - using Address for address; - - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { - _callOptionalReturn( - token, - abi.encodeWithSelector(token.transferFrom.selector, from, to, value) - ); - } - - /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. - */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function _callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that - // the target address contains contract code and also asserts for success in the low-level call. - - bytes memory returndata = address(token).functionCall( - data, - "SafeERC20: low-level call failed" - ); - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} - -contract InitializableReentrancyGuard { - bool private _notEntered; - - function _initializeReentrancyGuard() internal { - // Storing an initial non-zero value makes deployment a bit more - // expensive, but in exchange the refund on every call to nonReentrant - // will be lower in amount. Since refunds are capped to a percetange of - // the total transaction's gas, it is best to keep them low in cases - // like this one, to increase the likelihood of the full refund coming - // into effect. - _notEntered = true; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_notEntered, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _notEntered = false; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _notEntered = true; - } -} - -library StableMath { - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @dev Token Ratios are used when converting between units of bAsset, mAsset and MTA - * Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold) - * bAsset ratio unit for use in exact calculations, - * where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit - */ - uint256 private constant RATIO_SCALE = 1e8; - - /** - * @dev Provides an interface to the scaling unit - * @return Scaling unit (1e18 or 1 * 10**18) - */ - function getFullScale() internal pure returns (uint256) { - return FULL_SCALE; - } - - /** - * @dev Provides an interface to the ratio unit - * @return Ratio scale unit (1e8 or 1 * 10**8) - */ - function getRatioScale() internal pure returns (uint256) { - return RATIO_SCALE; - } - - /** - * @dev Scales a given integer to the power of the full scale. - * @param x Simple uint256 to scale - * @return Scaled value a to an exact number - */ - function scaleInteger(uint256 x) internal pure returns (uint256) { - return x * FULL_SCALE; - } - - /*************************************** - PRECISE ARITHMETIC - ****************************************/ - - /** - * @dev Multiplies two precise units, and then truncates by the full scale - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) { - return mulTruncateScale(x, y, FULL_SCALE); - } - - /** - * @dev Multiplies two precise units, and then truncates by the given scale. For example, - * when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18 - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @param scale Scale unit - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncateScale( - uint256 x, - uint256 y, - uint256 scale - ) internal pure returns (uint256) { - // e.g. assume scale = fullScale - // z = 10e18 * 9e17 = 9e36 - // return 9e36 / 1e18 = 9e18 - return (x * y) / scale; - } - - /** - * @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit, rounded up to the closest base unit. - */ - function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e17 * 17268172638 = 138145381104e17 - uint256 scaled = x * y; - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled + FULL_SCALE - 1; - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil / FULL_SCALE; - } - - /** - * @dev Precisely divides two units, by first scaling the left hand operand. Useful - * for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17) - * @param x Left hand input to division - * @param y Right hand input to division - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e18 * 1e18 = 8e36 - // e.g. 8e36 / 10e18 = 8e17 - return (x * FULL_SCALE) / y; - } - - /*************************************** - RATIO FUNCS - ****************************************/ - - /** - * @dev Multiplies and truncates a token ratio, essentially flooring the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand operand to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return c Result after multiplying the two inputs and then dividing by the ratio scale - */ - function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - return mulTruncateScale(x, ratio, RATIO_SCALE); - } - - /** - * @dev Multiplies and truncates a token ratio, rounding up the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand input to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the shared - * ratio scale, rounded up to the closest base unit. - */ - function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) { - // e.g. How much mAsset should I burn for this bAsset (x)? - // 1e18 * 1e8 = 1e26 - uint256 scaled = x * ratio; - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled + RATIO_SCALE - 1; - // return 100..00.999e8 / 1e8 = 1e18 - return ceil / RATIO_SCALE; - } - - /** - * @dev Precisely divides two ratioed units, by first scaling the left hand operand - * i.e. How much bAsset is this mAsset worth? - * @param x Left hand operand in division - * @param ratio bAsset ratio - * @return c Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - // e.g. 1e14 * 1e8 = 1e22 - // return 1e22 / 1e12 = 1e10 - return (x * RATIO_SCALE) / ratio; - } - - /*************************************** - HELPERS - ****************************************/ - - /** - * @dev Calculates minimum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Minimum of the two inputs - */ - function min(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? y : x; - } - - /** - * @dev Calculated maximum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Maximum of the two inputs - */ - function max(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? x : y; - } - - /** - * @dev Clamps a value to an upper bound - * @param x Left hand input - * @param upperBound Maximum possible value to return - * @return Input x clamped to a maximum value, upperBound - */ - function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) { - return x > upperBound ? upperBound : x; - } -} - -library Root { - /** - * @dev Returns the square root of a given number - * @param x Input - * @return y Square root of Input - */ - function sqrt(uint256 x) internal pure returns (uint256 y) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; // Seven iterations should be enough - uint256 r1 = x / r; - return uint256(r < r1 ? r : r1); - } - } -} - -contract BoostedTokenWrapper is InitializableReentrancyGuard { - using StableMath for uint256; - using SafeERC20 for IERC20; - - event Transfer(address indexed from, address indexed to, uint256 value); - - string private _name; - string private _symbol; - - IERC20 public immutable stakingToken; - IBoostDirector public immutable boostDirector; - - uint256 private _totalBoostedSupply; - mapping(address => uint256) private _boostedBalances; - mapping(address => uint256) private _rawBalances; - - // Vars for use in the boost calculations - uint256 private constant MIN_DEPOSIT = 1e18; - uint256 private constant MAX_VMTA = 600000e18; - uint256 private constant MAX_BOOST = 3e18; - uint256 private constant MIN_BOOST = 1e18; - uint256 private constant FLOOR = 98e16; - uint256 public immutable boostCoeff; // scaled by 10 - uint256 public immutable priceCoeff; - - /** - * @dev TokenWrapper constructor - * @param _stakingToken Wrapped token to be staked - * @param _boostDirector vMTA boost director - * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 - * @param _boostCoeff Boost coefficent using the the boost formula - */ - constructor( - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _boostCoeff - ) { - stakingToken = IERC20(_stakingToken); - boostDirector = IBoostDirector(_boostDirector); - priceCoeff = _priceCoeff; - boostCoeff = _boostCoeff; - } - - /** - * @param _nameArg token name. eg imUSD Vault or GUSD Feeder Pool Vault - * @param _symbolArg token symbol. eg v-imUSD or v-fPmUSD/GUSD - */ - function _initialize(string memory _nameArg, string memory _symbolArg) internal { - _initializeReentrancyGuard(); - _name = _nameArg; - _symbol = _symbolArg; - } - - function name() public view virtual returns (string memory) { - return _name; - } - - function symbol() public view virtual returns (string memory) { - return _symbol; - } - - function decimals() public view virtual returns (uint8) { - return 18; - } - - /** - * @dev Get the total boosted amount - * @return uint256 total supply - */ - function totalSupply() public view returns (uint256) { - return _totalBoostedSupply; - } - - /** - * @dev Get the boosted balance of a given account - * @param _account User for which to retrieve balance - */ - function balanceOf(address _account) public view returns (uint256) { - return _boostedBalances[_account]; - } - - /** - * @dev Get the RAW balance of a given account - * @param _account User for which to retrieve balance - */ - function rawBalanceOf(address _account) public view returns (uint256) { - return _rawBalances[_account]; - } - - /** - * @dev Read the boost for the given address - * @param _account User for which to return the boost - * @return boost where 1x == 1e18 - */ - function getBoost(address _account) public view returns (uint256) { - return balanceOf(_account).divPrecisely(rawBalanceOf(_account)); - } - - /** - * @dev Deposits a given amount of StakingToken from sender - * @param _amount Units of StakingToken - */ - function _stakeRaw(address _beneficiary, uint256 _amount) internal nonReentrant { - _rawBalances[_beneficiary] += _amount; - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - } - - /** - * @dev Withdraws a given stake from sender - * @param _amount Units of StakingToken - */ - function _withdrawRaw(uint256 _amount) internal nonReentrant { - _rawBalances[msg.sender] -= _amount; - stakingToken.safeTransfer(msg.sender, _amount); - } - - /** - * @dev Updates the boost for the given address according to the formula - * boost = min(0.5 + c * vMTA_balance / imUSD_locked^(7/8), 1.5) - * If rawBalance <= MIN_DEPOSIT, boost is 0 - * @param _account User for which to update the boost - */ - function _setBoost(address _account) internal { - uint256 rawBalance = _rawBalances[_account]; - uint256 boostedBalance = _boostedBalances[_account]; - uint256 boost = MIN_BOOST; - - // Check whether balance is sufficient - // is_boosted is used to minimize gas usage - uint256 scaledBalance = (rawBalance * priceCoeff) / 1e18; - if (scaledBalance >= MIN_DEPOSIT) { - uint256 votingWeight = boostDirector.getBalance(_account); - boost = _computeBoost(scaledBalance, votingWeight); - } - - uint256 newBoostedBalance = rawBalance.mulTruncate(boost); - - if (newBoostedBalance != boostedBalance) { - _totalBoostedSupply = _totalBoostedSupply - boostedBalance + newBoostedBalance; - _boostedBalances[_account] = newBoostedBalance; - - if (newBoostedBalance > boostedBalance) { - emit Transfer(address(0), _account, newBoostedBalance - boostedBalance); - } else { - emit Transfer(_account, address(0), boostedBalance - newBoostedBalance); - } - } - } - - /** - * @dev Computes the boost for - * boost = min(m, max(1, 0.95 + c * min(voting_weight, f) / deposit^(3/4))) - * @param _scaledDeposit deposit amount in terms of USD - */ - function _computeBoost(uint256 _scaledDeposit, uint256 _votingWeight) - private - view - returns (uint256 boost) - { - if (_votingWeight == 0) return MIN_BOOST; - - // Compute balance to the power 3/4 - uint256 sqrt1 = Root.sqrt(_scaledDeposit * 1e6); - uint256 sqrt2 = Root.sqrt(sqrt1); - uint256 denominator = sqrt1 * sqrt2; - boost = - (((StableMath.min(_votingWeight, MAX_VMTA) * boostCoeff) / 10) * 1e18) / - denominator; - boost = StableMath.min(MAX_BOOST, StableMath.max(MIN_BOOST, FLOOR + boost)); - } -} - -library MassetHelpers { - using SafeERC20 for IERC20; - - function transferReturnBalance( - address _sender, - address _recipient, - address _bAsset, - uint256 _qty - ) internal returns (uint256 receivedQty, uint256 recipientBalance) { - uint256 balBefore = IERC20(_bAsset).balanceOf(_recipient); - IERC20(_bAsset).safeTransferFrom(_sender, _recipient, _qty); - recipientBalance = IERC20(_bAsset).balanceOf(_recipient); - receivedQty = recipientBalance - balBefore; - } - - function safeInfiniteApprove(address _asset, address _spender) internal { - IERC20(_asset).safeApprove(_spender, 0); - IERC20(_asset).safeApprove(_spender, 2**256 - 1); - } -} - -contract PlatformTokenVendor { - IERC20 public immutable platformToken; - address public immutable parentStakingContract; - - /** @dev Simple constructor that stores the parent address */ - constructor(IERC20 _platformToken) public { - parentStakingContract = msg.sender; - platformToken = _platformToken; - MassetHelpers.safeInfiniteApprove(address(_platformToken), msg.sender); - } - - /** - * @dev Re-approves the StakingReward contract to spend the platform token. - * Just incase for some reason approval has been reset. - */ - function reApproveOwner() external { - MassetHelpers.safeInfiniteApprove(address(platformToken), parentStakingContract); - } -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library SafeCast { - /** - * @dev Returns the downcasted uint128 from uint256, reverting on - * overflow (when the input is greater than largest uint128). - * - * Counterpart to Solidity's `uint128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - */ - function toUint128(uint256 value) internal pure returns (uint128) { - require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); - return uint128(value); - } - - /** - * @dev Returns the downcasted uint64 from uint256, reverting on - * overflow (when the input is greater than largest uint64). - * - * Counterpart to Solidity's `uint64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - */ - function toUint64(uint256 value) internal pure returns (uint64) { - require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); - return uint64(value); - } - - /** - * @dev Returns the downcasted uint32 from uint256, reverting on - * overflow (when the input is greater than largest uint32). - * - * Counterpart to Solidity's `uint32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - */ - function toUint32(uint256 value) internal pure returns (uint32) { - require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); - return uint32(value); - } - - /** - * @dev Returns the downcasted uint16 from uint256, reverting on - * overflow (when the input is greater than largest uint16). - * - * Counterpart to Solidity's `uint16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - */ - function toUint16(uint256 value) internal pure returns (uint16) { - require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); - return uint16(value); - } - - /** - * @dev Returns the downcasted uint8 from uint256, reverting on - * overflow (when the input is greater than largest uint8). - * - * Counterpart to Solidity's `uint8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - */ - function toUint8(uint256 value) internal pure returns (uint8) { - require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); - return uint8(value); - } - - /** - * @dev Converts a signed int256 into an unsigned uint256. - * - * Requirements: - * - * - input must be greater than or equal to 0. - */ - function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); - return uint256(value); - } - - /** - * @dev Returns the downcasted int128 from int256, reverting on - * overflow (when the input is less than smallest int128 or - * greater than largest int128). - * - * Counterpart to Solidity's `int128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - * - * _Available since v3.1._ - */ - function toInt128(int256 value) internal pure returns (int128) { - require(value >= -2**127 && value < 2**127, "SafeCast: value doesn't fit in 128 bits"); - return int128(value); - } - - /** - * @dev Returns the downcasted int64 from int256, reverting on - * overflow (when the input is less than smallest int64 or - * greater than largest int64). - * - * Counterpart to Solidity's `int64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - * - * _Available since v3.1._ - */ - function toInt64(int256 value) internal pure returns (int64) { - require(value >= -2**63 && value < 2**63, "SafeCast: value doesn't fit in 64 bits"); - return int64(value); - } - - /** - * @dev Returns the downcasted int32 from int256, reverting on - * overflow (when the input is less than smallest int32 or - * greater than largest int32). - * - * Counterpart to Solidity's `int32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - * - * _Available since v3.1._ - */ - function toInt32(int256 value) internal pure returns (int32) { - require(value >= -2**31 && value < 2**31, "SafeCast: value doesn't fit in 32 bits"); - return int32(value); - } - - /** - * @dev Returns the downcasted int16 from int256, reverting on - * overflow (when the input is less than smallest int16 or - * greater than largest int16). - * - * Counterpart to Solidity's `int16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - * - * _Available since v3.1._ - */ - function toInt16(int256 value) internal pure returns (int16) { - require(value >= -2**15 && value < 2**15, "SafeCast: value doesn't fit in 16 bits"); - return int16(value); - } - - /** - * @dev Returns the downcasted int8 from int256, reverting on - * overflow (when the input is less than smallest int8 or - * greater than largest int8). - * - * Counterpart to Solidity's `int8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - * - * _Available since v3.1._ - */ - function toInt8(int256 value) internal pure returns (int8) { - require(value >= -2**7 && value < 2**7, "SafeCast: value doesn't fit in 8 bits"); - return int8(value); - } - - /** - * @dev Converts an unsigned uint256 into a signed int256. - * - * Requirements: - * - * - input must be less than or equal to maxInt256. - */ - function toInt256(uint256 value) internal pure returns (int256) { - require(value < 2**255, "SafeCast: value doesn't fit in an int256"); - return int256(value); - } -} - -// SPDX-License-Identifier: AGPL-3.0-or-later -// Internal -// Libs -/** - * @title BoostedDualVault - * @author mStable - * @notice Accrues rewards second by second, based on a users boosted balance - * @dev Forked from rewards/staking/StakingRewards.sol - * Changes: - * - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months) - * - `updateBoost` hook called after every external action to reset a users boost - * - Struct packing of common data - * - Searching for and claiming of unlocked rewards - * - Add a second rewards token in the platform rewards - */ -contract BoostedDualVault is - IBoostedDualVaultWithLockup, - IRewardsRecipientWithPlatformToken, - Initializable, - InitializableRewardsDistributionRecipient, - BoostedTokenWrapper -{ - using SafeERC20 for IERC20; - using StableMath for uint256; - using SafeCast for uint256; - - event RewardAdded(uint256 reward, uint256 platformReward); - event Staked(address indexed user, uint256 amount, address payer); - event Withdrawn(address indexed user, uint256 amount); - event Poked(address indexed user); - event RewardPaid(address indexed user, uint256 reward, uint256 platformReward); - - /// @notice token the rewards are distributed in. eg MTA - IERC20 public immutable rewardsToken; - /// @notice token the platform rewards are distributed in. eg WMATIC - IERC20 public immutable platformToken; - /// @notice contract that holds the platform tokens - PlatformTokenVendor public platformTokenVendor; - /// @notice total raw balance - uint256 public totalRaw; - - /// @notice length of each staking period in seconds. 7 days = 604,800; 3 months = 7,862,400 - uint64 public constant DURATION = 7 days; - /// @notice Length of token lockup, after rewards are earned - uint256 public constant LOCKUP = 26 weeks; - /// @notice Percentage of earned tokens unlocked immediately - uint64 public constant UNLOCK = 33e16; - - /// @notice Timestamp for current period finish - uint256 public periodFinish; - /// @notice Reward rate for the rest of the period - uint256 public rewardRate; - /// @notice Platform reward rate for the rest of the period - uint256 public platformRewardRate; - /// @notice Last time any user took action - uint256 public lastUpdateTime; - /// @notice Ever increasing rewardPerToken rate, based on % of total supply - uint256 public rewardPerTokenStored; - /// @notice Ever increasing platformRewardPerToken rate, based on % of total supply - uint256 public platformRewardPerTokenStored; - - mapping(address => UserData) public userData; - /// @notice Locked reward tracking - mapping(address => Reward[]) public userRewards; - mapping(address => uint64) public userClaim; - - struct UserData { - uint128 rewardPerTokenPaid; - uint128 rewards; - uint128 platformRewardPerTokenPaid; - uint128 platformRewards; - uint64 lastAction; - uint64 rewardCount; - } - - struct Reward { - uint64 start; - uint64 finish; - uint128 rate; - } - - /** - * @param _nexus mStable system Nexus address - * @param _stakingToken token that is being rewarded for being staked. eg MTA, imUSD or fPmUSD/GUSD - * @param _boostDirector vMTA boost director - * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 - * @param _boostCoeff Boost coefficent using the the boost formula - * @param _rewardsToken first token that is being distributed as a reward. eg MTA - * @param _platformToken second token that is being distributed as a reward. eg FXS for FRAX - */ - constructor( - address _nexus, - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _boostCoeff, - address _rewardsToken, - address _platformToken - ) - InitializableRewardsDistributionRecipient(_nexus) - BoostedTokenWrapper(_stakingToken, _boostDirector, _priceCoeff, _boostCoeff) - { - rewardsToken = IERC20(_rewardsToken); - platformToken = IERC20(_platformToken); - } - - /** - * @dev Initialization function for upgradable proxy contract. - * This function should be called via Proxy just after contract deployment. - * To avoid variable shadowing appended `Arg` after arguments name. - * @param _rewardsDistributorArg mStable Reward Distributor contract address - * @param _nameArg token name. eg imUSD Vault or GUSD Feeder Pool Vault - * @param _symbolArg token symbol. eg v-imUSD or v-fPmUSD/GUSD - */ - function initialize( - address _rewardsDistributorArg, - string calldata _nameArg, - string calldata _symbolArg - ) external initializer { - InitializableRewardsDistributionRecipient._initialize(_rewardsDistributorArg); - BoostedTokenWrapper._initialize(_nameArg, _symbolArg); - platformTokenVendor = new PlatformTokenVendor(platformToken); - } - - /** - * @dev Updates the reward for a given address, before executing function. - * Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to - * (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed - * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). - */ - modifier updateReward(address _account) { - _updateReward(_account); - _; - } - - function _updateReward(address _account) internal { - uint256 currentTime = block.timestamp; - uint64 currentTime64 = SafeCast.toUint64(currentTime); - - // Setting of global vars - ( - uint256 newRewardPerToken, - uint256 newPlatformRewardPerToken, - uint256 lastApplicableTime - ) = _rewardPerToken(); - // If statement protects against loss in initialisation case - if (newRewardPerToken > 0 || newPlatformRewardPerToken > 0) { - rewardPerTokenStored = newRewardPerToken; - platformRewardPerTokenStored = newPlatformRewardPerToken; - lastUpdateTime = lastApplicableTime; - - // Setting of personal vars based on new globals - if (_account != address(0)) { - UserData memory data = userData[_account]; - uint256 earned_ = _earned( - _account, - data.rewardPerTokenPaid, - newRewardPerToken, - false - ); - uint256 platformEarned_ = _earned( - _account, - data.platformRewardPerTokenPaid, - newPlatformRewardPerToken, - true - ); - - // If earned == 0, then it must either be the initial stake, or an action in the - // same block, since new rewards unlock after each block. - if (earned_ > 0) { - uint256 unlocked = earned_.mulTruncate(UNLOCK); - uint256 locked = earned_ - unlocked; - - userRewards[_account].push( - Reward({ - start: SafeCast.toUint64(LOCKUP + data.lastAction), - finish: SafeCast.toUint64(LOCKUP + currentTime), - rate: SafeCast.toUint128(locked / (currentTime - data.lastAction)) - }) - ); - - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: SafeCast.toUint128(unlocked + data.rewards), - platformRewardPerTokenPaid: SafeCast.toUint128(newPlatformRewardPerToken), - platformRewards: data.platformRewards + SafeCast.toUint128(platformEarned_), - lastAction: currentTime64, - rewardCount: data.rewardCount + 1 - }); - } else { - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: data.rewards, - platformRewardPerTokenPaid: SafeCast.toUint128(newPlatformRewardPerToken), - platformRewards: data.platformRewards + SafeCast.toUint128(platformEarned_), - lastAction: currentTime64, - rewardCount: data.rewardCount - }); - } - } - } else if (_account != address(0)) { - // This should only be hit once, for first staker in initialisation case - userData[_account].lastAction = currentTime64; - } - } - - /** @dev Updates the boost for a given address, after the rest of the function has executed */ - modifier updateBoost(address _account) { - _; - _setBoost(_account); - } - - /*************************************** - ACTIONS - EXTERNAL - ****************************************/ - - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _stake(msg.sender, _amount); - } - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) - external - override - updateReward(_beneficiary) - updateBoost(_beneficiary) - { - _stake(_beneficiary, _amount); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external override updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(rawBalanceOf(msg.sender)); - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - _claimRewards(first, last); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(rawBalanceOf(msg.sender)); - _claimRewards(_first, _last); - } - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(_amount); - } - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external override updateReward(msg.sender) updateBoost(msg.sender) { - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - if (unlocked > 0) { - rewardsToken.safeTransfer(msg.sender, unlocked); - } - - uint256 platformReward = _claimPlatformReward(); - - emit RewardPaid(msg.sender, unlocked, platformReward); - } - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external override updateReward(msg.sender) updateBoost(msg.sender) { - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - - _claimRewards(first, last); - } - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _claimRewards(_first, _last); - } - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) - external - override - updateReward(_account) - updateBoost(_account) - { - emit Poked(_account); - } - - /*************************************** - ACTIONS - INTERNAL - ****************************************/ - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function _claimRewards(uint256 _first, uint256 _last) internal { - (uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last); - userClaim[msg.sender] = uint64(lastTimestamp); - - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - uint256 total = unclaimed + unlocked; - - if (total > 0) { - rewardsToken.safeTransfer(msg.sender, total); - } - - uint256 platformReward = _claimPlatformReward(); - - emit RewardPaid(msg.sender, total, platformReward); - } - - /** - * @dev Claims any outstanding platform reward tokens - */ - function _claimPlatformReward() internal returns (uint256) { - uint256 platformReward = userData[msg.sender].platformRewards; - if (platformReward > 0) { - userData[msg.sender].platformRewards = 0; - platformToken.safeTransferFrom( - address(platformTokenVendor), - msg.sender, - platformReward - ); - } - return platformReward; - } - - /** - * @dev Internally stakes an amount by depositing from sender, - * and crediting to the specified beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function _stake(address _beneficiary, uint256 _amount) internal { - require(_amount > 0, "Cannot stake 0"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - _stakeRaw(_beneficiary, _amount); - totalRaw += _amount; - emit Staked(_beneficiary, _amount, msg.sender); - } - - /** - * @dev Withdraws raw units from the sender - * @param _amount Units of StakingToken - */ - function _withdraw(uint256 _amount) internal { - require(_amount > 0, "Cannot withdraw 0"); - _withdrawRaw(_amount); - totalRaw -= _amount; - emit Withdrawn(msg.sender, _amount); - } - - /*************************************** - GETTERS - ****************************************/ - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() - external - view - override(IRewardsDistributionRecipient, IRewardsRecipientWithPlatformToken) - returns (IERC20) - { - return rewardsToken; - } - - /** - * @dev Gets the PlatformToken - */ - function getPlatformToken() external view override returns (IERC20) { - return platformToken; - } - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() public view override returns (uint256) { - return StableMath.min(block.timestamp, periodFinish); - } - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() public view override returns (uint256, uint256) { - (uint256 rewardPerToken_, uint256 platformRewardPerToken_, ) = _rewardPerToken(); - return (rewardPerToken_, platformRewardPerToken_); - } - - function _rewardPerToken() - internal - view - returns ( - uint256 rewardPerToken_, - uint256 platformRewardPerToken_, - uint256 lastTimeRewardApplicable_ - ) - { - uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD - uint256 timeDelta = lastApplicableTime - lastUpdateTime; // + 1 SLOAD - // If this has been called twice in the same block, shortcircuit to reduce gas - if (timeDelta == 0) { - return (rewardPerTokenStored, platformRewardPerTokenStored, lastApplicableTime); - } - // new reward units to distribute = rewardRate * timeSinceLastUpdate - uint256 rewardUnitsToDistribute = rewardRate * timeDelta; // + 1 SLOAD - uint256 platformRewardUnitsToDistribute = platformRewardRate * timeDelta; // + 1 SLOAD - // If there is no StakingToken liquidity, avoid div(0) - // If there is nothing to distribute, short circuit - if ( - totalSupply() == 0 || - (rewardUnitsToDistribute == 0 && platformRewardUnitsToDistribute == 0) - ) { - return (rewardPerTokenStored, platformRewardPerTokenStored, lastApplicableTime); - } - // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens - uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(totalSupply()); - uint256 platformUnitsToDistributePerToken = platformRewardUnitsToDistribute.divPrecisely( - totalRaw - ); - // return summed rate - return ( - rewardPerTokenStored + unitsToDistributePerToken, - platformRewardPerTokenStored + platformUnitsToDistributePerToken, - lastApplicableTime - ); // + 1 SLOAD - } - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - * @return Platform reward claimable - */ - function earned(address _account) public view override returns (uint256, uint256) { - (uint256 rewardPerToken_, uint256 platformRewardPerToken_) = rewardPerToken(); - uint256 newEarned = _earned( - _account, - userData[_account].rewardPerTokenPaid, - rewardPerToken_, - false - ); - uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK); - return ( - immediatelyUnlocked + userData[_account].rewards, - _earned( - _account, - userData[_account].platformRewardPerTokenPaid, - platformRewardPerToken_, - true - ) - ); - } - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - override - returns ( - uint256 amount, - uint256 first, - uint256 last, - uint256 platformAmount - ) - { - (first, last) = _unclaimedEpochs(_account); - (uint256 unlocked, ) = _unclaimedRewards(_account, first, last); - (uint256 earned_, uint256 platformEarned_) = earned(_account); - amount = unlocked + earned_; - platformAmount = platformEarned_; - } - - /** @dev Returns only the most recently earned rewards */ - function _earned( - address _account, - uint256 _userRewardPerTokenPaid, - uint256 _currentRewardPerToken, - bool _useRawBalance - ) internal view returns (uint256) { - // current rate per token - rate user previously received - uint256 userRewardDelta = _currentRewardPerToken - _userRewardPerTokenPaid; - // Short circuit if there is nothing new to distribute - if (userRewardDelta == 0) { - return 0; - } - // new reward = staked tokens * difference in rate - uint256 bal = _useRawBalance ? rawBalanceOf(_account) : balanceOf(_account); - return bal.mulTruncate(userRewardDelta); - } - - /** - * @dev Gets the first and last indexes of array elements containing unclaimed rewards - */ - function _unclaimedEpochs(address _account) - internal - view - returns (uint256 first, uint256 last) - { - uint64 lastClaim = userClaim[_account]; - - uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account); - uint256 lastUnclaimed = _findLastUnclaimed(_account); - - return (firstUnclaimed, lastUnclaimed); - } - - /** - * @dev Sums the cumulative rewards from a valid range - */ - function _unclaimedRewards( - address _account, - uint256 _first, - uint256 _last - ) internal view returns (uint256 amount, uint256 latestTimestamp) { - uint256 currentTime = block.timestamp; - uint64 lastClaim = userClaim[_account]; - - // Check for no rewards unlocked - uint256 totalLen = userRewards[_account].length; - if (_first == 0 && _last == 0) { - if (totalLen == 0 || currentTime <= userRewards[_account][0].start) { - return (0, currentTime); - } - } - // If there are previous unlocks, check for claims that would leave them untouchable - if (_first > 0) { - require( - lastClaim >= userRewards[_account][_first - 1].finish, - "Invalid _first arg: Must claim earlier entries" - ); - } - - uint256 count = _last - _first + 1; - for (uint256 i = 0; i < count; i++) { - uint256 id = _first + i; - Reward memory rwd = userRewards[_account][id]; - - require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch"); - - uint256 endTime = StableMath.min(rwd.finish, currentTime); - uint256 startTime = StableMath.max(rwd.start, lastClaim); - uint256 unclaimed = (endTime - startTime) * rwd.rate; - - amount += unclaimed; - } - - // Calculate last relevant timestamp here to allow users to avoid issue of OOG errors - // by claiming rewards in batches. - latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish); - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findFirstUnclaimed(uint64 _lastClaim, address _account) - internal - view - returns (uint256 first) - { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (_lastClaim > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findLastUnclaimed(address _account) internal view returns (uint256 first) { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (block.timestamp > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /*************************************** - ADMIN - ****************************************/ - - /** - * @dev Notifies the contract that new rewards have been added. - * Calculates an updated rewardRate based on the rewards in period. - * @param _reward Units of RewardToken that have been added to the pool - */ - function notifyRewardAmount(uint256 _reward) - external - override(IRewardsDistributionRecipient, IRewardsRecipientWithPlatformToken) - onlyRewardsDistributor - updateReward(address(0)) - { - require(_reward < 1e24, "Cannot notify with more than a million units"); - - uint256 newPlatformRewards = platformToken.balanceOf(address(this)); - if (newPlatformRewards > 0) { - platformToken.safeTransfer(address(platformTokenVendor), newPlatformRewards); - } - - uint256 currentTime = block.timestamp; - // If previous period over, reset rewardRate - if (currentTime >= periodFinish) { - rewardRate = _reward / DURATION; - platformRewardRate = newPlatformRewards / DURATION; - } - // If additional reward to existing period, calc sum - else { - uint256 remaining = periodFinish - currentTime; - - uint256 leftoverReward = remaining * rewardRate; - rewardRate = (_reward + leftoverReward) / DURATION; - - uint256 leftoverPlatformReward = remaining * platformRewardRate; - platformRewardRate = (newPlatformRewards + leftoverPlatformReward) / DURATION; - } - - lastUpdateTime = currentTime; - periodFinish = currentTime + DURATION; - - emit RewardAdded(_reward, newPlatformRewards); - } -} diff --git a/contracts/legacy/v-mBTC.sol b/contracts/legacy/v-mBTC.sol deleted file mode 100644 index a84c4b5a..00000000 --- a/contracts/legacy/v-mBTC.sol +++ /dev/null @@ -1,1994 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.2; - -interface IERC20 { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); -} - -interface IBoostedVaultWithLockup { - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external; - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) external; - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external; - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external; - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) external; - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external; - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() external view returns (uint256); - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() external view returns (uint256); - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) external view returns (uint256); - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last - ); -} - -contract ModuleKeys { - // Governance - // =========== - // keccak256("Governance"); - bytes32 internal constant KEY_GOVERNANCE = - 0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d; - //keccak256("Staking"); - bytes32 internal constant KEY_STAKING = - 0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034; - //keccak256("ProxyAdmin"); - bytes32 internal constant KEY_PROXY_ADMIN = - 0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1; - - // mStable - // ======= - // keccak256("OracleHub"); - bytes32 internal constant KEY_ORACLE_HUB = - 0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040; - // keccak256("Manager"); - bytes32 internal constant KEY_MANAGER = - 0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f; - //keccak256("Recollateraliser"); - bytes32 internal constant KEY_RECOLLATERALISER = - 0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f; - //keccak256("MetaToken"); - bytes32 internal constant KEY_META_TOKEN = - 0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2; - // keccak256("SavingsManager"); - bytes32 internal constant KEY_SAVINGS_MANAGER = - 0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1; - // keccak256("Liquidator"); - bytes32 internal constant KEY_LIQUIDATOR = - 0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4; - // keccak256("InterestValidator"); - bytes32 internal constant KEY_INTEREST_VALIDATOR = - 0xc10a28f028c7f7282a03c90608e38a4a646e136e614e4b07d119280c5f7f839f; -} - -interface INexus { - function governor() external view returns (address); - - function getModule(bytes32 key) external view returns (address); - - function proposeModule(bytes32 _key, address _addr) external; - - function cancelProposedModule(bytes32 _key) external; - - function acceptProposedModule(bytes32 _key) external; - - function acceptProposedModules(bytes32[] calldata _keys) external; - - function requestLockModule(bytes32 _key) external; - - function cancelLockModule(bytes32 _key) external; - - function lockModule(bytes32 _key) external; -} - -abstract contract ImmutableModule is ModuleKeys { - INexus public immutable nexus; - - /** - * @dev Initialization function for upgradable proxy contracts - * @param _nexus Nexus contract address - */ - constructor(address _nexus) { - require(_nexus != address(0), "Nexus address is zero"); - nexus = INexus(_nexus); - } - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - _onlyGovernor(); - _; - } - - function _onlyGovernor() internal view { - require(msg.sender == _governor(), "Only governor can execute"); - } - - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - - /** - * @dev Modifier to allow function calls only from the ProxyAdmin. - */ - modifier onlyProxyAdmin() { - require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Manager. - */ - modifier onlyManager() { - require(msg.sender == _manager(), "Only manager can execute"); - _; - } - - /** - * @dev Returns Governor address from the Nexus - * @return Address of Governor Contract - */ - function _governor() internal view returns (address) { - return nexus.governor(); - } - - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - - /** - * @dev Return Staking Module address from the Nexus - * @return Address of the Staking Module contract - */ - function _staking() internal view returns (address) { - return nexus.getModule(KEY_STAKING); - } - - /** - * @dev Return ProxyAdmin Module address from the Nexus - * @return Address of the ProxyAdmin Module contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } - - /** - * @dev Return MetaToken Module address from the Nexus - * @return Address of the MetaToken Module contract - */ - function _metaToken() internal view returns (address) { - return nexus.getModule(KEY_META_TOKEN); - } - - /** - * @dev Return OracleHub Module address from the Nexus - * @return Address of the OracleHub Module contract - */ - function _oracleHub() internal view returns (address) { - return nexus.getModule(KEY_ORACLE_HUB); - } - - /** - * @dev Return Manager Module address from the Nexus - * @return Address of the Manager Module contract - */ - function _manager() internal view returns (address) { - return nexus.getModule(KEY_MANAGER); - } - - /** - * @dev Return SavingsManager Module address from the Nexus - * @return Address of the SavingsManager Module contract - */ - function _savingsManager() internal view returns (address) { - return nexus.getModule(KEY_SAVINGS_MANAGER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _recollateraliser() internal view returns (address) { - return nexus.getModule(KEY_RECOLLATERALISER); - } -} - -interface IRewardsDistributionRecipient { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); -} - -abstract contract InitializableRewardsDistributionRecipient is - IRewardsDistributionRecipient, - ImmutableModule -{ - // This address has the ability to distribute the rewards - address public rewardsDistributor; - - constructor(address _nexus) ImmutableModule(_nexus) {} - - /** @dev Recipient is a module, governed by mStable governance */ - function _initialize(address _rewardsDistributor) internal { - rewardsDistributor = _rewardsDistributor; - } - - /** - * @dev Only the rewards distributor can notify about rewards - */ - modifier onlyRewardsDistributor() { - require(msg.sender == rewardsDistributor, "Caller is not reward distributor"); - _; - } - - /** - * @dev Change the rewardsDistributor - only called by mStable governor - * @param _rewardsDistributor Address of the new distributor - */ - function setRewardsDistribution(address _rewardsDistributor) external onlyGovernor { - rewardsDistributor = _rewardsDistributor; - } -} - -interface IBoostDirector { - function getBalance(address _user) external returns (uint256); - - function setDirection( - address _old, - address _new, - bool _pokeNew - ) external; - - function whitelistVaults(address[] calldata _vaults) external; -} - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { - size := extcodesize(account) - } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return - functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) - internal - view - returns (bytes memory) - { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) - internal - returns (bytes memory) - { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) private pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} - -library SafeERC20 { - using Address for address; - - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { - _callOptionalReturn( - token, - abi.encodeWithSelector(token.transferFrom.selector, from, to, value) - ); - } - - /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. - */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function _callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that - // the target address contains contract code and also asserts for success in the low-level call. - - bytes memory returndata = address(token).functionCall( - data, - "SafeERC20: low-level call failed" - ); - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} - -contract InitializableReentrancyGuard { - bool private _notEntered; - - function _initializeReentrancyGuard() internal { - // Storing an initial non-zero value makes deployment a bit more - // expensive, but in exchange the refund on every call to nonReentrant - // will be lower in amount. Since refunds are capped to a percetange of - // the total transaction's gas, it is best to keep them low in cases - // like this one, to increase the likelihood of the full refund coming - // into effect. - _notEntered = true; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_notEntered, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _notEntered = false; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _notEntered = true; - } -} - -library StableMath { - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @dev Token Ratios are used when converting between units of bAsset, mAsset and MTA - * Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold) - * bAsset ratio unit for use in exact calculations, - * where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit - */ - uint256 private constant RATIO_SCALE = 1e8; - - /** - * @dev Provides an interface to the scaling unit - * @return Scaling unit (1e18 or 1 * 10**18) - */ - function getFullScale() internal pure returns (uint256) { - return FULL_SCALE; - } - - /** - * @dev Provides an interface to the ratio unit - * @return Ratio scale unit (1e8 or 1 * 10**8) - */ - function getRatioScale() internal pure returns (uint256) { - return RATIO_SCALE; - } - - /** - * @dev Scales a given integer to the power of the full scale. - * @param x Simple uint256 to scale - * @return Scaled value a to an exact number - */ - function scaleInteger(uint256 x) internal pure returns (uint256) { - return x * FULL_SCALE; - } - - /*************************************** - PRECISE ARITHMETIC - ****************************************/ - - /** - * @dev Multiplies two precise units, and then truncates by the full scale - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) { - return mulTruncateScale(x, y, FULL_SCALE); - } - - /** - * @dev Multiplies two precise units, and then truncates by the given scale. For example, - * when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18 - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @param scale Scale unit - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncateScale( - uint256 x, - uint256 y, - uint256 scale - ) internal pure returns (uint256) { - // e.g. assume scale = fullScale - // z = 10e18 * 9e17 = 9e36 - // return 9e38 / 1e18 = 9e18 - return (x * y) / scale; - } - - /** - * @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit, rounded up to the closest base unit. - */ - function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e17 * 17268172638 = 138145381104e17 - uint256 scaled = x * y; - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled + FULL_SCALE - 1; - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil / FULL_SCALE; - } - - /** - * @dev Precisely divides two units, by first scaling the left hand operand. Useful - * for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17) - * @param x Left hand input to division - * @param y Right hand input to division - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e18 * 1e18 = 8e36 - // e.g. 8e36 / 10e18 = 8e17 - return (x * FULL_SCALE) / y; - } - - /*************************************** - RATIO FUNCS - ****************************************/ - - /** - * @dev Multiplies and truncates a token ratio, essentially flooring the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand operand to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return c Result after multiplying the two inputs and then dividing by the ratio scale - */ - function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - return mulTruncateScale(x, ratio, RATIO_SCALE); - } - - /** - * @dev Multiplies and truncates a token ratio, rounding up the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand input to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the shared - * ratio scale, rounded up to the closest base unit. - */ - function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) { - // e.g. How much mAsset should I burn for this bAsset (x)? - // 1e18 * 1e8 = 1e26 - uint256 scaled = x * ratio; - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled + RATIO_SCALE - 1; - // return 100..00.999e8 / 1e8 = 1e18 - return ceil / RATIO_SCALE; - } - - /** - * @dev Precisely divides two ratioed units, by first scaling the left hand operand - * i.e. How much bAsset is this mAsset worth? - * @param x Left hand operand in division - * @param ratio bAsset ratio - * @return c Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - // e.g. 1e14 * 1e8 = 1e22 - // return 1e22 / 1e12 = 1e10 - return (x * RATIO_SCALE) / ratio; - } - - /*************************************** - HELPERS - ****************************************/ - - /** - * @dev Calculates minimum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Minimum of the two inputs - */ - function min(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? y : x; - } - - /** - * @dev Calculated maximum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Maximum of the two inputs - */ - function max(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? x : y; - } - - /** - * @dev Clamps a value to an upper bound - * @param x Left hand input - * @param upperBound Maximum possible value to return - * @return Input x clamped to a maximum value, upperBound - */ - function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) { - return x > upperBound ? upperBound : x; - } -} - -library Root { - /** - * @dev Returns the square root of a given number - * @param x Input - * @return y Square root of Input - */ - function sqrt(uint256 x) internal pure returns (uint256 y) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; // Seven iterations should be enough - uint256 r1 = x / r; - return uint256(r < r1 ? r : r1); - } - } -} - -contract BoostedTokenWrapper is InitializableReentrancyGuard { - using StableMath for uint256; - using SafeERC20 for IERC20; - - event Transfer(address indexed from, address indexed to, uint256 value); - - string private _name; - string private _symbol; - - IERC20 public immutable stakingToken; - IBoostDirector public immutable boostDirector; - - uint256 private _totalBoostedSupply; - mapping(address => uint256) private _boostedBalances; - mapping(address => uint256) private _rawBalances; - - // Vars for use in the boost calculations - uint256 private constant MIN_DEPOSIT = 1e18; - uint256 private constant MAX_VMTA = 600000e18; - uint256 private constant MAX_BOOST = 3e18; - uint256 private constant MIN_BOOST = 1e18; - uint256 private constant FLOOR = 98e16; - uint256 public immutable boostCoeff; // scaled by 10 - uint256 public immutable priceCoeff; - - /** - * @dev TokenWrapper constructor - * @param _stakingToken Wrapped token to be staked - * @param _boostDirector vMTA boost director - * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 - */ - constructor( - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _boostCoeff - ) { - stakingToken = IERC20(_stakingToken); - boostDirector = IBoostDirector(_boostDirector); - priceCoeff = _priceCoeff; - boostCoeff = _boostCoeff; - } - - function _initialize(string memory _nameArg, string memory _symbolArg) internal { - _initializeReentrancyGuard(); - _name = _nameArg; - _symbol = _symbolArg; - } - - function name() public view virtual returns (string memory) { - return _name; - } - - function symbol() public view virtual returns (string memory) { - return _symbol; - } - - function decimals() public view virtual returns (uint8) { - return 18; - } - - /** - * @dev Get the total boosted amount - * @return uint256 total supply - */ - function totalSupply() public view returns (uint256) { - return _totalBoostedSupply; - } - - /** - * @dev Get the boosted balance of a given account - * @param _account User for which to retrieve balance - */ - function balanceOf(address _account) public view returns (uint256) { - return _boostedBalances[_account]; - } - - /** - * @dev Get the RAW balance of a given account - * @param _account User for which to retrieve balance - */ - function rawBalanceOf(address _account) public view returns (uint256) { - return _rawBalances[_account]; - } - - /** - * @dev Read the boost for the given address - * @param _account User for which to return the boost - * @return boost where 1x == 1e18 - */ - function getBoost(address _account) public view returns (uint256) { - return balanceOf(_account).divPrecisely(rawBalanceOf(_account)); - } - - /** - * @dev Deposits a given amount of StakingToken from sender - * @param _amount Units of StakingToken - */ - function _stakeRaw(address _beneficiary, uint256 _amount) internal nonReentrant { - _rawBalances[_beneficiary] += _amount; - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - } - - /** - * @dev Withdraws a given stake from sender - * @param _amount Units of StakingToken - */ - function _withdrawRaw(uint256 _amount) internal nonReentrant { - _rawBalances[msg.sender] -= _amount; - stakingToken.safeTransfer(msg.sender, _amount); - } - - /** - * @dev Updates the boost for the given address according to the formula - * boost = min(0.5 + c * vMTA_balance / imUSD_locked^(7/8), 1.5) - * If rawBalance <= MIN_DEPOSIT, boost is 0 - * @param _account User for which to update the boost - */ - function _setBoost(address _account) internal { - uint256 rawBalance = _rawBalances[_account]; - uint256 boostedBalance = _boostedBalances[_account]; - uint256 boost = MIN_BOOST; - - // Check whether balance is sufficient - // is_boosted is used to minimize gas usage - uint256 scaledBalance = (rawBalance * priceCoeff) / 1e18; - if (scaledBalance >= MIN_DEPOSIT) { - uint256 votingWeight = boostDirector.getBalance(_account); - boost = _computeBoost(scaledBalance, votingWeight); - } - - uint256 newBoostedBalance = rawBalance.mulTruncate(boost); - - if (newBoostedBalance != boostedBalance) { - _totalBoostedSupply = _totalBoostedSupply - boostedBalance + newBoostedBalance; - _boostedBalances[_account] = newBoostedBalance; - - if (newBoostedBalance > boostedBalance) { - emit Transfer(address(0), _account, newBoostedBalance - boostedBalance); - } else { - emit Transfer(_account, address(0), boostedBalance - newBoostedBalance); - } - } - } - - /** - * @dev Computes the boost for - * boost = min(m, max(1, 0.95 + c * min(voting_weight, f) / deposit^(3/4))) - * @param _scaledDeposit deposit amount in terms of USD - */ - function _computeBoost(uint256 _scaledDeposit, uint256 _votingWeight) - private - view - returns (uint256 boost) - { - if (_votingWeight == 0) return MIN_BOOST; - - // Compute balance to the power 3/4 - uint256 sqrt1 = Root.sqrt(_scaledDeposit * 1e6); - uint256 sqrt2 = Root.sqrt(sqrt1); - uint256 denominator = sqrt1 * sqrt2; - boost = - (((StableMath.min(_votingWeight, MAX_VMTA) * boostCoeff) / 10) * 1e18) / - denominator; - boost = StableMath.min(MAX_BOOST, StableMath.max(MIN_BOOST, FLOOR + boost)); - } -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library SafeCast { - /** - * @dev Returns the downcasted uint128 from uint256, reverting on - * overflow (when the input is greater than largest uint128). - * - * Counterpart to Solidity's `uint128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - */ - function toUint128(uint256 value) internal pure returns (uint128) { - require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); - return uint128(value); - } - - /** - * @dev Returns the downcasted uint64 from uint256, reverting on - * overflow (when the input is greater than largest uint64). - * - * Counterpart to Solidity's `uint64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - */ - function toUint64(uint256 value) internal pure returns (uint64) { - require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); - return uint64(value); - } - - /** - * @dev Returns the downcasted uint32 from uint256, reverting on - * overflow (when the input is greater than largest uint32). - * - * Counterpart to Solidity's `uint32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - */ - function toUint32(uint256 value) internal pure returns (uint32) { - require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); - return uint32(value); - } - - /** - * @dev Returns the downcasted uint16 from uint256, reverting on - * overflow (when the input is greater than largest uint16). - * - * Counterpart to Solidity's `uint16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - */ - function toUint16(uint256 value) internal pure returns (uint16) { - require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); - return uint16(value); - } - - /** - * @dev Returns the downcasted uint8 from uint256, reverting on - * overflow (when the input is greater than largest uint8). - * - * Counterpart to Solidity's `uint8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - */ - function toUint8(uint256 value) internal pure returns (uint8) { - require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); - return uint8(value); - } - - /** - * @dev Converts a signed int256 into an unsigned uint256. - * - * Requirements: - * - * - input must be greater than or equal to 0. - */ - function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); - return uint256(value); - } - - /** - * @dev Returns the downcasted int128 from int256, reverting on - * overflow (when the input is less than smallest int128 or - * greater than largest int128). - * - * Counterpart to Solidity's `int128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - * - * _Available since v3.1._ - */ - function toInt128(int256 value) internal pure returns (int128) { - require(value >= -2**127 && value < 2**127, "SafeCast: value doesn't fit in 128 bits"); - return int128(value); - } - - /** - * @dev Returns the downcasted int64 from int256, reverting on - * overflow (when the input is less than smallest int64 or - * greater than largest int64). - * - * Counterpart to Solidity's `int64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - * - * _Available since v3.1._ - */ - function toInt64(int256 value) internal pure returns (int64) { - require(value >= -2**63 && value < 2**63, "SafeCast: value doesn't fit in 64 bits"); - return int64(value); - } - - /** - * @dev Returns the downcasted int32 from int256, reverting on - * overflow (when the input is less than smallest int32 or - * greater than largest int32). - * - * Counterpart to Solidity's `int32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - * - * _Available since v3.1._ - */ - function toInt32(int256 value) internal pure returns (int32) { - require(value >= -2**31 && value < 2**31, "SafeCast: value doesn't fit in 32 bits"); - return int32(value); - } - - /** - * @dev Returns the downcasted int16 from int256, reverting on - * overflow (when the input is less than smallest int16 or - * greater than largest int16). - * - * Counterpart to Solidity's `int16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - * - * _Available since v3.1._ - */ - function toInt16(int256 value) internal pure returns (int16) { - require(value >= -2**15 && value < 2**15, "SafeCast: value doesn't fit in 16 bits"); - return int16(value); - } - - /** - * @dev Returns the downcasted int8 from int256, reverting on - * overflow (when the input is less than smallest int8 or - * greater than largest int8). - * - * Counterpart to Solidity's `int8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - * - * _Available since v3.1._ - */ - function toInt8(int256 value) internal pure returns (int8) { - require(value >= -2**7 && value < 2**7, "SafeCast: value doesn't fit in 8 bits"); - return int8(value); - } - - /** - * @dev Converts an unsigned uint256 into a signed int256. - * - * Requirements: - * - * - input must be less than or equal to maxInt256. - */ - function toInt256(uint256 value) internal pure returns (int256) { - require(value < 2**255, "SafeCast: value doesn't fit in an int256"); - return int256(value); - } -} - -// Internal -// Libs -/** - * @title BoostedSavingsVault - * @author mStable - * @notice Accrues rewards second by second, based on a users boosted balance - * @dev Forked from rewards/staking/StakingRewards.sol - * Changes: - * - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months) - * - `updateBoost` hook called after every external action to reset a users boost - * - Struct packing of common data - * - Searching for and claiming of unlocked rewards - */ -contract BoostedSavingsVault is - IBoostedVaultWithLockup, - Initializable, - InitializableRewardsDistributionRecipient, - BoostedTokenWrapper -{ - using SafeERC20 for IERC20; - using StableMath for uint256; - using SafeCast for uint256; - - event RewardAdded(uint256 reward); - event Staked(address indexed user, uint256 amount, address payer); - event Withdrawn(address indexed user, uint256 amount); - event Poked(address indexed user); - event RewardPaid(address indexed user, uint256 reward); - - IERC20 public immutable rewardsToken; - - uint64 public constant DURATION = 7 days; - // Length of token lockup, after rewards are earned - uint256 public constant LOCKUP = 26 weeks; - // Percentage of earned tokens unlocked immediately - uint64 public constant UNLOCK = 33e16; - - // Timestamp for current period finish - uint256 public periodFinish; - // RewardRate for the rest of the PERIOD - uint256 public rewardRate; - // Last time any user took action - uint256 public lastUpdateTime; - // Ever increasing rewardPerToken rate, based on % of total supply - uint256 public rewardPerTokenStored; - mapping(address => UserData) public userData; - // Locked reward tracking - mapping(address => Reward[]) public userRewards; - mapping(address => uint64) public userClaim; - - struct UserData { - uint128 rewardPerTokenPaid; - uint128 rewards; - uint64 lastAction; - uint64 rewardCount; - } - - struct Reward { - uint64 start; - uint64 finish; - uint128 rate; - } - - constructor( - address _nexus, - address _stakingToken, - address _boostDirector, - uint256 _priceCoeff, - uint256 _coeff, - address _rewardsToken - ) - InitializableRewardsDistributionRecipient(_nexus) - BoostedTokenWrapper(_stakingToken, _boostDirector, _priceCoeff, _coeff) - { - rewardsToken = IERC20(_rewardsToken); - } - - /** - * @dev StakingRewards is a TokenWrapper and RewardRecipient - * Constants added to bytecode at deployTime to reduce SLOAD cost - */ - function initialize( - address _rewardsDistributor, - string calldata _nameArg, - string calldata _symbolArg - ) external initializer { - InitializableRewardsDistributionRecipient._initialize(_rewardsDistributor); - BoostedTokenWrapper._initialize(_nameArg, _symbolArg); - } - - /** - * @dev Updates the reward for a given address, before executing function. - * Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to - * (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed - * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). - */ - modifier updateReward(address _account) { - _updateReward(_account); - _; - } - - function _updateReward(address _account) internal { - uint256 currentTime = block.timestamp; - uint64 currentTime64 = SafeCast.toUint64(currentTime); - - // Setting of global vars - (uint256 newRewardPerToken, uint256 lastApplicableTime) = _rewardPerToken(); - // If statement protects against loss in initialisation case - if (newRewardPerToken > 0) { - rewardPerTokenStored = newRewardPerToken; - lastUpdateTime = lastApplicableTime; - - // Setting of personal vars based on new globals - if (_account != address(0)) { - UserData memory data = userData[_account]; - uint256 earned_ = _earned(_account, data.rewardPerTokenPaid, newRewardPerToken); - - // If earned == 0, then it must either be the initial stake, or an action in the - // same block, since new rewards unlock after each block. - if (earned_ > 0) { - uint256 unlocked = earned_.mulTruncate(UNLOCK); - uint256 locked = earned_ - unlocked; - - userRewards[_account].push( - Reward({ - start: SafeCast.toUint64(LOCKUP + data.lastAction), - finish: SafeCast.toUint64(LOCKUP + currentTime), - rate: SafeCast.toUint128(locked / (currentTime - data.lastAction)) - }) - ); - - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: SafeCast.toUint128(unlocked + data.rewards), - lastAction: currentTime64, - rewardCount: data.rewardCount + 1 - }); - } else { - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: data.rewards, - lastAction: currentTime64, - rewardCount: data.rewardCount - }); - } - } - } else if (_account != address(0)) { - // This should only be hit once, for first staker in initialisation case - userData[_account].lastAction = currentTime64; - } - } - - /** @dev Updates the boost for a given address, after the rest of the function has executed */ - modifier updateBoost(address _account) { - _; - _setBoost(_account); - } - - /*************************************** - ACTIONS - EXTERNAL - ****************************************/ - - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _stake(msg.sender, _amount); - } - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) - external - override - updateReward(_beneficiary) - updateBoost(_beneficiary) - { - _stake(_beneficiary, _amount); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external override updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(rawBalanceOf(msg.sender)); - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - _claimRewards(first, last); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(rawBalanceOf(msg.sender)); - _claimRewards(_first, _last); - } - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(_amount); - } - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external override updateReward(msg.sender) updateBoost(msg.sender) { - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - if (unlocked > 0) { - rewardsToken.safeTransfer(msg.sender, unlocked); - emit RewardPaid(msg.sender, unlocked); - } - } - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external override updateReward(msg.sender) updateBoost(msg.sender) { - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - - _claimRewards(first, last); - } - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) - external - override - updateReward(msg.sender) - updateBoost(msg.sender) - { - _claimRewards(_first, _last); - } - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) - external - override - updateReward(_account) - updateBoost(_account) - { - emit Poked(_account); - } - - /*************************************** - ACTIONS - INTERNAL - ****************************************/ - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function _claimRewards(uint256 _first, uint256 _last) internal { - (uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last); - userClaim[msg.sender] = uint64(lastTimestamp); - - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - uint256 total = unclaimed + unlocked; - - if (total > 0) { - rewardsToken.safeTransfer(msg.sender, total); - - emit RewardPaid(msg.sender, total); - } - } - - /** - * @dev Internally stakes an amount by depositing from sender, - * and crediting to the specified beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function _stake(address _beneficiary, uint256 _amount) internal { - require(_amount > 0, "Cannot stake 0"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - _stakeRaw(_beneficiary, _amount); - emit Staked(_beneficiary, _amount, msg.sender); - } - - /** - * @dev Withdraws raw units from the sender - * @param _amount Units of StakingToken - */ - function _withdraw(uint256 _amount) internal { - require(_amount > 0, "Cannot withdraw 0"); - _withdrawRaw(_amount); - emit Withdrawn(msg.sender, _amount); - } - - /*************************************** - GETTERS - ****************************************/ - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() external view override returns (IERC20) { - return rewardsToken; - } - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() public view override returns (uint256) { - return StableMath.min(block.timestamp, periodFinish); - } - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() public view override returns (uint256) { - (uint256 rewardPerToken_, ) = _rewardPerToken(); - return rewardPerToken_; - } - - function _rewardPerToken() - internal - view - returns (uint256 rewardPerToken_, uint256 lastTimeRewardApplicable_) - { - uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD - uint256 timeDelta = lastApplicableTime - lastUpdateTime; // + 1 SLOAD - // If this has been called twice in the same block, shortcircuit to reduce gas - if (timeDelta == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units to distribute = rewardRate * timeSinceLastUpdate - uint256 rewardUnitsToDistribute = rewardRate * timeDelta; // + 1 SLOAD - uint256 supply = totalSupply(); // + 1 SLOAD - // If there is no StakingToken liquidity, avoid div(0) - // If there is nothing to distribute, short circuit - if (supply == 0 || rewardUnitsToDistribute == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens - uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(supply); - // return summed rate - return (rewardPerTokenStored + unitsToDistributePerToken, lastApplicableTime); // + 1 SLOAD - } - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) public view override returns (uint256) { - uint256 newEarned = _earned( - _account, - userData[_account].rewardPerTokenPaid, - rewardPerToken() - ); - uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK); - return immediatelyUnlocked + userData[_account].rewards; - } - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - override - returns ( - uint256 amount, - uint256 first, - uint256 last - ) - { - (first, last) = _unclaimedEpochs(_account); - (uint256 unlocked, ) = _unclaimedRewards(_account, first, last); - amount = unlocked + earned(_account); - } - - /** @dev Returns only the most recently earned rewards */ - function _earned( - address _account, - uint256 _userRewardPerTokenPaid, - uint256 _currentRewardPerToken - ) internal view returns (uint256) { - // current rate per token - rate user previously received - uint256 userRewardDelta = _currentRewardPerToken - _userRewardPerTokenPaid; // + 1 SLOAD - // Short circuit if there is nothing new to distribute - if (userRewardDelta == 0) { - return 0; - } - // new reward = staked tokens * difference in rate - uint256 userNewReward = balanceOf(_account).mulTruncate(userRewardDelta); // + 1 SLOAD - // add to previous rewards - return userNewReward; - } - - /** - * @dev Gets the first and last indexes of array elements containing unclaimed rewards - */ - function _unclaimedEpochs(address _account) - internal - view - returns (uint256 first, uint256 last) - { - uint64 lastClaim = userClaim[_account]; - - uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account); - uint256 lastUnclaimed = _findLastUnclaimed(_account); - - return (firstUnclaimed, lastUnclaimed); - } - - /** - * @dev Sums the cumulative rewards from a valid range - */ - function _unclaimedRewards( - address _account, - uint256 _first, - uint256 _last - ) internal view returns (uint256 amount, uint256 latestTimestamp) { - uint256 currentTime = block.timestamp; - uint64 lastClaim = userClaim[_account]; - - // Check for no rewards unlocked - uint256 totalLen = userRewards[_account].length; - if (_first == 0 && _last == 0) { - if (totalLen == 0 || currentTime <= userRewards[_account][0].start) { - return (0, currentTime); - } - } - // If there are previous unlocks, check for claims that would leave them untouchable - if (_first > 0) { - require( - lastClaim >= userRewards[_account][_first - 1].finish, - "Invalid _first arg: Must claim earlier entries" - ); - } - - uint256 count = _last - _first + 1; - for (uint256 i = 0; i < count; i++) { - uint256 id = _first + i; - Reward memory rwd = userRewards[_account][id]; - - require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch"); - - uint256 endTime = StableMath.min(rwd.finish, currentTime); - uint256 startTime = StableMath.max(rwd.start, lastClaim); - uint256 unclaimed = (endTime - startTime) * rwd.rate; - - amount += unclaimed; - } - - // Calculate last relevant timestamp here to allow users to avoid issue of OOG errors - // by claiming rewards in batches. - latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish); - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findFirstUnclaimed(uint64 _lastClaim, address _account) - internal - view - returns (uint256 first) - { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (_lastClaim > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findLastUnclaimed(address _account) internal view returns (uint256 first) { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min + max + 1) / 2; - if (block.timestamp > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid - 1; - } - } - return min; - } - - /*************************************** - ADMIN - ****************************************/ - - /** - * @dev Notifies the contract that new rewards have been added. - * Calculates an updated rewardRate based on the rewards in period. - * @param _reward Units of RewardToken that have been added to the pool - */ - function notifyRewardAmount(uint256 _reward) - external - override - onlyRewardsDistributor - updateReward(address(0)) - { - require(_reward < 1e24, "Cannot notify with more than a million units"); - - uint256 currentTime = block.timestamp; - // If previous period over, reset rewardRate - if (currentTime >= periodFinish) { - rewardRate = _reward / DURATION; - } - // If additional reward to existing period, calc sum - else { - uint256 remaining = periodFinish - currentTime; - uint256 leftover = remaining * rewardRate; - rewardRate = (_reward + leftover) / DURATION; - } - - lastUpdateTime = currentTime; - periodFinish = currentTime + DURATION; - - emit RewardAdded(_reward); - } -} diff --git a/contracts/legacy/v-mUSD.sol b/contracts/legacy/v-mUSD.sol deleted file mode 100644 index a27b4ee4..00000000 --- a/contracts/legacy/v-mUSD.sol +++ /dev/null @@ -1,1861 +0,0 @@ -pragma solidity 0.5.16; - -interface IERC20 { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); -} - -interface IBoostedVaultWithLockup { - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external; - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external; - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) external; - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external; - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external; - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) external; - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external; - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() external view returns (IERC20); - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() external view returns (uint256); - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() external view returns (uint256); - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) external view returns (uint256); - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last - ); -} - -contract ModuleKeys { - // Governance - // =========== - // keccak256("Governance"); - bytes32 internal constant KEY_GOVERNANCE = - 0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d; - //keccak256("Staking"); - bytes32 internal constant KEY_STAKING = - 0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034; - //keccak256("ProxyAdmin"); - bytes32 internal constant KEY_PROXY_ADMIN = - 0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1; - - // mStable - // ======= - // keccak256("OracleHub"); - bytes32 internal constant KEY_ORACLE_HUB = - 0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040; - // keccak256("Manager"); - bytes32 internal constant KEY_MANAGER = - 0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f; - //keccak256("Recollateraliser"); - bytes32 internal constant KEY_RECOLLATERALISER = - 0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f; - //keccak256("MetaToken"); - bytes32 internal constant KEY_META_TOKEN = - 0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2; - // keccak256("SavingsManager"); - bytes32 internal constant KEY_SAVINGS_MANAGER = - 0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1; - // keccak256("Liquidator"); - bytes32 internal constant KEY_LIQUIDATOR = - 0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4; -} - -interface INexus { - function governor() external view returns (address); - - function getModule(bytes32 key) external view returns (address); - - function proposeModule(bytes32 _key, address _addr) external; - - function cancelProposedModule(bytes32 _key) external; - - function acceptProposedModule(bytes32 _key) external; - - function acceptProposedModules(bytes32[] calldata _keys) external; - - function requestLockModule(bytes32 _key) external; - - function cancelLockModule(bytes32 _key) external; - - function lockModule(bytes32 _key) external; -} - -contract InitializableModule2 is ModuleKeys { - INexus public constant nexus = INexus(0xAFcE80b19A8cE13DEc0739a1aaB7A028d6845Eb3); - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - require(msg.sender == _governor(), "Only governor can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Governance. - * Governance is either Governor address or Governance address. - */ - modifier onlyGovernance() { - require( - msg.sender == _governor() || msg.sender == _governance(), - "Only governance can execute" - ); - _; - } - - /** - * @dev Modifier to allow function calls only from the ProxyAdmin. - */ - modifier onlyProxyAdmin() { - require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Manager. - */ - modifier onlyManager() { - require(msg.sender == _manager(), "Only manager can execute"); - _; - } - - /** - * @dev Returns Governor address from the Nexus - * @return Address of Governor Contract - */ - function _governor() internal view returns (address) { - return nexus.governor(); - } - - /** - * @dev Returns Governance Module address from the Nexus - * @return Address of the Governance (Phase 2) - */ - function _governance() internal view returns (address) { - return nexus.getModule(KEY_GOVERNANCE); - } - - /** - * @dev Return Staking Module address from the Nexus - * @return Address of the Staking Module contract - */ - function _staking() internal view returns (address) { - return nexus.getModule(KEY_STAKING); - } - - /** - * @dev Return ProxyAdmin Module address from the Nexus - * @return Address of the ProxyAdmin Module contract - */ - function _proxyAdmin() internal view returns (address) { - return nexus.getModule(KEY_PROXY_ADMIN); - } - - /** - * @dev Return MetaToken Module address from the Nexus - * @return Address of the MetaToken Module contract - */ - function _metaToken() internal view returns (address) { - return nexus.getModule(KEY_META_TOKEN); - } - - /** - * @dev Return OracleHub Module address from the Nexus - * @return Address of the OracleHub Module contract - */ - function _oracleHub() internal view returns (address) { - return nexus.getModule(KEY_ORACLE_HUB); - } - - /** - * @dev Return Manager Module address from the Nexus - * @return Address of the Manager Module contract - */ - function _manager() internal view returns (address) { - return nexus.getModule(KEY_MANAGER); - } - - /** - * @dev Return SavingsManager Module address from the Nexus - * @return Address of the SavingsManager Module contract - */ - function _savingsManager() internal view returns (address) { - return nexus.getModule(KEY_SAVINGS_MANAGER); - } - - /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) - */ - function _recollateraliser() internal view returns (address) { - return nexus.getModule(KEY_RECOLLATERALISER); - } -} - -interface IRewardsDistributionRecipient { - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); -} - -contract InitializableRewardsDistributionRecipient is - IRewardsDistributionRecipient, - InitializableModule2 -{ - // @abstract - function notifyRewardAmount(uint256 reward) external; - - function getRewardToken() external view returns (IERC20); - - // This address has the ability to distribute the rewards - address public rewardsDistributor; - - /** @dev Recipient is a module, governed by mStable governance */ - function _initialize(address _rewardsDistributor) internal { - rewardsDistributor = _rewardsDistributor; - } - - /** - * @dev Only the rewards distributor can notify about rewards - */ - modifier onlyRewardsDistributor() { - require(msg.sender == rewardsDistributor, "Caller is not reward distributor"); - _; - } - - /** - * @dev Change the rewardsDistributor - only called by mStable governor - * @param _rewardsDistributor Address of the new distributor - */ - function setRewardsDistribution(address _rewardsDistributor) external onlyGovernor { - rewardsDistributor = _rewardsDistributor; - } -} - -contract IERC20WithCheckpointing { - function balanceOf(address _owner) public view returns (uint256); - - function balanceOfAt(address _owner, uint256 _blockNumber) public view returns (uint256); - - function totalSupply() public view returns (uint256); - - function totalSupplyAt(uint256 _blockNumber) public view returns (uint256); -} - -contract IIncentivisedVotingLockup is IERC20WithCheckpointing { - function getLastUserPoint(address _addr) - external - view - returns ( - int128 bias, - int128 slope, - uint256 ts - ); - - function createLock(uint256 _value, uint256 _unlockTime) external; - - function withdraw() external; - - function increaseLockAmount(uint256 _value) external; - - function increaseLockLength(uint256 _unlockTime) external; - - function eject(address _user) external; - - function expireContract() external; - - function claimReward() public; - - function earned(address _account) public view returns (uint256); -} - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. Does not include - * the optional functions; to access them see {ERC20Detailed}. - */ - -/** - * @dev Wrappers over Solidity's arithmetic operations with added overflow - * checks. - * - * Arithmetic operations in Solidity wrap on overflow. This can easily result - * in bugs, because programmers usually assume that an overflow raises an - * error, which is the standard behavior in high level programming languages. - * `SafeMath` restores this intuition by reverting the transaction when an - * operation overflows. - * - * Using this library instead of the unchecked operations eliminates an entire - * class of bugs, so it's recommended to use it always. - */ -library SafeMath { - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, "SafeMath: addition overflow"); - - return c; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - return sub(a, b, "SafeMath: subtraction overflow"); - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - * - * _Available since v2.4.0._ - */ - function sub( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b <= a, errorMessage); - uint256 c = a - b; - - return c; - } - - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) { - return 0; - } - - uint256 c = a * b; - require(c / a == b, "SafeMath: multiplication overflow"); - - return c; - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - return div(a, b, "SafeMath: division by zero"); - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts with custom message on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - * - * _Available since v2.4.0._ - */ - function div( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - // Solidity only automatically asserts when dividing by 0 - require(b > 0, errorMessage); - uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold - - return c; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - return mod(a, b, "SafeMath: modulo by zero"); - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts with custom message when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - * - * _Available since v2.4.0._ - */ - function mod( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b != 0, errorMessage); - return a % b; - } -} - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // According to EIP-1052, 0x0 is the value returned for not-yet created accounts - // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned - // for accounts without code, i.e. `keccak256('')` - bytes32 codehash; - bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; - // solhint-disable-next-line no-inline-assembly - assembly { - codehash := extcodehash(account) - } - return (codehash != accountHash && codehash != 0x0); - } - - /** - * @dev Converts an `address` into `address payable`. Note that this is - * simply a type cast: the actual underlying value is not changed. - * - * _Available since v2.4.0._ - */ - function toPayable(address account) internal pure returns (address payable) { - return address(uint160(account)); - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - * - * _Available since v2.4.0._ - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-call-value - (bool success, ) = recipient.call.value(amount)(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } -} - -library SafeERC20 { - using SafeMath for uint256; - using Address for address; - - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { - callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { - callOptionalReturn( - token, - abi.encodeWithSelector(token.transferFrom.selector, from, to, value) - ); - } - - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender).add(value); - callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { - uint256 newAllowance = token.allowance(address(this), spender).sub( - value, - "SafeERC20: decreased allowance below zero" - ); - callOptionalReturn( - token, - abi.encodeWithSelector(token.approve.selector, spender, newAllowance) - ); - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. - - // A Solidity high level call has three parts: - // 1. The target address is checked to verify it contains contract code - // 2. The call itself is made, and success asserted - // 3. The return value is decoded, which in turn checks the size of the returned data. - // solhint-disable-next-line max-line-length - require(address(token).isContract(), "SafeERC20: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = address(token).call(data); - require(success, "SafeERC20: low-level call failed"); - - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} - -contract InitializableReentrancyGuard { - bool private _notEntered; - - function _initialize() internal { - // Storing an initial non-zero value makes deployment a bit more - // expensive, but in exchange the refund on every call to nonReentrant - // will be lower in amount. Since refunds are capped to a percetange of - // the total transaction's gas, it is best to keep them low in cases - // like this one, to increase the likelihood of the full refund coming - // into effect. - _notEntered = true; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_notEntered, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _notEntered = false; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _notEntered = true; - } -} - -library StableMath { - using SafeMath for uint256; - - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @notice Token Ratios are used when converting between units of bAsset, mAsset and MTA - * Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold) - * @dev bAsset ratio unit for use in exact calculations, - * where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit - */ - uint256 private constant RATIO_SCALE = 1e8; - - /** - * @dev Provides an interface to the scaling unit - * @return Scaling unit (1e18 or 1 * 10**18) - */ - function getFullScale() internal pure returns (uint256) { - return FULL_SCALE; - } - - /** - * @dev Provides an interface to the ratio unit - * @return Ratio scale unit (1e8 or 1 * 10**8) - */ - function getRatioScale() internal pure returns (uint256) { - return RATIO_SCALE; - } - - /** - * @dev Scales a given integer to the power of the full scale. - * @param x Simple uint256 to scale - * @return Scaled value a to an exact number - */ - function scaleInteger(uint256 x) internal pure returns (uint256) { - return x.mul(FULL_SCALE); - } - - /*************************************** - PRECISE ARITHMETIC - ****************************************/ - - /** - * @dev Multiplies two precise units, and then truncates by the full scale - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) { - return mulTruncateScale(x, y, FULL_SCALE); - } - - /** - * @dev Multiplies two precise units, and then truncates by the given scale. For example, - * when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18 - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @param scale Scale unit - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit - */ - function mulTruncateScale( - uint256 x, - uint256 y, - uint256 scale - ) internal pure returns (uint256) { - // e.g. assume scale = fullScale - // z = 10e18 * 9e17 = 9e36 - uint256 z = x.mul(y); - // return 9e38 / 1e18 = 9e18 - return z.div(scale); - } - - /** - * @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result - * @param x Left hand input to multiplication - * @param y Right hand input to multiplication - * @return Result after multiplying the two inputs and then dividing by the shared - * scale unit, rounded up to the closest base unit. - */ - function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e17 * 17268172638 = 138145381104e17 - uint256 scaled = x.mul(y); - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled.add(FULL_SCALE.sub(1)); - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil.div(FULL_SCALE); - } - - /** - * @dev Precisely divides two units, by first scaling the left hand operand. Useful - * for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17) - * @param x Left hand input to division - * @param y Right hand input to division - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) { - // e.g. 8e18 * 1e18 = 8e36 - uint256 z = x.mul(FULL_SCALE); - // e.g. 8e36 / 10e18 = 8e17 - return z.div(y); - } - - /*************************************** - RATIO FUNCS - ****************************************/ - - /** - * @dev Multiplies and truncates a token ratio, essentially flooring the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand operand to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the ratio scale - */ - function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - return mulTruncateScale(x, ratio, RATIO_SCALE); - } - - /** - * @dev Multiplies and truncates a token ratio, rounding up the result - * i.e. How much mAsset is this bAsset worth? - * @param x Left hand input to multiplication (i.e Exact quantity) - * @param ratio bAsset ratio - * @return Result after multiplying the two inputs and then dividing by the shared - * ratio scale, rounded up to the closest base unit. - */ - function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) { - // e.g. How much mAsset should I burn for this bAsset (x)? - // 1e18 * 1e8 = 1e26 - uint256 scaled = x.mul(ratio); - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled.add(RATIO_SCALE.sub(1)); - // return 100..00.999e8 / 1e8 = 1e18 - return ceil.div(RATIO_SCALE); - } - - /** - * @dev Precisely divides two ratioed units, by first scaling the left hand operand - * i.e. How much bAsset is this mAsset worth? - * @param x Left hand operand in division - * @param ratio bAsset ratio - * @return Result after multiplying the left operand by the scale, and - * executing the division on the right hand input. - */ - function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) { - // e.g. 1e14 * 1e8 = 1e22 - uint256 y = x.mul(RATIO_SCALE); - // return 1e22 / 1e12 = 1e10 - return y.div(ratio); - } - - /*************************************** - HELPERS - ****************************************/ - - /** - * @dev Calculates minimum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Minimum of the two inputs - */ - function min(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? y : x; - } - - /** - * @dev Calculated maximum of two numbers - * @param x Left hand input - * @param y Right hand input - * @return Maximum of the two inputs - */ - function max(uint256 x, uint256 y) internal pure returns (uint256) { - return x > y ? x : y; - } - - /** - * @dev Clamps a value to an upper bound - * @param x Left hand input - * @param upperBound Maximum possible value to return - * @return Input x clamped to a maximum value, upperBound - */ - function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) { - return x > upperBound ? upperBound : x; - } -} - -library Root { - using SafeMath for uint256; - - /** - * @dev Returns the square root of a given number - * @param x Input - * @return y Square root of Input - */ - function sqrt(uint256 x) internal pure returns (uint256 y) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r.add(x.div(r))) >> 1; - r = (r.add(x.div(r))) >> 1; - r = (r.add(x.div(r))) >> 1; - r = (r.add(x.div(r))) >> 1; - r = (r.add(x.div(r))) >> 1; - r = (r.add(x.div(r))) >> 1; - r = (r.add(x.div(r))) >> 1; // Seven iterations should be enough - uint256 r1 = x.div(r); - - return uint256(r < r1 ? r : r1); - } - } -} - -interface IBoostDirector { - function getBalance(address _user) external returns (uint256); - - function setDirection( - address _old, - address _new, - bool _pokeNew - ) external; - - function whitelistVaults(address[] calldata _vaults) external; -} - -contract BoostedTokenWrapper is InitializableReentrancyGuard { - using SafeMath for uint256; - using StableMath for uint256; - using SafeERC20 for IERC20; - - IERC20 public constant stakingToken = IERC20(0x30647a72Dc82d7Fbb1123EA74716aB8A317Eac19); - // mStable MTA Staking contract via the BoostDirectorV2 - IBoostDirector public constant boostDirector = - IBoostDirector(0xBa05FD2f20AE15B0D3f20DDc6870FeCa6ACd3592); - - uint256 private _totalBoostedSupply; - mapping(address => uint256) private _boostedBalances; - mapping(address => uint256) private _rawBalances; - - // Vars for use in the boost calculations - uint256 private constant MIN_DEPOSIT = 1e18; - uint256 private constant MAX_VMTA = 600000e18; - uint256 private constant MAX_BOOST = 3e18; - uint256 private constant MIN_BOOST = 1e18; - uint256 private constant FLOOR = 98e16; - uint256 public constant boostCoeff = 9; - uint256 public constant priceCoeff = 1e17; - - /** - * @dev TokenWrapper constructor - **/ - function _initialize() internal { - InitializableReentrancyGuard._initialize(); - } - - /** - * @dev Get the total boosted amount - * @return uint256 total supply - */ - function totalSupply() public view returns (uint256) { - return _totalBoostedSupply; - } - - /** - * @dev Get the boosted balance of a given account - * @param _account User for which to retrieve balance - */ - function balanceOf(address _account) public view returns (uint256) { - return _boostedBalances[_account]; - } - - /** - * @dev Get the RAW balance of a given account - * @param _account User for which to retrieve balance - */ - function rawBalanceOf(address _account) public view returns (uint256) { - return _rawBalances[_account]; - } - - /** - * @dev Read the boost for the given address - * @param _account User for which to return the boost - * @return boost where 1x == 1e18 - */ - function getBoost(address _account) public view returns (uint256) { - return balanceOf(_account).divPrecisely(rawBalanceOf(_account)); - } - - /** - * @dev Deposits a given amount of StakingToken from sender - * @param _amount Units of StakingToken - */ - function _stakeRaw(address _beneficiary, uint256 _amount) internal nonReentrant { - _rawBalances[_beneficiary] = _rawBalances[_beneficiary].add(_amount); - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - } - - /** - * @dev Withdraws a given stake from sender - * @param _amount Units of StakingToken - */ - function _withdrawRaw(uint256 _amount) internal nonReentrant { - _rawBalances[msg.sender] = _rawBalances[msg.sender].sub(_amount); - stakingToken.safeTransfer(msg.sender, _amount); - } - - /** - * @dev Updates the boost for the given address according to the formula - * boost = min(0.5 + c * vMTA_balance / imUSD_locked^(7/8), 1.5) - * If rawBalance <= MIN_DEPOSIT, boost is 0 - * @param _account User for which to update the boost - */ - function _setBoost(address _account) internal { - uint256 rawBalance = _rawBalances[_account]; - uint256 boostedBalance = _boostedBalances[_account]; - uint256 boost = MIN_BOOST; - - // Check whether balance is sufficient - // is_boosted is used to minimize gas usage - uint256 scaledBalance = (rawBalance * priceCoeff) / 1e18; - if (rawBalance >= MIN_DEPOSIT) { - uint256 votingWeight = boostDirector.getBalance(_account); - boost = _computeBoost(scaledBalance, votingWeight); - } - - uint256 newBoostedBalance = rawBalance.mulTruncate(boost); - - if (newBoostedBalance != boostedBalance) { - _totalBoostedSupply = _totalBoostedSupply.sub(boostedBalance).add(newBoostedBalance); - _boostedBalances[_account] = newBoostedBalance; - } - } - - /** - * @dev Computes the boost for - * boost = min(m, max(1, 0.95 + c * min(voting_weight, f) / deposit^(3/4))) - * @param _scaledDeposit deposit amount in terms of USD - */ - function _computeBoost(uint256 _scaledDeposit, uint256 _votingWeight) - private - view - returns (uint256 boost) - { - if (_votingWeight == 0) return MIN_BOOST; - - // Compute balance to the power 3/4 - uint256 sqrt1 = Root.sqrt(_scaledDeposit * 1e6); - uint256 sqrt2 = Root.sqrt(sqrt1); - uint256 denominator = sqrt1 * sqrt2; - boost = - (((StableMath.min(_votingWeight, MAX_VMTA) * boostCoeff) / 10) * 1e18) / - denominator; - boost = StableMath.min(MAX_BOOST, StableMath.max(MIN_BOOST, FLOOR + boost)); - } -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library SafeCast { - /** - * @dev Returns the downcasted uint128 from uint256, reverting on - * overflow (when the input is greater than largest uint128). - * - * Counterpart to Solidity's `uint128` operator. - * - * Requirements: - * - * - input must fit into 128 bits - */ - function toUint128(uint256 value) internal pure returns (uint128) { - require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); - return uint128(value); - } - - /** - * @dev Returns the downcasted uint64 from uint256, reverting on - * overflow (when the input is greater than largest uint64). - * - * Counterpart to Solidity's `uint64` operator. - * - * Requirements: - * - * - input must fit into 64 bits - */ - function toUint64(uint256 value) internal pure returns (uint64) { - require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); - return uint64(value); - } - - /** - * @dev Returns the downcasted uint32 from uint256, reverting on - * overflow (when the input is greater than largest uint32). - * - * Counterpart to Solidity's `uint32` operator. - * - * Requirements: - * - * - input must fit into 32 bits - */ - function toUint32(uint256 value) internal pure returns (uint32) { - require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); - return uint32(value); - } - - /** - * @dev Returns the downcasted uint16 from uint256, reverting on - * overflow (when the input is greater than largest uint16). - * - * Counterpart to Solidity's `uint16` operator. - * - * Requirements: - * - * - input must fit into 16 bits - */ - function toUint16(uint256 value) internal pure returns (uint16) { - require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); - return uint16(value); - } - - /** - * @dev Returns the downcasted uint8 from uint256, reverting on - * overflow (when the input is greater than largest uint8). - * - * Counterpart to Solidity's `uint8` operator. - * - * Requirements: - * - * - input must fit into 8 bits. - */ - function toUint8(uint256 value) internal pure returns (uint8) { - require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); - return uint8(value); - } -} - -// Internal -// Libs -/** - * @title BoostedSavingsVault - * @author Stability Labs Pty. Ltd. - * @notice Accrues rewards second by second, based on a users boosted balance - * @dev Forked from rewards/staking/StakingRewards.sol - * Changes: - * - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months) - * - `updateBoost` hook called after every external action to reset a users boost - * - Struct packing of common data - * - Searching for and claiming of unlocked rewards - */ -contract BoostedSavingsVault is - IBoostedVaultWithLockup, - Initializable, - InitializableRewardsDistributionRecipient, - BoostedTokenWrapper -{ - using StableMath for uint256; - using SafeCast for uint256; - - event RewardAdded(uint256 reward); - event Staked(address indexed user, uint256 amount, address payer); - event Withdrawn(address indexed user, uint256 amount); - event Poked(address indexed user); - event RewardPaid(address indexed user, uint256 reward); - - IERC20 public constant rewardsToken = IERC20(0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2); - - uint64 public constant DURATION = 7 days; - // Length of token lockup, after rewards are earned - uint256 public constant LOCKUP = 26 weeks; - // Percentage of earned tokens unlocked immediately - uint64 public constant UNLOCK = 2e17; - - // Timestamp for current period finish - uint256 public periodFinish; - // RewardRate for the rest of the PERIOD - uint256 public rewardRate; - // Last time any user took action - uint256 public lastUpdateTime; - // Ever increasing rewardPerToken rate, based on % of total supply - uint256 public rewardPerTokenStored; - mapping(address => UserData) public userData; - // Locked reward tracking - mapping(address => Reward[]) public userRewards; - mapping(address => uint64) public userClaim; - - struct UserData { - uint128 rewardPerTokenPaid; - uint128 rewards; - uint64 lastAction; - uint64 rewardCount; - } - - struct Reward { - uint64 start; - uint64 finish; - uint128 rate; - } - - /** - * @dev StakingRewards is a TokenWrapper and RewardRecipient - * Constants added to bytecode at deployTime to reduce SLOAD cost - */ - function initialize(address _rewardsDistributor) external initializer { - InitializableRewardsDistributionRecipient._initialize(_rewardsDistributor); - BoostedTokenWrapper._initialize(); - } - - /** - * @dev Updates the reward for a given address, before executing function. - * Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to - * (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed - * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). - */ - modifier updateReward(address _account) { - _updateReward(_account); - _; - } - - function _updateReward(address _account) internal { - uint256 currentTime = block.timestamp; - uint64 currentTime64 = SafeCast.toUint64(currentTime); - - // Setting of global vars - (uint256 newRewardPerToken, uint256 lastApplicableTime) = _rewardPerToken(); - // If statement protects against loss in initialisation case - if (newRewardPerToken > 0) { - rewardPerTokenStored = newRewardPerToken; - lastUpdateTime = lastApplicableTime; - - // Setting of personal vars based on new globals - if (_account != address(0)) { - UserData memory data = userData[_account]; - uint256 earned = _earned(_account, data.rewardPerTokenPaid, newRewardPerToken); - - // If earned == 0, then it must either be the initial stake, or an action in the - // same block, since new rewards unlock after each block. - if (earned > 0) { - uint256 unlocked = earned.mulTruncate(UNLOCK); - uint256 locked = earned.sub(unlocked); - - userRewards[_account].push( - Reward({ - start: SafeCast.toUint64(LOCKUP.add(data.lastAction)), - finish: SafeCast.toUint64(LOCKUP.add(currentTime)), - rate: SafeCast.toUint128(locked.div(currentTime.sub(data.lastAction))) - }) - ); - - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: SafeCast.toUint128(unlocked.add(data.rewards)), - lastAction: currentTime64, - rewardCount: data.rewardCount + 1 - }); - } else { - userData[_account] = UserData({ - rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken), - rewards: data.rewards, - lastAction: currentTime64, - rewardCount: data.rewardCount - }); - } - } - } else if (_account != address(0)) { - // This should only be hit once, for first staker in initialisation case - userData[_account].lastAction = currentTime64; - } - } - - /** @dev Updates the boost for a given address, after the rest of the function has executed */ - modifier updateBoost(address _account) { - _; - _setBoost(_account); - } - - /*************************************** - ACTIONS - EXTERNAL - ****************************************/ - - /** - * @dev Stakes a given amount of the StakingToken for the sender - * @param _amount Units of StakingToken - */ - function stake(uint256 _amount) external updateReward(msg.sender) updateBoost(msg.sender) { - _stake(msg.sender, _amount); - } - - /** - * @dev Stakes a given amount of the StakingToken for a given beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function stake(address _beneficiary, uint256 _amount) - external - updateReward(_beneficiary) - updateBoost(_beneficiary) - { - _stake(_beneficiary, _amount); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function exit() external updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(rawBalanceOf(msg.sender)); - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - _claimRewards(first, last); - } - - /** - * @dev Withdraws stake from pool and claims any unlocked rewards. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function exit(uint256 _first, uint256 _last) - external - updateReward(msg.sender) - updateBoost(msg.sender) - { - _withdraw(rawBalanceOf(msg.sender)); - _claimRewards(_first, _last); - } - - /** - * @dev Withdraws given stake amount from the pool - * @param _amount Units of the staked token to withdraw - */ - function withdraw(uint256 _amount) external updateReward(msg.sender) updateBoost(msg.sender) { - _withdraw(_amount); - } - - /** - * @dev Claims only the tokens that have been immediately unlocked, not including - * those that are in the lockers. - */ - function claimReward() external updateReward(msg.sender) updateBoost(msg.sender) { - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - if (unlocked > 0) { - rewardsToken.safeTransfer(msg.sender, unlocked); - emit RewardPaid(msg.sender, unlocked); - } - } - - /** - * @dev Claims all unlocked rewards for sender. - * Note, this function is costly - the args for _claimRewards - * should be determined off chain and then passed to other fn - */ - function claimRewards() external updateReward(msg.sender) updateBoost(msg.sender) { - (uint256 first, uint256 last) = _unclaimedEpochs(msg.sender); - - _claimRewards(first, last); - } - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function claimRewards(uint256 _first, uint256 _last) - external - updateReward(msg.sender) - updateBoost(msg.sender) - { - _claimRewards(_first, _last); - } - - /** - * @dev Pokes a given account to reset the boost - */ - function pokeBoost(address _account) external updateReward(_account) updateBoost(_account) { - emit Poked(_account); - } - - /*************************************** - ACTIONS - INTERNAL - ****************************************/ - - /** - * @dev Claims all unlocked rewards for sender. Both immediately unlocked - * rewards and also locked rewards past their time lock. - * @param _first Index of the first array element to claim - * @param _last Index of the last array element to claim - */ - function _claimRewards(uint256 _first, uint256 _last) internal { - (uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last); - userClaim[msg.sender] = uint64(lastTimestamp); - - uint256 unlocked = userData[msg.sender].rewards; - userData[msg.sender].rewards = 0; - - uint256 total = unclaimed.add(unlocked); - - if (total > 0) { - rewardsToken.safeTransfer(msg.sender, total); - - emit RewardPaid(msg.sender, total); - } - } - - /** - * @dev Internally stakes an amount by depositing from sender, - * and crediting to the specified beneficiary - * @param _beneficiary Staked tokens are credited to this address - * @param _amount Units of StakingToken - */ - function _stake(address _beneficiary, uint256 _amount) internal { - require(_amount > 0, "Cannot stake 0"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - _stakeRaw(_beneficiary, _amount); - emit Staked(_beneficiary, _amount, msg.sender); - } - - /** - * @dev Withdraws raw units from the sender - * @param _amount Units of StakingToken - */ - function _withdraw(uint256 _amount) internal { - require(_amount > 0, "Cannot withdraw 0"); - _withdrawRaw(_amount); - emit Withdrawn(msg.sender, _amount); - } - - /*************************************** - GETTERS - ****************************************/ - - /** - * @dev Gets the RewardsToken - */ - function getRewardToken() external view returns (IERC20) { - return rewardsToken; - } - - /** - * @dev Gets the last applicable timestamp for this reward period - */ - function lastTimeRewardApplicable() public view returns (uint256) { - return StableMath.min(block.timestamp, periodFinish); - } - - /** - * @dev Calculates the amount of unclaimed rewards per token since last update, - * and sums with stored to give the new cumulative reward per token - * @return 'Reward' per staked token - */ - function rewardPerToken() public view returns (uint256) { - (uint256 rewardPerToken_, ) = _rewardPerToken(); - return rewardPerToken_; - } - - function _rewardPerToken() - internal - view - returns (uint256 rewardPerToken_, uint256 lastTimeRewardApplicable_) - { - uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD - uint256 timeDelta = lastApplicableTime.sub(lastUpdateTime); // + 1 SLOAD - // If this has been called twice in the same block, shortcircuit to reduce gas - if (timeDelta == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units to distribute = rewardRate * timeSinceLastUpdate - uint256 rewardUnitsToDistribute = rewardRate.mul(timeDelta); // + 1 SLOAD - uint256 supply = totalSupply(); // + 1 SLOAD - // If there is no StakingToken liquidity, avoid div(0) - // If there is nothing to distribute, short circuit - if (supply == 0 || rewardUnitsToDistribute == 0) { - return (rewardPerTokenStored, lastApplicableTime); - } - // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens - uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(supply); - // return summed rate - return (rewardPerTokenStored.add(unitsToDistributePerToken), lastApplicableTime); // + 1 SLOAD - } - - /** - * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this - * does NOT include the majority of rewards which will be locked up. - * @param _account User address - * @return Total reward amount earned - */ - function earned(address _account) public view returns (uint256) { - uint256 newEarned = _earned( - _account, - userData[_account].rewardPerTokenPaid, - rewardPerToken() - ); - uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK); - return immediatelyUnlocked.add(userData[_account].rewards); - } - - /** - * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards - * and those that have passed their time lock. - * @param _account User address - * @return amount Total units of unclaimed rewards - * @return first Index of the first userReward that has unlocked - * @return last Index of the last userReward that has unlocked - */ - function unclaimedRewards(address _account) - external - view - returns ( - uint256 amount, - uint256 first, - uint256 last - ) - { - (first, last) = _unclaimedEpochs(_account); - (uint256 unlocked, ) = _unclaimedRewards(_account, first, last); - amount = unlocked.add(earned(_account)); - } - - /** @dev Returns only the most recently earned rewards */ - function _earned( - address _account, - uint256 _userRewardPerTokenPaid, - uint256 _currentRewardPerToken - ) internal view returns (uint256) { - // current rate per token - rate user previously received - uint256 userRewardDelta = _currentRewardPerToken.sub(_userRewardPerTokenPaid); // + 1 SLOAD - // Short circuit if there is nothing new to distribute - if (userRewardDelta == 0) { - return 0; - } - // new reward = staked tokens * difference in rate - uint256 userNewReward = balanceOf(_account).mulTruncate(userRewardDelta); // + 1 SLOAD - // add to previous rewards - return userNewReward; - } - - /** - * @dev Gets the first and last indexes of array elements containing unclaimed rewards - */ - function _unclaimedEpochs(address _account) - internal - view - returns (uint256 first, uint256 last) - { - uint64 lastClaim = userClaim[_account]; - - uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account); - uint256 lastUnclaimed = _findLastUnclaimed(_account); - - return (firstUnclaimed, lastUnclaimed); - } - - /** - * @dev Sums the cumulative rewards from a valid range - */ - function _unclaimedRewards( - address _account, - uint256 _first, - uint256 _last - ) internal view returns (uint256 amount, uint256 latestTimestamp) { - uint256 currentTime = block.timestamp; - uint64 lastClaim = userClaim[_account]; - - // Check for no rewards unlocked - uint256 totalLen = userRewards[_account].length; - if (_first == 0 && _last == 0) { - if (totalLen == 0 || currentTime <= userRewards[_account][0].start) { - return (0, currentTime); - } - } - // If there are previous unlocks, check for claims that would leave them untouchable - if (_first > 0) { - require( - lastClaim >= userRewards[_account][_first.sub(1)].finish, - "Invalid _first arg: Must claim earlier entries" - ); - } - - uint256 count = _last.sub(_first).add(1); - for (uint256 i = 0; i < count; i++) { - uint256 id = _first.add(i); - Reward memory rwd = userRewards[_account][id]; - - require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch"); - - uint256 endTime = StableMath.min(rwd.finish, currentTime); - uint256 startTime = StableMath.max(rwd.start, lastClaim); - uint256 unclaimed = endTime.sub(startTime).mul(rwd.rate); - - amount = amount.add(unclaimed); - } - - // Calculate last relevant timestamp here to allow users to avoid issue of OOG errors - // by claiming rewards in batches. - latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish); - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findFirstUnclaimed(uint64 _lastClaim, address _account) - internal - view - returns (uint256 first) - { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min.add(max).add(1)).div(2); - if (_lastClaim > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid.sub(1); - } - } - return min; - } - - /** - * @dev Uses binarysearch to find the unclaimed lockups for a given account - */ - function _findLastUnclaimed(address _account) internal view returns (uint256 first) { - uint256 len = userRewards[_account].length; - if (len == 0) return 0; - // Binary search - uint256 min = 0; - uint256 max = len - 1; - // Will be always enough for 128-bit numbers - for (uint256 i = 0; i < 128; i++) { - if (min >= max) break; - uint256 mid = (min.add(max).add(1)).div(2); - if (now > userRewards[_account][mid].start) { - min = mid; - } else { - max = mid.sub(1); - } - } - return min; - } - - /*************************************** - ADMIN - ****************************************/ - - /** - * @dev Notifies the contract that new rewards have been added. - * Calculates an updated rewardRate based on the rewards in period. - * @param _reward Units of RewardToken that have been added to the pool - */ - function notifyRewardAmount(uint256 _reward) - external - onlyRewardsDistributor - updateReward(address(0)) - { - require(_reward < 1e24, "Cannot notify with more than a million units"); - - uint256 currentTime = block.timestamp; - // If previous period over, reset rewardRate - if (currentTime >= periodFinish) { - rewardRate = _reward.div(DURATION); - } - // If additional reward to existing period, calc sum - else { - uint256 remaining = periodFinish.sub(currentTime); - uint256 leftover = remaining.mul(rewardRate); - rewardRate = _reward.add(leftover).div(DURATION); - } - - lastUpdateTime = currentTime; - periodFinish = currentTime.add(DURATION); - - emit RewardAdded(_reward); - } -} diff --git a/contracts/rewards/BoostDirector.sol b/contracts/rewards/BoostDirector.sol index 901edf07..3c2e85cc 100644 --- a/contracts/rewards/BoostDirector.sol +++ b/contracts/rewards/BoostDirector.sol @@ -102,7 +102,7 @@ contract BoostDirector is IBoostDirector, ImmutableModule { return bal; } - if (count >= 3) return 0; + return 0; } /** diff --git a/contracts/rewards/BoostDirectorV2.sol b/contracts/rewards/BoostDirectorV2.sol index 365e7443..9b6a293b 100644 --- a/contracts/rewards/BoostDirectorV2.sol +++ b/contracts/rewards/BoostDirectorV2.sol @@ -69,7 +69,7 @@ contract BoostDirectorV2 is IBoostDirector, ImmutableModule { /** * @dev Removes a staked token from the list */ - function removeStakedTkoen(address _stakedToken) external onlyGovernor { + function removeStakedToken(address _stakedToken) external onlyGovernor { uint256 len = stakedTokenContracts.length; for (uint256 i = 0; i < len; i++) { // If we find it, then swap it with the last element and delete the end diff --git a/contracts/rewards/staking/StakingRewards.sol b/contracts/rewards/staking/StakingRewards.sol index b8e7d36d..8a4ecd22 100644 --- a/contracts/rewards/staking/StakingRewards.sol +++ b/contracts/rewards/staking/StakingRewards.sol @@ -75,7 +75,7 @@ contract StakingRewards is address _stakingToken, address _rewardsToken, uint256 _duration - ) public StakingTokenWrapper(_stakingToken) InitializableRewardsDistributionRecipient(_nexus) { + ) StakingTokenWrapper(_stakingToken) InitializableRewardsDistributionRecipient(_nexus) { rewardsToken = IERC20(_rewardsToken); DURATION = _duration; } diff --git a/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol b/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol index c1aaaf52..f84dc0cb 100644 --- a/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol +++ b/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol @@ -78,7 +78,7 @@ contract StakingRewardsWithPlatformToken is address _rewardsToken, address _platformToken, uint256 _duration - ) public StakingTokenWrapper(_stakingToken) InitializableRewardsDistributionRecipient(_nexus) { + ) StakingTokenWrapper(_stakingToken) InitializableRewardsDistributionRecipient(_nexus) { rewardsToken = IERC20(_rewardsToken); platformToken = IERC20(_platformToken); DURATION = _duration; diff --git a/contracts/shared/@openzeppelin/InstantProxyAdmin.sol b/contracts/shared/@openzeppelin/InstantProxyAdmin.sol new file mode 100644 index 00000000..2bbf2bf9 --- /dev/null +++ b/contracts/shared/@openzeppelin/InstantProxyAdmin.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +contract InstantProxyAdmin is ProxyAdmin {} diff --git a/contracts/z_mocks/governance/MockBPT.sol b/contracts/z_mocks/governance/MockBPT.sol new file mode 100644 index 00000000..adafd833 --- /dev/null +++ b/contracts/z_mocks/governance/MockBPT.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.6; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; + +contract MockBPT is ERC20, ERC20Burnable { + constructor(string memory name, string memory symbol) ERC20(name, symbol) { + // 100m initial supply + _mint(msg.sender, 10000 * (10**18)); + } + + function onExitPool(address sender, uint256 amt) external { + _burn(sender, amt); + } +} diff --git a/contracts/z_mocks/governance/MockBVault.sol b/contracts/z_mocks/governance/MockBVault.sol new file mode 100644 index 00000000..594f47cc --- /dev/null +++ b/contracts/z_mocks/governance/MockBVault.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../governance/staking/interfaces/IBVault.sol"; +import "./MockBPT.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Creating mock token: +// - create MockBPT and mint some supply +// - call addPool with the tokens and the raito +// - addPool sets the ratios, and transfers required underlying from the sender + +contract MockBVault is IBVault { + mapping(bytes32 => MockBPT) public pools; + mapping(address => bytes32) public poolIds; + mapping(address => IERC20[]) public tokenData; + + function addPool( + address _addr, + IERC20[] memory _tokens, + uint256[] memory _unitsPerBpt + ) external { + pools[blockhash(block.number - 1)] = MockBPT(_addr); + poolIds[_addr] = blockhash(block.number - 1); + uint256 supply = IERC20(_addr).totalSupply(); + require(supply > 1000e18, "Must have tokens"); + require(_unitsPerBpt.length == 2, "Invalid ratio"); + tokenData[_addr] = _tokens; + _tokens[0].transferFrom(msg.sender, address(this), (_unitsPerBpt[0] * supply) / 1e18); + _tokens[1].transferFrom(msg.sender, address(this), (_unitsPerBpt[1] * supply) / 1e18); + } + + function setUnitsPerBpt(address _poolAddr, uint256[] memory _unitsPerBpt) external { + IERC20[] memory tokens = tokenData[_poolAddr]; + require(_unitsPerBpt.length == tokens.length, "Invalid length"); + // desired + uint256 supply = IERC20(_poolAddr).totalSupply(); + uint256 bal0 = tokens[0].balanceOf(address(this)); + uint256 bal1 = tokens[1].balanceOf(address(this)); + uint256 desired0 = (_unitsPerBpt[0] * supply) / 1e18; + uint256 desired1 = (_unitsPerBpt[1] * supply) / 1e18; + // token 1 + if (bal0 > desired0) { + tokens[0].transfer(msg.sender, bal0 - desired0); + } else { + tokens[0].transferFrom(msg.sender, address(this), desired0 - bal0); + } + // token 2 + if (bal1 > desired1) { + tokens[1].transfer(msg.sender, bal1 - desired1); + } else { + tokens[1].transferFrom(msg.sender, address(this), desired1 - bal1); + } + } + + function exitPool( + bytes32 poolId, + address sender, + address payable recipient, + ExitPoolRequest memory request + ) external override { + MockBPT pool = pools[poolId]; + require(address(pool) != address(0), "Invalid addr"); + + address output = request.assets[0]; + uint256 minOut = request.minAmountsOut[0]; + (, uint256 bptIn, ) = abi.decode(request.userData, (uint256, uint256, uint256)); + // Burn the tokens + pool.onExitPool(sender, bptIn); + + uint256 bptSupply = pool.totalSupply(); + uint256 outputBal = IERC20(output).balanceOf(address(this)); + + // Pay out the underlying + uint256 returnUnits = (((outputBal * bptIn) / bptSupply) * 125) / 100; + require(returnUnits > minOut, "Min out not met"); + IERC20(output).transfer(recipient, returnUnits); + } + + function getPoolTokens(bytes32 poolId) + external + view + override + returns ( + address[] memory tokens, + uint256[] memory balances, + uint256 /*lastChangeBlock*/ + ) + { + MockBPT pool = pools[poolId]; + IERC20[] memory tokenDatas = tokenData[address(pool)]; + uint256 len = tokenDatas.length; + tokens = new address[](len); + balances = new uint256[](len); + for (uint256 i = 0; i < len; i++) { + tokens[i] = address(tokenDatas[i]); + balances[i] = tokenDatas[i].balanceOf(address(this)); + } + } +} diff --git a/contracts/z_mocks/rewards/MockRewardsDistributionRecipient.sol b/contracts/z_mocks/rewards/MockRewardsDistributionRecipient.sol index 7dc2a140..77871dbb 100644 --- a/contracts/z_mocks/rewards/MockRewardsDistributionRecipient.sol +++ b/contracts/z_mocks/rewards/MockRewardsDistributionRecipient.sol @@ -7,7 +7,7 @@ contract MockRewardsDistributionRecipient is IRewardsRecipientWithPlatformToken IERC20 public rewardToken; IERC20 public platformToken; - constructor(IERC20 _rewardToken, IERC20 _platformToken) public { + constructor(IERC20 _rewardToken, IERC20 _platformToken) { rewardToken = _rewardToken; platformToken = _platformToken; } diff --git a/contracts/z_mocks/shared/IMStableVoterProxy.sol b/contracts/z_mocks/shared/IMStableVoterProxy.sol new file mode 100644 index 00000000..0bdf6e7a --- /dev/null +++ b/contracts/z_mocks/shared/IMStableVoterProxy.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.6; + +interface IMStableVoterProxy { + function createLock(uint256 _endTime) external; + + function harvestMta() external; + + function extendLock(uint256 _unlockTime) external; + + function exitLock() external returns (uint256 mtaBalance); + + function changeLockAddress(address _newLock) external; +} diff --git a/hardhat.config.ts b/hardhat.config.ts index a3b25683..d05ee3f6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -43,36 +43,7 @@ export const hardhatConfig = { }, }, solidity: { - // version: "0.8.6", - compilers: [ - { - version: "0.8.6", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - { - version: "0.8.2", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - { - version: "0.5.16", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - ], + version: "0.8.6", settings: { optimizer: { enabled: true, diff --git a/tasks-fork.config.ts b/tasks-fork.config.ts index 70fc1377..02200db1 100644 --- a/tasks-fork.config.ts +++ b/tasks-fork.config.ts @@ -2,7 +2,6 @@ import config from "./hardhat-fork.config" import "./tasks/deployIntegration" import "./tasks/deployRewards" -import "./tasks/deployLegacy" import "./tasks/deployMbtc" import "./tasks/deployFeeders" import "./tasks/deployMV3" diff --git a/tasks.config.ts b/tasks.config.ts index 2de3172c..36261230 100644 --- a/tasks.config.ts +++ b/tasks.config.ts @@ -2,7 +2,6 @@ import config from "./hardhat.config" import "./tasks/deployIntegration" import "./tasks/deployRewards" -import "./tasks/deployLegacy" import "./tasks/deployMbtc" import "./tasks/deployFeeders" import "./tasks/deployMV3" diff --git a/tasks/deployLegacy.ts b/tasks/deployLegacy.ts deleted file mode 100644 index 7acef462..00000000 --- a/tasks/deployLegacy.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* eslint-disable no-await-in-loop */ -/* eslint-disable no-restricted-syntax */ -import "ts-node/register" -import "tsconfig-paths/register" -import { subtask, task, types } from "hardhat/config" - -import { BN, simpleToExactAmount } from "@utils/math" -import { DelayedProxyAdmin__factory } from "types" -import { Contract } from "@ethersproject/contracts" -import { ONE_DAY } from "@utils/constants" -import { expect } from "chai" -import { BigNumberish, Signer } from "ethers" -import { getChain, getChainAddress, resolveAddress } from "./utils/networkAddressFactory" -import { getSigner } from "./utils/signerFactory" -import { Chain, deployContract, logTxDetails } from "./utils" -import { verifyEtherscan } from "./utils/etherscan" - -interface UserBalance { - user: string - balance: BigNumberish -} -interface VaultData { - underlyingTokenSymbol: string - stakingTokenType: "savings" | "feederPool" - priceCoeff?: BN - platformToken?: string - name: string - symbol: string - userBal: UserBalance -} - -const boostCoeff = 9 -const btcPriceCoeff = simpleToExactAmount(48000) -const vaults: VaultData[] = [ - { - underlyingTokenSymbol: "mBTC", - stakingTokenType: "savings", - priceCoeff: btcPriceCoeff.div(10), - name: "imBTC Vault", - symbol: "v-imBTC", - userBal: { - user: "0x25953c127efd1e15f4d2be82b753d49b12d626d7", - balance: simpleToExactAmount(172), - }, - }, - { - underlyingTokenSymbol: "GUSD", - stakingTokenType: "feederPool", - name: "mUSD/GUSD fPool Vault", - symbol: "v-fPmUSD/GUSD", - userBal: { - user: "0xf794CF2d946BC6eE6eD905F47db211EBd451Aa5F", - balance: simpleToExactAmount(425000), - }, - }, - { - underlyingTokenSymbol: "BUSD", - stakingTokenType: "feederPool", - name: "mUSD/BUSD fPool Vault", - symbol: "v-fPmUSD/BUSD", - userBal: { - user: "0xc09111f9d094d07fc013fd45c4081510ca4275cf", - balance: simpleToExactAmount(1400000), - }, - }, - { - underlyingTokenSymbol: "HBTC", - stakingTokenType: "feederPool", - priceCoeff: btcPriceCoeff, - name: "mBTC/HBTC fPool Vault", - symbol: "v-fPmBTC/HBTC", - userBal: { - user: "0x8d0f5678557192e23d1da1c689e40f25c063eaa5", - balance: simpleToExactAmount(2.4), - }, - }, - { - underlyingTokenSymbol: "TBTC", - stakingTokenType: "feederPool", - priceCoeff: btcPriceCoeff, - name: "mBTC/TBTC fPool Vault", - symbol: "v-fPmBTC/TBTC", - userBal: { - user: "0x6f500bb95ee1cf1a92e45f7697fabb2d477087af", - balance: simpleToExactAmount(2.2), - }, - }, - { - underlyingTokenSymbol: "alUSD", - stakingTokenType: "feederPool", - name: "mUSD/alUSD fPool Vault", - symbol: "v-fPmUSD/alUSD", - platformToken: "ALCX", - userBal: { - user: "0x97020c9ec66e0f59231918b1d2f167a66026aff2", - balance: simpleToExactAmount(1200000), - }, - }, - { - underlyingTokenSymbol: "mUSD", - stakingTokenType: "savings", - priceCoeff: simpleToExactAmount(1, 17), - name: "imUSD Vault", - symbol: "v-imUSD", - userBal: { - user: "0x7606ccf1c5f2a908423eb8dd2fa5d82a12255700", - balance: simpleToExactAmount(68000), - }, - }, -] - -task("LegacyVault.deploy", "Deploys a vault contract") - .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) - .setAction(async (taskArgs, hre) => { - const signer = await getSigner(hre, taskArgs.speed) - const chain = getChain(hre) - - const governorAddress = resolveAddress("Governor", chain) - if (hre.network.name === "hardhat") { - // TODO use impersonate function instead of the following - // impersonate fails with "You probably tried to import the "hardhat" module from your config or a file imported from it." - await hre.network.provider.request({ - method: "hardhat_impersonateAccount", - params: [governorAddress], - }) - await hre.network.provider.request({ - method: "hardhat_setBalance", - params: [governorAddress, "0x8AC7230489E80000"], - }) - } - const governor = hre.ethers.provider.getSigner(governorAddress) - - const nexusAddress = getChainAddress("Nexus", chain) - const boostDirectorAddress = getChainAddress("BoostDirector", chain) - const rewardTokenAddress = resolveAddress("MTA", chain) - const delayedProxyAdminAddress = resolveAddress("DelayedProxyAdmin", chain) - - for (const vault of vaults) { - const stakingTokenAddress = resolveAddress(vault.underlyingTokenSymbol, chain, vault.stakingTokenType) - const vaultProxyAddress = resolveAddress(vault.underlyingTokenSymbol, chain, "vault") - const contractName = vault.platformToken ? "BoostedDualVault" : "BoostedSavingsVault" - - const priceCoeff = vault.priceCoeff ? vault.priceCoeff : simpleToExactAmount(1) - let vaultImpl: Contract - let constructorArguments: any[] - if (vault.underlyingTokenSymbol === "mUSD") { - const vaultFactory = await hre.ethers.getContractFactory( - `contracts/legacy/v-${vault.underlyingTokenSymbol}.sol:${contractName}`, - ) - vaultImpl = await deployContract(vaultFactory.connect(signer), `${vault.underlyingTokenSymbol} vault`) - } else if (vault.platformToken) { - const platformTokenAddress = resolveAddress(vault.platformToken, chain) - constructorArguments = [ - nexusAddress, - stakingTokenAddress, - boostDirectorAddress, - priceCoeff, - boostCoeff, - rewardTokenAddress, - platformTokenAddress, - ] - const vaultFactory = await hre.ethers.getContractFactory( - `contracts/legacy/v-${vault.underlyingTokenSymbol}.sol:${contractName}`, - ) - vaultImpl = await deployContract(vaultFactory.connect(signer), `${vault.underlyingTokenSymbol} vault`, constructorArguments) - } else { - constructorArguments = [nexusAddress, stakingTokenAddress, boostDirectorAddress, priceCoeff, boostCoeff, rewardTokenAddress] - const vaultFactory = await hre.ethers.getContractFactory(`contracts/legacy/v-mBTC.sol:${contractName}`) - vaultImpl = await deployContract(vaultFactory.connect(signer), `${vault.underlyingTokenSymbol} vault`, constructorArguments) - } - - if (hre.network.name === "hardhat") { - const proxyAdmin = DelayedProxyAdmin__factory.connect(delayedProxyAdminAddress, governor) - // the contracts have already been initialized so don't need to call it again - const tx = await proxyAdmin.proposeUpgrade(vaultProxyAddress, vaultImpl.address, "0x") - await logTxDetails(tx, `${vault.underlyingTokenSymbol} proposeUpgrade`) - // increaseTime fails with "You probably tried to import the "hardhat" module from your config or a file imported from it." - // await increaseTime(ONE_WEEK) - - // TODO use increaseTime instead of the following - await hre.ethers.provider.send("evm_increaseTime", [ONE_DAY.mul(8).toNumber()]) - await hre.ethers.provider.send("evm_mine", []) - - const tx2 = await proxyAdmin.acceptUpgradeRequest(vaultProxyAddress) - await logTxDetails(tx2, `${vault.underlyingTokenSymbol} acceptUpgradeRequest`) - } else { - await verifyEtherscan(hre, { - address: vaultImpl.address, - constructorArguments, - }) - console.log(`Delayed Proxy Admin contract ${delayedProxyAdminAddress}`) - console.log(`${vault.underlyingTokenSymbol} proposeUpgrade tx args: proxy ${vaultProxyAddress}, impl ${vaultImpl.address}`) - } - } - - await vaultVerification(hre, signer, chain) - }) - -task("LegacyVault.check", "Checks the vaults post upgrade") - .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) - .setAction(async (taskArgs, hre) => { - const signer = await getSigner(hre, taskArgs.speed) - const chain = getChain(hre) - - await vaultVerification(hre, signer, chain) - }) - -// Post upgrade verification tasks -const vaultVerification = async (hre, signer: Signer, chain: Chain) => { - const nexusAddress = getChainAddress("Nexus", chain) - const boostDirectorAddress = getChainAddress("BoostDirector", chain) - const rewardTokenAddress = resolveAddress("MTA", chain) - - for (const vault of vaults) { - const vaultProxyAddress = resolveAddress(vault.underlyingTokenSymbol, chain, "vault") - const contractName = vault.platformToken ? "BoostedDualVault" : "BoostedSavingsVault" - const vaultFactory = await hre.ethers.getContractFactory( - `contracts/legacy/v-${vault.underlyingTokenSymbol}.sol:${contractName}`, - signer, - ) - const proxy = await vaultFactory.attach(vaultProxyAddress) - - console.log(`About to verify the ${vault.underlyingTokenSymbol} vault`) - - if (vault.underlyingTokenSymbol !== "mUSD") { - expect(await proxy.name(), `${vault.underlyingTokenSymbol} vault name`).to.eq(vault.name) - expect(await proxy.symbol(), `${vault.underlyingTokenSymbol} vault symbol`).to.eq(vault.symbol) - expect(await proxy.decimals(), `${vault.underlyingTokenSymbol} decimals`).to.eq(18) - } - expect(await proxy.nexus(), `${vault.underlyingTokenSymbol} vault nexus`).to.eq(nexusAddress) - expect(await proxy.boostDirector(), `${vault.underlyingTokenSymbol} vault boost director`).to.eq(boostDirectorAddress) - expect(await proxy.getRewardToken(), `${vault.underlyingTokenSymbol} vault reward token`).to.eq(rewardTokenAddress) - expect(await proxy.priceCoeff(), `${vault.underlyingTokenSymbol} vault priceCoeff`).to.eq( - vault.priceCoeff ? vault.priceCoeff : simpleToExactAmount(1), - ) - if (vault.underlyingTokenSymbol === "alUSD") { - expect(await proxy.getPlatformToken(), `${vault.underlyingTokenSymbol} vault platform token`).to.eq( - resolveAddress(vault.platformToken, chain), - ) - } - expect(await proxy.balanceOf(vault.userBal.user), `${vault.underlyingTokenSymbol} vault user balance`).to.gt(vault.userBal.balance) - expect(await proxy.totalSupply(), `${vault.underlyingTokenSymbol} vault total supply`).to.gt(0) - } -} - -export {} diff --git a/tasks/deployRewards.ts b/tasks/deployRewards.ts index 25e2be2d..549e6b98 100644 --- a/tasks/deployRewards.ts +++ b/tasks/deployRewards.ts @@ -3,26 +3,14 @@ import "tsconfig-paths/register" import { task, types } from "hardhat/config" import { ONE_WEEK } from "@utils/constants" -import { Contract } from "@ethersproject/contracts" -import { formatBytes32String } from "ethers/lib/utils" import { simpleToExactAmount } from "@utils/math" -import { params } from "./taskUtils" -import { - AssetProxy__factory, - BoostedDualVault__factory, - SignatureVerifier__factory, - PlatformTokenVendorFactory__factory, - StakedTokenMTA__factory, - QuestManager__factory, - StakedTokenBPT__factory, - BoostDirectorV2__factory, - BoostDirectorV2, -} from "../types/generated" +import { BoostedDualVault__factory, BoostDirectorV2__factory, BoostDirectorV2 } from "../types/generated" import { getChain, getChainAddress, resolveAddress } from "./utils/networkAddressFactory" import { getSignerAccount, getSigner } from "./utils/signerFactory" import { deployContract, logTxDetails } from "./utils/deploy-utils" import { deployVault, VaultData } from "./utils/feederUtils" import { verifyEtherscan } from "./utils/etherscan" +import { deployStakingToken, StakedTokenData } from "./utils/rewardsUtils" task("getBytecode-BoostedDualVault").setAction(async () => { const size = BoostedDualVault__factory.bytecode.length / 2 / 1000 @@ -86,161 +74,26 @@ task("Vault.deploy", "Deploys a vault contract") task("StakedToken.deploy", "Deploys a Staked Token behind a proxy") .addOptionalParam("rewardsToken", "Symbol of rewards token. eg MTA or RMTA for Ropsten", "MTA", types.string) - .addOptionalParam("stakedToken", "Symbol of staked token. eg MTA, BAL, RMTA, RBAL", "MTA", types.string) + .addOptionalParam("stakedToken", "Symbol of staked token. eg MTA, RMTA, BPT or RBPT", "MTA", types.string) + .addOptionalParam("balToken", "Symbol of balancer token. eg BAL or RBAL", "BAL", types.string) .addOptionalParam("balPoolId", "Balancer Pool Id", "0001", types.string) - .addOptionalParam("questMaster", "Address of account that administrates quests", undefined, params.address) - .addOptionalParam("questSigner", "Address of account that signs completed quests", undefined, params.address) - .addOptionalParam("name", "Staked Token name", "Voting MTA V2", types.string) - .addOptionalParam("symbol", "Staked Token symbol", "vMTA", types.string) + .addOptionalParam("name", "Staked Token name", "Staked MTA", types.string) + .addOptionalParam("symbol", "Staked Token symbol", "stkMTA", types.string) .addOptionalParam("cooldown", "Number of seconds for the cooldown period", ONE_WEEK.mul(3).toNumber(), types.int) .addOptionalParam("unstakeWindow", "Number of seconds for the unstake window", ONE_WEEK.mul(2).toNumber(), types.int) .setAction(async (taskArgs, hre) => { const deployer = await getSignerAccount(hre, taskArgs.speed) - const chain = getChain(hre) - - const nexusAddress = getChainAddress("Nexus", chain) - const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) - const rewardsTokenAddress = resolveAddress(taskArgs.rewardsToken, chain) - const stakedTokenAddress = resolveAddress(taskArgs.stakedToken, chain) - const questMasterAddress = taskArgs.questMasterAddress || getChainAddress("QuestMaster", chain) - const questSignerAddress = taskArgs.questSignerAddress || getChainAddress("QuestSigner", chain) - - let signatureVerifierAddress = getChainAddress("SignatureVerifier", chain) - if (!signatureVerifierAddress) { - const signatureVerifier = await deployContract(new SignatureVerifier__factory(deployer.signer), "SignatureVerifier") - signatureVerifierAddress = signatureVerifier.address - - await verifyEtherscan(hre, { - address: signatureVerifierAddress, - contract: "contracts/governance/staking/deps/SignatureVerifier.sol:SignatureVerifier", - }) - } - - let questManagerAddress = getChainAddress("QuestManager", chain) - if (!questManagerAddress) { - const questManagerLibraryAddresses = { - "contracts/governance/staking/deps/SignatureVerifier.sol:SignatureVerifier": signatureVerifierAddress, - } - const questManagerImpl = await deployContract( - new QuestManager__factory(questManagerLibraryAddresses, deployer.signer), - "QuestManager", - [nexusAddress], - ) - const data = questManagerImpl.interface.encodeFunctionData("initialize", [questMasterAddress, questSignerAddress]) - - await verifyEtherscan(hre, { - address: questManagerImpl.address, - contract: "contracts/governance/staking/QuestManager.sol:QuestManager", - constructorArguments: [nexusAddress], - libraries: { - SignatureVerifier: signatureVerifierAddress, - }, - }) - - const constructorArguments = [questManagerImpl.address, deployer.address, data] - const questManagerProxy = await deployContract(new AssetProxy__factory(deployer.signer), "AssetProxy", constructorArguments) - questManagerAddress = questManagerProxy.address - - await verifyEtherscan(hre, { - address: questManagerAddress, - contract: "contracts/upgradability/Proxies.sol:AssetProxy", - constructorArguments, - }) - } - - let platformTokenVendorFactoryAddress = getChainAddress("PlatformTokenVendorFactory", chain) - if (!platformTokenVendorFactoryAddress) { - const platformTokenVendorFactory = await deployContract( - new PlatformTokenVendorFactory__factory(deployer.signer), - "PlatformTokenVendorFactory", - ) - platformTokenVendorFactoryAddress = platformTokenVendorFactory.address - - await verifyEtherscan(hre, { - address: platformTokenVendorFactoryAddress, - constructorArguments: [], - }) - } - const stakedTokenLibraryAddresses = { - "contracts/rewards/staking/PlatformTokenVendorFactory.sol:PlatformTokenVendorFactory": platformTokenVendorFactoryAddress, + const stakingTokenData: StakedTokenData = { + rewardsTokenSymbol: taskArgs.rewardsToken, + stakedTokenSymbol: taskArgs.stakedToken, + balTokenSymbol: taskArgs.balToken, + cooldown: taskArgs.cooldown, + unstakeWindow: taskArgs.unstakeWindow, + name: taskArgs.name, + symbol: taskArgs.symbol, } - let constructorArguments: any[] - let stakedTokenImpl: Contract - let data: string - if (stakedTokenAddress === rewardsTokenAddress) { - constructorArguments = [ - nexusAddress, - rewardsTokenAddress, - questManagerAddress, - rewardsTokenAddress, - taskArgs.cooldown, - taskArgs.unstakeWindow, - ] - - stakedTokenImpl = await deployContract( - new StakedTokenMTA__factory(stakedTokenLibraryAddresses, deployer.signer), - "StakedTokenMTA", - constructorArguments, - ) - data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ - formatBytes32String(taskArgs.name), - formatBytes32String(taskArgs.symbol), - rewardsDistributorAddress, - ]) - } else { - const balPoolIdStr = taskArgs.balPoolId || "1" - const balPoolId = formatBytes32String(balPoolIdStr) - - const balancerVaultAddress = resolveAddress("BalancerVault", chain) - const balancerRecipientAddress = resolveAddress("BalancerRecipient", chain) - - constructorArguments = [ - nexusAddress, - rewardsTokenAddress, - questManagerAddress, - stakedTokenAddress, - taskArgs.cooldown, - taskArgs.unstakeWindow, - [stakedTokenAddress, balancerVaultAddress], - balPoolId, - ] - - console.log(`Staked Token BPT contract size ${StakedTokenBPT__factory.bytecode.length / 2} bytes`) - - stakedTokenImpl = await deployContract( - new StakedTokenBPT__factory(stakedTokenLibraryAddresses, deployer.signer), - "StakedTokenBPT", - constructorArguments, - ) - - data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ - formatBytes32String(taskArgs.name), - formatBytes32String(taskArgs.symbol), - rewardsDistributorAddress, - balancerRecipientAddress, - ]) - } - - await verifyEtherscan(hre, { - address: stakedTokenImpl.address, - constructorArguments, - libraries: { - PlatformTokenVendorFactory: platformTokenVendorFactoryAddress, - }, - }) - - const proxy = await deployContract(new AssetProxy__factory(deployer.signer), "AssetProxy", [ - stakedTokenImpl.address, - deployer.address, - data, - ]) - - await verifyEtherscan(hre, { - address: proxy.address, - contract: "contracts/upgradability/Proxies.sol:AssetProxy", - constructorArguments: [stakedTokenImpl.address, deployer.address, data], - }) + await deployStakingToken(stakingTokenData, deployer, hre) }) export {} diff --git a/tasks/utils/deploy-utils.ts b/tasks/utils/deploy-utils.ts index cd29f54c..3928d088 100644 --- a/tasks/utils/deploy-utils.ts +++ b/tasks/utils/deploy-utils.ts @@ -1,12 +1,13 @@ -import { Contract, ContractFactory, ContractReceipt, ContractTransaction } from "ethers" +import { Contract, ContractFactory, ContractReceipt, ContractTransaction, Overrides } from "ethers" import { formatUnits } from "@ethersproject/units" export const deployContract = async ( contractFactory: ContractFactory, contractName = "Contract", contractorArgs: Array = [], + overrides: Overrides = {}, ): Promise => { - const contract = (await contractFactory.deploy(...contractorArgs)) as T + const contract = (await contractFactory.deploy(...contractorArgs, overrides)) as T console.log( `Deployed ${contractName} contract with hash ${contract.deployTransaction.hash} from ${ contract.deployTransaction.from diff --git a/tasks/utils/networkAddressFactory.ts b/tasks/utils/networkAddressFactory.ts index b1238c62..cf8894c7 100644 --- a/tasks/utils/networkAddressFactory.ts +++ b/tasks/utils/networkAddressFactory.ts @@ -5,6 +5,7 @@ import { AssetAddressTypes, Chain, Token, tokens } from "./tokens" export const contractNames = [ "Nexus", "DelayedProxyAdmin", + "ProxyAdmin", "ProtocolDAO", "Governor", "FundManager", @@ -33,6 +34,7 @@ export const contractNames = [ "PlatformTokenVendorFactory", "BalancerVault", "BalancerRecipient", + "BalancerStakingPoolId", "AaveIncentivesController", "AaveLendingPoolAddressProvider", "AlchemixStakingPool", @@ -46,6 +48,7 @@ export const contractNames = [ "OperationsSigner", "ENSRegistrarController", "ENSResolver", + "IncentivisedVotingLockup", ] as const export type ContractNames = typeof contractNames[number] @@ -66,11 +69,14 @@ export const getChainAddress = (contractName: ContractNames, chain: Chain): stri return "0xAFcE80b19A8cE13DEc0739a1aaB7A028d6845Eb3" case "DelayedProxyAdmin": return "0x5C8eb57b44C1c6391fC7a8A0cf44d26896f92386" + case "ProxyAdmin": + return null case "ProtocolDAO": case "Governor": return "0xF6FF1F7FCEB2cE6d26687EaaB5988b445d0b94a2" + case "BalancerRecipient": case "FundManager": - return "0x437e8c54db5c66bb3d80d2ff156e9bfe31a017db" + return "0x437E8C54Db5C66Bb3D80D2FF156e9bfe31a017db" case "mStableDAO": return "0x3dd46846eed8D147841AE162C8425c08BD8E1b41" case "SavingsManager": @@ -107,8 +113,8 @@ export const getChainAddress = (contractName: ContractNames, chain: Chain): stri return "0xfe99964d9677d7dfb66c5ca609b64f710d2808b8" case "BalancerVault": return "0xBA12222222228d8Ba445958a75a0704d566BF2C8" - case "BalancerRecipient": - return DEAD_ADDRESS + case "BalancerStakingPoolId": + return "0xe2469f47ab58cf9cf59f9822e3c5de4950a41c49000200000000000000000089" case "BasketManager": return "0x66126B4aA2a1C07536Ef8E5e8bD4EfDA1FdEA96D" case "AaveIncentivesController": @@ -135,6 +141,8 @@ export const getChainAddress = (contractName: ContractNames, chain: Chain): stri return "0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5" case "ENSResolver": return "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41" + case "IncentivisedVotingLockup": + return "0xae8bc96da4f9a9613c323478be181fdb2aa0e1bf" default: } } else if (chain === Chain.polygon) { @@ -204,15 +212,17 @@ export const getChainAddress = (contractName: ContractNames, chain: Chain): stri case "QuestManager": return "0x3e8aa84E846EEb89392E99d44cD51acA668ae7BA" case "StakedTokenMTA": - return "0xa5583F67311231A2127D2C6f9a15aB112222C080" + return "0xc3DCB920C30D4a4222220250DD2E8bA0c5A40d51" case "StakedTokenBPT": - return "0x2813Baaf158F53F8251a369c296c7934cb1fbAF0" + return "0x96a3Ee762022be1EA48Fc35DB46169a6182ba5c8" case "PlatformTokenVendorFactory": return "0x91fdDea51aD5A4e050c2A34e209284344206aF8e" case "BalancerVault": - return DEAD_ADDRESS + return "0xBA12222222228d8Ba445958a75a0704d566BF2C8" case "BalancerRecipient": return DEAD_ADDRESS + case "BalancerStakingPoolId": + return `0x021c343c6180f03ce9e48fae3ff432309b9af199000200000000000000000001` case "QuestMaster": return "0x04617083205b2fdd18b15bcf60d06674c6e2c1dc" case "QuestSigner": diff --git a/tasks/utils/rewardsUtils.ts b/tasks/utils/rewardsUtils.ts new file mode 100644 index 00000000..6519c8a3 --- /dev/null +++ b/tasks/utils/rewardsUtils.ts @@ -0,0 +1,210 @@ +import { InstantProxyAdmin__factory } from "./../../types/generated/factories/InstantProxyAdmin__factory" +import { BigNumberish } from "@ethersproject/bignumber" +import { Contract } from "@ethersproject/contracts" +import { formatBytes32String } from "@ethersproject/strings" +import { Account } from "types/common" +import { + AssetProxy__factory, + PlatformTokenVendorFactory__factory, + QuestManager__factory, + SignatureVerifier__factory, + StakedTokenBPT__factory, + StakedTokenMTA__factory, +} from "types/generated" +import { deployContract } from "./deploy-utils" +import { verifyEtherscan } from "./etherscan" +import { getChain, getChainAddress, resolveAddress } from "./networkAddressFactory" + +export interface StakedTokenData { + rewardsTokenSymbol: string + stakedTokenSymbol: string + balTokenSymbol?: string + cooldown: BigNumberish + unstakeWindow: BigNumberish + name: string + symbol: string +} + +export interface StakedTokenDeployAddresses { + stakedToken: string + questManager: string + signatureVerifier: string + platformTokenVendorFactory: string + proxyAdminAddress?: string +} + +export const deployStakingToken = async ( + stakedTokenData: StakedTokenData, + deployer: Account, + hre: any, + overrides?: StakedTokenDeployAddresses, + overrideSigner?: string, +): Promise => { + const chain = getChain(hre) + + const nexusAddress = getChainAddress("Nexus", chain) + const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) + const rewardsTokenAddress = resolveAddress(stakedTokenData.rewardsTokenSymbol, chain) + const stakedTokenAddress = resolveAddress(stakedTokenData.stakedTokenSymbol, chain) + const questMasterAddress = getChainAddress("QuestMaster", chain) + const questSignerAddress = overrideSigner ?? getChainAddress("QuestSigner", chain) + const delayedProxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) + let proxyAdminAddress = overrides + ? overrides.proxyAdminAddress ?? getChainAddress("ProxyAdmin", chain) + : getChainAddress("ProxyAdmin", chain) + + if (!proxyAdminAddress) { + const proxyAdmin = await deployContract(new InstantProxyAdmin__factory(deployer.signer), "InstantProxyAdmin") + await proxyAdmin.transferOwnership(getChainAddress("ProtocolDAO", chain)) + proxyAdminAddress = proxyAdmin.address + } + + let signatureVerifierAddress = overrides ? overrides.signatureVerifier : getChainAddress("SignatureVerifier", chain) + if (!signatureVerifierAddress) { + const signatureVerifier = await deployContract(new SignatureVerifier__factory(deployer.signer), "SignatureVerifier") + signatureVerifierAddress = signatureVerifier.address + + await verifyEtherscan(hre, { + address: signatureVerifierAddress, + contract: "contracts/governance/staking/deps/SignatureVerifier.sol:SignatureVerifier", + }) + } + + let questManagerAddress = overrides ? overrides.questManager : getChainAddress("QuestManager", chain) + if (!questManagerAddress) { + const questManagerLibraryAddresses = { + "contracts/governance/staking/deps/SignatureVerifier.sol:SignatureVerifier": signatureVerifierAddress, + } + const questManagerImpl = await deployContract( + new QuestManager__factory(questManagerLibraryAddresses, deployer.signer), + "QuestManager", + [nexusAddress], + ) + const data = questManagerImpl.interface.encodeFunctionData("initialize", [questMasterAddress, questSignerAddress]) + + await verifyEtherscan(hre, { + address: questManagerImpl.address, + contract: "contracts/governance/staking/QuestManager.sol:QuestManager", + constructorArguments: [nexusAddress], + libraries: { + SignatureVerifier: signatureVerifierAddress, + }, + }) + + const constructorArguments = [questManagerImpl.address, delayedProxyAdminAddress, data] + const questManagerProxy = await deployContract(new AssetProxy__factory(deployer.signer), "AssetProxy", constructorArguments) + questManagerAddress = questManagerProxy.address + + await verifyEtherscan(hre, { + address: questManagerAddress, + contract: "contracts/upgradability/Proxies.sol:AssetProxy", + constructorArguments, + }) + } + + let platformTokenVendorFactoryAddress = overrides + ? overrides.platformTokenVendorFactory + : getChainAddress("PlatformTokenVendorFactory", chain) + if (!platformTokenVendorFactoryAddress) { + const platformTokenVendorFactory = await deployContract( + new PlatformTokenVendorFactory__factory(deployer.signer), + "PlatformTokenVendorFactory", + ) + platformTokenVendorFactoryAddress = platformTokenVendorFactory.address + + await verifyEtherscan(hre, { + address: platformTokenVendorFactoryAddress, + constructorArguments: [], + }) + } + + const stakedTokenLibraryAddresses = { + "contracts/rewards/staking/PlatformTokenVendorFactory.sol:PlatformTokenVendorFactory": platformTokenVendorFactoryAddress, + } + let constructorArguments: any[] + let stakedTokenImpl: Contract + let data: string + if (rewardsTokenAddress === stakedTokenAddress) { + constructorArguments = [ + nexusAddress, + rewardsTokenAddress, + questManagerAddress, + rewardsTokenAddress, + stakedTokenData.cooldown, + stakedTokenData.unstakeWindow, + ] + + stakedTokenImpl = await deployContract( + new StakedTokenMTA__factory(stakedTokenLibraryAddresses, deployer.signer), + "StakedTokenMTA", + constructorArguments, + ) + data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ + formatBytes32String(stakedTokenData.name), + formatBytes32String(stakedTokenData.symbol), + rewardsDistributorAddress, + ]) + } else { + const balAddress = resolveAddress(stakedTokenData.balTokenSymbol, chain) + + const balPoolId = resolveAddress("BalancerStakingPoolId", chain) + const balancerVaultAddress = resolveAddress("BalancerVault", chain) + const balancerRecipientAddress = resolveAddress("BalancerRecipient", chain) + + constructorArguments = [ + nexusAddress, + rewardsTokenAddress, + questManagerAddress, + stakedTokenAddress, + stakedTokenData.cooldown, + stakedTokenData.unstakeWindow, + [balAddress, balancerVaultAddress], + balPoolId, + ] + + console.log(`Staked Token BPT contract size ${StakedTokenBPT__factory.bytecode.length / 2} bytes`) + + stakedTokenImpl = await deployContract( + new StakedTokenBPT__factory(stakedTokenLibraryAddresses, deployer.signer), + "StakedTokenBPT", + constructorArguments, + ) + + const priceCoeff = 42550 + data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ + formatBytes32String(stakedTokenData.name), + formatBytes32String(stakedTokenData.symbol), + rewardsDistributorAddress, + balancerRecipientAddress, + priceCoeff, + ]) + } + + await verifyEtherscan(hre, { + address: stakedTokenImpl.address, + constructorArguments, + libraries: { + PlatformTokenVendorFactory: platformTokenVendorFactoryAddress, + }, + }) + + const proxy = await deployContract(new AssetProxy__factory(deployer.signer), "AssetProxy", [ + stakedTokenImpl.address, + proxyAdminAddress, + data, + ]) + + await verifyEtherscan(hre, { + address: proxy.address, + contract: "contracts/upgradability/Proxies.sol:AssetProxy", + constructorArguments: [stakedTokenImpl.address, deployer.address, data], + }) + + return { + stakedToken: proxy.address, + questManager: questManagerAddress, + signatureVerifier: signatureVerifierAddress, + platformTokenVendorFactory: platformTokenVendorFactoryAddress, + proxyAdminAddress, + } +} diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index a2770c49..8dd66451 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -384,7 +384,7 @@ export const cyMUSD: Token = { export const BAL: Token = { symbol: "BAL", - address: "0xba100000625a3754423978a60c9317c58a424e3d", + address: "0xba100000625a3754423978a60c9317c58a424e3D", chain: Chain.mainnet, decimals: 18, quantityFormatter: "USD", @@ -398,6 +398,22 @@ export const RBAL: Token = { quantityFormatter: "USD", } +export const BPT: Token = { + symbol: "BPT", + address: "0xe2469f47aB58cf9CF59F9822e3C5De4950a41C49", + chain: Chain.mainnet, + decimals: 18, + quantityFormatter: "USD", +} + +export const RBPT: Token = { + symbol: "RBPT", + address: "0x021c343C6180f03cE9E48FaE3ff432309b9aF199", + chain: Chain.ropsten, + decimals: 18, + quantityFormatter: "USD", +} + export const tokens = [ MTA, PMTA, @@ -425,6 +441,8 @@ export const tokens = [ PWMATIC, RmUSD, RmBTC, + BPT, + RBPT, BAL, RBAL, ] diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index 44eeb0f9..ab533e11 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -196,7 +196,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { rewardToken: MTA.address, } - vault = (await deployVault(deployer, vaultData, chain)) as BoostedVault + vault = (await deployVault(deployer, vaultData)) as BoostedVault }) it("Distribute MTA rewards to vault", async () => { const distributionAmount = simpleToExactAmount(20000) diff --git a/test-fork/governance/staked-token-deploy.spec.ts b/test-fork/governance/staked-token-deploy.spec.ts new file mode 100644 index 00000000..27610372 --- /dev/null +++ b/test-fork/governance/staked-token-deploy.spec.ts @@ -0,0 +1,604 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address" +import { formatUnits } from "@ethersproject/units" +import { ONE_DAY, ONE_WEEK, ZERO_ADDRESS, DEAD_ADDRESS } from "@utils/constants" +import { assertBNClose, assertBNClosePercent } from "@utils/assertions" +import { impersonate } from "@utils/fork" +import { BN, simpleToExactAmount } from "@utils/math" +import { increaseTime, getTimestamp } from "@utils/time" +import { expect } from "chai" +import { Signer, utils } from "ethers" +import * as hre from "hardhat" +import { deployStakingToken, StakedTokenData, StakedTokenDeployAddresses } from "tasks/utils/rewardsUtils" +import { arrayify, formatBytes32String, solidityKeccak256 } from "ethers/lib/utils" +import { + IERC20, + IERC20__factory, + StakedTokenBPT, + StakedTokenMTA, + QuestManager, + SignatureVerifier, + PlatformTokenVendorFactory, + BoostDirectorV2, + BoostDirectorV2__factory, + PlatformTokenVendorFactory__factory, + SignatureVerifier__factory, + QuestManager__factory, + StakedTokenMTA__factory, + StakedTokenBPT__factory, + DelayedProxyAdmin__factory, + InstantProxyAdmin__factory, + DelayedProxyAdmin, + InstantProxyAdmin, + IMStableVoterProxy, + IMStableVoterProxy__factory, + IncentivisedVotingLockup__factory, + BoostedVault, + BoostedVault__factory, + StakedToken, +} from "types/generated" +import { RewardsDistributorEth__factory } from "types/generated/factories/RewardsDistributorEth__factory" +import { Account, QuestType, QuestStatus, BalConfig, UserStakingData } from "types" +import { getChain, getChainAddress, resolveAddress } from "../../tasks/utils/networkAddressFactory" + +const governorAddress = "0xF6FF1F7FCEB2cE6d26687EaaB5988b445d0b94a2" +const ethWhaleAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" +const mStableVoterProxy = "0x10d96b1fd46ce7ce092aa905274b8ed9d4585a6e" +const sharedBadgerGov = "0xca045cc466f14c33a516d98abcab5c55c2f5112c" +const deployerAddress = "0xb81473f20818225302b8fffb905b53d58a793d84" + +const staker1 = "0x19F12C947D25Ff8a3b748829D8001cA09a28D46d" +const staker2 = "0x0fc4b69958cb2fa320a96d54168b89953a953fbf" + +const vaultAddresses = [ + "0xAdeeDD3e5768F7882572Ad91065f93BA88343C99", + "0xF38522f63f40f9Dd81aBAfD2B8EFc2EC958a3016", + "0x78BefCa7de27d07DC6e71da295Cc2946681A6c7B", + "0x760ea8CfDcC4e78d8b9cA3088ECD460246DC0731", + "0xF65D53AA6e2E4A5f4F026e73cb3e22C22D75E35C", + "0x0997dDdc038c8A958a3A3d00425C16f8ECa87deb", + "0xD124B55f70D374F58455c8AEdf308E52Cf2A6207", +] + +interface StakedTokenDeployment { + stakedTokenBPT: StakedTokenBPT + stakedTokenMTA: StakedTokenMTA + questManager: QuestManager + signatureVerifier: SignatureVerifier + platformTokenVendorFactory: PlatformTokenVendorFactory + mta: IERC20 + bpt: IERC20 + boostDirector: BoostDirectorV2 + proxyAdmin: InstantProxyAdmin + delayedProxyAdmin: DelayedProxyAdmin +} + +// 1. Deploy core stkMTA, BPT variant & QuestManager +// 2. Gov TX's +// 1. Add StakingTokens to BoostDirector & QuestManager +// 2. Add Quest to QuestManager +// 3. Add small amt of rewards to get cogs turning +// 3. Vault contract upgrades +// 1. Upgrade +// 2. Verify balance retrieval and boosting (same on all accs) +// 4. Testing +// 1. Stake +// 2. Complete quests +// 3. Enter cooldown +// 4. Boost +// 5. Add rewards for pools +// 1. 32.5k for stkMTA, 20k for stkMBPT +// 6. Gov tx: Expire old Staking contract +context("StakedToken deployments and vault upgrades", () => { + let deployer: Signer + let governor: Signer + let ethWhale: Signer + let questSigner: SignerWithAddress + + const { network } = hre + + let deployedContracts: StakedTokenDeployment + + const snapConfig = async (stakedToken: StakedToken): Promise => { + const safetyData = await stakedToken.safetyData() + return { + name: await stakedToken.name(), + symbol: await stakedToken.symbol(), + decimals: await stakedToken.decimals(), + rewardsDistributor: await stakedToken.rewardsDistributor(), + nexus: await stakedToken.nexus(), + stakingToken: await stakedToken.STAKED_TOKEN(), + rewardToken: await stakedToken.REWARDS_TOKEN(), + cooldown: await stakedToken.COOLDOWN_SECONDS(), + unstake: await stakedToken.UNSTAKE_WINDOW(), + questManager: await stakedToken.questManager(), + hasPriceCoeff: await stakedToken.hasPriceCoeff(), + colRatio: safetyData.collateralisationRatio, + slashingPercentage: safetyData.slashingPercentage, + } + } + + const snapBalData = async (stakedTokenBpt: StakedTokenBPT): Promise => { + const balRecipient = await stakedTokenBpt.balRecipient() + const keeper = await stakedTokenBpt.keeper() + const pendingBPTFees = await stakedTokenBpt.pendingBPTFees() + const priceCoefficient = await stakedTokenBpt.priceCoefficient() + const lastPriceUpdateTime = await stakedTokenBpt.lastPriceUpdateTime() + return { + balRecipient, + keeper, + pendingBPTFees, + priceCoefficient, + lastPriceUpdateTime, + } + } + + const snapshotUserStakingData = async ( + stakedToken: StakedToken, + questManager: QuestManager, + rewardToken: IERC20, + user: string, + skipBalData = false, + ): Promise => { + const scaledBalance = await stakedToken.balanceOf(user) + const votes = await stakedToken.getVotes(user) + const earnedRewards = await stakedToken.earned(user) + const rewardTokenBalance = await rewardToken.balanceOf(user) + const rawBalance = await stakedToken.balanceData(user) + const userPriceCoeff = await stakedToken.userPriceCoeff(user) + const questBalance = await questManager.balanceData(user) + + return { + scaledBalance, + votes, + earnedRewards, + rewardTokenBalance, + rawBalance, + numCheckpoints: 0, + userPriceCoeff, + questBalance, + balData: skipBalData ? null : await snapBalData(stakedToken as StakedTokenBPT), + } + } + + before("reset block number", async () => { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.NODE_URL, + blockNumber: 13198333, + }, + }, + ], + }) + deployer = await impersonate(deployerAddress) + governor = await impersonate(governorAddress) + ethWhale = await impersonate(ethWhaleAddress) + + const { ethers } = hre + ;[questSigner] = await ethers.getSigners() + + // send some Ether to the impersonated multisig contract as it doesn't have Ether + await ethWhale.sendTransaction({ + to: governorAddress, + value: simpleToExactAmount(1), + }) + + await increaseTime(ONE_DAY.mul(6)) + }) + context("1. Deploying", () => { + it("deploys the contracts", async () => { + // Deploy StakedTokenMTA + const stakedTokenMTA = await deployStakingToken( + { + rewardsTokenSymbol: "MTA", + stakedTokenSymbol: "MTA", + cooldown: ONE_WEEK.mul(3).toNumber(), + unstakeWindow: ONE_WEEK.mul(2).toNumber(), + name: "StakedTokenMTA", + symbol: "stkMTA", + }, + { signer: deployer, address: deployerAddress }, + hre, + undefined, + questSigner.address, + ) + + // Deploy StakedTokenBPT + const stakedTokenBPT = await deployStakingToken( + { + rewardsTokenSymbol: "MTA", + stakedTokenSymbol: "BPT", + balTokenSymbol: "BAL", + cooldown: ONE_WEEK.mul(3).toNumber(), + unstakeWindow: ONE_WEEK.mul(2).toNumber(), + name: "StakedTokenBPT", + symbol: "stkBPT", + }, + { signer: deployer, address: deployerAddress }, + hre, + stakedTokenMTA, + questSigner.address, + ) + + deployedContracts = { + stakedTokenBPT: StakedTokenBPT__factory.connect(stakedTokenBPT.stakedToken, deployer), + stakedTokenMTA: StakedTokenMTA__factory.connect(stakedTokenMTA.stakedToken, deployer), + questManager: QuestManager__factory.connect(stakedTokenMTA.questManager, deployer), + signatureVerifier: SignatureVerifier__factory.connect(stakedTokenMTA.signatureVerifier, deployer), + platformTokenVendorFactory: PlatformTokenVendorFactory__factory.connect( + stakedTokenMTA.platformTokenVendorFactory, + deployer, + ), + mta: IERC20__factory.connect(resolveAddress("MTA", 0), deployer), + bpt: IERC20__factory.connect(resolveAddress("BPT", 0), deployer), + boostDirector: BoostDirectorV2__factory.connect(resolveAddress("BoostDirector", 0), governor), + proxyAdmin: InstantProxyAdmin__factory.connect(stakedTokenMTA.proxyAdminAddress, governor), + delayedProxyAdmin: DelayedProxyAdmin__factory.connect(resolveAddress("DelayedProxyAdmin", 0), governor), + } + }) + it("verifies stakedTokenMTA config", async () => { + const config = await snapConfig(deployedContracts.stakedTokenMTA) + expect(config.name).eq("StakedTokenMTA") + expect(config.symbol).eq("stkMTA") + expect(config.decimals).eq(18) + expect(config.rewardsDistributor).eq(resolveAddress("RewardsDistributor", 0)) + expect(config.nexus).eq(resolveAddress("Nexus", 0)) + expect(config.stakingToken).eq(resolveAddress("MTA", 0)) + expect(config.rewardToken).eq(resolveAddress("MTA", 0)) + expect(config.cooldown).eq(ONE_WEEK.mul(3)) + expect(config.unstake).eq(ONE_WEEK.mul(2)) + expect(config.questManager).eq(deployedContracts.questManager.address) + expect(config.hasPriceCoeff).eq(false) + expect(config.colRatio).eq(simpleToExactAmount(1)) + expect(config.slashingPercentage).eq(0) + }) + it("verifies stakedTokenBPT config", async () => { + const config = await snapConfig(deployedContracts.stakedTokenBPT) + expect(config.name).eq("StakedTokenBPT") + expect(config.symbol).eq("stkBPT") + expect(config.decimals).eq(18) + expect(config.rewardsDistributor).eq(resolveAddress("RewardsDistributor", 0)) + expect(config.nexus).eq(resolveAddress("Nexus", 0)) + expect(config.stakingToken).eq(resolveAddress("BPT", 0)) + expect(config.rewardToken).eq(resolveAddress("MTA", 0)) + expect(config.cooldown).eq(ONE_WEEK.mul(3)) + expect(config.unstake).eq(ONE_WEEK.mul(2)) + expect(config.questManager).eq(deployedContracts.questManager.address) + expect(config.hasPriceCoeff).eq(true) + expect(config.colRatio).eq(simpleToExactAmount(1)) + expect(config.slashingPercentage).eq(0) + const data = await snapBalData(deployedContracts.stakedTokenBPT) + expect(await deployedContracts.stakedTokenBPT.BAL()).eq(resolveAddress("BAL", 0)) + expect(await deployedContracts.stakedTokenBPT.balancerVault()).eq(resolveAddress("BalancerVault", 0)) + expect(await deployedContracts.stakedTokenBPT.poolId()).eq(resolveAddress("BalancerStakingPoolId", 0)) + expect(data.balRecipient).eq(resolveAddress("FundManager", 0)) + expect(data.keeper).eq(ZERO_ADDRESS) + expect(data.pendingBPTFees).eq(0) + expect(data.priceCoefficient).eq(42550) + expect(data.lastPriceUpdateTime).eq(0) + }) + it("verifies questManager config", async () => { + const seasonEpoch = await deployedContracts.questManager.seasonEpoch() + const startTime = await deployedContracts.questManager.startTime() + const questMaster = await deployedContracts.questManager.questMaster() + const nexus = await deployedContracts.questManager.nexus() + + expect(seasonEpoch).eq(0) + expect(startTime).gt(1631197683) + expect(questMaster).eq(resolveAddress("QuestMaster", 0)) + expect(nexus).eq(resolveAddress("Nexus", 0)) + }) + }) + context("2. Sending Gov Tx's", () => { + it("adds StakingTokens to BoostDirector and QuestManager", async () => { + // 1. BoostDirector + await deployedContracts.boostDirector.connect(governor).addStakedToken(deployedContracts.stakedTokenMTA.address) + await deployedContracts.boostDirector.connect(governor).addStakedToken(deployedContracts.stakedTokenBPT.address) + + // 2. QuestManager + await deployedContracts.questManager.connect(governor).addStakedToken(deployedContracts.stakedTokenMTA.address) + await deployedContracts.questManager.connect(governor).addStakedToken(deployedContracts.stakedTokenBPT.address) + }) + it("adds initial quest to QuestManager", async () => { + const currentTime = await getTimestamp() + await deployedContracts.questManager.connect(governor).addQuest(QuestType.PERMANENT, 10, currentTime.add(ONE_WEEK).add(2)) + await deployedContracts.questManager.connect(governor).addQuest(QuestType.SEASONAL, 25, currentTime.add(ONE_WEEK).add(2)) + }) + it("adds small amount of rewards to both reward contracts", async () => { + const fundManager = await impersonate(resolveAddress("FundManager", 0)) + const rewardsDistributor = RewardsDistributorEth__factory.connect(resolveAddress("RewardsDistributor", 0), fundManager) + await rewardsDistributor + .connect(fundManager) + .distributeRewards( + [deployedContracts.stakedTokenMTA.address, deployedContracts.stakedTokenBPT.address], + [simpleToExactAmount(1), simpleToExactAmount(1)], + ) + }) + it("whitelists Badger voterproxy", async () => { + await deployedContracts.stakedTokenMTA.connect(governor).whitelistWrapper(mStableVoterProxy) + }) + }) + context("3. Vault upgrades", () => { + it("should upgrade all vaults", async () => { + const proxyAdmin = await DelayedProxyAdmin__factory.connect(resolveAddress("DelayedProxyAdmin", 0), governor) + await Promise.all(vaultAddresses.map((v) => proxyAdmin.acceptUpgradeRequest(v))) + }) + it("should verify the vault upgrades have executed succesfully and all behaviour is in tact") + }) + + const signUserQuests = async (user: string, questIds: number[], signer: SignerWithAddress): Promise => { + const messageHash = solidityKeccak256(["address", "uint256[]"], [user, questIds]) + const signature = await signer.signMessage(arrayify(messageHash)) + return signature + } + // deployer transfers 50k MTA to Staker1 & 100k to Staker2 + // staker1 stakes in both + // staker2 stakes in MTA + context("4. Beta testing", () => { + let staker1signer: Signer + const staker1bpt = simpleToExactAmount(3000) + let staker2signer: Signer + it("tops up users with MTA", async () => { + await deployedContracts.mta.transfer(staker1, simpleToExactAmount(50000)) + await deployedContracts.mta.transfer(staker2, simpleToExactAmount(100000)) + + staker1signer = await impersonate(staker1) + staker2signer = await impersonate(staker2) + }) + it("allows basic staking on StakedTokenBPT", async () => { + await deployedContracts.bpt.connect(staker1signer).approve(deployedContracts.stakedTokenBPT.address, staker1bpt) + await deployedContracts.stakedTokenBPT + .connect(staker1signer) + ["stake(uint256,address)"](staker1bpt, resolveAddress("ProtocolDAO")) + }) + it("allows basic staking on StakedTokenMTA", async () => { + await deployedContracts.mta.connect(staker1signer).approve(deployedContracts.stakedTokenMTA.address, simpleToExactAmount(50000)) + await deployedContracts.stakedTokenMTA.connect(staker1signer)["stake(uint256)"](simpleToExactAmount(50000)) + + await deployedContracts.mta + .connect(staker2signer) + .approve(deployedContracts.stakedTokenMTA.address, simpleToExactAmount(100000)) + await deployedContracts.stakedTokenMTA.connect(staker2signer)["stake(uint256)"](simpleToExactAmount(100000)) + }) + it("allows fetching and setting of the priceCoefficinet", async () => { + const priceCoeff = await deployedContracts.stakedTokenBPT.getProspectivePriceCoefficient() + assertBNClose(priceCoeff, BN.from(42000), 1000) + await expect(deployedContracts.stakedTokenBPT.connect(governor).fetchPriceCoefficient()).to.be.revertedWith("Must be > 5% diff") + }) + it("should allow users to complete quests", async () => { + const balBefore = await snapshotUserStakingData( + deployedContracts.stakedTokenMTA, + deployedContracts.questManager, + deployedContracts.mta, + staker1, + true, + ) + const signature = await signUserQuests(staker1, [0], questSigner) + await deployedContracts.questManager.completeUserQuests(staker1, [0], signature) + + const balAfter = await snapshotUserStakingData( + deployedContracts.stakedTokenMTA, + deployedContracts.questManager, + deployedContracts.mta, + staker1, + true, + ) + expect(balAfter.questBalance.permMultiplier).eq(10) + expect(balAfter.questBalance.lastAction).eq(0) + expect(balAfter.earnedRewards).gt(0) + expect(balAfter.scaledBalance).eq(balBefore.scaledBalance.mul(110).div(100)) + expect(balAfter.votes).eq(balAfter.scaledBalance) + expect(await deployedContracts.questManager.hasCompleted(staker1, 0)).eq(true) + }) + const calcBoost = (raw: BN, vMTA: BN, priceCoefficient = simpleToExactAmount(1)): BN => { + const maxVMTA = simpleToExactAmount(600000, 18) + const maxBoost = simpleToExactAmount(3, 18) + const minBoost = simpleToExactAmount(1, 18) + const floor = simpleToExactAmount(98, 16) + const coeff = BN.from(9) + // min(m, max(d, (d * 0.95) + c * min(vMTA, f) / USD^b)) + const scaledBalance = raw.mul(priceCoefficient).div(simpleToExactAmount(1, 18)) + + if (scaledBalance.lt(simpleToExactAmount(1, 18))) return simpleToExactAmount(1) + + let denom = parseFloat(utils.formatUnits(scaledBalance)) + denom **= 0.75 + const flooredMTA = vMTA.gt(maxVMTA) ? maxVMTA : vMTA + let rhs = floor.add(flooredMTA.mul(coeff).div(10).mul(simpleToExactAmount(1)).div(simpleToExactAmount(denom))) + rhs = rhs.gt(minBoost) ? rhs : minBoost + return rhs.gt(maxBoost) ? maxBoost : rhs + } + it("should fetch the correct balances from the BoostDirector", async () => { + // staker 1 just call staticBalance on the boost director + await deployedContracts.boostDirector.whitelistVaults([deployerAddress]) + const bal1 = await deployedContracts.boostDirector.connect(deployer).callStatic.getBalance(staker1) + const staker1bal1 = await snapshotUserStakingData( + deployedContracts.stakedTokenMTA, + deployedContracts.questManager, + deployedContracts.mta, + staker1, + true, + ) + const staker1bal2 = await snapshotUserStakingData( + deployedContracts.stakedTokenBPT, + deployedContracts.questManager, + deployedContracts.mta, + staker1, + true, + ) + expect(bal1).eq(staker1bal1.scaledBalance.add(staker1bal2.scaledBalance).div(12)) + + // staker 2 poke boost on the gusd fPool and check the multiplier + const gusdPool = BoostedVault__factory.connect("0xAdeeDD3e5768F7882572Ad91065f93BA88343C99", staker2signer) + const boost2 = await gusdPool.getBoost(staker2) + const rawBal2 = await gusdPool.rawBalanceOf(staker2) + await gusdPool.pokeBoost(staker2) + const boost2after = await gusdPool.getBoost(staker2) + expect(boost2after).not.eq(boost2) + assertBNClosePercent(boost2after, calcBoost(rawBal2, simpleToExactAmount(100000).div(12)), "0.001") + + // staker 3 (no stake) poke boost and see it go to 0 multiplier + const btcPool = BoostedVault__factory.connect("0xF38522f63f40f9Dd81aBAfD2B8EFc2EC958a3016", staker2signer) + const boost3 = await btcPool.getBoost("0x25953c127efd1e15f4d2be82b753d49b12d626d7") + await btcPool.pokeBoost("0x25953c127efd1e15f4d2be82b753d49b12d626d7") + const boost3after = await btcPool.getBoost("0x25953c127efd1e15f4d2be82b753d49b12d626d7") + expect(boost3).gt(simpleToExactAmount(2)) + expect(boost3after).eq(simpleToExactAmount(1)) + }) + // staker 1 withdraws from BPT + // staker 2 withdraws from MTA + it("should allow users to enter cooldown and withdraw", async () => { + const staker1balbefore = await snapshotUserStakingData( + deployedContracts.stakedTokenBPT, + deployedContracts.questManager, + deployedContracts.mta, + staker1, + false, + ) + const staker2balbefore = await snapshotUserStakingData( + deployedContracts.stakedTokenMTA, + deployedContracts.questManager, + deployedContracts.mta, + staker2, + true, + ) + await deployedContracts.stakedTokenBPT.connect(staker1signer).startCooldown(staker1bpt) + await deployedContracts.stakedTokenMTA.connect(staker2signer).startCooldown(simpleToExactAmount(50000)) + + const staker1balmid = await snapshotUserStakingData( + deployedContracts.stakedTokenBPT, + deployedContracts.questManager, + deployedContracts.mta, + staker1, + false, + ) + const staker2balmid = await snapshotUserStakingData( + deployedContracts.stakedTokenMTA, + deployedContracts.questManager, + deployedContracts.mta, + staker2, + true, + ) + + expect(staker1balmid.scaledBalance).eq(0) + expect(staker1balmid.rawBalance.raw).eq(0) + expect(staker1balmid.rawBalance.cooldownUnits).eq(staker1bpt) + + expect(staker2balmid.scaledBalance).eq(staker2balbefore.scaledBalance.div(2)) + expect(staker2balmid.rawBalance.raw).eq(simpleToExactAmount(50000)) + expect(staker2balmid.rawBalance.cooldownUnits).eq(simpleToExactAmount(50000)) + + await increaseTime(ONE_WEEK.mul(3).add(1)) + + await deployedContracts.stakedTokenBPT.connect(staker1signer).withdraw(staker1bpt, staker1, true, true) + await deployedContracts.stakedTokenMTA.connect(staker2signer).withdraw(simpleToExactAmount(40000), staker2, false, true) + const staker1balend = await snapshotUserStakingData( + deployedContracts.stakedTokenBPT, + deployedContracts.questManager, + deployedContracts.mta, + staker1, + false, + ) + const staker2balend = await snapshotUserStakingData( + deployedContracts.stakedTokenMTA, + deployedContracts.questManager, + deployedContracts.mta, + staker2, + true, + ) + + expect(staker1balend.scaledBalance).eq(0) + expect(staker1balend.rawBalance.raw).eq(0) + expect(staker1balend.rawBalance.cooldownUnits).eq(0) + + assertBNClosePercent(staker2balend.scaledBalance, BN.from("57000009920800000000000"), "0.001") + assertBNClosePercent(staker2balend.rawBalance.raw, BN.from("57000009920800000000000"), "0.001") + expect(staker2balend.rawBalance.cooldownUnits).eq(0) + expect(staker2balend.rawBalance.cooldownTimestamp).eq(0) + }) + it("should allow recycling of BPT redemption fees", async () => { + const fees = await deployedContracts.stakedTokenBPT.pendingBPTFees() + expect(fees).gt(simpleToExactAmount(150)) + + await deployedContracts.stakedTokenBPT.connect(governor).convertFees() + + expect(await deployedContracts.stakedTokenBPT.pendingAdditionalReward()).gt(600) + + const priceCoeff = await deployedContracts.stakedTokenBPT.getProspectivePriceCoefficient() + console.log(priceCoeff.toString()) + expect(priceCoeff).lt(await deployedContracts.stakedTokenBPT.priceCoefficient()) + }) + it("should allow immediate upgrades of staking tokens", async () => { + // - get impl addr from ProxyAdmin and check (this verifies that its owned by ProxyAdmin) + expect(await deployedContracts.proxyAdmin.getProxyAdmin(deployedContracts.stakedTokenMTA.address)).eq( + deployedContracts.proxyAdmin.address, + ) + expect(await deployedContracts.proxyAdmin.getProxyAdmin(deployedContracts.stakedTokenBPT.address)).eq( + deployedContracts.proxyAdmin.address, + ) + // - Propose it again through the ProxyAdmin + await deployedContracts.proxyAdmin.changeProxyAdmin(deployedContracts.stakedTokenMTA.address, DEAD_ADDRESS) + }) + it("should allow proposal of upgrades for questManager", async () => { + // - get impl addr from DelayedProxyAdmin and check (this verifies that its owned by DelayedProxyAdmin) + expect(await deployedContracts.delayedProxyAdmin.getProxyAdmin(deployedContracts.questManager.address)).eq( + deployedContracts.delayedProxyAdmin.address, + ) + // - Propose it again through the DelayedProxyAdmin + await deployedContracts.delayedProxyAdmin + .connect(governor) + .proposeUpgrade(deployedContracts.questManager.address, DEAD_ADDRESS, "0x") + }) + }) + context("5. Finalise", () => { + it("should add all launch rewards", async () => { + // - Add the rewards (32.5k, 20k) to each stakedtoken + const fundManager = await impersonate(resolveAddress("FundManager", 0)) + const rewardsDistributor = RewardsDistributorEth__factory.connect(resolveAddress("RewardsDistributor", 0), fundManager) + await rewardsDistributor + .connect(fundManager) + .distributeRewards( + [deployedContracts.stakedTokenMTA.address, deployedContracts.stakedTokenBPT.address], + [simpleToExactAmount(32500), simpleToExactAmount(20000)], + ) + }) + it("should expire the old staking contract", async () => { + // - Expire old staking contract + const votingLockup = IncentivisedVotingLockup__factory.connect(resolveAddress("IncentivisedVotingLockup", 0), governor) + await votingLockup.expireContract() + // - Check that it's possible to exit for all users + expect(await votingLockup.expired()).eq(true) + + const activeUser = await impersonate("0xd4e692eb01861f2bc0534b9a1afd840719648c49") + await votingLockup.connect(activeUser).exit() + }) + }) + context("6. Test Badger migration", () => { + it("should allow badger to stake in new contract", async () => { + const badgerGovSigner = await impersonate(sharedBadgerGov) + const voterProxy = IMStableVoterProxy__factory.connect(mStableVoterProxy, badgerGovSigner) + // 1. it should fail to change addr unless exited - this can be skipped as bias is now 0 + // await expect(voterProxy.changeLockAddress(deployedContracts.stakedTokenMTA.address)).to.be.revertedWith("Active lockup") + // 2. Exit from old (exit) + await voterProxy.connect(governor).exitLock() + // 3. Update address () + await voterProxy.changeLockAddress(deployedContracts.stakedTokenMTA.address) + // 4. fail when calling harvestMta or increaseLockAmount/length + await expect(voterProxy.connect(governor).harvestMta()).to.be.revertedWith("Nothing to increase") + await expect(voterProxy.extendLock(BN.from(5000000))).to.be.reverted + // 5. call createLock + await voterProxy.createLock(BN.from(5000000)) + // 6. Check output + const userData = await snapshotUserStakingData( + deployedContracts.stakedTokenMTA, + deployedContracts.questManager, + deployedContracts.mta, + voterProxy.address, + true, + ) + expect(userData.rawBalance.raw).gt(simpleToExactAmount(500000)) + }) + }) +}) diff --git a/test-utils/machines/standardAccounts.ts b/test-utils/machines/standardAccounts.ts index ec565048..5f392091 100644 --- a/test-utils/machines/standardAccounts.ts +++ b/test-utils/machines/standardAccounts.ts @@ -24,6 +24,12 @@ export class StandardAccounts { public dummy4: Account + public dummy5: Account + + public dummy6: Account + + public dummy7: Account + public fundManager: Account public fundManager2: Account @@ -57,6 +63,9 @@ export class StandardAccounts { this.dummy2, this.dummy3, this.dummy4, + this.dummy5, + this.dummy6, + this.dummy7, this.fundManager, this.fundManager2, this.questMaster, diff --git a/test/governance/staking/boost-director-v2.spec.ts b/test/governance/staking/boost-director-v2.spec.ts new file mode 100644 index 00000000..730ce6c7 --- /dev/null +++ b/test/governance/staking/boost-director-v2.spec.ts @@ -0,0 +1,487 @@ +/* eslint-disable no-underscore-dangle */ + +import { ethers } from "hardhat" +import { expect } from "chai" +import { BN } from "@utils/math" +import { StandardAccounts, MassetMachine } from "@utils/machines" +import { DEAD_ADDRESS, ZERO_ADDRESS } from "@utils/constants" +import { + MockStakingContract, + MockStakingContract__factory, + MockNexus, + MockNexus__factory, + MockBoostedVault, + MockBoostedVault__factory, + BoostDirectorV2__factory, + BoostDirectorV2, +} from "types/generated" +import { Account } from "types" +import { Contract } from "@ethersproject/contracts" + +const vaultNumbers = [...Array(7).keys()] + +context("Govern boost director v2", () => { + let sa: StandardAccounts + let mAssetMachine: MassetMachine + + let nexus: MockNexus + let stakingContract: MockStakingContract + let boostDirector: BoostDirectorV2 + + let vaults: Account[] + let vaultUnlisted: Account + let user1NoStake: Account + let user2Staked: Account + let user3Staked: Account + + const user2StakedBalance = BN.from(20000).div(12) + const user3StakedBalance = BN.from(30000).div(12) + + before(async () => { + const accounts = await ethers.getSigners() + mAssetMachine = await new MassetMachine().initAccounts(accounts) + sa = mAssetMachine.sa + + vaults = [sa.dummy1, sa.dummy2, sa.dummy3, sa.dummy4, sa.dummy5, sa.dummy6, sa.dummy7] + vaultUnlisted = sa.all[11] + user1NoStake = sa.all[12] + user2Staked = sa.all[13] + user3Staked = sa.all[14] + + nexus = await new MockNexus__factory(sa.default.signer).deploy(sa.governor.address, DEAD_ADDRESS, DEAD_ADDRESS) + stakingContract = await new MockStakingContract__factory(sa.default.signer).deploy() + await stakingContract.setBalanceOf(user2Staked.address, 20000) + await stakingContract.setBalanceOf(user3Staked.address, 30000) + }) + context("Whitelisting boost savings vaults", () => { + before(async () => { + boostDirector = await new BoostDirectorV2__factory(sa.default.signer).deploy(nexus.address) + await boostDirector.connect(sa.governor.signer).addStakedToken(stakingContract.address) + await boostDirector.initialize([vaults[0].address]) + }) + it("should get first vault", async () => { + expect(await boostDirector._vaults(vaults[0].address)).to.eq(1) + }) + it("should fail if not governor", async () => { + let tx = boostDirector.connect(sa.default.signer).whitelistVaults([vaults[1].address]) + await expect(tx).to.revertedWith("Only governor can execute") + tx = boostDirector.connect(sa.fundManager.signer).whitelistVaults([vaults[1].address]) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should succeed in whitelisting no boost savings vault", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([]) + await expect(tx).to.revertedWith("Must be at least one vault") + }) + it("should succeed in whitelisting one boost savings vault", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([vaults[1].address]) + await expect(tx).to.emit(boostDirector, "Whitelisted").withArgs(vaults[1].address, 2) + expect(await boostDirector._vaults(vaults[1].address)).to.eq(2) + }) + it("should fail if already whitelisted", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([vaults[1].address]) + await expect(tx).to.revertedWith("Vault already whitelisted") + }) + it("should succeed in whitelisting two boost savings vault", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([vaults[2].address, vaults[3].address]) + await expect(tx).to.emit(boostDirector, "Whitelisted").withArgs(vaults[2].address, 3) + await expect(tx).to.emit(boostDirector, "Whitelisted").withArgs(vaults[3].address, 4) + expect(await boostDirector._vaults(vaults[2].address)).to.eq(3) + expect(await boostDirector._vaults(vaults[3].address)).to.eq(4) + }) + }) + context("get boost balance", () => { + let boostDirectorVaults: BoostDirectorV2[] + before(async () => { + boostDirector = await new BoostDirectorV2__factory(sa.default.signer).deploy(nexus.address) + await boostDirector.connect(sa.governor.signer).addStakedToken(stakingContract.address) + const vaultAddresses = vaults.map((vault) => vault.address) + await boostDirector.initialize(vaultAddresses) + boostDirectorVaults = vaults.map((vault) => boostDirector.connect(vault.signer)) + }) + context("called from first vault", () => { + context("for user 1 with nothing staked", () => { + it("should get zero balance", async () => { + const bal = await boostDirectorVaults[0].callStatic.getBalance(user1NoStake.address) + expect(bal).to.eq(0) + }) + it("should add user to boost director", async () => { + const tx = boostDirectorVaults[0].getBalance(user1NoStake.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user1NoStake.address, vaults[0].address) + }) + it("should fail to add user to boost director again", async () => { + const tx = boostDirectorVaults[0].getBalance(user1NoStake.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("should get user zero balance after being added", async () => { + const bal = await boostDirectorVaults[0].callStatic.getBalance(user1NoStake.address) + expect(bal).to.eq(0) + }) + }) + context("for user 2 with 20,000 staked", () => { + it("should get user 2 balance", async () => { + const bal = await boostDirectorVaults[0].callStatic.getBalance(user2Staked.address) + expect(bal).to.eq(user2StakedBalance) + }) + it("should add user 2 to boost director", async () => { + const tx = boostDirectorVaults[0].getBalance(user2Staked.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user2Staked.address, vaults[0].address) + }) + it("should fail to add user to boost director again", async () => { + const tx = boostDirectorVaults[0].getBalance(user2Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("should get user 2 balance after being added", async () => { + const bal = await boostDirectorVaults[0].callStatic.getBalance(user2Staked.address) + expect(bal).to.eq(user2StakedBalance) + }) + }) + }) + context("user 3 with 30,000 staked added to 6 vaults but not the 7th", () => { + vaultNumbers.forEach((i) => { + if (i >= 6) return + it(`vault ${i + 1} should get user balance before being added to any vaults`, async () => { + const bal = await boostDirectorVaults[i].callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(user3StakedBalance) + }) + it(`vault ${i + 1} should add user to boost director`, async () => { + const tx = boostDirectorVaults[i].getBalance(user3Staked.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user3Staked.address, vaults[i].address) + }) + it(`vault ${i + 1} should still user balance`, async () => { + const bal = await boostDirectorVaults[i].callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(user3StakedBalance) + }) + }) + it("7th vault should fail to add user as its the fourth", async () => { + const tx = boostDirectorVaults[6].getBalance(user3Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("7th vault should get zero balance for the user", async () => { + const bal = await boostDirectorVaults[6].callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(0) + }) + }) + context("adding non whitelisted vaults", () => { + it("should fail to add user from unlisted vault", async () => { + const tx = boostDirector.connect(vaultUnlisted.signer).getBalance(user2Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("should get zero balance for unlisted vault", async () => { + const bal = await boostDirector.connect(vaultUnlisted.signer).callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(0) + }) + it("should fail for user to add themselves as a vault", async () => { + const tx = boostDirector.connect(user2Staked.signer).getBalance(user2Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + }) + }) + context("redirect staked rewards to new boost savings vault", () => { + let mockedVaults: MockBoostedVault[] + before(async () => { + boostDirector = await new BoostDirectorV2__factory(sa.default.signer).deploy(nexus.address) + await boostDirector.connect(sa.governor.signer).addStakedToken(stakingContract.address) + + const mockedVaultsPromises = [...Array(8).keys()].map(() => + new MockBoostedVault__factory(sa.default.signer).deploy(boostDirector.address), + ) + mockedVaults = await Promise.all(mockedVaultsPromises) + const mockedVaultAddresses = mockedVaults.map((vault) => vault.address) + await boostDirector.initialize(mockedVaultAddresses) + + // For user 1, add the first three vaults to the Boost Director. + await mockedVaults[0].testGetBalance(user1NoStake.address) + await mockedVaults[1].testGetBalance(user1NoStake.address) + await mockedVaults[2].testGetBalance(user1NoStake.address) + await mockedVaults[3].testGetBalance(user1NoStake.address) + await mockedVaults[4].testGetBalance(user1NoStake.address) + await mockedVaults[5].testGetBalance(user1NoStake.address) + // For user 2, add the first two vaults to the Boost Director. + await mockedVaults[0].testGetBalance(user2Staked.address) + await mockedVaults[1].testGetBalance(user2Staked.address) + await mockedVaults[2].testGetBalance(user2Staked.address) + await mockedVaults[3].testGetBalance(user2Staked.address) + await mockedVaults[4].testGetBalance(user2Staked.address) + await mockedVaults[5].testGetBalance(user2Staked.address) + // For user 3, just add the first vault + await mockedVaults[0].testGetBalance(user3Staked.address) + }) + it("should get initial balancers", async () => { + expect(await mockedVaults[0].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + expect(await mockedVaults[1].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + expect(await mockedVaults[2].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + expect(await mockedVaults[3].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + expect(await mockedVaults[4].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + expect(await mockedVaults[5].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + expect(await mockedVaults[6].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + expect(await mockedVaults[7].callStatic.testGetBalance(user1NoStake.address)).to.eq(0) + + expect(await mockedVaults[0].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[1].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[2].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[7].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + + // This will always return the staked balance as the changes are not persisted + expect(await mockedVaults[0].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + expect(await mockedVaults[1].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + expect(await mockedVaults[2].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + expect(await mockedVaults[7].callStatic.testGetBalance(user3Staked.address)).to.eq(user3StakedBalance) + }) + it("should fail as old vault is not whitelisted", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(sa.dummy1.address, mockedVaults[3].address, false) + await expect(tx).to.revertedWith("Vaults not whitelisted") + }) + it("should fail as user 1 has not been added to the old vault 7", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[6].address, mockedVaults[3].address, false) + await expect(tx).to.revertedWith("No need to replace old") + }) + it("should fail as new vault is not whitelisted", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[0].address, sa.dummy1.address, false) + await expect(tx).to.revertedWith("Vaults not whitelisted") + }) + it("user 1 should succeed in replacing vault 1 with vault 7 that is not poked", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[0].address, mockedVaults[6].address, false) + await expect(tx).to.emit(mockedVaults[0], "Poked").withArgs(user1NoStake.address) + await expect(tx).to.not.emit(mockedVaults[6], "Poked") + await expect(tx) + .to.emit(boostDirector, "RedirectedBoost") + .withArgs(user1NoStake.address, mockedVaults[6].address, mockedVaults[0].address) + }) + it("user 1 should succeed in replacing vault 2 vault 8 that is poked", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[1].address, mockedVaults[7].address, true) + await expect(tx).to.emit(mockedVaults[1], "Poked").withArgs(user1NoStake.address) + await expect(tx).to.emit(mockedVaults[7], "Poked").withArgs(user1NoStake.address) + await expect(tx) + .to.emit(boostDirector, "RedirectedBoost") + .withArgs(user1NoStake.address, mockedVaults[7].address, mockedVaults[1].address) + }) + it("user 2 should succeed in replacing vault 1 with vault 7 that is not poked", async () => { + expect(await mockedVaults[0].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[1].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[2].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[7].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + + const tx = boostDirector.connect(user2Staked.signer).setDirection(mockedVaults[0].address, mockedVaults[6].address, false) + await expect(tx).to.emit(mockedVaults[0], "Poked").withArgs(user2Staked.address) + await expect(tx).to.not.emit(mockedVaults[6], "Poked") + await expect(tx) + .to.emit(boostDirector, "RedirectedBoost") + .withArgs(user2Staked.address, mockedVaults[6].address, mockedVaults[0].address) + + expect(await mockedVaults[0].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[1].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[2].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[7].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + }) + it("user 2 should succeed in replacing vault 2 vault 8 that is poked", async () => { + expect(await mockedVaults[0].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[1].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[2].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[7].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + + const tx = boostDirector.connect(user2Staked.signer).setDirection(mockedVaults[1].address, mockedVaults[7].address, true) + await expect(tx).to.emit(mockedVaults[1], "Poked").withArgs(user2Staked.address) + await expect(tx).to.emit(mockedVaults[7], "Poked").withArgs(user2Staked.address) + await expect(tx) + .to.emit(boostDirector, "RedirectedBoost") + .withArgs(user2Staked.address, mockedVaults[7].address, mockedVaults[1].address) + + expect(await mockedVaults[0].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[1].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[2].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[7].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + }) + it("user 2 should succeed in replacing vault 8 back to vault 2 that is poked", async () => { + expect(await mockedVaults[0].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[1].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[2].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[7].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + + const tx = boostDirector.connect(user2Staked.signer).setDirection(mockedVaults[7].address, mockedVaults[1].address, true) + await expect(tx).to.emit(mockedVaults[7], "Poked").withArgs(user2Staked.address) + await expect(tx).to.emit(mockedVaults[1], "Poked").withArgs(user2Staked.address) + await expect(tx) + .to.emit(boostDirector, "RedirectedBoost") + .withArgs(user2Staked.address, mockedVaults[1].address, mockedVaults[7].address) + + expect(await mockedVaults[0].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + expect(await mockedVaults[1].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[2].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[3].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[4].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[5].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[6].callStatic.testGetBalance(user2Staked.address)).to.eq(user2StakedBalance) + expect(await mockedVaults[7].callStatic.testGetBalance(user2Staked.address)).to.eq(0) + }) + it("should fail as user 3 only has 1 vault", async () => { + const tx = boostDirector.connect(user3Staked.signer).setDirection(mockedVaults[0].address, mockedVaults[3].address, false) + await expect(tx).to.revertedWith("No need to replace old") + }) + }) + context("set balance divisor", () => { + let boostDirectorVault1: BoostDirectorV2 + before(async () => { + boostDirector = await new BoostDirectorV2__factory(sa.default.signer).deploy(nexus.address) + await boostDirector.connect(sa.governor.signer).addStakedToken(stakingContract.address) + await boostDirector.initialize([vaults[0].address]) + + boostDirectorVault1 = boostDirector.connect(vaults[0].signer) + await boostDirectorVault1.getBalance(user2Staked.address) + await boostDirectorVault1.getBalance(user3Staked.address) + }) + it("should fail to set balance divisor not governor", async () => { + const tx = boostDirector.connect(sa.default.signer).setBalanceDivisor(6) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should fail to set balance divisor to same value", async () => { + const tx = boostDirector.connect(sa.governor.signer).setBalanceDivisor(12) + await expect(tx).to.revertedWith("No change in divisor") + }) + it("should fail to set balance divisor to 15", async () => { + const tx = boostDirector.connect(sa.governor.signer).setBalanceDivisor(15) + await expect(tx).to.revertedWith("Divisor too large") + }) + it("should change balance divisor from 12 to 5 by governor", async () => { + expect(await boostDirectorVault1.callStatic.getBalance(user1NoStake.address), "user1 bal before").to.eq(0) + expect(await boostDirectorVault1.callStatic.getBalance(user2Staked.address), "user2 bal before").to.eq(BN.from(20000).div(12)) + expect(await boostDirectorVault1.callStatic.getBalance(user3Staked.address), "user3 bal before").to.eq(BN.from(30000).div(12)) + + const tx = await boostDirector.connect(sa.governor.signer).setBalanceDivisor(5) + await expect(tx).to.emit(boostDirector, "BalanceDivisorChanged").withArgs(5) + + expect(await boostDirectorVault1.callStatic.getBalance(user1NoStake.address), "user1 bal after").to.eq(0) + expect(await boostDirectorVault1.callStatic.getBalance(user2Staked.address), "user2 bal after").to.eq(BN.from(20000).div(5)) + expect(await boostDirectorVault1.callStatic.getBalance(user3Staked.address), "user3 bal after").to.eq(BN.from(30000).div(5)) + }) + }) + context("add staking tokens", () => { + let boostDirectorVault1: BoostDirectorV2 + let newStakingContract: Contract + before(async () => { + boostDirector = await new BoostDirectorV2__factory(sa.default.signer).deploy(nexus.address) + await boostDirector.connect(sa.governor.signer).addStakedToken(stakingContract.address) + await boostDirector.initialize([vaults[0].address]) + + boostDirectorVault1 = boostDirector.connect(vaults[0].signer) + await boostDirectorVault1.getBalance(user2Staked.address) + await boostDirectorVault1.getBalance(user3Staked.address) + + newStakingContract = await new MockStakingContract__factory(sa.default.signer).deploy() + await newStakingContract.setBalanceOf(user2Staked.address, 50000) + }) + it("should fail to add staking token when not governor", async () => { + const tx = boostDirector.connect(sa.default.signer).addStakedToken(newStakingContract.address) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should add new staking token by governor", async () => { + const tx = await boostDirector.connect(sa.governor.signer).addStakedToken(newStakingContract.address) + await expect(tx).to.emit(boostDirector, "StakedTokenAdded").withArgs(newStakingContract.address) + + expect(await boostDirector.stakedTokenContracts(0)).to.eq(stakingContract.address) + expect(await boostDirector.stakedTokenContracts(1)).to.eq(newStakingContract.address) + + expect(await boostDirectorVault1.callStatic.getBalance(user2Staked.address), "user2 bal after").to.eq( + BN.from(50000 + 20000).div(12), + ) + }) + it("should fail to add duplicate staking token", async () => { + const tx = boostDirector.connect(sa.governor.signer).addStakedToken(newStakingContract.address) + await expect(tx).to.revertedWith("StakedToken already added") + }) + }) + context("remove staking tokens", () => { + let boostDirectorVault1: BoostDirectorV2 + let newStaking1: Contract + let newStaking2: Contract + beforeEach(async () => { + boostDirector = await new BoostDirectorV2__factory(sa.default.signer).deploy(nexus.address) + await boostDirector.connect(sa.governor.signer).addStakedToken(stakingContract.address) + await boostDirector.initialize([vaults[0].address]) + + boostDirectorVault1 = boostDirector.connect(vaults[0].signer) + await boostDirectorVault1.getBalance(user2Staked.address) + await boostDirectorVault1.getBalance(user3Staked.address) + + newStaking1 = await new MockStakingContract__factory(sa.default.signer).deploy() + await boostDirector.connect(sa.governor.signer).addStakedToken(newStaking1.address) + await newStaking1.setBalanceOf(user2Staked.address, 50000) + + newStaking2 = await new MockStakingContract__factory(sa.default.signer).deploy() + await boostDirector.connect(sa.governor.signer).addStakedToken(newStaking2.address) + await newStaking2.setBalanceOf(user2Staked.address, 120) + + expect(await boostDirector.stakedTokenContracts(0)).to.eq(stakingContract.address) + expect(await boostDirector.stakedTokenContracts(1)).to.eq(newStaking1.address) + expect(await boostDirector.stakedTokenContracts(2)).to.eq(newStaking2.address) + + expect(await boostDirectorVault1.callStatic.getBalance(user2Staked.address), "user2 bal before").to.eq( + BN.from(20000 + 50000 + 120).div(12), + ) + }) + it("should fail to remove staking token when not governor", async () => { + const tx = boostDirector.connect(sa.default.signer).removeStakedToken(newStaking1.address) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should remove first staking token by governor", async () => { + const tx = await boostDirector.connect(sa.governor.signer).removeStakedToken(stakingContract.address) + await expect(tx).to.emit(boostDirector, "StakedTokenRemoved").withArgs(stakingContract.address) + + expect(await boostDirector.stakedTokenContracts(0), "first contract").to.eq(newStaking2.address) + expect(await boostDirector.stakedTokenContracts(1), "second contract").to.eq(newStaking1.address) + + expect(await boostDirectorVault1.callStatic.getBalance(user2Staked.address), "user2 bal after").to.eq( + BN.from(50000 + 120).div(12), + ) + }) + it("should remove middle staking token by governor", async () => { + const tx = await boostDirector.connect(sa.governor.signer).removeStakedToken(newStaking1.address) + await expect(tx).to.emit(boostDirector, "StakedTokenRemoved").withArgs(newStaking1.address) + + expect(await boostDirector.stakedTokenContracts(0), "first contract").to.eq(stakingContract.address) + expect(await boostDirector.stakedTokenContracts(1), "second contract").to.eq(newStaking2.address) + + expect(await boostDirectorVault1.callStatic.getBalance(user2Staked.address), "user2 bal after").to.eq( + BN.from(20000 + 120).div(12), + ) + }) + it("should remove last staking token by governor", async () => { + const tx = await boostDirector.connect(sa.governor.signer).removeStakedToken(newStaking2.address) + await expect(tx).to.emit(boostDirector, "StakedTokenRemoved").withArgs(newStaking2.address) + + expect(await boostDirector.stakedTokenContracts(0), "first contract").to.eq(stakingContract.address) + expect(await boostDirector.stakedTokenContracts(1), "second contract").to.eq(newStaking1.address) + + expect(await boostDirectorVault1.callStatic.getBalance(user2Staked.address), "user2 bal after").to.eq( + BN.from(20000 + 50000).div(12), + ) + }) + }) +}) diff --git a/test/governance/staking/signature-verifier.spec.ts b/test/governance/staking/signature-verifier.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/test/governance/staking/staked-token-bpt.spec.ts b/test/governance/staking/staked-token-bpt.spec.ts index 244d3893..39e0550d 100644 --- a/test/governance/staking/staked-token-bpt.spec.ts +++ b/test/governance/staking/staked-token-bpt.spec.ts @@ -16,26 +16,32 @@ import { MockStakedTokenWithPrice__factory, QuestManager, MockEmissionController__factory, + MockBPT, + MockBPT__factory, + MockBVault, + MockBVault__factory, + StakedTokenBPT__factory, + StakedTokenBPT, } from "types" import { assertBNClose, DEAD_ADDRESS } from "index" import { ONE_DAY, ONE_WEEK, ZERO_ADDRESS } from "@utils/constants" import { BN, simpleToExactAmount } from "@utils/math" import { expect } from "chai" import { getTimestamp, increaseTime } from "@utils/time" -import { arrayify, formatBytes32String, solidityKeccak256 } from "ethers/lib/utils" -import { BigNumberish, Signer } from "ethers" -import { QuestStatus, QuestType, UserStakingData } from "types/stakedToken" - -const signUserQuests = async (user: string, questIds: BigNumberish[], questSigner: Signer): Promise => { - const messageHash = solidityKeccak256(["address", "uint256[]"], [user, questIds]) - const signature = await questSigner.signMessage(arrayify(messageHash)) - return signature +import { formatBytes32String } from "ethers/lib/utils" +import { BalConfig, UserStakingData } from "types/stakedToken" + +interface Deployment { + stakedToken: StakedTokenBPT + questManager: QuestManager + bpt: BPTDeployment } -const signQuestUsers = async (questId: BigNumberish, users: string[], questSigner: Signer): Promise => { - const messageHash = solidityKeccak256(["uint256", "address[]"], [questId, users]) - const signature = await questSigner.signMessage(arrayify(messageHash)) - return signature +interface BPTDeployment { + vault: MockBVault + bpt: MockBPT + bal: MockERC20 + underlying: MockERC20[] } describe("Staked Token BPT", () => { @@ -44,23 +50,38 @@ describe("Staked Token BPT", () => { let nexus: MockNexus let rewardToken: MockERC20 - let stakedToken: MockStakedTokenWithPrice + let stakedToken: StakedTokenBPT let questManager: QuestManager + let bpt: BPTDeployment - const startingMintAmount = simpleToExactAmount(10000000) - - console.log(`Staked contract size ${MockStakedTokenWithPrice__factory.bytecode.length / 2} bytes`) + console.log(`Staked contract size ${StakedTokenBPT__factory.bytecode.length / 2} bytes`) - interface Deployment { - stakedToken: MockStakedTokenWithPrice - questManager: QuestManager + const deployBPT = async (mockMTA: MockERC20): Promise => { + const token2 = await new MockERC20__factory(sa.default.signer).deploy("Test Token 2", "TST2", 18, sa.default.address, 10000000) + const mockBal = await new MockERC20__factory(sa.default.signer).deploy("Mock BAL", "mkBAL", 18, sa.default.address, 10000000) + const bptLocal = await new MockBPT__factory(sa.default.signer).deploy("Balance Pool Token", "mBPT") + const vault = await new MockBVault__factory(sa.default.signer).deploy() + await mockMTA.approve(vault.address, simpleToExactAmount(100000)) + await token2.approve(vault.address, simpleToExactAmount(100000)) + await vault.addPool( + bptLocal.address, + [mockMTA.address, token2.address], + [simpleToExactAmount(3.28), simpleToExactAmount(0.0002693)], + ) + return { + vault, + bpt: bptLocal, + bal: mockBal, + underlying: [mockMTA, token2], + } } - const redeployStakedToken = async (): Promise => { + const redeployStakedToken = async (useFakePrice = false): Promise => { deployTime = await getTimestamp() nexus = await new MockNexus__factory(sa.default.signer).deploy(sa.governor.address, DEAD_ADDRESS, DEAD_ADDRESS) await nexus.setRecollateraliser(sa.mockRecollateraliser.address) rewardToken = await new MockERC20__factory(sa.default.signer).deploy("Reward", "RWD", 18, sa.default.address, 10000000) + const bptLocal = await deployBPT(rewardToken) const signatureVerifier = await new SignatureVerifier__factory(sa.default.signer).deploy() const questManagerLibraryAddresses = { @@ -74,25 +95,49 @@ describe("Staked Token BPT", () => { const stakedTokenLibraryAddresses = { "contracts/rewards/staking/PlatformTokenVendorFactory.sol:PlatformTokenVendorFactory": platformTokenVendorFactory.address, } - const stakedTokenFactory = new MockStakedTokenWithPrice__factory(stakedTokenLibraryAddresses, sa.default.signer) - const stakedTokenImpl = await stakedTokenFactory.deploy( - nexus.address, - rewardToken.address, - questManagerProxy.address, - rewardToken.address, - ONE_WEEK, - ONE_DAY.mul(2), - ) - data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ - formatBytes32String("Staked Rewards"), - formatBytes32String("stkRWD"), - sa.mockRewardsDistributor.address, - ]) - const stakedTokenProxy = await new AssetProxy__factory(sa.default.signer).deploy(stakedTokenImpl.address, DEAD_ADDRESS, data) - const sToken = stakedTokenFactory.attach(stakedTokenProxy.address) as MockStakedTokenWithPrice + let sToken + if (useFakePrice) { + const stakedTokenFactory = new MockStakedTokenWithPrice__factory(stakedTokenLibraryAddresses, sa.default.signer) + const stakedTokenImpl = await stakedTokenFactory.deploy( + nexus.address, + rewardToken.address, + questManagerProxy.address, + bptLocal.bpt.address, + ONE_WEEK, + ONE_DAY.mul(2), + ) + data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ + formatBytes32String("Staked Rewards"), + formatBytes32String("stkRWD"), + sa.mockRewardsDistributor.address, + ]) + const stakedTokenProxy = await new AssetProxy__factory(sa.default.signer).deploy(stakedTokenImpl.address, DEAD_ADDRESS, data) + sToken = stakedTokenFactory.attach(stakedTokenProxy.address) as any as StakedTokenBPT + } else { + const stakedTokenFactory = new StakedTokenBPT__factory(stakedTokenLibraryAddresses, sa.default.signer) + const stakedTokenImpl = await stakedTokenFactory.deploy( + nexus.address, + rewardToken.address, + questManagerProxy.address, + bptLocal.bpt.address, + ONE_WEEK, + ONE_DAY.mul(2), + [bptLocal.bal.address, bptLocal.vault.address], + await bptLocal.vault.poolIds(bptLocal.bpt.address), + ) + data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ + formatBytes32String("Staked Rewards"), + formatBytes32String("stkRWD"), + sa.mockRewardsDistributor.address, + sa.fundManager.address, + 44000, + ]) + const stakedTokenProxy = await new AssetProxy__factory(sa.default.signer).deploy(stakedTokenImpl.address, DEAD_ADDRESS, data) + sToken = stakedTokenFactory.attach(stakedTokenProxy.address) as StakedTokenBPT + } const qMaster = QuestManager__factory.connect(questManagerProxy.address, sa.default.signer) - await qMaster.connect(sa.governor.signer).addStakedToken(stakedTokenProxy.address) + await qMaster.connect(sa.governor.signer).addStakedToken(sToken.address) // Test: Add Emission Data const emissionController = await new MockEmissionController__factory(sa.default.signer).deploy() @@ -103,26 +148,45 @@ describe("Staked Token BPT", () => { return { stakedToken: sToken, questManager: qMaster, + bpt: bptLocal, } } - const snapshotUserStakingData = async (user = sa.default.address): Promise => { - const stakedBalance = await stakedToken.balanceOf(user) + const snapBalData = async (): Promise => { + const balRecipient = await stakedToken.balRecipient() + const keeper = await stakedToken.keeper() + const pendingBPTFees = await stakedToken.pendingBPTFees() + const priceCoefficient = await stakedToken.priceCoefficient() + const lastPriceUpdateTime = await stakedToken.lastPriceUpdateTime() + return { + balRecipient, + keeper, + pendingBPTFees, + priceCoefficient, + lastPriceUpdateTime, + } + } + + const snapshotUserStakingData = async (user = sa.default.address, skipBalData = false): Promise => { + const scaledBalance = await stakedToken.balanceOf(user) const votes = await stakedToken.getVotes(user) const earnedRewards = await stakedToken.earned(user) - const rewardsBalance = await rewardToken.balanceOf(user) - const userBalances = await stakedToken.balanceData(user) + const numCheckpoints = await stakedToken.numCheckpoints(user) + const rewardTokenBalance = await rewardToken.balanceOf(user) + const rawBalance = await stakedToken.balanceData(user) const userPriceCoeff = await stakedToken.userPriceCoeff(user) const questBalance = await questManager.balanceData(user) return { - stakedBalance, + scaledBalance, votes, earnedRewards, - rewardsBalance, - userBalances, + numCheckpoints, + rewardTokenBalance, + rawBalance, userPriceCoeff, questBalance, + balData: skipBalData ? null : await snapBalData(), } } @@ -138,10 +202,95 @@ describe("Staked Token BPT", () => { context("deploy and initialize", () => { before(async () => { - ;({ stakedToken, questManager } = await redeployStakedToken()) + ;({ stakedToken, questManager, bpt } = await redeployStakedToken()) }) it("post initialize", async () => { - expect(await stakedToken.priceCoefficient()).eq(10000) + const data = await snapBalData() + expect(await stakedToken.BAL()).eq(bpt.bal.address) + expect(await stakedToken.balancerVault()).eq(bpt.vault.address) + expect(await stakedToken.poolId()).eq(await bpt.vault.poolIds(bpt.bpt.address)) + expect(data.balRecipient).eq(sa.fundManager.address) + expect(data.keeper).eq(ZERO_ADDRESS) + expect(data.pendingBPTFees).eq(0) + expect(data.priceCoefficient).eq(44000) + expect(data.lastPriceUpdateTime).eq(0) + }) + }) + + // '''..................................................................''' + // '''................... BAL TOKENS ......................''' + // '''..................................................................''' + + context("claiming BAL rewards", () => { + const balAirdrop = simpleToExactAmount(100) + before(async () => { + ;({ stakedToken, questManager, bpt } = await redeployStakedToken()) + await bpt.bal.transfer(stakedToken.address, balAirdrop) + }) + it("should allow governor to set bal recipient", async () => { + await expect(stakedToken.setBalRecipient(sa.fundManager.address)).to.be.revertedWith("Only governor can execute") + const tx = stakedToken.connect(sa.governor.signer).setBalRecipient(sa.fundManager.address) + await expect(tx).to.emit(stakedToken, "BalRecipientChanged").withArgs(sa.fundManager.address) + expect(await stakedToken.balRecipient()).to.eq(sa.fundManager.address) + }) + it("should allow BAL tokens to be claimed", async () => { + const balBefore = await bpt.bal.balanceOf(sa.fundManager.address) + const tx = stakedToken.claimBal() + await expect(tx).to.emit(stakedToken, "BalClaimed") + const balAfter = await bpt.bal.balanceOf(sa.fundManager.address) + expect(balAfter.sub(balBefore)).eq(balAirdrop) + }) + }) + + // '''..................................................................''' + // '''........................ FEES ETC ..........................''' + // '''..................................................................''' + + context("collecting fees", () => { + const stakeAmount = simpleToExactAmount(100) + const expectedFees = stakeAmount.sub(stakeAmount.mul(1000).div(1075)) + let data: UserStakingData + let expectedMTA: BN + before(async () => { + ;({ stakedToken, questManager, bpt } = await redeployStakedToken()) + await bpt.bpt.approve(stakedToken.address, stakeAmount) + await stakedToken["stake(uint256)"](stakeAmount) + await stakedToken.startCooldown(stakeAmount) + await increaseTime(ONE_WEEK.add(1)) + await stakedToken.withdraw(stakeAmount, sa.default.address, true, true) + data = await snapshotUserStakingData() + expectedMTA = expectedFees.mul(data.balData.priceCoefficient).div(12000) + }) + it("should collect 7.5% as fees", async () => { + expect(await stakedToken.pendingAdditionalReward()).eq(0) + expect(data.balData.pendingBPTFees).eq(expectedFees) + }) + it("should convert fees back into $MTA", async () => { + const bptBalBefore = await bpt.bpt.balanceOf(stakedToken.address) + const mtaBalBefore = await rewardToken.balanceOf(stakedToken.address) + const tx = stakedToken.convertFees() + // it should emit the event + await expect(tx).to.emit(stakedToken, "FeesConverted") + const dataAfter = await snapshotUserStakingData() + // should reset the pendingFeesBPT var to 1 + expect(dataAfter.balData.pendingBPTFees).eq(1) + // should add the new fees to headlessstakingrewards + expect(await stakedToken.pendingAdditionalReward()).gt(expectedMTA) + + // should burn bpt and receive mta + const bptBalAfter = await bpt.bpt.balanceOf(stakedToken.address) + const mtaBalAfter = await rewardToken.balanceOf(stakedToken.address) + expect(mtaBalAfter.sub(mtaBalBefore)).gt(expectedMTA) + expect(mtaBalAfter).eq(await stakedToken.pendingAdditionalReward()) + expect(bptBalBefore.sub(bptBalAfter)).eq(expectedFees.sub(1)) + }) + it("should add the correct amount of fees, and deposit to the vendor when notifying", async () => { + await stakedToken.connect(sa.mockRewardsDistributor.signer).notifyRewardAmount(0) + expect(await rewardToken.balanceOf(stakedToken.address)).eq(1) + expect(await stakedToken.pendingAdditionalReward()).eq(1) + }) + it("should fail if there is nothing to collect", async () => { + await expect(stakedToken.convertFees()).to.be.revertedWith("Must have something to convert") }) }) @@ -149,50 +298,91 @@ describe("Staked Token BPT", () => { // '''................... PRICE COEFFICIENT ......................''' // '''..................................................................''' + context("setting keeper", () => { + before(async () => { + ;({ stakedToken, questManager, bpt } = await redeployStakedToken()) + }) + it("should allow governance to set keeper", async () => { + await expect(stakedToken.setKeeper(sa.default.address)).to.be.revertedWith("Only governor can execute") + const tx = stakedToken.connect(sa.governor.signer).setKeeper(sa.default.address) + await expect(tx).to.emit(stakedToken, "KeeperUpdated").withArgs(sa.default.address) + expect(await stakedToken.keeper()).to.eq(sa.default.address) + }) + }) + + context("fetching live priceCoeff", () => { + before(async () => { + ;({ stakedToken, questManager, bpt } = await redeployStakedToken()) + }) + it("should fail if not called by governor or keeper", async () => { + await expect(stakedToken.fetchPriceCoefficient()).to.be.revertedWith("Gov or keeper") + }) + it("should allow govenror or keeper to fetch new price Coeff", async () => { + const newPrice = await stakedToken.getProspectivePriceCoefficient() + expect(newPrice).gt(30000) + expect(newPrice).lt(55000) + const tx = stakedToken.connect(sa.governor.signer).fetchPriceCoefficient() + await expect(tx).to.emit(stakedToken, "PriceCoefficientUpdated").withArgs(newPrice) + const timeNow = await getTimestamp() + expect(await stakedToken.priceCoefficient()).eq(newPrice) + assertBNClose(await stakedToken.lastPriceUpdateTime(), timeNow, 3) + }) + it("should fail to set more than once per 14 days", async () => { + await expect(stakedToken.connect(sa.governor.signer).fetchPriceCoefficient()).to.be.revertedWith("Max 1 update per 14 days") + }) + it("should fail to set if the diff is < 5%", async () => { + await increaseTime(ONE_WEEK.mul(2).add(1)) + await expect(stakedToken.connect(sa.governor.signer).fetchPriceCoefficient()).to.be.revertedWith("Must be > 5% diff") + }) + it("should fail if its's out of bounds", async () => { + await bpt.vault.setUnitsPerBpt(bpt.bpt.address, [simpleToExactAmount(0.5), simpleToExactAmount(0.0002693)]) + let priceCoeff = await stakedToken.getProspectivePriceCoefficient() + expect(priceCoeff).eq(6250) + await expect(stakedToken.connect(sa.governor.signer).fetchPriceCoefficient()).to.be.revertedWith("Out of bounds") + + await bpt.vault.setUnitsPerBpt(bpt.bpt.address, [simpleToExactAmount(6.5), simpleToExactAmount(0.0002693)]) + priceCoeff = await stakedToken.getProspectivePriceCoefficient() + expect(priceCoeff).eq(81250) + await expect(stakedToken.connect(sa.governor.signer).fetchPriceCoefficient()).to.be.revertedWith("Out of bounds") + + await bpt.vault.setUnitsPerBpt(bpt.bpt.address, [simpleToExactAmount(4.2), simpleToExactAmount(0.0002693)]) + priceCoeff = await stakedToken.getProspectivePriceCoefficient() + expect(priceCoeff).eq(52500) + await stakedToken.connect(sa.governor.signer).fetchPriceCoefficient() + expect(await stakedToken.priceCoefficient()).eq(priceCoeff) + }) + }) + context("when a StakedToken has price coefficient", () => { const stakedAmount = simpleToExactAmount(1000) + let mockStakedToken: MockStakedTokenWithPrice before(async () => { - ;({ stakedToken, questManager } = await redeployStakedToken()) - await rewardToken.connect(sa.default.signer).approve(stakedToken.address, stakedAmount.mul(3)) + ;({ stakedToken, bpt, questManager } = await redeployStakedToken(true)) + mockStakedToken = stakedToken as any as MockStakedTokenWithPrice + await bpt.bpt.connect(sa.default.signer).approve(mockStakedToken.address, stakedAmount.mul(3)) }) it("should allow basic staking and save coeff to users acc", async () => { - await stakedToken["stake(uint256)"](stakedAmount) - const data = await snapshotUserStakingData(sa.default.address) + await mockStakedToken["stake(uint256)"](stakedAmount) + const data = await snapshotUserStakingData(sa.default.address, true) expect(data.userPriceCoeff).eq(10000) expect(data.votes).eq(stakedAmount) }) it("should allow setting of a new priceCoeff", async () => { - await stakedToken.setPriceCoefficient(15000) - expect(await stakedToken.priceCoefficient()).eq(15000) + await mockStakedToken.setPriceCoefficient(15000) + expect(await mockStakedToken.priceCoefficient()).eq(15000) }) it("should update the users balance when they claim rewards", async () => { - await stakedToken["claimReward()"]() - const data = await snapshotUserStakingData(sa.default.address) + await mockStakedToken["claimReward()"]() + const data = await snapshotUserStakingData(sa.default.address, true) expect(data.userPriceCoeff).eq(15000) expect(data.votes).eq(stakedAmount.mul(3).div(2)) }) it("should update the users balance when they stake more", async () => { - await stakedToken.setPriceCoefficient(10000) - await stakedToken["stake(uint256)"](stakedAmount) - const data = await snapshotUserStakingData(sa.default.address) + await mockStakedToken.setPriceCoefficient(10000) + await mockStakedToken["stake(uint256)"](stakedAmount) + const data = await snapshotUserStakingData(sa.default.address, true) expect(data.userPriceCoeff).eq(10000) expect(data.votes).eq(stakedAmount.mul(2)) }) }) - - // '''..................................................................''' - // '''................... BAL TOKENS ......................''' - // '''..................................................................''' - - context("claiming BAL rewards", () => { - it("should allow BAL tokens to be claimed") - }) - - // '''..................................................................''' - // '''........................ FEES ETC ..........................''' - // '''..................................................................''' - - context("collecting fees", () => { - it("should convert fees back into $MTA") - }) }) diff --git a/test/governance/staking/staked-token-mta.spec.ts b/test/governance/staking/staked-token-mta.spec.ts index acb7c3c5..6e3bb021 100644 --- a/test/governance/staking/staked-token-mta.spec.ts +++ b/test/governance/staking/staked-token-mta.spec.ts @@ -145,7 +145,7 @@ describe("Staked Token MTA rewards", () => { // '''..................................................................''' context("compound rewards", () => { - it("should compound rewards") + it("should compound a users rewards") }) // '''..................................................................''' @@ -154,6 +154,7 @@ describe("Staked Token MTA rewards", () => { context("collecting fees in $MTA", () => { it("should collect the fees and notify as part of notification") + it("should add the correct amount of fees, and deposit to the vendor") }) context("distribute rewards", () => { @@ -317,4 +318,11 @@ describe("Staked Token MTA rewards", () => { }) }) }) + + context("earning rewards", () => { + it("should earn rewards on all user actions") + it("should claim rewards (from the token vendor)") + it("should calculate earned") + it("should use boostedBalance and totalSupply to earn rewards") + }) }) diff --git a/test/governance/staking/staked-token.spec.ts b/test/governance/staking/staked-token.spec.ts index c6ded783..796b6e85 100644 --- a/test/governance/staking/staked-token.spec.ts +++ b/test/governance/staking/staked-token.spec.ts @@ -22,10 +22,11 @@ import { assertBNClose, DEAD_ADDRESS } from "index" import { ONE_DAY, ONE_WEEK, ZERO_ADDRESS } from "@utils/constants" import { BN, simpleToExactAmount } from "@utils/math" import { expect } from "chai" -import { getTimestamp, increaseTime } from "@utils/time" +import { advanceBlock, getTimestamp, increaseTime } from "@utils/time" import { arrayify, formatBytes32String, solidityKeccak256 } from "ethers/lib/utils" import { BigNumberish, Signer } from "ethers" import { QuestStatus, QuestType, UserStakingData } from "types/stakedToken" +import { Block } from "@ethersproject/abstract-provider" const signUserQuests = async (user: string, questIds: BigNumberish[], questSigner: Signer): Promise => { const messageHash = solidityKeccak256(["address", "uint256[]"], [user, questIds]) @@ -39,6 +40,36 @@ const signQuestUsers = async (questId: BigNumberish, users: string[], questSigne return signature } +/** + * Calculate the new weighted timestamp after a stake or withdraw + * @param oldWeightedTimestamp + * @param currentTimestamp + * @param oldStakedBalance + * @param stakedDelta the absolute difference between new and old balances. Always positive + * @param stake true if staking, false if withdrawing + * @returns + */ +const calcWeightedTimestamp = ( + oldWeightedTimestamp: BN, + currentTimestamp: BN, + oldStakedBalance: BN, + stakedDelta: BN, + stake: boolean, +): BN => { + const oldWeightedSeconds = currentTimestamp.sub(oldWeightedTimestamp) + const adjustedStakedBalanceDelta = stake ? stakedDelta.div(2) : stakedDelta.div(8) + const adjustedNewStakedBalance = stake + ? oldStakedBalance.add(adjustedStakedBalanceDelta) + : oldStakedBalance.sub(adjustedStakedBalanceDelta) + const newWeightedSeconds = stake + ? oldStakedBalance.mul(oldWeightedSeconds).div(adjustedNewStakedBalance) + : adjustedNewStakedBalance.mul(oldWeightedSeconds).div(oldStakedBalance) + + return currentTimestamp.sub(newWeightedSeconds) +} + +// TODO +// - Consider how to enforce invariant that sum(balances) == totalSupply. describe("Staked Token", () => { let sa: StandardAccounts let deployTime: BN @@ -61,7 +92,7 @@ describe("Staked Token", () => { deployTime = await getTimestamp() nexus = await new MockNexus__factory(sa.default.signer).deploy(sa.governor.address, DEAD_ADDRESS, DEAD_ADDRESS) await nexus.setRecollateraliser(sa.mockRecollateraliser.address) - rewardToken = await new MockERC20__factory(sa.default.signer).deploy("Reward", "RWD", 18, sa.default.address, 10000000) + rewardToken = await new MockERC20__factory(sa.default.signer).deploy("Reward", "RWD", 18, sa.default.address, 10000100) const signatureVerifier = await new SignatureVerifier__factory(sa.default.signer).deploy() const questManagerLibraryAddresses = { @@ -102,6 +133,10 @@ describe("Staked Token", () => { await emissionController.setPreferences(65793) await sToken.connect(sa.governor.signer).setGovernanceHook(emissionController.address) + await rewardToken.transfer(sa.mockRewardsDistributor.address, simpleToExactAmount(100)) + await rewardToken.connect(sa.mockRewardsDistributor.signer).transfer(sToken.address, simpleToExactAmount(100)) + await sToken.connect(sa.mockRewardsDistributor.signer).notifyRewardAmount(simpleToExactAmount(100)) + return { stakedToken: sToken, questManager: qMaster, @@ -109,20 +144,22 @@ describe("Staked Token", () => { } const snapshotUserStakingData = async (user = sa.default.address): Promise => { - const stakedBalance = await stakedToken.balanceOf(user) + const scaledBalance = await stakedToken.balanceOf(user) const votes = await stakedToken.getVotes(user) const earnedRewards = await stakedToken.earned(user) - const rewardsBalance = await rewardToken.balanceOf(user) - const userBalances = await stakedToken.balanceData(user) + const numCheckpoints = await stakedToken.numCheckpoints(user) + const rewardTokenBalance = await rewardToken.balanceOf(user) + const rawBalance = await stakedToken.balanceData(user) const userPriceCoeff = await stakedToken.userPriceCoeff(user) const questBalance = await questManager.balanceData(user) return { - stakedBalance, + scaledBalance, votes, earnedRewards, - rewardsBalance, - userBalances, + numCheckpoints, + rewardTokenBalance, + rawBalance, userPriceCoeff, questBalance, } @@ -153,6 +190,7 @@ describe("Staked Token", () => { expect(await stakedToken.COOLDOWN_SECONDS(), "cooldown").to.eq(ONE_WEEK) expect(await stakedToken.UNSTAKE_WINDOW(), "unstake window").to.eq(ONE_DAY.mul(2)) expect(await stakedToken.questManager(), "quest manager").to.eq(questManager.address) + expect(await stakedToken.hasPriceCoeff(), "price coeff").to.eq(false) const safetyData = await stakedToken.safetyData() expect(safetyData.collateralisationRatio, "Collateralisation ratio").to.eq(simpleToExactAmount(1)) @@ -164,6 +202,7 @@ describe("Staked Token", () => { // '''............... STAKEDTOKEN.STAKE & DELEGATE ..................''' // '''..................................................................''' + // TODO - factor in `rawBalanceOf` here context("staking and delegating", () => { const stakedAmount = simpleToExactAmount(1000) beforeEach(async () => { @@ -171,80 +210,166 @@ describe("Staked Token", () => { await rewardToken.connect(sa.default.signer).approve(stakedToken.address, stakedAmount.mul(3)) const stakerDataBefore = await snapshotUserStakingData(sa.default.address) - expect(stakerDataBefore.userBalances.weightedTimestamp, "weighted timestamp before").to.eq(0) - expect(stakerDataBefore.userBalances.questMultiplier, "quest multiplier").to.eq(0) + expect(stakerDataBefore.rawBalance.weightedTimestamp, "weighted timestamp before").to.eq(0) + expect(stakerDataBefore.rawBalance.questMultiplier, "quest multiplier").to.eq(0) expect(stakerDataBefore.questBalance.lastAction, "last action before").to.eq(0) expect(stakerDataBefore.questBalance.permMultiplier, "perm multiplier before").to.eq(0) expect(stakerDataBefore.questBalance.seasonMultiplier, "season multiplier before").to.eq(0) - expect(stakerDataBefore.userBalances.timeMultiplier, "time multiplier before").to.eq(0) - expect(stakerDataBefore.userBalances.cooldownUnits, "cooldown multiplier before").to.eq(0) - expect(stakerDataBefore.stakedBalance, "staker stkRWD before").to.eq(0) - expect(stakerDataBefore.rewardsBalance, "staker RWD before").to.eq(startingMintAmount) + expect(stakerDataBefore.rawBalance.timeMultiplier, "time multiplier before").to.eq(0) + expect(stakerDataBefore.rawBalance.cooldownUnits, "cooldown multiplier before").to.eq(0) + expect(stakerDataBefore.scaledBalance, "staker stkRWD before").to.eq(0) + expect(stakerDataBefore.rewardTokenBalance, "staker RWD before").to.eq(startingMintAmount) expect(stakerDataBefore.votes, "staker votes before").to.eq(0) - expect(stakerDataBefore.userBalances.cooldownTimestamp, "staker cooldown before").to.eq(0) + expect(stakerDataBefore.numCheckpoints, "staked checkpoints before").to.eq(0) + expect(stakerDataBefore.rawBalance.cooldownTimestamp, "staker cooldown before").to.eq(0) const delegateDataBefore = await snapshotUserStakingData(sa.dummy1.address) - expect(delegateDataBefore.stakedBalance, "delegate stkRWD before").to.eq(0) - expect(delegateDataBefore.rewardsBalance, "delegate RWD before").to.eq(0) + expect(delegateDataBefore.scaledBalance, "delegate stkRWD before").to.eq(0) + expect(delegateDataBefore.rewardTokenBalance, "delegate RWD before").to.eq(0) expect(delegateDataBefore.votes, "delegate votes before").to.eq(0) - expect(delegateDataBefore.userBalances.cooldownTimestamp, "delegate cooldown before").to.eq(0) + expect(delegateDataBefore.numCheckpoints, "delegate checkpoints before").to.eq(0) + expect(delegateDataBefore.rawBalance.cooldownTimestamp, "delegate cooldown before").to.eq(0) expect(await stakedToken.totalSupply(), "total staked before").to.eq(0) }) it("should delegate to self by default", async () => { + const stakerAddress = sa.default.address const tx = await stakedToken["stake(uint256)"](stakedAmount) const stakedTimestamp = await getTimestamp() - await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, stakedAmount, ZERO_ADDRESS) - await expect(tx).to.emit(stakedToken, "DelegateChanged").not - await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, 0, stakedAmount) - await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, stakedAmount) + await expect(tx).to.emit(stakedToken, "Staked").withArgs(stakerAddress, stakedAmount, ZERO_ADDRESS) + await expect(tx).to.not.emit(stakedToken, "DelegateChanged") + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(stakerAddress, 0, stakedAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakerAddress, stakedToken.address, stakedAmount) + await expect(tx).to.not.emit(stakedToken, "CooldownExited") - const afterData = await snapshotUserStakingData(sa.default.address) + const afterData = await snapshotUserStakingData(stakerAddress) - expect(afterData.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(afterData.userBalances.questMultiplier, "quest multiplier").to.eq(0) - expect(afterData.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(afterData.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(afterData.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(afterData.rawBalance.questMultiplier, "quest multiplier").to.eq(0) + expect(afterData.questBalance.lastAction, "last action after").to.eq(0) expect(afterData.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(afterData.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(afterData.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(afterData.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(afterData.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(afterData.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(afterData.votes, "staker votes after").to.eq(stakedAmount) + // Staker checkpoint + expect(afterData.numCheckpoints, "staked checkpoints after").to.eq(1) + const checkpoint = await stakedToken.checkpoints(stakerAddress, 0) + const receipt = await tx.wait() + expect(checkpoint.fromBlock, "staked checkpoint block").to.eq(receipt.blockNumber) + expect(checkpoint.votes, "staked checkpoint votes").to.eq(stakedAmount) expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount) }) it("should assign delegate", async () => { - const tx = await stakedToken["stake(uint256,address)"](stakedAmount, sa.dummy1.address) + const stakerAddress = sa.default.address + const delegateAddress = sa.dummy1.address + const tx = await stakedToken["stake(uint256,address)"](stakedAmount, delegateAddress) const stakedTimestamp = await getTimestamp() - await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, stakedAmount, sa.dummy1.address) - await expect(tx).to.emit(stakedToken, "DelegateChanged").withArgs(sa.default.address, sa.default.address, sa.dummy1.address) - await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.dummy1.address, 0, stakedAmount) - await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, stakedAmount) + await expect(tx).to.emit(stakedToken, "Staked").withArgs(stakerAddress, stakedAmount, delegateAddress) + await expect(tx).to.emit(stakedToken, "DelegateChanged").withArgs(stakerAddress, stakerAddress, delegateAddress) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(delegateAddress, 0, stakedAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakerAddress, stakedToken.address, stakedAmount) + await expect(tx).to.not.emit(stakedToken, "CooldownExited") const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.raw, "staker raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "staker weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "staker last action after").to.eq(stakedTimestamp) - expect(stakerDataAfter.stakedBalance, "staker stkRWD after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.raw, "staker raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "staker weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "staker last action after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staker stkRWD after").to.eq(stakedAmount) expect(stakerDataAfter.votes, "staker votes after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "staker cooldown after").to.eq(0) + expect(stakerDataAfter.numCheckpoints, "staker checkpoints after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "staker cooldown after").to.eq(0) const delegateDataAfter = await snapshotUserStakingData(sa.dummy1.address) - expect(delegateDataAfter.userBalances.raw, "delegate raw balance after").to.eq(0) - expect(delegateDataAfter.userBalances.weightedTimestamp, "delegate weighted timestamp after").to.eq(0) + expect(delegateDataAfter.rawBalance.raw, "delegate raw balance after").to.eq(0) + expect(delegateDataAfter.rawBalance.weightedTimestamp, "delegate weighted timestamp after").to.eq(0) expect(delegateDataAfter.questBalance.lastAction, "delegate last action after").to.eq(0) - expect(delegateDataAfter.stakedBalance, "delegate stkRWD after").to.eq(0) + expect(delegateDataAfter.scaledBalance, "delegate stkRWD after").to.eq(0) expect(delegateDataAfter.votes, "delegate votes after").to.eq(stakedAmount) - expect(delegateDataAfter.userBalances.cooldownTimestamp, "delegate cooldown after").to.eq(0) + expect(delegateDataAfter.numCheckpoints, "delegate checkpoints after").to.eq(1) + expect(delegateDataAfter.rawBalance.cooldownTimestamp, "delegate cooldown after").to.eq(0) + // Delegate Checkpoint + const checkpoint = await stakedToken.checkpoints(delegateAddress, 0) + const receipt = await tx.wait() + expect(checkpoint.fromBlock, "delegate checkpoint block").to.eq(receipt.blockNumber) + expect(checkpoint.votes, "delegate checkpoint votes").to.eq(stakedAmount) expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount) }) + it("should stake to a delegate after staking with self as delegate", async () => { + const firstStakedAmount = simpleToExactAmount(100) + const secondStakedAmount = simpleToExactAmount(200) + const bothStakedAmounts = firstStakedAmount.add(secondStakedAmount) + const stakerAddress = sa.default.address + const delegateAddress = sa.dummy1.address + const tx1 = await stakedToken["stake(uint256)"](firstStakedAmount) + const receipt1 = await tx1.wait() + const firstStakedTimestamp = await getTimestamp() + + await increaseTime(ONE_WEEK) + + const tx2 = await stakedToken["stake(uint256,address)"](secondStakedAmount, delegateAddress) + const receipt2 = await tx2.wait() + + const secondStakedTimestamp = await getTimestamp() + + await expect(tx2).to.emit(stakedToken, "Staked").withArgs(stakerAddress, secondStakedAmount, delegateAddress) + await expect(tx2).to.emit(stakedToken, "DelegateChanged").withArgs(stakerAddress, stakerAddress, delegateAddress) + await expect(tx2).to.emit(stakedToken, "DelegateVotesChanged").withArgs(stakerAddress, firstStakedAmount, 0) + await expect(tx2).to.emit(stakedToken, "DelegateVotesChanged").withArgs(delegateAddress, 0, firstStakedAmount) + await expect(tx2).to.emit(stakedToken, "DelegateVotesChanged").withArgs(delegateAddress, firstStakedAmount, bothStakedAmounts) + await expect(tx2).to.emit(rewardToken, "Transfer").withArgs(stakerAddress, stakedToken.address, secondStakedAmount) + await expect(tx2).to.not.emit(stakedToken, "CooldownExited") + + // Staker + const stakerDataAfter = await snapshotUserStakingData(stakerAddress) + expect(stakerDataAfter.rawBalance.raw, "staker raw balance after").to.eq(bothStakedAmounts) + const newWeightedTimestamp = calcWeightedTimestamp( + firstStakedTimestamp, + secondStakedTimestamp, + firstStakedAmount, + secondStakedAmount, + true, + ) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "staker weighted timestamp after").to.eq(newWeightedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "staker last action after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staker stkRWD after").to.eq(bothStakedAmounts) + expect(stakerDataAfter.votes, "staker votes after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "staker cooldown after").to.eq(0) + expect(stakerDataAfter.numCheckpoints, "staker checkpoints after").to.eq(2) + // Staker 1st checkpoint + const stakerCheckpoint1 = await stakedToken.checkpoints(stakerAddress, 0) + expect(stakerCheckpoint1.fromBlock, "staker 1st checkpoint block").to.eq(receipt1.blockNumber) + expect(stakerCheckpoint1.votes, "staker 1st checkpoint votes").to.eq(firstStakedAmount) + // Staker 2nd checkpoint + const stakerCheckpoint2 = await stakedToken.checkpoints(stakerAddress, 1) + expect(stakerCheckpoint2.fromBlock, "staker 2nd checkpoint block").to.eq(receipt2.blockNumber) + expect(stakerCheckpoint2.votes, "staker 2nd checkpoint votes").to.eq(0) + + // Delegate + const delegateDataAfter = await snapshotUserStakingData(delegateAddress) + expect(delegateDataAfter.rawBalance.raw, "delegate raw balance after").to.eq(0) + expect(delegateDataAfter.rawBalance.weightedTimestamp, "delegate weighted timestamp after").to.eq(0) + expect(delegateDataAfter.questBalance.lastAction, "delegate last action after").to.eq(0) + expect(delegateDataAfter.scaledBalance, "delegate stkRWD after").to.eq(0) + expect(delegateDataAfter.votes, "delegate votes after").to.eq(bothStakedAmounts) + expect(delegateDataAfter.rawBalance.cooldownTimestamp, "delegate cooldown after").to.eq(0) + expect(delegateDataAfter.numCheckpoints, "delegate checkpoints after").to.eq(1) + // Delegate Checkpoint + const delegateCheckpoint = await stakedToken.checkpoints(delegateAddress, 0) + expect(delegateCheckpoint.fromBlock, "delegate checkpoint block").to.eq(receipt2.blockNumber) + expect(delegateCheckpoint.votes, "delegate checkpoint votes").to.eq(bothStakedAmounts) + + expect(await stakedToken.totalSupply(), "total staked after").to.eq(bothStakedAmounts) + }) it("should not chain delegate votes", async () => { const delegateStakedAmount = simpleToExactAmount(2000) await rewardToken.transfer(sa.dummy1.address, delegateStakedAmount) @@ -254,19 +379,37 @@ describe("Staked Token", () => { await stakedToken.connect(sa.dummy1.signer)["stake(uint256,address)"](delegateStakedAmount, sa.dummy2.address) const afterStakerData = await snapshotUserStakingData(sa.default.address) - expect(afterStakerData.stakedBalance, "staker stkRWD after").to.eq(stakedAmount) + expect(afterStakerData.scaledBalance, "staker stkRWD after").to.eq(stakedAmount) expect(afterStakerData.votes, "staker votes after").to.eq(0) const afterDelegateData = await snapshotUserStakingData(sa.dummy1.address) - expect(afterDelegateData.stakedBalance, "delegate stkRWD after").to.eq(delegateStakedAmount) + expect(afterDelegateData.scaledBalance, "delegate stkRWD after").to.eq(delegateStakedAmount) expect(afterDelegateData.votes, "delegate votes after").to.eq(stakedAmount) const afterDelegatesDelegateData = await snapshotUserStakingData(sa.dummy2.address) - expect(afterDelegatesDelegateData.stakedBalance, "delegate stkRWD after").to.eq(0) + expect(afterDelegatesDelegateData.scaledBalance, "delegate stkRWD after").to.eq(0) expect(afterDelegatesDelegateData.votes, "delegate votes after").to.eq(delegateStakedAmount) expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount.add(delegateStakedAmount)) }) + // TODO + it("should stake twice in the same block") + it("should update weightedTimestamp after subsequent stake") + it("should exit cooldown if cooldown period has expired") + context("should fail when", () => { + it("staking 0 amount", async () => { + const tx = stakedToken["stake(uint256)"](0) + await expect(tx).to.revertedWith("INVALID_ZERO_AMOUNT") + }) + it("staking 0 amount while exiting cooldown", async () => { + const tx = stakedToken["stake(uint256,bool)"](0, true) + await expect(tx).to.revertedWith("INVALID_ZERO_AMOUNT") + }) + it("staking 0 amount with delegate", async () => { + const tx = stakedToken["stake(uint256,address)"](0, sa.dummy1.address) + await expect(tx).to.revertedWith("INVALID_ZERO_AMOUNT") + }) + }) }) context("change delegate votes", () => { const stakedAmount = simpleToExactAmount(100) @@ -280,7 +423,7 @@ describe("Staked Token", () => { const stakedTimestamp = await getTimestamp() const stakerDataBefore = await snapshotUserStakingData(sa.default.address) expect(stakerDataBefore.votes).to.equal(stakedAmount) - expect(stakerDataBefore.stakedBalance).to.equal(stakedAmount) + expect(stakerDataBefore.scaledBalance).to.equal(stakedAmount) const delegateDataBefore = await snapshotUserStakingData(sa.dummy1.address) expect(delegateDataBefore.votes).to.equal(0) @@ -296,18 +439,18 @@ describe("Staked Token", () => { // Staker const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.raw, "staker raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "staker weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "staker last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.raw, "staker raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "staker weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "staker last action after").to.eq(0) expect(stakerDataAfter.votes, "staker votes after").to.equal(0) - expect(stakerDataAfter.stakedBalance, "staker staked balance after").to.equal(stakedAmount) + expect(stakerDataAfter.scaledBalance, "staker staked balance after").to.equal(stakedAmount) // Delegate const delegateDataAfter = await snapshotUserStakingData(sa.dummy1.address) - expect(delegateDataAfter.userBalances.raw, "delegate raw balance after").to.eq(0) - expect(delegateDataAfter.userBalances.weightedTimestamp, "delegate weighted timestamp after").to.eq(0) + expect(delegateDataAfter.rawBalance.raw, "delegate raw balance after").to.eq(0) + expect(delegateDataAfter.rawBalance.weightedTimestamp, "delegate weighted timestamp after").to.eq(0) expect(delegateDataAfter.questBalance.lastAction, "delegate last action after").to.eq(0) expect(delegateDataAfter.votes, "delegate votes after").to.equal(stakedAmount) - expect(delegateDataAfter.stakedBalance, "delegate staked balance after").to.equal(0) + expect(delegateDataAfter.scaledBalance, "delegate staked balance after").to.equal(0) }) it("should change delegate by staker from dummy 1 to 2", async () => { await stakedToken["stake(uint256,address)"](stakedAmount, sa.dummy1.address) @@ -326,13 +469,13 @@ describe("Staked Token", () => { const stakerDataAfter = await snapshotUserStakingData(sa.default.address) expect(stakerDataAfter.votes).to.equal(0) - expect(stakerDataAfter.stakedBalance).to.equal(stakedAmount) + expect(stakerDataAfter.scaledBalance).to.equal(stakedAmount) const oldDelegateDataAfter = await snapshotUserStakingData(sa.dummy1.address) expect(oldDelegateDataAfter.votes).to.equal(0) - expect(oldDelegateDataAfter.stakedBalance).to.equal(0) + expect(oldDelegateDataAfter.scaledBalance).to.equal(0) const newDelegateDataAfter = await snapshotUserStakingData(sa.dummy2.address) expect(newDelegateDataAfter.votes).to.equal(stakedAmount) - expect(newDelegateDataAfter.stakedBalance).to.equal(0) + expect(newDelegateDataAfter.scaledBalance).to.equal(0) }) it("should change by staker from delegate to self", async () => { await stakedToken["stake(uint256,address)"](stakedAmount, sa.dummy1.address) @@ -340,7 +483,7 @@ describe("Staked Token", () => { const stakedTimestamp = await getTimestamp() const stakerDataBefore = await snapshotUserStakingData(sa.default.address) expect(stakerDataBefore.votes).to.equal(0) - expect(stakerDataBefore.stakedBalance).to.equal(stakedAmount) + expect(stakerDataBefore.scaledBalance).to.equal(stakedAmount) const delegateDataBefore = await snapshotUserStakingData(sa.dummy1.address) expect(delegateDataBefore.votes).to.equal(stakedAmount) @@ -354,15 +497,15 @@ describe("Staked Token", () => { // Staker const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.raw, "staker raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "staker weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "staker last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.raw, "staker raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "staker weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "staker last action after").to.eq(0) expect(stakerDataAfter.votes, "staker votes after").to.equal(stakedAmount) - expect(stakerDataAfter.stakedBalance, "staker staked balance after").to.equal(stakedAmount) + expect(stakerDataAfter.scaledBalance, "staker staked balance after").to.equal(stakedAmount) // Delegate const delegateDataAfter = await snapshotUserStakingData(sa.dummy1.address) expect(delegateDataAfter.votes, "delegate votes after").to.equal(0) - expect(delegateDataAfter.stakedBalance, "delegate staked balance after").to.equal(0) + expect(delegateDataAfter.scaledBalance, "delegate staked balance after").to.equal(0) }) it("by delegate", async () => { const tx = await stakedToken.connect(sa.dummy1.signer).delegate(sa.dummy2.address) @@ -379,6 +522,7 @@ describe("Staked Token", () => { // '''............ STAKEDTOKEN.COOLDOWN & WITHDRAW ...............''' // '''..................................................................''' + // TODO - factor in `rawBalanceOf` here context("cooldown", () => { const stakedAmount = simpleToExactAmount(7000) context("with no delegate", () => { @@ -410,20 +554,20 @@ describe("Staked Token", () => { const startCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(startCooldownTimestamp) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(0) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(startCooldownTimestamp) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(0) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(0) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(0) expect(stakerDataAfter.votes, "votes after").to.eq(0) }) it("should partial cooldown again after it has already started", async () => { const stakerDataBefore = await snapshotUserStakingData(sa.default.address) - expect(stakerDataBefore.userBalances.weightedTimestamp, "weighted timestamp before").to.eq(stakedTimestamp) + expect(stakerDataBefore.rawBalance.weightedTimestamp, "weighted timestamp before").to.eq(stakedTimestamp) // First cooldown for 80% of stake const firstCooldown = stakedAmount.mul(4).div(5) @@ -432,17 +576,15 @@ describe("Staked Token", () => { const cooldown1stTimestamp = await getTimestamp() const stakerDataAfter1stCooldown = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter1stCooldown.userBalances.cooldownTimestamp, "cooldown timestamp after 1st").to.eq( - cooldown1stTimestamp, - ) - expect(stakerDataAfter1stCooldown.userBalances.cooldownUnits, "cooldown units after 1st").to.eq(firstCooldown) - expect(stakerDataAfter1stCooldown.userBalances.raw, "staked raw balance after 1st").to.eq(stakedAmount.sub(firstCooldown)) - expect(stakerDataAfter1stCooldown.userBalances.weightedTimestamp, "weighted timestamp after 1st").to.eq(stakedTimestamp) - expect(stakerDataAfter1stCooldown.questBalance.lastAction, "last action after 1st").to.eq(stakedTimestamp) + expect(stakerDataAfter1stCooldown.rawBalance.cooldownTimestamp, "cooldown timestamp after 1st").to.eq(cooldown1stTimestamp) + expect(stakerDataAfter1stCooldown.rawBalance.cooldownUnits, "cooldown units after 1st").to.eq(firstCooldown) + expect(stakerDataAfter1stCooldown.rawBalance.raw, "staked raw balance after 1st").to.eq(stakedAmount.sub(firstCooldown)) + expect(stakerDataAfter1stCooldown.rawBalance.weightedTimestamp, "weighted timestamp after 1st").to.eq(stakedTimestamp) + expect(stakerDataAfter1stCooldown.questBalance.lastAction, "last action after 1st").to.eq(0) expect(stakerDataAfter1stCooldown.questBalance.permMultiplier, "perm multiplier after 1st").to.eq(0) expect(stakerDataAfter1stCooldown.questBalance.seasonMultiplier, "season multiplier after 1st").to.eq(0) - expect(stakerDataAfter1stCooldown.userBalances.timeMultiplier, "time multiplier after 1st").to.eq(0) - expect(stakerDataAfter1stCooldown.stakedBalance, "staked balance after 1st").to.eq(stakedAmount.div(5)) + expect(stakerDataAfter1stCooldown.rawBalance.timeMultiplier, "time multiplier after 1st").to.eq(0) + expect(stakerDataAfter1stCooldown.scaledBalance, "staked balance after 1st").to.eq(stakedAmount.div(5)) await increaseTime(ONE_DAY) @@ -452,19 +594,20 @@ describe("Staked Token", () => { const cooldown2ndTimestamp = await getTimestamp() const stakerDataAfter2ndCooldown = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter2ndCooldown.userBalances.cooldownTimestamp, "cooldown timestamp after 2nd").to.eq( - cooldown2ndTimestamp, - ) - expect(stakerDataAfter2ndCooldown.userBalances.cooldownUnits, "cooldown units after 2nd").to.eq(secondCooldown) - expect(stakerDataAfter2ndCooldown.userBalances.raw, "staked raw balance after 2nd").to.eq(stakedAmount.sub(secondCooldown)) - expect(stakerDataAfter2ndCooldown.userBalances.weightedTimestamp, "weighted timestamp after 2nd").to.eq(stakedTimestamp) - expect(stakerDataAfter2ndCooldown.questBalance.lastAction, "last action after 2nd").to.eq(stakedTimestamp) + expect(stakerDataAfter2ndCooldown.rawBalance.cooldownTimestamp, "cooldown timestamp after 2nd").to.eq(cooldown2ndTimestamp) + expect(stakerDataAfter2ndCooldown.rawBalance.cooldownUnits, "cooldown units after 2nd").to.eq(secondCooldown) + expect(stakerDataAfter2ndCooldown.rawBalance.raw, "staked raw balance after 2nd").to.eq(stakedAmount.sub(secondCooldown)) + expect(stakerDataAfter2ndCooldown.rawBalance.weightedTimestamp, "weighted timestamp after 2nd").to.eq(stakedTimestamp) + expect(stakerDataAfter2ndCooldown.questBalance.lastAction, "last action after 2nd").to.eq(0) expect(stakerDataAfter2ndCooldown.questBalance.permMultiplier, "perm multiplier after 2nd").to.eq(0) expect(stakerDataAfter2ndCooldown.questBalance.seasonMultiplier, "season multiplier after 2nd").to.eq(0) - expect(stakerDataAfter2ndCooldown.userBalances.timeMultiplier, "time multiplier after 2nd").to.eq(0) - expect(stakerDataAfter2ndCooldown.stakedBalance, "staked balance after 2nd").to.eq(stakedAmount.mul(4).div(5)) + expect(stakerDataAfter2ndCooldown.rawBalance.timeMultiplier, "time multiplier after 2nd").to.eq(0) + expect(stakerDataAfter2ndCooldown.scaledBalance, "staked balance after 2nd").to.eq(stakedAmount.mul(4).div(5)) + expect(stakerDataAfter2ndCooldown.votes, "votes balance after 2nd").to.eq(stakedAmount.mul(4).div(5)) + expect(await stakedToken.totalSupply(), "total supply").to.eq(stakedAmount.mul(4).div(5)) }) it("should reduce cooldown percentage enough to end the cooldown") + context("should end 100% cooldown", () => { beforeEach(async () => { await increaseTime(ONE_WEEK) @@ -478,15 +621,15 @@ describe("Staked Token", () => { const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) it("in unstake window", async () => { @@ -497,15 +640,15 @@ describe("Staked Token", () => { const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) it("after unstake window", async () => { @@ -516,15 +659,15 @@ describe("Staked Token", () => { const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) it("after time multiplier increases", async () => { @@ -535,15 +678,15 @@ describe("Staked Token", () => { const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(20) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(stakedAmount.mul(12).div(10)) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(20) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(stakedAmount.mul(12).div(10)) expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount.mul(12).div(10)) }) }) @@ -561,15 +704,15 @@ describe("Staked Token", () => { const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) it("in unstake window", async () => { @@ -580,15 +723,15 @@ describe("Staked Token", () => { const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) it("after unstake window", async () => { @@ -599,15 +742,15 @@ describe("Staked Token", () => { const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) }) @@ -626,42 +769,48 @@ describe("Staked Token", () => { const cooldownTimestamp = await getTimestamp() const stakerDataAfterCooldown = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfterCooldown.userBalances.cooldownTimestamp, "cooldown timestamp after cooldown").to.eq(cooldownTimestamp) - expect(stakerDataAfterCooldown.userBalances.cooldownUnits, "cooldown units after cooldown").to.eq(cooldownAmount) - expect(stakerDataAfterCooldown.userBalances.raw, "staked raw balance after cooldown").to.eq( - stakedAmount.sub(cooldownAmount), - ) - expect(stakerDataAfterCooldown.userBalances.weightedTimestamp, "weighted timestamp after cooldown").to.eq(stakedTimestamp) - expect(stakerDataAfterCooldown.questBalance.lastAction, "last action after cooldown").to.eq(stakedTimestamp) - expect(stakerDataAfterCooldown.stakedBalance, "staked after cooldown").to.eq(stakedAmount.div(5)) - expect(stakerDataAfterCooldown.userBalances.timeMultiplier, "time multiplier after cooldown").to.eq(0) + expect(stakerDataAfterCooldown.rawBalance.cooldownTimestamp, "cooldown timestamp after cooldown").to.eq(cooldownTimestamp) + expect(stakerDataAfterCooldown.rawBalance.cooldownUnits, "cooldown units after cooldown").to.eq(cooldownAmount) + expect(stakerDataAfterCooldown.rawBalance.raw, "staked raw balance after cooldown").to.eq(stakedAmount.sub(cooldownAmount)) + expect(stakerDataAfterCooldown.rawBalance.weightedTimestamp, "weighted timestamp after cooldown").to.eq(stakedTimestamp) + expect(stakerDataAfterCooldown.questBalance.lastAction, "last action after cooldown").to.eq(0) + expect(stakerDataAfterCooldown.scaledBalance, "staked after cooldown").to.eq(stakedAmount.div(5)) + expect(stakerDataAfterCooldown.rawBalance.timeMultiplier, "time multiplier after cooldown").to.eq(0) expect(stakerDataAfterCooldown.votes, "20% of vote after 80% cooldown").to.eq(stakedAmount.div(5)) // Stake 3000 on top of 7000 and end cooldown - const secondStakeAmount = simpleToExactAmount(3000) - const tx = await stakedToken["stake(uint256,bool)"](secondStakeAmount, true) + const secondStakedAmount = simpleToExactAmount(3000) + const tx = await stakedToken["stake(uint256,bool)"](secondStakedAmount, true) const secondStakedTimestamp = await getTimestamp() - await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, secondStakeAmount, ZERO_ADDRESS) - await expect(tx).to.emit(stakedToken, "DelegateChanged").not + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, secondStakedAmount, ZERO_ADDRESS) + await expect(tx).to.not.emit(stakedToken, "DelegateChanged") await expect(tx) .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.default.address, stakedAmount.div(5), stakedAmount.add(secondStakeAmount)) - await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, secondStakeAmount) + .withArgs(sa.default.address, stakedAmount.div(5), stakedAmount.add(secondStakedAmount)) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, secondStakedAmount) await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const stakerDataAfter2ndStake = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter2ndStake.userBalances.cooldownTimestamp, "cooldown timestamp after 2nd stake").to.eq(0) - expect(stakerDataAfter2ndStake.userBalances.cooldownUnits, "cooldown units after 2nd stake").to.eq(0) - expect(stakerDataAfter2ndStake.userBalances.raw, "staked raw balance after 2nd stake").to.eq( - stakedAmount.add(secondStakeAmount), + expect(stakerDataAfter2ndStake.rawBalance.cooldownTimestamp, "cooldown timestamp after 2nd stake").to.eq(0) + expect(stakerDataAfter2ndStake.rawBalance.cooldownUnits, "cooldown units after 2nd stake").to.eq(0) + expect(stakerDataAfter2ndStake.rawBalance.raw, "staked raw balance after 2nd stake").to.eq( + stakedAmount.add(secondStakedAmount), + ) + const newWeightedTimestamp = calcWeightedTimestamp( + stakedTimestamp, + secondStakedTimestamp, + stakedAmount, + secondStakedAmount, + true, ) - // TODO need to calculate the weightedTimestamp = - // expect(stakerDataAfter2ndStake.userBalances.weightedTimestamp, "weighted timestamp after 2nd stake").to.eq(stakedTimestamp) - expect(stakerDataAfter2ndStake.questBalance.lastAction, "last action after 2nd stake").to.eq(stakedTimestamp) - expect(stakerDataAfter2ndStake.userBalances.timeMultiplier, "time multiplier after 2nd stake").to.eq(0) - expect(stakerDataAfter2ndStake.stakedBalance, "staked after 2nd stake").to.eq(stakedAmount.add(secondStakeAmount)) - expect(stakerDataAfter2ndStake.votes, "vote after 2nd stake").to.eq(stakedAmount.add(secondStakeAmount)) + expect(stakerDataAfter2ndStake.rawBalance.weightedTimestamp, "weighted timestamp after 2nd stake").to.eq( + newWeightedTimestamp, + ) + expect(stakerDataAfter2ndStake.questBalance.lastAction, "last action after 2nd stake").to.eq(0) + expect(stakerDataAfter2ndStake.rawBalance.timeMultiplier, "time multiplier after 2nd stake").to.eq(0) + expect(stakerDataAfter2ndStake.scaledBalance, "staked after 2nd stake").to.eq(stakedAmount.add(secondStakedAmount)) + expect(stakerDataAfter2ndStake.votes, "vote after 2nd stake").to.eq(stakedAmount.add(secondStakedAmount)) }) it("should proportionally reset cooldown when staking in cooldown", async () => { await increaseTime(ONE_WEEK) @@ -671,30 +820,34 @@ describe("Staked Token", () => { const stakerDataAfterCooldown = await snapshotUserStakingData(sa.default.address) const cooldownTime = await getTimestamp() - expect(stakerDataAfterCooldown.userBalances.cooldownTimestamp, "staker cooldown timestamp after cooldown").to.eq( - cooldownTime, - ) + expect(stakerDataAfterCooldown.rawBalance.cooldownTimestamp, "staker cooldown timestamp after cooldown").to.eq(cooldownTime) await increaseTime(ONE_DAY.mul(5)) // 2nd stake of 3000 on top of the existing 7000 - const secondStakeAmount = simpleToExactAmount(3000) - await stakedToken["stake(uint256,address)"](secondStakeAmount, sa.default.address) + const secondStakedAmount = simpleToExactAmount(3000) + await stakedToken["stake(uint256,address)"](secondStakedAmount, sa.default.address) - const secondStakeTimestamp = await getTimestamp() + const secondStakedTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "staker cooldown timestamp after 2nd stake").to.eq( - stakerDataAfterCooldown.userBalances.cooldownTimestamp, + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "staker cooldown timestamp after 2nd stake").to.eq( + stakerDataAfterCooldown.rawBalance.cooldownTimestamp, ) - expect(stakerDataAfter.userBalances.cooldownUnits, "staker cooldown units after 2nd stake").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after 2nd stake").to.eq(secondStakeAmount) - expect(stakerDataAfter.stakedBalance, "staker staked after 2nd stake").to.eq(secondStakeAmount) - expect(stakerDataAfter.votes, "staker votes after 2nd stake").to.eq(secondStakeAmount) - // TODO calculate new weighted timestamp - // expect(stakerDataAfter.userBalances.weightedTimestamp, "staker weighted timestamp after").to.eq(stakedTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "staker last action after 2nd stake").to.eq(stakedTimestamp) - expect(stakerDataAfter.userBalances.timeMultiplier, "staker time multiplier after 2nd stake").to.eq(0) + expect(stakerDataAfter.rawBalance.cooldownUnits, "staker cooldown units after 2nd stake").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after 2nd stake").to.eq(secondStakedAmount) + expect(stakerDataAfter.scaledBalance, "staker staked after 2nd stake").to.eq(secondStakedAmount) + expect(stakerDataAfter.votes, "staker votes after 2nd stake").to.eq(secondStakedAmount) + const newWeightedTimestamp = calcWeightedTimestamp( + stakedTimestamp, + secondStakedTimestamp, + stakedAmount, + secondStakedAmount, + true, + ) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "staker weighted timestamp after").to.eq(newWeightedTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "staker last action after 2nd stake").to.eq(0) + expect(stakerDataAfter.rawBalance.timeMultiplier, "staker time multiplier after 2nd stake").to.eq(0) }) }) context("with delegate", () => { @@ -746,9 +899,12 @@ describe("Staked Token", () => { it("when withdrawing too much", async () => { await stakedToken.startCooldown(10000) await increaseTime(ONE_DAY.mul(7).add(60)) - await expect(stakedToken.withdraw(stakedAmount.add(1), sa.default.address, false, false)).to.reverted + await expect(stakedToken.withdraw(stakedAmount.add(1), sa.default.address, false, false)).to.revertedWith( + "Exceeds max withdrawal", + ) }) }) + context("with no delegate, after 100% cooldown and in unstake window", () => { let beforeData: UserStakingData beforeEach(async () => { @@ -761,13 +917,13 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY.mul(7).add(60)) beforeData = await snapshotUserStakingData(sa.default.address) - expect(beforeData.userBalances.raw, "staked raw balance before").to.eq(0) - expect(beforeData.stakedBalance, "staker staked before").to.eq(0) + expect(beforeData.rawBalance.raw, "staked raw balance before").to.eq(0) + expect(beforeData.scaledBalance, "staker staked before").to.eq(0) expect(beforeData.votes, "staker votes before").to.eq(0) - expect(beforeData.rewardsBalance, "staker rewards before").to.eq(startingMintAmount.sub(stakedAmount)) - expect(beforeData.userBalances.cooldownTimestamp, "cooldown timestamp before").to.eq(cooldownTimestamp) - expect(beforeData.userBalances.cooldownUnits, "cooldown units before").to.eq(stakedAmount) - // expect(beforeData.userBalances.cooldownMultiplier, "cooldown multiplier before").to.eq(100) + expect(beforeData.rewardTokenBalance, "staker rewards before").to.eq(startingMintAmount.sub(stakedAmount)) + expect(beforeData.rawBalance.cooldownTimestamp, "cooldown timestamp before").to.eq(cooldownTimestamp) + expect(beforeData.rawBalance.cooldownUnits, "cooldown units before").to.eq(stakedAmount) + // expect(beforeData.rawBalance.cooldownMultiplier, "cooldown multiplier before").to.eq(100) }) it("partial withdraw not including fee", async () => { const withdrawAmount = simpleToExactAmount(100) @@ -776,16 +932,14 @@ describe("Staked Token", () => { await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, withdrawAmount) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker staked after").to.eq(0) + expect(afterData.scaledBalance, "staker staked after").to.eq(0) expect(afterData.votes, "staker votes after").to.eq(0) - expect(afterData.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq( - beforeData.userBalances.cooldownTimestamp, - ) - expect(afterData.userBalances.cooldownUnits, "cooldown units after").to.eq( - beforeData.userBalances.cooldownUnits.sub(withdrawAmount).sub(redemptionFee), + expect(afterData.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(beforeData.rawBalance.cooldownTimestamp) + expect(afterData.rawBalance.cooldownUnits, "cooldown units after").to.eq( + beforeData.rawBalance.cooldownUnits.sub(withdrawAmount).sub(redemptionFee), ) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) - expect(afterData.rewardsBalance, "staker rewards after").to.eq(beforeData.rewardsBalance.add(withdrawAmount)) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(0) + expect(afterData.rewardTokenBalance, "staker rewards after").to.eq(beforeData.rewardTokenBalance.add(withdrawAmount)) }) it("full withdraw including fee", async () => { // withdrawal = stakedAmount / (1 + rate) @@ -797,17 +951,18 @@ describe("Staked Token", () => { await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.scaledBalance, "staker stkRWD after").to.eq(0) expect(afterData.votes, "staker votes after").to.eq(0) - expect(afterData.userBalances.cooldownTimestamp, "staked cooldown start after").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "staked cooldown units after").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) - assertBNClose(afterData.rewardsBalance, beforeData.rewardsBalance.add(stakedAmount).sub(redemptionFee), 1) + expect(afterData.rawBalance.cooldownTimestamp, "staked cooldown start after").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "staked cooldown units after").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(0) + assertBNClose(afterData.rewardTokenBalance, beforeData.rewardTokenBalance.add(stakedAmount).sub(redemptionFee), 1) }) it("not reset the cooldown timer unless all is all unstaked") it("apply a redemption fee which is added to the pendingRewards from the rewards contract") it("distribute these pendingAdditionalReward with the next notification") }) + // TODO - calculate weightedTimestamp updates, voting balance, totalSupply context("with no delegate, after 70% cooldown and in unstake window", () => { let beforeData: UserStakingData // 2000 * 0.3 = 600 @@ -826,13 +981,13 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY.mul(7).add(60)) beforeData = await snapshotUserStakingData(sa.default.address) - expect(beforeData.userBalances.raw, "staked raw balance before").to.eq(remainingBalance) - expect(beforeData.stakedBalance, "staker staked before").to.eq(remainingBalance) + expect(beforeData.rawBalance.raw, "staked raw balance before").to.eq(remainingBalance) + expect(beforeData.scaledBalance, "staker staked before").to.eq(remainingBalance) expect(beforeData.votes, "staker votes before").to.eq(remainingBalance) - expect(beforeData.rewardsBalance, "staker rewards before").to.eq(startingMintAmount.sub(stakedAmount)) - expect(beforeData.userBalances.cooldownTimestamp, "cooldown timestamp before").to.eq(cooldownTimestamp) - expect(beforeData.userBalances.cooldownUnits, "cooldown units before").to.eq(cooldownAmount) - // expect(beforeData.userBalances.cooldownMultiplier, "cooldown multiplier before").to.eq(70) + expect(beforeData.rewardTokenBalance, "staker rewards before").to.eq(startingMintAmount.sub(stakedAmount)) + expect(beforeData.rawBalance.cooldownTimestamp, "cooldown timestamp before").to.eq(cooldownTimestamp) + expect(beforeData.rawBalance.cooldownUnits, "cooldown units before").to.eq(cooldownAmount) + // expect(beforeData.rawBalance.cooldownMultiplier, "cooldown multiplier before").to.eq(70) }) it("partial withdraw not including fee", async () => { const withdrawAmount = simpleToExactAmount(300) @@ -841,18 +996,16 @@ describe("Staked Token", () => { await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, withdrawAmount) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker staked after").to.eq(remainingBalance) + expect(afterData.scaledBalance, "staker staked after").to.eq(remainingBalance) expect(afterData.votes, "staker votes after").to.eq(remainingBalance) - expect(afterData.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq( - beforeData.userBalances.cooldownTimestamp, - ) - expect(afterData.userBalances.cooldownUnits, "cooldown units after").to.eq( + expect(afterData.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(beforeData.rawBalance.cooldownTimestamp) + expect(afterData.rawBalance.cooldownUnits, "cooldown units after").to.eq( cooldownAmount.sub(withdrawAmount).sub(redemptionFee), ) - // expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(64) + // expect(afterData.rawBalance.cooldownMultiplier, "cooldown multiplier after").to.eq(64) // 2000 - 300 - 30 = 1670 - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(remainingBalance) - expect(afterData.rewardsBalance, "staker rewards after").to.eq(beforeData.rewardsBalance.add(withdrawAmount)) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(remainingBalance) + expect(afterData.rewardTokenBalance, "staker rewards after").to.eq(beforeData.rewardTokenBalance.add(withdrawAmount)) }) it("full withdraw of cooldown amount including fee", async () => { const redemptionFee = cooldownAmount.sub(cooldownAmount.mul(1000).div(1075)) @@ -860,12 +1013,12 @@ describe("Staked Token", () => { await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, cooldownAmount) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker stkRWD after").to.eq(remainingBalance) + expect(afterData.scaledBalance, "staker stkRWD after").to.eq(remainingBalance) expect(afterData.votes, "staker votes after").to.eq(remainingBalance) - expect(afterData.userBalances.cooldownTimestamp, "staked cooldown start after").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "staked cooldown units after").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(remainingBalance) - assertBNClose(afterData.rewardsBalance, beforeData.rewardsBalance.add(cooldownAmount).sub(redemptionFee), 1) + expect(afterData.rawBalance.cooldownTimestamp, "staked cooldown start after").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "staked cooldown units after").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(remainingBalance) + assertBNClose(afterData.rewardTokenBalance, beforeData.rewardTokenBalance.add(cooldownAmount).sub(redemptionFee), 1) }) it("not reset the cooldown timer unless all is all unstaked") it("apply a redemption fee which is added to the pendingRewards from the rewards contract") @@ -881,7 +1034,7 @@ describe("Staked Token", () => { await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) await stakedToken.connect(sa.mockRecollateraliser.signer).emergencyRecollateralisation() }) - it("should withdraw all incl fee and get 75% of rewards", async () => { + it("should withdraw all incl fee and get 75% of balance", async () => { const tx = await stakedToken.withdraw(stakedAmount, sa.default.address, true, false) await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) await expect(tx) @@ -889,13 +1042,13 @@ describe("Staked Token", () => { .withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.scaledBalance, "staker stkRWD after").to.eq(0) expect(afterData.votes, "staker votes after").to.eq(0) - expect(afterData.userBalances.cooldownTimestamp, "staked cooldown start after").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "staked cooldown units after").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) + expect(afterData.rawBalance.cooldownTimestamp, "staked cooldown start after").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "staked cooldown units after").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(0) }) - it("should withdraw all excl. fee and get 75% of rewards", async () => { + it("should withdraw all excl. fee and get 75% of balance", async () => { const tx = await stakedToken.withdraw(stakedAmount, sa.default.address, false, false) await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) await expect(tx) @@ -903,13 +1056,13 @@ describe("Staked Token", () => { .withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.scaledBalance, "staker stkRWD after").to.eq(0) expect(afterData.votes, "staker votes after").to.eq(0) - expect(afterData.userBalances.cooldownTimestamp, "staked cooldown start").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "staked cooldown units").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) + expect(afterData.rawBalance.cooldownTimestamp, "staked cooldown start").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "staked cooldown units").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(0) }) - it("should partial withdraw and get 75% of rewards", async () => { + it("should partial withdraw and get 75% of balance", async () => { const withdrawAmount = stakedAmount.div(10) const tx = await stakedToken.withdraw(withdrawAmount, sa.default.address, true, false) await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, withdrawAmount) @@ -918,11 +1071,11 @@ describe("Staked Token", () => { .withArgs(stakedToken.address, sa.default.address, withdrawAmount.mul(3).div(4)) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.scaledBalance, "staker stkRWD after").to.eq(0) expect(afterData.votes, "staker votes after").to.eq(0) - expect(afterData.userBalances.cooldownTimestamp, "staked cooldown start").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "staked cooldown units").to.eq(stakedAmount.sub(withdrawAmount)) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) + expect(afterData.rawBalance.cooldownTimestamp, "staked cooldown start").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "staked cooldown units").to.eq(stakedAmount.sub(withdrawAmount)) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(0) }) }) context("calc redemption fee", () => { @@ -969,49 +1122,52 @@ describe("Staked Token", () => { const stakedTimestamp = await getTimestamp() await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, stakedAmount, ZERO_ADDRESS) - await expect(tx).to.emit(stakedToken, "DelegateChanged").not + await expect(tx).to.not.emit(stakedToken, "DelegateChanged") await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, 0, stakedAmount) await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, stakedAmount) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(afterData.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(afterData.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(afterData.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(afterData.questBalance.lastAction, "last action after").to.eq(0) expect(afterData.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(afterData.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(afterData.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(afterData.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(afterData.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(afterData.scaledBalance, "staked balance after").to.eq(stakedAmount) expect(afterData.votes, "staker votes after").to.eq(stakedAmount) expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount) }) it("increaseLockAmount", async () => { await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(12)) - const stakeTimestamp = await getTimestamp() + const stakedTimestamp = await getTimestamp() await increaseTime(ONE_WEEK.mul(10)) const increaseAmount = simpleToExactAmount(200) const newBalance = stakedAmount.add(increaseAmount) const tx = await stakedToken.increaseLockAmount(increaseAmount) + const increaseStakeTimestamp = await getTimestamp() + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, increaseAmount, ZERO_ADDRESS) - await expect(tx).to.emit(stakedToken, "DelegateChanged").not + await expect(tx).to.not.emit(stakedToken, "DelegateChanged") await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, stakedAmount, newBalance) await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, increaseAmount) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(newBalance) - // expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(increaseTimestamp) - expect(afterData.questBalance.lastAction, "last action after").to.eq(stakeTimestamp) + expect(afterData.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(newBalance) + const newWeightedTimestamp = calcWeightedTimestamp(stakedTimestamp, increaseStakeTimestamp, stakedAmount, increaseAmount, true) + expect(afterData.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(newWeightedTimestamp) + expect(afterData.questBalance.lastAction, "last action after").to.eq(0) expect(afterData.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(afterData.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(afterData.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(afterData.stakedBalance, "staked balance after").to.eq(newBalance) + expect(afterData.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(afterData.scaledBalance, "staked balance after").to.eq(newBalance) expect(afterData.votes, "staker votes after").to.eq(newBalance) expect(await stakedToken.totalSupply(), "total staked after").to.eq(newBalance) @@ -1026,33 +1182,11 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY.mul(8)) await stakedToken.withdraw(stakedAmount, sa.default.address, true, false) const data = await snapshotUserStakingData() - expect(data.stakedBalance).eq(BN.from(0)) - expect(data.userBalances.cooldownTimestamp).eq(BN.from(0)) + expect(data.scaledBalance).eq(BN.from(0)) + expect(data.rawBalance.cooldownTimestamp).eq(BN.from(0)) await expect(stakedToken.increaseLockAmount(stakedAmount)).to.revertedWith("Nothing to increase") }) - it("increase lock length", async () => { - await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(12)) - const stakedTimestamp = await getTimestamp() - await increaseTime(ONE_WEEK.mul(10)) - - await stakedToken.increaseLockLength(ONE_WEEK.mul(20)) - - const afterData = await snapshotUserStakingData(sa.default.address) - - expect(afterData.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) - expect(afterData.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) - expect(afterData.questBalance.permMultiplier, "perm multiplier after").to.eq(0) - expect(afterData.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(afterData.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(afterData.stakedBalance, "staked balance after").to.eq(stakedAmount) - expect(afterData.votes, "staker votes after").to.eq(stakedAmount) - - expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount) - }) - it("first exit", async () => { + it("first exit to cooldown", async () => { await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(20)) const stakeTimestamp = await getTimestamp() await increaseTime(ONE_WEEK.mul(18)) @@ -1063,18 +1197,18 @@ describe("Staked Token", () => { const startCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(startCooldownTimestamp) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(stakedAmount) - expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(0) - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakeTimestamp) - expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(stakeTimestamp) + expect(stakerDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(startCooldownTimestamp) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.raw, "staked raw balance after").to.eq(0) + expect(stakerDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakeTimestamp) + expect(stakerDataAfter.questBalance.lastAction, "last action after").to.eq(0) expect(stakerDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(20) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(0) + expect(stakerDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(20) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(0) expect(stakerDataAfter.votes, "votes after").to.eq(0) }) - it("second exit", async () => { + it("second exit to withdraw", async () => { await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(20)) const stakedTimestamp = await getTimestamp() await increaseTime(ONE_DAY.mul(1)) @@ -1091,14 +1225,15 @@ describe("Staked Token", () => { const withdrawTimestamp = await getTimestamp() const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.scaledBalance, "staker stkRWD after").to.eq(0) expect(afterData.votes, "staker votes after").to.eq(0) - expect(afterData.userBalances.cooldownTimestamp, "staked cooldown start").to.eq(0) - expect(afterData.userBalances.cooldownUnits, "staked cooldown units").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) - // TODO expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(withdrawTimestamp) - expect(afterData.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) - expect(afterData.rewardsBalance, "staker rewards after").to.eq(startingMintAmount.sub(redemptionFee)) + expect(afterData.rawBalance.cooldownTimestamp, "staked cooldown start").to.eq(0) + expect(afterData.rawBalance.cooldownUnits, "staked cooldown units").to.eq(0) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(0) + const newWeightedTimestamp = calcWeightedTimestamp(stakedTimestamp, withdrawTimestamp, stakedAmount, stakedAmount, false) + expect(afterData.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(newWeightedTimestamp) + expect(afterData.questBalance.lastAction, "last action after").to.eq(0) + expect(afterData.rewardTokenBalance, "staker rewards after").to.eq(startingMintAmount.sub(redemptionFee)) }) }) context("interacting from a smart contract", () => { @@ -1161,6 +1296,9 @@ describe("Staked Token", () => { }) }) }) + context("when there is a priceCoeff but no overload", () => { + it("should default to 10000") + }) // '''..................................................................''' // '''................... STAKEDTOKEN.ADMIN ......................''' @@ -1215,6 +1353,8 @@ describe("Staked Token", () => { expect(await safetyDataAfter.collateralisationRatio, "collateralisation ratio after").to.eq( simpleToExactAmount(1).sub(slashingPercentage), ) + + // TODO - withdrawal should return 75% }) context("should not allow", () => { const slashingPercentage = simpleToExactAmount(10, 16) @@ -1257,18 +1397,40 @@ describe("Staked Token", () => { // '''................. QUESTING & MULTIPLIERS ...................''' // '''..................................................................''' + // TODO - test startTime and seasonEpoch context("questManager", () => { - it("should allow admin to add stakingtokens") - }) - - context("questing and multipliers", () => { - const stakedAmount = simpleToExactAmount(5000) - before(async () => { - ;({ stakedToken, questManager } = await redeployStakedToken()) - await rewardToken.connect(sa.default.signer).approve(stakedToken.address, simpleToExactAmount(10000)) - await stakedToken["stake(uint256,address)"](stakedAmount, sa.default.address) + context("adding staked token", () => { + before(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) + }) + it("should fail if address 0", async () => { + const tx = questManager.connect(sa.governor.signer).addStakedToken(ZERO_ADDRESS) + await expect(tx).to.revertedWith("Invalid StakedToken") + }) + it("should fail if not governor", async () => { + const tx = questManager.addStakedToken(sa.mockInterestValidator.address) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should fail if quest master", async () => { + const tx = questManager.connect(sa.questMaster.signer).addStakedToken(sa.mockInterestValidator.address) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should fail if quest signer", async () => { + const tx = questManager.connect(sa.questSigner.signer).addStakedToken(sa.mockInterestValidator.address) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should allow governor to add staked token", async () => { + const tx = await questManager.connect(sa.governor.signer).addStakedToken(sa.mockInterestValidator.address) + await expect(tx).to.emit(questManager, "StakedTokenAdded").withArgs(sa.mockInterestValidator.address) + }) }) context("add quest", () => { + const stakedAmount = simpleToExactAmount(5000) + before(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) + await rewardToken.connect(sa.default.signer).approve(stakedToken.address, simpleToExactAmount(10000)) + await stakedToken["stake(uint256,address)"](stakedAmount, sa.default.address) + }) let id = 0 it("should allow governor to add a seasonal quest", async () => { const multiplier = 20 // 1.2x @@ -1340,6 +1502,7 @@ describe("Staked Token", () => { context("expire quest", () => { let expiry: BN before(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) expiry = deployTime.add(ONE_WEEK.mul(12)) }) it("should allow governor to expire a seasonal quest", async () => { @@ -1352,12 +1515,11 @@ describe("Staked Token", () => { await expect(tx).to.emit(questManager, "QuestExpired").withArgs(id) const quest = await questManager.getQuest(id) - expect(quest.status).to.eq(QuestStatus.EXPIRED) - expect(quest.expiry).to.lt(expiry) - expect(quest.expiry).to.eq(currentTime.add(1)) + expect(quest.status, "status after").to.eq(QuestStatus.EXPIRED) + expect(quest.expiry, "expiry after").to.eq(currentTime.add(1)) }) - it("should allow governor to expire a permanent quest", async () => { - const tx0 = await questManager.connect(sa.governor.signer).addQuest(QuestType.PERMANENT, 10, expiry) + it("should allow quest master to expire a permanent quest", async () => { + const tx0 = await questManager.connect(sa.questMaster.signer).addQuest(QuestType.PERMANENT, 10, expiry) const receipt = await tx0.wait() const { id } = receipt.events[0].args const currentTime = await getTimestamp() @@ -1366,13 +1528,38 @@ describe("Staked Token", () => { await expect(tx).to.emit(questManager, "QuestExpired").withArgs(id) const quest = await questManager.getQuest(id) - expect(quest.status).to.eq(QuestStatus.EXPIRED) - expect(quest.expiry).to.lt(expiry) - expect(quest.expiry).to.eq(currentTime.add(1)) + expect(quest.status, "status after").to.eq(QuestStatus.EXPIRED) + expect(quest.expiry, "expiry after").to.eq(currentTime.add(1)) + }) + it("expired quest can no longer be completed", async () => { + const tx0 = await questManager.connect(sa.questMaster.signer).addQuest(QuestType.PERMANENT, 10, expiry) + const receipt = await tx0.wait() + const { id } = receipt.events[0].args + await questManager.connect(sa.governor.signer).expireQuest(id) + + const signature = await signUserQuests(sa.dummy1.address, [id], sa.questSigner.signer) + const tx = questManager.connect(sa.default.signer).completeUserQuests(sa.dummy1.address, [id], signature) + await expect(tx).revertedWith("Invalid Quest ID") + }) + it("should expire quest after expiry", async () => { + const tx0 = await questManager.connect(sa.governor.signer).addQuest(QuestType.PERMANENT, 5, expiry) + const receipt = await tx0.wait() + const { id } = receipt.events[0].args + await increaseTime(ONE_WEEK.mul(13)) + + const tx = await questManager.connect(sa.governor.signer).expireQuest(id) + + await expect(tx).to.emit(questManager, "QuestExpired").withArgs(id) + + const quest = await questManager.getQuest(id) + expect(quest.status, "status after").to.eq(QuestStatus.EXPIRED) + expect(quest.expiry, "expiry after").to.eq(expiry) }) context("should fail to expire quest", () => { let id: number before(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) + expiry = deployTime.add(ONE_WEEK.mul(12)) const tx = await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 10, expiry) const receipt = await tx.wait() id = receipt.events[0].args.id @@ -1380,6 +1567,9 @@ describe("Staked Token", () => { it("from deployer", async () => { await expect(questManager.expireQuest(id)).to.revertedWith("Not verified") }) + it("from quest signer", async () => { + await expect(questManager.connect(sa.questSigner.signer).expireQuest(id)).to.revertedWith("Not verified") + }) it("with id does not exists", async () => { await expect(questManager.connect(sa.governor.signer).expireQuest(id + 1)).to.revertedWith("Quest does not exist") }) @@ -1388,14 +1578,16 @@ describe("Staked Token", () => { await expect(questManager.connect(sa.governor.signer).expireQuest(id)).to.revertedWith("Quest already expired") }) }) - it("expired quest can no longer be completed") }) context("start season", () => { - before(async () => { + beforeEach(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) const expiry = deployTime.add(ONE_WEEK.mul(12)) await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 10, expiry) - await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 10, expiry) - expect(await questManager.seasonEpoch(), "season epoch before").to.gt(deployTime) + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 11, expiry) + await questManager.connect(sa.governor.signer).addQuest(QuestType.PERMANENT, 12, deployTime.add(ONE_WEEK.mul(50))) + expect(await questManager.startTime(), "season epoch before").to.gt(deployTime) + expect(await questManager.seasonEpoch(), "season epoch before").to.eq(0) }) it("should allow governor to start season after 39 weeks", async () => { await increaseTime(ONE_WEEK.mul(39).add(60)) @@ -1406,27 +1598,90 @@ describe("Staked Token", () => { }) context("should fail to start season", () => { it("from deployer", async () => { - await expect(questManager.startNewQuestSeason()).to.revertedWith("Not verified") + const tx = questManager.startNewQuestSeason() + await expect(tx).to.revertedWith("Not verified") + }) + it("should fail if called within 39 weeks of the startTime", async () => { + await increaseTime(ONE_WEEK.mul(39).sub(60)) + const tx = questManager.connect(sa.governor.signer).startNewQuestSeason() + await expect(tx).revertedWith("First season has not elapsed") }) it("before 39 week from last season", async () => { + await increaseTime(ONE_WEEK.mul(39).add(60)) + await questManager.connect(sa.governor.signer).startNewQuestSeason() + const newSeasonStart = await getTimestamp() + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 10, newSeasonStart.add(ONE_WEEK.mul(39))) + await increaseTime(ONE_WEEK.mul(39).sub(60)) - await expect(questManager.connect(sa.governor.signer).startNewQuestSeason()).to.revertedWith("Season has not elapsed") + const tx = questManager.connect(sa.governor.signer).startNewQuestSeason() + await expect(tx).to.revertedWith("Season has not elapsed") + }) + it("if there are still active quests", async () => { + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 12, deployTime.add(ONE_WEEK.mul(40))) + await increaseTime(ONE_WEEK.mul(39).add(60)) + const tx = questManager.connect(sa.governor.signer).startNewQuestSeason() + await expect(tx).to.revertedWith("All seasonal quests must have expired") }) }) }) - context("complete quests", () => { + context("questMaster", () => { + beforeEach(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) + expect(await questManager.questMaster(), "quest master before").to.eq(sa.questMaster.address) + }) + it("should set questMaster by governor", async () => { + const tx = await questManager.connect(sa.governor.signer).setQuestMaster(sa.dummy1.address) + await expect(tx).to.emit(questManager, "QuestMaster").withArgs(sa.questMaster.address, sa.dummy1.address) + expect(await questManager.questMaster(), "quest master after").to.eq(sa.dummy1.address) + }) + it("should set questMaster by quest master", async () => { + const tx = await questManager.connect(sa.questMaster.signer).setQuestMaster(sa.dummy2.address) + await expect(tx).to.emit(questManager, "QuestMaster").withArgs(sa.questMaster.address, sa.dummy2.address) + expect(await questManager.questMaster(), "quest master after").to.eq(sa.dummy2.address) + }) + it("should fail to set quest master by anyone", async () => { + await expect(questManager.connect(sa.dummy3.signer).setQuestMaster(sa.dummy3.address)).to.revertedWith("Not verified") + }) + }) + context("questSigner", () => { + beforeEach(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) + }) + it("should set quest signer by governor", async () => { + const tx = await questManager.connect(sa.governor.signer).setQuestSigner(sa.dummy1.address) + await expect(tx).to.emit(questManager, "QuestSigner").withArgs(sa.questSigner.address, sa.dummy1.address) + }) + it("should fail to set quest signer by quest master", async () => { + await expect(questManager.connect(sa.questMaster.signer).setQuestSigner(sa.dummy3.address)).to.revertedWith( + "Only governor can execute", + ) + }) + it("should fail to set quest signer by anyone", async () => { + await expect(questManager.connect(sa.dummy3.signer).setQuestSigner(sa.dummy3.address)).to.revertedWith( + "Only governor can execute", + ) + }) + }) + }) + + context("questing and multipliers", () => { + context("complete user quests", () => { let stakedTime let permanentQuestId: BN let seasonQuestId: BN const permanentMultiplier = 10 const seasonMultiplier = 20 + const stakedAmount = simpleToExactAmount(5000) beforeEach(async () => { ;({ stakedToken, questManager } = await redeployStakedToken()) await rewardToken.connect(sa.default.signer).approve(stakedToken.address, simpleToExactAmount(10000)) await stakedToken["stake(uint256,address)"](stakedAmount, sa.default.address) - stakedTime = await getTimestamp() - const expiry = stakedTime.add(ONE_WEEK.mul(12)) + + await increaseTime(ONE_WEEK.mul(39).add(1)) + await questManager.connect(sa.governor.signer).startNewQuestSeason() + + const expiry = (await getTimestamp()).add(ONE_WEEK.mul(25)) await questManager.connect(sa.governor.signer).addQuest(QuestType.PERMANENT, permanentMultiplier, expiry) const tx = await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, seasonMultiplier, expiry) const receipt = await tx.wait() @@ -1434,142 +1689,349 @@ describe("Staked Token", () => { permanentQuestId = seasonQuestId.sub(1) await increaseTime(ONE_DAY) }) - it("should allow quest signer to complete a user's seasonal quest", async () => { - const userAddress = sa.default.address - expect(await questManager.hasCompleted(userAddress, seasonQuestId), "quest completed before").to.be.false - - // Complete User Season Quest - const signature = await signUserQuests(userAddress, [seasonQuestId], sa.questSigner.signer) - const tx = await questManager.connect(sa.default.signer).completeUserQuests(userAddress, [seasonQuestId], signature) - - // Check events - await expect(tx).to.emit(questManager, "QuestCompleteQuests").withArgs(userAddress, [seasonQuestId]) - - // Check data - expect(await questManager.hasCompleted(userAddress, seasonQuestId), "quest completed after").to.be.true - const userDataAfter = await snapshotUserStakingData(userAddress) - expect(userDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(userDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(userDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(userDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTime) - expect(userDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTime) - expect(userDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) - expect(userDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(seasonMultiplier) - expect(userDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - const expectedBalance = stakedAmount.mul(100 + seasonMultiplier).div(100) - expect(userDataAfter.stakedBalance, "staked balance after").to.eq(expectedBalance) - expect(userDataAfter.votes, "votes after").to.eq(expectedBalance) - }) - it("should allow quest signer to complete a user's permanent quest", async () => { - const userAddress = sa.default.address - expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed before").to.be.false - - // Complete User Permanent Quest - const signature = await signUserQuests(userAddress, [permanentQuestId], sa.questSigner.signer) - const tx = await questManager.connect(sa.questSigner.signer).completeUserQuests(userAddress, [permanentQuestId], signature) - - // Check events - await expect(tx).to.emit(questManager, "QuestCompleteQuests").withArgs(userAddress, [permanentQuestId]) - // Check data - expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed after").to.be.true - const userDataAfter = await snapshotUserStakingData(userAddress) - expect(userDataAfter.userBalances.cooldownTimestamp, "cooldown timestamp after").to.eq(0) - expect(userDataAfter.userBalances.cooldownUnits, "cooldown units after").to.eq(0) - expect(userDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - expect(userDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTime) - expect(userDataAfter.questBalance.lastAction, "last action after").to.eq(stakedTime) - expect(userDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(permanentMultiplier) - expect(userDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) - expect(userDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - const expectedBalance = stakedAmount.mul(100 + permanentMultiplier).div(100) - expect(userDataAfter.stakedBalance, "staked balance after").to.eq(expectedBalance) - expect(userDataAfter.votes, "votes after").to.eq(expectedBalance) - }) - it("should complete user quest before a user stakes", async () => { - const userAddress = sa.dummy1.address - expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed before").to.be.false - - // Complete User Permanent and Seasonal Quests - const signature = await signUserQuests(userAddress, [permanentQuestId, seasonQuestId], sa.questSigner.signer) - const tx = await questManager - .connect(sa.questSigner.signer) - .completeUserQuests(userAddress, [permanentQuestId, seasonQuestId], signature) - - const completeQuestTimestamp = await getTimestamp() - - // Check events - await expect(tx).to.emit(questManager, "QuestCompleteQuests").withArgs(userAddress, [permanentQuestId, seasonQuestId]) - - // Check data - expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed after").to.be.true - const userDataAfter = await snapshotUserStakingData(userAddress) - expect(userDataAfter.userBalances.raw, "staked raw balance after").to.eq(0) - expect(userDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(0) - expect(userDataAfter.questBalance.lastAction, "last action after").to.eq(completeQuestTimestamp) - expect(userDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(permanentMultiplier) - expect(userDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(seasonMultiplier) - expect(userDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(0) - expect(userDataAfter.stakedBalance, "staked balance after").to.eq(0) - expect(userDataAfter.votes, "votes after").to.eq(0) - }) - it("should complete a quest for 4 users", async () => { - const user1Address = sa.dummy1.address - const user2Address = sa.dummy2.address - const user3Address = sa.dummy3.address - const user4Address = sa.dummy4.address - expect(await questManager.hasCompleted(user1Address, permanentQuestId), "user 1 quest completed before").to.be.false - expect(await questManager.hasCompleted(user2Address, permanentQuestId), "user 2 quest completed before").to.be.false - expect(await questManager.hasCompleted(user3Address, permanentQuestId), "user 3 quest completed before").to.be.false - expect(await questManager.hasCompleted(user4Address, permanentQuestId), "user 4 quest completed before").to.be.false - - // Complete User Permanent and Seasonal Quests - const signature = await signQuestUsers( - permanentQuestId, - [user1Address, user2Address, user3Address, user4Address], - sa.questSigner.signer, - ) - const tx = await questManager - .connect(sa.questSigner.signer) - .completeQuestUsers(permanentQuestId, [user1Address, user2Address, user3Address, user4Address], signature) + context("complete multiple users of a quest", () => { + const tests: ("permanent" | "season")[] = ["permanent", "season"] + tests.forEach((questType, i) => { + it(`should complete ${questType} quest for 4 users`, async () => { + // the second quest is the season quest id + const questId = permanentQuestId.add(i) + const user1Address = sa.dummy1.address + const user2Address = sa.dummy2.address + const user3Address = sa.dummy3.address + const user4Address = sa.dummy4.address + expect(await questManager.hasCompleted(user1Address, questId), "user 1 quest not completed before").to.be.false + expect(await questManager.hasCompleted(user2Address, questId), "user 2 quest not completed before").to.be.false + expect(await questManager.hasCompleted(user3Address, questId), "user 3 quest not completed before").to.be.false + expect(await questManager.hasCompleted(user4Address, questId), "user 4 quest not completed before").to.be.false + + // Complete quests + const signature = await signQuestUsers( + questId, + [user1Address, user2Address, user3Address, user4Address], + sa.questSigner.signer, + ) + const tx = await questManager + .connect(sa.questSigner.signer) + .completeQuestUsers(questId, [user1Address, user2Address, user3Address, user4Address], signature) + + const completeQuestTimestamp = await getTimestamp() + + // Check events + await expect(tx) + .to.emit(questManager, "QuestCompleteUsers") + .withArgs(questId, [user1Address, user2Address, user3Address, user4Address]) + + // Check data + expect(await questManager.hasCompleted(user1Address, questId), "user 1 quest completed after").to.be.true + expect(await questManager.hasCompleted(user2Address, questId), "user 2 quest completed after").to.be.true + // User 1 + const user1DataAfter = await snapshotUserStakingData(user1Address) + expect(user1DataAfter.questBalance.lastAction, "user 1 last action after").to.eq(completeQuestTimestamp) + if (questType === "permanent") { + expect(user1DataAfter.questBalance.permMultiplier, "user 1 perm multiplier after").to.eq(permanentMultiplier) + expect(user1DataAfter.questBalance.seasonMultiplier, "user 1 season multiplier after").to.eq(0) + } else { + expect(user1DataAfter.questBalance.permMultiplier, "user 1 perm multiplier after").to.eq(0) + expect(user1DataAfter.questBalance.seasonMultiplier, "user 1 season multiplier after").to.eq(seasonMultiplier) + } + // User 2 + const user2DataAfter = await snapshotUserStakingData(user2Address) + expect(user2DataAfter.questBalance.lastAction, "user 2 last action after").to.eq(completeQuestTimestamp) + if (questType === "permanent") { + expect(user2DataAfter.questBalance.permMultiplier, "user 2 perm multiplier after").to.eq(permanentMultiplier) + expect(user2DataAfter.questBalance.seasonMultiplier, "user 2 season multiplier after").to.eq(0) + } else { + expect(user2DataAfter.questBalance.permMultiplier, "user 2 perm multiplier after").to.eq(0) + expect(user2DataAfter.questBalance.seasonMultiplier, "user 2 season multiplier after").to.eq(seasonMultiplier) + } + }) + }) + it("should complete quest before stake", async () => { + const userAddress = sa.dummy1.address + expect(await questManager.hasCompleted(userAddress, seasonQuestId), "user quest not completed before").to.be.false - const completeQuestTimestamp = await getTimestamp() + // Complete quests + const signature = await signQuestUsers(seasonQuestId, [userAddress], sa.questSigner.signer) + const tx = await questManager.connect(sa.questSigner.signer).completeQuestUsers(seasonQuestId, [userAddress], signature) - // Check events - await expect(tx) - .to.emit(questManager, "QuestCompleteUsers") - .withArgs(permanentQuestId, [user1Address, user2Address, user3Address, user4Address]) - - // Check data - expect(await questManager.hasCompleted(user1Address, permanentQuestId), "user 1 quest completed after").to.be.true - expect(await questManager.hasCompleted(user2Address, permanentQuestId), "user 2 quest completed after").to.be.true - const user1DataAfter = await snapshotUserStakingData(user1Address) - expect(user1DataAfter.questBalance.lastAction, "user 1 last action after").to.eq(completeQuestTimestamp) - expect(user1DataAfter.questBalance.permMultiplier, "user 1 perm multiplier after").to.eq(permanentMultiplier) - expect(user1DataAfter.questBalance.seasonMultiplier, "user 1 season multiplier after").to.eq(0) - const user2DataAfter = await snapshotUserStakingData(user2Address) - expect(user2DataAfter.questBalance.lastAction, "user 2 last action after").to.eq(completeQuestTimestamp) - expect(user2DataAfter.questBalance.permMultiplier, "user 2 perm multiplier after").to.eq(permanentMultiplier) - expect(user2DataAfter.questBalance.seasonMultiplier, "user 2 season multiplier after").to.eq(0) - }) - it("should fail to complete a user quest again", async () => { - const userAddress = sa.dummy2.address - const signature = await signUserQuests(userAddress, [permanentQuestId], sa.questSigner.signer) - await questManager.connect(sa.questSigner.signer).completeUserQuests(userAddress, [permanentQuestId], signature) - await expect( - questManager.connect(sa.questSigner.signer).completeUserQuests(userAddress, [permanentQuestId], signature), - ).to.revertedWith("Err: Already Completed") - }) - it("should fail a user signing quest completion", async () => { - const userAddress = sa.dummy3.address - const signature = await signUserQuests(userAddress, [permanentQuestId], sa.dummy3.signer) - await expect( - questManager.connect(sa.dummy3.signer).completeUserQuests(userAddress, [permanentQuestId], signature), - ).to.revertedWith("Invalid Quest Signer Signature") + const completeQuestTimestamp = await getTimestamp() + + // Check events + await expect(tx).to.emit(questManager, "QuestCompleteUsers").withArgs(seasonQuestId, [userAddress]) + + expect(await questManager.hasCompleted(userAddress, seasonQuestId), "user quest completed after").to.be.true + + // User data after quest complete + const afterCompleteData = await snapshotUserStakingData(userAddress) + expect(afterCompleteData.rawBalance.raw, "staked raw balance after quest complete").to.eq(0) + expect(afterCompleteData.rawBalance.weightedTimestamp, "weighted timestamp after quest complete").to.eq(0) + expect(afterCompleteData.questBalance.lastAction, "last action after quest complete").to.eq(completeQuestTimestamp) + expect(afterCompleteData.questBalance.permMultiplier, "perm multiplier after quest complete").to.eq(0) + expect(afterCompleteData.questBalance.seasonMultiplier, "season multiplier after quest complete").to.eq( + seasonMultiplier, + ) + expect(afterCompleteData.rawBalance.timeMultiplier, "time multiplier after quest complete").to.eq(0) + expect(afterCompleteData.scaledBalance, "staked balance after quest complete").to.eq(0) + expect(afterCompleteData.votes, "staker votes after quest complete").to.eq(0) + + await increaseTime(ONE_WEEK) + + await rewardToken.transfer(userAddress, stakedAmount) + await rewardToken.connect(sa.dummy1.signer).approve(stakedToken.address, simpleToExactAmount(10000)) + await stakedToken.connect(sa.dummy1.signer)["stake(uint256)"](stakedAmount) + + const stakedTimestamp = await getTimestamp() + + // User data after quest complete + const afterStakeData = await snapshotUserStakingData(userAddress) + expect(afterStakeData.rawBalance.raw, "staked raw balance after stake").to.eq(stakedAmount) + expect(afterStakeData.rawBalance.weightedTimestamp, "weighted timestamp after stake").to.eq(stakedTimestamp) + expect(afterStakeData.questBalance.lastAction, "last action after stake").to.eq(completeQuestTimestamp) + expect(afterStakeData.questBalance.permMultiplier, "perm multiplier after stake").to.eq(0) + expect(afterStakeData.questBalance.seasonMultiplier, "season multiplier after stake").to.eq(seasonMultiplier) + expect(afterStakeData.rawBalance.timeMultiplier, "time multiplier after stake").to.eq(0) + const votesExpected = stakedAmount.mul(120).div(100) + expect(afterStakeData.scaledBalance, "staked balance after stake").to.eq(votesExpected) + expect(afterStakeData.votes, "staker votes after stake").to.eq(votesExpected) + }) + it("should update quest & time multiplier", async () => { + const userAddress = sa.dummy1.address + + await rewardToken.transfer(userAddress, stakedAmount) + await rewardToken.connect(sa.dummy1.signer).approve(stakedToken.address, stakedAmount) + await stakedToken.connect(sa.dummy1.signer)["stake(uint256)"](stakedAmount) + const stakedTimestamp = await getTimestamp() + + const newSeasonMultiplier = 50 + const tx = await questManager + .connect(sa.governor.signer) + .addQuest(QuestType.SEASONAL, newSeasonMultiplier, stakedTimestamp.add(ONE_WEEK.mul(20))) + const receipt = await tx.wait() + const newSeasonQuestId = receipt.events[0].args.id + + // increase time into the first time multiplier + await increaseTime(ONE_WEEK.mul(14)) + + // Complete permanent quest + const signature = await signQuestUsers(newSeasonQuestId, [userAddress], sa.questSigner.signer) + await questManager.connect(sa.questSigner.signer).completeQuestUsers(newSeasonQuestId, [userAddress], signature) + + const afterData = await snapshotUserStakingData(userAddress) + expect(afterData.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(afterData.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(afterData.questBalance.lastAction, "last action after").to.eq(stakedTimestamp) + expect(afterData.questBalance.permMultiplier, "perm multiplier after").to.eq(0) + expect(afterData.questBalance.seasonMultiplier, "season multiplier after").to.eq(50) + expect(afterData.rawBalance.timeMultiplier, "time multiplier after").to.eq(20) + const votesExpected = stakedAmount.mul(120).mul(150).div(10000) + expect(afterData.scaledBalance, "staked balance after").to.eq(votesExpected) + expect(afterData.votes, "staker votes after").to.eq(votesExpected) + }) + context("should fail", () => { + let userAddress: string + before(async () => { + userAddress = sa.dummy3.address + }) + it("user signing own quest completion", async () => { + const signature = await signQuestUsers(permanentQuestId, [userAddress], sa.dummy3.signer) + const tx = questManager + .connect(sa.questSigner.signer) + .completeQuestUsers(permanentQuestId, [userAddress], signature) + await expect(tx).to.revertedWith("Invalid Quest Signer Signature") + }) + it("signature with a different quest id", async () => { + const signature = await signQuestUsers(permanentQuestId, [userAddress], sa.dummy3.signer) + const tx = questManager.completeQuestUsers(seasonQuestId, [userAddress], signature) + await expect(tx).to.revertedWith("Invalid Quest Signer Signature") + }) + it("signature with a different user", async () => { + const signature = await signQuestUsers(permanentQuestId, [userAddress], sa.questSigner.signer) + const tx = questManager.completeQuestUsers(permanentQuestId, [sa.dummy4.address], signature) + await expect(tx).to.revertedWith("Invalid Quest Signer Signature") + }) + it("an invalid quest ID", async () => { + const signature = await signQuestUsers(seasonQuestId.add(1), [userAddress], sa.questSigner.signer) + const tx = questManager.completeQuestUsers(seasonQuestId.add(1), [userAddress], signature) + await expect(tx).to.revertedWith("Invalid Quest ID") + }) + it("no user accounts", async () => { + const signature = await signQuestUsers(seasonQuestId, [userAddress], sa.questSigner.signer) + const tx = questManager.completeQuestUsers(seasonQuestId, [], signature) + await expect(tx).to.revertedWith("No accounts") + }) + it("already completed quest", async () => { + const signature = await signQuestUsers(seasonQuestId, [userAddress], sa.questSigner.signer) + await questManager.completeQuestUsers(seasonQuestId, [userAddress], signature) + + const tx = questManager.completeQuestUsers(seasonQuestId, [userAddress], signature) + await expect(tx).to.revertedWith("Quest already completed") + }) + }) + }) + context("complete multiple quests for a user", () => { + it("should allow quest signer to complete a user's seasonal quest", async () => { + const userAddress = sa.default.address + expect(await questManager.hasCompleted(userAddress, seasonQuestId), "quest completed before").to.be.false + + // Complete User Season Quest + const signature = await signUserQuests(userAddress, [seasonQuestId], sa.questSigner.signer) + const tx = await questManager.connect(sa.default.signer).completeUserQuests(userAddress, [seasonQuestId], signature) + + const completeTime = await getTimestamp() + // Check events + await expect(tx).to.emit(questManager, "QuestCompleteQuests").withArgs(userAddress, [seasonQuestId]) + + // Check data + expect(await questManager.hasCompleted(userAddress, seasonQuestId), "quest completed after").to.be.true + const userDataAfter = await snapshotUserStakingData(userAddress) + expect(userDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(userDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(userDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(userDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTime) + expect(userDataAfter.questBalance.lastAction, "last action after").to.eq(completeTime) + expect(userDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(0) + expect(userDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(seasonMultiplier) + expect(userDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(30) + const expectedBalance = stakedAmount + .mul(100 + seasonMultiplier) + .div(100) + .mul(130) + .div(100) + expect(userDataAfter.scaledBalance, "staked balance after").to.eq(expectedBalance) + expect(userDataAfter.votes, "votes after").to.eq(expectedBalance) + }) + it("should allow quest signer to complete a user's permanent quest", async () => { + const userAddress = sa.default.address + expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed before").to.be.false + + // Complete User Permanent Quest + const signature = await signUserQuests(userAddress, [permanentQuestId], sa.questSigner.signer) + const tx = await questManager + .connect(sa.questSigner.signer) + .completeUserQuests(userAddress, [permanentQuestId], signature) + const completeTime = await getTimestamp() + + // Check events + await expect(tx).to.emit(questManager, "QuestCompleteQuests").withArgs(userAddress, [permanentQuestId]) + // Check data + expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed after").to.be.true + const userDataAfter = await snapshotUserStakingData(userAddress) + expect(userDataAfter.rawBalance.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(userDataAfter.rawBalance.cooldownUnits, "cooldown units after").to.eq(0) + expect(userDataAfter.rawBalance.raw, "staked raw balance after").to.eq(stakedAmount) + expect(userDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(stakedTime) + expect(userDataAfter.questBalance.lastAction, "last action after").to.eq(completeTime) + expect(userDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(permanentMultiplier) + expect(userDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(0) + expect(userDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(30) + const expectedBalance = stakedAmount + .mul(100 + permanentMultiplier) + .div(100) + .mul(130) + .div(100) + expect(userDataAfter.scaledBalance, "staked balance after").to.eq(expectedBalance) + expect(userDataAfter.votes, "votes after").to.eq(expectedBalance) + }) + it("should complete user quest before a user stakes", async () => { + const userAddress = sa.dummy1.address + expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed before").to.be.false + + // Complete User Permanent and Seasonal Quests + const signature = await signUserQuests(userAddress, [permanentQuestId, seasonQuestId], sa.questSigner.signer) + const tx = await questManager + .connect(sa.questSigner.signer) + .completeUserQuests(userAddress, [permanentQuestId, seasonQuestId], signature) + + const completeQuestTimestamp = await getTimestamp() + + // Check events + await expect(tx).to.emit(questManager, "QuestCompleteQuests").withArgs(userAddress, [permanentQuestId, seasonQuestId]) + + // Check data + expect(await questManager.hasCompleted(userAddress, permanentQuestId), "quest completed after").to.be.true + const userDataAfter = await snapshotUserStakingData(userAddress) + expect(userDataAfter.rawBalance.raw, "staked raw balance after").to.eq(0) + expect(userDataAfter.rawBalance.weightedTimestamp, "weighted timestamp after").to.eq(0) + expect(userDataAfter.questBalance.lastAction, "last action after").to.eq(completeQuestTimestamp) + expect(userDataAfter.questBalance.permMultiplier, "perm multiplier after").to.eq(permanentMultiplier) + expect(userDataAfter.questBalance.seasonMultiplier, "season multiplier after").to.eq(seasonMultiplier) + expect(userDataAfter.rawBalance.timeMultiplier, "time multiplier after").to.eq(0) + expect(userDataAfter.scaledBalance, "staked balance after").to.eq(0) + expect(userDataAfter.votes, "votes after").to.eq(0) + }) + context("should fail", () => { + let userAddress: string + before(async () => { + userAddress = sa.dummy3.address + }) + it("user signing own quest completion", async () => { + const signature = await signUserQuests(userAddress, [permanentQuestId], sa.dummy3.signer) + const tx = questManager.connect(sa.dummy3.signer).completeUserQuests(userAddress, [permanentQuestId], signature) + await expect(tx).to.revertedWith("Invalid Quest Signer Signature") + }) + it("signature with a different quest id", async () => { + const signature = await signUserQuests(userAddress, [permanentQuestId], sa.questSigner.signer) + const tx = questManager.completeUserQuests(userAddress, [seasonQuestId], signature) + await expect(tx).to.revertedWith("Invalid Quest Signer Signature") + }) + it("signature with a different user", async () => { + const signature = await signUserQuests(sa.dummy4.address, [permanentQuestId], sa.questSigner.signer) + const tx = questManager.completeUserQuests(userAddress, [permanentQuestId], signature) + await expect(tx).to.revertedWith("Invalid Quest Signer Signature") + }) + it("invalid quest ID", async () => { + const signature = await signUserQuests(userAddress, [seasonQuestId.add(1)], sa.questSigner.signer) + const tx = questManager.completeUserQuests(userAddress, [seasonQuestId.add(1)], signature) + await expect(tx).to.revertedWith("Invalid Quest ID") + }) + it("no quest IDs", async () => { + const signature = await signUserQuests(userAddress, [seasonQuestId], sa.questSigner.signer) + const tx = questManager.completeUserQuests(userAddress, [], signature) + await expect(tx).to.revertedWith("No quest IDs") + }) + it("already completed quest", async () => { + const signature = await signUserQuests(userAddress, [seasonQuestId], sa.questSigner.signer) + await questManager.completeUserQuests(userAddress, [seasonQuestId], signature) + + const tx = questManager.completeUserQuests(userAddress, [seasonQuestId], signature) + await expect(tx).to.revertedWith("Quest already completed") + }) + // NOTE - permMultiplier and seasonMultiplier are uint8 so max user multiplier is 2.55x + it("if user's multiplier over 2.5x", async () => { + const currentTime = await getTimestamp() + const expiry = currentTime.add(ONE_WEEK) + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 50, expiry) + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 50, expiry) + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 50, expiry) + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 50, expiry) + await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 50, expiry) + const tx1 = await questManager.connect(sa.governor.signer).addQuest(QuestType.SEASONAL, 6, expiry) + const receipt = await tx1.wait() + const lastQuestId = receipt.events[0].args.id + + const completedQuests = [ + lastQuestId, + lastQuestId.sub(1), + lastQuestId.sub(2), + lastQuestId.sub(3), + lastQuestId.sub(4), + lastQuestId.sub(5), + ] + const signature = await signUserQuests(userAddress, completedQuests, sa.questSigner.signer) + const tx2 = questManager.completeUserQuests(userAddress, completedQuests, signature) + await expect(tx2).to.revertedWith( + "reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)", + ) + }) + }) + // TODO - for both types of completion + it("should propagate quest completion to all stakedTokens", async () => {}) }) }) context("time multiplier", () => { let stakerDataBefore: UserStakingData let anySigner: Signer + const stakedAmount = simpleToExactAmount(5000) beforeEach(async () => { ;({ stakedToken, questManager } = await redeployStakedToken()) await rewardToken.connect(sa.default.signer).approve(stakedToken.address, simpleToExactAmount(10000)) @@ -1579,10 +2041,10 @@ describe("Staked Token", () => { }) it("staker data just after stake", async () => { stakerDataBefore = await snapshotUserStakingData(sa.default.address) - expect(stakerDataBefore.userBalances.timeMultiplier).to.eq(0) - expect(stakerDataBefore.userBalances.raw).to.eq(stakedAmount) + expect(stakerDataBefore.rawBalance.timeMultiplier).to.eq(0) + expect(stakerDataBefore.rawBalance.raw).to.eq(stakedAmount) expect(stakerDataBefore.votes).to.eq(stakedAmount) - expect(stakerDataBefore.stakedBalance).to.eq(stakedAmount) + expect(stakerDataBefore.scaledBalance).to.eq(stakedAmount) }) const runs = [ { weeks: 13, multiplierBefore: BN.from(0), multiplierAfter: BN.from(20) }, @@ -1605,12 +2067,12 @@ describe("Staked Token", () => { } const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.timeMultiplier, "timeMultiplier after").to.eq(run.multiplierBefore) - expect(stakerDataAfter.userBalances.raw, "raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "timeMultiplier after").to.eq(run.multiplierBefore) + expect(stakerDataAfter.rawBalance.raw, "raw balance after").to.eq(stakedAmount) // balance = staked amount * (100 + time multiplier) / 100 const expectedBalance = stakedAmount.mul(run.multiplierBefore.add(100)).div(100) expect(stakerDataAfter.votes, "votes after").to.eq(expectedBalance) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(expectedBalance) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(expectedBalance) }) it(`anyone can review timestamp after ${run.weeks} weeks`, async () => { await increaseTime(ONE_WEEK.mul(run.weeks).add(60)) @@ -1618,12 +2080,12 @@ describe("Staked Token", () => { await stakedToken.connect(anySigner).reviewTimestamp(sa.default.address) const stakerDataAfter = await snapshotUserStakingData(sa.default.address) - expect(stakerDataAfter.userBalances.timeMultiplier, "timeMultiplier after").to.eq(run.multiplierAfter) - expect(stakerDataAfter.userBalances.raw, "raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.rawBalance.timeMultiplier, "timeMultiplier after").to.eq(run.multiplierAfter) + expect(stakerDataAfter.rawBalance.raw, "raw balance after").to.eq(stakedAmount) // balance = staked amount * (100 + time multiplier) / 100 const expectedBalance = stakedAmount.mul(run.multiplierAfter.add(100)).div(100) expect(stakerDataAfter.votes, "votes after").to.eq(expectedBalance) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(expectedBalance) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(expectedBalance) }) }) }) @@ -1634,6 +2096,7 @@ describe("Staked Token", () => { { type: QuestType.SEASONAL, multiplier: 5, weeks: 6 }, { type: QuestType.SEASONAL, multiplier: 8, weeks: 10 }, ] + const stakedAmount = simpleToExactAmount(5000) beforeEach(async () => { ;({ stakedToken, questManager } = await redeployStakedToken()) @@ -1846,71 +2309,488 @@ describe("Staked Token", () => { const balanceExpected = questBalanceExpected.mul(timeMultiplierExpected.add(100)).div(100) const stakerDataAfter = await snapshotUserStakingData(user) - expect(stakerDataAfter.userBalances.timeMultiplier, "timeMultiplier After").to.eq(timeMultiplierExpected) + expect(stakerDataAfter.rawBalance.timeMultiplier, "timeMultiplier After").to.eq(timeMultiplierExpected) expect(stakerDataAfter.questBalance.permMultiplier, "permMultiplier After").to.eq(permMultiplierExpected) expect(stakerDataAfter.questBalance.seasonMultiplier, "seasonMultiplier After").to.eq(seasonMultiplierExpected) - expect(stakerDataAfter.userBalances.cooldownUnits, "cooldownUnits After").to.eq(cooldownUnitsExpected) - expect(stakerDataAfter.userBalances.raw, "raw balance after").to.eq(rawBalanceExpected) + expect(stakerDataAfter.rawBalance.cooldownUnits, "cooldownUnits After").to.eq(cooldownUnitsExpected) + expect(stakerDataAfter.rawBalance.raw, "raw balance after").to.eq(rawBalanceExpected) expect(stakerDataAfter.votes, "votes after").to.eq(balanceExpected) - expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(balanceExpected) + expect(stakerDataAfter.scaledBalance, "staked balance after").to.eq(balanceExpected) }) }) }) - context("questMaster", () => { - beforeEach(async () => { + // Important that each action (checkTimestamp, completeQuest, mint) applies this because + // scaledBalance could actually decrease, even in these situations, since old seasonMultipliers are slashed + context("in a new season", () => { + it("should slash an old seasons reward on any action") + }) + it("should always keep totalSupply == sum(boostedBalances)") + it("should update total votingPower, totalSupply, etc, retroactively") + }) + + context("claiming rewards after season finish", () => { + it("should update the users scaled balance and multiplier") + }) + + context("reviewing timestamp and completing quests with no stake", () => { + it("should do nothing") + it("should not result in unfair advantages somehow") + }) + + // '''..................................................................''' + // '''...................... VOTINGTOKEN .........................''' + // '''..................................................................''' + + context("maintaining checkpoints and balances", () => { + const assertPastCheckpoint = async ( + user: string, + blockNumber: number, + _votesBefore: BN | undefined, + changeAmount: BN, + stake = true, + ): Promise => { + const votesBefore = _votesBefore === undefined ? BN.from(0) : _votesBefore + const votesAfter = stake ? votesBefore.add(changeAmount) : votesBefore.sub(changeAmount) + if (votesBefore) { + expect(await stakedToken.getPastVotes(user, blockNumber - 1), "just before").to.eq(votesBefore) + } + expect(await stakedToken.getPastVotes(user, blockNumber), "at").to.eq(votesAfter) + expect(await stakedToken.getPastVotes(user, blockNumber + 1), "just after").to.eq(votesAfter) + + return votesAfter + } + const assertPastTotalSupply = async ( + action: string, + blockNumber: number, + _supplyBefore: BN | undefined, + changeAmount: BN, + stake = true, + ): Promise => { + const supplyBefore = _supplyBefore === undefined ? BN.from(0) : _supplyBefore + const supplyAfter = stake ? supplyBefore.add(changeAmount) : supplyBefore.sub(changeAmount) + if (_supplyBefore) { + expect(await stakedToken.getPastTotalSupply(blockNumber - 1), `just before ${action}`).to.eq(supplyBefore) + } + expect(await stakedToken.getPastTotalSupply(blockNumber), `at ${action}`).to.eq(supplyAfter) + expect(await stakedToken.getPastTotalSupply(blockNumber + 1), `just after ${action}`).to.eq(supplyAfter) + + return supplyAfter + } + context("with no delegate", () => { + // stake, stake again, other stake, partial cooldown, partial withdraw, partial cooldown, stake and exit cooldown, full cooldown, full withdraw + let stakerAddress + let otherStakerAddress + const firstStakedAmount = simpleToExactAmount(1000) + const secondStakedAmount = simpleToExactAmount(2000) + const thirdStakedAmount = simpleToExactAmount(3000) + const firstCooldownAmount = simpleToExactAmount(500) + const secondCooldownAmount = simpleToExactAmount(2500) + const thirdCooldownAmount = simpleToExactAmount(5500) + + const firstOtherStakedAmount = simpleToExactAmount(100) + const blocks: number[] = [] + let totalSupply = BN.from(0) + let stakerVotes = BN.from(0) + before(async () => { + stakerAddress = sa.default.address + otherStakerAddress = sa.dummy1.address ;({ stakedToken, questManager } = await redeployStakedToken()) - expect(await questManager.questMaster(), "quest master before").to.eq(sa.questMaster.address) + await rewardToken.connect(sa.default.signer).approve(stakedToken.address, simpleToExactAmount(1000000)) + await rewardToken.transfer(otherStakerAddress, simpleToExactAmount(100000)) + await rewardToken.connect(sa.dummy1.signer).approve(stakedToken.address, simpleToExactAmount(1000000)) + const block = await sa.default.signer.provider.getBlock("latest") + blocks.push(block.number) }) - it("should set questMaster by governor", async () => { - const tx = await questManager.connect(sa.governor.signer).setQuestMaster(sa.dummy1.address) - await expect(tx).to.emit(questManager, "QuestMaster").withArgs(sa.questMaster.address, sa.dummy1.address) - expect(await questManager.questMaster(), "quest master after").to.eq(sa.dummy1.address) + beforeEach(async () => { + await increaseTime(ONE_WEEK) + await advanceBlock() }) - it("should set questMaster by quest master", async () => { - const tx = await questManager.connect(sa.questMaster.signer).setQuestMaster(sa.dummy2.address) - await expect(tx).to.emit(questManager, "QuestMaster").withArgs(sa.questMaster.address, sa.dummy2.address) - expect(await questManager.questMaster(), "quest master after").to.eq(sa.dummy2.address) + it("should first stake", async () => { + const tx = await stakedToken["stake(uint256)"](firstStakedAmount) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Staker Checkpoint + const stakerCheckpoint = await stakedToken.checkpoints(stakerAddress, 0) + expect(stakerCheckpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + stakerVotes = stakerVotes.add(firstStakedAmount) + expect(stakerCheckpoint.votes, "checkpoint votes").to.eq(stakerVotes) + + // Total Supply + totalSupply = totalSupply.add(firstStakedAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) }) - it("should fail to set quest master by anyone", async () => { - await expect(questManager.connect(sa.dummy3.signer).setQuestMaster(sa.dummy3.address)).to.revertedWith("Not verified") + it("should second stake", async () => { + const tx = await stakedToken["stake(uint256)"](secondStakedAmount) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Staker Checkpoint + const stakerCheckpoint = await stakedToken.checkpoints(stakerAddress, 1) + expect(stakerCheckpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + stakerVotes = stakerVotes.add(secondStakedAmount) + expect(stakerCheckpoint.votes, "checkpoint votes").to.eq(stakerVotes) + + // Total Supply + totalSupply = totalSupply.add(secondStakedAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) }) - }) - context("questSigner", () => { - beforeEach(async () => { - ;({ stakedToken, questManager } = await redeployStakedToken()) + it("should first stake from other", async () => { + const tx = await stakedToken.connect(sa.dummy1.signer)["stake(uint256)"](firstOtherStakedAmount) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Staker Checkpoint + const stakerCheckpoint = await stakedToken.checkpoints(otherStakerAddress, 0) + expect(stakerCheckpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + expect(stakerCheckpoint.votes, "checkpoint votes").to.eq(firstOtherStakedAmount) + + // Total Supply + totalSupply = totalSupply.add(firstOtherStakedAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) }) - it("should set quest signer by governor", async () => { - const tx = await questManager.connect(sa.governor.signer).setQuestSigner(sa.dummy1.address) - await expect(tx).to.emit(questManager, "QuestSigner").withArgs(sa.questSigner.address, sa.dummy1.address) + it("should first cooldown partial", async () => { + const tx = await stakedToken.startCooldown(firstCooldownAmount) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Staker Checkpoint + const stakerCheckpoint = await stakedToken.checkpoints(stakerAddress, 2) + expect(stakerCheckpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + stakerVotes = stakerVotes.sub(firstCooldownAmount) + expect(stakerCheckpoint.votes, "checkpoint votes").to.eq(stakerVotes) + + // Total Supply + totalSupply = totalSupply.sub(firstCooldownAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) }) - it("should fail to set quest signer by quest master", async () => { - await expect(questManager.connect(sa.questMaster.signer).setQuestSigner(sa.dummy3.address)).to.revertedWith( - "Only governor can execute", - ) + it("should first withdraw partial", async () => { + const tx = await stakedToken.withdraw(firstCooldownAmount, stakerAddress, true, true) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Not new Staker Checkpoint + expect(await stakedToken.numCheckpoints(stakerAddress), "checkpoint block").to.eq(3) + // Total Supply unchanged + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) }) - it("should fail to set quest signer by anyone", async () => { - await expect(questManager.connect(sa.dummy3.signer).setQuestSigner(sa.dummy3.address)).to.revertedWith( - "Only governor can execute", + it("should second cooldown full", async () => { + const tx = await stakedToken.startCooldown(secondCooldownAmount) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Staker Checkpoint + const stakerCheckpoint = await stakedToken.checkpoints(stakerAddress, 3) + expect(stakerCheckpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + stakerVotes = BN.from(0) + expect(stakerCheckpoint.votes, "checkpoint votes").to.eq(stakerVotes) + + // Total Supply + totalSupply = totalSupply.sub(secondCooldownAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should third stake and exit cooldown", async () => { + const tx = await stakedToken["stake(uint256,bool)"](thirdStakedAmount, true) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Staker Checkpoint + const stakerCheckpoint = await stakedToken.checkpoints(stakerAddress, 4) + expect(stakerCheckpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + stakerVotes = secondCooldownAmount.add(thirdStakedAmount) + expect(stakerCheckpoint.votes, "checkpoint votes").to.eq(stakerVotes) + + // Total Supply + totalSupply = totalSupply.add(secondCooldownAmount).add(thirdStakedAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should third cooldown full", async () => { + const tx = await stakedToken.startCooldown(thirdCooldownAmount) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Staker Checkpoint + const stakerCheckpoint = await stakedToken.checkpoints(stakerAddress, 5) + expect(stakerCheckpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + stakerVotes = BN.from(0) + expect(stakerCheckpoint.votes, "checkpoint votes").to.eq(stakerVotes) + + // Total Supply + totalSupply = totalSupply.sub(thirdCooldownAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should third withdraw full", async () => { + const tx = await stakedToken.withdraw(thirdCooldownAmount, stakerAddress, true, true) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + // Not new Staker Checkpoint + expect(await stakedToken.numCheckpoints(stakerAddress), "checkpoint block").to.eq(6) + // Total Supply unchanged + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + context("should get staker past votes", () => { + const stakerChanges: [string, BN, boolean][] = [ + ["staked token deploy", BN.from(0), true], + ["first stake", firstStakedAmount, true], + ["second stake", secondStakedAmount, true], + ["first other stake", BN.from(0), true], + ["first cooldown partial", firstCooldownAmount, false], + ["first withdraw partial", BN.from(0), true], + ["second cooldown full", secondCooldownAmount, false], + ["third stake and exit cooldown", secondCooldownAmount.add(thirdStakedAmount), true], + ["third cooldown full", thirdCooldownAmount, false], + ["third withdraw full", BN.from(0), true], + ] + let votesAfter + stakerChanges.forEach((test, i) => { + it(test[0], async () => { + votesAfter = await assertPastCheckpoint(stakerAddress, blocks[i], votesAfter, test[1], test[2]) + }) + }) + }) + it("should get past total supply", async () => { + let afterTotal = await assertPastTotalSupply("staked token deploy", blocks[0], undefined, BN.from(0)) + afterTotal = await assertPastTotalSupply("first stake", blocks[1], afterTotal, firstStakedAmount) + afterTotal = await assertPastTotalSupply("second stake", blocks[2], afterTotal, secondStakedAmount) + afterTotal = await assertPastTotalSupply("first other stake", blocks[3], afterTotal, firstOtherStakedAmount) + afterTotal = await assertPastTotalSupply("first cooldown partial", blocks[4], afterTotal, firstCooldownAmount, false) + afterTotal = await assertPastTotalSupply("first withdraw partial", blocks[5], afterTotal, BN.from(0), false) + afterTotal = await assertPastTotalSupply("second cooldown full", blocks[6], afterTotal, secondCooldownAmount, false) + afterTotal = await assertPastTotalSupply( + "third stake and exit cooldown", + blocks[7], + afterTotal, + secondCooldownAmount.add(thirdStakedAmount), ) + afterTotal = await assertPastTotalSupply("third cooldown full", blocks[8], afterTotal, thirdCooldownAmount, false) + await assertPastTotalSupply("third withdraw full", blocks[9], afterTotal, BN.from(0), false) + }) + context("should fail to get future block for", () => { + let block: Block + before(async () => { + block = await ethers.provider.getBlock("latest") + }) + it("past votes", async () => { + const tx = stakedToken.getPastVotes(stakerAddress, block.number + 100) + await expect(tx).to.revertedWith("ERC20Votes: block not yet mined") + }) + it("past total supply", async () => { + const tx = stakedToken.getPastTotalSupply(block.number + 100) + await expect(tx).to.revertedWith("ERC20Votes: block not yet mined") + }) }) }) + context("with delegate", () => { + // stake 11 to 1st delegate + // stake 22 again to 1st delegate + // change to 2nd delegate + // partial cooldown 16 + // stake 33 back to 1st delegate + // end cooldown + let stakerAddress + let otherStakerAddress + let firstDelegateAddress + let secondDelegateAddress + const firstStakedAmount = simpleToExactAmount(11) + const secondStakedAmount = simpleToExactAmount(22) + const thirdStakedAmount = simpleToExactAmount(33) + const firstCooldownAmount = simpleToExactAmount(16) + + const blocks: number[] = [] + let totalSupply = BN.from(0) + let firstDelegateVotes = BN.from(0) + let secondDelegateVotes = BN.from(0) + before(async () => { + stakerAddress = sa.default.address + otherStakerAddress = sa.dummy1.address + firstDelegateAddress = sa.dummy2.address + secondDelegateAddress = sa.dummy3.address + ;({ stakedToken, questManager } = await redeployStakedToken()) + await rewardToken.connect(sa.default.signer).approve(stakedToken.address, simpleToExactAmount(1000000)) + await rewardToken.transfer(otherStakerAddress, simpleToExactAmount(100000)) + await rewardToken.connect(sa.dummy1.signer).approve(stakedToken.address, simpleToExactAmount(1000000)) + const block = await sa.default.signer.provider.getBlock("latest") + blocks.push(block.number) + }) + beforeEach(async () => { + await increaseTime(ONE_WEEK) + await advanceBlock() + }) - // Important that each action (checkTimestamp, completeQuest, mint) applies this because - // scaledBalance could actually decrease, even in these situations, since old seasonMultipliers are slashed - it("should slash an old seasons reward on any action") - }) - context("boosting", () => { - it("should apply a multiplier if the user stakes within the migration window") - it("should apply the multiplier to voting power but not raw balance") - it("should update total votingPower, totalSupply, etc, retroactively") - }) + it("should first stake", async () => { + const tx = await stakedToken["stake(uint256,address)"](firstStakedAmount, firstDelegateAddress) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) - // '''..................................................................''' - // '''...................... VOTINGTOKEN .........................''' - // '''..................................................................''' + expect(await stakedToken.numCheckpoints(stakerAddress), "staker num checkpoints").to.eq(0) + expect(await stakedToken.numCheckpoints(firstDelegateAddress), "1st delegate num checkpoints").to.eq(1) + expect(await stakedToken.numCheckpoints(secondDelegateAddress), "2nd delegate num checkpoints").to.eq(0) + + // First delegate Checkpoint + const checkpoint = await stakedToken.checkpoints(firstDelegateAddress, 0) + expect(checkpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + firstDelegateVotes = firstDelegateVotes.add(firstStakedAmount) + expect(checkpoint.votes, "checkpoint votes").to.eq(firstDelegateVotes) + + // Total Supply + totalSupply = totalSupply.add(firstStakedAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should second stake", async () => { + const tx = await stakedToken["stake(uint256,address)"](secondStakedAmount, firstDelegateAddress) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + expect(await stakedToken.numCheckpoints(stakerAddress), "staker num checkpoints").to.eq(0) + expect(await stakedToken.numCheckpoints(firstDelegateAddress), "1st delegate num checkpoints").to.eq(2) + expect(await stakedToken.numCheckpoints(secondDelegateAddress), "2nd delegate num checkpoints").to.eq(0) + + // First delegate Checkpoint + const checkpoint = await stakedToken.checkpoints(firstDelegateAddress, 1) + expect(checkpoint.fromBlock, "checkpoint block").to.eq(receipt.blockNumber) + firstDelegateVotes = firstDelegateVotes.add(secondStakedAmount) + expect(checkpoint.votes, "checkpoint votes").to.eq(firstDelegateVotes) + + // Total Supply + totalSupply = totalSupply.add(secondStakedAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should change delegate", async () => { + const tx = await stakedToken.delegate(secondDelegateAddress) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + expect(await stakedToken.numCheckpoints(stakerAddress), "staker num checkpoints").to.eq(0) + expect(await stakedToken.numCheckpoints(firstDelegateAddress), "1st delegate num checkpoints").to.eq(3) + expect(await stakedToken.numCheckpoints(secondDelegateAddress), "2nd delegate num checkpoints").to.eq(1) + + // First delegate Checkpoint + const firstDelegateCheckpoint = await stakedToken.checkpoints(firstDelegateAddress, 2) + expect(firstDelegateCheckpoint.fromBlock, "1st delegate checkpoint block").to.eq(receipt.blockNumber) + firstDelegateVotes = BN.from(0) + expect(firstDelegateCheckpoint.votes, "1st delegate checkpoint votes").to.eq(firstDelegateVotes) - context("maintaining checkpoints", () => { - it("should track users balance as checkpoints") + // Second delegate Checkpoint + const secondDelegateCheckpoint = await stakedToken.checkpoints(secondDelegateAddress, 0) + expect(secondDelegateCheckpoint.fromBlock, "2nd delegate checkpoint block").to.eq(receipt.blockNumber) + secondDelegateVotes = firstStakedAmount.add(secondStakedAmount) + expect(secondDelegateCheckpoint.votes, "2nd delegate checkpoint votes").to.eq(secondDelegateVotes) + + // Total Supply unchanged + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should first cooldown partial", async () => { + const tx = await stakedToken.startCooldown(firstCooldownAmount) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + expect(await stakedToken.numCheckpoints(stakerAddress), "staker num checkpoints").to.eq(0) + expect(await stakedToken.numCheckpoints(firstDelegateAddress), "1st delegate num checkpoints").to.eq(3) + expect(await stakedToken.numCheckpoints(secondDelegateAddress), "2nd delegate num checkpoints").to.eq(2) + + // Second delegate Checkpoint + const secondDelegateCheckpoint = await stakedToken.checkpoints(secondDelegateAddress, 1) + expect(secondDelegateCheckpoint.fromBlock, "2nd delegate checkpoint block").to.eq(receipt.blockNumber) + secondDelegateVotes = secondDelegateVotes.sub(firstCooldownAmount) + expect(secondDelegateCheckpoint.votes, "2nd delegate checkpoint votes").to.eq(secondDelegateVotes) + + // Total Supply + totalSupply = totalSupply.sub(firstCooldownAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should third stake and change delegate back to first delegate", async () => { + const tx = await stakedToken["stake(uint256,address)"](thirdStakedAmount, firstDelegateAddress) + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + expect(await stakedToken.numCheckpoints(stakerAddress), "staker num checkpoints").to.eq(0) + expect(await stakedToken.numCheckpoints(firstDelegateAddress), "1st delegate num checkpoints").to.eq(4) + expect(await stakedToken.numCheckpoints(secondDelegateAddress), "2nd delegate num checkpoints").to.eq(3) + + // First delegate Checkpoint + const firstDelegateCheckpoint = await stakedToken.checkpoints(firstDelegateAddress, 3) + expect(firstDelegateCheckpoint.fromBlock, "1st delegate checkpoint block").to.eq(receipt.blockNumber) + firstDelegateVotes = secondDelegateVotes.add(thirdStakedAmount) + expect(firstDelegateCheckpoint.votes, "1st delegate checkpoint votes").to.eq(firstDelegateVotes) + + // Second delegate Checkpoint + const secondDelegateCheckpoint = await stakedToken.checkpoints(secondDelegateAddress, 2) + expect(secondDelegateCheckpoint.fromBlock, "2nd delegate checkpoint block").to.eq(receipt.blockNumber) + secondDelegateVotes = BN.from(0) + expect(secondDelegateCheckpoint.votes, "2nd delegate checkpoint votes").to.eq(secondDelegateVotes) + + // Total Supply + totalSupply = totalSupply.add(thirdStakedAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + it("should end cooldown", async () => { + const tx = await stakedToken.endCooldown() + const receipt = await tx.wait() + blocks.push(receipt.blockNumber) + + expect(await stakedToken.numCheckpoints(stakerAddress), "staker num checkpoints").to.eq(0) + expect(await stakedToken.numCheckpoints(firstDelegateAddress), "1st delegate num checkpoints").to.eq(5) + expect(await stakedToken.numCheckpoints(secondDelegateAddress), "2nd delegate num checkpoints").to.eq(3) + + // Second delegate Checkpoint + const firstDelegateCheckpoint = await stakedToken.checkpoints(firstDelegateAddress, 4) + expect(firstDelegateCheckpoint.fromBlock, "1st delegate checkpoint block").to.eq(receipt.blockNumber) + firstDelegateVotes = firstDelegateVotes.add(firstCooldownAmount) + expect(firstDelegateCheckpoint.votes, "1st delegate checkpoint votes").to.eq(firstDelegateVotes) + + // Total Supply + totalSupply = totalSupply.add(firstCooldownAmount) + expect(await stakedToken.totalSupply(), "total staked after").to.eq(totalSupply) + }) + context("should get first delegate past votes", () => { + const firstDelegateChanges: [string, BN, boolean][] = [ + ["staked token deploy", BN.from(0), true], + ["first stake", firstStakedAmount, true], + ["second stake", secondStakedAmount, true], + ["change delegate", firstStakedAmount.add(secondStakedAmount), false], + ["first cooldown partial", BN.from(0), true], + [ + "third stake and change delegate", + firstStakedAmount.add(secondStakedAmount).add(thirdStakedAmount).sub(firstCooldownAmount), + true, + ], + ["end cooldown", firstCooldownAmount, true], + ] + let votesAfter + firstDelegateChanges.forEach((test, i) => { + it(test[0], async () => { + votesAfter = await assertPastCheckpoint(firstDelegateAddress, blocks[i], votesAfter, test[1], test[2]) + }) + }) + }) + context("should get second delegate past votes", () => { + const firstDelegateChanges: [string, BN, boolean][] = [ + ["staked token deploy", BN.from(0), true], + ["first stake", BN.from(0), true], + ["second stake", BN.from(0), true], + ["change delegate", firstStakedAmount.add(secondStakedAmount), true], + ["first cooldown partial", firstCooldownAmount, false], + ["third stake and change delegate", firstStakedAmount.add(secondStakedAmount).sub(firstCooldownAmount), false], + ["end cooldown", BN.from(0), true], + ] + let votesAfter + firstDelegateChanges.forEach((test, i) => { + it(test[0], async () => { + votesAfter = await assertPastCheckpoint(secondDelegateAddress, blocks[i], votesAfter, test[1], test[2]) + }) + }) + }) + it("should get past total supply", async () => { + let afterTotal = await assertPastTotalSupply("staked token deploy", blocks[0], undefined, BN.from(0)) + afterTotal = await assertPastTotalSupply("first stake", blocks[1], afterTotal, firstStakedAmount) + afterTotal = await assertPastTotalSupply("second stake", blocks[2], afterTotal, secondStakedAmount) + afterTotal = await assertPastTotalSupply("change delegate", blocks[3], afterTotal, BN.from(0)) + afterTotal = await assertPastTotalSupply("first cooldown partial", blocks[4], afterTotal, firstCooldownAmount, false) + afterTotal = await assertPastTotalSupply("third stake and change delegate", blocks[5], afterTotal, thirdStakedAmount) + await assertPastTotalSupply("end cooldown", blocks[6], afterTotal, firstCooldownAmount, true) + }) + }) }) context("triggering the governance hook", () => { it("should allow governor to add a governanceHook") @@ -1930,6 +2810,12 @@ describe("Staked Token", () => { // '''..................................................................''' context("calling applyQuestMultiplier", () => { - it("should fail unless called by questManager") + before(async () => { + ;({ stakedToken, questManager } = await redeployStakedToken()) + }) + it("should fail unless called by questManager", async () => { + const tx = stakedToken.applyQuestMultiplier(sa.dummy1.address, 50) + await expect(tx).to.revertedWith("Not verified") + }) }) }) diff --git a/types/index.ts b/types/index.ts index 598a1b6c..6cc46547 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,3 +1,4 @@ export * from "./common" export * from "./machines" export * from "./generated" +export * from "./stakedToken" diff --git a/types/stakedToken.ts b/types/stakedToken.ts index 06f3d9be..1fed8bcc 100644 --- a/types/stakedToken.ts +++ b/types/stakedToken.ts @@ -1,6 +1,6 @@ import { BN } from "@utils/math" -export interface UserBalances { +export interface UserBalance { raw: BN weightedTimestamp: number questMultiplier: number @@ -16,13 +16,22 @@ export interface QuestBalance { } export interface UserStakingData { - stakedBalance: BN + scaledBalance: BN votes: BN earnedRewards: BN - rewardsBalance: BN - userBalances: UserBalances + numCheckpoints: number + rewardTokenBalance: BN + rawBalance: UserBalance userPriceCoeff: BN questBalance: QuestBalance + balData?: BalConfig +} +export interface BalConfig { + balRecipient: string + keeper: string + pendingBPTFees: BN + priceCoefficient: BN + lastPriceUpdateTime: BN } export enum QuestType {