From 9e0416bfab66a5fdca1e439f05a60f67025218a1 Mon Sep 17 00:00:00 2001 From: Swamp Thing Date: Wed, 11 Aug 2021 11:24:20 -0300 Subject: [PATCH 01/17] feat: new liquidity mining that rewards in multiple tokens --- contracts/farm/ERC20TransferLogic.sol | 41 + contracts/farm/ERC20TransferLogicStorage.sol | 9 + contracts/farm/ILiquidityMiningV1.sol | 40 + contracts/farm/ILiquidityMiningV2.sol | 44 + contracts/farm/IRewardTransferLogic.sol | 22 + contracts/farm/LMV1toLMV2Migrator.sol | 153 ++ contracts/farm/LiquidityMiningProxyV2.sol | 11 + contracts/farm/LiquidityMiningStorageV1.sol | 18 + contracts/farm/LiquidityMiningStorageV2.sol | 84 + contracts/farm/LiquidityMiningV1.sol | 767 +++++++ contracts/farm/LiquidityMiningV2.sol | 982 ++++++++ .../farm/LockedSOVRewardTransferLogic.sol | 66 + .../LockedSOVRewardTransferLogicStorage.sol | 13 + contracts/locked/ILockedSOV.sol | 5 + contracts/mockup/LiquidityMiningV1Mockup.sol | 16 + contracts/mockup/LiquidityMiningV2Mockup.sol | 14 + contracts/mockup/RBTCWrapperProxyMockupV2.sol | 23 + contracts/mockup/TestPoolToken.sol | 26 + diagrams/LiquidityMiningMigration.puml | 27 + diagrams/LiquidityMiningStateDiagram.puml | 28 + hardhat.config.js | 5 +- package.json | 2 +- .../contract_interaction.py | 2 + .../contractInteraction/liquidity_miningV2.py | 60 + .../liquidity_mining_V1toV2_migrator.py | 65 + .../deploy-liquidity-mining-logic.py | 30 + .../upgrade-liquidity-mining-logic.py | 49 + .../deploy-liquidity-mining-logic.py | 30 + .../deploy-liquidity-mining.py | 78 + .../liquidity-miningV2/deploy-migrator.py | 36 + .../liquidity-miningV2/update-lm.py | 138 ++ .../upgrade-liquidity-mining-logic.py | 47 + tests/farm/ERC20TransferLogic.js | 136 ++ tests/farm/LiquidityMiningMigration.js | 988 ++++++++ tests/farm/LiquidityMiningV1.js | 2037 +++++++++++++++++ tests/farm/LiquidityMiningV2.js | 2008 ++++++++++++++++ tests/farm/LockedSOVRewardTransferLogic.js | 224 ++ tests/loan-token/LendingWithLMV2.test.js | 272 +++ 38 files changed, 8594 insertions(+), 2 deletions(-) create mode 100644 contracts/farm/ERC20TransferLogic.sol create mode 100644 contracts/farm/ERC20TransferLogicStorage.sol create mode 100644 contracts/farm/ILiquidityMiningV1.sol create mode 100644 contracts/farm/ILiquidityMiningV2.sol create mode 100644 contracts/farm/IRewardTransferLogic.sol create mode 100644 contracts/farm/LMV1toLMV2Migrator.sol create mode 100644 contracts/farm/LiquidityMiningProxyV2.sol create mode 100644 contracts/farm/LiquidityMiningStorageV1.sol create mode 100644 contracts/farm/LiquidityMiningStorageV2.sol create mode 100644 contracts/farm/LiquidityMiningV1.sol create mode 100644 contracts/farm/LiquidityMiningV2.sol create mode 100644 contracts/farm/LockedSOVRewardTransferLogic.sol create mode 100644 contracts/farm/LockedSOVRewardTransferLogicStorage.sol create mode 100644 contracts/mockup/LiquidityMiningV1Mockup.sol create mode 100644 contracts/mockup/LiquidityMiningV2Mockup.sol create mode 100644 contracts/mockup/RBTCWrapperProxyMockupV2.sol create mode 100644 contracts/mockup/TestPoolToken.sol create mode 100644 diagrams/LiquidityMiningMigration.puml create mode 100644 diagrams/LiquidityMiningStateDiagram.puml create mode 100644 scripts/contractInteraction/liquidity_miningV2.py create mode 100644 scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py create mode 100644 scripts/deployment/liquidity-miningV1/deploy-liquidity-mining-logic.py create mode 100644 scripts/deployment/liquidity-miningV1/upgrade-liquidity-mining-logic.py create mode 100644 scripts/deployment/liquidity-miningV2/deploy-liquidity-mining-logic.py create mode 100644 scripts/deployment/liquidity-miningV2/deploy-liquidity-mining.py create mode 100644 scripts/deployment/liquidity-miningV2/deploy-migrator.py create mode 100644 scripts/deployment/liquidity-miningV2/update-lm.py create mode 100644 scripts/deployment/liquidity-miningV2/upgrade-liquidity-mining-logic.py create mode 100644 tests/farm/ERC20TransferLogic.js create mode 100644 tests/farm/LiquidityMiningMigration.js create mode 100644 tests/farm/LiquidityMiningV1.js create mode 100644 tests/farm/LiquidityMiningV2.js create mode 100644 tests/farm/LockedSOVRewardTransferLogic.js create mode 100644 tests/loan-token/LendingWithLMV2.test.js diff --git a/contracts/farm/ERC20TransferLogic.sol b/contracts/farm/ERC20TransferLogic.sol new file mode 100644 index 000000000..79141c027 --- /dev/null +++ b/contracts/farm/ERC20TransferLogic.sol @@ -0,0 +1,41 @@ +pragma solidity 0.5.17; + +import "./IRewardTransferLogic.sol"; +import "./ERC20TransferLogicStorage.sol"; +import "../interfaces/IERC20.sol"; +import "../openzeppelin/SafeERC20.sol"; + +contract ERC20TransferLogic is IRewardTransferLogic, ERC20TransferLogicStorage { + using SafeERC20 for IERC20; + + event TokenAddressUpdated(address _newTokenAddress); + + /** + * @param _token Reward token to be distributed + */ + function initialize(address _token) public onlyAuthorized { + setTokenAddress(_token); + } + + function setTokenAddress(address _token) public onlyAuthorized { + require(_token != address(0), "Invalid token address"); + token = IERC20(_token); + emit TokenAddressUpdated(_token); + } + + function getRewardTokenAddress() external view returns (address) { + return address(token); + } + + function senderToAuthorize() external view returns (address) { + return address(this); + } + + function transferReward( + address _to, + uint256 _value, + bool // it doesn't matter if it's a withdrawal or not + ) external { + token.safeTransferFrom(msg.sender, _to, _value); + } +} diff --git a/contracts/farm/ERC20TransferLogicStorage.sol b/contracts/farm/ERC20TransferLogicStorage.sol new file mode 100644 index 000000000..756df6e39 --- /dev/null +++ b/contracts/farm/ERC20TransferLogicStorage.sol @@ -0,0 +1,9 @@ +pragma solidity 0.5.17; + +import "./IRewardTransferLogic.sol"; +import "../utils/AdminRole.sol"; +import "../interfaces/IERC20.sol"; + +contract ERC20TransferLogicStorage is IRewardTransferLogic, AdminRole { + IERC20 public token; +} diff --git a/contracts/farm/ILiquidityMiningV1.sol b/contracts/farm/ILiquidityMiningV1.sol new file mode 100644 index 000000000..d471a1c7c --- /dev/null +++ b/contracts/farm/ILiquidityMiningV1.sol @@ -0,0 +1,40 @@ +pragma solidity 0.5.17; + +interface ILiquidityMiningV1 { + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external; + + function onTokensDeposited(address _user, uint256 _amount) external; + + function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256); + + function getPoolInfoListArray() + external + view + returns ( + address[] memory, + uint96[] memory, + uint256[] memory, + uint256[] memory + ); + + function getUserInfoListArray(address _user) + external + view + returns ( + uint256[] memory, + uint256[] memory, + uint256[] memory + ); + + function migrateFunds() external; + + function finishMigrationGracePeriod() external; + + function getTotalUsersBalance() external view returns (uint256); + + function getStartBlock() external view returns (uint256); +} diff --git a/contracts/farm/ILiquidityMiningV2.sol b/contracts/farm/ILiquidityMiningV2.sol new file mode 100644 index 000000000..8e21ffab8 --- /dev/null +++ b/contracts/farm/ILiquidityMiningV2.sol @@ -0,0 +1,44 @@ +pragma solidity 0.5.17; + +interface ILiquidityMiningV2 { + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external; + + function onTokensDeposited(address _user, uint256 _amount) external; + + function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256); + + function setPoolInfoRewardToken( + address _poolToken, + address _rewardToken, + uint256 _lastRewardBlock, + uint256 _accumulatedRewardPerShare + ) external; + + function setRewardToken( + address _rewardToken, + uint256 _startBlock, + uint256 _totalUsersBalance + ) external; + + function setUserInfo( + uint256 _poolId, + address _user, + address _rewardToken, + uint256 _amount, + uint256 _rewardDebt, + uint256 _accumulatedReward + ) external; + + function add( + address _poolToken, + address[] calldata _rewardTokens, + uint96[] calldata _allocationPoints, + bool _withUpdate + ) external; + + function finishMigration() external; +} diff --git a/contracts/farm/IRewardTransferLogic.sol b/contracts/farm/IRewardTransferLogic.sol new file mode 100644 index 000000000..923276356 --- /dev/null +++ b/contracts/farm/IRewardTransferLogic.sol @@ -0,0 +1,22 @@ +pragma solidity 0.5.17; + +/// @title This interface helps decoupling the Liquidity Mining reward +/// @dev Implement this interface in order to transfer the rewards with different logic. For example: +/// SOV tokens +interface IRewardTransferLogic { + /// @dev Returns the reward token address this contract will transfer + function getRewardTokenAddress() external view returns (address); + + /// @notice Transfers will be executed from this address so it must be approved before invoking + function senderToAuthorize() external view returns (address); + + /// @notice Transfers the reward amount to the specified address + /// @param _to The address to transfer the reward to + /// @param _value The amount of the reward to transfer + /// @param _isWithdrawal If true, means that the reward and the LP deposited tokens are being compeltely withdrawn + function transferReward( + address _to, + uint256 _value, + bool _isWithdrawal + ) external; +} diff --git a/contracts/farm/LMV1toLMV2Migrator.sol b/contracts/farm/LMV1toLMV2Migrator.sol new file mode 100644 index 000000000..edf69c0cd --- /dev/null +++ b/contracts/farm/LMV1toLMV2Migrator.sol @@ -0,0 +1,153 @@ +pragma solidity 0.5.17; +pragma experimental ABIEncoderV2; + +import "../openzeppelin/ERC20.sol"; +import "../openzeppelin/SafeERC20.sol"; +import "../openzeppelin/SafeMath.sol"; +import "../utils/AdminRole.sol"; +import "./ILiquidityMiningV1.sol"; +import "./ILiquidityMiningV2.sol"; + +contract LMV1toLMV2Migrator is AdminRole { + using SafeMath for uint256; + using SafeERC20 for IERC20; + enum MigrationStates { MigratingPools, MigratingUsers, MigratingFunds, MigrationFinished } + + //represents de migration state from LiquidityMiningV1 to LiquidityMiningV2 + MigrationStates public migrationState; + + //LiquidityMiningV1 contract address + ILiquidityMiningV1 public liquidityMiningV1; + + //LiquidityMiningV2 contract address + ILiquidityMiningV2 public liquidityMiningV2; + + /// @dev it is true if the user has been already migrated + mapping(address => bool) public userMigrated; + + /// @dev The SOV token + IERC20 public SOV; + + event UserMigrated(address indexed user); + + /* Modifiers */ + modifier onlyPoolsMigrationState() { + require(migrationState == MigrationStates.MigratingPools, "Wrong state: should be MigratingPools"); + _; + } + + modifier onlyUsersMigrationState() { + require(migrationState == MigrationStates.MigratingUsers, "Wrong state: should be MigratingUsers"); + _; + } + + modifier onlyFundsMigrationState() { + require(migrationState == MigrationStates.MigratingFunds, "Wrong state: should be MigratingFunds"); + _; + } + + /** + * @notice Initialize migrator + * + * @param _SOV The SOV token address + * @param _liquidityMiningV1 The LiquidityMiningV1 contract address + * @param _liquidityMiningV2 The LiquidityMiningV2 contract address + */ + function initialize( + IERC20 _SOV, + ILiquidityMiningV1 _liquidityMiningV1, + ILiquidityMiningV2 _liquidityMiningV2 + ) public onlyAuthorized { + require(address(_SOV) != address(0), "invalid token address"); + require(address(_liquidityMiningV1) != address(0), "invalid contract address"); + require(address(_liquidityMiningV2) != address(0), "invalid contract address"); + liquidityMiningV1 = _liquidityMiningV1; + liquidityMiningV2 = _liquidityMiningV2; + SOV = _SOV; + migrationState = MigrationStates.MigratingPools; + } + + function _finishPoolsMigration() internal onlyPoolsMigrationState { + migrationState = MigrationStates.MigratingUsers; + } + + function finishUsersMigration() external onlyAuthorized onlyUsersMigrationState { + migrationState = MigrationStates.MigratingFunds; + } + + function _finishFundsMigration() internal onlyFundsMigrationState { + migrationState = MigrationStates.MigrationFinished; + } + + /** + * @notice read all pools from liquidity mining V1 contract and add them + */ + function migratePools() external onlyAuthorized onlyPoolsMigrationState { + ( + address[] memory _poolToken, + uint96[] memory _allocationPoints, + uint256[] memory _lastRewardBlock, + uint256[] memory _accumulatedRewardPerShare + ) = liquidityMiningV1.getPoolInfoListArray(); + + require(_poolToken.length == _allocationPoints.length, "Arrays mismatch"); + require(_poolToken.length == _lastRewardBlock.length, "Arrays mismatch"); + + liquidityMiningV1.finishMigrationGracePeriod(); + for (uint256 i = 0; i < _poolToken.length; i++) { + address poolToken = _poolToken[i]; + uint96[] memory allocationPoints = new uint96[](1); + allocationPoints[0] = _allocationPoints[i]; + uint256 lastRewardBlock = _lastRewardBlock[i]; + uint256 accumulatedRewardPerShare = _accumulatedRewardPerShare[i]; + address[] memory SOVAddress = new address[](1); + SOVAddress[0] = address(SOV); + //add will revert if poolToken is invalid or if it was already added + liquidityMiningV2.add(poolToken, SOVAddress, allocationPoints, false); + //add pool function put lastRewardBlock with current block number value, so we need to retrieve the original + liquidityMiningV2.setPoolInfoRewardToken(poolToken, address(SOV), lastRewardBlock, accumulatedRewardPerShare); + } + uint256 _startblock = liquidityMiningV1.getStartBlock(); + uint256 _totalUsersBalance = liquidityMiningV1.getTotalUsersBalance(); + liquidityMiningV2.setRewardToken(address(SOV), _startblock, _totalUsersBalance); + _finishPoolsMigration(); + } + + /** + * @notice read all users of all the pools from liquidity mining V1 contract and copy their info + * @param _users a list of users to be copied + */ + + function migrateUsers(address[] calldata _users) external onlyAuthorized onlyUsersMigrationState { + for (uint256 i = 0; i < _users.length; i++) { + (uint256[] memory _amount, uint256[] memory _rewardDebt, uint256[] memory _accumulatedReward) = + liquidityMiningV1.getUserInfoListArray(_users[i]); + + require(_amount.length == _rewardDebt.length, "Arrays mismatch"); + require(_amount.length == _accumulatedReward.length, "Arrays mismatch"); + + address user = _users[i]; + + if (userMigrated[user] == false) { + userMigrated[user] = true; + for (uint256 j = 0; j < _amount.length; j++) { + uint256 poolId = j; + uint256 _userAmount = _amount[j]; + uint256 _userRewardDebt = _rewardDebt[j]; + uint256 _userAccumulatedReward = _accumulatedReward[j]; + liquidityMiningV2.setUserInfo(poolId, user, address(SOV), _userAmount, _userRewardDebt, _userAccumulatedReward); + } + emit UserMigrated(user); + } + } + } + + /** + * @notice transfer all funds from liquidity mining V1 + */ + function migrateFunds() external onlyAuthorized onlyFundsMigrationState { + liquidityMiningV1.migrateFunds(); + liquidityMiningV2.finishMigration(); + _finishFundsMigration(); + } +} diff --git a/contracts/farm/LiquidityMiningProxyV2.sol b/contracts/farm/LiquidityMiningProxyV2.sol new file mode 100644 index 000000000..67ed659b3 --- /dev/null +++ b/contracts/farm/LiquidityMiningProxyV2.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.5.17; + +import "./LiquidityMiningStorageV2.sol"; +import "../proxy/UpgradableProxy.sol"; + +/** + * @dev LiquidityMining contract should be upgradable, use UpgradableProxy + */ +contract LiquidityMiningProxyV2 is LiquidityMiningStorageV2, UpgradableProxy { + +} diff --git a/contracts/farm/LiquidityMiningStorageV1.sol b/contracts/farm/LiquidityMiningStorageV1.sol new file mode 100644 index 000000000..7d3a1847e --- /dev/null +++ b/contracts/farm/LiquidityMiningStorageV1.sol @@ -0,0 +1,18 @@ +pragma solidity 0.5.17; + +import "./LiquidityMiningStorage.sol"; + +contract LiquidityMiningStorageV1 is LiquidityMiningStorage { + /// @dev Careful when adding new states as there is a < comparison being used in the modifiers + enum MigrationGracePeriodStates { + None, + Started, // users can withdraw funds and rewards but not deposit + Finished // users can't operate with the contract + } + + /// @dev Represents migration grace period state + MigrationGracePeriodStates public migrationGracePeriodState; + + /// @dev liquidity mining V2 contract address + address public liquidityMiningV2; +} diff --git a/contracts/farm/LiquidityMiningStorageV2.sol b/contracts/farm/LiquidityMiningStorageV2.sol new file mode 100644 index 000000000..6016cf640 --- /dev/null +++ b/contracts/farm/LiquidityMiningStorageV2.sol @@ -0,0 +1,84 @@ +pragma solidity 0.5.17; + +import "../openzeppelin/ERC20.sol"; +import "../openzeppelin/SafeERC20.sol"; +import "../openzeppelin/SafeMath.sol"; +import "../locked/ILockedSOV.sol"; +import "../utils/AdminRole.sol"; +import "./IRewardTransferLogic.sol"; + +contract LiquidityMiningStorageV2 is AdminRole { + // Info of each user. + struct UserInfo { + uint256 amount; // How many pool tokens the user has provided. + mapping(address => UserReward) rewards; // Mapping between reward tokens and the user rewards. + } + + struct UserReward { + uint256 rewardDebt; // Reward debt. See explanation below. + uint256 accumulatedReward; //Reward that's ready to be transferred + // + // We do some fancy math here. Basically, any point in time, the amount of reward tokens + // entitled to a user but is accumulated to be distributed is: + // + // accumulated reward = (user.amount * pool.accumulatedRewardPerShare) - user.rewardDebt + // + // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: + // 1. The pool's `accumulatedRewardPerShare` (and `lastRewardBlock`) gets updated. + // 2. User receives the accumulated reward sent to his/her address. + // 3. User's `amount` gets updated. + // 4. User's `rewardDebt` gets updated. + } + + // Info of each pool. + struct PoolInfo { + IERC20 poolToken; // Address of LP token contract. + address[] rewardTokens; + } + + // Info about each token to be rewarded to different stakers + struct RewardToken { + // SVR tokens created per block. + uint256 rewardTokensPerBlock; + // The block number when reward token mining starts. + uint256 startBlock; + // Block number when reward token period ends. + uint256 endBlock; + // Total allocation points. Must be the sum of all allocation points in all pools. + uint256 totalAllocationPoint; + // Total balance this contract should have to handle withdrawal for all users + uint256 totalUsersBalance; + // Contract to be invoked in order to transfer reward tokens. + IRewardTransferLogic rewardTransferLogic; + } + + struct PoolInfoRewardToken { + uint96 allocationPoint; // How many allocation points assigned to this pool. Amount of reward tokens to distribute per block. + uint256 lastRewardBlock; // Last block number that reward tokens distribution occurs. + uint256 accumulatedRewardPerShare; // Accumulated amount of reward tokens per share, times 1e12. See below. + } + + //Wrapper contract which will be a proxy between user and LM + address public wrapper; + + // Info of each pool. + PoolInfo[] public poolInfoList; + + // Mapping pool token address => pool id + mapping(address => uint256) public poolIdList; + + // Mapping reward token address => reward token + mapping(address => RewardToken) public rewardTokensMap; + + // Mapping to link a LP pool to a reward token + // poolId => rewardTokenAddress => PoolInfoRewardToken + mapping(uint256 => mapping(address => PoolInfoRewardToken)) public poolInfoRewardTokensMap; + + // Info of each user that stakes LP tokens. + mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + + //LiquidityMiningV1 to LiquidityMiningV2 migrator contract address + address public migrator; + + bool public migrationFinished; +} diff --git a/contracts/farm/LiquidityMiningV1.sol b/contracts/farm/LiquidityMiningV1.sol new file mode 100644 index 000000000..e45cce023 --- /dev/null +++ b/contracts/farm/LiquidityMiningV1.sol @@ -0,0 +1,767 @@ +pragma solidity 0.5.17; +pragma experimental ABIEncoderV2; + +import "../openzeppelin/ERC20.sol"; +import "../openzeppelin/SafeERC20.sol"; +import "../openzeppelin/SafeMath.sol"; +import "./LiquidityMiningStorageV1.sol"; +import "./ILiquidityMiningV1.sol"; + +contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* Constants */ + + uint256 public constant PRECISION = 1e12; + // Bonus multiplier for early liquidity providers. + // During bonus period each passed block will be calculated like N passed blocks, where N = BONUS_MULTIPLIER + uint256 public constant BONUS_BLOCK_MULTIPLIER = 10; + + uint256 public constant SECONDS_PER_BLOCK = 30; + + /* Events */ + + event SOVTransferred(address indexed receiver, uint256 amount); + event PoolTokenAdded(address indexed user, address indexed poolToken, uint256 allocationPoint); + event PoolTokenUpdated(address indexed user, address indexed poolToken, uint256 newAllocationPoint, uint256 oldAllocationPoint); + event Deposit(address indexed user, address indexed poolToken, uint256 amount); + event RewardClaimed(address indexed user, address indexed poolToken, uint256 amount); + event Withdraw(address indexed user, address indexed poolToken, uint256 amount); + event EmergencyWithdraw(address indexed user, address indexed poolToken, uint256 amount, uint256 accumulatedReward); + + /* Modifiers */ + modifier onlyBeforeMigrationGracePeriod() { + require(migrationGracePeriodState < MigrationGracePeriodStates.Started, "Forbidden: migration already started"); + _; + } + + modifier onlyBeforeMigrationGracePeriodFinished() { + require(migrationGracePeriodState < MigrationGracePeriodStates.Finished, "Forbidden: contract deprecated"); + _; + } + + modifier onlyAfterMigrationFinished() { + require(migrationGracePeriodState == MigrationGracePeriodStates.Finished, "Forbidden: migration is not over yet"); + _; + } + + /* Functions */ + + /** + * @notice Initialize mining. + * + * @param _liquidityMiningV2 The LiquidityMiningV2 contract address + */ + function initialize(address _liquidityMiningV2) external onlyAuthorized { + /// @dev Non-idempotent function. Must be called just once. + require(_liquidityMiningV2 != address(0), "Invalid address"); + liquidityMiningV2 = _liquidityMiningV2; + } + + /** + * @notice Sets lockedSOV contract. + * @param _lockedSOV The contract instance address of the lockedSOV vault. + */ + function setLockedSOV(ILockedSOV _lockedSOV) external onlyAuthorized { + require(address(_lockedSOV) != address(0), "Invalid lockedSOV Address."); + lockedSOV = _lockedSOV; + } + + /** + * @notice Sets unlocked immediately percent. + * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. + * @dev @dev 10000 is 100% + */ + function setUnlockedImmediatelyPercent(uint256 _unlockedImmediatelyPercent) external onlyAuthorized { + require(_unlockedImmediatelyPercent < 10000, "Unlocked immediately percent has to be less than 10000."); + unlockedImmediatelyPercent = _unlockedImmediatelyPercent; + } + + /** + * @notice sets wrapper proxy contract + * @dev can be set to zero address to remove wrapper + */ + function setWrapper(address _wrapper) external onlyAuthorized { + wrapper = _wrapper; + } + + /** + * @notice stops mining by setting end block + */ + function stopMining() public onlyAuthorized { + require(endBlock == 0, "Already stopped"); + + endBlock = block.number; + } + + // TODO: this should only be used by the LiquidityMiningV2 contract?? + /// @notice This function starts the migration process which involves two steps: + /// 1. Starts the migration grace period when people can withdraw or claim for rewards + /// 2. Stops mining, i.e., no more rewards are paid + function startMigrationGracePeriod() external onlyAuthorized onlyBeforeMigrationGracePeriod { + migrationGracePeriodState = MigrationGracePeriodStates.Started; + stopMining(); + } + + // TODO: this should only be used by the LiquidityMiningV2 contract?? + /// @notice This function finishes the migration process disabling further withdrawals and claims + /// @dev migration grace period should have started before this function is called. + function finishMigrationGracePeriod() external onlyAuthorized onlyBeforeMigrationGracePeriodFinished { + require(migrationGracePeriodState == MigrationGracePeriodStates.Started, "Migration hasn't started yet"); + migrationGracePeriodState = MigrationGracePeriodStates.Finished; + } + + /** + * @notice Transfers SOV tokens to given address. + * Owner use this function to withdraw SOV from LM contract + * into another account. + * @param _receiver The address of the SOV receiver. + * @param _amount The amount to be transferred. + * */ + function transferSOV(address _receiver, uint256 _amount) public onlyAuthorized { + require(_receiver != address(0), "Receiver address invalid"); + require(_amount != 0, "Amount invalid"); + + /// @dev Do not transfer more SOV than available. + uint256 SOVBal = SOV.balanceOf(address(this)); + if (_amount > SOVBal) { + _amount = SOVBal; + } + + /// @dev The actual transfer. + require(SOV.transfer(_receiver, _amount), "Transfer failed"); + + /// @dev Event log. + emit SOVTransferred(_receiver, _amount); + } + + /** + * @notice Get the missed SOV balance of LM contract. + * + * @return The amount of SOV tokens according to totalUsersBalance + * in excess of actual SOV balance of the LM contract. + * */ + function getMissedBalance() external view returns (uint256) { + uint256 balance = SOV.balanceOf(address(this)); + return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); + } + + /** + * @notice adds a new lp to the pool. Can only be called by the owner or an admin + * @param _poolToken the address of pool token + * @param _allocationPoint the allocation point (weight) for the given pool + * @param _withUpdate the flag whether we need to update all pools + */ + function add( + address _poolToken, + uint96 _allocationPoint, + bool _withUpdate + ) external onlyAuthorized { + require(_allocationPoint > 0, "Invalid allocation point"); + require(_poolToken != address(0), "Invalid token address"); + require(poolIdList[_poolToken] == 0, "Token already added"); + + if (_withUpdate) { + updateAllPools(); + } + + uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; + totalAllocationPoint = totalAllocationPoint.add(_allocationPoint); + + poolInfoList.push( + PoolInfo({ + poolToken: IERC20(_poolToken), + allocationPoint: _allocationPoint, + lastRewardBlock: lastRewardBlock, + accumulatedRewardPerShare: 0 + }) + ); + //indexing starts from 1 in order to check whether token was already added + poolIdList[_poolToken] = poolInfoList.length; + + emit PoolTokenAdded(msg.sender, _poolToken, _allocationPoint); + } + + /** + * @notice updates the given pool's reward tokens allocation point + * @param _poolToken the address of pool token + * @param _allocationPoint the allocation point (weight) for the given pool + * @param _updateAllFlag the flag whether we need to update all pools + */ + function update( + address _poolToken, + uint96 _allocationPoint, + bool _updateAllFlag + ) external onlyAuthorized { + if (_updateAllFlag) { + updateAllPools(); + } else { + updatePool(_poolToken); + } + _updateToken(_poolToken, _allocationPoint); + } + + function _updateToken(address _poolToken, uint96 _allocationPoint) internal { + uint256 poolId = _getPoolId(_poolToken); + + uint256 previousAllocationPoint = poolInfoList[poolId].allocationPoint; + totalAllocationPoint = totalAllocationPoint.sub(previousAllocationPoint).add(_allocationPoint); + poolInfoList[poolId].allocationPoint = _allocationPoint; + + emit PoolTokenUpdated(msg.sender, _poolToken, _allocationPoint, previousAllocationPoint); + } + + /** + * @notice updates the given pools' reward tokens allocation points + * @param _poolTokens array of addresses of pool tokens + * @param _allocationPoints array of allocation points (weight) for the given pools + * @param _updateAllFlag the flag whether we need to update all pools + */ + function updateTokens( + address[] calldata _poolTokens, + uint96[] calldata _allocationPoints, + bool _updateAllFlag + ) external onlyAuthorized { + require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); + + if (_updateAllFlag) { + updateAllPools(); + } + uint256 length = _poolTokens.length; + for (uint256 i = 0; i < length; i++) { + if (!_updateAllFlag) { + updatePool(_poolTokens[i]); + } + _updateToken(_poolTokens[i], _allocationPoints[i]); + } + } + + /** + * @notice returns reward multiplier over the given _from to _to block + * @param _from the first block for a calculation + * @param _to the last block for a calculation + */ + function _getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) internal view returns (uint256) { + if (_from < startBlock) { + _from = startBlock; + } + if (endBlock > 0 && _to > endBlock) { + _to = endBlock; + } + if (_to <= bonusEndBlock) { + return _to.sub(_from).mul(BONUS_BLOCK_MULTIPLIER); + } else if (_from >= bonusEndBlock) { + return _to.sub(_from); + } else { + return bonusEndBlock.sub(_from).mul(BONUS_BLOCK_MULTIPLIER).add(_to.sub(bonusEndBlock)); + } + } + + function _getUserAccumulatedReward(uint256 _poolId, address _user) internal view returns (uint256) { + PoolInfo storage pool = poolInfoList[_poolId]; + UserInfo storage user = userInfoMap[_poolId][_user]; + + uint256 accumulatedRewardPerShare = pool.accumulatedRewardPerShare; + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && poolTokenBalance != 0) { + (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); + accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); + } + return user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); + } + + /** + * @notice returns accumulated reward + * @param _poolToken the address of pool token + * @param _user the user address + */ + function getUserAccumulatedReward(address _poolToken, address _user) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + return _getUserAccumulatedReward(poolId, _user); + } + + /** + * @notice returns estimated reward + * @param _poolToken the address of pool token + * @param _amount the amount of tokens to be deposited + * @param _duration the duration of liquidity providing in seconds + */ + function getEstimatedReward( + address _poolToken, + uint256 _amount, + uint256 _duration + ) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + uint256 start = block.number; + uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); + (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward(pool, _amount, start, end); + return _amount.mul(accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Updates reward variables for all pools. + * @dev Be careful of gas spending! + */ + function updateAllPools() public onlyBeforeMigrationGracePeriodFinished { + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + _updatePool(i); + } + } + + /** + * @notice Updates reward variables of the given pool to be up-to-date + * @param _poolToken the address of pool token + */ + function updatePool(address _poolToken) public onlyBeforeMigrationGracePeriodFinished { + uint256 poolId = _getPoolId(_poolToken); + _updatePool(poolId); + } + + function _updatePool(uint256 _poolId) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + + //this pool has been updated recently + if (block.number <= pool.lastRewardBlock) { + return; + } + + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (poolTokenBalance == 0) { + pool.lastRewardBlock = block.number; + return; + } + + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); + pool.accumulatedRewardPerShare = pool.accumulatedRewardPerShare.add(accumulatedRewardPerShare_); + pool.lastRewardBlock = block.number; + + totalUsersBalance = totalUsersBalance.add(accumulatedReward_); + } + + function _getPoolAccumulatedReward(PoolInfo storage _pool) internal view returns (uint256, uint256) { + return _getPoolAccumulatedReward(_pool, 0, _pool.lastRewardBlock, block.number); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + uint256 _additionalAmount, + uint256 _startBlock, + uint256 _endBlock + ) internal view returns (uint256, uint256) { + uint256 passedBlocks = _getPassedBlocksWithBonusMultiplier(_startBlock, _endBlock); + uint256 accumulatedReward = passedBlocks.mul(rewardTokensPerBlock).mul(_pool.allocationPoint).div(totalAllocationPoint); + + uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); + poolTokenBalance = poolTokenBalance.add(_additionalAmount); + uint256 accumulatedRewardPerShare = accumulatedReward.mul(PRECISION).div(poolTokenBalance); + return (accumulatedReward, accumulatedRewardPerShare); + } + + /** + * @notice deposits pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it or to msg.sender + */ + function deposit( + address _poolToken, + uint256 _amount, + address _user + ) external onlyBeforeMigrationGracePeriod { + _deposit(_poolToken, _amount, _user, false); + } + + /** + * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit + * @dev only callable by the pool which issues the tokens + * @param _user the user address + * @param _amount the minted amount + */ + function onTokensDeposited(address _user, uint256 _amount) external onlyBeforeMigrationGracePeriod { + //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert + _deposit(msg.sender, _amount, _user, true); + } + + /** + * @notice internal function for depositing pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it + * @param alreadyTransferred true if the pool tokens have already been transferred + */ + function _deposit( + address _poolToken, + uint256 _amount, + address _user, + bool alreadyTransferred + ) internal { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _user != address(0) ? _user : msg.sender; + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + + _updatePool(poolId); + //sends reward directly to the user + _updateReward(pool, user); + + if (_amount > 0) { + //receives pool tokens from msg.sender, it can be user or WrapperProxy contract + if (!alreadyTransferred) pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); + user.amount = user.amount.add(_amount); + } + _updateRewardDebt(pool, user); + emit Deposit(userAddress, _poolToken, _amount); + } + + /** + * @notice transfers reward tokens + * @param _poolToken the address of pool token + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimReward(address _poolToken, address _user) external onlyBeforeMigrationGracePeriodFinished { + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + _claimReward(poolId, userAddress, true); + } + + function _claimReward( + uint256 _poolId, + address _userAddress, + bool _isStakingTokens + ) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + UserInfo storage user = userInfoMap[_poolId][_userAddress]; + + _updatePool(_poolId); + _updateReward(pool, user); + _transferReward(address(pool.poolToken), user, _userAddress, _isStakingTokens, true); + _updateRewardDebt(pool, user); + } + + /** + * @notice transfers reward tokens from all pools + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimRewardFromAllPools(address _user) external onlyBeforeMigrationGracePeriodFinished { + address userAddress = _getUserAddress(_user); + + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + uint256 poolId = i; + _claimReward(poolId, userAddress, false); + } + lockedSOV.withdrawAndStakeTokensFrom(userAddress); + } + + /** + * @notice withdraws pool tokens and transfers reward tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) + */ + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external onlyBeforeMigrationGracePeriodFinished { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + require(user.amount >= _amount, "Not enough balance"); + + _updatePool(poolId); + _updateReward(pool, user); + _transferReward(_poolToken, user, userAddress, false, false); + + user.amount = user.amount.sub(_amount); + + //msg.sender is wrapper -> send to wrapper + if (msg.sender == wrapper) { + pool.poolToken.safeTransfer(address(msg.sender), _amount); + } + //msg.sender is user or pool token (lending pool) -> send to user + else { + pool.poolToken.safeTransfer(userAddress, _amount); + } + + _updateRewardDebt(pool, user); + emit Withdraw(userAddress, _poolToken, _amount); + } + + function _getUserAddress(address _user) internal view returns (address) { + address userAddress = msg.sender; + if (_user != address(0)) { + //only wrapper can pass _user parameter + require(msg.sender == wrapper || poolIdList[msg.sender] != 0, "only wrapper or pools may withdraw for a user"); + userAddress = _user; + } + return userAddress; + } + + function _updateReward(PoolInfo storage pool, UserInfo storage user) internal { + //update user accumulated reward + if (user.amount > 0) { + //add reward for the previous amount of deposited tokens + uint256 accumulatedReward = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); + user.accumulatedReward = user.accumulatedReward.add(accumulatedReward); + } + } + + function _updateRewardDebt(PoolInfo storage pool, UserInfo storage user) internal { + //reward accumulated before amount update (should be subtracted during next reward calculation) + user.rewardDebt = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Send reward in SOV to the lockedSOV vault. + * @param _user The user info, to get its reward share. + * @param _userAddress The address of the user, to send SOV in its behalf. + * @param _isStakingTokens The flag whether we need to stake tokens + * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough + */ + function _transferReward( + address _poolToken, + UserInfo storage _user, + address _userAddress, + bool _isStakingTokens, + bool _isCheckingBalance + ) internal { + uint256 userAccumulatedReward = _user.accumulatedReward; + + /// @dev Transfer if enough SOV balance on this LM contract. + uint256 balance = SOV.balanceOf(address(this)); + if (balance >= userAccumulatedReward) { + totalUsersBalance = totalUsersBalance.sub(userAccumulatedReward); + _user.accumulatedReward = 0; + + /// @dev Instead of transferring the reward to the LP (user), + /// deposit it into lockedSOV vault contract, but first + /// SOV deposit must be approved to move the SOV tokens + /// from this LM contract into the lockedSOV vault. + require(SOV.approve(address(lockedSOV), userAccumulatedReward), "Approve failed"); + lockedSOV.deposit(_userAddress, userAccumulatedReward, unlockedImmediatelyPercent); + + if (_isStakingTokens) { + lockedSOV.withdrawAndStakeTokensFrom(_userAddress); + } + + /// @dev Event log. + emit RewardClaimed(_userAddress, _poolToken, userAccumulatedReward); + } else { + require(!_isCheckingBalance, "Claiming reward failed"); + } + } + + /** + * @notice withdraws pool tokens without transferring reward tokens + * @param _poolToken the address of pool token + * @dev EMERGENCY ONLY + */ + function emergencyWithdraw(address _poolToken) external onlyBeforeMigrationGracePeriodFinished { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][msg.sender]; + + _updatePool(poolId); + _updateReward(pool, user); + + totalUsersBalance = totalUsersBalance.sub(user.accumulatedReward); + uint256 userAmount = user.amount; + uint256 userAccumulatedReward = user.accumulatedReward; + user.amount = 0; + user.rewardDebt = 0; + user.accumulatedReward = 0; + pool.poolToken.safeTransfer(address(msg.sender), userAmount); + + _updateRewardDebt(pool, user); + + emit EmergencyWithdraw(msg.sender, _poolToken, userAmount, userAccumulatedReward); + } + + /** + * @notice returns pool id + * @param _poolToken the address of pool token + */ + function getPoolId(address _poolToken) external view returns (uint256) { + return _getPoolId(_poolToken); + } + + function _getPoolId(address _poolToken) internal view returns (uint256) { + uint256 poolId = poolIdList[_poolToken]; + require(poolId > 0, "Pool token not found"); + return poolId - 1; + } + + /** + * @notice returns count of pool tokens + */ + function getPoolLength() external view returns (uint256) { + return poolInfoList.length; + } + + /** + * @notice returns list of pool token's info + */ + function getPoolInfoList() external view returns (PoolInfo[] memory) { + return poolInfoList; + } + + /** + * @notice returns pool info for the given token + * @param _poolToken the address of pool token + */ + function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return poolInfoList[poolId]; + } + + /** + * @notice returns list of [amount, accumulatedReward] for the given user for each pool token + * @param _user the address of the user + */ + function getUserBalanceList(address _user) external view returns (uint256[2][] memory) { + uint256 length = poolInfoList.length; + uint256[2][] memory userBalanceList = new uint256[2][](length); + for (uint256 i = 0; i < length; i++) { + userBalanceList[i][0] = userInfoMap[i][_user].amount; + userBalanceList[i][1] = _getUserAccumulatedReward(i, _user); + } + return userBalanceList; + } + + /** + * @notice returns UserInfo for the given pool and user + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserInfo(address _poolToken, address _user) public view returns (UserInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return userInfoMap[poolId][_user]; + } + + /** + * @notice returns list of UserInfo for the given user for each pool token + * @param _user the address of the user + */ + function getUserInfoList(address _user) external view returns (UserInfo[] memory) { + uint256 length = poolInfoList.length; + UserInfo[] memory userInfoList = new UserInfo[](length); + for (uint256 i = 0; i < length; i++) { + userInfoList[i] = userInfoMap[i][_user]; + } + return userInfoList; + } + + /** + * @notice returns accumulated reward for the given user for each pool token + * @param _user the address of the user + */ + function getUserAccumulatedRewardList(address _user) external view returns (uint256[] memory) { + uint256 length = poolInfoList.length; + uint256[] memory rewardList = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + rewardList[i] = _getUserAccumulatedReward(i, _user); + } + return rewardList; + } + + /** + * @notice returns the pool token balance a user has on the contract + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256) { + UserInfo memory ui = getUserInfo(_poolToken, _user); + return ui.amount; + } + + /** + * @notice returns arrays with all the pools on the contract + */ + function getPoolInfoListArray() + external + view + returns ( + address[] memory _poolToken, + uint96[] memory _allocationPoints, + uint256[] memory _lastRewardBlock, + uint256[] memory _accumulatedRewardPerShare + ) + { + uint256 length = poolInfoList.length; + _poolToken = new address[](length); + _allocationPoints = new uint96[](length); + _lastRewardBlock = new uint256[](length); + _accumulatedRewardPerShare = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + _poolToken[i] = address(poolInfoList[i].poolToken); + _allocationPoints[i] = poolInfoList[i].allocationPoint; + _lastRewardBlock[i] = poolInfoList[i].lastRewardBlock; + _accumulatedRewardPerShare[i] = poolInfoList[i].accumulatedRewardPerShare; + } + return (_poolToken, _allocationPoints, _lastRewardBlock, _accumulatedRewardPerShare); + } + + /** + * @notice returns all pools that a user has on the contract, the poolId it's the index of arrays + * @param _user the address of the user + */ + function getUserInfoListArray(address _user) + external + view + returns ( + uint256[] memory _amount, + uint256[] memory _rewardDebt, + uint256[] memory _accumulatedReward + ) + { + uint256 length = poolInfoList.length; + _amount = new uint256[](length); + _rewardDebt = new uint256[](length); + _accumulatedReward = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + _amount[i] = userInfoMap[i][_user].amount; + _rewardDebt[i] = userInfoMap[i][_user].rewardDebt; + _accumulatedReward[i] = userInfoMap[i][_user].accumulatedReward; + } + return (_amount, _rewardDebt, _accumulatedReward); + } + + /** + * @notice send all funds from this contract to LiquidityMiningV2 + */ + function migrateFunds() external onlyAuthorized onlyAfterMigrationFinished { + require(liquidityMiningV2 != address(0), "Address not initialized"); + uint256 SOVBalance = SOV.balanceOf(address(this)); + transferSOV(liquidityMiningV2, SOVBalance); + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + IERC20 poolToken = poolInfoList[i].poolToken; + uint256 balancePoolToken = poolToken.balanceOf(address(this)); + poolToken.safeTransfer(liquidityMiningV2, balancePoolToken); + } + } + + /** + * @notice return reward token total users balance + */ + function getTotalUsersBalance() external view returns (uint256) { + return totalUsersBalance; + } + + /** + * @notice return reward token start block + */ + function getStartBlock() external view returns (uint256) { + return startBlock; + } +} diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol new file mode 100644 index 000000000..0b9a4b5ef --- /dev/null +++ b/contracts/farm/LiquidityMiningV2.sol @@ -0,0 +1,982 @@ +pragma solidity 0.5.17; +pragma experimental ABIEncoderV2; + +import "../openzeppelin/ERC20.sol"; +import "../openzeppelin/SafeERC20.sol"; +import "../openzeppelin/SafeMath.sol"; +import "./LiquidityMiningStorageV2.sol"; +import "./IRewardTransferLogic.sol"; +import "./ILiquidityMiningV2.sol"; + +/// @notice This contract is a new liquidity mining version that let's the user +/// to earn multiple reward tokens by staking LP tokens as opposed to the +/// previous one that only rewarded SOV +contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* Constants */ + + uint256 public constant PRECISION = 1e12; + + uint256 public constant SECONDS_PER_BLOCK = 30; + + /* Events */ + + event RewardTransferred(address indexed rewardToken, address indexed receiver, uint256 amount); + event PoolTokenAdded(address indexed user, address indexed poolToken, address[] rewardTokens, uint96[] allocationPoints); + event PoolTokenUpdated( + address indexed user, + address indexed poolToken, + address indexed rewardToken, + uint96 newAllocationPoint, + uint96 oldAllocationPoint + ); + event PoolTokenAssociation(address indexed user, uint256 indexed poolId, address indexed rewardToken, uint96 allocationPoint); + event Deposit(address indexed user, address indexed poolToken, uint256 amount); + event RewardClaimed(address indexed user, address indexed rewardToken, uint256 amount); + event Withdraw(address indexed user, address indexed poolToken, uint256 amount); + event EmergencyWithdraw( + address indexed user, + address indexed poolToken, + address indexed rewardToken, + uint256 amount, + uint256 accumulatedReward + ); + + /* Functions */ + + /** + * @notice Initialize mining. + */ + function initialize( + address _wrapper, + address _migrator, + IERC20 _SOV + ) external onlyAuthorized { + /// @dev Non-idempotent function. Must be called just once. + require(_migrator != address(0), "invalid contract address"); + require(address(_SOV) != address(0), "invalid token address"); + wrapper = _wrapper; + migrator = _migrator; + } + + /** + * @notice Add a new reward token + * + * @param _rewardToken The token to be rewarded to LP stakers. + * @param _rewardTokensPerBlock The number of reward tokens per block. + * @param _startDelayBlocks The number of blocks should be passed to start + * mining. + */ + function addRewardToken( + address _rewardToken, + uint256 _rewardTokensPerBlock, + uint256 _startDelayBlocks, + address _rewardTransferLogic + ) external onlyAuthorized { + /// @dev Non-idempotent function. Must be called just once. + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + require(rewardToken.startBlock == 0, "Already added"); + require(address(_rewardToken) != address(0), "Invalid token address"); + require(_startDelayBlocks > 0, "Invalid start block"); + + IRewardTransferLogic rewardTransferLogic = IRewardTransferLogic(_rewardTransferLogic); + require(_rewardToken == rewardTransferLogic.getRewardTokenAddress(), "Reward token and transfer logic mismatch"); + rewardTokensMap[_rewardToken] = RewardToken({ + rewardTokensPerBlock: _rewardTokensPerBlock, + startBlock: block.number + _startDelayBlocks, + endBlock: 0, + totalAllocationPoint: 0, + totalUsersBalance: 0, + rewardTransferLogic: rewardTransferLogic + }); + } + + /** + * @notice sets wrapper proxy contract + * @dev can be set to zero address to remove wrapper + */ + function setWrapper(address _wrapper) external onlyAuthorized { + wrapper = _wrapper; + } + + /** + * @notice stops mining by setting end block + */ + function stopMining(address _rewardToken) external onlyAuthorized { + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + require(rewardToken.startBlock != 0, "Not initialized"); + require(rewardToken.endBlock == 0, "Already stopped"); + + rewardToken.endBlock = block.number; + } + + /** + * @notice Transfers reward tokens to given address. + * Owner use this function to withdraw reward tokens from LM contract + * into another account. + * @param _rewardToken The address of the rewardToken + * @param _receiver The address of the tokens receiver. + * @param _amount The amount to be transferred. + * */ + function transferRewardTokens( + address _rewardToken, + address _receiver, + uint256 _amount + ) external onlyAuthorized { + require(_rewardToken != address(0), "Reward address invalid"); + require(_receiver != address(0), "Receiver address invalid"); + require(_amount != 0, "Amount invalid"); + + IERC20 rewardToken = IERC20(_rewardToken); + + /// @dev Do not transfer more SOV than available. + uint256 balance = rewardToken.balanceOf(address(this)); + if (_amount > balance) { + _amount = balance; + } + + /// @dev The actual transfer. + require(rewardToken.transfer(_receiver, _amount), "Transfer failed"); + + /// @dev Event log. + emit RewardTransferred(_rewardToken, _receiver, _amount); + } + + /** + * @notice Get the missed rewardTokens balance of LM contract. + * + * @return The amount of reward tokens according to totalUsersBalance + * in excess of actual balance of the LM contract. + * */ + function getMissedBalance(address _rewardToken) external view returns (uint256) { + IERC20 rewardToken = IERC20(_rewardToken); + uint256 totalUsersBalance = rewardTokensMap[_rewardToken].totalUsersBalance; + uint256 balance = rewardToken.balanceOf(address(this)); + return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); + } + + /** + * @notice adds a new lp to the pool. Can only be called by the owner or an admin + * @param _poolToken the address of pool token + * @param _rewardTokens the addresses of reward tokens for given pool + * @param _allocationPoints the allocation points (weight) for the given pool and each reward token + * @param _withUpdate the flag whether we need to update all pools + */ + function add( + address _poolToken, + address[] calldata _rewardTokens, + uint96[] calldata _allocationPoints, + bool _withUpdate + ) external onlyAuthorized { + require(_rewardTokens.length > 0, "Invalid reward tokens length"); + require(_rewardTokens.length == _allocationPoints.length, "Invalid allocation points length"); + require(_poolToken != address(0), "Invalid token address"); + require(poolIdList[_poolToken] == 0, "Token already added"); + + if (_withUpdate) { + updateAllPools(); + } + + poolInfoList.push(PoolInfo({ poolToken: IERC20(_poolToken), rewardTokens: _rewardTokens })); + //indexing starts from 1 in order to check whether token was already added + poolIdList[_poolToken] = poolInfoList.length; + + for (uint256 i = 0; i < _rewardTokens.length; i++) { + associatePoolToRewardToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); + } + + emit PoolTokenAdded(msg.sender, _poolToken, _rewardTokens, _allocationPoints); + } + + function associatePoolToRewardToken( + address _poolToken, + address _rewardToken, + uint96 _allocationPoint + ) internal { + uint256 poolId = _getPoolId(_poolToken); + + // Allocation point checks + require(_allocationPoint > 0, "Invalid allocation point"); + + // Reward token checks + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + uint256 startBlock = rewardToken.startBlock; + require(startBlock != 0, "Not initialized"); + + // Check association is not done twice + + require(poolInfoRewardTokensMap[poolId][_rewardToken].allocationPoint == 0, "Already associated"); + + uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; + rewardToken.totalAllocationPoint = rewardToken.totalAllocationPoint.add(_allocationPoint); + + poolInfoRewardTokensMap[poolId][_rewardToken] = PoolInfoRewardToken({ + allocationPoint: _allocationPoint, + lastRewardBlock: lastRewardBlock, + accumulatedRewardPerShare: 0 + }); + + emit PoolTokenAssociation(msg.sender, poolId, _rewardToken, _allocationPoint); + } + + /** + * @notice updates the given pool's reward tokens allocation point + * @param _poolToken the address of pool token + * @param _rewardTokens the addresses of reward tokens for given pool + * @param _allocationPoints the allocation points (weight) for the given pool and each reward token + * @param _updateAllFlag the flag whether we need to update all pools + */ + function update( + address _poolToken, + address[] memory _rewardTokens, + uint96[] memory _allocationPoints, + bool _updateAllFlag + ) public onlyAuthorized { + if (_updateAllFlag) { + updateAllPools(); + } else { + updatePool(_poolToken); + } + _updateTokens(_poolToken, _rewardTokens, _allocationPoints); + } + + function _updateTokens( + address _poolToken, + address[] memory _rewardTokens, + uint96[] memory _allocationPoints + ) internal { + for (uint256 i = 0; i < _rewardTokens.length; i++) { + _updateToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); + } + } + + function _updateToken( + address _poolToken, + address _rewardToken, + uint96 _allocationPoint + ) internal { + uint256 poolId = _getPoolId(_poolToken); + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; + + uint96 previousAllocationPoint = poolInfoRewardToken.allocationPoint; + rewardToken.totalAllocationPoint = rewardToken.totalAllocationPoint.sub(previousAllocationPoint).add(_allocationPoint); + poolInfoRewardToken.allocationPoint = _allocationPoint; + + emit PoolTokenUpdated(msg.sender, _poolToken, _rewardToken, _allocationPoint, previousAllocationPoint); + } + + /** + * @notice updates the given pools' reward tokens allocation points + * @param _poolTokens array of addresses of pool tokens + * @param _allocationPoints array of allocation points (weight) for the given pools + * @param _updateAllFlag the flag whether we need to update all pools + */ + function updateTokens( + address[] calldata _poolTokens, + address[][] calldata _rewardTokens, + uint96[][] calldata _allocationPoints, + bool _updateAllFlag + ) external onlyAuthorized { + require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); + require(_poolTokens.length == _rewardTokens.length, "Arrays mismatch"); + + if (_updateAllFlag) { + updateAllPools(); + } + uint256 length = _poolTokens.length; + for (uint256 i = 0; i < length; i++) { + require(_allocationPoints[i].length == _rewardTokens[i].length, "Arrays mismatch"); + _updateTokens(_poolTokens[i], _rewardTokens[i], _allocationPoints[i]); + } + } + + /** + * @notice returns reward multiplier over the given _from to _to block + * @param _from the first block for a calculation + * @param _to the last block for a calculation + */ + function _getPassedBlocks( + RewardToken storage _rewardToken, + uint256 _from, + uint256 _to + ) internal view returns (uint256) { + if (_from < _rewardToken.startBlock) { + _from = _rewardToken.startBlock; + } + + if (_rewardToken.endBlock > 0 && _to > _rewardToken.endBlock) { + _to = _rewardToken.endBlock; + } + + if (_to <= _from) { + return 0; + } + + return _to.sub(_from); + } + + function _getUserAccumulatedReward( + uint256 _poolId, + address _rewardToken, + address _user + ) internal view returns (uint256) { + PoolInfo storage pool = poolInfoList[_poolId]; + PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][_rewardToken]; + UserInfo storage user = userInfoMap[_poolId][_user]; + + uint256 accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare; + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (block.number > poolRewardToken.lastRewardBlock && poolTokenBalance != 0) { + (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardTokensMap[_rewardToken]); + accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); + } + return user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub(user.rewards[_rewardToken].rewardDebt); + } + + /** + * @notice returns accumulated reward + * @param _poolToken the address of pool token + * @param _rewardToken the reward token address + * @param _user the user address + */ + function getUserAccumulatedReward( + address _poolToken, + address _rewardToken, + address _user + ) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + return _getUserAccumulatedReward(poolId, _rewardToken, _user); + } + + /** + * @notice returns estimated reward + * @param _poolToken the address of pool token + * @param _rewardToken the reward token address + * @param _amount the amount of tokens to be deposited + * @param _duration the duration of liquidity providing in seconds + */ + function getEstimatedReward( + address _poolToken, + address _rewardToken, + uint256 _amount, + uint256 _duration + ) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + uint256 start = block.number; + uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); + (, uint256 accumulatedRewardPerShare) = + _getPoolAccumulatedReward( + pool, + _amount, + rewardTokensMap[_rewardToken], + poolInfoRewardTokensMap[poolId][_rewardToken], + start, + end + ); + return _amount.mul(accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Updates reward variables for all pools. + * @dev Be careful of gas spending! + */ + function updateAllPools() public { + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + _updatePool(i); + } + } + + /** + * @notice Updates reward variables of the given pool to be up-to-date + * @param _poolToken the address of pool token + */ + function updatePool(address _poolToken) public { + uint256 poolId = _getPoolId(_poolToken); + _updatePool(poolId); + } + + function _updatePool(uint256 _poolId) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + _updatePoolRewardToken(pool, _poolId, pool.rewardTokens[i]); + } + } + + function _updatePoolRewardToken( + PoolInfo storage pool, + uint256 _poolId, + address _rewardToken + ) internal { + PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][_rewardToken]; + // this pool has been updated recently + if (block.number <= poolRewardToken.lastRewardBlock) { + return; + } + + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (poolTokenBalance == 0) { + poolRewardToken.lastRewardBlock = block.number; + return; + } + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + poolRewardToken.accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare.add(accumulatedRewardPerShare_); + poolRewardToken.lastRewardBlock = block.number; + + rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.add(accumulatedReward_); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + PoolInfoRewardToken storage _poolRewardToken, + RewardToken storage _rewardToken + ) internal view returns (uint256, uint256) { + return _getPoolAccumulatedReward(_pool, 0, _rewardToken, _poolRewardToken, block.number); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + uint256 _additionalAmount, + RewardToken storage _rewardToken, + PoolInfoRewardToken storage _poolRewardToken, + uint256 _endBlock + ) internal view returns (uint256, uint256) { + return + _getPoolAccumulatedReward( + _pool, + _additionalAmount, + _rewardToken, + _poolRewardToken, + _poolRewardToken.lastRewardBlock, + _endBlock + ); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + uint256 _additionalAmount, + RewardToken storage _rewardToken, + PoolInfoRewardToken storage _poolRewardToken, + uint256 _startBlock, + uint256 _endBlock + ) internal view returns (uint256, uint256) { + uint256 passedBlocks = _getPassedBlocks(_rewardToken, _startBlock, _endBlock); + uint256 accumulatedReward = + passedBlocks.mul(_rewardToken.rewardTokensPerBlock).mul(PRECISION).mul(_poolRewardToken.allocationPoint).div( + _rewardToken.totalAllocationPoint + ); + + uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); + poolTokenBalance = poolTokenBalance.add(_additionalAmount); + uint256 accumulatedRewardPerShare = accumulatedReward.div(poolTokenBalance); + return (accumulatedReward.div(PRECISION), accumulatedRewardPerShare); + } + + /** + * @notice deposits pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it or to msg.sender + */ + function deposit( + address _poolToken, + uint256 _amount, + address _user + ) external { + require(migrationFinished, "Migration is not over yet"); + _deposit(_poolToken, _amount, _user, false); + } + + /** + * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit + * @dev only callable by the pool which issues the tokens + * @param _user the user address + * @param _amount the minted amount + */ + function onTokensDeposited(address _user, uint256 _amount) external { + //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert + _deposit(msg.sender, _amount, _user, true); + } + + /** + * @notice internal function for depositing pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it + * @param alreadyTransferred true if the pool tokens have already been transferred + */ + function _deposit( + address _poolToken, + uint256 _amount, + address _user, + bool alreadyTransferred + ) internal { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _user != address(0) ? _user : msg.sender; + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updatePoolRewardToken(pool, poolId, rewardTokenAddress); + //sends reward directly to the user + _updateReward(poolId, rewardTokenAddress, user); + } + + if (_amount > 0) { + //receives pool tokens from msg.sender, it can be user or WrapperProxy contract + if (!alreadyTransferred) pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); + user.amount = user.amount.add(_amount); + } + + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updateRewardDebt(poolId, rewardTokenAddress, user); + } + emit Deposit(userAddress, _poolToken, _amount); + } + + /** + * @notice transfers reward tokens + * @param _poolToken the address of pool token + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimRewards(address _poolToken, address _user) external { + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _claimReward(poolId, rewardTokenAddress, userAddress); + } + } + + /** + * @notice transfers rewards from a specific reward token + * @param _poolToken the address of pool token + * @param _rewardToken the address of reward token + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimReward( + address _poolToken, + address _rewardToken, + address _user + ) external { + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + _claimReward(poolId, _rewardToken, userAddress); + } + + function _claimReward( + uint256 _poolId, + address _rewardToken, + address _userAddress + ) internal { + UserInfo storage user = userInfoMap[_poolId][_userAddress]; + PoolInfo storage pool = poolInfoList[_poolId]; + + _updatePoolRewardToken(pool, _poolId, _rewardToken); + _updateReward(_poolId, _rewardToken, user); + _transferReward(_rewardToken, user, _userAddress, false, true); + _updateRewardDebt(_poolId, _rewardToken, user); + } + + /** + * @notice transfers reward tokens from all pools + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimRewardFromAllPools(address _user) external { + address userAddress = _getUserAddress(_user); + + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + uint256 poolId = i; + PoolInfo storage pool = poolInfoList[poolId]; + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 j = 0; j < rewardTokensLength; j++) { + address rewardTokenAddress = pool.rewardTokens[j]; + _claimReward(poolId, rewardTokenAddress, userAddress); + } + } + } + + /** + * @notice withdraws pool tokens and transfers reward tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) + */ + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + require(user.amount >= _amount, "Not enough balance"); + + // Start collecting rewards for each reward token the user holds + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updatePoolRewardToken(pool, poolId, rewardTokenAddress); + _updateReward(poolId, rewardTokenAddress, user); + _transferReward(rewardTokenAddress, user, userAddress, true, false); + } + user.amount = user.amount.sub(_amount); + + //msg.sender is wrapper -> send to wrapper + if (msg.sender == wrapper) { + pool.poolToken.safeTransfer(address(msg.sender), _amount); + } + //msg.sender is user or pool token (lending pool) -> send to user + else { + pool.poolToken.safeTransfer(userAddress, _amount); + } + + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updateRewardDebt(poolId, rewardTokenAddress, user); + } + emit Withdraw(userAddress, _poolToken, _amount); + } + + function _getUserAddress(address _user) internal view returns (address) { + address userAddress = msg.sender; + if (_user != address(0)) { + //only wrapper can pass _user parameter + require(msg.sender == wrapper || poolIdList[msg.sender] != 0, "only wrapper or pools may withdraw for a user"); + userAddress = _user; + } + return userAddress; + } + + function _updateReward( + uint256 _poolId, + address _rewardTokenAddress, + UserInfo storage user + ) internal { + UserReward storage reward = user.rewards[_rewardTokenAddress]; + //update user accumulated reward + if (user.amount > 0) { + //add reward for the previous amount of deposited tokens + uint256 accumulatedReward = + user.amount.mul(poolInfoRewardTokensMap[_poolId][_rewardTokenAddress].accumulatedRewardPerShare).div(PRECISION).sub( + reward.rewardDebt + ); + reward.accumulatedReward = reward.accumulatedReward.add(accumulatedReward); + } + } + + function _updateRewardDebt( + uint256 poolId, + address rewardToken, + UserInfo storage user + ) internal { + //reward accumulated before amount update (should be subtracted during next reward calculation) + user.rewards[rewardToken].rewardDebt = user.amount.mul(poolInfoRewardTokensMap[poolId][rewardToken].accumulatedRewardPerShare).div( + PRECISION + ); + } + + /** + * @notice Send reward in SOV to the lockedSOV vault. + * @param _user The user info, to get its reward share. + * @param _userAddress The address of the user, to send SOV in its behalf. + * @param _isWithdrawal The flag whether determines if the user is withdrawing all the funds + * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough + */ + function _transferReward( + address _rewardToken, + UserInfo storage _user, + address _userAddress, + bool _isWithdrawal, + bool _isCheckingBalance + ) internal { + uint256 userAccumulatedReward = _user.rewards[_rewardToken].accumulatedReward; + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + IERC20 token = IERC20(_rewardToken); + /// @dev Transfer if enough token balance on this LM contract. + uint256 balance = token.balanceOf(address(this)); + if (balance >= userAccumulatedReward) { + rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.sub(userAccumulatedReward); + _user.rewards[_rewardToken].accumulatedReward = 0; + + IRewardTransferLogic transferLogic = rewardToken.rewardTransferLogic; + require(token.approve(transferLogic.senderToAuthorize(), userAccumulatedReward), "Approve failed"); + transferLogic.transferReward(_userAddress, userAccumulatedReward, _isWithdrawal); + /// @dev Event log. + emit RewardClaimed(_userAddress, _rewardToken, userAccumulatedReward); + } else { + require(!_isCheckingBalance, "Claiming reward failed"); + } + } + + /** + * @notice withdraws pool tokens without transferring reward tokens + * @param _poolToken the address of pool token + * @dev EMERGENCY ONLY + */ + function emergencyWithdraw(address _poolToken) external { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][msg.sender]; + + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updatePoolRewardToken(pool, poolId, rewardTokenAddress); + _updateReward(poolId, rewardTokenAddress, user); + // substract user balance from total balance for each reward token + UserReward storage userReward = user.rewards[pool.rewardTokens[i]]; + uint256 accumulatedReward = userReward.accumulatedReward; + RewardToken storage rewardToken = rewardTokensMap[pool.rewardTokens[i]]; + rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.sub(accumulatedReward); + emit EmergencyWithdraw(msg.sender, _poolToken, rewardTokenAddress, user.amount, accumulatedReward); + + userReward.rewardDebt = 0; + userReward.accumulatedReward = 0; + } + + uint256 userAmount = user.amount; + user.amount = 0; + pool.poolToken.safeTransfer(address(msg.sender), userAmount); + + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updateRewardDebt(poolId, rewardTokenAddress, user); + } + } + + function getRewardToken(address _rewardToken) external view returns (RewardToken memory) { + return rewardTokensMap[_rewardToken]; + } + + /** + * @notice returns a list of PoolInfoRewardToken for the given pool + * @param _poolToken the address of pool token + */ + function getPoolRewards(address _poolToken) external view returns (PoolInfoRewardToken[] memory) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo memory poolInfo = poolInfoList[poolId]; + uint256 rewardsLength = poolInfo.rewardTokens.length; + PoolInfoRewardToken[] memory rewards = new PoolInfoRewardToken[](rewardsLength); + for (uint256 i = 0; i < rewardsLength; i++) { + rewards[i] = poolInfoRewardTokensMap[poolId][poolInfo.rewardTokens[i]]; + } + return rewards; + } + + /** + * @notice returns a PoolInfoRewardToken for the given pool and reward token + * @param _poolToken the address of pool token + * @param _rewardToken the address of reward token + */ + function getPoolReward(address _poolToken, address _rewardToken) public view returns (PoolInfoRewardToken memory) { + uint256 poolId = _getPoolId(_poolToken); + return poolInfoRewardTokensMap[poolId][_rewardToken]; + } + + /** + * @notice returns pool id + * @param _poolToken the address of pool token + */ + function getPoolId(address _poolToken) external view returns (uint256) { + return _getPoolId(_poolToken); + } + + function _getPoolId(address _poolToken) internal view returns (uint256) { + uint256 poolId = poolIdList[_poolToken]; + require(poolId > 0, "Pool token not found"); + return poolId - 1; + } + + /** + * @notice returns count of pool tokens + */ + function getPoolLength() external view returns (uint256) { + return poolInfoList.length; + } + + /** + * @notice returns list of pool token's info + */ + function getPoolInfoList() external view returns (PoolInfo[] memory) { + return poolInfoList; + } + + /** + * @notice returns pool info for the given token + * @param _poolToken the address of pool token + */ + function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return poolInfoList[poolId]; + } + + struct UserBalance { + uint256 amount; + address rewardToken; + uint256 accumulatedReward; + } + + /** + * @notice returns list of [amount, rewardToken, accumulatedReward] for the given user for each pool token and reward token + * @param _user the address of the user + */ + function getUserBalanceList(address _user) external view returns (UserBalance[][] memory) { + uint256 length = poolInfoList.length; + UserBalance[][] memory userBalanceList = new UserBalance[][](length); + for (uint256 i = 0; i < length; i++) { + PoolInfo memory poolInfo = poolInfoList[i]; + uint256 rewardLength = poolInfo.rewardTokens.length; + userBalanceList[i] = new UserBalance[](rewardLength); + for (uint256 j = 0; j < rewardLength; j++) { + address _rewardToken = poolInfo.rewardTokens[j]; + userBalanceList[i][j].amount = userInfoMap[i][_user].amount; + userBalanceList[i][j].rewardToken = _rewardToken; + userBalanceList[i][j].accumulatedReward = _getUserAccumulatedReward(i, _rewardToken, _user); + } + } + return userBalanceList; + } + + struct PoolUserInfo { + uint256 amount; + UserReward[] rewards; + } + + /** + * @notice returns UserInfo for the given pool and user + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserInfo(address _poolToken, address _user) public view returns (PoolUserInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return _getPoolUserInfo(poolId, _user); + } + + /** + * @notice returns list of UserInfo for the given user for each pool token + * @param _user the address of the user + */ + function getUserInfoList(address _user) external view returns (PoolUserInfo[] memory) { + uint256 length = poolInfoList.length; + PoolUserInfo[] memory userInfoList = new PoolUserInfo[](length); + for (uint256 i = 0; i < length; i++) { + userInfoList[i] = _getPoolUserInfo(i, _user); + } + return userInfoList; + } + + function _getPoolUserInfo(uint256 _poolId, address _user) internal view returns (PoolUserInfo memory) { + PoolInfo memory pool = poolInfoList[_poolId]; + uint256 rewardsLength = pool.rewardTokens.length; + UserInfo storage userInfo = userInfoMap[_poolId][_user]; + PoolUserInfo memory poolUserInfo; + poolUserInfo.amount = userInfo.amount; + poolUserInfo.rewards = new UserReward[](rewardsLength); + for (uint256 i = 0; i < rewardsLength; i++) { + poolUserInfo.rewards[i] = userInfo.rewards[pool.rewardTokens[i]]; + } + return poolUserInfo; + } + + struct UserAccumulatedReward { + address rewardToken; + uint256 accumulatedReward; + } + + /** + * @notice returns accumulated reward for the given user for each pool token and reward token + * @param _user the address of the user + */ + function getUserAccumulatedRewardList(address _user) external view returns (UserAccumulatedReward[][] memory) { + uint256 length = poolInfoList.length; + UserAccumulatedReward[][] memory rewardList = new UserAccumulatedReward[][](length); + for (uint256 i = 0; i < length; i++) { + PoolInfo memory poolInfo = poolInfoList[i]; + uint256 rewardsLength = poolInfo.rewardTokens.length; + rewardList[i] = new UserAccumulatedReward[](rewardsLength); + for (uint256 j = 0; j < rewardsLength; j++) { + rewardList[i][j].rewardToken = poolInfo.rewardTokens[j]; + rewardList[i][j].accumulatedReward = _getUserAccumulatedReward(i, poolInfo.rewardTokens[j], _user); + } + } + return rewardList; + } + + /** + * @notice returns the pool token balance a user has on the contract + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + return userInfoMap[poolId][_user].amount; + } + + function setPoolInfoRewardToken( + address _poolToken, + address _rewardToken, + uint256 _lastRewardBlock, + uint256 _accumulatedRewardPerShare + ) external onlyAuthorized { + require(msg.sender == migrator, "only allowed to migrator contract"); + uint256 poolId = _getPoolId(_poolToken); + PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; + poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; + poolInfoRewardToken.accumulatedRewardPerShare = _accumulatedRewardPerShare; + } + + function setRewardToken( + address _rewardToken, + uint256 _startBlock, + uint256 _totalUsersBalance + ) external onlyAuthorized { + require(msg.sender == migrator, "only allowed to migrator contract"); + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + rewardToken.startBlock = _startBlock; + rewardToken.totalUsersBalance = _totalUsersBalance; + } + + function setUserInfo( + uint256 _poolId, + address _user, + address _rewardToken, + uint256 _amount, + uint256 _rewardDebt, + uint256 _accumulatedReward + ) external onlyAuthorized { + require(msg.sender == migrator, "only allowed to migrator contract"); + UserInfo storage userInfo = userInfoMap[_poolId][_user]; + userInfo.amount = _amount; + userInfo.rewards[_rewardToken] = UserReward(_rewardDebt, _accumulatedReward); + } + + /** + * @notice finish migration + */ + function finishMigration() external onlyAuthorized { + require(msg.sender == migrator, "only allowed to migrator contract"); + migrationFinished = true; + } +} diff --git a/contracts/farm/LockedSOVRewardTransferLogic.sol b/contracts/farm/LockedSOVRewardTransferLogic.sol new file mode 100644 index 000000000..0ef97ecc7 --- /dev/null +++ b/contracts/farm/LockedSOVRewardTransferLogic.sol @@ -0,0 +1,66 @@ +pragma solidity 0.5.17; + +import "./IRewardTransferLogic.sol"; +import "./LockedSOVRewardTransferLogicStorage.sol"; +import "../locked/ILockedSOV.sol"; + +import "../openzeppelin/ERC20.sol"; +import "../openzeppelin/SafeERC20.sol"; + +contract LockedSOVRewardTransferLogic is IRewardTransferLogic, LockedSOVRewardTransferLogicStorage { + using SafeERC20 for IERC20; + + event LockedSOVChanged(address _newAddress); + event UnlockImmediatelyPercentChanged(uint256 _newAmount); + + /** + * @param _lockedSOV The contract instance address of the lockedSOV vault. + * SOV rewards are not paid directly to liquidity providers. Instead they + * are deposited into a lockedSOV vault contract. + * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. + */ + function initialize(address _lockedSOV, uint256 _unlockedImmediatelyPercent) public onlyAuthorized { + changeLockedSOV(_lockedSOV); + changeUnlockedImmediatelyPercent(_unlockedImmediatelyPercent); + } + + /** + * @param _newLockedSOV The contract instance address of the lockedSOV vault. + */ + function changeLockedSOV(address _newLockedSOV) public onlyAuthorized { + require(_newLockedSOV != address(0), "Invalid address"); + lockedSOV = ILockedSOV(_newLockedSOV); + emit LockedSOVChanged(_newLockedSOV); + } + + /** + * @param _newUnlockedImmediatelyPercent The new unlocked immediately percent. + */ + function changeUnlockedImmediatelyPercent(uint256 _newUnlockedImmediatelyPercent) public onlyAuthorized { + require(_newUnlockedImmediatelyPercent < 10000, "Unlocked immediately percent has to be less than 10000."); + unlockedImmediatelyPercent = _newUnlockedImmediatelyPercent; + emit UnlockImmediatelyPercentChanged(_newUnlockedImmediatelyPercent); + } + + function getRewardTokenAddress() external view returns (address) { + return lockedSOV.SOV(); + } + + function senderToAuthorize() external view returns (address) { + return address(this); + } + + function transferReward( + address _to, + uint256 _value, + bool _isWithdrawal + ) external { + IERC20 token = IERC20(lockedSOV.SOV()); + require(token.transferFrom(msg.sender, address(this), _value), "Transfer failed"); + require(token.approve(address(lockedSOV), _value), "Approve failed"); + lockedSOV.deposit(_to, _value, unlockedImmediatelyPercent); + if (!_isWithdrawal) { + lockedSOV.withdrawAndStakeTokensFrom(_to); + } + } +} diff --git a/contracts/farm/LockedSOVRewardTransferLogicStorage.sol b/contracts/farm/LockedSOVRewardTransferLogicStorage.sol new file mode 100644 index 000000000..777878b5c --- /dev/null +++ b/contracts/farm/LockedSOVRewardTransferLogicStorage.sol @@ -0,0 +1,13 @@ +pragma solidity 0.5.17; + +import "../locked/ILockedSOV.sol"; +import "../utils/AdminRole.sol"; + +contract LockedSOVRewardTransferLogicStorage is AdminRole { + /// LockedSOV vault when reward transfer is performed + ILockedSOV public lockedSOV; + + /// Determines the amount of tokens that will be unlocked and ready + /// to be withdrawn + uint256 public unlockedImmediatelyPercent; +} diff --git a/contracts/locked/ILockedSOV.sol b/contracts/locked/ILockedSOV.sol index b0cd1bd43..32b0217a5 100644 --- a/contracts/locked/ILockedSOV.sol +++ b/contracts/locked/ILockedSOV.sol @@ -31,4 +31,9 @@ interface ILockedSOV { * @param _userAddress The address of user tokens will be withdrawn. */ function withdrawAndStakeTokensFrom(address _userAddress) external; + + /** + * @notice Returns the SOV address this contract is interacting with + */ + function SOV() external view returns (address); } diff --git a/contracts/mockup/LiquidityMiningV1Mockup.sol b/contracts/mockup/LiquidityMiningV1Mockup.sol new file mode 100644 index 000000000..022842368 --- /dev/null +++ b/contracts/mockup/LiquidityMiningV1Mockup.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "../farm/LiquidityMiningV1.sol"; + +contract LiquidityMiningV1Mockup is LiquidityMiningV1 { + function getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) public view returns (uint256) { + return _getPassedBlocksWithBonusMultiplier(_from, _to); + } + + function getPoolAccumulatedReward(address _poolToken) public view returns (uint256, uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + return _getPoolAccumulatedReward(pool); + } +} diff --git a/contracts/mockup/LiquidityMiningV2Mockup.sol b/contracts/mockup/LiquidityMiningV2Mockup.sol new file mode 100644 index 000000000..69b5dfc96 --- /dev/null +++ b/contracts/mockup/LiquidityMiningV2Mockup.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "../farm/LiquidityMiningV2.sol"; + +contract LiquidityMiningMockupV2 is LiquidityMiningV2 { + function getPoolAccumulatedReward(address _poolToken, address _rewardToken) public view returns (uint256, uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + return _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + } +} diff --git a/contracts/mockup/RBTCWrapperProxyMockupV2.sol b/contracts/mockup/RBTCWrapperProxyMockupV2.sol new file mode 100644 index 000000000..34af16ffd --- /dev/null +++ b/contracts/mockup/RBTCWrapperProxyMockupV2.sol @@ -0,0 +1,23 @@ +pragma solidity 0.5.17; + +import "../farm/LiquidityMiningV2.sol"; + +contract RBTCWrapperProxyMockupV2 { + LiquidityMiningV2 public liquidityMining; + + constructor(LiquidityMiningV2 _liquidityMining) public { + liquidityMining = _liquidityMining; + } + + function claimReward(address _poolToken) public { + liquidityMining.claimRewards(_poolToken, msg.sender); + } + + function claimRewardFromAllPools() public { + liquidityMining.claimRewardFromAllPools(msg.sender); + } + + function withdraw(address _poolToken, uint256 _amount) public { + liquidityMining.withdraw(_poolToken, _amount, msg.sender); + } +} diff --git a/contracts/mockup/TestPoolToken.sol b/contracts/mockup/TestPoolToken.sol new file mode 100644 index 000000000..57c56ad64 --- /dev/null +++ b/contracts/mockup/TestPoolToken.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.5.17; + +import "../farm/LiquidityMiningV2.sol"; +import "../testhelpers/TestToken.sol"; + +contract TestPoolToken is TestToken { + LiquidityMiningV2 private liquidityMining; + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _initialAmount, + address _liquidityMining + ) public TestToken(_name, _symbol, _decimals, _initialAmount) { + setLiquidityMining(_liquidityMining); + } + + function setLiquidityMining(address liquidityMining_) public { + liquidityMining = LiquidityMiningV2(liquidityMining_); + } + + function depositFor(address _user, uint256 _amount) external { + liquidityMining.onTokensDeposited(_user, _amount); + } +} diff --git a/diagrams/LiquidityMiningMigration.puml b/diagrams/LiquidityMiningMigration.puml new file mode 100644 index 000000000..4f1f4834c --- /dev/null +++ b/diagrams/LiquidityMiningMigration.puml @@ -0,0 +1,27 @@ +@startuml + +Admin -> LiquidityMiningV2: addRewardToken(address _SOV) +Admin -> LiquidityMiningV1: startMigrationGracePeriod() +User -> LiquidityMiningV1: withdraw() or claim() +Admin -> LMV1toLMV2Migrator: migratePools() +LMV1toLMV2Migrator -> LiquidityMiningV1: finishMigrationGracePeriod() +LMV1toLMV2Migrator -> LiquidityMiningV1: getPoolInfoListArray() +LiquidityMiningV1 -> LMV1toLMV2Migrator: poolInfo[] +LMV1toLMV2Migrator -> LiquidityMiningV2: adds pool info (allocation point, lastReward) +LMV1toLMV2Migrator -> LMV1toLMV2Migrator: finish pools migration +loop N batches + Admin -> LMV1toLMV2Migrator: migrateUsers() + LMV1toLMV2Migrator -> LiquidityMiningV1: getUserInfoListArray() + LiquidityMiningV1 -> LMV1toLMV2Migrator: userInfo[] + LMV1toLMV2Migrator -> LiquidityMiningV2: adds user info (amount, rewards) +end +Admin -> LMV1toLMV2Migrator: finish users migration +Admin -> LMV1toLMV2Migrator: migrateFunds() +LMV1toLMV2Migrator -> LiquidityMiningV1: migrateFunds() +LiquidityMiningV1 -> SOV: transfer(LiquidityMiningV2, balanceOf(LiquidityMiningV1)) +loop for each reward token + LiquidityMiningV1 -> ERC20: transfer(LiquidityMiningV2, balanceOf(LiquidityMiningV1)) +end +LMV1toLMV2Migrator -> LiquidityMiningV2: finish migration +LMV1toLMV2Migrator -> LMV1toLMV2Migrator: finish migration +@enduml diff --git a/diagrams/LiquidityMiningStateDiagram.puml b/diagrams/LiquidityMiningStateDiagram.puml new file mode 100644 index 000000000..aa53bea87 --- /dev/null +++ b/diagrams/LiquidityMiningStateDiagram.puml @@ -0,0 +1,28 @@ +@startuml + +[*] --> GracePeriodStarted +GracePeriodStarted --> GracePeriodFinished +GracePeriodFinished -> MigratingPools +MigratingPools --> MigratingUsers +MigratingUsers --> MigratingFunds +MigratingFunds --> MigrationFinished +MigrationFinished -> [*] + +GracePeriodStarted: LiquidityV1 deposits disabled +GracePeriodStarted: LiquidityV1 withdrawals +GracePeriodStarted: LiquidityV1 claims enabled +GracePeriodStarted: LiquidityV2 deposits disabled + +GracePeriodFinished: LiquidityV1 withdrawals +GracePeriodFinished: LiquidityV1 claims disabled + +note right of MigratingFunds + There is no need to disable claims or withdrawals + on LiquidityMiningV2 prior funds are moved as + there is no balance in LiquidityMiningV2. +end note + +MigrationFinished: LiquidityV2 deposits enabled +MigrationFinished: LiquidityV2 claims enabled +MigrationFinished: LiquidityV2 claims enabled +@enduml diff --git a/hardhat.config.js b/hardhat.config.js index 3cf5be2fc..37f62059a 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -61,7 +61,10 @@ module.exports = { disambiguatePaths: false, }, networks: { - hardhat: {}, + hardhat: { + // hardfork: 'london' + // hardfork: process.env.COVERAGE ? 'berlin' : 'london', + }, rskPublicTestnet: { url: "https://public-node.testnet.rsk.co/", accounts: { mnemonic: "brownie", count: 10 }, diff --git a/package.json b/package.json index 3fea3be90..28060cf63 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "scripts": { "analyze-contracts": "slither .", "contract-size": "yarn run hardhat size-contracts", - "coverage": "npx hardhat coverage", + "coverage": "env COVERAGE=true npx hardhat coverage", "doc": "yarn run hardhat docgen", "lint": "npm run lint-sol && npm run lint-js", "lint-sol": "solhint contracts/{*,**/*,**/**/*,**/**/**/*,**/**/**/**/*}.sol", diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index 6c40ba892..9ac7f43a3 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -21,6 +21,8 @@ from scripts.contractInteraction.ownership import * from scripts.contractInteraction.misc import * from scripts.contractInteraction.prices import * +from scripts.contractInteraction.liquidity_miningV2 import * +from scripts.contractInteraction.liquidity_mining_V1toV2_migrator import * def main(): diff --git a/scripts/contractInteraction/liquidity_miningV2.py b/scripts/contractInteraction/liquidity_miningV2.py new file mode 100644 index 000000000..8bf747b71 --- /dev/null +++ b/scripts/contractInteraction/liquidity_miningV2.py @@ -0,0 +1,60 @@ +from brownie import * +from brownie.network.contract import InterfaceContainer +import json +import time; +import copy +from scripts.utils import * +import scripts.contractInteraction.config as conf + +def setLiquidityMiningV2AddressOnAllContracts(): + print("setting LM address") + setLiquidityMiningV2Address(conf.contracts['iDOC']) + setLiquidityMiningV2Address(conf.contracts['iUSDT']) + setLiquidityMiningV2Address(conf.contracts['iBPro']) + setLiquidityMiningV2Address(conf.contracts['iXUSD']) + setLiquidityMiningV2Address(conf.contracts['iRBTC']) + +def getLiquidityMiningV2Address(loanTokenAddress): + loanToken = Contract.from_abi("loanToken", address=loanTokenAddress, abi=LoanTokenLogicLM.abi, owner=conf.acct) + print(loanToken.liquidityMiningAddress()) + print(loanToken.target_()) + +def setLiquidityMiningV2Address(loanTokenAddress): + loanToken = Contract.from_abi("loanToken", address=loanTokenAddress, abi=LoanTokenLogicLM.abi, owner=conf.acct) + data = loanToken.setLiquidityMiningAddress.encode_input(conf.contracts['LiquidityMiningProxyV2']) + + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + +def getLiquidityMiningV2AddressOnAllContracts(): + print("setting LM address") + getLiquidityMiningV2Address(conf.contracts['iDOC']) + getLiquidityMiningV2Address(conf.contracts['iUSDT']) + getLiquidityMiningV2Address(conf.contracts['iBPro']) + getLiquidityMiningV2Address(conf.contracts['iRBTC']) + +def setWrapperOnLMV2(): + lm = Contract.from_abi("LiquidityMiningV2", address = conf.contracts['LiquidityMiningProxyV2'], abi = LiquidityMiningV2.abi, owner = conf.acct) + + data = lm.setWrapper.encode_input(conf.contracts['RBTCWrapperProxy']) + sendWithMultisig(conf.contracts['multisig'], lm.address, data, conf.acct) + + +def getPoolIdOnLMV2(poolToken): + lm = Contract.from_abi("LiquidityMiningV2", address = conf.contracts['LiquidityMiningProxyV2'], abi = LiquidityMiningV2.abi, owner = conf.acct) + print(lm.getPoolId(poolToken)) + + +def getLMV2Info(): + lm = Contract.from_abi("LiquidityMiningV2", address = conf.contracts['LiquidityMiningProxyV2'], abi = LiquidityMiningV2.abi, owner = conf.acct) + print(lm.getPoolLength()) + print(lm.getPoolInfoList()) + print(lm.wrapper()) + + +def transferSOVtoLMV2(amount): + lm = conf.contracts['LiquidityMiningProxyV2'] + SOVtoken = Contract.from_abi("SOV", address=conf.contracts['SOV'], abi=SOV.abi, owner=conf.acct) + data = SOVtoken.transfer.encode_input(lm, amount) + print(data) + + sendWithMultisig(conf.contracts['multisig'], SOVtoken.address, data, conf.acct) diff --git a/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py b/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py new file mode 100644 index 000000000..85f240c4d --- /dev/null +++ b/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py @@ -0,0 +1,65 @@ +from brownie import * +from brownie.network.contract import InterfaceContainer +import json +import time; +import copy +from scripts.utils import * +import scripts.contractInteraction.config as conf + +def initializeLiquidityMiningV1(): + liquidityMiningV1 = Contract.from_abi("LiquidityMiningV1", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMiningV1.abi, owner = conf.acct) + + data = liquidityMiningV1.initialize.encode_input(conf.contracts['LiquidityMiningProxyV2']) + sendWithMultisig(conf.contracts['multisig'], liquidityMiningV1.address, data, conf.acct) + +def initializeLiquidityMiningV2(): + wrapper = "0x0000000000000000000000000000000000000000" + liquidityMiningV2 = Contract.from_abi("LiquidityMiningV2", address = conf.contracts['LiquidityMiningProxyV2'], abi = LiquidityMiningV2.abi, owner = conf.acct) + + data = liquidityMiningV2.initialize.encode_input(wrapper,conf.contracts['LMV1toLMV2Migrator'],conf.contracts['SOV']) + sendWithMultisig(conf.contracts['multisig'], liquidityMiningV2.address, data, conf.acct) + +def setMigratorAsAdmin(): + liquidityMiningV1 = Contract.from_abi("LiquidityMiningV1", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMiningV1.abi, owner = conf.acct) + + data = liquidityMiningV1.addAdmin.encode_input(conf.contracts['LMV1toLMV2Migrator']) + sendWithMultisig(conf.contracts['multisig'], liquidityMiningV1.address, data, conf.acct) + + liquidityMiningV2 = Contract.from_abi("LiquidityMiningV2", address = conf.contracts['LiquidityMiningProxyV2'], abi = LiquidityMiningV2.abi, owner = conf.acct) + + data = liquidityMiningV2.addAdmin.encode_input(conf.contracts['LMV1toLMV2Migrator']) + sendWithMultisig(conf.contracts['multisig'], liquidityMiningV2.address, data, conf.acct) + + +def startMigrationGracePeriod(): + liquidityMiningV1 = Contract.from_abi("LiquidityMiningV1", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMiningV1.abi, owner = conf.acct) + + data = liquidityMiningV1.startMigrationGracePeriod.encode_input() + sendWithMultisig(conf.contracts['multisig'], liquidityMiningV1.address, data, conf.acct) + + +def migratePools(): + lMV1toLMV2Migrator = Contract.from_abi("LMV1toLMV2Migrator", address = conf.contracts['LMV1toLMV2Migrator'], abi = LMV1toLMV2Migrator.abi, owner = conf.acct) + + data = lMV1toLMV2Migrator.migratePools.encode_input() + sendWithMultisig(conf.contracts['multisig'], lMV1toLMV2Migrator.address, data, conf.acct) + +def migrateUsers(): + lMV1toLMV2Migrator = Contract.from_abi("LMV1toLMV2Migrator", address = conf.contracts['LMV1toLMV2Migrator'], abi = LMV1toLMV2Migrator.abi, owner = conf.acct) + configFile = open('./scripts/contractInteraction/usersToMigrate.json') + users = json.load(configFile) + + data = lMV1toLMV2Migrator.migrateUsers.encode_input(users['users']) + sendWithMultisig(conf.contracts['multisig'], lMV1toLMV2Migrator.address, data, conf.acct) + +def finishUsersMigration(): + lMV1toLMV2Migrator = Contract.from_abi("LMV1toLMV2Migrator", address = conf.contracts['LMV1toLMV2Migrator'], abi = LMV1toLMV2Migrator.abi, owner = conf.acct) + + data = lMV1toLMV2Migrator.finishUsersMigration.encode_input() + sendWithMultisig(conf.contracts['multisig'], lMV1toLMV2Migrator.address, data, conf.acct) + +def migrateFunds(): + lMV1toLMV2Migrator = Contract.from_abi("LMV1toLMV2Migrator", address = conf.contracts['LMV1toLMV2Migrator'], abi = LMV1toLMV2Migrator.abi, owner = conf.acct) + + data = lMV1toLMV2Migrator.migrateFunds.encode_input() + sendWithMultisig(conf.contracts['multisig'], lMV1toLMV2Migrator.address, data, conf.acct) \ No newline at end of file diff --git a/scripts/deployment/liquidity-miningV1/deploy-liquidity-mining-logic.py b/scripts/deployment/liquidity-miningV1/deploy-liquidity-mining-logic.py new file mode 100644 index 000000000..51fe66826 --- /dev/null +++ b/scripts/deployment/liquidity-miningV1/deploy-liquidity-mining-logic.py @@ -0,0 +1,30 @@ +from brownie import * + +import json + +def main(): + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + + balanceBefore = acct.balance() + + # == LiquidityMining =================================================================================================================== + liquidityMiningLogicV1 = acct.deploy(LiquidityMiningV1) + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) diff --git a/scripts/deployment/liquidity-miningV1/upgrade-liquidity-mining-logic.py b/scripts/deployment/liquidity-miningV1/upgrade-liquidity-mining-logic.py new file mode 100644 index 000000000..7b40012e2 --- /dev/null +++ b/scripts/deployment/liquidity-miningV1/upgrade-liquidity-mining-logic.py @@ -0,0 +1,49 @@ + +''' +This script serves the purpose of interacting with existing smart contracts on the testnet or mainnet. +''' + +from brownie import * +from brownie.network.contract import InterfaceContainer +import json +import time; +import copy + +def main(): + + #load the contracts and acct depending on the network + loadConfig() + + #call the function you want here + upgradeLiquidityMiningLogic() + + +def loadConfig(): + global contracts, acct + thisNetwork = network.show_active() + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("Network not supported.") + contracts = json.load(configFile) + +def upgradeLiquidityMiningLogic(): + multisig = Contract.from_abi("MultiSig", address=contracts['multisig'], abi=MultiSigWallet.abi, owner=acct) + liquidityMiningProxy = Contract.from_abi("LiquidityMiningProxy", address = contracts['LiquidityMiningProxy'], abi = UpgradableProxy.abi, owner = acct) + + data = liquidityMiningProxy.setImplementation.encode_input(contracts['LiquidityMiningLogicV1']) + tx = multisig.submitTransaction(liquidityMiningProxy.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) + + diff --git a/scripts/deployment/liquidity-miningV2/deploy-liquidity-mining-logic.py b/scripts/deployment/liquidity-miningV2/deploy-liquidity-mining-logic.py new file mode 100644 index 000000000..82d5aa2d8 --- /dev/null +++ b/scripts/deployment/liquidity-miningV2/deploy-liquidity-mining-logic.py @@ -0,0 +1,30 @@ +from brownie import * + +import json + +def main(): + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + + balanceBefore = acct.balance() + + # == LiquidityMining =================================================================================================================== + liquidityMiningLogicV2 = acct.deploy(LiquidityMiningV2) + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) diff --git a/scripts/deployment/liquidity-miningV2/deploy-liquidity-mining.py b/scripts/deployment/liquidity-miningV2/deploy-liquidity-mining.py new file mode 100644 index 000000000..7daae6001 --- /dev/null +++ b/scripts/deployment/liquidity-miningV2/deploy-liquidity-mining.py @@ -0,0 +1,78 @@ +''' +Steps for deploying liquidity mining + +1. Run this deployment script +2. Deploy the wrapper proxy +3. Set wrapper proxy on LMcontract +4. Set lockedSOV as admin of VestingRegistry3 +5. If on mainnet: transfer SOV from adoption pool +''' + +from brownie import * + +import time +import json +import csv +import math + +def main(): + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + multisig = contracts['multisig'] + + balanceBefore = acct.balance() + + # == LiquidityMining =================================================================================================================== + + liquidityMiningLogicV2 = acct.deploy(LiquidityMiningV2) + liquidityMiningProxyV2 = acct.deploy(LiquidityMiningProxyV2) + liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address) + liquidityMiningV2 = Contract.from_abi("LiquidityMiningV2", address=liquidityMiningProxyV2.address, abi=LiquidityMiningV2.abi, owner=acct) + + # TODO define values + # Maximum reward per week: 100M SOV + # Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) + + # we need to multiply by 1000 to have 100 M + rewardTokensPerBlock = 49604 * 10**14 * 1000 #100M per week + + # ~1 day in blocks (assuming 30s blocks) + BLOCKS_PER_DAY = 2740 + BLOCKS_PER_HOUR = 114 + startDelayBlocks = 3 * BLOCKS_PER_DAY - 2 * BLOCKS_PER_HOUR + numberOfBonusBlocks = 1 # ~1 day in blocks (assuming 30s blocks) + wrapper = "0x0000000000000000000000000000000000000000" # can be updated later using setWrapper + # The % (in Basis Point) which determines how much will be unlocked immediately. + # 10000 is 100% + unlockedImmediatelyPercent = 0 # 0% + + #Reward transfer logic + + SOVRewardTransferLogic = acct.deploy(LockedSOVRewardTransferLogic) + SOVRewardTransferLogic.initialize(contracts['LockedSOV'],unlockedImmediatelyPercent) + + #add reward tokens + liquidityMiningV2.addRewardToken(contracts['SOV'],rewardTokensPerBlock,startDelayBlocks,SOVRewardTransferLogic.address) + + liquidityMiningProxyV2.addAdmin(multisig) + liquidityMiningProxyV2.setProxyOwner(multisig) + liquidityMiningV2.transferOwnership(multisig) + + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) diff --git a/scripts/deployment/liquidity-miningV2/deploy-migrator.py b/scripts/deployment/liquidity-miningV2/deploy-migrator.py new file mode 100644 index 000000000..a5ce7ab4f --- /dev/null +++ b/scripts/deployment/liquidity-miningV2/deploy-migrator.py @@ -0,0 +1,36 @@ +from brownie import * + +import json + +def main(): + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + multisig = contracts['multisig'] + + balanceBefore = acct.balance() + + # == migrator =================================================================================================================== + migrator = acct.deploy(LMV1toLMV2Migrator) + + migrator.initialize(contracts['SOV'],contracts['LiquidityMiningProxy'],contracts['LiquidityMiningProxyV2']) + + migrator.addAdmin(multisig) + migrator.transferOwnership(multisig) + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) \ No newline at end of file diff --git a/scripts/deployment/liquidity-miningV2/update-lm.py b/scripts/deployment/liquidity-miningV2/update-lm.py new file mode 100644 index 000000000..6f2345f0f --- /dev/null +++ b/scripts/deployment/liquidity-miningV2/update-lm.py @@ -0,0 +1,138 @@ + +''' +This script serves the purpose of interacting with existing smart contracts on the testnet or mainnet. +''' + +from brownie import * +from brownie.network.contract import InterfaceContainer +import json +import time; +import copy + +def main(): + + #load the contracts and acct depending on the network + loadConfig() + + #call the function you want here + + # updateLMV2Config() + + + # checkLMV2() + updateAllPoolsLMV2() + + # lendLMV2() + # checkTxnsLMV2() + # checkUserBalanceLMV2() + +def loadConfig(): + global contracts, acct + thisNetwork = network.show_active() + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("Network not supported.") + contracts = json.load(configFile) + + +def updateLMV2Config(): + multisig = Contract.from_abi("MultiSig", address=contracts['multisig'], abi=MultiSigWallet.abi, owner=acct) + lm = Contract.from_abi("LiquidityMiningV2", address = contracts['LiquidityMiningProxyV2'], abi = LiquidityMiningV2.abi, owner = acct) + + # SOV/rBTC - 15k SOV + # ETH/rBTC - 15k SOV + # xUSD/rBTC - 15k SOV + # BNB/rBTC - 15k SOV + + MAX_ALLOCATION_POINT = 100000 * 1000 # 100 M + # SOV/rBTC - 25k SOV + ALLOCATION_POINT_BTC_SOV = 15000 # (WR)BTC/SOV + # ETH/rBTC - 20k SOV + ALLOCATION_POINT_BTC_ETH = 15000 # (WR)BTC/ETH + # xUSD/rBTC - 20k SOV + ALLOCATION_POINT_BTC_XUSD = 15000 # (WR)BTC/XUSD + # BNB/rBTC - 35k SOV + ALLOCATION_POINT_BTC_BNB = 15000 # (WR)BTC/BNB + ALLOCATION_POINT_DEFAULT = 1 # (WR)BTC/USDT1 | (WR)BTC/USDT2 | (WR)BTC/DOC1 | (WR)BTC/DOC2 | (WR)BTC/BPRO1 | (WR)BTC/BPRO2 | (WR)BTC/MOC + ALLOCATION_POINT_CONFIG_TOKEN = MAX_ALLOCATION_POINT - ALLOCATION_POINT_BTC_SOV - ALLOCATION_POINT_BTC_ETH - ALLOCATION_POINT_BTC_XUSD - ALLOCATION_POINT_BTC_BNB - ALLOCATION_POINT_DEFAULT * 7 + + print("ALLOCATION_POINT_CONFIG_TOKEN: ", ALLOCATION_POINT_CONFIG_TOKEN) + + data = lm.update.encode_input(contracts['(WR)BTC/SOV'],[contracts['SOV']],[ALLOCATION_POINT_BTC_SOV],True) + tx = multisig.submitTransaction(lm.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) + + data = lm.update.encode_input(contracts['(WR)BTC/ETH'],[contracts['SOV']],[ALLOCATION_POINT_BTC_ETH],True) + tx = multisig.submitTransaction(lm.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) + + data = lm.update.encode_input(contracts['(WR)BTC/XUSD'],[contracts['SOV']],[ALLOCATION_POINT_BTC_XUSD],True) + tx = multisig.submitTransaction(lm.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) + + data = lm.update.encode_input(contracts['(WR)BTC/BNB'],[contracts['SOV']],[ALLOCATION_POINT_BTC_BNB],True) + tx = multisig.submitTransaction(lm.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) + + data = lm.update.encode_input(contracts['LiquidityMiningConfigToken'],[contracts['SOV']],[ALLOCATION_POINT_CONFIG_TOKEN],True) + tx = multisig.submitTransaction(lm.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) + +def checkLMV2(): + liquidityMiningV2 = Contract.from_abi("LiquidityMiningV2", address=contracts['LiquidityMiningProxyV2'], abi=LiquidityMiningV2.abi, owner=acct) + print("MissedBalance: ", liquidityMiningV2.getMissedBalance(contracts['SOV']) / 10**18) + print("TotalUsersBalance: ", liquidityMiningV2.rewardTokensMap(contracts['SOV'])[4]) + + print("Pool info list:") + print(liquidityMiningV2.getPoolInfoList()) + +def updateAllPoolsLMV2(): + liquidityMiningV2 = Contract.from_abi("LiquidityMiningV2", address=contracts['LiquidityMiningProxyV2'], abi=LiquidityMiningV2.abi, owner=acct) + data = liquidityMiningV2.updateAllPools.encode_input() + print(data) + + multisig = Contract.from_abi("MultiSig", address=contracts['multisig'], abi=MultiSigWallet.abi, owner=acct) + tx = multisig.submitTransaction(liquidityMiningV2.address,0,data) + #xId = tx.events["Submission"]["transactionId"] + print(txId) + +def lendLMV2(): + token = Contract.from_abi("TestToken", address=contracts['DoC'], abi=TestToken.abi, owner=acct) + loanToken = Contract.from_abi("LoanTokenLogicLM", address=contracts['iDOC'], abi=LoanTokenLogicLM.abi, owner=acct) + + print(acct) + token.approve(loanToken.address, 4 * 10**18) + loanToken.mint(acct, 2 * 10**18, False, {'allow_revert': True}) + loanToken.mint(acct, 2 * 10**18, True, {'allow_revert': True}) + +def checkTxnsLMV2(): + multisig = Contract.from_abi("MultiSig", address=contracts['multisig'], abi=MultiSigWallet.abi, owner=acct) + + # for i in range(229, 238): + # print(i) + # multisig.confirmTransaction(i) + + for i in range(166, 174): + print(i) + print(multisig.transactions(i)) + +def checkUserBalanceLMV2(): + liquidityMiningV2 = Contract.from_abi("LiquidityMiningV2", address=contracts['LiquidityMiningProxyV2'], abi=LiquidityMiningV2.abi, owner=acct) + poolId = liquidityMiningV2.getUserPoolTokenBalance(contracts['iDOC'], acct) + print(poolId / 10**18) diff --git a/scripts/deployment/liquidity-miningV2/upgrade-liquidity-mining-logic.py b/scripts/deployment/liquidity-miningV2/upgrade-liquidity-mining-logic.py new file mode 100644 index 000000000..5c94b7559 --- /dev/null +++ b/scripts/deployment/liquidity-miningV2/upgrade-liquidity-mining-logic.py @@ -0,0 +1,47 @@ + +''' +This script serves the purpose of interacting with existing smart contracts on the testnet or mainnet. +''' + +from brownie import * +from brownie.network.contract import InterfaceContainer +import json +import time; +import copy + +def main(): + + #load the contracts and acct depending on the network + loadConfig() + + #call the function you want here + upgradeLiquidityMiningLogic() + + +def loadConfig(): + global contracts, acct + thisNetwork = network.show_active() + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("Network not supported.") + contracts = json.load(configFile) + +def upgradeLiquidityMiningLogic(): + multisig = Contract.from_abi("MultiSig", address=contracts['multisig'], abi=MultiSigWallet.abi, owner=acct) + liquidityMiningProxyV2 = Contract.from_abi("LiquidityMiningProxyV2", address = contracts['LiquidityMiningProxyV2'], abi = UpgradableProxy.abi, owner = acct) + + data = liquidityMiningProxyV2.setImplementation.encode_input(contracts['LiquidityMiningLogicV2']) + tx = multisig.submitTransaction(liquidityMiningProxyV2.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) diff --git a/tests/farm/ERC20TransferLogic.js b/tests/farm/ERC20TransferLogic.js new file mode 100644 index 000000000..77f08f963 --- /dev/null +++ b/tests/farm/ERC20TransferLogic.js @@ -0,0 +1,136 @@ +const { expect } = require("chai"); +const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); +const { etherMantissa, mineBlock, advanceBlocks } = require("../Utils/Ethereum"); + +const { ZERO_ADDRESS } = constants; +const TOTAL_SUPPLY = etherMantissa(1000000000); + +const TestToken = artifacts.require("TestToken"); +const ERC20TransferLogic = artifacts.require("ERC20TransferLogic"); + +describe("ERC20TransferLogic", () => { + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let root, account1, account2, account3, account4; + let token1, token2, token3; + let transferLogic; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + + transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + }); + + describe("initialize", () => { + it("fails if not an owner or admin", async () => { + transferLogic = await ERC20TransferLogic.new(); + await expectRevert(transferLogic.initialize(token1.address, { from: account1 }), "unauthorized"); + + await transferLogic.addAdmin(account1); + await transferLogic.initialize(token1.address, { from: account1 }); + }); + + it("sets the expected values", async () => { + transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + let _tokenAddress = await transferLogic.token(); + expect(_tokenAddress).equal(token1.address); + }); + }); + + describe("setTokenAddress", () => { + it("fails if not an owner or admin", async () => { + await expectRevert(transferLogic.setTokenAddress(token1.address, { from: account1 }), "unauthorized"); + + await transferLogic.addAdmin(account1); + await transferLogic.setTokenAddress(token1.address, { from: account1 }); + }); + + it("fails if invalid address", async () => { + await expectRevert(transferLogic.setTokenAddress(ZERO_ADDRESS), "Invalid token address"); + }); + + it("should set a new token address", async () => { + //first check original token address + let oldTokenAddress = await transferLogic.token(); + expect(oldTokenAddress).equal(token1.address); + + //then check new token addres + tx = await transferLogic.setTokenAddress(token2.address); + let newTokenAddress = await transferLogic.token(); + expect(newTokenAddress).equal(token2.address); + + expectEvent(tx, "TokenAddressUpdated", { + _newTokenAddress: newTokenAddress, + }); + }); + }); + + describe("getRewardTokenAddress", async () => { + it("should return token1 address", async () => { + let _tokenAddress = await transferLogic.getRewardTokenAddress(); + expect(_tokenAddress).equal(token1.address); + }); + }); + + describe("senderToAuthorize", async () => { + it("should return contract address", async () => { + let transferLogicAddress = await transferLogic.senderToAuthorize(); + expect(transferLogicAddress).equal(transferLogic.address); + }); + }); + + describe("transferReward", async () => { + const account1InitialBalance = new BN(100); + const amountToTransfer = new BN(50); + + it("fails if account doesn't have reward tokens", async () => { + await expectRevert( + transferLogic.transferReward(account2, new BN(5), false, { from: account1 }), + "SafeERC20: low-level call failed" + ); + }); + + it("fails if account didn't approve before", async () => { + //send some token1 to account1 to be able to transfer + await token1.mint(account1, new BN(10)); + await expectRevert( + transferLogic.transferReward(account2, new BN(5), false, { from: account1 }), + "SafeERC20: low-level call failed" + ); + }); + + it("fails if invalid address to transfer", async () => { + //send some token1 to account1 to be able to transfer + await token1.mint(account1, account1InitialBalance); + await token1.approve(transferLogic.address, account1InitialBalance, { from: account1 }); + await expectRevert( + transferLogic.transferReward(ZERO_ADDRESS, new BN(5), false, { from: account1 }), + "SafeERC20: low-level call failed" + ); + }); + + it("should account1 transfer reward to account2", async () => { + //send some token1 to account1 to be able to transfer + await token1.mint(account1, account1InitialBalance); + await token1.approve(transferLogic.address, account1InitialBalance, { from: account1 }); + + await transferLogic.transferReward(account2, amountToTransfer, false, { from: account1 }); + let account1FinalBalance = await token1.balanceOf(account1); + let account2FinalBalance = await token1.balanceOf(account2); + expect(account1FinalBalance).bignumber.equal(account1InitialBalance.sub(amountToTransfer)); + expect(account2FinalBalance).bignumber.equal(amountToTransfer); + }); + }); +}); diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js new file mode 100644 index 000000000..ebd3f8bfb --- /dev/null +++ b/tests/farm/LiquidityMiningMigration.js @@ -0,0 +1,988 @@ +const { expect } = require("chai"); +const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); +const { etherMantissa, mineBlock } = require("../Utils/Ethereum"); +var ethers = require("ethers"); +var crypto = require("crypto"); + +const { ZERO_ADDRESS } = constants; +const TOTAL_SUPPLY = etherMantissa(1000000000); + +const TestToken = artifacts.require("TestToken"); +const LiquidityMiningConfigToken = artifacts.require("LiquidityMiningConfigToken"); +const LiquidityMiningLogic = artifacts.require("LiquidityMiningMockup"); +const LiquidityMiningLogicV1 = artifacts.require("LiquidityMiningV1Mockup"); +const LiquidityMiningProxy = artifacts.require("LiquidityMiningProxy"); +const LiquidityMiningLogicV2 = artifacts.require("LiquidityMiningMockupV2"); +const LiquidityMiningProxyV2 = artifacts.require("LiquidityMiningProxyV2"); +const TestLockedSOV = artifacts.require("LockedSOVMockup"); +const Wrapper = artifacts.require("RBTCWrapperProxyMockupV2"); +const LockedSOVRewardTransferLogic = artifacts.require("LockedSOVRewardTransferLogic"); +const ERC20TransferLogic = artifacts.require("ERC20TransferLogic"); +const TestPoolToken = artifacts.require("TestPoolToken"); +const Migrator = artifacts.require("LMV1toLMV2Migrator"); + +describe("LiquidityMiningMigration", () => { + const name = "Test SOV Token"; + const symbol = "TST"; + + const PRECISION = 1e12; + + const rewardTokensPerBlock = new BN(3); + const startDelayBlocks = new BN(1); + const numberOfBonusBlocks = new BN(50); + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let root, account1, account2, account3, account4, account5, account6, account7, account8, account9; + let SOVToken, token1, token2, token3, token4, token5, token6, token7, token8, liquidityMiningConfigToken; + let liquidityMiningProxy, liquidityMining, liquidityMiningV2, migrator, wrapper; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + let erc20RewardTransferLogic; + let allocationPoint = new BN(10); + + const MigrationStates = { + MigratingPools: 0, + MigratingUsers: 1, + MigratingFunds: 2, + MigrationFinished: 3, + }; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + token4 = await TestToken.new("Test token 4", "TST-4", 18, TOTAL_SUPPLY); + token5 = await TestToken.new("Test token 5", "TST-5", 18, TOTAL_SUPPLY); + token6 = await TestToken.new("Test token 6", "TST-6", 18, TOTAL_SUPPLY); + token7 = await TestToken.new("Test token 7", "TST-7", 18, TOTAL_SUPPLY); + token8 = await TestToken.new("Test token 8", "TST-8", 18, TOTAL_SUPPLY); + + tokens = [token1, token2, token3, token4, token5, token6, token7, token8]; + + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + //set accounts deposits pools in liquidity mining V1 + setAccountsDepositsConstants(); + //mint some tokens to all the accounts + await initializaAccountsTokensBalance(); + //add all poolTokens to liquidityMining + await initializeLiquidityMiningPools(); + //make deposits from accounts to some pools + await initializeLiquidityMiningDeposits(); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + + erc20RewardTransferLogic = await ERC20TransferLogic.new(); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + }); + + describe("initializeLiquidityMining", () => { + it("should check all user deposits", async () => { + for (let i = 0; i < accountDeposits.length; i++) { + for (let j = 0; j < accountDeposits[i].deposit.length; j++) { + let poolToken = accountDeposits[i].deposit[j].token; + let poolId = await liquidityMining.getPoolId(poolToken); + let { amount } = await liquidityMining.userInfoMap(poolId, accountDeposits[i].account); + expect(amount).bignumber.equal(accountDeposits[i].deposit[j].amount); + } + } + }); + it("should check all pool have been added", async () => { + const { _poolToken } = await liquidityMining.getPoolInfoListArray(); + for (let i = 0; i < tokens.length; i++) { + expect(_poolToken[i]).equal(tokens[i].address); + } + }); + it("should fail if liquidity mining V2 address is invalid", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + + await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); + }); + it("should fail if SOV address is invalid in migrator contract", async () => { + migrator = await Migrator.new(); + await expectRevert( + migrator.initialize(ZERO_ADDRESS, liquidityMining.address, liquidityMiningV2.address), + "invalid token address" + ); + }); + it("should fail if liquidity mining V1 address is invalid in migrator contract", async () => { + migrator = await Migrator.new(); + await expectRevert(migrator.initialize(SOVToken.address, ZERO_ADDRESS, liquidityMiningV2.address), "invalid contract address"); + }); + it("should fail if liquidity mining V2 address is invalid in migrator contract", async () => { + migrator = await Migrator.new(); + await expectRevert(migrator.initialize(SOVToken.address, liquidityMining.address, ZERO_ADDRESS), "invalid contract address"); + }); + }); + + describe("migratePools", () => { + it("should only allow to migrate pools by the admin", async () => { + await expectRevert(migrator.migratePools({ from: account1 }), "unauthorized"); + }); + it("should fail if migrator contract was not added as admin in liquidity mining V1", async () => { + await expectRevert(migrator.migratePools(), "unauthorized"); + }); + it("should fail if migrator contract was not added as admin in liquidity mining V2", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await expectRevert(migrator.migratePools(), "unauthorized"); + }); + it("should only allow to migrate pools by migrator contract", async () => { + await liquidityMiningV2.initialize(wrapper.address, SOVToken.address, SOVToken.address); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await expectRevert(migrator.migratePools(), "only allowed to migrator contract"); + }); + it("should only allow to migrate pools if the migrate grace period started", async () => { + await liquidityMining.addAdmin(migrator.address); + await expectRevert(migrator.migratePools(), "Migration hasn't started yet"); + }); + it("should only allow to migrate pools once", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await expectRevert(migrator.migratePools(), "Wrong state: should be MigratingPools"); + }); + it("should add pools from liquidityMininigV1", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + for (let i = 0; i < tokens.length; i++) { + let poolToken = await liquidityMiningV2.poolInfoList(i); + expect(poolToken).equal(tokens[i].address); + + let { + allocationPoint: allocationPointV2, + lastRewardBlock: lastRewardBlockV2, + accumulatedRewardPerShare: accumulatedRewardPerShareV2, + } = await liquidityMiningV2.poolInfoRewardTokensMap(i, SOVToken.address); + let { + allocationPoint: allocationPointV1, + lastRewardBlock: lastRewardBlockV1, + accumulatedRewardPerShare: accumulatedRewardPerShareV1, + } = await liquidityMining.poolInfoList(i); + expect(allocationPointV2).bignumber.equal(allocationPointV1); + expect(lastRewardBlockV2).bignumber.equal(lastRewardBlockV1); + expect(accumulatedRewardPerShareV2).bignumber.equal(accumulatedRewardPerShareV1); + + let { startBlock: startBlockV2, totalUsersBalance: totalUsersBalanceV2 } = await liquidityMiningV2.rewardTokensMap( + SOVToken.address + ); + let startBlockV1 = await liquidityMining.startBlock(); + let totalUsersBalanceV1 = await liquidityMining.totalUsersBalance(); + + expect(startBlockV2).bignumber.equal(startBlockV1); + expect(totalUsersBalanceV2).bignumber.equal(totalUsersBalanceV1); + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingUsers); + } + }); + }); + + describe("migrateUsers", () => { + it("should only allow to migrate users by the admin", async () => { + await expectRevert(migrator.migrateUsers(accounts, { from: account1 }), "unauthorized"); + }); + it("should fail migrating users if pools were not migrated", async () => { + await expectRevert(migrator.migrateUsers(accounts), "Wrong state: should be MigratingUsers"); + }); + it("should fail finishing users migration if pools were not migrated", async () => { + await expectRevert(migrator.finishUsersMigration(), "Wrong state: should be MigratingUsers"); + }); + it("should only allow to migrate users by migrator contract", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await liquidityMiningV2.initialize(wrapper.address, SOVToken.address, SOVToken.address); + await expectRevert(migrator.migrateUsers(accounts), "only allowed to migrator contract"); + }); + it("should only allow to finish users migration by the admin", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await expectRevert(migrator.finishUsersMigration({ from: account1 }), "unauthorized"); + }); + it("should only allow to migrate users before finish user migration", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await expectRevert(migrator.migrateUsers(accounts), "Wrong state: should be MigratingUsers"); + }); + it("should save migrated users", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(accounts); + accounts.forEach(async (account) => { + expect(await migrator.userMigrated(account)); + }); + + await migrator.finishUsersMigration(); + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); + }); + it("should emit user migrated event", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + tx = await migrator.migrateUsers([accounts[0]]); + expectEvent(tx, "UserMigrated", { + user: accounts[0], + }); + }); + it("should be able to migrate users in differents tx", async () => { + let userInfoV1 = []; + for (let i = 0; i < tokens.length; i++) { + userInfoV1[i] = []; + for (let j = 0; j < accountDeposits.length; j++) { + let userInfo = await liquidityMining.getUserInfo(tokens[i].address, accountDeposits[j].account); + userInfoV1[i][j] = userInfo; + } + } + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + let halfLength = accounts.length / 2; + await migrator.migrateUsers(accounts.slice(0, halfLength)); + + let migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingUsers); + + await migrator.migrateUsers(accounts.slice(-halfLength)); + + await migrator.finishUsersMigration(); + migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); + + for (let i = 0; i < tokens.length; i++) { + for (let j = 0; j < accountDeposits.length; j++) { + let userInfoV2 = await liquidityMiningV2.getUserInfo(tokens[i].address, accountDeposits[j].account); + + expect(userInfoV2.amount).bignumber.equal(userInfoV1[i][j].amount); + expect(userInfoV2.rewards[0].rewardDebt).bignumber.equal(userInfoV1[i][j].rewardDebt); + expect(userInfoV2.rewards[0].accumulatedReward).bignumber.equal(userInfoV1[i][j].accumulatedReward); + } + } + }); + it("should migrate all accounts with deposits from liquidityMininigV1", async () => { + let userInfoV1 = []; + for (let i = 0; i < tokens.length; i++) { + userInfoV1[i] = []; + for (let j = 0; j < accountDeposits.length; j++) { + let userInfo = await liquidityMining.getUserInfo(tokens[i].address, accountDeposits[j].account); + userInfoV1[i][j] = userInfo; + } + } + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(accounts); + + await migrator.finishUsersMigration(); + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); + + for (let i = 0; i < tokens.length; i++) { + for (let j = 0; j < accountDeposits.length; j++) { + let userInfoV2 = await liquidityMiningV2.getUserInfo(tokens[i].address, accountDeposits[j].account); + + expect(userInfoV2.amount).bignumber.equal(userInfoV1[i][j].amount); + expect(userInfoV2.rewards[0].rewardDebt).bignumber.equal(userInfoV1[i][j].rewardDebt); + expect(userInfoV2.rewards[0].accumulatedReward).bignumber.equal(userInfoV1[i][j].accumulatedReward); + } + } + }); + it("should migrate 65 random accounts from liquidityMininigV1", async () => { + let randomAccounts = createRandomAccounts(65); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(randomAccounts); + }); + }); + + describe("migrateFunds", () => { + it("should only allow to migrate funds by the admin", async () => { + await expectRevert(migrator.migrateFunds({ from: account1 }), "unauthorized"); + }); + it("should fail migrating funds if users were not migrated", async () => { + await expectRevert(migrator.migrateFunds(), "Wrong state: should be MigratingFunds"); + }); + it("should only allow to migrate funds by migrator contract", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await liquidityMiningV2.initialize(wrapper.address, SOVToken.address, SOVToken.address); + await expectRevert(migrator.migrateFunds(), "only allowed to migrator contract"); + }); + it("should fail trying to migrate funds without SOV tokens in liquidityMiningV1", async () => { + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await liquidityMining.transferSOV(liquidityMiningV2.address, balanceSOV); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await expectRevert(migrator.migrateFunds(), "Amount invalid"); + }); + it("should fail trying to migrate funds twice", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + await expectRevert(migrator.migrateFunds(), "Wrong state: should be MigratingFunds"); + }); + it("should fail if liquidity mining V2 is not initialized in liquidity mining V1", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await expectRevert(migrator.migrateFunds(), "Address not initialized"); + }); + it("should migrate funds from liquidityMining", async () => { + let SOVBalanceV1Before = await SOVToken.balanceOf(liquidityMining.address); + let SOVBalanceV2Before = await SOVToken.balanceOf(liquidityMiningV2.address); + let tokenBalancesV1Before = []; + let tokenBalancesV2Before = []; + for (let i = 0; i < tokens.length; i++) { + tokenBalancesV1Before.push(await tokens[i].balanceOf(liquidityMining.address)); + tokenBalancesV2Before.push(await tokens[i].balanceOf(liquidityMiningV2.address)); + expect(tokenBalancesV2Before[i]).bignumber.equal(new BN(0)); + } + expect(SOVBalanceV2Before).bignumber.equal(new BN(0)); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigrationFinished); + + let SOVBalanceV1After = await SOVToken.balanceOf(liquidityMining.address); + let SOVBalanceV2After = await SOVToken.balanceOf(liquidityMiningV2.address); + let tokenBalancesV1After = []; + let tokenBalancesV2After = []; + for (let i = 0; i < tokens.length; i++) { + tokenBalancesV1After.push(await tokens[i].balanceOf(liquidityMining.address)); + tokenBalancesV2After.push(await tokens[i].balanceOf(liquidityMiningV2.address)); + expect(tokenBalancesV1After[i]).bignumber.equal(new BN(0)); + expect(tokenBalancesV2After[i]).bignumber.equal(tokenBalancesV1Before[i]); + } + expect(SOVBalanceV1After).bignumber.equal(new BN(0)); + expect(SOVBalanceV2After).bignumber.equal(SOVBalanceV1Before); + }); + }); + + describe("withdraws", () => { + it("should withdraw all before migration and revert trying to withdraw after", async () => { + await liquidityMining.withdraw(accountDeposits[0].deposit[0].token, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(accounts); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await expectRevert( + liquidityMiningV2.withdraw(accountDeposits[0].deposit[0].token, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }), + "Not enough balance" + ); + }); + it("should withdraw half before migration and withdraw the other half after", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + tokenBalanceBefore = await token1.balanceOf(accountDeposits[0].account); + await liquidityMining.withdraw( + accountDeposits[0].deposit[0].token, + accountDeposits[0].deposit[0].amount.div(new BN(2)), + ZERO_ADDRESS, + { from: accountDeposits[0].account } + ); + + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + await liquidityMiningV2.withdraw( + accountDeposits[0].deposit[0].token, + accountDeposits[0].deposit[0].amount.div(new BN(2)), + ZERO_ADDRESS, + { from: accountDeposits[0].account } + ); + + tokenBalanceAfter = await token1.balanceOf(accountDeposits[0].account); + + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).bignumber.equal(accountDeposits[0].deposit[0].amount); + }); + it("should withdraw all before migration, migrate, deposit and withdraw all again", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); + await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + await mineBlocks(20); + + await liquidityMining.withdraw(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + let balanceLockedBefore = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await token1.approve(liquidityMiningV2.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); + await liquidityMiningV2.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + await mineBlocks(20); + + await liquidityMiningV2.withdraw(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + let balanceLockedAfter = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + expect(balanceLockedAfter).bignumber.equal(balanceLockedBefore.mul(new BN(2))); + }); + it("should get rewards in liquidity mining V2 after migration", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); + await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + await mineBlocks(1); + + tx = await liquidityMining.withdraw(token1.address, accountDeposits[0].deposit[0].amount.div(new BN(2)), ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + let blockStart = tx.receipt.blockNumber; + let balanceLockedBefore = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + + await mineBlocks(10); + + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + tx = await liquidityMiningV2.withdraw(token1.address, accountDeposits[0].deposit[0].amount.div(new BN(2)), ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + let blockEnd = tx.receipt.blockNumber; + let passedBlocks = new BN(blockEnd - blockStart); + let reward = passedBlocks.mul(rewardTokensPerBlock); + let balanceLockedAfter = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + expect(balanceLockedAfter).bignumber.equal(balanceLockedBefore.add(reward)); + }); + it("should migrate rewards", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); + await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + await mineBlocks(10); + + await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: accountDeposits[0].account }); + let { rewardDebt: rewardDebtBefore } = await liquidityMining.userInfoMap(0, accountDeposits[0].account); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + let userInfoV2 = await liquidityMiningV2.getUserInfo(token1.address, accountDeposits[0].account); + let rewardDebtAfter = userInfoV2.rewards[0].rewardDebt; + + expect(rewardDebtAfter).bignumber.equal(rewardDebtBefore); + }); + it("should be able to claim rewards after migration", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); + await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + await mineBlocks(10); + + tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: accountDeposits[0].account }); + let blockStart = tx.receipt.blockNumber; + let { rewardDebt: rewardDebtBefore } = await liquidityMining.userInfoMap(0, accountDeposits[0].account); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + + await mineBlocks(10); + + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + tx = await liquidityMiningV2.claimRewards(token1.address, ZERO_ADDRESS, { from: accountDeposits[0].account }); + let blockEnd = tx.receipt.blockNumber; + let passedBlocks = new BN(blockEnd - blockStart); + let rewardDebt = passedBlocks.mul(rewardTokensPerBlock); + let userInfoV2 = await liquidityMiningV2.getUserInfo(token1.address, accountDeposits[0].account); + let rewardDebtAfter = userInfoV2.rewards[0].rewardDebt; + + expect(rewardDebtAfter).bignumber.equal(rewardDebtBefore.add(rewardDebt)); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMining = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMiningV2 = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + + wrapper = await Wrapper.new(liquidityMiningV2.address); + } + + async function initializeLiquidityMiningPools() { + for (let i = 0; i < tokens.length; i++) { + await liquidityMining.add(tokens[i].address, allocationPoint, false); + } + } + + async function initializaAccountsTokensBalance() { + let amount = new BN(1000); + await SOVToken.mint(liquidityMining.address, amount); + tokens.forEach((token) => { + accounts.forEach(async (account) => { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + }); + }); + } + + async function initializeLiquidityMiningDeposits() { + accountDeposits.forEach((account) => { + account.deposit.forEach(async (deposit) => { + await liquidityMining.deposit(deposit.token, deposit.amount, ZERO_ADDRESS, { from: account.account }); + }); + }); + } + + function createRandomAccounts(length) { + const randomAccounts = []; + for (let i = 0; i < length; i++) { + let id = crypto.randomBytes(32).toString("hex"); + let privateKey = "0x" + id; + let wallet = new ethers.Wallet(privateKey); + randomAccounts.push(wallet.address); + } + return randomAccounts; + } + + function setAccountsDepositsConstants() { + accountDeposits = [ + { + account: accounts[0], + + deposit: [ + { + token: token1.address, + amount: new BN(100), + }, + { + token: token2.address, + amount: new BN(10), + }, + { + token: token3.address, + amount: new BN(10), + }, + { + token: token4.address, + amount: new BN(10), + }, + { + token: token5.address, + amount: new BN(10), + }, + { + token: token6.address, + amount: new BN(10), + }, + { + token: token7.address, + amount: new BN(10), + }, + { + token: token8.address, + amount: new BN(10), + }, + ], + }, + { + account: accounts[1], + + deposit: [ + { + token: token1.address, + amount: new BN(5), + }, + { + token: token2.address, + amount: new BN(5), + }, + { + token: token3.address, + amount: new BN(5), + }, + { + token: token4.address, + amount: new BN(5), + }, + ], + }, + { + account: accounts[2], + + deposit: [ + { + token: token1.address, + amount: new BN(55), + }, + ], + }, + { + account: accounts[3], + + deposit: [ + { + token: token8.address, + amount: new BN(1000), + }, + ], + }, + { + account: accounts[4], + + deposit: [ + { + token: token6.address, + amount: new BN(25), + }, + { + token: token7.address, + amount: new BN(100), + }, + { + token: token8.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[5], + + deposit: [ + { + token: token1.address, + amount: new BN(25), + }, + { + token: token3.address, + amount: new BN(100), + }, + { + token: token8.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[6], + + deposit: [ + { + token: token2.address, + amount: new BN(25), + }, + { + token: token4.address, + amount: new BN(100), + }, + { + token: token6.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[7], + + deposit: [ + { + token: token3.address, + amount: new BN(25), + }, + { + token: token5.address, + amount: new BN(100), + }, + { + token: token7.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[8], + + deposit: [ + { + token: token4.address, + amount: new BN(25), + }, + { + token: token5.address, + amount: new BN(100), + }, + { + token: token6.address, + amount: new BN(100), + }, + ], + }, + ]; + } + + async function mineBlocks(blocks) { + for (let i = 0; i < blocks; i++) { + await mineBlock(); + } + } +}); diff --git a/tests/farm/LiquidityMiningV1.js b/tests/farm/LiquidityMiningV1.js new file mode 100644 index 000000000..96a6ccedd --- /dev/null +++ b/tests/farm/LiquidityMiningV1.js @@ -0,0 +1,2037 @@ +const { expect } = require("chai"); +const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); +const { etherMantissa, mineBlock, advanceBlocks } = require("../Utils/Ethereum"); + +const { ZERO_ADDRESS } = constants; +const TOTAL_SUPPLY = etherMantissa(1000000000); + +const TestToken = artifacts.require("TestToken"); +const LiquidityMiningConfigToken = artifacts.require("LiquidityMiningConfigToken"); +const LiquidityMiningLogicV1 = artifacts.require("LiquidityMiningV1Mockup"); +const LiquidityMiningLogic = artifacts.require("LiquidityMiningMockup"); +const LiquidityMiningProxy = artifacts.require("LiquidityMiningProxy"); +const LiquidityMiningLogicV2 = artifacts.require("LiquidityMiningMockupV2"); +const LiquidityMiningProxyV2 = artifacts.require("LiquidityMiningProxyV2"); +const TestLockedSOV = artifacts.require("LockedSOVMockup"); +const Wrapper = artifacts.require("RBTCWrapperProxyMockup"); + +const name = "Test SOV Token"; +const symbol = "TST"; +const PRECISION = 1e12; +const rewardTokensPerBlock = new BN(3); +const startDelayBlocks = new BN(1); +const numberOfBonusBlocks = new BN(50); +// The % which determines how much will be unlocked immediately. +/// @dev 10000 is 100% +const unlockedImmediatelyPercent = new BN(1000); //10% + +async function mineBlocks(blocks) { + for (let i = 0; i < blocks; i++) { + await mineBlock(); + } +} + +describe("LiquidityMining", () => { + let accounts; + let root, account1, account2, account3, account4; + let SOVToken, token1, token2, token3, liquidityMiningConfigToken; + let liquidityMining, liquidityMiningProxy, liquidityMiningV2, liquidityMiningProxyV2, wrapper; + let lockedSOVAdmins, lockedSOV; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMining.initialize(liquidityMiningV2.address); + }); + + describe("initialize", () => { + it("sets the expected values", async () => { + await deployLiquidityMining(); + let tx = await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + let _SOV = await liquidityMining.SOV(); + let _rewardTokensPerBlock = await liquidityMining.rewardTokensPerBlock(); + let _startBlock = await liquidityMining.startBlock(); + let _bonusEndBlock = await liquidityMining.bonusEndBlock(); + let _wrapper = await liquidityMining.wrapper(); + + let blockNumber = new BN(tx.receipt.blockNumber); + + expect(_SOV).equal(SOVToken.address); + expect(_rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); + expect(_startBlock).bignumber.equal(startDelayBlocks.add(blockNumber)); + expect(_bonusEndBlock).bignumber.equal(startDelayBlocks.add(blockNumber).add(numberOfBonusBlocks)); + expect(_wrapper).equal(wrapper.address); + }); + + it("fails if not an owner or an admin", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent, + { from: account1 } + ), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent, + { from: account1 } + ); + }); + + it("fails if _startBlock = 0", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + 0, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Invalid start block" + ); + }); + + it("fails if already initialized", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Already initialized" + ); + }); + it("fails if the 0 address is passed as token address", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + ZERO_ADDRESS, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Invalid token address" + ); + }); + it("fails if the 0 address is passed as LiquidityMiningV2 address", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); + }); + + it("fails if unlockedImmediatelyPercent >= 10000", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + 12345 + ), + "Unlocked immediately percent has to be less than 10000." + ); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await liquidityMining.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(liquidityMining.addAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await liquidityMining.addAdmin(account1); + let tx = await liquidityMining.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(liquidityMining.removeAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("setLockedSOV", () => { + it("sets the expected values", async () => { + let newLockedSOV = account2; + await liquidityMining.setLockedSOV(newLockedSOV); + + let _lockedSOV = await liquidityMining.lockedSOV(); + expect(_lockedSOV).equal(newLockedSOV); + }); + + it("fails if not an owner and an admin", async () => { + await expectRevert(liquidityMining.setLockedSOV(account2, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setLockedSOV(account2, { from: account1 }); + }); + + it("fails if zero address passed", async () => { + await expectRevert(liquidityMining.setLockedSOV(ZERO_ADDRESS), "Invalid lockedSOV Address."); + }); + }); + + describe("setUnlockedImmediatelyPercent", () => { + it("sets the expected values", async () => { + let newUnlockedImmediatelyPercent = new BN(2000); + await liquidityMining.setUnlockedImmediatelyPercent(newUnlockedImmediatelyPercent); + + let _unlockedImmediatelyPercent = await liquidityMining.unlockedImmediatelyPercent(); + expect(_unlockedImmediatelyPercent).bignumber.equal(newUnlockedImmediatelyPercent); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert(liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }); + }); + + it("fails if unlockedImmediatelyPercent >= 10000", async () => { + await expectRevert( + liquidityMining.setUnlockedImmediatelyPercent(100000), + "Unlocked immediately percent has to be less than 10000." + ); + }); + }); + + describe("setWrapper", () => { + it("sets the expected values", async () => { + let newWrapper = account2; + await liquidityMining.setWrapper(newWrapper); + + let _wrapper = await liquidityMining.wrapper(); + expect(_wrapper).equal(newWrapper); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert(liquidityMining.setWrapper(account2, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setWrapper(account2, { from: account1 }); + }); + }); + + describe("stopMining", () => { + it("should set end block", async () => { + let tx = await liquidityMining.stopMining(); + + let blockNumber = new BN(tx.receipt.blockNumber); + let _endBlock = await liquidityMining.endBlock(); + expect(_endBlock).bignumber.equal(blockNumber); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert(liquidityMining.stopMining({ from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.stopMining({ from: account1 }); + }); + + it("fails if already stopped", async () => { + await liquidityMining.stopMining(); + await expectRevert(liquidityMining.stopMining(), "Already stopped"); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOVToken.transfer(liquidityMining.address, amount); + + let balanceBefore = await SOVToken.balanceOf(account1); + await liquidityMining.transferSOV(account1, amount); + let balanceAfter = await SOVToken.balanceOf(account1); + + expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert(liquidityMining.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.transferSOV(account1, 1000, { from: account1 }); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert(liquidityMining.transferSOV(ZERO_ADDRESS, 1000), "Receiver address invalid"); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(liquidityMining.transferSOV(account1, 0), "Amount invalid"); + }); + }); + + describe("add", () => { + it("should be able to add pool token", async () => { + let allocationPoint = new BN(1); + let tx = await liquidityMining.add(token1.address, allocationPoint, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); + + let poolInfo = await liquidityMining.poolInfoList(0); + expect(poolInfo.poolToken).equal(token1.address); + expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); + let blockNumber = new BN(tx.receipt.blockNumber); + expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); + expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + allocationPoint: allocationPoint, + }); + }); + + it("should be able to add 2 pool tokens and update pools", async () => { + let allocationPoint1 = new BN(1); + let tx1 = await liquidityMining.add(token1.address, allocationPoint1, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1); + + expectEvent(tx1, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + allocationPoint: allocationPoint1, + }); + + let allocationPoint2 = new BN(2); + let tx2 = await liquidityMining.add(token2.address, allocationPoint2, true); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1.add(allocationPoint2)); + + expectEvent(tx2, "PoolTokenAdded", { + user: root, + poolToken: token2.address, + allocationPoint: allocationPoint2, + }); + + let poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + let poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo1.lastRewardBlock).bignumber.equal(poolInfo2.lastRewardBlock); + }); + + it("fails if the 0 allocation point is passed", async () => { + await expectRevert(liquidityMining.add(token1.address, new BN(0), false), "Invalid allocation point"); + }); + + it("fails if the 0 address is passed as token address", async () => { + await expectRevert(liquidityMining.add(ZERO_ADDRESS, new BN(1), false), "Invalid token address"); + }); + + it("fails if token already added", async () => { + await liquidityMining.add(token1.address, new BN(1), false); + await expectRevert(liquidityMining.add(token1.address, new BN(1), false), "Token already added"); + }); + + it("only owner or admin should be able to add pool token", async () => { + await expectRevert(liquidityMining.add(token2.address, new BN(1), false, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.add(token2.address, new BN(1), false, { from: account1 }); + }); + }); + + describe("update", () => { + it("should be able to update pool token", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, oldAllocationPoint, false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, newAllocationPoint, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(newAllocationPoint); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, newAllocationPoint, blockNumber, new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: token1.address, + newAllocationPoint: newAllocationPoint, + oldAllocationPoint: oldAllocationPoint, + }); + }); + + it("should be able to update pool token and update pools", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, oldAllocationPoint, false); + + await liquidityMining.add(token2.address, oldAllocationPoint, false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, newAllocationPoint, true); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(oldAllocationPoint.add(newAllocationPoint)); + + let poolInfo = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo.lastRewardBlock).bignumber.equal(new BN(tx.receipt.blockNumber)); + }); + + it("fails if token wasn't added", async () => { + await expectRevert(liquidityMining.update(token1.address, new BN(1), false), "Pool token not found"); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert(liquidityMining.update(token2.address, new BN(1), false, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.update(token2.address, new BN(1), false, { from: account1 }); + }); + }); + + describe("updateTokens", () => { + it("should be able to update 2 pool tokens", async () => { + let poolTokens = [token1.address, token2.address, token3.address]; + let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; + + for (let i = 0; i < poolTokens.length; i++) { + await liquidityMining.add(poolTokens[i], oldAllocationPoints[i], false); + } + + let newAllocationPoints = [new BN(101), new BN(102), new BN(3)]; + let tx = await liquidityMining.updateTokens(poolTokens, newAllocationPoints, true); + + let totalAllocationPoint = new BN(0); + for (let i = 0; i < newAllocationPoints.length; i++) { + totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i]); + } + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(totalAllocationPoint); + + let blockNumber = new BN(tx.receipt.blockNumber); + for (let i = 0; i < poolTokens.length - 1; i++) { + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); + checkPoolInfo(poolInfo, poolTokens[i], newAllocationPoints[i], blockNumber, new BN(0)); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: poolTokens[i], + newAllocationPoint: newAllocationPoints[i], + oldAllocationPoint: oldAllocationPoints[i], + }); + } + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); + + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[poolTokens.length - 1]); + expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); + }); + + it("fails if token wasn't added", async () => { + await expectRevert(liquidityMining.updateTokens([token1.address], [new BN(1)], false), "Pool token not found"); + }); + + it("fails if arrays have different length", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert(liquidityMining.updateTokens([token1.address, token2.address], [new BN(1)], false), "Arrays mismatch"); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert(liquidityMining.updateTokens([token2.address], [new BN(1)], false, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.updateTokens([token2.address], [new BN(1)], false, { from: account1 }); + }); + }); + + describe("deposit", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to deposit", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to deposit using wrapper", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, account2, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); + }); + }); + + describe("claimReward", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await expectRevert(liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward, + }); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimReward(token1.address, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.claimReward(account1, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); + }); + }); + + describe("claimRewardFromAllPools", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await liquidityMining.add(token2.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await expectRevert(liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); + let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); + + let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward1 = await checkUserReward(account1, token1, depositBlockNumber1, latestBlockNumber); + //we have 2 pools with the same allocation points + userReward1 = userReward1.div(new BN(2)); + + await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); + let userReward2 = await checkUserReward(account1, token2, depositBlockNumber2, latestBlockNumber); + //we have 2 pools with the same allocation points + userReward2 = userReward2.div(new BN(2)); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward1, + }); + + expect(userReward1, tx.logs[0].args.amount); + expect(token1.address, tx.logs[0].args.poolToken); + expect(userReward2, tx.logs[1].args.amount); + expect(token2.address, tx.logs[1].args.poolToken); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + }); + + describe("withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw (without claiming reward)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + // User's balance on lockedSOV vault + let userRewardBalance = await lockedSOV.getLockedBalance(account1); + expect(userRewardBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to withdraw (with claiming reward)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was not invoked + let expectedUnlockedBalance = userReward.mul(unlockedImmediatelyPercent).div(new BN(10000)); + let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); + expect(lockedBalance).bignumber.equal(expectedLockedBalance); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward, + }); + }); + + it("should be able to withdraw using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount, wrapper.address); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { from: account1 }), + "Not enough balance" + ); + }); + }); + + describe("emergencyWithdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + + let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + let expectedAccumulatedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); + expectEvent(tx, "EmergencyWithdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + accumulatedReward: expectedAccumulatedReward, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.emergencyWithdraw(account1, { from: account1 }), "Pool token not found"); + }); + }); + + describe("getPassedBlocksWithBonusMultiplier", () => { + it("check calculation", async () => { + let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + let startBlock = await liquidityMining.startBlock(); + let bonusEndBlock = await liquidityMining.bonusEndBlock(); + let blocks; + + //[startBlock, bonusEndBlock] + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock, bonusEndBlock); + expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); + + //[startBlock - 100, bonusEndBlock] + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock.sub(new BN(100)), bonusEndBlock); + expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); + + //[startBlock, bonusEndBlock + 100] + let blocksAfterBonusPeriod = new BN(100); + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock, + bonusEndBlock.add(new BN(blocksAfterBonusPeriod)) + ); + expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier).add(blocksAfterBonusPeriod)); + + //[startBlock, stopMining, ... bonusEndBlock] + await mineBlocks(5); + await liquidityMining.stopMining(); + let endBlock = await liquidityMining.endBlock(); + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock, bonusEndBlock); + expect(blocks).bignumber.equal(endBlock.sub(startBlock).mul(bonusBlockMultiplier)); + }); + }); + + describe("getUserAccumulatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let bonusBlockMultiplier; + let bonusEndBlock; + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint1, false); + await liquidityMining.add(token2.address, allocationPoint2, false); + + await token1.mint(account1, amount1); + await token2.mint(account2, amount2); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token2.approve(liquidityMining.address, amount2, { from: account2 }); + + bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + bonusEndBlock = await liquidityMining.bonusEndBlock(); + }); + + it("check calculation for no deposits", async () => { + const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + expect(reward1).bignumber.equal("0"); + expect(reward2).bignumber.equal("0"); + }); + + it("check calculation for single user, token 1, bonus period off", async () => { + await advanceBlocks(bonusEndBlock); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + + // 1 block has passed, bonus period is off + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 1 * 3 * 1/3 = 1 + const expectedReward = rewardTokensPerBlock.mul(allocationPoint1).div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("1"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + expect(reward).bignumber.equal("2"); + }); + + it("check calculation for single user, token 2, bonus period off", async () => { + await advanceBlocks(bonusEndBlock); + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + + // 1 block has passed, bonus period is off + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 1 * 3 * 2/3 = 2 + const expectedReward = rewardTokensPerBlock.mul(allocationPoint2).div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("2"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + expect(reward).bignumber.equal("4"); + }); + + it("check calculation for single user, token 1, bonus period on", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 10 * 3 * 1/3 = 10 + const expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier).mul(allocationPoint1).div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("10"); // sanity check + expect(reward).bignumber.equal(expectedReward); + }); + + it("check calculation for single user, token 1, bonus period on, smaller amount", async () => { + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 10 * 3 * 1/3 = 10 + // Note that the actual amount deposited plays no role here + expect(reward).bignumber.equal("10"); + }); + + it("check calculation for single user, token 2, bonus period on", async () => { + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 10 * 3 * 2/3 = 20 + const expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier).mul(allocationPoint2).div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("20"); // sanity check + expect(reward).bignumber.equal(expectedReward); + }); + + it("check calculation for two users and tokens", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); + // sanity checks + expect(await liquidityMining.getUserAccumulatedReward(token1.address, account1)).bignumber.equal("10"); + expect(await liquidityMining.getUserAccumulatedReward(token2.address, account2)).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + + // for the first block, user 1 will receive the reward of 10 + // for the second block: + // - user 1 still owns 100% of the shares for token1, so same reward (total 10 + 10 = 20) + // - user 2 owns 100% of the shares for token2, so same reward as in the other cases + expect(reward1).bignumber.equal("20"); + expect(reward2).bignumber.equal("20"); + }); + + it("check calculation for two users, same token (shares taken into account)", async () => { + const token = token1; + const amount = amount1; + await token.mint(account2, amount); + await token.approve(liquidityMining.address, amount, { from: account2 }); + + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); + // sanity checks + expect(await liquidityMining.getUserAccumulatedReward(token.address, account1)).bignumber.equal("10"); + expect(await liquidityMining.getUserAccumulatedReward(token.address, account2)).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward(token.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token.address, account2); + + // for the first block, user 1 will receive the reward of 10 (reward given per block for 100% of shares) + // for the second block: + // - user 1 owns 1/2 of the shares => expected reward = 5 (total 10 + 5 = 15) + // - user 2 owns 1/2 of the shares => expected reward = 5 + expect(reward1).bignumber.equal("15"); + expect(reward2).bignumber.equal("5"); + }); + }); + + describe("getEstimatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const amount3 = new BN(4000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let bonusBlockMultiplier; + let bonusEndBlock; + let secondsPerBlock; + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint1, false); + + await token1.mint(account1, amount1); + await token1.mint(account2, amount2); + await token1.mint(account3, amount3); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + + bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + bonusEndBlock = await liquidityMining.bonusEndBlock(); + + secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); + }); + + it("check calculation for 1 user, period less than 1 block", async () => { + let duration = secondsPerBlock.sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); + let expectedReward = "0"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 1 block", async () => { + let duration = secondsPerBlock; + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); + let expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 30 blocks", async () => { + let blocks = new BN(30); + let duration = secondsPerBlock.mul(blocks); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); + let expectedReward = rewardTokensPerBlock.mul(blocks).mul(bonusBlockMultiplier); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 2 users, period is 100 blocks", async () => { + //turn off bonus period + await advanceBlocks(bonusEndBlock); + + let blocks = new BN(100); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); + let expectedReward = rewardTokensPerBlock.mul(blocks); + let totalAmount = amount1.add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { + await liquidityMining.add(token2.address, allocationPoint2, false); + //turn off bonus period + await advanceBlocks(bonusEndBlock); + + let blocks = new BN(1000); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { from: account2 }); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); + let expectedReward = rewardTokensPerBlock.mul(blocks); + expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); + let totalAmount = amount1.add(amount2).add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + }); + + describe("deposit/withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + // make sure the pool has tokens to distribute + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + }); + + it("add, add, deposit, deposit", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + await liquidityMining.add(token2.address, allocationPoint, false); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); // 1 block passed + + // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); //weight 2/3 + await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit + + const currentBlockNumber = await web3.eth.getBlockNumber(); + + // 3 tokens per share per block, times bonus multiplier (10), times precision (1e12), times weight (1/2), divided by total shares + const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock.mul(new BN(10)).mul(new BN(1e12)).div(new BN(2)).div(amount); + + const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + expect(poolInfo1.poolToken).equal(token1.address); + expect(poolInfo1.allocationPoint).equal("1"); + expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); + // token1 deposit has been there for 2 blocks because of automining + expect(poolInfo1.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString()); + + const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo2.poolToken).equal(token2.address); + expect(poolInfo2.allocationPoint).equal("1"); + expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); + // token2 deposit has been there for only 1 block + expect(poolInfo2.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.toString()); + }); + + // tricky case 1 + it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + await liquidityMining.add(token2.address, allocationPoint, false); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await liquidityMining.update(token1.address, new BN("2"), false); // 1 block passed, new weight 2/3 + const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); // 2 blocks passed + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), 1 block with weight 1/2 = 15, 1 block with weight 2/3 = 20 + const expectedRewardAmount = new BN("35"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: rewardAmount, + }); + }); + + // tricky case 2 + it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks + await mineBlock(); + + // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account2 }); + + // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) + const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) + const withdrawTx2 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account2 }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 30 (because of bonus period), 2 block with 100% shares = 60, 1 block with 50% shares = 15 + const expectedReward1 = new BN("75"); + + // reward per block 30 (because of bonus period), 1 block with 50% shares = 15, 1 block with 100% shares = 30 + const expectedReward2 = new BN("45"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + await checkUserPoolTokens( + account2, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + poolToken: token1.address, + amount: reward2, + }); + }); + + // tricky case 3a + it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit: 1 blocks, note: pool1 is NOT updated + await liquidityMining.add(token2.address, new BN(2), false); // new weight: 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), + // because add was called without updating the pool, the new weight is used for all blocks + // so 2 blocks with weight 1/3 = 20 + const expectedRewardAmount = new BN("20"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 3b + it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit: 1 blocks, note: pool1 IS updated + await liquidityMining.add(token2.address, new BN(2), true); // new weight: 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), + // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block + // so 1 block with weight 1/1 = 30 and 1 block with weight 1/3 = 10 + const expectedRewardAmount = new BN("40"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 4 + it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated + await liquidityMining.add(token2.address, allocationPoint, true); //weight 1/2 + + // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account2 }); + + // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) + const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) + const withdrawTx2 = await liquidityMining.withdraw(token2.address, amount, ZERO_ADDRESS, { from: account2 }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 30 (because of bonus period) + // deposit 1 has 1 block with weight 1/1 (30) and 2 blocks with weight 1/2 (15*2 = 30) + const expectedReward1 = new BN("60"); + + // deposit 2 has 2 blocks with weight 1/2 (15 * 2 = 30) + const expectedReward2 = new BN("30"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + for (let account of [account1, account2]) { + for (let token of [token1, token2]) { + await checkUserPoolTokens( + account, + token, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + } + } + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token2.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + poolToken: token2.address, + amount: reward2, + }); + }); + }); + + describe("LM configuration", () => { + //Maximum reward per week: 100K SOV (or 100M SOV) + //Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) + + const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); + // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); + + //SOV/BTC pool 40K per week + //ETH/BTC pool 37.5K per week (from second week) + //Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) + + const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); + // const MAX_ALLOCATION_POINT = new BN(100000); + const ALLOCATION_POINT_SOV_BTC = new BN(40000); + const ALLOCATION_POINT_ETH_BTC = new BN(37500); + + const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); + + const amount = new BN(1000); + + beforeEach(async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + REWARD_TOKENS_PER_BLOCK, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + 0 + ); + + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + //turn off bonus period + let bonusEndBlock = await liquidityMining.bonusEndBlock(); + await advanceBlocks(bonusEndBlock); + }); + + it("dummy pool + 1 pool", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + + await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); //weight 40000 / 100000 + await liquidityMining.add(dummyPool, MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC), false); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks passed since last deposit + await mineBlocks(9); + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks passed + let passedBlocks = 10; + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); + console.log(expectedUserReward.toString()); + }); + + it("dummy pool + 2 pools", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + let ETHBTCpoll = token2.address; + + await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); //weight 40000 / 100000 + const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); + await liquidityMining.add(dummyPool, DUMMY_ALLOCATION_POINT, false); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlocks(9); + await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit + + //update config + //this method will also update pool reward using previous allocation point, + //so this block should be add to calculation with old values + await liquidityMining.update(SOVBTCpool, ALLOCATION_POINT_SOV_BTC_2, false); //weight 30000 / 100000 + + await liquidityMining.add(ETHBTCpoll, ALLOCATION_POINT_ETH_BTC, false); //weight 37500 / 100000 + const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC_2).sub(ALLOCATION_POINT_ETH_BTC); + await liquidityMining.update(dummyPool, DUMMY_ALLOCATION_POINT_2, false); //weight (100000 - 30000 - 37500) / 100000 + await liquidityMining.updateAllPools(); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks + 5 blocks passed + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks + 5 blocks passed + let passedBlocks = 10 + 1; //block should be add to calculation with old values + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + passedBlocks = 5 - 1; //block should be removed from calculation with new values + expectedUserReward = expectedUserReward.add( + REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)).mul(ALLOCATION_POINT_SOV_BTC_2).div(MAX_ALLOCATION_POINT) + ); + expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); + console.log(expectedUserReward.toString()); + }); + }); + + describe("onTokensDeposited", () => { + it("should revert if the sender is not a valid pool token", async () => { + await expectRevert(liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), "Pool token not found"); + }); + }); + + describe("external getters", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await liquidityMining.add(token1.address, allocationPoint, false); + }); + + it("PRECISION", async () => { + expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); + }); + + it("BONUS_BLOCK_MULTIPLIER", async () => { + expect(await liquidityMining.BONUS_BLOCK_MULTIPLIER()).bignumber.equal("10"); + }); + + it("SVR", async () => { + expect(await liquidityMining.SOV()).equal(SOVToken.address); + }); + + it("rewardTokensPerBlock", async () => { + expect(await liquidityMining.rewardTokensPerBlock()).bignumber.equal(rewardTokensPerBlock); + }); + + it("startBlock", async () => { + expect(await liquidityMining.startBlock()).bignumber.gt("0"); + }); + + it("bonusEndBlock", async () => { + const startBlock = await liquidityMining.startBlock(); + expect(await liquidityMining.bonusEndBlock()).bignumber.equal(startBlock.add(numberOfBonusBlocks)); + }); + + it("endBlock", async () => { + expect(await liquidityMining.endBlock()).bignumber.equal("0"); + }); + + it("wrapper", async () => { + expect(await liquidityMining.wrapper()).equal(wrapper.address); + }); + + it("totalAllocationPoint", async () => { + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); + await liquidityMining.add(token2.address, allocationPoint, false); + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint.mul(new BN(2))); + }); + + it("totalUsersBalance", async () => { + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("30"); + }); + + // could still test these, but I don't see much point: + // PoolInfo[] public poolInfoList; + // mapping(address => uint256) poolIdList; + // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + + it("getMissedBalance", async () => { + let missedBalance = await liquidityMining.getMissedBalance(); + expect(missedBalance).bignumber.equal("0"); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await liquidityMining.updatePool(token1.address); + + missedBalance = await liquidityMining.getMissedBalance(); + expect(missedBalance).bignumber.equal("30"); + }); + + it("getUserAccumulatedReward", async () => { + // real tests are elsewhere in this file + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token1.address, account2); + expect(reward1).bignumber.equal("30"); + expect(reward2).bignumber.equal("0"); + }); + + it("getPoolId", async () => { + const poolId = await liquidityMining.getPoolId(token1.address); + expect(poolId).bignumber.equal("0"); + await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); + await liquidityMining.add(token2.address, allocationPoint, false); + const poolId2 = await liquidityMining.getPoolId(token2.address); + expect(poolId2).bignumber.equal("1"); + }); + + it("getPoolLength", async () => { + let length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("1"); + + await liquidityMining.add(token2.address, allocationPoint, false); + length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("2"); + }); + + it("getPoolInfoList", async () => { + const infoList = await liquidityMining.getPoolInfoList(); + expect(infoList).to.be.an("array"); + expect(infoList.length).equal(1); + const info = infoList[0]; + expect(info.poolToken).equal(token1.address); + expect(info.allocationPoint).equal(allocationPoint.toString()); + expect(info.accumulatedRewardPerShare).equal("0"); + expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + }); + + it("getPoolInfo", async () => { + const info = await liquidityMining.getPoolInfo(token1.address); + expect(info.poolToken).equal(token1.address); + expect(info.allocationPoint).equal(allocationPoint.toString()); + expect(info.accumulatedRewardPerShare).equal("0"); + expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + + await expectRevert(liquidityMining.getPoolInfo(token2.address), "Pool token not found"); + }); + + it("getUserBalanceList", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + const balanceList = await liquidityMining.getUserBalanceList(account1); + + expect(balanceList).to.be.an("array"); + expect(balanceList.length).equal(1); + const balanceData = balanceList[0]; + expect(balanceData).to.be.an("array"); + expect(balanceData[0]).bignumber.equal(amount); + expect(balanceData[1]).bignumber.equal("30"); + }); + + it("getUserInfo", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? + expect(userInfo.rewardDebt).bignumber.equal("0"); // not yet updated either + + // deposit updates it. + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); + userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("501"); + expect(userInfo.accumulatedReward).bignumber.equal("30"); + expect(userInfo.rewardDebt).bignumber.equal("30"); + }); + + it("getUserInfoList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + + let userInfoList = await liquidityMining.getUserInfoList(account1); + expect(userInfoList).to.be.an("array"); + expect(userInfoList.length).equal(1); + const userInfo = userInfoList[0]; + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.accumulatedReward).bignumber.equal("0"); + expect(userInfo.rewardDebt).bignumber.equal("0"); + }); + + it("getUserAccumulatedRewardList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + + let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); + expect(rewardList).to.be.an("array"); + expect(rewardList.length).equal(1); + expect(rewardList[0]).bignumber.equal("0"); + }); + + it("getUserPoolTokenBalance", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance(token1.address, account1); + expect(poolTokenBalance).bignumber.equal(new BN(500)); + }); + }); + + describe("Migration", () => { + const MigrationGracePeriodStates = { + None: 0, + Started: 1, + Finished: 2, + }; + + describe("Grace period start", () => { + it("should be deployed with state being before grace period", async () => { + const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.None); + }); + + it("should only allow grace period to be started by the admin", async () => { + await expectRevert(liquidityMining.startMigrationGracePeriod({ from: account1 }), "unauthorized"); + }); + + it("should properly start grace period", async () => { + await liquidityMining.startMigrationGracePeriod(); + + const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Started); + + const endBlock = await liquidityMining.endBlock(); + expect(endBlock).not.equal(0); + }); + + it("should fail when trying to start the grace period if already started", async () => { + await liquidityMining.startMigrationGracePeriod(); + // check that the grace period is started + const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Started); + + await expectRevert(liquidityMining.startMigrationGracePeriod(), "Forbidden: migration already started"); + }); + + it("should not let users to deposit", async () => { + await liquidityMining.startMigrationGracePeriod(); + await expectRevert( + liquidityMining.deposit(token1.address, new BN(10), ZERO_ADDRESS, { from: account1 }), + "Forbidden: migration already started" + ); + }); + + it("should not let lending pools to deposit", async () => { + await liquidityMining.startMigrationGracePeriod(); + await expectRevert( + liquidityMining.onTokensDeposited(token1.address, new BN(10), { from: account1 }), + "Forbidden: migration already started" + ); + }); + + it("should let users to update the pools information", async () => { + let allocationPoint = new BN(1); + await liquidityMining.add(token1.address, allocationPoint, false); + await liquidityMining.startMigrationGracePeriod(); + + await liquidityMining.updatePool(token1.address); + await liquidityMining.updateAllPools(); + }); + }); + + describe("Grace period finish", () => { + it("should only allow admin to finish the grace period", async () => { + await expectRevert(liquidityMining.finishMigrationGracePeriod({ from: account1 }), "unauthorized"); + }); + + it("should fail if grace period hasn't started before finishing", async () => { + await expectRevert(liquidityMining.finishMigrationGracePeriod(), "Migration hasn't started yet"); + }); + + it("should fail if grace period has already finished", async () => { + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + + const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Finished); + + await expectRevert(liquidityMining.finishMigrationGracePeriod(), "Forbidden: contract deprecated"); + }); + + it("should properly finish grace period", async () => { + await liquidityMining.startMigrationGracePeriod(); + // check that the grace period is started + let migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Started); + + await liquidityMining.finishMigrationGracePeriod(); + // check that the grace period is finished + migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Finished); + }); + + it("should not let users to withdraw", async () => { + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + await expectRevert(liquidityMining.withdraw(token1.address, new BN(10), account1), "Forbidden: contract deprecated"); + await expectRevert(liquidityMining.emergencyWithdraw(token1.address), "Forbidden: contract deprecated"); + }); + + it("should not let users to claim rewards", async () => { + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + await expectRevert(liquidityMining.claimRewardFromAllPools(account1), "Forbidden: contract deprecated"); + + await expectRevert(liquidityMining.claimReward(token1.address, account1), "Forbidden: contract deprecated"); + }); + + it("should not let users to update the pools information", async () => { + let allocationPoint = new BN(1); + await liquidityMining.add(token1.address, allocationPoint, false); + + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + await expectRevert(liquidityMining.updatePool(token1.address), "Forbidden: contract deprecated"); + await expectRevert(liquidityMining.updateAllPools(), "Forbidden: contract deprecated"); + }); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMining = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMiningV2 = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + } + + async function checkBonusPeriodHasNotEnded() { + expect(await liquidityMining.bonusEndBlock()).bignumber.gt((await web3.eth.getBlockNumber()).toString()); + } + + function checkPoolInfo(poolInfo, token, allocationPoint, lastRewardBlock, accumulatedRewardPerShare) { + expect(poolInfo.poolToken).equal(token); + expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); + expect(poolInfo.lastRewardBlock).bignumber.equal(lastRewardBlock); + if (accumulatedRewardPerShare.toNumber() !== -1) { + expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(accumulatedRewardPerShare); + } + } + + async function checkUserPoolTokens(user, poolToken, _userAmount, _liquidityMiningBalance, _userBalance, wrapper) { + //user balance in pool + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.amount).bignumber.equal(_userAmount); + //LM balance of pool tokens + let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); + expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); + //user's balance of pool tokens + let userBalance = await poolToken.balanceOf(user); + if (wrapper !== undefined) { + userBalance = await poolToken.balanceOf(wrapper); + } + expect(userBalance).bignumber.equal(_userBalance); + } + + //user's balance of reward token + async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { + let passedBlocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(depositBlockNumber, latestBlockNumber); + let userReward = passedBlocks.mul(rewardTokensPerBlock); + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + return userReward; + } +}); + +describe("Contract upgrade", async () => { + let liquidityMiningProxy; + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + [root, account1, account2] = await web3.eth.getAccounts(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + }); + + it("should be able to withdraw and SOV tokens to be transferred", async () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + + SOVToken.transfer(liquidityMining.address, amount); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await upgradeLiquidityMining(); + + let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + + expectEvent(tx, "EmergencyWithdraw", { + user: account1, + poolToken: token1.address, + amount, + }); + + tx = await liquidityMining.transferSOV(root, amount, { from: root }); + expectEvent(tx, "SOVTransferred", { + receiver: root, + amount: amount, + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + } +}); diff --git a/tests/farm/LiquidityMiningV2.js b/tests/farm/LiquidityMiningV2.js new file mode 100644 index 000000000..ab57796c6 --- /dev/null +++ b/tests/farm/LiquidityMiningV2.js @@ -0,0 +1,2008 @@ +const { expect } = require("chai"); +const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); +const { etherMantissa, mineBlock } = require("../Utils/Ethereum"); + +const { ZERO_ADDRESS } = constants; +const TOTAL_SUPPLY = etherMantissa(1000000000); + +const TestToken = artifacts.require("TestToken"); +const LiquidityMiningConfigToken = artifacts.require("LiquidityMiningConfigToken"); +const LiquidityMiningLogic = artifacts.require("LiquidityMiningMockup"); +const LiquidityMiningLogicV1 = artifacts.require("LiquidityMiningV1Mockup"); +const LiquidityMiningProxy = artifacts.require("LiquidityMiningProxy"); +const LiquidityMiningLogicV2 = artifacts.require("LiquidityMiningMockupV2"); +const LiquidityMiningProxyV2 = artifacts.require("LiquidityMiningProxyV2"); +const TestLockedSOV = artifacts.require("LockedSOVMockup"); +const Wrapper = artifacts.require("RBTCWrapperProxyMockupV2"); +const LockedSOVRewardTransferLogic = artifacts.require("LockedSOVRewardTransferLogic"); +const ERC20TransferLogic = artifacts.require("ERC20TransferLogic"); +const TestPoolToken = artifacts.require("TestPoolToken"); +const Migrator = artifacts.require("LMV1toLMV2Migrator"); + +describe("LiquidityMiningV2", () => { + const name = "Test SOV Token"; + const symbol = "TST"; + + const PRECISION = 1e12; + + const rewardTokensPerBlock = new BN(3); + const startDelayBlocks = new BN(1); + const numberOfBonusBlocks = new BN(50); + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let root, account1, account2, account3, account4; + let SOVToken, token1, token2, token3, liquidityMiningConfigToken; + let liquidityMiningV1, liquidityMining, migrator, wrapper; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + let erc20RewardTransferLogic; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMiningV1.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMiningV1.initialize(liquidityMining.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); + + await liquidityMining.initialize(wrapper.address, migrator.address, SOVToken.address); + + erc20RewardTransferLogic = await ERC20TransferLogic.new(); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMining.setWrapper(wrapper.address); + await liquidityMining.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + //mint SOVs to lvm1 for migrations + await SOVToken.mint(liquidityMiningV1.address, new BN(10)); + await liquidityMiningV1.addAdmin(migrator.address); + await liquidityMiningV1.startMigrationGracePeriod(); + await liquidityMining.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + //burn SOVs for testing + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await SOVToken.burn(liquidityMining.address, balanceSOV); + }); + + describe("initialize", () => { + it("should fail if migrator address is invalid", async () => { + await deployLiquidityMiningV2(); + await expectRevert(liquidityMining.initialize(wrapper.address, ZERO_ADDRESS, SOVToken.address), "invalid contract address"); + }); + + it("should fail if SOV address is invalid", async () => { + await deployLiquidityMiningV2(); + await expectRevert( + liquidityMining.initialize(wrapper.address, liquidityMiningV1.address, ZERO_ADDRESS), + "invalid token address" + ); + }); + + it("sets the expected values", async () => { + await deployLiquidityMiningV2(); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address, SOVToken.address); + + let _wrapper = await liquidityMining.wrapper(); + + expect(_wrapper).equal(wrapper.address); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await liquidityMining.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(liquidityMining.addAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await liquidityMining.addAdmin(account1); + let tx = await liquidityMining.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(liquidityMining.removeAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("setWrapper", () => { + it("sets the expected values", async () => { + let newWrapper = account2; + await liquidityMining.setWrapper(newWrapper); + + let _wrapper = await liquidityMining.wrapper(); + expect(_wrapper).equal(newWrapper); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert(liquidityMining.setWrapper(account2, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setWrapper(account2, { from: account1 }); + }); + }); + + describe("stopMining", () => { + it("should set end block", async () => { + let tx = await liquidityMining.stopMining(SOVToken.address); + + let blockNumber = new BN(tx.receipt.blockNumber); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + let _endBlock = rewardToken.endBlock; + expect(_endBlock).bignumber.equal(blockNumber); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert(liquidityMining.stopMining(SOVToken.address, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.stopMining(SOVToken.address, { from: account1 }); + }); + + it("fails if already stopped", async () => { + await liquidityMining.stopMining(SOVToken.address); + await expectRevert(liquidityMining.stopMining(SOVToken.address), "Already stopped"); + }); + + it("fails if reward token is not initialized", async () => { + await liquidityMining.stopMining(SOVToken.address); + await expectRevert(liquidityMining.stopMining(token3.address), "Not initialized"); + }); + }); + + describe("transferRewardTokens", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOVToken.transfer(liquidityMining.address, amount); + + let balanceBefore = await SOVToken.balanceOf(account1); + await liquidityMining.transferRewardTokens(SOVToken.address, account1, amount); + let balanceAfter = await SOVToken.balanceOf(account1); + + expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert(liquidityMining.transferRewardTokens(SOVToken.address, account1, 1000, { from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.transferRewardTokens(SOVToken.address, account1, 1000, { from: account1 }); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert(liquidityMining.transferRewardTokens(SOVToken.address, ZERO_ADDRESS, 1000), "Receiver address invalid"); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(liquidityMining.transferRewardTokens(SOVToken.address, account1, 0), "Amount invalid"); + }); + + it("fails if the zero address is passed as token", async () => { + await expectRevert(liquidityMining.transferRewardTokens(ZERO_ADDRESS, account1, 0), "Reward address invalid"); + }); + }); + + describe("addRewardToken", () => { + /* + address _rewardToken, + uint256 _rewardTokensPerBlock, + uint256 _startDelayBlocks, + address _rewardTransferLogic + */ + let otherRewardTokensPerBlock = 2; + let otherStartDelayBlocks = 3; + + it("should be able to add a reward token", async () => { + const transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + await liquidityMining.addRewardToken(token1.address, otherRewardTokensPerBlock, otherStartDelayBlocks, transferLogic.address); + const rewardToken = await liquidityMining.getRewardToken(token1.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(new BN(0)); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + expect(rewardToken.rewardTokensPerBlock).bignumber.equal(new BN(2)); + }); + + it("fails if start delay blocks is not greater than 0", async () => { + const addInvalidAddress = liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + 0, + erc20RewardTransferLogic.address + ); + await expectRevert(addInvalidAddress, "Invalid start block"); + }); + + it("fails if reward token address is not valid", async () => { + const addInvalidAddress = liquidityMining.addRewardToken( + ZERO_ADDRESS, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + erc20RewardTransferLogic.address + ); + await expectRevert(addInvalidAddress, "Invalid token address"); + }); + + it("fails if token is already added as reward token", async () => { + const transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + await liquidityMining.addRewardToken(token1.address, otherRewardTokensPerBlock, otherStartDelayBlocks, transferLogic.address); + const addReward = liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + transferLogic.address + ); + await expectRevert(addReward, "Already added"); + }); + + it("fails if reward transfer logic doesn't correspond to given reward token", async () => { + const addInvalidAddress = liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + erc20RewardTransferLogic.address + ); + await expectRevert(addInvalidAddress, "Reward token and transfer logic mismatch"); + }); + }); + + describe("add", () => { + it("should be able to add pool token", async () => { + let allocationPoint = new BN(1); + let tx = await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint); + + let poolInfo = await liquidityMining.poolInfoList(0); + expect(poolInfo).equal(token1.address); + + let poolReward = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + expect(poolReward.allocationPoint).bignumber.equal(allocationPoint); + let blockNumber = new BN(tx.receipt.blockNumber); + expect(poolReward.lastRewardBlock).bignumber.equal(blockNumber); + expect(poolReward.accumulatedRewardPerShare).bignumber.equal(new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + rewardTokens: [SOVToken.address], + }); + expect(tx.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint); + }); + + it("should be able to add 2 pool tokens and update pools", async () => { + let allocationPoint1 = new BN(1); + let tx1 = await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint1], false); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint1); + + expectEvent(tx1, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + rewardTokens: [SOVToken.address], + }); + expect(tx1.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint1); + + let allocationPoint2 = new BN(2); + let tx2 = await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint2], true); + + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint1.add(allocationPoint2)); + + expectEvent(tx2, "PoolTokenAdded", { + user: root, + poolToken: token2.address, + rewardTokens: [SOVToken.address], + }); + expect(tx2.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint2); + + let poolRewardInfo1 = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + let poolRewardInfo2 = await liquidityMining.getPoolReward(token2.address, SOVToken.address); + expect(poolRewardInfo1.lastRewardBlock).bignumber.equal(poolRewardInfo2.lastRewardBlock); + }); + + it("fails if the reward token list is empty", async () => { + await expectRevert(liquidityMining.add(token1.address, [], [new BN(0)], false), "Invalid reward tokens length"); + }); + + it("fails if the reward tokens length is different to allocation points", async () => { + await expectRevert( + liquidityMining.add(token1.address, [token2.address, token3.address], [new BN(1)], false), + "Invalid allocation points length" + ); + }); + + it("fails if the reward token is repeated", async () => { + await expectRevert( + liquidityMining.add(token1.address, [SOVToken.address, SOVToken.address], [new BN(1), new BN(1)], false), + "Already associated" + ); + }); + + it("fails if the reward token is not valid", async () => { + await expectRevert(liquidityMining.add(token1.address, [token2.address], [new BN(1)], false), "Not initialized"); + }); + + it("fails if the 0 allocation point is passed", async () => { + await expectRevert(liquidityMining.add(token1.address, [SOVToken.address], [new BN(0)], false), "Invalid allocation point"); + }); + + it("fails if the 0 address is passed as token address", async () => { + await expectRevert(liquidityMining.add(ZERO_ADDRESS, [SOVToken.address], [new BN(0)], false), "Invalid token address"); + }); + + it("fails if token already added", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [new BN(1)], false); + await expectRevert(liquidityMining.add(token1.address, [SOVToken.address], [new BN(0)], false), "Token already added"); + }); + + it("only owner or admin should be able to add pool token", async () => { + await expectRevert( + liquidityMining.add(token2.address, [SOVToken.address], [new BN(0)], false, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false, { from: account1 }); + }); + }); + + describe("update", () => { + it("should be able to update pool token", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, [SOVToken.address], [oldAllocationPoint], false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, [SOVToken.address], [newAllocationPoint], false); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(newAllocationPoint); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, newAllocationPoint, blockNumber, new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: token1.address, + rewardToken: SOVToken.address, + newAllocationPoint: newAllocationPoint, + oldAllocationPoint: oldAllocationPoint, + }); + }); + + it("should be able to update pool token and update pools", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, [SOVToken.address], [oldAllocationPoint], false); + + await liquidityMining.add(token2.address, [SOVToken.address], [oldAllocationPoint], false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, [SOVToken.address], [newAllocationPoint], true); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(oldAllocationPoint.add(newAllocationPoint)); + + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + expect(poolRewardToken.lastRewardBlock).bignumber.equal(new BN(tx.receipt.blockNumber)); + }); + + it("fails if token wasn't added", async () => { + await expectRevert(liquidityMining.update(token1.address, [SOVToken.address], [new BN(1)], false), "Pool token not found"); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); + await expectRevert( + liquidityMining.update(token2.address, [SOVToken.address], [new BN(1)], false, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.update(token2.address, [SOVToken.address], [new BN(1)], false, { from: account1 }); + }); + }); + + describe("updateTokens", () => { + it("should be able to update 2 pool tokens", async () => { + let poolTokens = [token1.address, token2.address, token3.address]; + let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; + + for (let i = 0; i < poolTokens.length; i++) { + await liquidityMining.add(poolTokens[i], [SOVToken.address], [oldAllocationPoints[i]], false); + } + + let newAllocationPoints = [[new BN(101)], [new BN(102)], [new BN(3)]]; + let rewardTokens = new Array(3).fill([SOVToken.address]); + let tx = await liquidityMining.updateTokens(poolTokens, rewardTokens, newAllocationPoints, true); + + let totalAllocationPoint = new BN(0); + for (let i = 0; i < newAllocationPoints.length; i++) { + totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i][0]); + } + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(totalAllocationPoint); + + let blockNumber = new BN(tx.receipt.blockNumber); + for (let i = 0; i < poolTokens.length - 1; i++) { + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); + const poolRewardToken = await liquidityMining.getPoolReward(poolTokens[i], SOVToken.address); + checkPoolRewardInfo(poolInfo, poolTokens[i], poolRewardToken, newAllocationPoints[i][0], blockNumber, new BN(0)); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: poolTokens[i], + rewardToken: SOVToken.address, + newAllocationPoint: newAllocationPoints[i][0], + oldAllocationPoint: oldAllocationPoints[i], + }); + } + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); + + const poolRewardToken = await liquidityMining.getPoolReward(poolTokens[poolTokens.length - 1], SOVToken.address); + expect(poolRewardToken.lastRewardBlock).bignumber.equal(blockNumber); + }); + + it("fails if token wasn't added", async () => { + await expectRevert( + liquidityMining.updateTokens([token1.address], [[SOVToken.address]], [[new BN(1)]], false), + "Pool token not found" + ); + }); + + it("fails if arrays have different length", async () => { + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); + await expectRevert( + liquidityMining.updateTokens([token1.address, token2.address], [[SOVToken.address]], [[new BN(1)]], false), + "Arrays mismatch" + ); + + await expectRevert( + liquidityMining.updateTokens([token1.address, token2.address], [[SOVToken.address]], [[new BN(1)], [new BN(1)]], false), + "Arrays mismatch" + ); + + await expectRevert( + liquidityMining.updateTokens( + [token1.address, token2.address], + [[SOVToken.address, token3.address], [SOVToken.address]], + [[new BN(1)], [new BN(1)]], + false + ), + "Arrays mismatch" + ); + + await expectRevert( + liquidityMining.updateTokens( + [token1.address, token2.address], + [[SOVToken.address, token3.address]], + [[new BN(1)], [new BN(1)]], + false + ), + "Arrays mismatch" + ); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); + await expectRevert( + liquidityMining.updateTokens([token2.address], [[SOVToken.address]], [[new BN(1)]], false, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.updateTokens([token2.address], [[SOVToken.address]], [[new BN(1)]], false, { from: account1 }); + }); + }); + + describe("deposit", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + it("should only allow to deposit if migration is finished", async () => { + await deployLiquidityMiningV2(); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address, SOVToken.address); + await liquidityMining.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + await expectRevert( + liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }), + "Migration is not over yet" + ); + }); + it("should be able to deposit", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to deposit using wrapper", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, account2, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to deposit 0 amount", async () => { + let tx = await liquidityMining.deposit(token1.address, new BN(0), ZERO_ADDRESS, { from: account1 }); + + expectEvent(tx, "Deposit", { + user: account1, + poolToken: token1.address, + amount: new BN(0), + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); + }); + }); + + describe("claimRewards", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await expectRevert(liquidityMining.claimRewards(token1.address, ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.claimRewards(token1.address, ZERO_ADDRESS, { from: account1 }); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + amount: userReward, + rewardToken: SOVToken.address, + }); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimReward(token1.address, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + + it("should not take into account blocks before start delay", async () => { + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + await token3.transfer(liquidityMining.address, new BN(1000)); + + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(5), erc20RewardTransferLogic.address); + await liquidityMining.add(token2.address, [token3.address], [allocationPoint], true); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlock(); + + await liquidityMining.updateAllPools(); + const reward1 = await liquidityMining.getUserAccumulatedReward(token2.address, token3.address, account1); + expect(reward1).bignumber.equal(new BN(0)); + + await mineBlock(); + + let tx = await liquidityMining.claimRewards(token2.address, ZERO_ADDRESS, { from: account1 }); + // at this point there was 1 mined block after start block + expectEvent(tx, "RewardClaimed", { + user: account1, + amount: new BN(3), + rewardToken: token3.address, + }); + }); + + it("should not take into account blocks after stop mining", async () => { + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + await token3.transfer(liquidityMining.address, new BN(1000)); + + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(2), erc20RewardTransferLogic.address); + + await liquidityMining.add(token2.address, [token3.address], [allocationPoint], true); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlocks(1); + await liquidityMining.stopMining(token3.address); + await mineBlocks(2); + + let tx = await liquidityMining.claimRewards(token2.address, ZERO_ADDRESS, { from: account1 }); + // last 2 blocks should not accumulate rewards + expectEvent(tx, "RewardClaimed", { + user: account1, + amount: new BN(6), + rewardToken: token3.address, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.claimRewards(account1, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); + }); + }); + + describe("claimRewardFromAllPools", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await expectRevert(liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); + let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); + + let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward1 = await checkUserReward(account1, token1, depositBlockNumber1, latestBlockNumber); + //we have 2 pools with the same allocation points + userReward1 = userReward1.div(new BN(2)); + + await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); + let userReward2 = await checkUserReward(account1, token2, depositBlockNumber2, latestBlockNumber); + //we have 2 pools with the same allocation points + userReward2 = userReward2.div(new BN(2)); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: userReward1, + }); + + expect(userReward1, tx.logs[0].args.amount); + expect(token1.address, tx.logs[0].args.poolToken); + expect(userReward2, tx.logs[1].args.amount); + expect(token2.address, tx.logs[1].args.poolToken); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + }); + + describe("withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw (without claiming reward)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, blockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + // User's balance on lockedSOV vault + let userRewardBalance = await lockedSOV.getLockedBalance(account1); + expect(userRewardBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to withdraw (with claiming reward)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was not invoked + let expectedUnlockedBalance = userReward.mul(unlockedImmediatelyPercent).div(new BN(10000)); + let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); + expect(lockedBalance).bignumber.equal(expectedLockedBalance); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: userReward, + }); + }); + + it("should be able to withdraw using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount, wrapper.address); + let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + }); + + it("fails if withdraw for a user without using wrapper or pool", async () => { + await expectRevert( + liquidityMining.withdraw(token1.address, amount, account1, { from: account3 }), + "only wrapper or pools may withdraw for a user" + ); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { from: account1 }), + "Not enough balance" + ); + }); + }); + + describe("emergencyWithdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.rewards[0].rewardDebt).bignumber.equal(new BN(0)); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(new BN(0)); + + expectEvent(tx, "EmergencyWithdraw", { + user: account1, + poolToken: token1.address, + rewardToken: SOVToken.address, + amount: amount, + accumulatedReward: rewardTokensPerBlock, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert(liquidityMining.emergencyWithdraw(account1, { from: account1 }), "Pool token not found"); + }); + }); + + describe("getUserAccumulatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + + beforeEach(async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint1], false); + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint2], false); + + await token1.mint(account1, amount1); + await token2.mint(account2, amount2); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token2.approve(liquidityMining.address, amount2, { from: account2 }); + }); + + it("check calculation for no deposits", async () => { + const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); + expect(reward1).bignumber.equal("0"); + expect(reward2).bignumber.equal("0"); + }); + + it("check calculation for single user, token 1", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); + + // 1 block has passed + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 1 * 3 * 1/3 = 1 + const expectedReward = rewardTokensPerBlock.mul(allocationPoint1).div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("1"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); + expect(reward).bignumber.equal("2"); + }); + + it("check calculation for single user, token 2", async () => { + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); + + // 1 block has passed + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 1 * 3 * 2/3 = 2 + const expectedReward = rewardTokensPerBlock.mul(allocationPoint2).div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("2"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); + expect(reward).bignumber.equal("4"); + }); + + it("check calculation for two users and tokens", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); + // sanity checks + expect(await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1)).bignumber.equal("1"); + expect(await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2)).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); + + // for the first block, user 1 will receive the reward of 1 + // for the second block: + // - user 1 still owns 100% of the shares for token1, so same reward (total 1 + 1 = 2) + // - user 2 owns 100% of the shares for token2, so same reward as in the other cases + expect(reward1).bignumber.equal("2"); + expect(reward2).bignumber.equal("2"); + }); + + it("check calculation for two users, same token (shares taken into account)", async () => { + const token = token1; + const amount = amount1; + await token.mint(account2, amount); + await token.approve(liquidityMining.address, amount, { from: account2 }); + + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); + // sanity checks + expect(await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account1)).bignumber.equal("1"); + expect(await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account2)).bignumber.equal("0"); + await mineBlock(); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account2); + + // for the first block, user 1 will receive the reward of 1 (reward given per block for 100% of shares) + // after 2 blocks: + // - user 1 owns 1/2 of the shares => expected reward = 1 (total 1 + 1 = 2) + // - user 2 owns 1/2 of the shares => expected reward = 1 + expect(reward1).bignumber.equal("2"); + expect(reward2).bignumber.equal("1"); + }); + }); + + describe("getEstimatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const amount3 = new BN(4000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let secondsPerBlock; + + beforeEach(async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint1], false); + + await token1.mint(account1, amount1); + await token1.mint(account2, amount2); + await token1.mint(account3, amount3); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + + secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); + }); + + it("check calculation for 1 user 2 delayed block reward, period less than 2 blocks", async () => { + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(2), erc20RewardTransferLogic.address); + await liquidityMining.add(token2.address, [token3.address], [allocationPoint1], false); + let duration = secondsPerBlock.mul(new BN(2)).sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward(token2.address, token3.address, amount3, duration); + let expectedReward = "0"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user 2 delayed block reward, period less than 3 blocks", async () => { + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(2), erc20RewardTransferLogic.address); + await liquidityMining.add(token2.address, [token3.address], [allocationPoint1], false); + let duration = secondsPerBlock.mul(new BN(3)).sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward(token2.address, token3.address, amount3, duration); + let expectedReward = "3"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period less than 1 block", async () => { + let duration = secondsPerBlock.sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); + let expectedReward = "0"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 1 block", async () => { + let duration = secondsPerBlock; + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); + let expectedReward = rewardTokensPerBlock; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 40 blocks", async () => { + let blocks = new BN(40); + let duration = secondsPerBlock.mul(blocks); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); + let expectedReward = rewardTokensPerBlock.mul(blocks); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 2 users, period is 100 blocks", async () => { + let blocks = new BN(100); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); + let expectedReward = rewardTokensPerBlock.mul(blocks); + let totalAmount = amount1.add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint2], false); + + let blocks = new BN(1000); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { from: account2 }); + + let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); + let expectedReward = rewardTokensPerBlock.mul(blocks); + expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); + let totalAmount = amount1.add(amount2).add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + }); + + describe("deposit/withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + // make sure the pool has tokens to distribute + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + }); + + it("add, add, deposit, deposit", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); // 1 block passed + + // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); //weight 2/3 + await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit + + const currentBlockNumber = await web3.eth.getBlockNumber(); + + // 3 tokens per share per block, times precision (1e12), times weight (1/2), divided by total shares + const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock.mul(new BN(1e12)).div(new BN(2)).div(amount); + + const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + const poolReward = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + expect(poolInfo1.poolToken).equal(token1.address); + expect(poolReward.allocationPoint).equal("1"); + expect(poolReward.lastRewardBlock).equal(currentBlockNumber.toString()); + // token1 deposit has been there for 2 blocks because of automining + expect(poolReward.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString()); + + const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + const poolReward2 = await liquidityMining.getPoolReward(token2.address, SOVToken.address); + expect(poolInfo2.poolToken).equal(token2.address); + expect(poolReward2.allocationPoint).equal("1"); + expect(poolReward2.lastRewardBlock).equal(currentBlockNumber.toString()); + // token2 deposit has been there for only 1 block + expect(poolReward2.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.toString()); + }); + + // // tricky case 1 + it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await liquidityMining.update(token1.address, [SOVToken.address], [new BN("2")], false); // 1 block passed, new weight 2/3 + const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); // 2 blocks passed + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 3 (because of bonus period), 1 block with weight 1/2 = 1, 1 block with weight 2/3 = 2 + const expectedRewardAmount = new BN("3"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: rewardAmount, + }); + }); + + // // tricky case 2 + it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks + await mineBlock(); + + // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account2 }); + + // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) + const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) + const withdrawTx2 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account2 }); + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 3, 2 block with 100% shares = 6, 1 block with 50% shares = 1 + const expectedReward1 = new BN("7"); + + // reward per block 3, 1 block with 50% shares = 1, 1 block with 100% shares = 3 + const expectedReward2 = new BN("4"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + await checkUserPoolTokens( + account2, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + rewardToken: SOVToken.address, + amount: reward2, + }); + }); + + // tricky case 3a + it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit: 1 blocks, note: pool1 is NOT updated + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(2)], false); //weight 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 3, + // because add was called without updating the pool, the new weight is used for all blocks + // so 2 blocks with weight 1/3 = 2 + const expectedRewardAmount = new BN("2"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 3b + it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit: 1 blocks, note: pool1 IS updated + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(2)], true); //weight 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 3, + // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block + // so 1 block with weight 1/1 = 3 and 1 block with weight 1/3 = 1 + const expectedRewardAmount = new BN("4"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 4 + it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], true); //weight 1/2 + + // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account2 }); + + // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) + const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) + const withdrawTx2 = await liquidityMining.withdraw(token2.address, amount, ZERO_ADDRESS, { from: account2 }); + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 3 + // deposit 1 has 1 block with weight 1/1 (3) and 2 blocks with weight 1/2 + const expectedReward1 = new BN("6"); + + // deposit 2 has 2 blocks with weight 1/2 + const expectedReward2 = new BN("3"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + for (let account of [account1, account2]) { + for (let token of [token1, token2]) { + await checkUserPoolTokens( + account, + token, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + } + } + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token2.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + rewardToken: SOVToken.address, + amount: reward2, + }); + }); + }); + + describe("LM configuration", () => { + //Maximum reward per week: 100K SOV (or 100M SOV) + //Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) + + const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); + // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); + + //SOV/BTC pool 40K per week + //ETH/BTC pool 37.5K per week (from second week) + //Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) + + const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); + // const MAX_ALLOCATION_POINT = new BN(100000); + const ALLOCATION_POINT_SOV_BTC = new BN(40000); + const ALLOCATION_POINT_ETH_BTC = new BN(37500); + + const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); + + const amount = new BN(1000); + + beforeEach(async () => { + await deployLiquidityMining(); + await liquidityMiningV1.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMiningV1.initialize(liquidityMining.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); + + await liquidityMining.initialize(wrapper.address, migrator.address, SOVToken.address); + + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMining.addRewardToken(SOVToken.address, REWARD_TOKENS_PER_BLOCK, startDelayBlocks, rewardTransferLogic.address); + + //mint SOVs to lvm1 for migrations + await SOVToken.mint(liquidityMiningV1.address, new BN(10)); + await liquidityMiningV1.addAdmin(migrator.address); + await liquidityMiningV1.startMigrationGracePeriod(); + await liquidityMining.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + //burn SOVs for testing + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await SOVToken.burn(liquidityMining.address, balanceSOV); + }); + + it("dummy pool + 1 pool", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + + await liquidityMining.add(SOVBTCpool, [SOVToken.address], [ALLOCATION_POINT_SOV_BTC], false); //weight 40000 / 100000 + await liquidityMining.add(dummyPool, [SOVToken.address], [MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC)], false); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks passed since last deposit + await mineBlocks(9); + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks passed + let passedBlocks = 10; + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(expectedUserReward); + }); + + it("dummy pool + 2 pools", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + let ETHBTCpoll = token2.address; + + await liquidityMining.add(SOVBTCpool, [SOVToken.address], [ALLOCATION_POINT_SOV_BTC], false); //weight 40000 / 100000 + const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); + await liquidityMining.add(dummyPool, [SOVToken.address], [DUMMY_ALLOCATION_POINT], false); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlocks(9); + await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit + + //update config + //this method will also update pool reward using previous allocation point, + //so this block should be add to calculation with old values + await liquidityMining.update(SOVBTCpool, [SOVToken.address], [ALLOCATION_POINT_SOV_BTC_2], false); //weight 30000 / 100000 + + await liquidityMining.add(ETHBTCpoll, [SOVToken.address], [ALLOCATION_POINT_ETH_BTC], false); //weight 37500 / 100000 + const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC_2).sub(ALLOCATION_POINT_ETH_BTC); + await liquidityMining.update(dummyPool, [SOVToken.address], [DUMMY_ALLOCATION_POINT_2], false); //weight (100000 - 30000 - 37500) / 100000 + await liquidityMining.updateAllPools(); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks + 5 blocks passed + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks + 5 blocks passed + let passedBlocks = 10 + 1; //block should be add to calculation with old values + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + passedBlocks = 5 - 1; //block should be removed from calculation with new values + expectedUserReward = expectedUserReward.add( + REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)).mul(ALLOCATION_POINT_SOV_BTC_2).div(MAX_ALLOCATION_POINT) + ); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(expectedUserReward); + }); + }); + + describe("multiple rewards tokens per pool", () => { + let rewardToken1; + let rewardToken2; + let transferLogic1; + let transferLogic2; + const otherRewardTokensPerBlock = new BN(6); + const allocationPoint = new BN(1); + + beforeEach(async () => { + // add other reward token + rewardToken1 = await TestToken.new("Reward token 1", "RWT-1", 18, TOTAL_SUPPLY); + rewardToken2 = await TestToken.new("Reward token 2", "RWT-2", 18, TOTAL_SUPPLY); + + transferLogic1 = await ERC20TransferLogic.new(); + transferLogic2 = await ERC20TransferLogic.new(); + await transferLogic1.initialize(rewardToken1.address); + await transferLogic2.initialize(rewardToken2.address); + + await liquidityMining.addRewardToken(rewardToken1.address, rewardTokensPerBlock, startDelayBlocks, transferLogic1.address); + await liquidityMining.addRewardToken(rewardToken2.address, otherRewardTokensPerBlock, startDelayBlocks, transferLogic2.address); + + await rewardToken1.transfer(liquidityMining.address, new BN(1000)); + await rewardToken2.transfer(liquidityMining.address, new BN(1000)); + }); + + it("add 2 reward tokens to one pool", async () => { + await liquidityMining.add( + token1.address, + [rewardToken1.address, rewardToken2.address], + [allocationPoint, allocationPoint], + false + ); + + const poolRewards = await liquidityMining.getPoolRewards(token1.address); + expect(poolRewards).to.be.an("array"); + expect(poolRewards).to.have.length(2); + expect(poolRewards[0].allocationPoint).bignumber.equal(allocationPoint); + expect(poolRewards[1].allocationPoint).bignumber.equal(allocationPoint); + }); + + it("update 2 reward tokens with new allocation points", async () => { + await liquidityMining.add( + token1.address, + [rewardToken1.address, rewardToken2.address], + [allocationPoint, allocationPoint], + false + ); + + let poolRewards = await liquidityMining.getPoolRewards(token1.address); + expect(poolRewards).to.be.an("array"); + expect(poolRewards).to.have.length(2); + expect(poolRewards[0].allocationPoint).bignumber.equal(allocationPoint); + expect(poolRewards[1].allocationPoint).bignumber.equal(allocationPoint); + + const newAllocationPoints = [new BN(3), new BN(4)]; + + await liquidityMining.update(token1.address, [rewardToken1.address, rewardToken2.address], newAllocationPoints, false); + + poolRewards = await liquidityMining.getPoolRewards(token1.address); + expect(poolRewards).to.be.an("array"); + expect(poolRewards).to.have.length(2); + expect(poolRewards[0].allocationPoint).bignumber.equal(newAllocationPoints[0]); + expect(poolRewards[1].allocationPoint).bignumber.equal(newAllocationPoints[1]); + }); + + it("check rewards for two reward tokens and one pool", async () => { + const amount = new BN(1000); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await liquidityMining.add( + token1.address, + [rewardToken1.address, rewardToken2.address], + [allocationPoint, allocationPoint], + false + ); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, rewardToken1.address, account1); + expect(reward1).bignumber.equal(rewardTokensPerBlock); + + const claimRewardTx1 = await liquidityMining.claimReward(token1.address, rewardToken1.address, ZERO_ADDRESS, { + from: account1, + }); + expectEvent(claimRewardTx1, "RewardClaimed", { + user: account1, + amount: new BN(6), + rewardToken: rewardToken1.address, + }); + + const reward2 = await liquidityMining.getUserAccumulatedReward(token1.address, rewardToken2.address, account1); + expect(reward2).bignumber.equal(new BN(12)); + + const claimRewardTx2 = await liquidityMining.claimReward(token1.address, rewardToken2.address, ZERO_ADDRESS, { + from: account1, + }); + expectEvent(claimRewardTx2, "RewardClaimed", { + user: account1, + amount: new BN(18), + rewardToken: rewardToken2.address, + }); + }); + }); + + describe("onTokensDeposited", () => { + it("a pool should be able to deposit for a user", async () => { + const poolToken = await TestPoolToken.new("Test Pool Token", "TPT", 18, TOTAL_SUPPLY, liquidityMining.address); + + await liquidityMining.add(poolToken.address, [SOVToken.address], [new BN(1)], false); + const tx = await poolToken.depositFor(account1, new BN(1000)); + + const userInfo = await liquidityMining.getUserInfo(poolToken.address, account1); + expect(userInfo.amount).bignumber.equal(new BN(1000)); + }); + it("should revert if the sender is not a valid pool token", async () => { + await expectRevert(liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), "Pool token not found"); + }); + }); + + describe("external getters", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); + }); + + it("PRECISION", async () => { + expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); + }); + + it("rewardTokensPerBlock", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); + }); + + it("startBlock", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.startBlock).bignumber.gt("0"); + }); + + it("endBlock", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.endBlock).bignumber.equal("0"); + }); + + it("wrapper", async () => { + expect(await liquidityMining.wrapper()).equal(wrapper.address); + }); + + it("totalAllocationPoint", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint); + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint.mul(new BN(2))); + }); + + it("totalUsersBalance", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal("3"); + }); + + // could still test these, but I don't see much point: + // PoolInfo[] public poolInfoList; + // mapping(address => uint256) poolIdList; + // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + + it("getMissedBalance", async () => { + let missedBalance = await liquidityMining.getMissedBalance(SOVToken.address); + expect(missedBalance).bignumber.equal("0"); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await liquidityMining.updatePool(token1.address); + + missedBalance = await liquidityMining.getMissedBalance(SOVToken.address); + expect(missedBalance).bignumber.equal("3"); + }); + + it("getUserAccumulatedReward", async () => { + // real tests are elsewhere in this file + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); + const reward2 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account2); + expect(reward1).bignumber.equal("3"); + expect(reward2).bignumber.equal("0"); + }); + + it("getPoolId", async () => { + const poolId = await liquidityMining.getPoolId(token1.address); + expect(poolId).bignumber.equal("0"); + await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); + const poolId2 = await liquidityMining.getPoolId(token2.address); + expect(poolId2).bignumber.equal("1"); + }); + + it("getPoolLength", async () => { + let length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("1"); + + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); + length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("2"); + }); + + it("getPoolInfoList", async () => { + const infoList = await liquidityMining.getPoolInfoList(); + expect(infoList).to.be.an("array"); + expect(infoList.length).equal(1); + const info = infoList[0]; + expect(info.poolToken).equal(token1.address); + expect(info.rewardTokens[0]).equal(SOVToken.address); + + const poolRewardInfo = await liquidityMining.getPoolReward(token1.address, info.rewardTokens[0]); + expect(poolRewardInfo.allocationPoint).equal(allocationPoint.toString()); + expect(poolRewardInfo.accumulatedRewardPerShare).equal("0"); + expect(poolRewardInfo.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + }); + + it("getPoolInfo", async () => { + const info = await liquidityMining.getPoolInfo(token1.address); + expect(info.poolToken).equal(token1.address); + + const poolRewardInfo = await liquidityMining.getPoolReward(token1.address, info.rewardTokens[0]); + expect(poolRewardInfo.allocationPoint).equal(allocationPoint.toString()); + expect(poolRewardInfo.accumulatedRewardPerShare).equal("0"); + expect(poolRewardInfo.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + + await expectRevert(liquidityMining.getPoolInfo(token2.address), "Pool token not found"); + }); + + it("getUserBalanceList", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + await mineBlock(); + const balanceList = await liquidityMining.getUserBalanceList(account1); + + expect(balanceList).to.be.an("array"); + expect(balanceList.length).equal(1); + const balanceData = balanceList[0]; + expect(balanceData).to.be.an("array"); + expect(balanceData[0].amount).bignumber.equal(amount); + expect(balanceData[0].accumulatedReward).bignumber.equal("3"); + }); + + it("getUserInfo", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? + expect(userInfo.rewards[0].rewardDebt).bignumber.equal("0"); // not yet updated either + + // deposit updates it. + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); + userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("501"); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("3"); + expect(userInfo.rewards[0].rewardDebt).bignumber.equal("3"); + }); + + it("getUserInfoList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + + let userInfoList = await liquidityMining.getUserInfoList(account1); + expect(userInfoList).to.be.an("array"); + expect(userInfoList.length).equal(1); + const userInfo = userInfoList[0]; + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("0"); + expect(userInfo.rewards[0].rewardDebt).bignumber.equal("0"); + }); + + it("getUserAccumulatedRewardList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + + let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); + expect(rewardList).to.be.an("array"); + expect(rewardList.length).equal(1); + expect(rewardList[0]).to.be.an("array"); + expect(rewardList[0].length).equal(1); + expect(rewardList[0][0].accumulatedReward).bignumber.equal("0"); + }); + + it("getUserPoolTokenBalance", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); + let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance(token1.address, account1); + expect(poolTokenBalance).bignumber.equal(new BN(500)); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMiningV1.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMining = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function mineBlocks(blocks) { + for (let i = 0; i < blocks; i++) { + await mineBlock(); + } + } + + function checkPoolRewardInfo(poolInfo, token, rewardToken, allocationPoint, lastRewardBlock, accumulatedRewardPerShare) { + expect(poolInfo.poolToken).equal(token); + expect(rewardToken.allocationPoint).bignumber.equal(allocationPoint); + expect(rewardToken.lastRewardBlock).bignumber.equal(lastRewardBlock); + if (accumulatedRewardPerShare.toNumber() !== -1) { + expect(rewardToken.accumulatedRewardPerShare).bignumber.equal(accumulatedRewardPerShare); + } + } + + async function checkUserPoolTokens(user, poolToken, _userAmount, _liquidityMiningBalance, _userBalance, wrapper) { + //user balance in pool + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.amount).bignumber.equal(_userAmount); + //LM balance of pool tokens + let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); + expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); + //user's balance of pool tokens + let userBalance = await poolToken.balanceOf(user); + if (wrapper !== undefined) { + userBalance = await poolToken.balanceOf(wrapper); + } + expect(userBalance).bignumber.equal(_userBalance); + } + + //user's balance of reward token + async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { + let passedBlocks = latestBlockNumber.sub(depositBlockNumber); + let userReward = passedBlocks.mul(rewardTokensPerBlock); + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(new BN(0)); + return userReward; + } +}); diff --git a/tests/farm/LockedSOVRewardTransferLogic.js b/tests/farm/LockedSOVRewardTransferLogic.js new file mode 100644 index 000000000..eb011192a --- /dev/null +++ b/tests/farm/LockedSOVRewardTransferLogic.js @@ -0,0 +1,224 @@ +const { expect } = require("chai"); +const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); +const { etherMantissa, mineBlock, advanceBlocks } = require("../Utils/Ethereum"); + +const { ZERO_ADDRESS } = constants; +const TOTAL_SUPPLY = etherMantissa(1000000000); + +const TestToken = artifacts.require("TestToken"); +const TestLockedSOV = artifacts.require("LockedSOVMockup"); +const LockedSOVRewardTransferLogic = artifacts.require("LockedSOVRewardTransferLogic"); + +describe("LockedSOVRewardTransferLogic", () => { + const name = "Test SOV Token"; + const symbol = "TST"; + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let root, account1, account2, account3, account4; + let SOVToken, token1, token2, token3; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + }); + + describe("initialize", () => { + it("fails if not an owner or admin", async () => { + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await expectRevert( + rewardTransferLogic.initialize(SOVToken.address, unlockedImmediatelyPercent, { from: account1 }), + "unauthorized" + ); + + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.initialize(SOVToken.address, unlockedImmediatelyPercent, { from: account1 }); + }); + + it("sets the expected values", async () => { + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + let _lockedSOVAddress = await rewardTransferLogic.lockedSOV(); + let _unlockedImmediatelyPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); + expect(_lockedSOVAddress).equal(lockedSOV.address); + expect(_unlockedImmediatelyPercent).bignumber.equal(unlockedImmediatelyPercent); + }); + }); + + describe("changeLockedSOV", () => { + it("fails if not an owner or admin", async () => { + await expectRevert(rewardTransferLogic.changeLockedSOV(SOVToken.address, { from: account1 }), "unauthorized"); + + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.changeLockedSOV(SOVToken.address, { from: account1 }); + }); + + it("fails if invalid address", async () => { + await rewardTransferLogic.addAdmin(account1); + await expectRevert(rewardTransferLogic.changeLockedSOV(ZERO_ADDRESS, { from: account1 }), "Invalid address"); + }); + + it("should set a new LockedSOV", async () => { + //first check original lockedSOV address + let lockedSOVAddress = await rewardTransferLogic.lockedSOV(); + expect(lockedSOV.address).equal(lockedSOVAddress); + + newLockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + await rewardTransferLogic.addAdmin(account1); + tx = await rewardTransferLogic.changeLockedSOV(newLockedSOV.address, { from: account1 }); + + //then check new lockedSOV address + let newLockedSOVAddress = await rewardTransferLogic.lockedSOV(); + expect(newLockedSOV.address).equal(newLockedSOVAddress); + + expectEvent(tx, "LockedSOVChanged", { + _newAddress: newLockedSOVAddress, + }); + }); + }); + + describe("changeUnlockedImmediatelyPercent", async () => { + it("fails if not an owner or admin", async () => { + await expectRevert( + rewardTransferLogic.changeUnlockedImmediatelyPercent(unlockedImmediatelyPercent, { from: account1 }), + "unauthorized" + ); + + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.changeUnlockedImmediatelyPercent(unlockedImmediatelyPercent, { from: account1 }); + }); + + it("fails if invalid unlocked percent", async () => { + await rewardTransferLogic.addAdmin(account1); + await expectRevert( + rewardTransferLogic.changeUnlockedImmediatelyPercent(new BN(10000), { from: account1 }), + "Unlocked immediately percent has to be less than 10000." + ); + }); + + it("should set a new unlocked percent", async () => { + //first check origin unlocked percent + let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); + expect(unlockedPercent).bignumber.equal(unlockedImmediatelyPercent); + + const newUnlockedPercent = new BN(10); + await rewardTransferLogic.addAdmin(account1); + tx = await rewardTransferLogic.changeUnlockedImmediatelyPercent(newUnlockedPercent, { from: account1 }); + + //then check new unlocked percent + let newUnlockedPercentAmount = await rewardTransferLogic.unlockedImmediatelyPercent(); + expect(newUnlockedPercentAmount).bignumber.equal(newUnlockedPercent); + + expectEvent(tx, "UnlockImmediatelyPercentChanged", { + _newAmount: newUnlockedPercentAmount, + }); + }); + }); + + describe("getRewardTokenAddress", async () => { + it("should return SOVToken address", async () => { + let SOVTokenAddress = await rewardTransferLogic.getRewardTokenAddress(); + expect(SOVTokenAddress).equal(SOVToken.address); + }); + + it("should change lockedSOV and return new token address", async () => { + let newLockedSOV = await TestLockedSOV.new(token1.address, lockedSOVAdmins); + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.changeLockedSOV(newLockedSOV.address, { from: account1 }); + + let token1Address = await rewardTransferLogic.getRewardTokenAddress(); + expect(token1Address).equal(token1.address); + }); + }); + + describe("senderToAuthorize", async () => { + it("should return contract address", async () => { + let rewardTransferLogicAddress = await rewardTransferLogic.senderToAuthorize(); + expect(rewardTransferLogicAddress).equal(rewardTransferLogic.address); + }); + }); + + describe("transferReward", async () => { + const account1InitialBalance = new BN(100); + const amountToTransfer = new BN(50); + + it("fails if account doesn't have reward tokens", async () => { + await expectRevert(rewardTransferLogic.transferReward(account2, new BN(5), false, { from: account1 }), "invalid transfer"); + }); + + it("fails if account didn't approve before", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, new BN(10)); + await expectRevert(rewardTransferLogic.transferReward(account2, new BN(5), false, { from: account1 }), "invalid transfer"); + }); + + it("fails if invalid address to transfer", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); + await expectRevert(rewardTransferLogic.transferReward(ZERO_ADDRESS, new BN(5), false, { from: account1 }), "invalid transfer"); + }); + + it("should account1 transfer reward to account2 without withdraw", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); + + await rewardTransferLogic.transferReward(account2, amountToTransfer, false, { from: account1 }); + let account1FinalBalance = await SOVToken.balanceOf(account1); + expect(account1FinalBalance).bignumber.equal(account1InitialBalance.sub(amountToTransfer)); + }); + + it("should account2 receive unlocked balance after transfer without withdraw", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); + + await rewardTransferLogic.transferReward(account2, amountToTransfer, false, { from: account1 }); + let lockedBalance = await lockedSOV.getLockedBalance(account2); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account2); + + let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); + let balancePercent = amountToTransfer.mul(unlockedPercent).div(new BN(10000)); + let balanceAccount2 = await SOVToken.balanceOf(account2); + + expect(balanceAccount2).bignumber.equal(balancePercent); + expect(lockedBalance).bignumber.equal(new BN(0)); + expect(unlockedBalance).bignumber.equal(new BN(0)); + }); + + it("should should account2 have locked and unlocked balance after transfer with withdraw", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); + + await rewardTransferLogic.transferReward(account2, amountToTransfer, true, { from: account1 }); + let lockedBalance = await lockedSOV.getLockedBalance(account2); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account2); + + let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); + let balancePercent = amountToTransfer.mul(unlockedPercent).div(new BN(10000)); + + expect(lockedBalance).bignumber.equal(amountToTransfer.sub(balancePercent)); + expect(unlockedBalance).bignumber.equal(balancePercent); + }); + }); +}); diff --git a/tests/loan-token/LendingWithLMV2.test.js b/tests/loan-token/LendingWithLMV2.test.js new file mode 100644 index 000000000..06e9b1a66 --- /dev/null +++ b/tests/loan-token/LendingWithLMV2.test.js @@ -0,0 +1,272 @@ +const { expect, assert } = require("chai"); +const { expectRevert, expectEvent, BN } = require("@openzeppelin/test-helpers"); +const { increaseTime, etherMantissa, mineBlock, advanceBlocks } = require("../Utils/Ethereum"); + +const TestToken = artifacts.require("TestToken"); +const TestWrbtc = artifacts.require("TestWrbtc"); + +const sovrynProtocol = artifacts.require("sovrynProtocol"); +const ProtocolSettings = artifacts.require("ProtocolSettings"); +const ISovryn = artifacts.require("ISovryn"); + +const LoanToken = artifacts.require("LoanToken"); +const LoanTokenLogicLM = artifacts.require("LoanTokenLogicLM"); +const LoanTokenLogicWRBTC = artifacts.require("LoanTokenLogicWrbtc"); +const LoanSettings = artifacts.require("LoanSettings"); +const LoanMaintenance = artifacts.require("LoanMaintenance"); +const LoanOpenings = artifacts.require("LoanOpenings"); +const LoanClosingsBase = artifacts.require("LoanClosingsBase"); +const LoanClosingsWith = artifacts.require("LoanClosingsWith"); +const SwapsExternal = artifacts.require("SwapsExternal"); + +const PriceFeedsLocal = artifacts.require("PriceFeedsLocal"); +const TestSovrynSwap = artifacts.require("TestSovrynSwap"); +const SwapsImplLocal = artifacts.require("SwapsImplLocal"); + +const LiquidityMiningLogic = artifacts.require("LiquidityMiningMockupV2"); +const LiquidityMiningProxy = artifacts.require("LiquidityMiningProxyV2"); +const LockedSOV = artifacts.require("LockedSOV"); +const LockedSOVRewardTransferLogic = artifacts.require("LockedSOVRewardTransferLogic"); + +const TOTAL_SUPPLY = web3.utils.toWei("1000", "ether"); + +//const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_more_of_lender_balance_should_not_fail } = require("./helpers"); +const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_uint256_max_should_withdraw_total_balance } = require("./helpers"); +const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); + +const wei = web3.utils.toWei; + +contract("LoanTokenLogicLM", (accounts) => { + const name = "Test token"; + const symbol = "TST"; + const depositAmount = new BN(wei("400", "ether")); + + let lender, account1, account2, account3, account4; + let underlyingToken, testWrbtc; + let sovryn, loanToken, loanTokenWRBTC; + let liquidityMining; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + + before(async () => { + [lender, account1, account2, account3, account4, ...accounts] = accounts; + await deployProtocol(); + await deployLoanTokens(); + await deployLiquidityMining(); + + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await liquidityMining.addRewardToken(SOVToken.address, new BN(3), new BN(1), rewardTransferLogic.address); + await liquidityMining.add(loanToken.address, [SOVToken.address], [new BN(10)], false); + await liquidityMining.add(loanTokenWRBTC.address, [SOVToken.address], [new BN(10)], true); + }); + + describe("Test lending with liquidity mining", () => { + it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { + //await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); + await underlyingToken.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, true); + const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + //expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + await underlyingToken.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, false); + const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + let userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + const tx = await loanToken.burn(lender, userInfo.amount, true); + userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + await loanToken.burn(lender, depositAmount, false); + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal("0"); + }); + }); + + describe("Test WRBTC lending with liquidity mining", () => { + it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { + //await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); + const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + //expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); + const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + let userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); + userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + await loanTokenWRBTC.burnToBTC(lender, depositAmount, false); + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); + }); + }); + + describe("Test setting the liquidity mining address", () => { + it("Should be able to set the liquidity mining address", async () => { + await loanToken.setLiquidityMiningAddress(account2); + expect(await loanToken.liquidityMiningAddress()).to.be.equal(account2); + }); + + it("Should fail to set the liquidity mining address with an unauthorized wallet", async () => { + await expectRevert(loanToken.setLiquidityMiningAddress(account2, { from: account1 }), "unauthorized"); + }); + }); + + async function deployLiquidityMining() { + SOVToken = await TestToken.new("SOV", "SOV", 18, etherMantissa(1000000000)); + lockedSOVAdmins = [lender, account1, account2]; + //account 1 is a dummy value for the vesting registry + lockedSOV = await LockedSOV.new(SOVToken.address, account1, 1, 10, lockedSOVAdmins); + + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + let liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(1000)); + //dummy settings + await liquidityMining.initialize(ZERO_ADDRESS, liquidityMiningLogic.address, SOVToken.address); + } + + async function deployProtocol() { + //Token + underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + testWrbtc = await TestWrbtc.new(); + + const sovrynproxy = await sovrynProtocol.new(); + sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await LoanClosingsBase.new()).address); + await sovryn.replaceContract((await LoanClosingsWith.new()).address); + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + await sovryn.replaceContract((await LoanOpenings.new()).address); + + await sovryn.setWrbtcToken(testWrbtc.address); + + feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); + await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); + const swaps = await SwapsImplLocal.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([underlyingToken.address, testWrbtc.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address //priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + } + + async function deployLoanTokens() { + loanTokenLogicLM = await LoanTokenLogicLM.new(); + loanToken = await LoanToken.new(lender, loanTokenLogicLM.address, sovryn.address, testWrbtc.address); + await loanToken.initialize(underlyingToken.address, name, symbol); //iToken + loanToken = await LoanTokenLogicLM.at(loanToken.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + underlyingToken.address, // address loanToken; // the token being loaned + testWrbtc.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + // --------------- WRBTC -----------------------// + + loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); + loanTokenWRBTC = await LoanToken.new(lender, loanTokenLogicWRBTC.address, sovryn.address, testWrbtc.address); + await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); + loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + testWrbtc.address, // address loanToken; // the token being loaned + underlyingToken.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanTokenWRBTC.setupLoanParams([params], false); + await sovryn.setLoanPool([loanTokenWRBTC.address], [testWrbtc.address]); + + // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// + await testWrbtc.mint(sovryn.address, wei("500", "ether")); + await underlyingToken.mint(sovryn.address, wei("50000", "ether")); + } +}); From 5018e83e16b6f0a0eb816e7e8e665dfe8c1907b8 Mon Sep 17 00:00:00 2001 From: py-ro Date: Mon, 20 Sep 2021 16:49:52 -0300 Subject: [PATCH 02/17] fix:solhint fixes --- contracts/farm/ERC20TransferLogic.sol | 2 +- contracts/farm/LMV1toLMV2Migrator.sol | 8 ++++---- contracts/farm/LiquidityMiningV2.sol | 16 ++++++++-------- contracts/farm/LockedSOVRewardTransferLogic.sol | 2 +- contracts/mockup/LiquidityMiningV2Mockup.sol | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/farm/ERC20TransferLogic.sol b/contracts/farm/ERC20TransferLogic.sol index 79141c027..3a9f2e707 100644 --- a/contracts/farm/ERC20TransferLogic.sol +++ b/contracts/farm/ERC20TransferLogic.sol @@ -13,7 +13,7 @@ contract ERC20TransferLogic is IRewardTransferLogic, ERC20TransferLogicStorage { /** * @param _token Reward token to be distributed */ - function initialize(address _token) public onlyAuthorized { + function initialize(address _token) external onlyAuthorized { setTokenAddress(_token); } diff --git a/contracts/farm/LMV1toLMV2Migrator.sol b/contracts/farm/LMV1toLMV2Migrator.sol index edf69c0cd..6c3e2ab48 100644 --- a/contracts/farm/LMV1toLMV2Migrator.sol +++ b/contracts/farm/LMV1toLMV2Migrator.sol @@ -57,7 +57,7 @@ contract LMV1toLMV2Migrator is AdminRole { IERC20 _SOV, ILiquidityMiningV1 _liquidityMiningV1, ILiquidityMiningV2 _liquidityMiningV2 - ) public onlyAuthorized { + ) external onlyAuthorized { require(address(_SOV) != address(0), "invalid token address"); require(address(_liquidityMiningV1) != address(0), "invalid contract address"); require(address(_liquidityMiningV2) != address(0), "invalid contract address"); @@ -93,6 +93,7 @@ contract LMV1toLMV2Migrator is AdminRole { require(_poolToken.length == _allocationPoints.length, "Arrays mismatch"); require(_poolToken.length == _lastRewardBlock.length, "Arrays mismatch"); + _finishPoolsMigration(); liquidityMiningV1.finishMigrationGracePeriod(); for (uint256 i = 0; i < _poolToken.length; i++) { address poolToken = _poolToken[i]; @@ -110,7 +111,6 @@ contract LMV1toLMV2Migrator is AdminRole { uint256 _startblock = liquidityMiningV1.getStartBlock(); uint256 _totalUsersBalance = liquidityMiningV1.getTotalUsersBalance(); liquidityMiningV2.setRewardToken(address(SOV), _startblock, _totalUsersBalance); - _finishPoolsMigration(); } /** @@ -128,7 +128,7 @@ contract LMV1toLMV2Migrator is AdminRole { address user = _users[i]; - if (userMigrated[user] == false) { + if (!userMigrated[user]) { userMigrated[user] = true; for (uint256 j = 0; j < _amount.length; j++) { uint256 poolId = j; @@ -146,8 +146,8 @@ contract LMV1toLMV2Migrator is AdminRole { * @notice transfer all funds from liquidity mining V1 */ function migrateFunds() external onlyAuthorized onlyFundsMigrationState { + _finishFundsMigration(); liquidityMiningV1.migrateFunds(); liquidityMiningV2.finishMigration(); - _finishFundsMigration(); } } diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index 0b9a4b5ef..f89e3d08e 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -137,11 +137,11 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { _amount = balance; } - /// @dev The actual transfer. - require(rewardToken.transfer(_receiver, _amount), "Transfer failed"); - /// @dev Event log. emit RewardTransferred(_rewardToken, _receiver, _amount); + + /// @dev The actual transfer. + require(rewardToken.transfer(_receiver, _amount), "Transfer failed"); } /** @@ -230,10 +230,10 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { */ function update( address _poolToken, - address[] memory _rewardTokens, - uint96[] memory _allocationPoints, + address[] calldata _rewardTokens, + uint96[] calldata _allocationPoints, bool _updateAllFlag - ) public onlyAuthorized { + ) external onlyAuthorized { if (_updateAllFlag) { updateAllPools(); } else { @@ -790,7 +790,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { * @param _poolToken the address of pool token * @param _rewardToken the address of reward token */ - function getPoolReward(address _poolToken, address _rewardToken) public view returns (PoolInfoRewardToken memory) { + function getPoolReward(address _poolToken, address _rewardToken) external view returns (PoolInfoRewardToken memory) { uint256 poolId = _getPoolId(_poolToken); return poolInfoRewardTokensMap[poolId][_rewardToken]; } @@ -869,7 +869,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { * @param _poolToken the address of pool token * @param _user the address of the user */ - function getUserInfo(address _poolToken, address _user) public view returns (PoolUserInfo memory) { + function getUserInfo(address _poolToken, address _user) external view returns (PoolUserInfo memory) { uint256 poolId = _getPoolId(_poolToken); return _getPoolUserInfo(poolId, _user); } diff --git a/contracts/farm/LockedSOVRewardTransferLogic.sol b/contracts/farm/LockedSOVRewardTransferLogic.sol index 0ef97ecc7..041f485fd 100644 --- a/contracts/farm/LockedSOVRewardTransferLogic.sol +++ b/contracts/farm/LockedSOVRewardTransferLogic.sol @@ -19,7 +19,7 @@ contract LockedSOVRewardTransferLogic is IRewardTransferLogic, LockedSOVRewardTr * are deposited into a lockedSOV vault contract. * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. */ - function initialize(address _lockedSOV, uint256 _unlockedImmediatelyPercent) public onlyAuthorized { + function initialize(address _lockedSOV, uint256 _unlockedImmediatelyPercent) external onlyAuthorized { changeLockedSOV(_lockedSOV); changeUnlockedImmediatelyPercent(_unlockedImmediatelyPercent); } diff --git a/contracts/mockup/LiquidityMiningV2Mockup.sol b/contracts/mockup/LiquidityMiningV2Mockup.sol index 69b5dfc96..96a406a36 100644 --- a/contracts/mockup/LiquidityMiningV2Mockup.sol +++ b/contracts/mockup/LiquidityMiningV2Mockup.sol @@ -4,7 +4,7 @@ pragma experimental ABIEncoderV2; import "../farm/LiquidityMiningV2.sol"; contract LiquidityMiningMockupV2 is LiquidityMiningV2 { - function getPoolAccumulatedReward(address _poolToken, address _rewardToken) public view returns (uint256, uint256) { + function getPoolAccumulatedReward(address _poolToken, address _rewardToken) external view returns (uint256, uint256) { uint256 poolId = _getPoolId(_poolToken); PoolInfo storage pool = poolInfoList[poolId]; PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; From c1c46e8bb2f76af137a7952b9a85ebfdb0659f28 Mon Sep 17 00:00:00 2001 From: py-ro Date: Wed, 29 Sep 2021 11:14:53 -0300 Subject: [PATCH 03/17] feat:allow lending with LM V2 during migration period --- contracts/farm/LiquidityMiningV2.sol | 43 ++- tests/farm/LiquidityMiningMigration.js | 427 ++++++++++++++++++++++- tests/loan-token/LendingWithLMV2.test.js | 104 +++++- 3 files changed, 537 insertions(+), 37 deletions(-) diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index f89e3d08e..118982649 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -44,6 +44,17 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 accumulatedReward ); + /* Modifiers */ + modifier onlyMigrator() { + require(msg.sender == migrator, "only allowed to migrator contract"); + _; + } + + modifier onlyAfterMigrationFinished() { + require(migrationFinished, "Migration is not over yet"); + _; + } + /* Functions */ /** @@ -139,7 +150,6 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { /// @dev Event log. emit RewardTransferred(_rewardToken, _receiver, _amount); - /// @dev The actual transfer. require(rewardToken.transfer(_receiver, _amount), "Transfer failed"); } @@ -490,8 +500,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _poolToken, uint256 _amount, address _user - ) external { - require(migrationFinished, "Migration is not over yet"); + ) external onlyAfterMigrationFinished { _deposit(_poolToken, _amount, _user, false); } @@ -552,7 +561,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { * @param _poolToken the address of pool token * @param _user the address of user to claim reward from (can be passed only by wrapper contract) */ - function claimRewards(address _poolToken, address _user) external { + function claimRewards(address _poolToken, address _user) external onlyAfterMigrationFinished { address userAddress = _getUserAddress(_user); uint256 poolId = _getPoolId(_poolToken); @@ -574,7 +583,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _poolToken, address _rewardToken, address _user - ) external { + ) external onlyAfterMigrationFinished { address userAddress = _getUserAddress(_user); uint256 poolId = _getPoolId(_poolToken); @@ -599,7 +608,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { * @notice transfers reward tokens from all pools * @param _user the address of user to claim reward from (can be passed only by wrapper contract) */ - function claimRewardFromAllPools(address _user) external { + function claimRewardFromAllPools(address _user) external onlyAfterMigrationFinished { address userAddress = _getUserAddress(_user); uint256 length = poolInfoList.length; @@ -624,7 +633,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _poolToken, uint256 _amount, address _user - ) external { + ) external onlyAfterMigrationFinished { require(poolIdList[_poolToken] != 0, "Pool token not found"); address userAddress = _getUserAddress(_user); @@ -735,7 +744,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { * @param _poolToken the address of pool token * @dev EMERGENCY ONLY */ - function emergencyWithdraw(address _poolToken) external { + function emergencyWithdraw(address _poolToken) external onlyAfterMigrationFinished { uint256 poolId = _getPoolId(_poolToken); PoolInfo storage pool = poolInfoList[poolId]; UserInfo storage user = userInfoMap[poolId][msg.sender]; @@ -939,8 +948,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _rewardToken, uint256 _lastRewardBlock, uint256 _accumulatedRewardPerShare - ) external onlyAuthorized { - require(msg.sender == migrator, "only allowed to migrator contract"); + ) external onlyAuthorized onlyMigrator { uint256 poolId = _getPoolId(_poolToken); PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; @@ -951,8 +959,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _rewardToken, uint256 _startBlock, uint256 _totalUsersBalance - ) external onlyAuthorized { - require(msg.sender == migrator, "only allowed to migrator contract"); + ) external onlyAuthorized onlyMigrator { RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; rewardToken.startBlock = _startBlock; rewardToken.totalUsersBalance = _totalUsersBalance; @@ -965,18 +972,18 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _amount, uint256 _rewardDebt, uint256 _accumulatedReward - ) external onlyAuthorized { - require(msg.sender == migrator, "only allowed to migrator contract"); + ) external onlyAuthorized onlyMigrator { UserInfo storage userInfo = userInfoMap[_poolId][_user]; - userInfo.amount = _amount; - userInfo.rewards[_rewardToken] = UserReward(_rewardDebt, _accumulatedReward); + UserReward storage userReward = userInfo.rewards[_rewardToken]; + userInfo.amount += _amount; + userReward.rewardDebt += _rewardDebt; + userReward.accumulatedReward += _accumulatedReward; } /** * @notice finish migration */ - function finishMigration() external onlyAuthorized { - require(msg.sender == migrator, "only allowed to migrator contract"); + function finishMigration() external onlyAuthorized onlyMigrator { migrationFinished = true; } } diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index ebd3f8bfb..e33070d18 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -21,6 +21,27 @@ const ERC20TransferLogic = artifacts.require("ERC20TransferLogic"); const TestPoolToken = artifacts.require("TestPoolToken"); const Migrator = artifacts.require("LMV1toLMV2Migrator"); +const TestWrbtc = artifacts.require("TestWrbtc"); +const sovrynProtocol = artifacts.require("sovrynProtocol"); +const ProtocolSettings = artifacts.require("ProtocolSettings"); +const ISovryn = artifacts.require("ISovryn"); + +const LoanToken = artifacts.require("LoanToken"); +const LoanTokenLogicLM = artifacts.require("LoanTokenLogicLM"); +const LoanTokenLogicWRBTC = artifacts.require("LoanTokenLogicWrbtc"); +const LoanSettings = artifacts.require("LoanSettings"); +const LoanMaintenance = artifacts.require("LoanMaintenance"); +const LoanOpenings = artifacts.require("LoanOpenings"); +const LoanClosingsBase = artifacts.require("LoanClosingsBase"); +const LoanClosingsWith = artifacts.require("LoanClosingsWith"); +const SwapsExternal = artifacts.require("SwapsExternal"); + +const PriceFeedsLocal = artifacts.require("PriceFeedsLocal"); +const TestSovrynSwap = artifacts.require("TestSovrynSwap"); +const SwapsImplLocal = artifacts.require("SwapsImplLocal"); + +const wei = web3.utils.toWei; + describe("LiquidityMiningMigration", () => { const name = "Test SOV Token"; const symbol = "TST"; @@ -36,7 +57,8 @@ describe("LiquidityMiningMigration", () => { const unlockedImmediatelyPercent = new BN(1000); //10% let accounts; - let root, account1, account2, account3, account4, account5, account6, account7, account8, account9; + let sovryn, loanToken, loanTokenWRBTC; + let lender, account1, account2, account3, account4, account5, account6, account7, account8, account9; let SOVToken, token1, token2, token3, token4, token5, token6, token7, token8, liquidityMiningConfigToken; let liquidityMiningProxy, liquidityMining, liquidityMiningV2, migrator, wrapper; let rewardTransferLogic, lockedSOVAdmins, lockedSOV; @@ -52,7 +74,7 @@ describe("LiquidityMiningMigration", () => { before(async () => { accounts = await web3.eth.getAccounts(); - [root, account1, account2, account3, ...accounts] = accounts; + [lender, account1, account2, account3, ...accounts] = accounts; }); beforeEach(async () => { @@ -73,6 +95,9 @@ describe("LiquidityMiningMigration", () => { lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + await deployProtocol(); + await deployLoanTokens(); + await deployLiquidityMining(); await liquidityMining.initialize( SOVToken.address, @@ -745,6 +770,323 @@ describe("LiquidityMiningMigration", () => { }); }); + describe("Test lending with liquidity mining V2", async () => { + it("Should lend to the pool at the liquidity mining V1 and migrate it", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + const userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, false); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + const userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + }); + + it("Should only allow to burn tokens if migration finished", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + await expectRevert(loanToken.burn(lender, userInfo.amount, true), "Migration is not over yet"); + }); + + it("Should remove the pool tokens from the liquidity mining V2 pool and burn them after migration", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + + let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + + const tx = await loanToken.burn(lender, userInfo.amount, true); + userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal("0"); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount.mul(new BN(2))); + await loanToken.mint(lender, depositAmount, true); + await loanToken.mint(lender, depositAmount, false); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + + let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); + + await loanToken.burn(lender, depositAmount, false); + + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + }); + }); + + describe("Test WRBTC lending with liquidity mining", () => { + it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + //expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + }); + + it("Should only allow to burn tokens if migration finished", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + await expectRevert(loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true), "Migration is not over yet"); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); + userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + + let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); + + const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, false); + + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + }); + }); + async function deployLiquidityMining() { let liquidityMiningLogic = await LiquidityMiningLogic.new(); liquidityMiningProxy = await LiquidityMiningProxy.new(); @@ -985,4 +1327,85 @@ describe("LiquidityMiningMigration", () => { await mineBlock(); } } + + async function deployProtocol() { + //Token + underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + testWrbtc = await TestWrbtc.new(); + + const sovrynproxy = await sovrynProtocol.new(); + sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await LoanClosingsBase.new()).address); + await sovryn.replaceContract((await LoanClosingsWith.new()).address); + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + await sovryn.replaceContract((await LoanOpenings.new()).address); + + await sovryn.setWrbtcToken(testWrbtc.address); + + feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); + await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); + const swaps = await SwapsImplLocal.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([underlyingToken.address, testWrbtc.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address //priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + } + + async function deployLoanTokens() { + loanTokenLogicLM = await LoanTokenLogicLM.new(); + loanToken = await LoanToken.new(lender, loanTokenLogicLM.address, sovryn.address, testWrbtc.address); + await loanToken.initialize(underlyingToken.address, name, symbol); //iToken + loanToken = await LoanTokenLogicLM.at(loanToken.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + underlyingToken.address, // address loanToken; // the token being loaned + testWrbtc.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + // --------------- WRBTC -----------------------// + + loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); + loanTokenWRBTC = await LoanToken.new(lender, loanTokenLogicWRBTC.address, sovryn.address, testWrbtc.address); + await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); + loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + testWrbtc.address, // address loanToken; // the token being loaned + underlyingToken.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanTokenWRBTC.setupLoanParams([params], false); + await sovryn.setLoanPool([loanTokenWRBTC.address], [testWrbtc.address]); + + // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// + await testWrbtc.mint(sovryn.address, wei("500", "ether")); + await underlyingToken.mint(sovryn.address, wei("50000", "ether")); + } }); diff --git a/tests/loan-token/LendingWithLMV2.test.js b/tests/loan-token/LendingWithLMV2.test.js index 06e9b1a66..afbaa38bd 100644 --- a/tests/loan-token/LendingWithLMV2.test.js +++ b/tests/loan-token/LendingWithLMV2.test.js @@ -4,6 +4,7 @@ const { increaseTime, etherMantissa, mineBlock, advanceBlocks } = require("../Ut const TestToken = artifacts.require("TestToken"); const TestWrbtc = artifacts.require("TestWrbtc"); +const LiquidityMiningConfigToken = artifacts.require("LiquidityMiningConfigToken"); const sovrynProtocol = artifacts.require("sovrynProtocol"); const ProtocolSettings = artifacts.require("ProtocolSettings"); @@ -23,10 +24,16 @@ const PriceFeedsLocal = artifacts.require("PriceFeedsLocal"); const TestSovrynSwap = artifacts.require("TestSovrynSwap"); const SwapsImplLocal = artifacts.require("SwapsImplLocal"); -const LiquidityMiningLogic = artifacts.require("LiquidityMiningMockupV2"); -const LiquidityMiningProxy = artifacts.require("LiquidityMiningProxyV2"); -const LockedSOV = artifacts.require("LockedSOV"); +const LiquidityMiningLogic = artifacts.require("LiquidityMiningMockup"); +const LiquidityMiningLogicV1 = artifacts.require("LiquidityMiningV1Mockup"); +const LiquidityMiningProxy = artifacts.require("LiquidityMiningProxy"); +const LiquidityMiningLogicV2 = artifacts.require("LiquidityMiningMockupV2"); +const LiquidityMiningProxyV2 = artifacts.require("LiquidityMiningProxyV2"); +const TestLockedSOV = artifacts.require("LockedSOVMockup"); +const Wrapper = artifacts.require("RBTCWrapperProxyMockupV2"); + const LockedSOVRewardTransferLogic = artifacts.require("LockedSOVRewardTransferLogic"); +const Migrator = artifacts.require("LMV1toLMV2Migrator"); const TOTAL_SUPPLY = web3.utils.toWei("1000", "ether"); @@ -41,22 +48,78 @@ contract("LoanTokenLogicLM", (accounts) => { const symbol = "TST"; const depositAmount = new BN(wei("400", "ether")); + const rewardTokensPerBlock = new BN(3); + const startDelayBlocks = new BN(1); + const numberOfBonusBlocks = new BN(50); + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + let lender, account1, account2, account3, account4; let underlyingToken, testWrbtc; + let SOVToken, token1, token2, token3, liquidityMiningConfigToken; let sovryn, loanToken, loanTokenWRBTC; - let liquidityMining; + let liquidityMiningV1, liquidityMining, migrator, wrapper; let rewardTransferLogic, lockedSOVAdmins, lockedSOV; before(async () => { [lender, account1, account2, account3, account4, ...accounts] = accounts; await deployProtocol(); await deployLoanTokens(); + + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + await deployLiquidityMining(); + await liquidityMiningV1.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMiningV1.initialize(liquidityMining.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); + + await liquidityMining.initialize(wrapper.address, migrator.address, SOVToken.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMining.setWrapper(wrapper.address); + await liquidityMining.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + //mint SOVs to lvm1 for migrations + await SOVToken.mint(liquidityMiningV1.address, new BN(10)); + await liquidityMiningV1.addAdmin(migrator.address); + await liquidityMiningV1.startMigrationGracePeriod(); + await liquidityMining.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + //burn SOVs for testing + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await SOVToken.burn(liquidityMining.address, balanceSOV); await loanToken.setLiquidityMiningAddress(liquidityMining.address); await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - await liquidityMining.addRewardToken(SOVToken.address, new BN(3), new BN(1), rewardTransferLogic.address); await liquidityMining.add(loanToken.address, [SOVToken.address], [new BN(10)], false); await liquidityMining.add(loanTokenWRBTC.address, [SOVToken.address], [new BN(10)], true); }); @@ -173,20 +236,27 @@ contract("LoanTokenLogicLM", (accounts) => { }); async function deployLiquidityMining() { - SOVToken = await TestToken.new("SOV", "SOV", 18, etherMantissa(1000000000)); - lockedSOVAdmins = [lender, account1, account2]; - //account 1 is a dummy value for the vesting registry - lockedSOV = await LockedSOV.new(SOVToken.address, account1, 1, 10, lockedSOVAdmins); + let liquidityMiningLogicV1 = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - let liquidityMiningLogic = await LiquidityMiningLogic.new(); - let liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); - liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + wrapper = await Wrapper.new(liquidityMiningV1.address); + } - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, new BN(1000)); - //dummy settings - await liquidityMining.initialize(ZERO_ADDRESS, liquidityMiningLogic.address, SOVToken.address); + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMining = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + + wrapper = await Wrapper.new(liquidityMining.address); } async function deployProtocol() { From 1aebbc99c52e97b4eff847d3958fa55173b479d0 Mon Sep 17 00:00:00 2001 From: py-ro Date: Thu, 30 Sep 2021 09:21:27 -0300 Subject: [PATCH 04/17] fix: increase github CI memory --- .github/workflows/node.js.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 2f9bb283f..d99db399c 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -25,10 +25,11 @@ jobs: run: npm ci - name: Checking Formatting run: npm run lint && npm run prettier-check + - name: Increasing the memory + run: export NODE_OPTIONS="--max-old-space-size=7168" - name: Running Test run: npm run test - # - name: Increasing the memory - # run: export NODE_OPTIONS="--max-old-space-size=7168" + # - name: Code Coverage # run: npm run coverage # - name: Coveralls GitHub Action From b0dd685c5281684d8504a5645c8aa9ecc9d36028 Mon Sep 17 00:00:00 2001 From: py-ro Date: Thu, 30 Sep 2021 11:48:33 -0300 Subject: [PATCH 05/17] fix: fix PR reviews --- .github/workflows/node.js.yml | 4 +- contracts/farm/LiquidityMiningV2.sol | 14 ++-- tests/farm/LiquidityMiningMigration.js | 101 +++++-------------------- 3 files changed, 30 insertions(+), 89 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index d99db399c..4114f217a 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -25,10 +25,10 @@ jobs: run: npm ci - name: Checking Formatting run: npm run lint && npm run prettier-check - - name: Increasing the memory - run: export NODE_OPTIONS="--max-old-space-size=7168" - name: Running Test run: npm run test + - name: Increasing the memory + run: export NODE_OPTIONS="--max-old-space-size=7168" # - name: Code Coverage # run: npm run coverage diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index 118982649..ccb0c02b9 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -561,7 +561,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { * @param _poolToken the address of pool token * @param _user the address of user to claim reward from (can be passed only by wrapper contract) */ - function claimRewards(address _poolToken, address _user) external onlyAfterMigrationFinished { + function claimRewards(address _poolToken, address _user) external { address userAddress = _getUserAddress(_user); uint256 poolId = _getPoolId(_poolToken); @@ -583,7 +583,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _poolToken, address _rewardToken, address _user - ) external onlyAfterMigrationFinished { + ) external { address userAddress = _getUserAddress(_user); uint256 poolId = _getPoolId(_poolToken); @@ -608,7 +608,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { * @notice transfers reward tokens from all pools * @param _user the address of user to claim reward from (can be passed only by wrapper contract) */ - function claimRewardFromAllPools(address _user) external onlyAfterMigrationFinished { + function claimRewardFromAllPools(address _user) external { address userAddress = _getUserAddress(_user); uint256 length = poolInfoList.length; @@ -948,7 +948,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _rewardToken, uint256 _lastRewardBlock, uint256 _accumulatedRewardPerShare - ) external onlyAuthorized onlyMigrator { + ) external onlyMigrator { uint256 poolId = _getPoolId(_poolToken); PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; @@ -959,7 +959,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _rewardToken, uint256 _startBlock, uint256 _totalUsersBalance - ) external onlyAuthorized onlyMigrator { + ) external onlyMigrator { RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; rewardToken.startBlock = _startBlock; rewardToken.totalUsersBalance = _totalUsersBalance; @@ -972,7 +972,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _amount, uint256 _rewardDebt, uint256 _accumulatedReward - ) external onlyAuthorized onlyMigrator { + ) external onlyMigrator { UserInfo storage userInfo = userInfoMap[_poolId][_user]; UserReward storage userReward = userInfo.rewards[_rewardToken]; userInfo.amount += _amount; @@ -983,7 +983,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { /** * @notice finish migration */ - function finishMigration() external onlyAuthorized onlyMigrator { + function finishMigration() external onlyMigrator { migrationFinished = true; } } diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index e33070d18..6794531f7 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -822,25 +822,29 @@ describe("LiquidityMiningMigration", () => { expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); }); - it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + it("Should lend to the pool at the liquidity mining V1, lend to liquidity mining V2 and migrate", async () => { const depositAmount = new BN(wei("400", "ether")); await liquidityMining.add(loanToken.address, new BN(10), false); await loanToken.setLiquidityMiningAddress(liquidityMining.address); await underlyingToken.approve(loanToken.address, depositAmount); - await loanToken.mint(lender, depositAmount, false); + await loanToken.mint(lender, depositAmount, true); await liquidityMining.addAdmin(migrator.address); await liquidityMining.startMigrationGracePeriod(); await liquidityMiningV2.addAdmin(migrator.address); await migrator.migratePools(); - await migrator.migrateUsers([lender]); await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await migrator.migrateUsers([lender]); + const userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged - expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); }); it("Should only allow to burn tokens if migration finished", async () => { @@ -897,36 +901,6 @@ describe("LiquidityMiningMigration", () => { assetAmount: depositAmount, }); }); - - it("Should burn pool tokens without removing them from the LM pool", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanToken.address, new BN(10), false); - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await underlyingToken.approve(loanToken.address, depositAmount.mul(new BN(2))); - await loanToken.mint(lender, depositAmount, true); - await loanToken.mint(lender, depositAmount, false); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); - - let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); - - await loanToken.burn(lender, depositAmount, false); - - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); - }); }); describe("Test WRBTC lending with liquidity mining", () => { @@ -942,8 +916,6 @@ describe("LiquidityMiningMigration", () => { await liquidityMiningV2.addAdmin(migrator.address); await migrator.migratePools(); await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); @@ -971,8 +943,6 @@ describe("LiquidityMiningMigration", () => { await liquidityMiningV2.addAdmin(migrator.address); await migrator.migratePools(); await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); @@ -983,27 +953,28 @@ describe("LiquidityMiningMigration", () => { expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); }); - it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + it("Should lend to the pool at the liquidity mining V1, lend to liquidity mining V2 and migrate", async () => { const depositAmount = new BN(wei("400", "ether")); await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); await liquidityMining.addAdmin(migrator.address); await liquidityMining.startMigrationGracePeriod(); await liquidityMiningV2.addAdmin(migrator.address); await migrator.migratePools(); - await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await migrator.migrateUsers([lender]); + const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); }); it("Should only allow to burn tokens if migration finished", async () => { @@ -1055,36 +1026,6 @@ describe("LiquidityMiningMigration", () => { assetAmount: depositAmount, }); }); - - it("Should burn pool tokens without removing them from the LM pool", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - - await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); - - let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); - - const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, false); - - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); - }); }); async function deployLiquidityMining() { From 4e4ca4045ad799e7a08910eababe868fc043de15 Mon Sep 17 00:00:00 2001 From: py-ro Date: Thu, 30 Sep 2021 12:35:30 -0300 Subject: [PATCH 06/17] fix: increase github CI memory --- .github/workflows/node.js.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 4114f217a..887304770 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -25,10 +25,10 @@ jobs: run: npm ci - name: Checking Formatting run: npm run lint && npm run prettier-check + - name: Increasing the memory + run: export NODE_OPTIONS="--max-old-space-size=8192" - name: Running Test run: npm run test - - name: Increasing the memory - run: export NODE_OPTIONS="--max-old-space-size=7168" # - name: Code Coverage # run: npm run coverage From bb4196756dba4e86e927e1610c071e66bee58d35 Mon Sep 17 00:00:00 2001 From: Swamp Thing Date: Tue, 12 Oct 2021 17:56:36 -0300 Subject: [PATCH 07/17] ci: increased nodejs memory so it can run the tests properly on node 14.x --- .github/workflows/node.js.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 887304770..b7ed2a36a 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -14,7 +14,8 @@ jobs: matrix: node-version: [12.x, 14.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - + env: + NODE_OPTIONS: "--max-old-space-size=8192" steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -25,8 +26,6 @@ jobs: run: npm ci - name: Checking Formatting run: npm run lint && npm run prettier-check - - name: Increasing the memory - run: export NODE_OPTIONS="--max-old-space-size=8192" - name: Running Test run: npm run test From 915dbb85dd5fac4fc144f2b1d45400654b4a324e Mon Sep 17 00:00:00 2001 From: py-ro Date: Wed, 13 Oct 2021 15:05:03 -0300 Subject: [PATCH 08/17] fix: LM contracts initialize only once --- contracts/farm/LMV1toLMV2Migrator.sol | 1 + contracts/farm/LiquidityMiningV1.sol | 12 ++++- contracts/farm/LiquidityMiningV2.sol | 8 +--- .../liquidity_mining_V1toV2_migrator.py | 2 +- tests/farm/LiquidityMiningMigration.js | 47 +++++++++---------- tests/farm/LiquidityMiningV1.js | 25 +++++++++- tests/farm/LiquidityMiningV2.js | 18 ++++--- tests/loan-token/LendingWithLMV2.test.js | 2 +- 8 files changed, 68 insertions(+), 47 deletions(-) diff --git a/contracts/farm/LMV1toLMV2Migrator.sol b/contracts/farm/LMV1toLMV2Migrator.sol index 6c3e2ab48..5c8104bf8 100644 --- a/contracts/farm/LMV1toLMV2Migrator.sol +++ b/contracts/farm/LMV1toLMV2Migrator.sol @@ -61,6 +61,7 @@ contract LMV1toLMV2Migrator is AdminRole { require(address(_SOV) != address(0), "invalid token address"); require(address(_liquidityMiningV1) != address(0), "invalid contract address"); require(address(_liquidityMiningV2) != address(0), "invalid contract address"); + require(address(SOV) == address(0), "Already initialized"); liquidityMiningV1 = _liquidityMiningV1; liquidityMiningV2 = _liquidityMiningV2; SOV = _SOV; diff --git a/contracts/farm/LiquidityMiningV1.sol b/contracts/farm/LiquidityMiningV1.sol index e45cce023..e9d138653 100644 --- a/contracts/farm/LiquidityMiningV1.sol +++ b/contracts/farm/LiquidityMiningV1.sol @@ -36,6 +36,14 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { _; } + modifier onlyMigrationGracePeriodStarted() { + require( + migrationGracePeriodState == MigrationGracePeriodStates.Started, + "Forbidden: Migration hasn't started yet or already finished" + ); + _; + } + modifier onlyBeforeMigrationGracePeriodFinished() { require(migrationGracePeriodState < MigrationGracePeriodStates.Finished, "Forbidden: contract deprecated"); _; @@ -56,6 +64,7 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { function initialize(address _liquidityMiningV2) external onlyAuthorized { /// @dev Non-idempotent function. Must be called just once. require(_liquidityMiningV2 != address(0), "Invalid address"); + require(liquidityMiningV2 == address(0), "Already initialized"); liquidityMiningV2 = _liquidityMiningV2; } @@ -107,8 +116,7 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { // TODO: this should only be used by the LiquidityMiningV2 contract?? /// @notice This function finishes the migration process disabling further withdrawals and claims /// @dev migration grace period should have started before this function is called. - function finishMigrationGracePeriod() external onlyAuthorized onlyBeforeMigrationGracePeriodFinished { - require(migrationGracePeriodState == MigrationGracePeriodStates.Started, "Migration hasn't started yet"); + function finishMigrationGracePeriod() external onlyAuthorized onlyMigrationGracePeriodStarted { migrationGracePeriodState = MigrationGracePeriodStates.Finished; } diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index ccb0c02b9..2f31bab1a 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -60,14 +60,10 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { /** * @notice Initialize mining. */ - function initialize( - address _wrapper, - address _migrator, - IERC20 _SOV - ) external onlyAuthorized { + function initialize(address _wrapper, address _migrator) external onlyAuthorized { /// @dev Non-idempotent function. Must be called just once. require(_migrator != address(0), "invalid contract address"); - require(address(_SOV) != address(0), "invalid token address"); + require(migrator == address(0), "Already initialized"); wrapper = _wrapper; migrator = _migrator; } diff --git a/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py b/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py index 85f240c4d..06891454f 100644 --- a/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py +++ b/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py @@ -16,7 +16,7 @@ def initializeLiquidityMiningV2(): wrapper = "0x0000000000000000000000000000000000000000" liquidityMiningV2 = Contract.from_abi("LiquidityMiningV2", address = conf.contracts['LiquidityMiningProxyV2'], abi = LiquidityMiningV2.abi, owner = conf.acct) - data = liquidityMiningV2.initialize.encode_input(wrapper,conf.contracts['LMV1toLMV2Migrator'],conf.contracts['SOV']) + data = liquidityMiningV2.initialize.encode_input(wrapper,conf.contracts['LMV1toLMV2Migrator']) sendWithMultisig(conf.contracts['multisig'], liquidityMiningV2.address, data, conf.acct) def setMigratorAsAdmin(): diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index 6794531f7..d51e7cd02 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -127,7 +127,7 @@ describe("LiquidityMiningMigration", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMiningV2.initialize(wrapper.address, migrator.address); erc20RewardTransferLogic = await ERC20TransferLogic.new(); @@ -184,6 +184,14 @@ describe("LiquidityMiningMigration", () => { migrator = await Migrator.new(); await expectRevert(migrator.initialize(SOVToken.address, liquidityMining.address, ZERO_ADDRESS), "invalid contract address"); }); + it("fails if already initialized", async () => { + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + await expectRevert( + migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address), + "Already initialized" + ); + }); }); describe("migratePools", () => { @@ -199,7 +207,13 @@ describe("LiquidityMiningMigration", () => { await expectRevert(migrator.migratePools(), "unauthorized"); }); it("should only allow to migrate pools by migrator contract", async () => { - await liquidityMiningV2.initialize(wrapper.address, SOVToken.address, SOVToken.address); + await deployLiquidityMiningV2(); + await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); + + migrator = await Migrator.new(); + await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); + + await liquidityMiningV2.initialize(wrapper.address, SOVToken.address); await liquidityMining.addAdmin(migrator.address); await liquidityMining.startMigrationGracePeriod(); await liquidityMiningV2.addAdmin(migrator.address); @@ -207,7 +221,7 @@ describe("LiquidityMiningMigration", () => { }); it("should only allow to migrate pools if the migrate grace period started", async () => { await liquidityMining.addAdmin(migrator.address); - await expectRevert(migrator.migratePools(), "Migration hasn't started yet"); + await expectRevert(migrator.migratePools(), "Forbidden: Migration hasn't started yet or already finished"); }); it("should only allow to migrate pools once", async () => { await liquidityMining.addAdmin(migrator.address); @@ -263,14 +277,6 @@ describe("LiquidityMiningMigration", () => { it("should fail finishing users migration if pools were not migrated", async () => { await expectRevert(migrator.finishUsersMigration(), "Wrong state: should be MigratingUsers"); }); - it("should only allow to migrate users by migrator contract", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await liquidityMiningV2.initialize(wrapper.address, SOVToken.address, SOVToken.address); - await expectRevert(migrator.migrateUsers(accounts), "only allowed to migrator contract"); - }); it("should only allow to finish users migration by the admin", async () => { await liquidityMining.addAdmin(migrator.address); await liquidityMining.startMigrationGracePeriod(); @@ -393,15 +399,6 @@ describe("LiquidityMiningMigration", () => { it("should fail migrating funds if users were not migrated", async () => { await expectRevert(migrator.migrateFunds(), "Wrong state: should be MigratingFunds"); }); - it("should only allow to migrate funds by migrator contract", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await liquidityMiningV2.initialize(wrapper.address, SOVToken.address, SOVToken.address); - await expectRevert(migrator.migrateFunds(), "only allowed to migrator contract"); - }); it("should fail trying to migrate funds without SOV tokens in liquidityMiningV1", async () => { const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); await liquidityMining.transferSOV(liquidityMiningV2.address, balanceSOV); @@ -438,7 +435,7 @@ describe("LiquidityMiningMigration", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMiningV2.initialize(wrapper.address, migrator.address); await liquidityMining.addAdmin(migrator.address); await liquidityMining.startMigrationGracePeriod(); @@ -550,7 +547,7 @@ describe("LiquidityMiningMigration", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMiningV2.initialize(wrapper.address, migrator.address); rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); @@ -612,7 +609,7 @@ describe("LiquidityMiningMigration", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMiningV2.initialize(wrapper.address, migrator.address); rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); @@ -675,7 +672,7 @@ describe("LiquidityMiningMigration", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMiningV2.initialize(wrapper.address, migrator.address); rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); @@ -727,7 +724,7 @@ describe("LiquidityMiningMigration", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - await liquidityMiningV2.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMiningV2.initialize(wrapper.address, migrator.address); rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); diff --git a/tests/farm/LiquidityMiningV1.js b/tests/farm/LiquidityMiningV1.js index 96a6ccedd..aa8deb1f3 100644 --- a/tests/farm/LiquidityMiningV1.js +++ b/tests/farm/LiquidityMiningV1.js @@ -168,6 +168,21 @@ describe("LiquidityMining", () => { "Already initialized" ); }); + it("fails if already initialized", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + await liquidityMining.initialize(liquidityMiningV2.address); + await expectRevert(liquidityMining.initialize(liquidityMiningV2.address), "Already initialized"); + }); it("fails if the 0 address is passed as token address", async () => { await deployLiquidityMining(); await expectRevert( @@ -1845,7 +1860,10 @@ describe("LiquidityMining", () => { }); it("should fail if grace period hasn't started before finishing", async () => { - await expectRevert(liquidityMining.finishMigrationGracePeriod(), "Migration hasn't started yet"); + await expectRevert( + liquidityMining.finishMigrationGracePeriod(), + "Forbidden: Migration hasn't started yet or already finished" + ); }); it("should fail if grace period has already finished", async () => { @@ -1855,7 +1873,10 @@ describe("LiquidityMining", () => { const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Finished); - await expectRevert(liquidityMining.finishMigrationGracePeriod(), "Forbidden: contract deprecated"); + await expectRevert( + liquidityMining.finishMigrationGracePeriod(), + "Forbidden: Migration hasn't started yet or already finished" + ); }); it("should properly finish grace period", async () => { diff --git a/tests/farm/LiquidityMiningV2.js b/tests/farm/LiquidityMiningV2.js index ab57796c6..9b0f11da4 100644 --- a/tests/farm/LiquidityMiningV2.js +++ b/tests/farm/LiquidityMiningV2.js @@ -75,7 +75,7 @@ describe("LiquidityMiningV2", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); - await liquidityMining.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMining.initialize(wrapper.address, migrator.address); erc20RewardTransferLogic = await ERC20TransferLogic.new(); @@ -101,20 +101,18 @@ describe("LiquidityMiningV2", () => { describe("initialize", () => { it("should fail if migrator address is invalid", async () => { await deployLiquidityMiningV2(); - await expectRevert(liquidityMining.initialize(wrapper.address, ZERO_ADDRESS, SOVToken.address), "invalid contract address"); + await expectRevert(liquidityMining.initialize(wrapper.address, ZERO_ADDRESS), "invalid contract address"); }); - it("should fail if SOV address is invalid", async () => { + it("fails if already initialized", async () => { await deployLiquidityMiningV2(); - await expectRevert( - liquidityMining.initialize(wrapper.address, liquidityMiningV1.address, ZERO_ADDRESS), - "invalid token address" - ); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); + await expectRevert(liquidityMining.initialize(wrapper.address, liquidityMiningV1.address), "Already initialized"); }); it("sets the expected values", async () => { await deployLiquidityMiningV2(); - await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address, SOVToken.address); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); let _wrapper = await liquidityMining.wrapper(); @@ -569,7 +567,7 @@ describe("LiquidityMiningV2", () => { }); it("should only allow to deposit if migration is finished", async () => { await deployLiquidityMiningV2(); - await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address, SOVToken.address); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); await liquidityMining.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); await expectRevert( @@ -1540,7 +1538,7 @@ describe("LiquidityMiningV2", () => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); - await liquidityMining.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMining.initialize(wrapper.address, migrator.address); for (let token of [token1, token2]) { for (let account of [account1, account2]) { diff --git a/tests/loan-token/LendingWithLMV2.test.js b/tests/loan-token/LendingWithLMV2.test.js index afbaa38bd..41dbe209d 100644 --- a/tests/loan-token/LendingWithLMV2.test.js +++ b/tests/loan-token/LendingWithLMV2.test.js @@ -97,7 +97,7 @@ contract("LoanTokenLogicLM", (accounts) => { migrator = await Migrator.new(); await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); - await liquidityMining.initialize(wrapper.address, migrator.address, SOVToken.address); + await liquidityMining.initialize(wrapper.address, migrator.address); rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); From f754f6457a4458280e4b13a7d210c01a6ddd9492 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 4 May 2022 20:22:01 +0530 Subject: [PATCH 09/17] Ran Prettier --- contracts/farm/ERC20TransferLogic.sol | 64 +- contracts/farm/ERC20TransferLogicStorage.sol | 2 +- contracts/farm/ILiquidityMiningV1.sol | 75 +- contracts/farm/ILiquidityMiningV2.sol | 83 +- contracts/farm/IRewardTransferLogic.sol | 26 +- contracts/farm/LMV1toLMV2Migrator.sol | 312 +- contracts/farm/LiquidityMiningStorageV1.sol | 20 +- contracts/farm/LiquidityMiningStorageV2.sol | 122 +- contracts/farm/LiquidityMiningV1.sol | 1602 +++--- contracts/farm/LiquidityMiningV2.sol | 2036 +++---- .../farm/LockedSOVRewardTransferLogic.sol | 110 +- .../LockedSOVRewardTransferLogicStorage.sol | 10 +- contracts/mockup/LiquidityMiningV1Mockup.sol | 20 +- contracts/mockup/LiquidityMiningV2Mockup.sol | 19 +- contracts/mockup/RBTCWrapperProxyMockupV2.sol | 26 +- contracts/mockup/TestPoolToken.sol | 32 +- tests/farm/ERC20TransferLogic.js | 265 +- tests/farm/LiquidityMiningMigration.js | 2848 +++++----- tests/farm/LiquidityMiningV1.js | 4509 ++++++++------- tests/farm/LiquidityMiningV2.js | 4820 ++++++++++------- tests/farm/LockedSOVRewardTransferLogic.js | 471 +- tests/loan-token/LendingWithLMV2.test.js | 624 ++- 22 files changed, 9994 insertions(+), 8102 deletions(-) diff --git a/contracts/farm/ERC20TransferLogic.sol b/contracts/farm/ERC20TransferLogic.sol index 3a9f2e707..12a0e4ea0 100644 --- a/contracts/farm/ERC20TransferLogic.sol +++ b/contracts/farm/ERC20TransferLogic.sol @@ -6,36 +6,36 @@ import "../interfaces/IERC20.sol"; import "../openzeppelin/SafeERC20.sol"; contract ERC20TransferLogic is IRewardTransferLogic, ERC20TransferLogicStorage { - using SafeERC20 for IERC20; - - event TokenAddressUpdated(address _newTokenAddress); - - /** - * @param _token Reward token to be distributed - */ - function initialize(address _token) external onlyAuthorized { - setTokenAddress(_token); - } - - function setTokenAddress(address _token) public onlyAuthorized { - require(_token != address(0), "Invalid token address"); - token = IERC20(_token); - emit TokenAddressUpdated(_token); - } - - function getRewardTokenAddress() external view returns (address) { - return address(token); - } - - function senderToAuthorize() external view returns (address) { - return address(this); - } - - function transferReward( - address _to, - uint256 _value, - bool // it doesn't matter if it's a withdrawal or not - ) external { - token.safeTransferFrom(msg.sender, _to, _value); - } + using SafeERC20 for IERC20; + + event TokenAddressUpdated(address _newTokenAddress); + + /** + * @param _token Reward token to be distributed + */ + function initialize(address _token) external onlyAuthorized { + setTokenAddress(_token); + } + + function setTokenAddress(address _token) public onlyAuthorized { + require(_token != address(0), "Invalid token address"); + token = IERC20(_token); + emit TokenAddressUpdated(_token); + } + + function getRewardTokenAddress() external view returns (address) { + return address(token); + } + + function senderToAuthorize() external view returns (address) { + return address(this); + } + + function transferReward( + address _to, + uint256 _value, + bool // it doesn't matter if it's a withdrawal or not + ) external { + token.safeTransferFrom(msg.sender, _to, _value); + } } diff --git a/contracts/farm/ERC20TransferLogicStorage.sol b/contracts/farm/ERC20TransferLogicStorage.sol index 756df6e39..66f5c045f 100644 --- a/contracts/farm/ERC20TransferLogicStorage.sol +++ b/contracts/farm/ERC20TransferLogicStorage.sol @@ -5,5 +5,5 @@ import "../utils/AdminRole.sol"; import "../interfaces/IERC20.sol"; contract ERC20TransferLogicStorage is IRewardTransferLogic, AdminRole { - IERC20 public token; + IERC20 public token; } diff --git a/contracts/farm/ILiquidityMiningV1.sol b/contracts/farm/ILiquidityMiningV1.sol index d471a1c7c..649b0d1ca 100644 --- a/contracts/farm/ILiquidityMiningV1.sol +++ b/contracts/farm/ILiquidityMiningV1.sol @@ -1,40 +1,43 @@ pragma solidity 0.5.17; interface ILiquidityMiningV1 { - function withdraw( - address _poolToken, - uint256 _amount, - address _user - ) external; - - function onTokensDeposited(address _user, uint256 _amount) external; - - function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256); - - function getPoolInfoListArray() - external - view - returns ( - address[] memory, - uint96[] memory, - uint256[] memory, - uint256[] memory - ); - - function getUserInfoListArray(address _user) - external - view - returns ( - uint256[] memory, - uint256[] memory, - uint256[] memory - ); - - function migrateFunds() external; - - function finishMigrationGracePeriod() external; - - function getTotalUsersBalance() external view returns (uint256); - - function getStartBlock() external view returns (uint256); + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external; + + function onTokensDeposited(address _user, uint256 _amount) external; + + function getUserPoolTokenBalance(address _poolToken, address _user) + external + view + returns (uint256); + + function getPoolInfoListArray() + external + view + returns ( + address[] memory, + uint96[] memory, + uint256[] memory, + uint256[] memory + ); + + function getUserInfoListArray(address _user) + external + view + returns ( + uint256[] memory, + uint256[] memory, + uint256[] memory + ); + + function migrateFunds() external; + + function finishMigrationGracePeriod() external; + + function getTotalUsersBalance() external view returns (uint256); + + function getStartBlock() external view returns (uint256); } diff --git a/contracts/farm/ILiquidityMiningV2.sol b/contracts/farm/ILiquidityMiningV2.sol index 8e21ffab8..a30e3ded3 100644 --- a/contracts/farm/ILiquidityMiningV2.sol +++ b/contracts/farm/ILiquidityMiningV2.sol @@ -1,44 +1,47 @@ pragma solidity 0.5.17; interface ILiquidityMiningV2 { - function withdraw( - address _poolToken, - uint256 _amount, - address _user - ) external; - - function onTokensDeposited(address _user, uint256 _amount) external; - - function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256); - - function setPoolInfoRewardToken( - address _poolToken, - address _rewardToken, - uint256 _lastRewardBlock, - uint256 _accumulatedRewardPerShare - ) external; - - function setRewardToken( - address _rewardToken, - uint256 _startBlock, - uint256 _totalUsersBalance - ) external; - - function setUserInfo( - uint256 _poolId, - address _user, - address _rewardToken, - uint256 _amount, - uint256 _rewardDebt, - uint256 _accumulatedReward - ) external; - - function add( - address _poolToken, - address[] calldata _rewardTokens, - uint96[] calldata _allocationPoints, - bool _withUpdate - ) external; - - function finishMigration() external; + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external; + + function onTokensDeposited(address _user, uint256 _amount) external; + + function getUserPoolTokenBalance(address _poolToken, address _user) + external + view + returns (uint256); + + function setPoolInfoRewardToken( + address _poolToken, + address _rewardToken, + uint256 _lastRewardBlock, + uint256 _accumulatedRewardPerShare + ) external; + + function setRewardToken( + address _rewardToken, + uint256 _startBlock, + uint256 _totalUsersBalance + ) external; + + function setUserInfo( + uint256 _poolId, + address _user, + address _rewardToken, + uint256 _amount, + uint256 _rewardDebt, + uint256 _accumulatedReward + ) external; + + function add( + address _poolToken, + address[] calldata _rewardTokens, + uint96[] calldata _allocationPoints, + bool _withUpdate + ) external; + + function finishMigration() external; } diff --git a/contracts/farm/IRewardTransferLogic.sol b/contracts/farm/IRewardTransferLogic.sol index 923276356..de562b812 100644 --- a/contracts/farm/IRewardTransferLogic.sol +++ b/contracts/farm/IRewardTransferLogic.sol @@ -4,19 +4,19 @@ pragma solidity 0.5.17; /// @dev Implement this interface in order to transfer the rewards with different logic. For example: /// SOV tokens interface IRewardTransferLogic { - /// @dev Returns the reward token address this contract will transfer - function getRewardTokenAddress() external view returns (address); + /// @dev Returns the reward token address this contract will transfer + function getRewardTokenAddress() external view returns (address); - /// @notice Transfers will be executed from this address so it must be approved before invoking - function senderToAuthorize() external view returns (address); + /// @notice Transfers will be executed from this address so it must be approved before invoking + function senderToAuthorize() external view returns (address); - /// @notice Transfers the reward amount to the specified address - /// @param _to The address to transfer the reward to - /// @param _value The amount of the reward to transfer - /// @param _isWithdrawal If true, means that the reward and the LP deposited tokens are being compeltely withdrawn - function transferReward( - address _to, - uint256 _value, - bool _isWithdrawal - ) external; + /// @notice Transfers the reward amount to the specified address + /// @param _to The address to transfer the reward to + /// @param _value The amount of the reward to transfer + /// @param _isWithdrawal If true, means that the reward and the LP deposited tokens are being compeltely withdrawn + function transferReward( + address _to, + uint256 _value, + bool _isWithdrawal + ) external; } diff --git a/contracts/farm/LMV1toLMV2Migrator.sol b/contracts/farm/LMV1toLMV2Migrator.sol index 5c8104bf8..bc2fe271e 100644 --- a/contracts/farm/LMV1toLMV2Migrator.sol +++ b/contracts/farm/LMV1toLMV2Migrator.sol @@ -9,146 +9,174 @@ import "./ILiquidityMiningV1.sol"; import "./ILiquidityMiningV2.sol"; contract LMV1toLMV2Migrator is AdminRole { - using SafeMath for uint256; - using SafeERC20 for IERC20; - enum MigrationStates { MigratingPools, MigratingUsers, MigratingFunds, MigrationFinished } - - //represents de migration state from LiquidityMiningV1 to LiquidityMiningV2 - MigrationStates public migrationState; - - //LiquidityMiningV1 contract address - ILiquidityMiningV1 public liquidityMiningV1; - - //LiquidityMiningV2 contract address - ILiquidityMiningV2 public liquidityMiningV2; - - /// @dev it is true if the user has been already migrated - mapping(address => bool) public userMigrated; - - /// @dev The SOV token - IERC20 public SOV; - - event UserMigrated(address indexed user); - - /* Modifiers */ - modifier onlyPoolsMigrationState() { - require(migrationState == MigrationStates.MigratingPools, "Wrong state: should be MigratingPools"); - _; - } - - modifier onlyUsersMigrationState() { - require(migrationState == MigrationStates.MigratingUsers, "Wrong state: should be MigratingUsers"); - _; - } - - modifier onlyFundsMigrationState() { - require(migrationState == MigrationStates.MigratingFunds, "Wrong state: should be MigratingFunds"); - _; - } - - /** - * @notice Initialize migrator - * - * @param _SOV The SOV token address - * @param _liquidityMiningV1 The LiquidityMiningV1 contract address - * @param _liquidityMiningV2 The LiquidityMiningV2 contract address - */ - function initialize( - IERC20 _SOV, - ILiquidityMiningV1 _liquidityMiningV1, - ILiquidityMiningV2 _liquidityMiningV2 - ) external onlyAuthorized { - require(address(_SOV) != address(0), "invalid token address"); - require(address(_liquidityMiningV1) != address(0), "invalid contract address"); - require(address(_liquidityMiningV2) != address(0), "invalid contract address"); - require(address(SOV) == address(0), "Already initialized"); - liquidityMiningV1 = _liquidityMiningV1; - liquidityMiningV2 = _liquidityMiningV2; - SOV = _SOV; - migrationState = MigrationStates.MigratingPools; - } - - function _finishPoolsMigration() internal onlyPoolsMigrationState { - migrationState = MigrationStates.MigratingUsers; - } - - function finishUsersMigration() external onlyAuthorized onlyUsersMigrationState { - migrationState = MigrationStates.MigratingFunds; - } - - function _finishFundsMigration() internal onlyFundsMigrationState { - migrationState = MigrationStates.MigrationFinished; - } - - /** - * @notice read all pools from liquidity mining V1 contract and add them - */ - function migratePools() external onlyAuthorized onlyPoolsMigrationState { - ( - address[] memory _poolToken, - uint96[] memory _allocationPoints, - uint256[] memory _lastRewardBlock, - uint256[] memory _accumulatedRewardPerShare - ) = liquidityMiningV1.getPoolInfoListArray(); - - require(_poolToken.length == _allocationPoints.length, "Arrays mismatch"); - require(_poolToken.length == _lastRewardBlock.length, "Arrays mismatch"); - - _finishPoolsMigration(); - liquidityMiningV1.finishMigrationGracePeriod(); - for (uint256 i = 0; i < _poolToken.length; i++) { - address poolToken = _poolToken[i]; - uint96[] memory allocationPoints = new uint96[](1); - allocationPoints[0] = _allocationPoints[i]; - uint256 lastRewardBlock = _lastRewardBlock[i]; - uint256 accumulatedRewardPerShare = _accumulatedRewardPerShare[i]; - address[] memory SOVAddress = new address[](1); - SOVAddress[0] = address(SOV); - //add will revert if poolToken is invalid or if it was already added - liquidityMiningV2.add(poolToken, SOVAddress, allocationPoints, false); - //add pool function put lastRewardBlock with current block number value, so we need to retrieve the original - liquidityMiningV2.setPoolInfoRewardToken(poolToken, address(SOV), lastRewardBlock, accumulatedRewardPerShare); - } - uint256 _startblock = liquidityMiningV1.getStartBlock(); - uint256 _totalUsersBalance = liquidityMiningV1.getTotalUsersBalance(); - liquidityMiningV2.setRewardToken(address(SOV), _startblock, _totalUsersBalance); - } - - /** - * @notice read all users of all the pools from liquidity mining V1 contract and copy their info - * @param _users a list of users to be copied - */ - - function migrateUsers(address[] calldata _users) external onlyAuthorized onlyUsersMigrationState { - for (uint256 i = 0; i < _users.length; i++) { - (uint256[] memory _amount, uint256[] memory _rewardDebt, uint256[] memory _accumulatedReward) = - liquidityMiningV1.getUserInfoListArray(_users[i]); - - require(_amount.length == _rewardDebt.length, "Arrays mismatch"); - require(_amount.length == _accumulatedReward.length, "Arrays mismatch"); - - address user = _users[i]; - - if (!userMigrated[user]) { - userMigrated[user] = true; - for (uint256 j = 0; j < _amount.length; j++) { - uint256 poolId = j; - uint256 _userAmount = _amount[j]; - uint256 _userRewardDebt = _rewardDebt[j]; - uint256 _userAccumulatedReward = _accumulatedReward[j]; - liquidityMiningV2.setUserInfo(poolId, user, address(SOV), _userAmount, _userRewardDebt, _userAccumulatedReward); - } - emit UserMigrated(user); - } - } - } - - /** - * @notice transfer all funds from liquidity mining V1 - */ - function migrateFunds() external onlyAuthorized onlyFundsMigrationState { - _finishFundsMigration(); - liquidityMiningV1.migrateFunds(); - liquidityMiningV2.finishMigration(); - } + using SafeMath for uint256; + using SafeERC20 for IERC20; + enum MigrationStates { MigratingPools, MigratingUsers, MigratingFunds, MigrationFinished } + + //represents de migration state from LiquidityMiningV1 to LiquidityMiningV2 + MigrationStates public migrationState; + + //LiquidityMiningV1 contract address + ILiquidityMiningV1 public liquidityMiningV1; + + //LiquidityMiningV2 contract address + ILiquidityMiningV2 public liquidityMiningV2; + + /// @dev it is true if the user has been already migrated + mapping(address => bool) public userMigrated; + + /// @dev The SOV token + IERC20 public SOV; + + event UserMigrated(address indexed user); + + /* Modifiers */ + modifier onlyPoolsMigrationState() { + require( + migrationState == MigrationStates.MigratingPools, + "Wrong state: should be MigratingPools" + ); + _; + } + + modifier onlyUsersMigrationState() { + require( + migrationState == MigrationStates.MigratingUsers, + "Wrong state: should be MigratingUsers" + ); + _; + } + + modifier onlyFundsMigrationState() { + require( + migrationState == MigrationStates.MigratingFunds, + "Wrong state: should be MigratingFunds" + ); + _; + } + + /** + * @notice Initialize migrator + * + * @param _SOV The SOV token address + * @param _liquidityMiningV1 The LiquidityMiningV1 contract address + * @param _liquidityMiningV2 The LiquidityMiningV2 contract address + */ + function initialize( + IERC20 _SOV, + ILiquidityMiningV1 _liquidityMiningV1, + ILiquidityMiningV2 _liquidityMiningV2 + ) external onlyAuthorized { + require(address(_SOV) != address(0), "invalid token address"); + require(address(_liquidityMiningV1) != address(0), "invalid contract address"); + require(address(_liquidityMiningV2) != address(0), "invalid contract address"); + require(address(SOV) == address(0), "Already initialized"); + liquidityMiningV1 = _liquidityMiningV1; + liquidityMiningV2 = _liquidityMiningV2; + SOV = _SOV; + migrationState = MigrationStates.MigratingPools; + } + + function _finishPoolsMigration() internal onlyPoolsMigrationState { + migrationState = MigrationStates.MigratingUsers; + } + + function finishUsersMigration() external onlyAuthorized onlyUsersMigrationState { + migrationState = MigrationStates.MigratingFunds; + } + + function _finishFundsMigration() internal onlyFundsMigrationState { + migrationState = MigrationStates.MigrationFinished; + } + + /** + * @notice read all pools from liquidity mining V1 contract and add them + */ + function migratePools() external onlyAuthorized onlyPoolsMigrationState { + ( + address[] memory _poolToken, + uint96[] memory _allocationPoints, + uint256[] memory _lastRewardBlock, + uint256[] memory _accumulatedRewardPerShare + ) = liquidityMiningV1.getPoolInfoListArray(); + + require(_poolToken.length == _allocationPoints.length, "Arrays mismatch"); + require(_poolToken.length == _lastRewardBlock.length, "Arrays mismatch"); + + _finishPoolsMigration(); + liquidityMiningV1.finishMigrationGracePeriod(); + for (uint256 i = 0; i < _poolToken.length; i++) { + address poolToken = _poolToken[i]; + uint96[] memory allocationPoints = new uint96[](1); + allocationPoints[0] = _allocationPoints[i]; + uint256 lastRewardBlock = _lastRewardBlock[i]; + uint256 accumulatedRewardPerShare = _accumulatedRewardPerShare[i]; + address[] memory SOVAddress = new address[](1); + SOVAddress[0] = address(SOV); + //add will revert if poolToken is invalid or if it was already added + liquidityMiningV2.add(poolToken, SOVAddress, allocationPoints, false); + //add pool function put lastRewardBlock with current block number value, so we need to retrieve the original + liquidityMiningV2.setPoolInfoRewardToken( + poolToken, + address(SOV), + lastRewardBlock, + accumulatedRewardPerShare + ); + } + uint256 _startblock = liquidityMiningV1.getStartBlock(); + uint256 _totalUsersBalance = liquidityMiningV1.getTotalUsersBalance(); + liquidityMiningV2.setRewardToken(address(SOV), _startblock, _totalUsersBalance); + } + + /** + * @notice read all users of all the pools from liquidity mining V1 contract and copy their info + * @param _users a list of users to be copied + */ + + function migrateUsers(address[] calldata _users) + external + onlyAuthorized + onlyUsersMigrationState + { + for (uint256 i = 0; i < _users.length; i++) { + ( + uint256[] memory _amount, + uint256[] memory _rewardDebt, + uint256[] memory _accumulatedReward + ) = liquidityMiningV1.getUserInfoListArray(_users[i]); + + require(_amount.length == _rewardDebt.length, "Arrays mismatch"); + require(_amount.length == _accumulatedReward.length, "Arrays mismatch"); + + address user = _users[i]; + + if (!userMigrated[user]) { + userMigrated[user] = true; + for (uint256 j = 0; j < _amount.length; j++) { + uint256 poolId = j; + uint256 _userAmount = _amount[j]; + uint256 _userRewardDebt = _rewardDebt[j]; + uint256 _userAccumulatedReward = _accumulatedReward[j]; + liquidityMiningV2.setUserInfo( + poolId, + user, + address(SOV), + _userAmount, + _userRewardDebt, + _userAccumulatedReward + ); + } + emit UserMigrated(user); + } + } + } + + /** + * @notice transfer all funds from liquidity mining V1 + */ + function migrateFunds() external onlyAuthorized onlyFundsMigrationState { + _finishFundsMigration(); + liquidityMiningV1.migrateFunds(); + liquidityMiningV2.finishMigration(); + } } diff --git a/contracts/farm/LiquidityMiningStorageV1.sol b/contracts/farm/LiquidityMiningStorageV1.sol index 7d3a1847e..0ebb0ce38 100644 --- a/contracts/farm/LiquidityMiningStorageV1.sol +++ b/contracts/farm/LiquidityMiningStorageV1.sol @@ -3,16 +3,16 @@ pragma solidity 0.5.17; import "./LiquidityMiningStorage.sol"; contract LiquidityMiningStorageV1 is LiquidityMiningStorage { - /// @dev Careful when adding new states as there is a < comparison being used in the modifiers - enum MigrationGracePeriodStates { - None, - Started, // users can withdraw funds and rewards but not deposit - Finished // users can't operate with the contract - } + /// @dev Careful when adding new states as there is a < comparison being used in the modifiers + enum MigrationGracePeriodStates { + None, + Started, // users can withdraw funds and rewards but not deposit + Finished // users can't operate with the contract + } - /// @dev Represents migration grace period state - MigrationGracePeriodStates public migrationGracePeriodState; + /// @dev Represents migration grace period state + MigrationGracePeriodStates public migrationGracePeriodState; - /// @dev liquidity mining V2 contract address - address public liquidityMiningV2; + /// @dev liquidity mining V2 contract address + address public liquidityMiningV2; } diff --git a/contracts/farm/LiquidityMiningStorageV2.sol b/contracts/farm/LiquidityMiningStorageV2.sol index 6016cf640..048bffe9d 100644 --- a/contracts/farm/LiquidityMiningStorageV2.sol +++ b/contracts/farm/LiquidityMiningStorageV2.sol @@ -8,77 +8,77 @@ import "../utils/AdminRole.sol"; import "./IRewardTransferLogic.sol"; contract LiquidityMiningStorageV2 is AdminRole { - // Info of each user. - struct UserInfo { - uint256 amount; // How many pool tokens the user has provided. - mapping(address => UserReward) rewards; // Mapping between reward tokens and the user rewards. - } + // Info of each user. + struct UserInfo { + uint256 amount; // How many pool tokens the user has provided. + mapping(address => UserReward) rewards; // Mapping between reward tokens and the user rewards. + } - struct UserReward { - uint256 rewardDebt; // Reward debt. See explanation below. - uint256 accumulatedReward; //Reward that's ready to be transferred - // - // We do some fancy math here. Basically, any point in time, the amount of reward tokens - // entitled to a user but is accumulated to be distributed is: - // - // accumulated reward = (user.amount * pool.accumulatedRewardPerShare) - user.rewardDebt - // - // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: - // 1. The pool's `accumulatedRewardPerShare` (and `lastRewardBlock`) gets updated. - // 2. User receives the accumulated reward sent to his/her address. - // 3. User's `amount` gets updated. - // 4. User's `rewardDebt` gets updated. - } + struct UserReward { + uint256 rewardDebt; // Reward debt. See explanation below. + uint256 accumulatedReward; //Reward that's ready to be transferred + // + // We do some fancy math here. Basically, any point in time, the amount of reward tokens + // entitled to a user but is accumulated to be distributed is: + // + // accumulated reward = (user.amount * pool.accumulatedRewardPerShare) - user.rewardDebt + // + // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: + // 1. The pool's `accumulatedRewardPerShare` (and `lastRewardBlock`) gets updated. + // 2. User receives the accumulated reward sent to his/her address. + // 3. User's `amount` gets updated. + // 4. User's `rewardDebt` gets updated. + } - // Info of each pool. - struct PoolInfo { - IERC20 poolToken; // Address of LP token contract. - address[] rewardTokens; - } + // Info of each pool. + struct PoolInfo { + IERC20 poolToken; // Address of LP token contract. + address[] rewardTokens; + } - // Info about each token to be rewarded to different stakers - struct RewardToken { - // SVR tokens created per block. - uint256 rewardTokensPerBlock; - // The block number when reward token mining starts. - uint256 startBlock; - // Block number when reward token period ends. - uint256 endBlock; - // Total allocation points. Must be the sum of all allocation points in all pools. - uint256 totalAllocationPoint; - // Total balance this contract should have to handle withdrawal for all users - uint256 totalUsersBalance; - // Contract to be invoked in order to transfer reward tokens. - IRewardTransferLogic rewardTransferLogic; - } + // Info about each token to be rewarded to different stakers + struct RewardToken { + // SVR tokens created per block. + uint256 rewardTokensPerBlock; + // The block number when reward token mining starts. + uint256 startBlock; + // Block number when reward token period ends. + uint256 endBlock; + // Total allocation points. Must be the sum of all allocation points in all pools. + uint256 totalAllocationPoint; + // Total balance this contract should have to handle withdrawal for all users + uint256 totalUsersBalance; + // Contract to be invoked in order to transfer reward tokens. + IRewardTransferLogic rewardTransferLogic; + } - struct PoolInfoRewardToken { - uint96 allocationPoint; // How many allocation points assigned to this pool. Amount of reward tokens to distribute per block. - uint256 lastRewardBlock; // Last block number that reward tokens distribution occurs. - uint256 accumulatedRewardPerShare; // Accumulated amount of reward tokens per share, times 1e12. See below. - } + struct PoolInfoRewardToken { + uint96 allocationPoint; // How many allocation points assigned to this pool. Amount of reward tokens to distribute per block. + uint256 lastRewardBlock; // Last block number that reward tokens distribution occurs. + uint256 accumulatedRewardPerShare; // Accumulated amount of reward tokens per share, times 1e12. See below. + } - //Wrapper contract which will be a proxy between user and LM - address public wrapper; + //Wrapper contract which will be a proxy between user and LM + address public wrapper; - // Info of each pool. - PoolInfo[] public poolInfoList; + // Info of each pool. + PoolInfo[] public poolInfoList; - // Mapping pool token address => pool id - mapping(address => uint256) public poolIdList; + // Mapping pool token address => pool id + mapping(address => uint256) public poolIdList; - // Mapping reward token address => reward token - mapping(address => RewardToken) public rewardTokensMap; + // Mapping reward token address => reward token + mapping(address => RewardToken) public rewardTokensMap; - // Mapping to link a LP pool to a reward token - // poolId => rewardTokenAddress => PoolInfoRewardToken - mapping(uint256 => mapping(address => PoolInfoRewardToken)) public poolInfoRewardTokensMap; + // Mapping to link a LP pool to a reward token + // poolId => rewardTokenAddress => PoolInfoRewardToken + mapping(uint256 => mapping(address => PoolInfoRewardToken)) public poolInfoRewardTokensMap; - // Info of each user that stakes LP tokens. - mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + // Info of each user that stakes LP tokens. + mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; - //LiquidityMiningV1 to LiquidityMiningV2 migrator contract address - address public migrator; + //LiquidityMiningV1 to LiquidityMiningV2 migrator contract address + address public migrator; - bool public migrationFinished; + bool public migrationFinished; } diff --git a/contracts/farm/LiquidityMiningV1.sol b/contracts/farm/LiquidityMiningV1.sol index e9d138653..e59c34afb 100644 --- a/contracts/farm/LiquidityMiningV1.sol +++ b/contracts/farm/LiquidityMiningV1.sol @@ -8,768 +8,842 @@ import "./LiquidityMiningStorageV1.sol"; import "./ILiquidityMiningV1.sol"; contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { - using SafeMath for uint256; - using SafeERC20 for IERC20; - - /* Constants */ - - uint256 public constant PRECISION = 1e12; - // Bonus multiplier for early liquidity providers. - // During bonus period each passed block will be calculated like N passed blocks, where N = BONUS_MULTIPLIER - uint256 public constant BONUS_BLOCK_MULTIPLIER = 10; - - uint256 public constant SECONDS_PER_BLOCK = 30; - - /* Events */ - - event SOVTransferred(address indexed receiver, uint256 amount); - event PoolTokenAdded(address indexed user, address indexed poolToken, uint256 allocationPoint); - event PoolTokenUpdated(address indexed user, address indexed poolToken, uint256 newAllocationPoint, uint256 oldAllocationPoint); - event Deposit(address indexed user, address indexed poolToken, uint256 amount); - event RewardClaimed(address indexed user, address indexed poolToken, uint256 amount); - event Withdraw(address indexed user, address indexed poolToken, uint256 amount); - event EmergencyWithdraw(address indexed user, address indexed poolToken, uint256 amount, uint256 accumulatedReward); - - /* Modifiers */ - modifier onlyBeforeMigrationGracePeriod() { - require(migrationGracePeriodState < MigrationGracePeriodStates.Started, "Forbidden: migration already started"); - _; - } - - modifier onlyMigrationGracePeriodStarted() { - require( - migrationGracePeriodState == MigrationGracePeriodStates.Started, - "Forbidden: Migration hasn't started yet or already finished" - ); - _; - } - - modifier onlyBeforeMigrationGracePeriodFinished() { - require(migrationGracePeriodState < MigrationGracePeriodStates.Finished, "Forbidden: contract deprecated"); - _; - } - - modifier onlyAfterMigrationFinished() { - require(migrationGracePeriodState == MigrationGracePeriodStates.Finished, "Forbidden: migration is not over yet"); - _; - } - - /* Functions */ - - /** - * @notice Initialize mining. - * - * @param _liquidityMiningV2 The LiquidityMiningV2 contract address - */ - function initialize(address _liquidityMiningV2) external onlyAuthorized { - /// @dev Non-idempotent function. Must be called just once. - require(_liquidityMiningV2 != address(0), "Invalid address"); - require(liquidityMiningV2 == address(0), "Already initialized"); - liquidityMiningV2 = _liquidityMiningV2; - } - - /** - * @notice Sets lockedSOV contract. - * @param _lockedSOV The contract instance address of the lockedSOV vault. - */ - function setLockedSOV(ILockedSOV _lockedSOV) external onlyAuthorized { - require(address(_lockedSOV) != address(0), "Invalid lockedSOV Address."); - lockedSOV = _lockedSOV; - } - - /** - * @notice Sets unlocked immediately percent. - * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. - * @dev @dev 10000 is 100% - */ - function setUnlockedImmediatelyPercent(uint256 _unlockedImmediatelyPercent) external onlyAuthorized { - require(_unlockedImmediatelyPercent < 10000, "Unlocked immediately percent has to be less than 10000."); - unlockedImmediatelyPercent = _unlockedImmediatelyPercent; - } - - /** - * @notice sets wrapper proxy contract - * @dev can be set to zero address to remove wrapper - */ - function setWrapper(address _wrapper) external onlyAuthorized { - wrapper = _wrapper; - } - - /** - * @notice stops mining by setting end block - */ - function stopMining() public onlyAuthorized { - require(endBlock == 0, "Already stopped"); - - endBlock = block.number; - } - - // TODO: this should only be used by the LiquidityMiningV2 contract?? - /// @notice This function starts the migration process which involves two steps: - /// 1. Starts the migration grace period when people can withdraw or claim for rewards - /// 2. Stops mining, i.e., no more rewards are paid - function startMigrationGracePeriod() external onlyAuthorized onlyBeforeMigrationGracePeriod { - migrationGracePeriodState = MigrationGracePeriodStates.Started; - stopMining(); - } - - // TODO: this should only be used by the LiquidityMiningV2 contract?? - /// @notice This function finishes the migration process disabling further withdrawals and claims - /// @dev migration grace period should have started before this function is called. - function finishMigrationGracePeriod() external onlyAuthorized onlyMigrationGracePeriodStarted { - migrationGracePeriodState = MigrationGracePeriodStates.Finished; - } - - /** - * @notice Transfers SOV tokens to given address. - * Owner use this function to withdraw SOV from LM contract - * into another account. - * @param _receiver The address of the SOV receiver. - * @param _amount The amount to be transferred. - * */ - function transferSOV(address _receiver, uint256 _amount) public onlyAuthorized { - require(_receiver != address(0), "Receiver address invalid"); - require(_amount != 0, "Amount invalid"); - - /// @dev Do not transfer more SOV than available. - uint256 SOVBal = SOV.balanceOf(address(this)); - if (_amount > SOVBal) { - _amount = SOVBal; - } - - /// @dev The actual transfer. - require(SOV.transfer(_receiver, _amount), "Transfer failed"); - - /// @dev Event log. - emit SOVTransferred(_receiver, _amount); - } - - /** - * @notice Get the missed SOV balance of LM contract. - * - * @return The amount of SOV tokens according to totalUsersBalance - * in excess of actual SOV balance of the LM contract. - * */ - function getMissedBalance() external view returns (uint256) { - uint256 balance = SOV.balanceOf(address(this)); - return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); - } - - /** - * @notice adds a new lp to the pool. Can only be called by the owner or an admin - * @param _poolToken the address of pool token - * @param _allocationPoint the allocation point (weight) for the given pool - * @param _withUpdate the flag whether we need to update all pools - */ - function add( - address _poolToken, - uint96 _allocationPoint, - bool _withUpdate - ) external onlyAuthorized { - require(_allocationPoint > 0, "Invalid allocation point"); - require(_poolToken != address(0), "Invalid token address"); - require(poolIdList[_poolToken] == 0, "Token already added"); - - if (_withUpdate) { - updateAllPools(); - } - - uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; - totalAllocationPoint = totalAllocationPoint.add(_allocationPoint); - - poolInfoList.push( - PoolInfo({ - poolToken: IERC20(_poolToken), - allocationPoint: _allocationPoint, - lastRewardBlock: lastRewardBlock, - accumulatedRewardPerShare: 0 - }) - ); - //indexing starts from 1 in order to check whether token was already added - poolIdList[_poolToken] = poolInfoList.length; - - emit PoolTokenAdded(msg.sender, _poolToken, _allocationPoint); - } - - /** - * @notice updates the given pool's reward tokens allocation point - * @param _poolToken the address of pool token - * @param _allocationPoint the allocation point (weight) for the given pool - * @param _updateAllFlag the flag whether we need to update all pools - */ - function update( - address _poolToken, - uint96 _allocationPoint, - bool _updateAllFlag - ) external onlyAuthorized { - if (_updateAllFlag) { - updateAllPools(); - } else { - updatePool(_poolToken); - } - _updateToken(_poolToken, _allocationPoint); - } - - function _updateToken(address _poolToken, uint96 _allocationPoint) internal { - uint256 poolId = _getPoolId(_poolToken); - - uint256 previousAllocationPoint = poolInfoList[poolId].allocationPoint; - totalAllocationPoint = totalAllocationPoint.sub(previousAllocationPoint).add(_allocationPoint); - poolInfoList[poolId].allocationPoint = _allocationPoint; - - emit PoolTokenUpdated(msg.sender, _poolToken, _allocationPoint, previousAllocationPoint); - } - - /** - * @notice updates the given pools' reward tokens allocation points - * @param _poolTokens array of addresses of pool tokens - * @param _allocationPoints array of allocation points (weight) for the given pools - * @param _updateAllFlag the flag whether we need to update all pools - */ - function updateTokens( - address[] calldata _poolTokens, - uint96[] calldata _allocationPoints, - bool _updateAllFlag - ) external onlyAuthorized { - require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); - - if (_updateAllFlag) { - updateAllPools(); - } - uint256 length = _poolTokens.length; - for (uint256 i = 0; i < length; i++) { - if (!_updateAllFlag) { - updatePool(_poolTokens[i]); - } - _updateToken(_poolTokens[i], _allocationPoints[i]); - } - } - - /** - * @notice returns reward multiplier over the given _from to _to block - * @param _from the first block for a calculation - * @param _to the last block for a calculation - */ - function _getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) internal view returns (uint256) { - if (_from < startBlock) { - _from = startBlock; - } - if (endBlock > 0 && _to > endBlock) { - _to = endBlock; - } - if (_to <= bonusEndBlock) { - return _to.sub(_from).mul(BONUS_BLOCK_MULTIPLIER); - } else if (_from >= bonusEndBlock) { - return _to.sub(_from); - } else { - return bonusEndBlock.sub(_from).mul(BONUS_BLOCK_MULTIPLIER).add(_to.sub(bonusEndBlock)); - } - } - - function _getUserAccumulatedReward(uint256 _poolId, address _user) internal view returns (uint256) { - PoolInfo storage pool = poolInfoList[_poolId]; - UserInfo storage user = userInfoMap[_poolId][_user]; - - uint256 accumulatedRewardPerShare = pool.accumulatedRewardPerShare; - uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); - if (block.number > pool.lastRewardBlock && poolTokenBalance != 0) { - (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); - accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); - } - return user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); - } - - /** - * @notice returns accumulated reward - * @param _poolToken the address of pool token - * @param _user the user address - */ - function getUserAccumulatedReward(address _poolToken, address _user) external view returns (uint256) { - uint256 poolId = _getPoolId(_poolToken); - return _getUserAccumulatedReward(poolId, _user); - } - - /** - * @notice returns estimated reward - * @param _poolToken the address of pool token - * @param _amount the amount of tokens to be deposited - * @param _duration the duration of liquidity providing in seconds - */ - function getEstimatedReward( - address _poolToken, - uint256 _amount, - uint256 _duration - ) external view returns (uint256) { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - uint256 start = block.number; - uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward(pool, _amount, start, end); - return _amount.mul(accumulatedRewardPerShare).div(PRECISION); - } - - /** - * @notice Updates reward variables for all pools. - * @dev Be careful of gas spending! - */ - function updateAllPools() public onlyBeforeMigrationGracePeriodFinished { - uint256 length = poolInfoList.length; - for (uint256 i = 0; i < length; i++) { - _updatePool(i); - } - } - - /** - * @notice Updates reward variables of the given pool to be up-to-date - * @param _poolToken the address of pool token - */ - function updatePool(address _poolToken) public onlyBeforeMigrationGracePeriodFinished { - uint256 poolId = _getPoolId(_poolToken); - _updatePool(poolId); - } - - function _updatePool(uint256 _poolId) internal { - PoolInfo storage pool = poolInfoList[_poolId]; - - //this pool has been updated recently - if (block.number <= pool.lastRewardBlock) { - return; - } - - uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); - if (poolTokenBalance == 0) { - pool.lastRewardBlock = block.number; - return; - } - - (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); - pool.accumulatedRewardPerShare = pool.accumulatedRewardPerShare.add(accumulatedRewardPerShare_); - pool.lastRewardBlock = block.number; - - totalUsersBalance = totalUsersBalance.add(accumulatedReward_); - } - - function _getPoolAccumulatedReward(PoolInfo storage _pool) internal view returns (uint256, uint256) { - return _getPoolAccumulatedReward(_pool, 0, _pool.lastRewardBlock, block.number); - } - - function _getPoolAccumulatedReward( - PoolInfo storage _pool, - uint256 _additionalAmount, - uint256 _startBlock, - uint256 _endBlock - ) internal view returns (uint256, uint256) { - uint256 passedBlocks = _getPassedBlocksWithBonusMultiplier(_startBlock, _endBlock); - uint256 accumulatedReward = passedBlocks.mul(rewardTokensPerBlock).mul(_pool.allocationPoint).div(totalAllocationPoint); - - uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); - poolTokenBalance = poolTokenBalance.add(_additionalAmount); - uint256 accumulatedRewardPerShare = accumulatedReward.mul(PRECISION).div(poolTokenBalance); - return (accumulatedReward, accumulatedRewardPerShare); - } - - /** - * @notice deposits pool tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the address of user, tokens will be deposited to it or to msg.sender - */ - function deposit( - address _poolToken, - uint256 _amount, - address _user - ) external onlyBeforeMigrationGracePeriod { - _deposit(_poolToken, _amount, _user, false); - } - - /** - * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit - * @dev only callable by the pool which issues the tokens - * @param _user the user address - * @param _amount the minted amount - */ - function onTokensDeposited(address _user, uint256 _amount) external onlyBeforeMigrationGracePeriod { - //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert - _deposit(msg.sender, _amount, _user, true); - } - - /** - * @notice internal function for depositing pool tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the address of user, tokens will be deposited to it - * @param alreadyTransferred true if the pool tokens have already been transferred - */ - function _deposit( - address _poolToken, - uint256 _amount, - address _user, - bool alreadyTransferred - ) internal { - require(poolIdList[_poolToken] != 0, "Pool token not found"); - address userAddress = _user != address(0) ? _user : msg.sender; - - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][userAddress]; - - _updatePool(poolId); - //sends reward directly to the user - _updateReward(pool, user); - - if (_amount > 0) { - //receives pool tokens from msg.sender, it can be user or WrapperProxy contract - if (!alreadyTransferred) pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); - user.amount = user.amount.add(_amount); - } - _updateRewardDebt(pool, user); - emit Deposit(userAddress, _poolToken, _amount); - } - - /** - * @notice transfers reward tokens - * @param _poolToken the address of pool token - * @param _user the address of user to claim reward from (can be passed only by wrapper contract) - */ - function claimReward(address _poolToken, address _user) external onlyBeforeMigrationGracePeriodFinished { - address userAddress = _getUserAddress(_user); - - uint256 poolId = _getPoolId(_poolToken); - _claimReward(poolId, userAddress, true); - } - - function _claimReward( - uint256 _poolId, - address _userAddress, - bool _isStakingTokens - ) internal { - PoolInfo storage pool = poolInfoList[_poolId]; - UserInfo storage user = userInfoMap[_poolId][_userAddress]; - - _updatePool(_poolId); - _updateReward(pool, user); - _transferReward(address(pool.poolToken), user, _userAddress, _isStakingTokens, true); - _updateRewardDebt(pool, user); - } - - /** - * @notice transfers reward tokens from all pools - * @param _user the address of user to claim reward from (can be passed only by wrapper contract) - */ - function claimRewardFromAllPools(address _user) external onlyBeforeMigrationGracePeriodFinished { - address userAddress = _getUserAddress(_user); - - uint256 length = poolInfoList.length; - for (uint256 i = 0; i < length; i++) { - uint256 poolId = i; - _claimReward(poolId, userAddress, false); - } - lockedSOV.withdrawAndStakeTokensFrom(userAddress); - } - - /** - * @notice withdraws pool tokens and transfers reward tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) - */ - function withdraw( - address _poolToken, - uint256 _amount, - address _user - ) external onlyBeforeMigrationGracePeriodFinished { - require(poolIdList[_poolToken] != 0, "Pool token not found"); - address userAddress = _getUserAddress(_user); - - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][userAddress]; - require(user.amount >= _amount, "Not enough balance"); - - _updatePool(poolId); - _updateReward(pool, user); - _transferReward(_poolToken, user, userAddress, false, false); - - user.amount = user.amount.sub(_amount); - - //msg.sender is wrapper -> send to wrapper - if (msg.sender == wrapper) { - pool.poolToken.safeTransfer(address(msg.sender), _amount); - } - //msg.sender is user or pool token (lending pool) -> send to user - else { - pool.poolToken.safeTransfer(userAddress, _amount); - } - - _updateRewardDebt(pool, user); - emit Withdraw(userAddress, _poolToken, _amount); - } - - function _getUserAddress(address _user) internal view returns (address) { - address userAddress = msg.sender; - if (_user != address(0)) { - //only wrapper can pass _user parameter - require(msg.sender == wrapper || poolIdList[msg.sender] != 0, "only wrapper or pools may withdraw for a user"); - userAddress = _user; - } - return userAddress; - } - - function _updateReward(PoolInfo storage pool, UserInfo storage user) internal { - //update user accumulated reward - if (user.amount > 0) { - //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); - user.accumulatedReward = user.accumulatedReward.add(accumulatedReward); - } - } - - function _updateRewardDebt(PoolInfo storage pool, UserInfo storage user) internal { - //reward accumulated before amount update (should be subtracted during next reward calculation) - user.rewardDebt = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION); - } - - /** - * @notice Send reward in SOV to the lockedSOV vault. - * @param _user The user info, to get its reward share. - * @param _userAddress The address of the user, to send SOV in its behalf. - * @param _isStakingTokens The flag whether we need to stake tokens - * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough - */ - function _transferReward( - address _poolToken, - UserInfo storage _user, - address _userAddress, - bool _isStakingTokens, - bool _isCheckingBalance - ) internal { - uint256 userAccumulatedReward = _user.accumulatedReward; - - /// @dev Transfer if enough SOV balance on this LM contract. - uint256 balance = SOV.balanceOf(address(this)); - if (balance >= userAccumulatedReward) { - totalUsersBalance = totalUsersBalance.sub(userAccumulatedReward); - _user.accumulatedReward = 0; - - /// @dev Instead of transferring the reward to the LP (user), - /// deposit it into lockedSOV vault contract, but first - /// SOV deposit must be approved to move the SOV tokens - /// from this LM contract into the lockedSOV vault. - require(SOV.approve(address(lockedSOV), userAccumulatedReward), "Approve failed"); - lockedSOV.deposit(_userAddress, userAccumulatedReward, unlockedImmediatelyPercent); - - if (_isStakingTokens) { - lockedSOV.withdrawAndStakeTokensFrom(_userAddress); - } - - /// @dev Event log. - emit RewardClaimed(_userAddress, _poolToken, userAccumulatedReward); - } else { - require(!_isCheckingBalance, "Claiming reward failed"); - } - } - - /** - * @notice withdraws pool tokens without transferring reward tokens - * @param _poolToken the address of pool token - * @dev EMERGENCY ONLY - */ - function emergencyWithdraw(address _poolToken) external onlyBeforeMigrationGracePeriodFinished { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][msg.sender]; - - _updatePool(poolId); - _updateReward(pool, user); - - totalUsersBalance = totalUsersBalance.sub(user.accumulatedReward); - uint256 userAmount = user.amount; - uint256 userAccumulatedReward = user.accumulatedReward; - user.amount = 0; - user.rewardDebt = 0; - user.accumulatedReward = 0; - pool.poolToken.safeTransfer(address(msg.sender), userAmount); - - _updateRewardDebt(pool, user); - - emit EmergencyWithdraw(msg.sender, _poolToken, userAmount, userAccumulatedReward); - } - - /** - * @notice returns pool id - * @param _poolToken the address of pool token - */ - function getPoolId(address _poolToken) external view returns (uint256) { - return _getPoolId(_poolToken); - } - - function _getPoolId(address _poolToken) internal view returns (uint256) { - uint256 poolId = poolIdList[_poolToken]; - require(poolId > 0, "Pool token not found"); - return poolId - 1; - } - - /** - * @notice returns count of pool tokens - */ - function getPoolLength() external view returns (uint256) { - return poolInfoList.length; - } - - /** - * @notice returns list of pool token's info - */ - function getPoolInfoList() external view returns (PoolInfo[] memory) { - return poolInfoList; - } - - /** - * @notice returns pool info for the given token - * @param _poolToken the address of pool token - */ - function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { - uint256 poolId = _getPoolId(_poolToken); - return poolInfoList[poolId]; - } - - /** - * @notice returns list of [amount, accumulatedReward] for the given user for each pool token - * @param _user the address of the user - */ - function getUserBalanceList(address _user) external view returns (uint256[2][] memory) { - uint256 length = poolInfoList.length; - uint256[2][] memory userBalanceList = new uint256[2][](length); - for (uint256 i = 0; i < length; i++) { - userBalanceList[i][0] = userInfoMap[i][_user].amount; - userBalanceList[i][1] = _getUserAccumulatedReward(i, _user); - } - return userBalanceList; - } - - /** - * @notice returns UserInfo for the given pool and user - * @param _poolToken the address of pool token - * @param _user the address of the user - */ - function getUserInfo(address _poolToken, address _user) public view returns (UserInfo memory) { - uint256 poolId = _getPoolId(_poolToken); - return userInfoMap[poolId][_user]; - } - - /** - * @notice returns list of UserInfo for the given user for each pool token - * @param _user the address of the user - */ - function getUserInfoList(address _user) external view returns (UserInfo[] memory) { - uint256 length = poolInfoList.length; - UserInfo[] memory userInfoList = new UserInfo[](length); - for (uint256 i = 0; i < length; i++) { - userInfoList[i] = userInfoMap[i][_user]; - } - return userInfoList; - } - - /** - * @notice returns accumulated reward for the given user for each pool token - * @param _user the address of the user - */ - function getUserAccumulatedRewardList(address _user) external view returns (uint256[] memory) { - uint256 length = poolInfoList.length; - uint256[] memory rewardList = new uint256[](length); - for (uint256 i = 0; i < length; i++) { - rewardList[i] = _getUserAccumulatedReward(i, _user); - } - return rewardList; - } - - /** - * @notice returns the pool token balance a user has on the contract - * @param _poolToken the address of pool token - * @param _user the address of the user - */ - function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256) { - UserInfo memory ui = getUserInfo(_poolToken, _user); - return ui.amount; - } - - /** - * @notice returns arrays with all the pools on the contract - */ - function getPoolInfoListArray() - external - view - returns ( - address[] memory _poolToken, - uint96[] memory _allocationPoints, - uint256[] memory _lastRewardBlock, - uint256[] memory _accumulatedRewardPerShare - ) - { - uint256 length = poolInfoList.length; - _poolToken = new address[](length); - _allocationPoints = new uint96[](length); - _lastRewardBlock = new uint256[](length); - _accumulatedRewardPerShare = new uint256[](length); - for (uint256 i = 0; i < length; i++) { - _poolToken[i] = address(poolInfoList[i].poolToken); - _allocationPoints[i] = poolInfoList[i].allocationPoint; - _lastRewardBlock[i] = poolInfoList[i].lastRewardBlock; - _accumulatedRewardPerShare[i] = poolInfoList[i].accumulatedRewardPerShare; - } - return (_poolToken, _allocationPoints, _lastRewardBlock, _accumulatedRewardPerShare); - } - - /** - * @notice returns all pools that a user has on the contract, the poolId it's the index of arrays - * @param _user the address of the user - */ - function getUserInfoListArray(address _user) - external - view - returns ( - uint256[] memory _amount, - uint256[] memory _rewardDebt, - uint256[] memory _accumulatedReward - ) - { - uint256 length = poolInfoList.length; - _amount = new uint256[](length); - _rewardDebt = new uint256[](length); - _accumulatedReward = new uint256[](length); - for (uint256 i = 0; i < length; i++) { - _amount[i] = userInfoMap[i][_user].amount; - _rewardDebt[i] = userInfoMap[i][_user].rewardDebt; - _accumulatedReward[i] = userInfoMap[i][_user].accumulatedReward; - } - return (_amount, _rewardDebt, _accumulatedReward); - } - - /** - * @notice send all funds from this contract to LiquidityMiningV2 - */ - function migrateFunds() external onlyAuthorized onlyAfterMigrationFinished { - require(liquidityMiningV2 != address(0), "Address not initialized"); - uint256 SOVBalance = SOV.balanceOf(address(this)); - transferSOV(liquidityMiningV2, SOVBalance); - uint256 length = poolInfoList.length; - for (uint256 i = 0; i < length; i++) { - IERC20 poolToken = poolInfoList[i].poolToken; - uint256 balancePoolToken = poolToken.balanceOf(address(this)); - poolToken.safeTransfer(liquidityMiningV2, balancePoolToken); - } - } - - /** - * @notice return reward token total users balance - */ - function getTotalUsersBalance() external view returns (uint256) { - return totalUsersBalance; - } - - /** - * @notice return reward token start block - */ - function getStartBlock() external view returns (uint256) { - return startBlock; - } + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* Constants */ + + uint256 public constant PRECISION = 1e12; + // Bonus multiplier for early liquidity providers. + // During bonus period each passed block will be calculated like N passed blocks, where N = BONUS_MULTIPLIER + uint256 public constant BONUS_BLOCK_MULTIPLIER = 10; + + uint256 public constant SECONDS_PER_BLOCK = 30; + + /* Events */ + + event SOVTransferred(address indexed receiver, uint256 amount); + event PoolTokenAdded(address indexed user, address indexed poolToken, uint256 allocationPoint); + event PoolTokenUpdated( + address indexed user, + address indexed poolToken, + uint256 newAllocationPoint, + uint256 oldAllocationPoint + ); + event Deposit(address indexed user, address indexed poolToken, uint256 amount); + event RewardClaimed(address indexed user, address indexed poolToken, uint256 amount); + event Withdraw(address indexed user, address indexed poolToken, uint256 amount); + event EmergencyWithdraw( + address indexed user, + address indexed poolToken, + uint256 amount, + uint256 accumulatedReward + ); + + /* Modifiers */ + modifier onlyBeforeMigrationGracePeriod() { + require( + migrationGracePeriodState < MigrationGracePeriodStates.Started, + "Forbidden: migration already started" + ); + _; + } + + modifier onlyMigrationGracePeriodStarted() { + require( + migrationGracePeriodState == MigrationGracePeriodStates.Started, + "Forbidden: Migration hasn't started yet or already finished" + ); + _; + } + + modifier onlyBeforeMigrationGracePeriodFinished() { + require( + migrationGracePeriodState < MigrationGracePeriodStates.Finished, + "Forbidden: contract deprecated" + ); + _; + } + + modifier onlyAfterMigrationFinished() { + require( + migrationGracePeriodState == MigrationGracePeriodStates.Finished, + "Forbidden: migration is not over yet" + ); + _; + } + + /* Functions */ + + /** + * @notice Initialize mining. + * + * @param _liquidityMiningV2 The LiquidityMiningV2 contract address + */ + function initialize(address _liquidityMiningV2) external onlyAuthorized { + /// @dev Non-idempotent function. Must be called just once. + require(_liquidityMiningV2 != address(0), "Invalid address"); + require(liquidityMiningV2 == address(0), "Already initialized"); + liquidityMiningV2 = _liquidityMiningV2; + } + + /** + * @notice Sets lockedSOV contract. + * @param _lockedSOV The contract instance address of the lockedSOV vault. + */ + function setLockedSOV(ILockedSOV _lockedSOV) external onlyAuthorized { + require(address(_lockedSOV) != address(0), "Invalid lockedSOV Address."); + lockedSOV = _lockedSOV; + } + + /** + * @notice Sets unlocked immediately percent. + * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. + * @dev @dev 10000 is 100% + */ + function setUnlockedImmediatelyPercent(uint256 _unlockedImmediatelyPercent) + external + onlyAuthorized + { + require( + _unlockedImmediatelyPercent < 10000, + "Unlocked immediately percent has to be less than 10000." + ); + unlockedImmediatelyPercent = _unlockedImmediatelyPercent; + } + + /** + * @notice sets wrapper proxy contract + * @dev can be set to zero address to remove wrapper + */ + function setWrapper(address _wrapper) external onlyAuthorized { + wrapper = _wrapper; + } + + /** + * @notice stops mining by setting end block + */ + function stopMining() public onlyAuthorized { + require(endBlock == 0, "Already stopped"); + + endBlock = block.number; + } + + // TODO: this should only be used by the LiquidityMiningV2 contract?? + /// @notice This function starts the migration process which involves two steps: + /// 1. Starts the migration grace period when people can withdraw or claim for rewards + /// 2. Stops mining, i.e., no more rewards are paid + function startMigrationGracePeriod() external onlyAuthorized onlyBeforeMigrationGracePeriod { + migrationGracePeriodState = MigrationGracePeriodStates.Started; + stopMining(); + } + + // TODO: this should only be used by the LiquidityMiningV2 contract?? + /// @notice This function finishes the migration process disabling further withdrawals and claims + /// @dev migration grace period should have started before this function is called. + function finishMigrationGracePeriod() external onlyAuthorized onlyMigrationGracePeriodStarted { + migrationGracePeriodState = MigrationGracePeriodStates.Finished; + } + + /** + * @notice Transfers SOV tokens to given address. + * Owner use this function to withdraw SOV from LM contract + * into another account. + * @param _receiver The address of the SOV receiver. + * @param _amount The amount to be transferred. + * */ + function transferSOV(address _receiver, uint256 _amount) public onlyAuthorized { + require(_receiver != address(0), "Receiver address invalid"); + require(_amount != 0, "Amount invalid"); + + /// @dev Do not transfer more SOV than available. + uint256 SOVBal = SOV.balanceOf(address(this)); + if (_amount > SOVBal) { + _amount = SOVBal; + } + + /// @dev The actual transfer. + require(SOV.transfer(_receiver, _amount), "Transfer failed"); + + /// @dev Event log. + emit SOVTransferred(_receiver, _amount); + } + + /** + * @notice Get the missed SOV balance of LM contract. + * + * @return The amount of SOV tokens according to totalUsersBalance + * in excess of actual SOV balance of the LM contract. + * */ + function getMissedBalance() external view returns (uint256) { + uint256 balance = SOV.balanceOf(address(this)); + return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); + } + + /** + * @notice adds a new lp to the pool. Can only be called by the owner or an admin + * @param _poolToken the address of pool token + * @param _allocationPoint the allocation point (weight) for the given pool + * @param _withUpdate the flag whether we need to update all pools + */ + function add( + address _poolToken, + uint96 _allocationPoint, + bool _withUpdate + ) external onlyAuthorized { + require(_allocationPoint > 0, "Invalid allocation point"); + require(_poolToken != address(0), "Invalid token address"); + require(poolIdList[_poolToken] == 0, "Token already added"); + + if (_withUpdate) { + updateAllPools(); + } + + uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; + totalAllocationPoint = totalAllocationPoint.add(_allocationPoint); + + poolInfoList.push( + PoolInfo({ + poolToken: IERC20(_poolToken), + allocationPoint: _allocationPoint, + lastRewardBlock: lastRewardBlock, + accumulatedRewardPerShare: 0 + }) + ); + //indexing starts from 1 in order to check whether token was already added + poolIdList[_poolToken] = poolInfoList.length; + + emit PoolTokenAdded(msg.sender, _poolToken, _allocationPoint); + } + + /** + * @notice updates the given pool's reward tokens allocation point + * @param _poolToken the address of pool token + * @param _allocationPoint the allocation point (weight) for the given pool + * @param _updateAllFlag the flag whether we need to update all pools + */ + function update( + address _poolToken, + uint96 _allocationPoint, + bool _updateAllFlag + ) external onlyAuthorized { + if (_updateAllFlag) { + updateAllPools(); + } else { + updatePool(_poolToken); + } + _updateToken(_poolToken, _allocationPoint); + } + + function _updateToken(address _poolToken, uint96 _allocationPoint) internal { + uint256 poolId = _getPoolId(_poolToken); + + uint256 previousAllocationPoint = poolInfoList[poolId].allocationPoint; + totalAllocationPoint = totalAllocationPoint.sub(previousAllocationPoint).add( + _allocationPoint + ); + poolInfoList[poolId].allocationPoint = _allocationPoint; + + emit PoolTokenUpdated(msg.sender, _poolToken, _allocationPoint, previousAllocationPoint); + } + + /** + * @notice updates the given pools' reward tokens allocation points + * @param _poolTokens array of addresses of pool tokens + * @param _allocationPoints array of allocation points (weight) for the given pools + * @param _updateAllFlag the flag whether we need to update all pools + */ + function updateTokens( + address[] calldata _poolTokens, + uint96[] calldata _allocationPoints, + bool _updateAllFlag + ) external onlyAuthorized { + require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); + + if (_updateAllFlag) { + updateAllPools(); + } + uint256 length = _poolTokens.length; + for (uint256 i = 0; i < length; i++) { + if (!_updateAllFlag) { + updatePool(_poolTokens[i]); + } + _updateToken(_poolTokens[i], _allocationPoints[i]); + } + } + + /** + * @notice returns reward multiplier over the given _from to _to block + * @param _from the first block for a calculation + * @param _to the last block for a calculation + */ + function _getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) + internal + view + returns (uint256) + { + if (_from < startBlock) { + _from = startBlock; + } + if (endBlock > 0 && _to > endBlock) { + _to = endBlock; + } + if (_to <= bonusEndBlock) { + return _to.sub(_from).mul(BONUS_BLOCK_MULTIPLIER); + } else if (_from >= bonusEndBlock) { + return _to.sub(_from); + } else { + return + bonusEndBlock.sub(_from).mul(BONUS_BLOCK_MULTIPLIER).add(_to.sub(bonusEndBlock)); + } + } + + function _getUserAccumulatedReward(uint256 _poolId, address _user) + internal + view + returns (uint256) + { + PoolInfo storage pool = poolInfoList[_poolId]; + UserInfo storage user = userInfoMap[_poolId][_user]; + + uint256 accumulatedRewardPerShare = pool.accumulatedRewardPerShare; + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && poolTokenBalance != 0) { + (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); + accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); + } + return user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); + } + + /** + * @notice returns accumulated reward + * @param _poolToken the address of pool token + * @param _user the user address + */ + function getUserAccumulatedReward(address _poolToken, address _user) + external + view + returns (uint256) + { + uint256 poolId = _getPoolId(_poolToken); + return _getUserAccumulatedReward(poolId, _user); + } + + /** + * @notice returns estimated reward + * @param _poolToken the address of pool token + * @param _amount the amount of tokens to be deposited + * @param _duration the duration of liquidity providing in seconds + */ + function getEstimatedReward( + address _poolToken, + uint256 _amount, + uint256 _duration + ) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + uint256 start = block.number; + uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); + (, uint256 accumulatedRewardPerShare) = + _getPoolAccumulatedReward(pool, _amount, start, end); + return _amount.mul(accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Updates reward variables for all pools. + * @dev Be careful of gas spending! + */ + function updateAllPools() public onlyBeforeMigrationGracePeriodFinished { + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + _updatePool(i); + } + } + + /** + * @notice Updates reward variables of the given pool to be up-to-date + * @param _poolToken the address of pool token + */ + function updatePool(address _poolToken) public onlyBeforeMigrationGracePeriodFinished { + uint256 poolId = _getPoolId(_poolToken); + _updatePool(poolId); + } + + function _updatePool(uint256 _poolId) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + + //this pool has been updated recently + if (block.number <= pool.lastRewardBlock) { + return; + } + + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (poolTokenBalance == 0) { + pool.lastRewardBlock = block.number; + return; + } + + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool); + pool.accumulatedRewardPerShare = pool.accumulatedRewardPerShare.add( + accumulatedRewardPerShare_ + ); + pool.lastRewardBlock = block.number; + + totalUsersBalance = totalUsersBalance.add(accumulatedReward_); + } + + function _getPoolAccumulatedReward(PoolInfo storage _pool) + internal + view + returns (uint256, uint256) + { + return _getPoolAccumulatedReward(_pool, 0, _pool.lastRewardBlock, block.number); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + uint256 _additionalAmount, + uint256 _startBlock, + uint256 _endBlock + ) internal view returns (uint256, uint256) { + uint256 passedBlocks = _getPassedBlocksWithBonusMultiplier(_startBlock, _endBlock); + uint256 accumulatedReward = + passedBlocks.mul(rewardTokensPerBlock).mul(_pool.allocationPoint).div( + totalAllocationPoint + ); + + uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); + poolTokenBalance = poolTokenBalance.add(_additionalAmount); + uint256 accumulatedRewardPerShare = accumulatedReward.mul(PRECISION).div(poolTokenBalance); + return (accumulatedReward, accumulatedRewardPerShare); + } + + /** + * @notice deposits pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it or to msg.sender + */ + function deposit( + address _poolToken, + uint256 _amount, + address _user + ) external onlyBeforeMigrationGracePeriod { + _deposit(_poolToken, _amount, _user, false); + } + + /** + * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit + * @dev only callable by the pool which issues the tokens + * @param _user the user address + * @param _amount the minted amount + */ + function onTokensDeposited(address _user, uint256 _amount) + external + onlyBeforeMigrationGracePeriod + { + //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert + _deposit(msg.sender, _amount, _user, true); + } + + /** + * @notice internal function for depositing pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it + * @param alreadyTransferred true if the pool tokens have already been transferred + */ + function _deposit( + address _poolToken, + uint256 _amount, + address _user, + bool alreadyTransferred + ) internal { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _user != address(0) ? _user : msg.sender; + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + + _updatePool(poolId); + //sends reward directly to the user + _updateReward(pool, user); + + if (_amount > 0) { + //receives pool tokens from msg.sender, it can be user or WrapperProxy contract + if (!alreadyTransferred) + pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); + user.amount = user.amount.add(_amount); + } + _updateRewardDebt(pool, user); + emit Deposit(userAddress, _poolToken, _amount); + } + + /** + * @notice transfers reward tokens + * @param _poolToken the address of pool token + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimReward(address _poolToken, address _user) + external + onlyBeforeMigrationGracePeriodFinished + { + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + _claimReward(poolId, userAddress, true); + } + + function _claimReward( + uint256 _poolId, + address _userAddress, + bool _isStakingTokens + ) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + UserInfo storage user = userInfoMap[_poolId][_userAddress]; + + _updatePool(_poolId); + _updateReward(pool, user); + _transferReward(address(pool.poolToken), user, _userAddress, _isStakingTokens, true); + _updateRewardDebt(pool, user); + } + + /** + * @notice transfers reward tokens from all pools + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimRewardFromAllPools(address _user) + external + onlyBeforeMigrationGracePeriodFinished + { + address userAddress = _getUserAddress(_user); + + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + uint256 poolId = i; + _claimReward(poolId, userAddress, false); + } + lockedSOV.withdrawAndStakeTokensFrom(userAddress); + } + + /** + * @notice withdraws pool tokens and transfers reward tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) + */ + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external onlyBeforeMigrationGracePeriodFinished { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + require(user.amount >= _amount, "Not enough balance"); + + _updatePool(poolId); + _updateReward(pool, user); + _transferReward(_poolToken, user, userAddress, false, false); + + user.amount = user.amount.sub(_amount); + + //msg.sender is wrapper -> send to wrapper + if (msg.sender == wrapper) { + pool.poolToken.safeTransfer(address(msg.sender), _amount); + } + //msg.sender is user or pool token (lending pool) -> send to user + else { + pool.poolToken.safeTransfer(userAddress, _amount); + } + + _updateRewardDebt(pool, user); + emit Withdraw(userAddress, _poolToken, _amount); + } + + function _getUserAddress(address _user) internal view returns (address) { + address userAddress = msg.sender; + if (_user != address(0)) { + //only wrapper can pass _user parameter + require( + msg.sender == wrapper || poolIdList[msg.sender] != 0, + "only wrapper or pools may withdraw for a user" + ); + userAddress = _user; + } + return userAddress; + } + + function _updateReward(PoolInfo storage pool, UserInfo storage user) internal { + //update user accumulated reward + if (user.amount > 0) { + //add reward for the previous amount of deposited tokens + uint256 accumulatedReward = + user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION).sub( + user.rewardDebt + ); + user.accumulatedReward = user.accumulatedReward.add(accumulatedReward); + } + } + + function _updateRewardDebt(PoolInfo storage pool, UserInfo storage user) internal { + //reward accumulated before amount update (should be subtracted during next reward calculation) + user.rewardDebt = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Send reward in SOV to the lockedSOV vault. + * @param _user The user info, to get its reward share. + * @param _userAddress The address of the user, to send SOV in its behalf. + * @param _isStakingTokens The flag whether we need to stake tokens + * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough + */ + function _transferReward( + address _poolToken, + UserInfo storage _user, + address _userAddress, + bool _isStakingTokens, + bool _isCheckingBalance + ) internal { + uint256 userAccumulatedReward = _user.accumulatedReward; + + /// @dev Transfer if enough SOV balance on this LM contract. + uint256 balance = SOV.balanceOf(address(this)); + if (balance >= userAccumulatedReward) { + totalUsersBalance = totalUsersBalance.sub(userAccumulatedReward); + _user.accumulatedReward = 0; + + /// @dev Instead of transferring the reward to the LP (user), + /// deposit it into lockedSOV vault contract, but first + /// SOV deposit must be approved to move the SOV tokens + /// from this LM contract into the lockedSOV vault. + require(SOV.approve(address(lockedSOV), userAccumulatedReward), "Approve failed"); + lockedSOV.deposit(_userAddress, userAccumulatedReward, unlockedImmediatelyPercent); + + if (_isStakingTokens) { + lockedSOV.withdrawAndStakeTokensFrom(_userAddress); + } + + /// @dev Event log. + emit RewardClaimed(_userAddress, _poolToken, userAccumulatedReward); + } else { + require(!_isCheckingBalance, "Claiming reward failed"); + } + } + + /** + * @notice withdraws pool tokens without transferring reward tokens + * @param _poolToken the address of pool token + * @dev EMERGENCY ONLY + */ + function emergencyWithdraw(address _poolToken) + external + onlyBeforeMigrationGracePeriodFinished + { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][msg.sender]; + + _updatePool(poolId); + _updateReward(pool, user); + + totalUsersBalance = totalUsersBalance.sub(user.accumulatedReward); + uint256 userAmount = user.amount; + uint256 userAccumulatedReward = user.accumulatedReward; + user.amount = 0; + user.rewardDebt = 0; + user.accumulatedReward = 0; + pool.poolToken.safeTransfer(address(msg.sender), userAmount); + + _updateRewardDebt(pool, user); + + emit EmergencyWithdraw(msg.sender, _poolToken, userAmount, userAccumulatedReward); + } + + /** + * @notice returns pool id + * @param _poolToken the address of pool token + */ + function getPoolId(address _poolToken) external view returns (uint256) { + return _getPoolId(_poolToken); + } + + function _getPoolId(address _poolToken) internal view returns (uint256) { + uint256 poolId = poolIdList[_poolToken]; + require(poolId > 0, "Pool token not found"); + return poolId - 1; + } + + /** + * @notice returns count of pool tokens + */ + function getPoolLength() external view returns (uint256) { + return poolInfoList.length; + } + + /** + * @notice returns list of pool token's info + */ + function getPoolInfoList() external view returns (PoolInfo[] memory) { + return poolInfoList; + } + + /** + * @notice returns pool info for the given token + * @param _poolToken the address of pool token + */ + function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return poolInfoList[poolId]; + } + + /** + * @notice returns list of [amount, accumulatedReward] for the given user for each pool token + * @param _user the address of the user + */ + function getUserBalanceList(address _user) external view returns (uint256[2][] memory) { + uint256 length = poolInfoList.length; + uint256[2][] memory userBalanceList = new uint256[2][](length); + for (uint256 i = 0; i < length; i++) { + userBalanceList[i][0] = userInfoMap[i][_user].amount; + userBalanceList[i][1] = _getUserAccumulatedReward(i, _user); + } + return userBalanceList; + } + + /** + * @notice returns UserInfo for the given pool and user + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserInfo(address _poolToken, address _user) public view returns (UserInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return userInfoMap[poolId][_user]; + } + + /** + * @notice returns list of UserInfo for the given user for each pool token + * @param _user the address of the user + */ + function getUserInfoList(address _user) external view returns (UserInfo[] memory) { + uint256 length = poolInfoList.length; + UserInfo[] memory userInfoList = new UserInfo[](length); + for (uint256 i = 0; i < length; i++) { + userInfoList[i] = userInfoMap[i][_user]; + } + return userInfoList; + } + + /** + * @notice returns accumulated reward for the given user for each pool token + * @param _user the address of the user + */ + function getUserAccumulatedRewardList(address _user) external view returns (uint256[] memory) { + uint256 length = poolInfoList.length; + uint256[] memory rewardList = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + rewardList[i] = _getUserAccumulatedReward(i, _user); + } + return rewardList; + } + + /** + * @notice returns the pool token balance a user has on the contract + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserPoolTokenBalance(address _poolToken, address _user) + external + view + returns (uint256) + { + UserInfo memory ui = getUserInfo(_poolToken, _user); + return ui.amount; + } + + /** + * @notice returns arrays with all the pools on the contract + */ + function getPoolInfoListArray() + external + view + returns ( + address[] memory _poolToken, + uint96[] memory _allocationPoints, + uint256[] memory _lastRewardBlock, + uint256[] memory _accumulatedRewardPerShare + ) + { + uint256 length = poolInfoList.length; + _poolToken = new address[](length); + _allocationPoints = new uint96[](length); + _lastRewardBlock = new uint256[](length); + _accumulatedRewardPerShare = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + _poolToken[i] = address(poolInfoList[i].poolToken); + _allocationPoints[i] = poolInfoList[i].allocationPoint; + _lastRewardBlock[i] = poolInfoList[i].lastRewardBlock; + _accumulatedRewardPerShare[i] = poolInfoList[i].accumulatedRewardPerShare; + } + return (_poolToken, _allocationPoints, _lastRewardBlock, _accumulatedRewardPerShare); + } + + /** + * @notice returns all pools that a user has on the contract, the poolId it's the index of arrays + * @param _user the address of the user + */ + function getUserInfoListArray(address _user) + external + view + returns ( + uint256[] memory _amount, + uint256[] memory _rewardDebt, + uint256[] memory _accumulatedReward + ) + { + uint256 length = poolInfoList.length; + _amount = new uint256[](length); + _rewardDebt = new uint256[](length); + _accumulatedReward = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + _amount[i] = userInfoMap[i][_user].amount; + _rewardDebt[i] = userInfoMap[i][_user].rewardDebt; + _accumulatedReward[i] = userInfoMap[i][_user].accumulatedReward; + } + return (_amount, _rewardDebt, _accumulatedReward); + } + + /** + * @notice send all funds from this contract to LiquidityMiningV2 + */ + function migrateFunds() external onlyAuthorized onlyAfterMigrationFinished { + require(liquidityMiningV2 != address(0), "Address not initialized"); + uint256 SOVBalance = SOV.balanceOf(address(this)); + transferSOV(liquidityMiningV2, SOVBalance); + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + IERC20 poolToken = poolInfoList[i].poolToken; + uint256 balancePoolToken = poolToken.balanceOf(address(this)); + poolToken.safeTransfer(liquidityMiningV2, balancePoolToken); + } + } + + /** + * @notice return reward token total users balance + */ + function getTotalUsersBalance() external view returns (uint256) { + return totalUsersBalance; + } + + /** + * @notice return reward token start block + */ + function getStartBlock() external view returns (uint256) { + return startBlock; + } } diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index 2f31bab1a..f7122acce 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -12,974 +12,1070 @@ import "./ILiquidityMiningV2.sol"; /// to earn multiple reward tokens by staking LP tokens as opposed to the /// previous one that only rewarded SOV contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { - using SafeMath for uint256; - using SafeERC20 for IERC20; - - /* Constants */ - - uint256 public constant PRECISION = 1e12; - - uint256 public constant SECONDS_PER_BLOCK = 30; - - /* Events */ - - event RewardTransferred(address indexed rewardToken, address indexed receiver, uint256 amount); - event PoolTokenAdded(address indexed user, address indexed poolToken, address[] rewardTokens, uint96[] allocationPoints); - event PoolTokenUpdated( - address indexed user, - address indexed poolToken, - address indexed rewardToken, - uint96 newAllocationPoint, - uint96 oldAllocationPoint - ); - event PoolTokenAssociation(address indexed user, uint256 indexed poolId, address indexed rewardToken, uint96 allocationPoint); - event Deposit(address indexed user, address indexed poolToken, uint256 amount); - event RewardClaimed(address indexed user, address indexed rewardToken, uint256 amount); - event Withdraw(address indexed user, address indexed poolToken, uint256 amount); - event EmergencyWithdraw( - address indexed user, - address indexed poolToken, - address indexed rewardToken, - uint256 amount, - uint256 accumulatedReward - ); - - /* Modifiers */ - modifier onlyMigrator() { - require(msg.sender == migrator, "only allowed to migrator contract"); - _; - } - - modifier onlyAfterMigrationFinished() { - require(migrationFinished, "Migration is not over yet"); - _; - } - - /* Functions */ - - /** - * @notice Initialize mining. - */ - function initialize(address _wrapper, address _migrator) external onlyAuthorized { - /// @dev Non-idempotent function. Must be called just once. - require(_migrator != address(0), "invalid contract address"); - require(migrator == address(0), "Already initialized"); - wrapper = _wrapper; - migrator = _migrator; - } - - /** - * @notice Add a new reward token - * - * @param _rewardToken The token to be rewarded to LP stakers. - * @param _rewardTokensPerBlock The number of reward tokens per block. - * @param _startDelayBlocks The number of blocks should be passed to start - * mining. - */ - function addRewardToken( - address _rewardToken, - uint256 _rewardTokensPerBlock, - uint256 _startDelayBlocks, - address _rewardTransferLogic - ) external onlyAuthorized { - /// @dev Non-idempotent function. Must be called just once. - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - require(rewardToken.startBlock == 0, "Already added"); - require(address(_rewardToken) != address(0), "Invalid token address"); - require(_startDelayBlocks > 0, "Invalid start block"); - - IRewardTransferLogic rewardTransferLogic = IRewardTransferLogic(_rewardTransferLogic); - require(_rewardToken == rewardTransferLogic.getRewardTokenAddress(), "Reward token and transfer logic mismatch"); - rewardTokensMap[_rewardToken] = RewardToken({ - rewardTokensPerBlock: _rewardTokensPerBlock, - startBlock: block.number + _startDelayBlocks, - endBlock: 0, - totalAllocationPoint: 0, - totalUsersBalance: 0, - rewardTransferLogic: rewardTransferLogic - }); - } - - /** - * @notice sets wrapper proxy contract - * @dev can be set to zero address to remove wrapper - */ - function setWrapper(address _wrapper) external onlyAuthorized { - wrapper = _wrapper; - } - - /** - * @notice stops mining by setting end block - */ - function stopMining(address _rewardToken) external onlyAuthorized { - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - require(rewardToken.startBlock != 0, "Not initialized"); - require(rewardToken.endBlock == 0, "Already stopped"); - - rewardToken.endBlock = block.number; - } - - /** - * @notice Transfers reward tokens to given address. - * Owner use this function to withdraw reward tokens from LM contract - * into another account. - * @param _rewardToken The address of the rewardToken - * @param _receiver The address of the tokens receiver. - * @param _amount The amount to be transferred. - * */ - function transferRewardTokens( - address _rewardToken, - address _receiver, - uint256 _amount - ) external onlyAuthorized { - require(_rewardToken != address(0), "Reward address invalid"); - require(_receiver != address(0), "Receiver address invalid"); - require(_amount != 0, "Amount invalid"); - - IERC20 rewardToken = IERC20(_rewardToken); - - /// @dev Do not transfer more SOV than available. - uint256 balance = rewardToken.balanceOf(address(this)); - if (_amount > balance) { - _amount = balance; - } - - /// @dev Event log. - emit RewardTransferred(_rewardToken, _receiver, _amount); - /// @dev The actual transfer. - require(rewardToken.transfer(_receiver, _amount), "Transfer failed"); - } - - /** - * @notice Get the missed rewardTokens balance of LM contract. - * - * @return The amount of reward tokens according to totalUsersBalance - * in excess of actual balance of the LM contract. - * */ - function getMissedBalance(address _rewardToken) external view returns (uint256) { - IERC20 rewardToken = IERC20(_rewardToken); - uint256 totalUsersBalance = rewardTokensMap[_rewardToken].totalUsersBalance; - uint256 balance = rewardToken.balanceOf(address(this)); - return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); - } - - /** - * @notice adds a new lp to the pool. Can only be called by the owner or an admin - * @param _poolToken the address of pool token - * @param _rewardTokens the addresses of reward tokens for given pool - * @param _allocationPoints the allocation points (weight) for the given pool and each reward token - * @param _withUpdate the flag whether we need to update all pools - */ - function add( - address _poolToken, - address[] calldata _rewardTokens, - uint96[] calldata _allocationPoints, - bool _withUpdate - ) external onlyAuthorized { - require(_rewardTokens.length > 0, "Invalid reward tokens length"); - require(_rewardTokens.length == _allocationPoints.length, "Invalid allocation points length"); - require(_poolToken != address(0), "Invalid token address"); - require(poolIdList[_poolToken] == 0, "Token already added"); - - if (_withUpdate) { - updateAllPools(); - } - - poolInfoList.push(PoolInfo({ poolToken: IERC20(_poolToken), rewardTokens: _rewardTokens })); - //indexing starts from 1 in order to check whether token was already added - poolIdList[_poolToken] = poolInfoList.length; - - for (uint256 i = 0; i < _rewardTokens.length; i++) { - associatePoolToRewardToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); - } - - emit PoolTokenAdded(msg.sender, _poolToken, _rewardTokens, _allocationPoints); - } - - function associatePoolToRewardToken( - address _poolToken, - address _rewardToken, - uint96 _allocationPoint - ) internal { - uint256 poolId = _getPoolId(_poolToken); - - // Allocation point checks - require(_allocationPoint > 0, "Invalid allocation point"); - - // Reward token checks - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - uint256 startBlock = rewardToken.startBlock; - require(startBlock != 0, "Not initialized"); - - // Check association is not done twice - - require(poolInfoRewardTokensMap[poolId][_rewardToken].allocationPoint == 0, "Already associated"); - - uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; - rewardToken.totalAllocationPoint = rewardToken.totalAllocationPoint.add(_allocationPoint); - - poolInfoRewardTokensMap[poolId][_rewardToken] = PoolInfoRewardToken({ - allocationPoint: _allocationPoint, - lastRewardBlock: lastRewardBlock, - accumulatedRewardPerShare: 0 - }); - - emit PoolTokenAssociation(msg.sender, poolId, _rewardToken, _allocationPoint); - } - - /** - * @notice updates the given pool's reward tokens allocation point - * @param _poolToken the address of pool token - * @param _rewardTokens the addresses of reward tokens for given pool - * @param _allocationPoints the allocation points (weight) for the given pool and each reward token - * @param _updateAllFlag the flag whether we need to update all pools - */ - function update( - address _poolToken, - address[] calldata _rewardTokens, - uint96[] calldata _allocationPoints, - bool _updateAllFlag - ) external onlyAuthorized { - if (_updateAllFlag) { - updateAllPools(); - } else { - updatePool(_poolToken); - } - _updateTokens(_poolToken, _rewardTokens, _allocationPoints); - } - - function _updateTokens( - address _poolToken, - address[] memory _rewardTokens, - uint96[] memory _allocationPoints - ) internal { - for (uint256 i = 0; i < _rewardTokens.length; i++) { - _updateToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); - } - } - - function _updateToken( - address _poolToken, - address _rewardToken, - uint96 _allocationPoint - ) internal { - uint256 poolId = _getPoolId(_poolToken); - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; - - uint96 previousAllocationPoint = poolInfoRewardToken.allocationPoint; - rewardToken.totalAllocationPoint = rewardToken.totalAllocationPoint.sub(previousAllocationPoint).add(_allocationPoint); - poolInfoRewardToken.allocationPoint = _allocationPoint; - - emit PoolTokenUpdated(msg.sender, _poolToken, _rewardToken, _allocationPoint, previousAllocationPoint); - } - - /** - * @notice updates the given pools' reward tokens allocation points - * @param _poolTokens array of addresses of pool tokens - * @param _allocationPoints array of allocation points (weight) for the given pools - * @param _updateAllFlag the flag whether we need to update all pools - */ - function updateTokens( - address[] calldata _poolTokens, - address[][] calldata _rewardTokens, - uint96[][] calldata _allocationPoints, - bool _updateAllFlag - ) external onlyAuthorized { - require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); - require(_poolTokens.length == _rewardTokens.length, "Arrays mismatch"); - - if (_updateAllFlag) { - updateAllPools(); - } - uint256 length = _poolTokens.length; - for (uint256 i = 0; i < length; i++) { - require(_allocationPoints[i].length == _rewardTokens[i].length, "Arrays mismatch"); - _updateTokens(_poolTokens[i], _rewardTokens[i], _allocationPoints[i]); - } - } - - /** - * @notice returns reward multiplier over the given _from to _to block - * @param _from the first block for a calculation - * @param _to the last block for a calculation - */ - function _getPassedBlocks( - RewardToken storage _rewardToken, - uint256 _from, - uint256 _to - ) internal view returns (uint256) { - if (_from < _rewardToken.startBlock) { - _from = _rewardToken.startBlock; - } - - if (_rewardToken.endBlock > 0 && _to > _rewardToken.endBlock) { - _to = _rewardToken.endBlock; - } - - if (_to <= _from) { - return 0; - } - - return _to.sub(_from); - } - - function _getUserAccumulatedReward( - uint256 _poolId, - address _rewardToken, - address _user - ) internal view returns (uint256) { - PoolInfo storage pool = poolInfoList[_poolId]; - PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][_rewardToken]; - UserInfo storage user = userInfoMap[_poolId][_user]; - - uint256 accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare; - uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); - if (block.number > poolRewardToken.lastRewardBlock && poolTokenBalance != 0) { - (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardTokensMap[_rewardToken]); - accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); - } - return user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub(user.rewards[_rewardToken].rewardDebt); - } - - /** - * @notice returns accumulated reward - * @param _poolToken the address of pool token - * @param _rewardToken the reward token address - * @param _user the user address - */ - function getUserAccumulatedReward( - address _poolToken, - address _rewardToken, - address _user - ) external view returns (uint256) { - uint256 poolId = _getPoolId(_poolToken); - return _getUserAccumulatedReward(poolId, _rewardToken, _user); - } - - /** - * @notice returns estimated reward - * @param _poolToken the address of pool token - * @param _rewardToken the reward token address - * @param _amount the amount of tokens to be deposited - * @param _duration the duration of liquidity providing in seconds - */ - function getEstimatedReward( - address _poolToken, - address _rewardToken, - uint256 _amount, - uint256 _duration - ) external view returns (uint256) { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - uint256 start = block.number; - uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = - _getPoolAccumulatedReward( - pool, - _amount, - rewardTokensMap[_rewardToken], - poolInfoRewardTokensMap[poolId][_rewardToken], - start, - end - ); - return _amount.mul(accumulatedRewardPerShare).div(PRECISION); - } - - /** - * @notice Updates reward variables for all pools. - * @dev Be careful of gas spending! - */ - function updateAllPools() public { - uint256 length = poolInfoList.length; - for (uint256 i = 0; i < length; i++) { - _updatePool(i); - } - } - - /** - * @notice Updates reward variables of the given pool to be up-to-date - * @param _poolToken the address of pool token - */ - function updatePool(address _poolToken) public { - uint256 poolId = _getPoolId(_poolToken); - _updatePool(poolId); - } - - function _updatePool(uint256 _poolId) internal { - PoolInfo storage pool = poolInfoList[_poolId]; - - uint256 rewardTokensLength = pool.rewardTokens.length; - for (uint256 i = 0; i < rewardTokensLength; i++) { - _updatePoolRewardToken(pool, _poolId, pool.rewardTokens[i]); - } - } - - function _updatePoolRewardToken( - PoolInfo storage pool, - uint256 _poolId, - address _rewardToken - ) internal { - PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][_rewardToken]; - // this pool has been updated recently - if (block.number <= poolRewardToken.lastRewardBlock) { - return; - } - - uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); - if (poolTokenBalance == 0) { - poolRewardToken.lastRewardBlock = block.number; - return; - } - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - - (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); - poolRewardToken.accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare.add(accumulatedRewardPerShare_); - poolRewardToken.lastRewardBlock = block.number; - - rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.add(accumulatedReward_); - } - - function _getPoolAccumulatedReward( - PoolInfo storage _pool, - PoolInfoRewardToken storage _poolRewardToken, - RewardToken storage _rewardToken - ) internal view returns (uint256, uint256) { - return _getPoolAccumulatedReward(_pool, 0, _rewardToken, _poolRewardToken, block.number); - } - - function _getPoolAccumulatedReward( - PoolInfo storage _pool, - uint256 _additionalAmount, - RewardToken storage _rewardToken, - PoolInfoRewardToken storage _poolRewardToken, - uint256 _endBlock - ) internal view returns (uint256, uint256) { - return - _getPoolAccumulatedReward( - _pool, - _additionalAmount, - _rewardToken, - _poolRewardToken, - _poolRewardToken.lastRewardBlock, - _endBlock - ); - } - - function _getPoolAccumulatedReward( - PoolInfo storage _pool, - uint256 _additionalAmount, - RewardToken storage _rewardToken, - PoolInfoRewardToken storage _poolRewardToken, - uint256 _startBlock, - uint256 _endBlock - ) internal view returns (uint256, uint256) { - uint256 passedBlocks = _getPassedBlocks(_rewardToken, _startBlock, _endBlock); - uint256 accumulatedReward = - passedBlocks.mul(_rewardToken.rewardTokensPerBlock).mul(PRECISION).mul(_poolRewardToken.allocationPoint).div( - _rewardToken.totalAllocationPoint - ); - - uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); - poolTokenBalance = poolTokenBalance.add(_additionalAmount); - uint256 accumulatedRewardPerShare = accumulatedReward.div(poolTokenBalance); - return (accumulatedReward.div(PRECISION), accumulatedRewardPerShare); - } - - /** - * @notice deposits pool tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the address of user, tokens will be deposited to it or to msg.sender - */ - function deposit( - address _poolToken, - uint256 _amount, - address _user - ) external onlyAfterMigrationFinished { - _deposit(_poolToken, _amount, _user, false); - } - - /** - * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit - * @dev only callable by the pool which issues the tokens - * @param _user the user address - * @param _amount the minted amount - */ - function onTokensDeposited(address _user, uint256 _amount) external { - //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert - _deposit(msg.sender, _amount, _user, true); - } - - /** - * @notice internal function for depositing pool tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the address of user, tokens will be deposited to it - * @param alreadyTransferred true if the pool tokens have already been transferred - */ - function _deposit( - address _poolToken, - uint256 _amount, - address _user, - bool alreadyTransferred - ) internal { - require(poolIdList[_poolToken] != 0, "Pool token not found"); - address userAddress = _user != address(0) ? _user : msg.sender; - - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][userAddress]; - - uint256 rewardTokensLength = pool.rewardTokens.length; - for (uint256 i = 0; i < rewardTokensLength; i++) { - address rewardTokenAddress = pool.rewardTokens[i]; - _updatePoolRewardToken(pool, poolId, rewardTokenAddress); - //sends reward directly to the user - _updateReward(poolId, rewardTokenAddress, user); - } - - if (_amount > 0) { - //receives pool tokens from msg.sender, it can be user or WrapperProxy contract - if (!alreadyTransferred) pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); - user.amount = user.amount.add(_amount); - } - - for (uint256 i = 0; i < rewardTokensLength; i++) { - address rewardTokenAddress = pool.rewardTokens[i]; - _updateRewardDebt(poolId, rewardTokenAddress, user); - } - emit Deposit(userAddress, _poolToken, _amount); - } - - /** - * @notice transfers reward tokens - * @param _poolToken the address of pool token - * @param _user the address of user to claim reward from (can be passed only by wrapper contract) - */ - function claimRewards(address _poolToken, address _user) external { - address userAddress = _getUserAddress(_user); - - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - uint256 rewardTokensLength = pool.rewardTokens.length; - for (uint256 i = 0; i < rewardTokensLength; i++) { - address rewardTokenAddress = pool.rewardTokens[i]; - _claimReward(poolId, rewardTokenAddress, userAddress); - } - } - - /** - * @notice transfers rewards from a specific reward token - * @param _poolToken the address of pool token - * @param _rewardToken the address of reward token - * @param _user the address of user to claim reward from (can be passed only by wrapper contract) - */ - function claimReward( - address _poolToken, - address _rewardToken, - address _user - ) external { - address userAddress = _getUserAddress(_user); - - uint256 poolId = _getPoolId(_poolToken); - _claimReward(poolId, _rewardToken, userAddress); - } - - function _claimReward( - uint256 _poolId, - address _rewardToken, - address _userAddress - ) internal { - UserInfo storage user = userInfoMap[_poolId][_userAddress]; - PoolInfo storage pool = poolInfoList[_poolId]; - - _updatePoolRewardToken(pool, _poolId, _rewardToken); - _updateReward(_poolId, _rewardToken, user); - _transferReward(_rewardToken, user, _userAddress, false, true); - _updateRewardDebt(_poolId, _rewardToken, user); - } - - /** - * @notice transfers reward tokens from all pools - * @param _user the address of user to claim reward from (can be passed only by wrapper contract) - */ - function claimRewardFromAllPools(address _user) external { - address userAddress = _getUserAddress(_user); - - uint256 length = poolInfoList.length; - for (uint256 i = 0; i < length; i++) { - uint256 poolId = i; - PoolInfo storage pool = poolInfoList[poolId]; - uint256 rewardTokensLength = pool.rewardTokens.length; - for (uint256 j = 0; j < rewardTokensLength; j++) { - address rewardTokenAddress = pool.rewardTokens[j]; - _claimReward(poolId, rewardTokenAddress, userAddress); - } - } - } - - /** - * @notice withdraws pool tokens and transfers reward tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) - */ - function withdraw( - address _poolToken, - uint256 _amount, - address _user - ) external onlyAfterMigrationFinished { - require(poolIdList[_poolToken] != 0, "Pool token not found"); - address userAddress = _getUserAddress(_user); - - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][userAddress]; - require(user.amount >= _amount, "Not enough balance"); - - // Start collecting rewards for each reward token the user holds - uint256 rewardTokensLength = pool.rewardTokens.length; - for (uint256 i = 0; i < rewardTokensLength; i++) { - address rewardTokenAddress = pool.rewardTokens[i]; - _updatePoolRewardToken(pool, poolId, rewardTokenAddress); - _updateReward(poolId, rewardTokenAddress, user); - _transferReward(rewardTokenAddress, user, userAddress, true, false); - } - user.amount = user.amount.sub(_amount); - - //msg.sender is wrapper -> send to wrapper - if (msg.sender == wrapper) { - pool.poolToken.safeTransfer(address(msg.sender), _amount); - } - //msg.sender is user or pool token (lending pool) -> send to user - else { - pool.poolToken.safeTransfer(userAddress, _amount); - } - - for (uint256 i = 0; i < rewardTokensLength; i++) { - address rewardTokenAddress = pool.rewardTokens[i]; - _updateRewardDebt(poolId, rewardTokenAddress, user); - } - emit Withdraw(userAddress, _poolToken, _amount); - } - - function _getUserAddress(address _user) internal view returns (address) { - address userAddress = msg.sender; - if (_user != address(0)) { - //only wrapper can pass _user parameter - require(msg.sender == wrapper || poolIdList[msg.sender] != 0, "only wrapper or pools may withdraw for a user"); - userAddress = _user; - } - return userAddress; - } - - function _updateReward( - uint256 _poolId, - address _rewardTokenAddress, - UserInfo storage user - ) internal { - UserReward storage reward = user.rewards[_rewardTokenAddress]; - //update user accumulated reward - if (user.amount > 0) { - //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = - user.amount.mul(poolInfoRewardTokensMap[_poolId][_rewardTokenAddress].accumulatedRewardPerShare).div(PRECISION).sub( - reward.rewardDebt - ); - reward.accumulatedReward = reward.accumulatedReward.add(accumulatedReward); - } - } - - function _updateRewardDebt( - uint256 poolId, - address rewardToken, - UserInfo storage user - ) internal { - //reward accumulated before amount update (should be subtracted during next reward calculation) - user.rewards[rewardToken].rewardDebt = user.amount.mul(poolInfoRewardTokensMap[poolId][rewardToken].accumulatedRewardPerShare).div( - PRECISION - ); - } - - /** - * @notice Send reward in SOV to the lockedSOV vault. - * @param _user The user info, to get its reward share. - * @param _userAddress The address of the user, to send SOV in its behalf. - * @param _isWithdrawal The flag whether determines if the user is withdrawing all the funds - * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough - */ - function _transferReward( - address _rewardToken, - UserInfo storage _user, - address _userAddress, - bool _isWithdrawal, - bool _isCheckingBalance - ) internal { - uint256 userAccumulatedReward = _user.rewards[_rewardToken].accumulatedReward; - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - IERC20 token = IERC20(_rewardToken); - /// @dev Transfer if enough token balance on this LM contract. - uint256 balance = token.balanceOf(address(this)); - if (balance >= userAccumulatedReward) { - rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.sub(userAccumulatedReward); - _user.rewards[_rewardToken].accumulatedReward = 0; - - IRewardTransferLogic transferLogic = rewardToken.rewardTransferLogic; - require(token.approve(transferLogic.senderToAuthorize(), userAccumulatedReward), "Approve failed"); - transferLogic.transferReward(_userAddress, userAccumulatedReward, _isWithdrawal); - /// @dev Event log. - emit RewardClaimed(_userAddress, _rewardToken, userAccumulatedReward); - } else { - require(!_isCheckingBalance, "Claiming reward failed"); - } - } - - /** - * @notice withdraws pool tokens without transferring reward tokens - * @param _poolToken the address of pool token - * @dev EMERGENCY ONLY - */ - function emergencyWithdraw(address _poolToken) external onlyAfterMigrationFinished { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][msg.sender]; - - uint256 rewardTokensLength = pool.rewardTokens.length; - for (uint256 i = 0; i < rewardTokensLength; i++) { - address rewardTokenAddress = pool.rewardTokens[i]; - _updatePoolRewardToken(pool, poolId, rewardTokenAddress); - _updateReward(poolId, rewardTokenAddress, user); - // substract user balance from total balance for each reward token - UserReward storage userReward = user.rewards[pool.rewardTokens[i]]; - uint256 accumulatedReward = userReward.accumulatedReward; - RewardToken storage rewardToken = rewardTokensMap[pool.rewardTokens[i]]; - rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.sub(accumulatedReward); - emit EmergencyWithdraw(msg.sender, _poolToken, rewardTokenAddress, user.amount, accumulatedReward); - - userReward.rewardDebt = 0; - userReward.accumulatedReward = 0; - } - - uint256 userAmount = user.amount; - user.amount = 0; - pool.poolToken.safeTransfer(address(msg.sender), userAmount); - - for (uint256 i = 0; i < rewardTokensLength; i++) { - address rewardTokenAddress = pool.rewardTokens[i]; - _updateRewardDebt(poolId, rewardTokenAddress, user); - } - } - - function getRewardToken(address _rewardToken) external view returns (RewardToken memory) { - return rewardTokensMap[_rewardToken]; - } - - /** - * @notice returns a list of PoolInfoRewardToken for the given pool - * @param _poolToken the address of pool token - */ - function getPoolRewards(address _poolToken) external view returns (PoolInfoRewardToken[] memory) { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo memory poolInfo = poolInfoList[poolId]; - uint256 rewardsLength = poolInfo.rewardTokens.length; - PoolInfoRewardToken[] memory rewards = new PoolInfoRewardToken[](rewardsLength); - for (uint256 i = 0; i < rewardsLength; i++) { - rewards[i] = poolInfoRewardTokensMap[poolId][poolInfo.rewardTokens[i]]; - } - return rewards; - } - - /** - * @notice returns a PoolInfoRewardToken for the given pool and reward token - * @param _poolToken the address of pool token - * @param _rewardToken the address of reward token - */ - function getPoolReward(address _poolToken, address _rewardToken) external view returns (PoolInfoRewardToken memory) { - uint256 poolId = _getPoolId(_poolToken); - return poolInfoRewardTokensMap[poolId][_rewardToken]; - } - - /** - * @notice returns pool id - * @param _poolToken the address of pool token - */ - function getPoolId(address _poolToken) external view returns (uint256) { - return _getPoolId(_poolToken); - } - - function _getPoolId(address _poolToken) internal view returns (uint256) { - uint256 poolId = poolIdList[_poolToken]; - require(poolId > 0, "Pool token not found"); - return poolId - 1; - } - - /** - * @notice returns count of pool tokens - */ - function getPoolLength() external view returns (uint256) { - return poolInfoList.length; - } - - /** - * @notice returns list of pool token's info - */ - function getPoolInfoList() external view returns (PoolInfo[] memory) { - return poolInfoList; - } - - /** - * @notice returns pool info for the given token - * @param _poolToken the address of pool token - */ - function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { - uint256 poolId = _getPoolId(_poolToken); - return poolInfoList[poolId]; - } - - struct UserBalance { - uint256 amount; - address rewardToken; - uint256 accumulatedReward; - } - - /** - * @notice returns list of [amount, rewardToken, accumulatedReward] for the given user for each pool token and reward token - * @param _user the address of the user - */ - function getUserBalanceList(address _user) external view returns (UserBalance[][] memory) { - uint256 length = poolInfoList.length; - UserBalance[][] memory userBalanceList = new UserBalance[][](length); - for (uint256 i = 0; i < length; i++) { - PoolInfo memory poolInfo = poolInfoList[i]; - uint256 rewardLength = poolInfo.rewardTokens.length; - userBalanceList[i] = new UserBalance[](rewardLength); - for (uint256 j = 0; j < rewardLength; j++) { - address _rewardToken = poolInfo.rewardTokens[j]; - userBalanceList[i][j].amount = userInfoMap[i][_user].amount; - userBalanceList[i][j].rewardToken = _rewardToken; - userBalanceList[i][j].accumulatedReward = _getUserAccumulatedReward(i, _rewardToken, _user); - } - } - return userBalanceList; - } - - struct PoolUserInfo { - uint256 amount; - UserReward[] rewards; - } - - /** - * @notice returns UserInfo for the given pool and user - * @param _poolToken the address of pool token - * @param _user the address of the user - */ - function getUserInfo(address _poolToken, address _user) external view returns (PoolUserInfo memory) { - uint256 poolId = _getPoolId(_poolToken); - return _getPoolUserInfo(poolId, _user); - } - - /** - * @notice returns list of UserInfo for the given user for each pool token - * @param _user the address of the user - */ - function getUserInfoList(address _user) external view returns (PoolUserInfo[] memory) { - uint256 length = poolInfoList.length; - PoolUserInfo[] memory userInfoList = new PoolUserInfo[](length); - for (uint256 i = 0; i < length; i++) { - userInfoList[i] = _getPoolUserInfo(i, _user); - } - return userInfoList; - } - - function _getPoolUserInfo(uint256 _poolId, address _user) internal view returns (PoolUserInfo memory) { - PoolInfo memory pool = poolInfoList[_poolId]; - uint256 rewardsLength = pool.rewardTokens.length; - UserInfo storage userInfo = userInfoMap[_poolId][_user]; - PoolUserInfo memory poolUserInfo; - poolUserInfo.amount = userInfo.amount; - poolUserInfo.rewards = new UserReward[](rewardsLength); - for (uint256 i = 0; i < rewardsLength; i++) { - poolUserInfo.rewards[i] = userInfo.rewards[pool.rewardTokens[i]]; - } - return poolUserInfo; - } - - struct UserAccumulatedReward { - address rewardToken; - uint256 accumulatedReward; - } - - /** - * @notice returns accumulated reward for the given user for each pool token and reward token - * @param _user the address of the user - */ - function getUserAccumulatedRewardList(address _user) external view returns (UserAccumulatedReward[][] memory) { - uint256 length = poolInfoList.length; - UserAccumulatedReward[][] memory rewardList = new UserAccumulatedReward[][](length); - for (uint256 i = 0; i < length; i++) { - PoolInfo memory poolInfo = poolInfoList[i]; - uint256 rewardsLength = poolInfo.rewardTokens.length; - rewardList[i] = new UserAccumulatedReward[](rewardsLength); - for (uint256 j = 0; j < rewardsLength; j++) { - rewardList[i][j].rewardToken = poolInfo.rewardTokens[j]; - rewardList[i][j].accumulatedReward = _getUserAccumulatedReward(i, poolInfo.rewardTokens[j], _user); - } - } - return rewardList; - } - - /** - * @notice returns the pool token balance a user has on the contract - * @param _poolToken the address of pool token - * @param _user the address of the user - */ - function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256) { - uint256 poolId = _getPoolId(_poolToken); - return userInfoMap[poolId][_user].amount; - } - - function setPoolInfoRewardToken( - address _poolToken, - address _rewardToken, - uint256 _lastRewardBlock, - uint256 _accumulatedRewardPerShare - ) external onlyMigrator { - uint256 poolId = _getPoolId(_poolToken); - PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; - poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; - poolInfoRewardToken.accumulatedRewardPerShare = _accumulatedRewardPerShare; - } - - function setRewardToken( - address _rewardToken, - uint256 _startBlock, - uint256 _totalUsersBalance - ) external onlyMigrator { - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - rewardToken.startBlock = _startBlock; - rewardToken.totalUsersBalance = _totalUsersBalance; - } - - function setUserInfo( - uint256 _poolId, - address _user, - address _rewardToken, - uint256 _amount, - uint256 _rewardDebt, - uint256 _accumulatedReward - ) external onlyMigrator { - UserInfo storage userInfo = userInfoMap[_poolId][_user]; - UserReward storage userReward = userInfo.rewards[_rewardToken]; - userInfo.amount += _amount; - userReward.rewardDebt += _rewardDebt; - userReward.accumulatedReward += _accumulatedReward; - } - - /** - * @notice finish migration - */ - function finishMigration() external onlyMigrator { - migrationFinished = true; - } + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* Constants */ + + uint256 public constant PRECISION = 1e12; + + uint256 public constant SECONDS_PER_BLOCK = 30; + + /* Events */ + + event RewardTransferred(address indexed rewardToken, address indexed receiver, uint256 amount); + event PoolTokenAdded( + address indexed user, + address indexed poolToken, + address[] rewardTokens, + uint96[] allocationPoints + ); + event PoolTokenUpdated( + address indexed user, + address indexed poolToken, + address indexed rewardToken, + uint96 newAllocationPoint, + uint96 oldAllocationPoint + ); + event PoolTokenAssociation( + address indexed user, + uint256 indexed poolId, + address indexed rewardToken, + uint96 allocationPoint + ); + event Deposit(address indexed user, address indexed poolToken, uint256 amount); + event RewardClaimed(address indexed user, address indexed rewardToken, uint256 amount); + event Withdraw(address indexed user, address indexed poolToken, uint256 amount); + event EmergencyWithdraw( + address indexed user, + address indexed poolToken, + address indexed rewardToken, + uint256 amount, + uint256 accumulatedReward + ); + + /* Modifiers */ + modifier onlyMigrator() { + require(msg.sender == migrator, "only allowed to migrator contract"); + _; + } + + modifier onlyAfterMigrationFinished() { + require(migrationFinished, "Migration is not over yet"); + _; + } + + /* Functions */ + + /** + * @notice Initialize mining. + */ + function initialize(address _wrapper, address _migrator) external onlyAuthorized { + /// @dev Non-idempotent function. Must be called just once. + require(_migrator != address(0), "invalid contract address"); + require(migrator == address(0), "Already initialized"); + wrapper = _wrapper; + migrator = _migrator; + } + + /** + * @notice Add a new reward token + * + * @param _rewardToken The token to be rewarded to LP stakers. + * @param _rewardTokensPerBlock The number of reward tokens per block. + * @param _startDelayBlocks The number of blocks should be passed to start + * mining. + */ + function addRewardToken( + address _rewardToken, + uint256 _rewardTokensPerBlock, + uint256 _startDelayBlocks, + address _rewardTransferLogic + ) external onlyAuthorized { + /// @dev Non-idempotent function. Must be called just once. + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + require(rewardToken.startBlock == 0, "Already added"); + require(address(_rewardToken) != address(0), "Invalid token address"); + require(_startDelayBlocks > 0, "Invalid start block"); + + IRewardTransferLogic rewardTransferLogic = IRewardTransferLogic(_rewardTransferLogic); + require( + _rewardToken == rewardTransferLogic.getRewardTokenAddress(), + "Reward token and transfer logic mismatch" + ); + rewardTokensMap[_rewardToken] = RewardToken({ + rewardTokensPerBlock: _rewardTokensPerBlock, + startBlock: block.number + _startDelayBlocks, + endBlock: 0, + totalAllocationPoint: 0, + totalUsersBalance: 0, + rewardTransferLogic: rewardTransferLogic + }); + } + + /** + * @notice sets wrapper proxy contract + * @dev can be set to zero address to remove wrapper + */ + function setWrapper(address _wrapper) external onlyAuthorized { + wrapper = _wrapper; + } + + /** + * @notice stops mining by setting end block + */ + function stopMining(address _rewardToken) external onlyAuthorized { + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + require(rewardToken.startBlock != 0, "Not initialized"); + require(rewardToken.endBlock == 0, "Already stopped"); + + rewardToken.endBlock = block.number; + } + + /** + * @notice Transfers reward tokens to given address. + * Owner use this function to withdraw reward tokens from LM contract + * into another account. + * @param _rewardToken The address of the rewardToken + * @param _receiver The address of the tokens receiver. + * @param _amount The amount to be transferred. + * */ + function transferRewardTokens( + address _rewardToken, + address _receiver, + uint256 _amount + ) external onlyAuthorized { + require(_rewardToken != address(0), "Reward address invalid"); + require(_receiver != address(0), "Receiver address invalid"); + require(_amount != 0, "Amount invalid"); + + IERC20 rewardToken = IERC20(_rewardToken); + + /// @dev Do not transfer more SOV than available. + uint256 balance = rewardToken.balanceOf(address(this)); + if (_amount > balance) { + _amount = balance; + } + + /// @dev Event log. + emit RewardTransferred(_rewardToken, _receiver, _amount); + /// @dev The actual transfer. + require(rewardToken.transfer(_receiver, _amount), "Transfer failed"); + } + + /** + * @notice Get the missed rewardTokens balance of LM contract. + * + * @return The amount of reward tokens according to totalUsersBalance + * in excess of actual balance of the LM contract. + * */ + function getMissedBalance(address _rewardToken) external view returns (uint256) { + IERC20 rewardToken = IERC20(_rewardToken); + uint256 totalUsersBalance = rewardTokensMap[_rewardToken].totalUsersBalance; + uint256 balance = rewardToken.balanceOf(address(this)); + return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); + } + + /** + * @notice adds a new lp to the pool. Can only be called by the owner or an admin + * @param _poolToken the address of pool token + * @param _rewardTokens the addresses of reward tokens for given pool + * @param _allocationPoints the allocation points (weight) for the given pool and each reward token + * @param _withUpdate the flag whether we need to update all pools + */ + function add( + address _poolToken, + address[] calldata _rewardTokens, + uint96[] calldata _allocationPoints, + bool _withUpdate + ) external onlyAuthorized { + require(_rewardTokens.length > 0, "Invalid reward tokens length"); + require( + _rewardTokens.length == _allocationPoints.length, + "Invalid allocation points length" + ); + require(_poolToken != address(0), "Invalid token address"); + require(poolIdList[_poolToken] == 0, "Token already added"); + + if (_withUpdate) { + updateAllPools(); + } + + poolInfoList.push( + PoolInfo({ poolToken: IERC20(_poolToken), rewardTokens: _rewardTokens }) + ); + //indexing starts from 1 in order to check whether token was already added + poolIdList[_poolToken] = poolInfoList.length; + + for (uint256 i = 0; i < _rewardTokens.length; i++) { + associatePoolToRewardToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); + } + + emit PoolTokenAdded(msg.sender, _poolToken, _rewardTokens, _allocationPoints); + } + + function associatePoolToRewardToken( + address _poolToken, + address _rewardToken, + uint96 _allocationPoint + ) internal { + uint256 poolId = _getPoolId(_poolToken); + + // Allocation point checks + require(_allocationPoint > 0, "Invalid allocation point"); + + // Reward token checks + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + uint256 startBlock = rewardToken.startBlock; + require(startBlock != 0, "Not initialized"); + + // Check association is not done twice + + require( + poolInfoRewardTokensMap[poolId][_rewardToken].allocationPoint == 0, + "Already associated" + ); + + uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; + rewardToken.totalAllocationPoint = rewardToken.totalAllocationPoint.add(_allocationPoint); + + poolInfoRewardTokensMap[poolId][_rewardToken] = PoolInfoRewardToken({ + allocationPoint: _allocationPoint, + lastRewardBlock: lastRewardBlock, + accumulatedRewardPerShare: 0 + }); + + emit PoolTokenAssociation(msg.sender, poolId, _rewardToken, _allocationPoint); + } + + /** + * @notice updates the given pool's reward tokens allocation point + * @param _poolToken the address of pool token + * @param _rewardTokens the addresses of reward tokens for given pool + * @param _allocationPoints the allocation points (weight) for the given pool and each reward token + * @param _updateAllFlag the flag whether we need to update all pools + */ + function update( + address _poolToken, + address[] calldata _rewardTokens, + uint96[] calldata _allocationPoints, + bool _updateAllFlag + ) external onlyAuthorized { + if (_updateAllFlag) { + updateAllPools(); + } else { + updatePool(_poolToken); + } + _updateTokens(_poolToken, _rewardTokens, _allocationPoints); + } + + function _updateTokens( + address _poolToken, + address[] memory _rewardTokens, + uint96[] memory _allocationPoints + ) internal { + for (uint256 i = 0; i < _rewardTokens.length; i++) { + _updateToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); + } + } + + function _updateToken( + address _poolToken, + address _rewardToken, + uint96 _allocationPoint + ) internal { + uint256 poolId = _getPoolId(_poolToken); + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + PoolInfoRewardToken storage poolInfoRewardToken = + poolInfoRewardTokensMap[poolId][_rewardToken]; + + uint96 previousAllocationPoint = poolInfoRewardToken.allocationPoint; + rewardToken.totalAllocationPoint = rewardToken + .totalAllocationPoint + .sub(previousAllocationPoint) + .add(_allocationPoint); + poolInfoRewardToken.allocationPoint = _allocationPoint; + + emit PoolTokenUpdated( + msg.sender, + _poolToken, + _rewardToken, + _allocationPoint, + previousAllocationPoint + ); + } + + /** + * @notice updates the given pools' reward tokens allocation points + * @param _poolTokens array of addresses of pool tokens + * @param _allocationPoints array of allocation points (weight) for the given pools + * @param _updateAllFlag the flag whether we need to update all pools + */ + function updateTokens( + address[] calldata _poolTokens, + address[][] calldata _rewardTokens, + uint96[][] calldata _allocationPoints, + bool _updateAllFlag + ) external onlyAuthorized { + require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); + require(_poolTokens.length == _rewardTokens.length, "Arrays mismatch"); + + if (_updateAllFlag) { + updateAllPools(); + } + uint256 length = _poolTokens.length; + for (uint256 i = 0; i < length; i++) { + require(_allocationPoints[i].length == _rewardTokens[i].length, "Arrays mismatch"); + _updateTokens(_poolTokens[i], _rewardTokens[i], _allocationPoints[i]); + } + } + + /** + * @notice returns reward multiplier over the given _from to _to block + * @param _from the first block for a calculation + * @param _to the last block for a calculation + */ + function _getPassedBlocks( + RewardToken storage _rewardToken, + uint256 _from, + uint256 _to + ) internal view returns (uint256) { + if (_from < _rewardToken.startBlock) { + _from = _rewardToken.startBlock; + } + + if (_rewardToken.endBlock > 0 && _to > _rewardToken.endBlock) { + _to = _rewardToken.endBlock; + } + + if (_to <= _from) { + return 0; + } + + return _to.sub(_from); + } + + function _getUserAccumulatedReward( + uint256 _poolId, + address _rewardToken, + address _user + ) internal view returns (uint256) { + PoolInfo storage pool = poolInfoList[_poolId]; + PoolInfoRewardToken storage poolRewardToken = + poolInfoRewardTokensMap[_poolId][_rewardToken]; + UserInfo storage user = userInfoMap[_poolId][_user]; + + uint256 accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare; + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (block.number > poolRewardToken.lastRewardBlock && poolTokenBalance != 0) { + (, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool, poolRewardToken, rewardTokensMap[_rewardToken]); + accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); + } + return + user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub( + user.rewards[_rewardToken].rewardDebt + ); + } + + /** + * @notice returns accumulated reward + * @param _poolToken the address of pool token + * @param _rewardToken the reward token address + * @param _user the user address + */ + function getUserAccumulatedReward( + address _poolToken, + address _rewardToken, + address _user + ) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + return _getUserAccumulatedReward(poolId, _rewardToken, _user); + } + + /** + * @notice returns estimated reward + * @param _poolToken the address of pool token + * @param _rewardToken the reward token address + * @param _amount the amount of tokens to be deposited + * @param _duration the duration of liquidity providing in seconds + */ + function getEstimatedReward( + address _poolToken, + address _rewardToken, + uint256 _amount, + uint256 _duration + ) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + uint256 start = block.number; + uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); + (, uint256 accumulatedRewardPerShare) = + _getPoolAccumulatedReward( + pool, + _amount, + rewardTokensMap[_rewardToken], + poolInfoRewardTokensMap[poolId][_rewardToken], + start, + end + ); + return _amount.mul(accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Updates reward variables for all pools. + * @dev Be careful of gas spending! + */ + function updateAllPools() public { + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + _updatePool(i); + } + } + + /** + * @notice Updates reward variables of the given pool to be up-to-date + * @param _poolToken the address of pool token + */ + function updatePool(address _poolToken) public { + uint256 poolId = _getPoolId(_poolToken); + _updatePool(poolId); + } + + function _updatePool(uint256 _poolId) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + _updatePoolRewardToken(pool, _poolId, pool.rewardTokens[i]); + } + } + + function _updatePoolRewardToken( + PoolInfo storage pool, + uint256 _poolId, + address _rewardToken + ) internal { + PoolInfoRewardToken storage poolRewardToken = + poolInfoRewardTokensMap[_poolId][_rewardToken]; + // this pool has been updated recently + if (block.number <= poolRewardToken.lastRewardBlock) { + return; + } + + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (poolTokenBalance == 0) { + poolRewardToken.lastRewardBlock = block.number; + return; + } + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + poolRewardToken.accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare.add( + accumulatedRewardPerShare_ + ); + poolRewardToken.lastRewardBlock = block.number; + + rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.add(accumulatedReward_); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + PoolInfoRewardToken storage _poolRewardToken, + RewardToken storage _rewardToken + ) internal view returns (uint256, uint256) { + return _getPoolAccumulatedReward(_pool, 0, _rewardToken, _poolRewardToken, block.number); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + uint256 _additionalAmount, + RewardToken storage _rewardToken, + PoolInfoRewardToken storage _poolRewardToken, + uint256 _endBlock + ) internal view returns (uint256, uint256) { + return + _getPoolAccumulatedReward( + _pool, + _additionalAmount, + _rewardToken, + _poolRewardToken, + _poolRewardToken.lastRewardBlock, + _endBlock + ); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + uint256 _additionalAmount, + RewardToken storage _rewardToken, + PoolInfoRewardToken storage _poolRewardToken, + uint256 _startBlock, + uint256 _endBlock + ) internal view returns (uint256, uint256) { + uint256 passedBlocks = _getPassedBlocks(_rewardToken, _startBlock, _endBlock); + uint256 accumulatedReward = + passedBlocks + .mul(_rewardToken.rewardTokensPerBlock) + .mul(PRECISION) + .mul(_poolRewardToken.allocationPoint) + .div(_rewardToken.totalAllocationPoint); + + uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); + poolTokenBalance = poolTokenBalance.add(_additionalAmount); + uint256 accumulatedRewardPerShare = accumulatedReward.div(poolTokenBalance); + return (accumulatedReward.div(PRECISION), accumulatedRewardPerShare); + } + + /** + * @notice deposits pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it or to msg.sender + */ + function deposit( + address _poolToken, + uint256 _amount, + address _user + ) external onlyAfterMigrationFinished { + _deposit(_poolToken, _amount, _user, false); + } + + /** + * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit + * @dev only callable by the pool which issues the tokens + * @param _user the user address + * @param _amount the minted amount + */ + function onTokensDeposited(address _user, uint256 _amount) external { + //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert + _deposit(msg.sender, _amount, _user, true); + } + + /** + * @notice internal function for depositing pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it + * @param alreadyTransferred true if the pool tokens have already been transferred + */ + function _deposit( + address _poolToken, + uint256 _amount, + address _user, + bool alreadyTransferred + ) internal { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _user != address(0) ? _user : msg.sender; + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updatePoolRewardToken(pool, poolId, rewardTokenAddress); + //sends reward directly to the user + _updateReward(poolId, rewardTokenAddress, user); + } + + if (_amount > 0) { + //receives pool tokens from msg.sender, it can be user or WrapperProxy contract + if (!alreadyTransferred) + pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); + user.amount = user.amount.add(_amount); + } + + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updateRewardDebt(poolId, rewardTokenAddress, user); + } + emit Deposit(userAddress, _poolToken, _amount); + } + + /** + * @notice transfers reward tokens + * @param _poolToken the address of pool token + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimRewards(address _poolToken, address _user) external { + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _claimReward(poolId, rewardTokenAddress, userAddress); + } + } + + /** + * @notice transfers rewards from a specific reward token + * @param _poolToken the address of pool token + * @param _rewardToken the address of reward token + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimReward( + address _poolToken, + address _rewardToken, + address _user + ) external { + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + _claimReward(poolId, _rewardToken, userAddress); + } + + function _claimReward( + uint256 _poolId, + address _rewardToken, + address _userAddress + ) internal { + UserInfo storage user = userInfoMap[_poolId][_userAddress]; + PoolInfo storage pool = poolInfoList[_poolId]; + + _updatePoolRewardToken(pool, _poolId, _rewardToken); + _updateReward(_poolId, _rewardToken, user); + _transferReward(_rewardToken, user, _userAddress, false, true); + _updateRewardDebt(_poolId, _rewardToken, user); + } + + /** + * @notice transfers reward tokens from all pools + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimRewardFromAllPools(address _user) external { + address userAddress = _getUserAddress(_user); + + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + uint256 poolId = i; + PoolInfo storage pool = poolInfoList[poolId]; + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 j = 0; j < rewardTokensLength; j++) { + address rewardTokenAddress = pool.rewardTokens[j]; + _claimReward(poolId, rewardTokenAddress, userAddress); + } + } + } + + /** + * @notice withdraws pool tokens and transfers reward tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) + */ + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external onlyAfterMigrationFinished { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + require(user.amount >= _amount, "Not enough balance"); + + // Start collecting rewards for each reward token the user holds + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updatePoolRewardToken(pool, poolId, rewardTokenAddress); + _updateReward(poolId, rewardTokenAddress, user); + _transferReward(rewardTokenAddress, user, userAddress, true, false); + } + user.amount = user.amount.sub(_amount); + + //msg.sender is wrapper -> send to wrapper + if (msg.sender == wrapper) { + pool.poolToken.safeTransfer(address(msg.sender), _amount); + } + //msg.sender is user or pool token (lending pool) -> send to user + else { + pool.poolToken.safeTransfer(userAddress, _amount); + } + + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updateRewardDebt(poolId, rewardTokenAddress, user); + } + emit Withdraw(userAddress, _poolToken, _amount); + } + + function _getUserAddress(address _user) internal view returns (address) { + address userAddress = msg.sender; + if (_user != address(0)) { + //only wrapper can pass _user parameter + require( + msg.sender == wrapper || poolIdList[msg.sender] != 0, + "only wrapper or pools may withdraw for a user" + ); + userAddress = _user; + } + return userAddress; + } + + function _updateReward( + uint256 _poolId, + address _rewardTokenAddress, + UserInfo storage user + ) internal { + UserReward storage reward = user.rewards[_rewardTokenAddress]; + //update user accumulated reward + if (user.amount > 0) { + //add reward for the previous amount of deposited tokens + uint256 accumulatedReward = + user + .amount + .mul( + poolInfoRewardTokensMap[_poolId][_rewardTokenAddress] + .accumulatedRewardPerShare + ) + .div(PRECISION) + .sub(reward.rewardDebt); + reward.accumulatedReward = reward.accumulatedReward.add(accumulatedReward); + } + } + + function _updateRewardDebt( + uint256 poolId, + address rewardToken, + UserInfo storage user + ) internal { + //reward accumulated before amount update (should be subtracted during next reward calculation) + user.rewards[rewardToken].rewardDebt = user + .amount + .mul(poolInfoRewardTokensMap[poolId][rewardToken].accumulatedRewardPerShare) + .div(PRECISION); + } + + /** + * @notice Send reward in SOV to the lockedSOV vault. + * @param _user The user info, to get its reward share. + * @param _userAddress The address of the user, to send SOV in its behalf. + * @param _isWithdrawal The flag whether determines if the user is withdrawing all the funds + * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough + */ + function _transferReward( + address _rewardToken, + UserInfo storage _user, + address _userAddress, + bool _isWithdrawal, + bool _isCheckingBalance + ) internal { + uint256 userAccumulatedReward = _user.rewards[_rewardToken].accumulatedReward; + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + IERC20 token = IERC20(_rewardToken); + /// @dev Transfer if enough token balance on this LM contract. + uint256 balance = token.balanceOf(address(this)); + if (balance >= userAccumulatedReward) { + rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.sub( + userAccumulatedReward + ); + _user.rewards[_rewardToken].accumulatedReward = 0; + + IRewardTransferLogic transferLogic = rewardToken.rewardTransferLogic; + require( + token.approve(transferLogic.senderToAuthorize(), userAccumulatedReward), + "Approve failed" + ); + transferLogic.transferReward(_userAddress, userAccumulatedReward, _isWithdrawal); + /// @dev Event log. + emit RewardClaimed(_userAddress, _rewardToken, userAccumulatedReward); + } else { + require(!_isCheckingBalance, "Claiming reward failed"); + } + } + + /** + * @notice withdraws pool tokens without transferring reward tokens + * @param _poolToken the address of pool token + * @dev EMERGENCY ONLY + */ + function emergencyWithdraw(address _poolToken) external onlyAfterMigrationFinished { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][msg.sender]; + + uint256 rewardTokensLength = pool.rewardTokens.length; + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updatePoolRewardToken(pool, poolId, rewardTokenAddress); + _updateReward(poolId, rewardTokenAddress, user); + // substract user balance from total balance for each reward token + UserReward storage userReward = user.rewards[pool.rewardTokens[i]]; + uint256 accumulatedReward = userReward.accumulatedReward; + RewardToken storage rewardToken = rewardTokensMap[pool.rewardTokens[i]]; + rewardToken.totalUsersBalance = rewardToken.totalUsersBalance.sub(accumulatedReward); + emit EmergencyWithdraw( + msg.sender, + _poolToken, + rewardTokenAddress, + user.amount, + accumulatedReward + ); + + userReward.rewardDebt = 0; + userReward.accumulatedReward = 0; + } + + uint256 userAmount = user.amount; + user.amount = 0; + pool.poolToken.safeTransfer(address(msg.sender), userAmount); + + for (uint256 i = 0; i < rewardTokensLength; i++) { + address rewardTokenAddress = pool.rewardTokens[i]; + _updateRewardDebt(poolId, rewardTokenAddress, user); + } + } + + function getRewardToken(address _rewardToken) external view returns (RewardToken memory) { + return rewardTokensMap[_rewardToken]; + } + + /** + * @notice returns a list of PoolInfoRewardToken for the given pool + * @param _poolToken the address of pool token + */ + function getPoolRewards(address _poolToken) + external + view + returns (PoolInfoRewardToken[] memory) + { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo memory poolInfo = poolInfoList[poolId]; + uint256 rewardsLength = poolInfo.rewardTokens.length; + PoolInfoRewardToken[] memory rewards = new PoolInfoRewardToken[](rewardsLength); + for (uint256 i = 0; i < rewardsLength; i++) { + rewards[i] = poolInfoRewardTokensMap[poolId][poolInfo.rewardTokens[i]]; + } + return rewards; + } + + /** + * @notice returns a PoolInfoRewardToken for the given pool and reward token + * @param _poolToken the address of pool token + * @param _rewardToken the address of reward token + */ + function getPoolReward(address _poolToken, address _rewardToken) + external + view + returns (PoolInfoRewardToken memory) + { + uint256 poolId = _getPoolId(_poolToken); + return poolInfoRewardTokensMap[poolId][_rewardToken]; + } + + /** + * @notice returns pool id + * @param _poolToken the address of pool token + */ + function getPoolId(address _poolToken) external view returns (uint256) { + return _getPoolId(_poolToken); + } + + function _getPoolId(address _poolToken) internal view returns (uint256) { + uint256 poolId = poolIdList[_poolToken]; + require(poolId > 0, "Pool token not found"); + return poolId - 1; + } + + /** + * @notice returns count of pool tokens + */ + function getPoolLength() external view returns (uint256) { + return poolInfoList.length; + } + + /** + * @notice returns list of pool token's info + */ + function getPoolInfoList() external view returns (PoolInfo[] memory) { + return poolInfoList; + } + + /** + * @notice returns pool info for the given token + * @param _poolToken the address of pool token + */ + function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return poolInfoList[poolId]; + } + + struct UserBalance { + uint256 amount; + address rewardToken; + uint256 accumulatedReward; + } + + /** + * @notice returns list of [amount, rewardToken, accumulatedReward] for the given user for each pool token and reward token + * @param _user the address of the user + */ + function getUserBalanceList(address _user) external view returns (UserBalance[][] memory) { + uint256 length = poolInfoList.length; + UserBalance[][] memory userBalanceList = new UserBalance[][](length); + for (uint256 i = 0; i < length; i++) { + PoolInfo memory poolInfo = poolInfoList[i]; + uint256 rewardLength = poolInfo.rewardTokens.length; + userBalanceList[i] = new UserBalance[](rewardLength); + for (uint256 j = 0; j < rewardLength; j++) { + address _rewardToken = poolInfo.rewardTokens[j]; + userBalanceList[i][j].amount = userInfoMap[i][_user].amount; + userBalanceList[i][j].rewardToken = _rewardToken; + userBalanceList[i][j].accumulatedReward = _getUserAccumulatedReward( + i, + _rewardToken, + _user + ); + } + } + return userBalanceList; + } + + struct PoolUserInfo { + uint256 amount; + UserReward[] rewards; + } + + /** + * @notice returns UserInfo for the given pool and user + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserInfo(address _poolToken, address _user) + external + view + returns (PoolUserInfo memory) + { + uint256 poolId = _getPoolId(_poolToken); + return _getPoolUserInfo(poolId, _user); + } + + /** + * @notice returns list of UserInfo for the given user for each pool token + * @param _user the address of the user + */ + function getUserInfoList(address _user) external view returns (PoolUserInfo[] memory) { + uint256 length = poolInfoList.length; + PoolUserInfo[] memory userInfoList = new PoolUserInfo[](length); + for (uint256 i = 0; i < length; i++) { + userInfoList[i] = _getPoolUserInfo(i, _user); + } + return userInfoList; + } + + function _getPoolUserInfo(uint256 _poolId, address _user) + internal + view + returns (PoolUserInfo memory) + { + PoolInfo memory pool = poolInfoList[_poolId]; + uint256 rewardsLength = pool.rewardTokens.length; + UserInfo storage userInfo = userInfoMap[_poolId][_user]; + PoolUserInfo memory poolUserInfo; + poolUserInfo.amount = userInfo.amount; + poolUserInfo.rewards = new UserReward[](rewardsLength); + for (uint256 i = 0; i < rewardsLength; i++) { + poolUserInfo.rewards[i] = userInfo.rewards[pool.rewardTokens[i]]; + } + return poolUserInfo; + } + + struct UserAccumulatedReward { + address rewardToken; + uint256 accumulatedReward; + } + + /** + * @notice returns accumulated reward for the given user for each pool token and reward token + * @param _user the address of the user + */ + function getUserAccumulatedRewardList(address _user) + external + view + returns (UserAccumulatedReward[][] memory) + { + uint256 length = poolInfoList.length; + UserAccumulatedReward[][] memory rewardList = new UserAccumulatedReward[][](length); + for (uint256 i = 0; i < length; i++) { + PoolInfo memory poolInfo = poolInfoList[i]; + uint256 rewardsLength = poolInfo.rewardTokens.length; + rewardList[i] = new UserAccumulatedReward[](rewardsLength); + for (uint256 j = 0; j < rewardsLength; j++) { + rewardList[i][j].rewardToken = poolInfo.rewardTokens[j]; + rewardList[i][j].accumulatedReward = _getUserAccumulatedReward( + i, + poolInfo.rewardTokens[j], + _user + ); + } + } + return rewardList; + } + + /** + * @notice returns the pool token balance a user has on the contract + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserPoolTokenBalance(address _poolToken, address _user) + external + view + returns (uint256) + { + uint256 poolId = _getPoolId(_poolToken); + return userInfoMap[poolId][_user].amount; + } + + function setPoolInfoRewardToken( + address _poolToken, + address _rewardToken, + uint256 _lastRewardBlock, + uint256 _accumulatedRewardPerShare + ) external onlyMigrator { + uint256 poolId = _getPoolId(_poolToken); + PoolInfoRewardToken storage poolInfoRewardToken = + poolInfoRewardTokensMap[poolId][_rewardToken]; + poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; + poolInfoRewardToken.accumulatedRewardPerShare = _accumulatedRewardPerShare; + } + + function setRewardToken( + address _rewardToken, + uint256 _startBlock, + uint256 _totalUsersBalance + ) external onlyMigrator { + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + rewardToken.startBlock = _startBlock; + rewardToken.totalUsersBalance = _totalUsersBalance; + } + + function setUserInfo( + uint256 _poolId, + address _user, + address _rewardToken, + uint256 _amount, + uint256 _rewardDebt, + uint256 _accumulatedReward + ) external onlyMigrator { + UserInfo storage userInfo = userInfoMap[_poolId][_user]; + UserReward storage userReward = userInfo.rewards[_rewardToken]; + userInfo.amount += _amount; + userReward.rewardDebt += _rewardDebt; + userReward.accumulatedReward += _accumulatedReward; + } + + /** + * @notice finish migration + */ + function finishMigration() external onlyMigrator { + migrationFinished = true; + } } diff --git a/contracts/farm/LockedSOVRewardTransferLogic.sol b/contracts/farm/LockedSOVRewardTransferLogic.sol index 041f485fd..486e69468 100644 --- a/contracts/farm/LockedSOVRewardTransferLogic.sol +++ b/contracts/farm/LockedSOVRewardTransferLogic.sol @@ -7,60 +7,72 @@ import "../locked/ILockedSOV.sol"; import "../openzeppelin/ERC20.sol"; import "../openzeppelin/SafeERC20.sol"; -contract LockedSOVRewardTransferLogic is IRewardTransferLogic, LockedSOVRewardTransferLogicStorage { - using SafeERC20 for IERC20; +contract LockedSOVRewardTransferLogic is + IRewardTransferLogic, + LockedSOVRewardTransferLogicStorage +{ + using SafeERC20 for IERC20; - event LockedSOVChanged(address _newAddress); - event UnlockImmediatelyPercentChanged(uint256 _newAmount); + event LockedSOVChanged(address _newAddress); + event UnlockImmediatelyPercentChanged(uint256 _newAmount); - /** - * @param _lockedSOV The contract instance address of the lockedSOV vault. - * SOV rewards are not paid directly to liquidity providers. Instead they - * are deposited into a lockedSOV vault contract. - * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. - */ - function initialize(address _lockedSOV, uint256 _unlockedImmediatelyPercent) external onlyAuthorized { - changeLockedSOV(_lockedSOV); - changeUnlockedImmediatelyPercent(_unlockedImmediatelyPercent); - } + /** + * @param _lockedSOV The contract instance address of the lockedSOV vault. + * SOV rewards are not paid directly to liquidity providers. Instead they + * are deposited into a lockedSOV vault contract. + * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. + */ + function initialize(address _lockedSOV, uint256 _unlockedImmediatelyPercent) + external + onlyAuthorized + { + changeLockedSOV(_lockedSOV); + changeUnlockedImmediatelyPercent(_unlockedImmediatelyPercent); + } - /** - * @param _newLockedSOV The contract instance address of the lockedSOV vault. - */ - function changeLockedSOV(address _newLockedSOV) public onlyAuthorized { - require(_newLockedSOV != address(0), "Invalid address"); - lockedSOV = ILockedSOV(_newLockedSOV); - emit LockedSOVChanged(_newLockedSOV); - } + /** + * @param _newLockedSOV The contract instance address of the lockedSOV vault. + */ + function changeLockedSOV(address _newLockedSOV) public onlyAuthorized { + require(_newLockedSOV != address(0), "Invalid address"); + lockedSOV = ILockedSOV(_newLockedSOV); + emit LockedSOVChanged(_newLockedSOV); + } - /** - * @param _newUnlockedImmediatelyPercent The new unlocked immediately percent. - */ - function changeUnlockedImmediatelyPercent(uint256 _newUnlockedImmediatelyPercent) public onlyAuthorized { - require(_newUnlockedImmediatelyPercent < 10000, "Unlocked immediately percent has to be less than 10000."); - unlockedImmediatelyPercent = _newUnlockedImmediatelyPercent; - emit UnlockImmediatelyPercentChanged(_newUnlockedImmediatelyPercent); - } + /** + * @param _newUnlockedImmediatelyPercent The new unlocked immediately percent. + */ + function changeUnlockedImmediatelyPercent(uint256 _newUnlockedImmediatelyPercent) + public + onlyAuthorized + { + require( + _newUnlockedImmediatelyPercent < 10000, + "Unlocked immediately percent has to be less than 10000." + ); + unlockedImmediatelyPercent = _newUnlockedImmediatelyPercent; + emit UnlockImmediatelyPercentChanged(_newUnlockedImmediatelyPercent); + } - function getRewardTokenAddress() external view returns (address) { - return lockedSOV.SOV(); - } + function getRewardTokenAddress() external view returns (address) { + return lockedSOV.SOV(); + } - function senderToAuthorize() external view returns (address) { - return address(this); - } + function senderToAuthorize() external view returns (address) { + return address(this); + } - function transferReward( - address _to, - uint256 _value, - bool _isWithdrawal - ) external { - IERC20 token = IERC20(lockedSOV.SOV()); - require(token.transferFrom(msg.sender, address(this), _value), "Transfer failed"); - require(token.approve(address(lockedSOV), _value), "Approve failed"); - lockedSOV.deposit(_to, _value, unlockedImmediatelyPercent); - if (!_isWithdrawal) { - lockedSOV.withdrawAndStakeTokensFrom(_to); - } - } + function transferReward( + address _to, + uint256 _value, + bool _isWithdrawal + ) external { + IERC20 token = IERC20(lockedSOV.SOV()); + require(token.transferFrom(msg.sender, address(this), _value), "Transfer failed"); + require(token.approve(address(lockedSOV), _value), "Approve failed"); + lockedSOV.deposit(_to, _value, unlockedImmediatelyPercent); + if (!_isWithdrawal) { + lockedSOV.withdrawAndStakeTokensFrom(_to); + } + } } diff --git a/contracts/farm/LockedSOVRewardTransferLogicStorage.sol b/contracts/farm/LockedSOVRewardTransferLogicStorage.sol index 777878b5c..fae79fd84 100644 --- a/contracts/farm/LockedSOVRewardTransferLogicStorage.sol +++ b/contracts/farm/LockedSOVRewardTransferLogicStorage.sol @@ -4,10 +4,10 @@ import "../locked/ILockedSOV.sol"; import "../utils/AdminRole.sol"; contract LockedSOVRewardTransferLogicStorage is AdminRole { - /// LockedSOV vault when reward transfer is performed - ILockedSOV public lockedSOV; + /// LockedSOV vault when reward transfer is performed + ILockedSOV public lockedSOV; - /// Determines the amount of tokens that will be unlocked and ready - /// to be withdrawn - uint256 public unlockedImmediatelyPercent; + /// Determines the amount of tokens that will be unlocked and ready + /// to be withdrawn + uint256 public unlockedImmediatelyPercent; } diff --git a/contracts/mockup/LiquidityMiningV1Mockup.sol b/contracts/mockup/LiquidityMiningV1Mockup.sol index 022842368..7a33dd362 100644 --- a/contracts/mockup/LiquidityMiningV1Mockup.sol +++ b/contracts/mockup/LiquidityMiningV1Mockup.sol @@ -4,13 +4,17 @@ pragma experimental ABIEncoderV2; import "../farm/LiquidityMiningV1.sol"; contract LiquidityMiningV1Mockup is LiquidityMiningV1 { - function getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) public view returns (uint256) { - return _getPassedBlocksWithBonusMultiplier(_from, _to); - } + function getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) + public + view + returns (uint256) + { + return _getPassedBlocksWithBonusMultiplier(_from, _to); + } - function getPoolAccumulatedReward(address _poolToken) public view returns (uint256, uint256) { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - return _getPoolAccumulatedReward(pool); - } + function getPoolAccumulatedReward(address _poolToken) public view returns (uint256, uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + return _getPoolAccumulatedReward(pool); + } } diff --git a/contracts/mockup/LiquidityMiningV2Mockup.sol b/contracts/mockup/LiquidityMiningV2Mockup.sol index 96a406a36..88caa168c 100644 --- a/contracts/mockup/LiquidityMiningV2Mockup.sol +++ b/contracts/mockup/LiquidityMiningV2Mockup.sol @@ -4,11 +4,16 @@ pragma experimental ABIEncoderV2; import "../farm/LiquidityMiningV2.sol"; contract LiquidityMiningMockupV2 is LiquidityMiningV2 { - function getPoolAccumulatedReward(address _poolToken, address _rewardToken) external view returns (uint256, uint256) { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[poolId][_rewardToken]; - RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - return _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); - } + function getPoolAccumulatedReward(address _poolToken, address _rewardToken) + external + view + returns (uint256, uint256) + { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + PoolInfoRewardToken storage poolRewardToken = + poolInfoRewardTokensMap[poolId][_rewardToken]; + RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; + return _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + } } diff --git a/contracts/mockup/RBTCWrapperProxyMockupV2.sol b/contracts/mockup/RBTCWrapperProxyMockupV2.sol index 34af16ffd..aa18454e2 100644 --- a/contracts/mockup/RBTCWrapperProxyMockupV2.sol +++ b/contracts/mockup/RBTCWrapperProxyMockupV2.sol @@ -3,21 +3,21 @@ pragma solidity 0.5.17; import "../farm/LiquidityMiningV2.sol"; contract RBTCWrapperProxyMockupV2 { - LiquidityMiningV2 public liquidityMining; + LiquidityMiningV2 public liquidityMining; - constructor(LiquidityMiningV2 _liquidityMining) public { - liquidityMining = _liquidityMining; - } + constructor(LiquidityMiningV2 _liquidityMining) public { + liquidityMining = _liquidityMining; + } - function claimReward(address _poolToken) public { - liquidityMining.claimRewards(_poolToken, msg.sender); - } + function claimReward(address _poolToken) public { + liquidityMining.claimRewards(_poolToken, msg.sender); + } - function claimRewardFromAllPools() public { - liquidityMining.claimRewardFromAllPools(msg.sender); - } + function claimRewardFromAllPools() public { + liquidityMining.claimRewardFromAllPools(msg.sender); + } - function withdraw(address _poolToken, uint256 _amount) public { - liquidityMining.withdraw(_poolToken, _amount, msg.sender); - } + function withdraw(address _poolToken, uint256 _amount) public { + liquidityMining.withdraw(_poolToken, _amount, msg.sender); + } } diff --git a/contracts/mockup/TestPoolToken.sol b/contracts/mockup/TestPoolToken.sol index 57c56ad64..09f65b14e 100644 --- a/contracts/mockup/TestPoolToken.sol +++ b/contracts/mockup/TestPoolToken.sol @@ -4,23 +4,23 @@ import "../farm/LiquidityMiningV2.sol"; import "../testhelpers/TestToken.sol"; contract TestPoolToken is TestToken { - LiquidityMiningV2 private liquidityMining; + LiquidityMiningV2 private liquidityMining; - constructor( - string memory _name, - string memory _symbol, - uint8 _decimals, - uint256 _initialAmount, - address _liquidityMining - ) public TestToken(_name, _symbol, _decimals, _initialAmount) { - setLiquidityMining(_liquidityMining); - } + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _initialAmount, + address _liquidityMining + ) public TestToken(_name, _symbol, _decimals, _initialAmount) { + setLiquidityMining(_liquidityMining); + } - function setLiquidityMining(address liquidityMining_) public { - liquidityMining = LiquidityMiningV2(liquidityMining_); - } + function setLiquidityMining(address liquidityMining_) public { + liquidityMining = LiquidityMiningV2(liquidityMining_); + } - function depositFor(address _user, uint256 _amount) external { - liquidityMining.onTokensDeposited(_user, _amount); - } + function depositFor(address _user, uint256 _amount) external { + liquidityMining.onTokensDeposited(_user, _amount); + } } diff --git a/tests/farm/ERC20TransferLogic.js b/tests/farm/ERC20TransferLogic.js index 77f08f963..53f231127 100644 --- a/tests/farm/ERC20TransferLogic.js +++ b/tests/farm/ERC20TransferLogic.js @@ -9,128 +9,145 @@ const TestToken = artifacts.require("TestToken"); const ERC20TransferLogic = artifacts.require("ERC20TransferLogic"); describe("ERC20TransferLogic", () => { - // The % which determines how much will be unlocked immediately. - /// @dev 10000 is 100% - const unlockedImmediatelyPercent = new BN(1000); //10% - - let accounts; - let root, account1, account2, account3, account4; - let token1, token2, token3; - let transferLogic; - - before(async () => { - accounts = await web3.eth.getAccounts(); - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - - transferLogic = await ERC20TransferLogic.new(); - await transferLogic.initialize(token1.address); - }); - - describe("initialize", () => { - it("fails if not an owner or admin", async () => { - transferLogic = await ERC20TransferLogic.new(); - await expectRevert(transferLogic.initialize(token1.address, { from: account1 }), "unauthorized"); - - await transferLogic.addAdmin(account1); - await transferLogic.initialize(token1.address, { from: account1 }); - }); - - it("sets the expected values", async () => { - transferLogic = await ERC20TransferLogic.new(); - await transferLogic.initialize(token1.address); - let _tokenAddress = await transferLogic.token(); - expect(_tokenAddress).equal(token1.address); - }); - }); - - describe("setTokenAddress", () => { - it("fails if not an owner or admin", async () => { - await expectRevert(transferLogic.setTokenAddress(token1.address, { from: account1 }), "unauthorized"); - - await transferLogic.addAdmin(account1); - await transferLogic.setTokenAddress(token1.address, { from: account1 }); - }); - - it("fails if invalid address", async () => { - await expectRevert(transferLogic.setTokenAddress(ZERO_ADDRESS), "Invalid token address"); - }); - - it("should set a new token address", async () => { - //first check original token address - let oldTokenAddress = await transferLogic.token(); - expect(oldTokenAddress).equal(token1.address); - - //then check new token addres - tx = await transferLogic.setTokenAddress(token2.address); - let newTokenAddress = await transferLogic.token(); - expect(newTokenAddress).equal(token2.address); - - expectEvent(tx, "TokenAddressUpdated", { - _newTokenAddress: newTokenAddress, - }); - }); - }); - - describe("getRewardTokenAddress", async () => { - it("should return token1 address", async () => { - let _tokenAddress = await transferLogic.getRewardTokenAddress(); - expect(_tokenAddress).equal(token1.address); - }); - }); - - describe("senderToAuthorize", async () => { - it("should return contract address", async () => { - let transferLogicAddress = await transferLogic.senderToAuthorize(); - expect(transferLogicAddress).equal(transferLogic.address); - }); - }); - - describe("transferReward", async () => { - const account1InitialBalance = new BN(100); - const amountToTransfer = new BN(50); - - it("fails if account doesn't have reward tokens", async () => { - await expectRevert( - transferLogic.transferReward(account2, new BN(5), false, { from: account1 }), - "SafeERC20: low-level call failed" - ); - }); - - it("fails if account didn't approve before", async () => { - //send some token1 to account1 to be able to transfer - await token1.mint(account1, new BN(10)); - await expectRevert( - transferLogic.transferReward(account2, new BN(5), false, { from: account1 }), - "SafeERC20: low-level call failed" - ); - }); - - it("fails if invalid address to transfer", async () => { - //send some token1 to account1 to be able to transfer - await token1.mint(account1, account1InitialBalance); - await token1.approve(transferLogic.address, account1InitialBalance, { from: account1 }); - await expectRevert( - transferLogic.transferReward(ZERO_ADDRESS, new BN(5), false, { from: account1 }), - "SafeERC20: low-level call failed" - ); - }); - - it("should account1 transfer reward to account2", async () => { - //send some token1 to account1 to be able to transfer - await token1.mint(account1, account1InitialBalance); - await token1.approve(transferLogic.address, account1InitialBalance, { from: account1 }); - - await transferLogic.transferReward(account2, amountToTransfer, false, { from: account1 }); - let account1FinalBalance = await token1.balanceOf(account1); - let account2FinalBalance = await token1.balanceOf(account2); - expect(account1FinalBalance).bignumber.equal(account1InitialBalance.sub(amountToTransfer)); - expect(account2FinalBalance).bignumber.equal(amountToTransfer); - }); - }); + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let root, account1, account2, account3, account4; + let token1, token2, token3; + let transferLogic; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + + transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + }); + + describe("initialize", () => { + it("fails if not an owner or admin", async () => { + transferLogic = await ERC20TransferLogic.new(); + await expectRevert( + transferLogic.initialize(token1.address, { from: account1 }), + "unauthorized" + ); + + await transferLogic.addAdmin(account1); + await transferLogic.initialize(token1.address, { from: account1 }); + }); + + it("sets the expected values", async () => { + transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + let _tokenAddress = await transferLogic.token(); + expect(_tokenAddress).equal(token1.address); + }); + }); + + describe("setTokenAddress", () => { + it("fails if not an owner or admin", async () => { + await expectRevert( + transferLogic.setTokenAddress(token1.address, { from: account1 }), + "unauthorized" + ); + + await transferLogic.addAdmin(account1); + await transferLogic.setTokenAddress(token1.address, { from: account1 }); + }); + + it("fails if invalid address", async () => { + await expectRevert( + transferLogic.setTokenAddress(ZERO_ADDRESS), + "Invalid token address" + ); + }); + + it("should set a new token address", async () => { + //first check original token address + let oldTokenAddress = await transferLogic.token(); + expect(oldTokenAddress).equal(token1.address); + + //then check new token addres + tx = await transferLogic.setTokenAddress(token2.address); + let newTokenAddress = await transferLogic.token(); + expect(newTokenAddress).equal(token2.address); + + expectEvent(tx, "TokenAddressUpdated", { + _newTokenAddress: newTokenAddress, + }); + }); + }); + + describe("getRewardTokenAddress", async () => { + it("should return token1 address", async () => { + let _tokenAddress = await transferLogic.getRewardTokenAddress(); + expect(_tokenAddress).equal(token1.address); + }); + }); + + describe("senderToAuthorize", async () => { + it("should return contract address", async () => { + let transferLogicAddress = await transferLogic.senderToAuthorize(); + expect(transferLogicAddress).equal(transferLogic.address); + }); + }); + + describe("transferReward", async () => { + const account1InitialBalance = new BN(100); + const amountToTransfer = new BN(50); + + it("fails if account doesn't have reward tokens", async () => { + await expectRevert( + transferLogic.transferReward(account2, new BN(5), false, { from: account1 }), + "SafeERC20: low-level call failed" + ); + }); + + it("fails if account didn't approve before", async () => { + //send some token1 to account1 to be able to transfer + await token1.mint(account1, new BN(10)); + await expectRevert( + transferLogic.transferReward(account2, new BN(5), false, { from: account1 }), + "SafeERC20: low-level call failed" + ); + }); + + it("fails if invalid address to transfer", async () => { + //send some token1 to account1 to be able to transfer + await token1.mint(account1, account1InitialBalance); + await token1.approve(transferLogic.address, account1InitialBalance, { + from: account1, + }); + await expectRevert( + transferLogic.transferReward(ZERO_ADDRESS, new BN(5), false, { from: account1 }), + "SafeERC20: low-level call failed" + ); + }); + + it("should account1 transfer reward to account2", async () => { + //send some token1 to account1 to be able to transfer + await token1.mint(account1, account1InitialBalance); + await token1.approve(transferLogic.address, account1InitialBalance, { + from: account1, + }); + + await transferLogic.transferReward(account2, amountToTransfer, false, { + from: account1, + }); + let account1FinalBalance = await token1.balanceOf(account1); + let account2FinalBalance = await token1.balanceOf(account2); + expect(account1FinalBalance).bignumber.equal( + account1InitialBalance.sub(amountToTransfer) + ); + expect(account2FinalBalance).bignumber.equal(amountToTransfer); + }); + }); }); diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index d51e7cd02..b30d0f5b6 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -43,1307 +43,1549 @@ const SwapsImplLocal = artifacts.require("SwapsImplLocal"); const wei = web3.utils.toWei; describe("LiquidityMiningMigration", () => { - const name = "Test SOV Token"; - const symbol = "TST"; - - const PRECISION = 1e12; - - const rewardTokensPerBlock = new BN(3); - const startDelayBlocks = new BN(1); - const numberOfBonusBlocks = new BN(50); - - // The % which determines how much will be unlocked immediately. - /// @dev 10000 is 100% - const unlockedImmediatelyPercent = new BN(1000); //10% - - let accounts; - let sovryn, loanToken, loanTokenWRBTC; - let lender, account1, account2, account3, account4, account5, account6, account7, account8, account9; - let SOVToken, token1, token2, token3, token4, token5, token6, token7, token8, liquidityMiningConfigToken; - let liquidityMiningProxy, liquidityMining, liquidityMiningV2, migrator, wrapper; - let rewardTransferLogic, lockedSOVAdmins, lockedSOV; - let erc20RewardTransferLogic; - let allocationPoint = new BN(10); - - const MigrationStates = { - MigratingPools: 0, - MigratingUsers: 1, - MigratingFunds: 2, - MigrationFinished: 3, - }; - - before(async () => { - accounts = await web3.eth.getAccounts(); - [lender, account1, account2, account3, ...accounts] = accounts; - }); - - beforeEach(async () => { - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - token4 = await TestToken.new("Test token 4", "TST-4", 18, TOTAL_SUPPLY); - token5 = await TestToken.new("Test token 5", "TST-5", 18, TOTAL_SUPPLY); - token6 = await TestToken.new("Test token 6", "TST-6", 18, TOTAL_SUPPLY); - token7 = await TestToken.new("Test token 7", "TST-7", 18, TOTAL_SUPPLY); - token8 = await TestToken.new("Test token 8", "TST-8", 18, TOTAL_SUPPLY); - - tokens = [token1, token2, token3, token4, token5, token6, token7, token8]; - - liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); - lockedSOVAdmins = [account1, account2]; - - lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - - await deployProtocol(); - await deployLoanTokens(); - - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - - //set accounts deposits pools in liquidity mining V1 - setAccountsDepositsConstants(); - //mint some tokens to all the accounts - await initializaAccountsTokensBalance(); - //add all poolTokens to liquidityMining - await initializeLiquidityMiningPools(); - //make deposits from accounts to some pools - await initializeLiquidityMiningDeposits(); - - await upgradeLiquidityMining(); - - await deployLiquidityMiningV2(); - - await liquidityMining.initialize(liquidityMiningV2.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - - await liquidityMiningV2.initialize(wrapper.address, migrator.address); - - erc20RewardTransferLogic = await ERC20TransferLogic.new(); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); - - await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - }); - - describe("initializeLiquidityMining", () => { - it("should check all user deposits", async () => { - for (let i = 0; i < accountDeposits.length; i++) { - for (let j = 0; j < accountDeposits[i].deposit.length; j++) { - let poolToken = accountDeposits[i].deposit[j].token; - let poolId = await liquidityMining.getPoolId(poolToken); - let { amount } = await liquidityMining.userInfoMap(poolId, accountDeposits[i].account); - expect(amount).bignumber.equal(accountDeposits[i].deposit[j].amount); - } - } - }); - it("should check all pool have been added", async () => { - const { _poolToken } = await liquidityMining.getPoolInfoListArray(); - for (let i = 0; i < tokens.length; i++) { - expect(_poolToken[i]).equal(tokens[i].address); - } - }); - it("should fail if liquidity mining V2 address is invalid", async () => { - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - await upgradeLiquidityMining(); - - await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); - }); - it("should fail if SOV address is invalid in migrator contract", async () => { - migrator = await Migrator.new(); - await expectRevert( - migrator.initialize(ZERO_ADDRESS, liquidityMining.address, liquidityMiningV2.address), - "invalid token address" - ); - }); - it("should fail if liquidity mining V1 address is invalid in migrator contract", async () => { - migrator = await Migrator.new(); - await expectRevert(migrator.initialize(SOVToken.address, ZERO_ADDRESS, liquidityMiningV2.address), "invalid contract address"); - }); - it("should fail if liquidity mining V2 address is invalid in migrator contract", async () => { - migrator = await Migrator.new(); - await expectRevert(migrator.initialize(SOVToken.address, liquidityMining.address, ZERO_ADDRESS), "invalid contract address"); - }); - it("fails if already initialized", async () => { - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - await expectRevert( - migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address), - "Already initialized" - ); - }); - }); - - describe("migratePools", () => { - it("should only allow to migrate pools by the admin", async () => { - await expectRevert(migrator.migratePools({ from: account1 }), "unauthorized"); - }); - it("should fail if migrator contract was not added as admin in liquidity mining V1", async () => { - await expectRevert(migrator.migratePools(), "unauthorized"); - }); - it("should fail if migrator contract was not added as admin in liquidity mining V2", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await expectRevert(migrator.migratePools(), "unauthorized"); - }); - it("should only allow to migrate pools by migrator contract", async () => { - await deployLiquidityMiningV2(); - await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - - await liquidityMiningV2.initialize(wrapper.address, SOVToken.address); - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await expectRevert(migrator.migratePools(), "only allowed to migrator contract"); - }); - it("should only allow to migrate pools if the migrate grace period started", async () => { - await liquidityMining.addAdmin(migrator.address); - await expectRevert(migrator.migratePools(), "Forbidden: Migration hasn't started yet or already finished"); - }); - it("should only allow to migrate pools once", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await expectRevert(migrator.migratePools(), "Wrong state: should be MigratingPools"); - }); - it("should add pools from liquidityMininigV1", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - for (let i = 0; i < tokens.length; i++) { - let poolToken = await liquidityMiningV2.poolInfoList(i); - expect(poolToken).equal(tokens[i].address); - - let { - allocationPoint: allocationPointV2, - lastRewardBlock: lastRewardBlockV2, - accumulatedRewardPerShare: accumulatedRewardPerShareV2, - } = await liquidityMiningV2.poolInfoRewardTokensMap(i, SOVToken.address); - let { - allocationPoint: allocationPointV1, - lastRewardBlock: lastRewardBlockV1, - accumulatedRewardPerShare: accumulatedRewardPerShareV1, - } = await liquidityMining.poolInfoList(i); - expect(allocationPointV2).bignumber.equal(allocationPointV1); - expect(lastRewardBlockV2).bignumber.equal(lastRewardBlockV1); - expect(accumulatedRewardPerShareV2).bignumber.equal(accumulatedRewardPerShareV1); - - let { startBlock: startBlockV2, totalUsersBalance: totalUsersBalanceV2 } = await liquidityMiningV2.rewardTokensMap( - SOVToken.address - ); - let startBlockV1 = await liquidityMining.startBlock(); - let totalUsersBalanceV1 = await liquidityMining.totalUsersBalance(); - - expect(startBlockV2).bignumber.equal(startBlockV1); - expect(totalUsersBalanceV2).bignumber.equal(totalUsersBalanceV1); - const migrationState = await migrator.migrationState(); - expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingUsers); - } - }); - }); - - describe("migrateUsers", () => { - it("should only allow to migrate users by the admin", async () => { - await expectRevert(migrator.migrateUsers(accounts, { from: account1 }), "unauthorized"); - }); - it("should fail migrating users if pools were not migrated", async () => { - await expectRevert(migrator.migrateUsers(accounts), "Wrong state: should be MigratingUsers"); - }); - it("should fail finishing users migration if pools were not migrated", async () => { - await expectRevert(migrator.finishUsersMigration(), "Wrong state: should be MigratingUsers"); - }); - it("should only allow to finish users migration by the admin", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await expectRevert(migrator.finishUsersMigration({ from: account1 }), "unauthorized"); - }); - it("should only allow to migrate users before finish user migration", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await expectRevert(migrator.migrateUsers(accounts), "Wrong state: should be MigratingUsers"); - }); - it("should save migrated users", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers(accounts); - accounts.forEach(async (account) => { - expect(await migrator.userMigrated(account)); - }); - - await migrator.finishUsersMigration(); - const migrationState = await migrator.migrationState(); - expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); - }); - it("should emit user migrated event", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - tx = await migrator.migrateUsers([accounts[0]]); - expectEvent(tx, "UserMigrated", { - user: accounts[0], - }); - }); - it("should be able to migrate users in differents tx", async () => { - let userInfoV1 = []; - for (let i = 0; i < tokens.length; i++) { - userInfoV1[i] = []; - for (let j = 0; j < accountDeposits.length; j++) { - let userInfo = await liquidityMining.getUserInfo(tokens[i].address, accountDeposits[j].account); - userInfoV1[i][j] = userInfo; - } - } - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - let halfLength = accounts.length / 2; - await migrator.migrateUsers(accounts.slice(0, halfLength)); - - let migrationState = await migrator.migrationState(); - expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingUsers); - - await migrator.migrateUsers(accounts.slice(-halfLength)); - - await migrator.finishUsersMigration(); - migrationState = await migrator.migrationState(); - expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); - - for (let i = 0; i < tokens.length; i++) { - for (let j = 0; j < accountDeposits.length; j++) { - let userInfoV2 = await liquidityMiningV2.getUserInfo(tokens[i].address, accountDeposits[j].account); - - expect(userInfoV2.amount).bignumber.equal(userInfoV1[i][j].amount); - expect(userInfoV2.rewards[0].rewardDebt).bignumber.equal(userInfoV1[i][j].rewardDebt); - expect(userInfoV2.rewards[0].accumulatedReward).bignumber.equal(userInfoV1[i][j].accumulatedReward); - } - } - }); - it("should migrate all accounts with deposits from liquidityMininigV1", async () => { - let userInfoV1 = []; - for (let i = 0; i < tokens.length; i++) { - userInfoV1[i] = []; - for (let j = 0; j < accountDeposits.length; j++) { - let userInfo = await liquidityMining.getUserInfo(tokens[i].address, accountDeposits[j].account); - userInfoV1[i][j] = userInfo; - } - } - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers(accounts); - - await migrator.finishUsersMigration(); - const migrationState = await migrator.migrationState(); - expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); - - for (let i = 0; i < tokens.length; i++) { - for (let j = 0; j < accountDeposits.length; j++) { - let userInfoV2 = await liquidityMiningV2.getUserInfo(tokens[i].address, accountDeposits[j].account); - - expect(userInfoV2.amount).bignumber.equal(userInfoV1[i][j].amount); - expect(userInfoV2.rewards[0].rewardDebt).bignumber.equal(userInfoV1[i][j].rewardDebt); - expect(userInfoV2.rewards[0].accumulatedReward).bignumber.equal(userInfoV1[i][j].accumulatedReward); - } - } - }); - it("should migrate 65 random accounts from liquidityMininigV1", async () => { - let randomAccounts = createRandomAccounts(65); - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers(randomAccounts); - }); - }); - - describe("migrateFunds", () => { - it("should only allow to migrate funds by the admin", async () => { - await expectRevert(migrator.migrateFunds({ from: account1 }), "unauthorized"); - }); - it("should fail migrating funds if users were not migrated", async () => { - await expectRevert(migrator.migrateFunds(), "Wrong state: should be MigratingFunds"); - }); - it("should fail trying to migrate funds without SOV tokens in liquidityMiningV1", async () => { - const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); - await liquidityMining.transferSOV(liquidityMiningV2.address, balanceSOV); - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await expectRevert(migrator.migrateFunds(), "Amount invalid"); - }); - it("should fail trying to migrate funds twice", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - await expectRevert(migrator.migrateFunds(), "Wrong state: should be MigratingFunds"); - }); - it("should fail if liquidity mining V2 is not initialized in liquidity mining V1", async () => { - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - await upgradeLiquidityMining(); - await deployLiquidityMiningV2(); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - - await liquidityMiningV2.initialize(wrapper.address, migrator.address); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await expectRevert(migrator.migrateFunds(), "Address not initialized"); - }); - it("should migrate funds from liquidityMining", async () => { - let SOVBalanceV1Before = await SOVToken.balanceOf(liquidityMining.address); - let SOVBalanceV2Before = await SOVToken.balanceOf(liquidityMiningV2.address); - let tokenBalancesV1Before = []; - let tokenBalancesV2Before = []; - for (let i = 0; i < tokens.length; i++) { - tokenBalancesV1Before.push(await tokens[i].balanceOf(liquidityMining.address)); - tokenBalancesV2Before.push(await tokens[i].balanceOf(liquidityMiningV2.address)); - expect(tokenBalancesV2Before[i]).bignumber.equal(new BN(0)); - } - expect(SOVBalanceV2Before).bignumber.equal(new BN(0)); - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - const migrationState = await migrator.migrationState(); - expect(migrationState.toNumber()).to.equal(MigrationStates.MigrationFinished); - - let SOVBalanceV1After = await SOVToken.balanceOf(liquidityMining.address); - let SOVBalanceV2After = await SOVToken.balanceOf(liquidityMiningV2.address); - let tokenBalancesV1After = []; - let tokenBalancesV2After = []; - for (let i = 0; i < tokens.length; i++) { - tokenBalancesV1After.push(await tokens[i].balanceOf(liquidityMining.address)); - tokenBalancesV2After.push(await tokens[i].balanceOf(liquidityMiningV2.address)); - expect(tokenBalancesV1After[i]).bignumber.equal(new BN(0)); - expect(tokenBalancesV2After[i]).bignumber.equal(tokenBalancesV1Before[i]); - } - expect(SOVBalanceV1After).bignumber.equal(new BN(0)); - expect(SOVBalanceV2After).bignumber.equal(SOVBalanceV1Before); - }); - }); - - describe("withdraws", () => { - it("should withdraw all before migration and revert trying to withdraw after", async () => { - await liquidityMining.withdraw(accountDeposits[0].deposit[0].token, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers(accounts); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - await expectRevert( - liquidityMiningV2.withdraw(accountDeposits[0].deposit[0].token, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }), - "Not enough balance" - ); - }); - it("should withdraw half before migration and withdraw the other half after", async () => { - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - tokenBalanceBefore = await token1.balanceOf(accountDeposits[0].account); - await liquidityMining.withdraw( - accountDeposits[0].deposit[0].token, - accountDeposits[0].deposit[0].amount.div(new BN(2)), - ZERO_ADDRESS, - { from: accountDeposits[0].account } - ); - - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([accountDeposits[0].account]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - await liquidityMiningV2.withdraw( - accountDeposits[0].deposit[0].token, - accountDeposits[0].deposit[0].amount.div(new BN(2)), - ZERO_ADDRESS, - { from: accountDeposits[0].account } - ); - - tokenBalanceAfter = await token1.balanceOf(accountDeposits[0].account); - - expect(tokenBalanceAfter.sub(tokenBalanceBefore)).bignumber.equal(accountDeposits[0].deposit[0].amount); - }); - it("should withdraw all before migration, migrate, deposit and withdraw all again", async () => { - //Re-initialization of liquidity mining contracts - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - new BN(0), - wrapper.address, - lockedSOV.address, - new BN(0) - ); - await upgradeLiquidityMining(); - await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - - await liquidityMiningV2.initialize(wrapper.address, migrator.address); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); - - await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); - - await SOVToken.mint(liquidityMining.address, new BN(1000)); - - await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); - await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - await mineBlocks(20); - - await liquidityMining.withdraw(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - let balanceLockedBefore = await lockedSOV.getLockedBalance(accountDeposits[0].account); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([accountDeposits[0].account]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - await token1.approve(liquidityMiningV2.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); - await liquidityMiningV2.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - await mineBlocks(20); - - await liquidityMiningV2.withdraw(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - let balanceLockedAfter = await lockedSOV.getLockedBalance(accountDeposits[0].account); - - expect(balanceLockedAfter).bignumber.equal(balanceLockedBefore.mul(new BN(2))); - }); - it("should get rewards in liquidity mining V2 after migration", async () => { - //Re-initialization of liquidity mining contracts - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - new BN(0), - wrapper.address, - lockedSOV.address, - new BN(0) - ); - await upgradeLiquidityMining(); - await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - - await liquidityMiningV2.initialize(wrapper.address, migrator.address); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); - - await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); - - await SOVToken.mint(liquidityMining.address, new BN(1000)); - - await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); - await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - await mineBlocks(1); - - tx = await liquidityMining.withdraw(token1.address, accountDeposits[0].deposit[0].amount.div(new BN(2)), ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - let blockStart = tx.receipt.blockNumber; - let balanceLockedBefore = await lockedSOV.getLockedBalance(accountDeposits[0].account); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - - await mineBlocks(10); - - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([accountDeposits[0].account]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - tx = await liquidityMiningV2.withdraw(token1.address, accountDeposits[0].deposit[0].amount.div(new BN(2)), ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - let blockEnd = tx.receipt.blockNumber; - let passedBlocks = new BN(blockEnd - blockStart); - let reward = passedBlocks.mul(rewardTokensPerBlock); - let balanceLockedAfter = await lockedSOV.getLockedBalance(accountDeposits[0].account); - - expect(balanceLockedAfter).bignumber.equal(balanceLockedBefore.add(reward)); - }); - it("should migrate rewards", async () => { - //Re-initialization of liquidity mining contracts - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - new BN(0), - wrapper.address, - lockedSOV.address, - new BN(0) - ); - await upgradeLiquidityMining(); - await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - - await liquidityMiningV2.initialize(wrapper.address, migrator.address); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); - - await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); - - await SOVToken.mint(liquidityMining.address, new BN(1000)); - - await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); - await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - await mineBlocks(10); - - await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: accountDeposits[0].account }); - let { rewardDebt: rewardDebtBefore } = await liquidityMining.userInfoMap(0, accountDeposits[0].account); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([accountDeposits[0].account]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - let userInfoV2 = await liquidityMiningV2.getUserInfo(token1.address, accountDeposits[0].account); - let rewardDebtAfter = userInfoV2.rewards[0].rewardDebt; - - expect(rewardDebtAfter).bignumber.equal(rewardDebtBefore); - }); - it("should be able to claim rewards after migration", async () => { - //Re-initialization of liquidity mining contracts - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - new BN(0), - wrapper.address, - lockedSOV.address, - new BN(0) - ); - await upgradeLiquidityMining(); - await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMining.address, liquidityMiningV2.address); - - await liquidityMiningV2.initialize(wrapper.address, migrator.address); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); - - await liquidityMiningV2.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); - - await SOVToken.mint(liquidityMining.address, new BN(1000)); - - await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { from: accountDeposits[0].account }); - await liquidityMining.deposit(token1.address, accountDeposits[0].deposit[0].amount, ZERO_ADDRESS, { - from: accountDeposits[0].account, - }); - await mineBlocks(10); - - tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: accountDeposits[0].account }); - let blockStart = tx.receipt.blockNumber; - let { rewardDebt: rewardDebtBefore } = await liquidityMining.userInfoMap(0, accountDeposits[0].account); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - - await mineBlocks(10); - - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([accountDeposits[0].account]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - tx = await liquidityMiningV2.claimRewards(token1.address, ZERO_ADDRESS, { from: accountDeposits[0].account }); - let blockEnd = tx.receipt.blockNumber; - let passedBlocks = new BN(blockEnd - blockStart); - let rewardDebt = passedBlocks.mul(rewardTokensPerBlock); - let userInfoV2 = await liquidityMiningV2.getUserInfo(token1.address, accountDeposits[0].account); - let rewardDebtAfter = userInfoV2.rewards[0].rewardDebt; - - expect(rewardDebtAfter).bignumber.equal(rewardDebtBefore.add(rewardDebt)); - }); - }); - - describe("Test lending with liquidity mining V2", async () => { - it("Should lend to the pool at the liquidity mining V1 and migrate it", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanToken.address, new BN(10), false); - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await underlyingToken.approve(loanToken.address, depositAmount); - const tx = await loanToken.mint(lender, depositAmount, true); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - - await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); - let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); - - expectEvent(tx, "Mint", { - minter: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanToken.address, new BN(10), false); - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await underlyingToken.approve(loanToken.address, depositAmount); - await loanToken.mint(lender, depositAmount, true); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - - await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); - await underlyingToken.approve(loanToken.address, depositAmount); - await loanToken.mint(lender, depositAmount, true); - - const userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); - }); - - it("Should lend to the pool at the liquidity mining V1, lend to liquidity mining V2 and migrate", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanToken.address, new BN(10), false); - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await underlyingToken.approve(loanToken.address, depositAmount); - await loanToken.mint(lender, depositAmount, true); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - - await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); - await underlyingToken.approve(loanToken.address, depositAmount); - await loanToken.mint(lender, depositAmount, true); - - await migrator.migrateUsers([lender]); - - const userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); - }); - - it("Should only allow to burn tokens if migration finished", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanToken.address, new BN(10), false); - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await underlyingToken.approve(loanToken.address, depositAmount); - await loanToken.mint(lender, depositAmount, true); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - - await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); - let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - await expectRevert(loanToken.burn(lender, userInfo.amount, true), "Migration is not over yet"); - }); - - it("Should remove the pool tokens from the liquidity mining V2 pool and burn them after migration", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanToken.address, new BN(10), false); - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await underlyingToken.approve(loanToken.address, depositAmount); - await loanToken.mint(lender, depositAmount, true); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); - - let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); - - const tx = await loanToken.burn(lender, userInfo.amount, true); - userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); - //expected: user pool token balance stayed the same but LM balance is 0 - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanToken.totalSupply()).bignumber.equal("0"); - //expect the Burn event to mention the lender - expectEvent(tx, "Burn", { - burner: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - }); - - describe("Test WRBTC lending with liquidity mining", () => { - it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - - const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); - const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance is 0, but balance of LM contract increased - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); - //expect the Mint event to mention the lender - expectEvent(tx, "Mint", { - minter: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - - await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); - await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance is 0, but balance of LM contract increased - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); - }); - - it("Should lend to the pool at the liquidity mining V1, lend to liquidity mining V2 and migrate", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - - await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); - await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - - await migrator.migrateUsers([lender]); - - const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance is 0, but balance of LM contract increased - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); - }); - - it("Should only allow to burn tokens if migration finished", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - - await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); - let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - await expectRevert(loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true), "Migration is not over yet"); - }); - - it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { - const depositAmount = new BN(wei("400", "ether")); - await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - - await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - - await liquidityMining.addAdmin(migrator.address); - await liquidityMining.startMigrationGracePeriod(); - await liquidityMiningV2.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.migrateUsers([lender]); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); - let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); - userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance stayed the same but LM balance is 0 - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); - //expect the Burn event to mention the lender - expectEvent(tx, "Burn", { - burner: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - }); - - async function deployLiquidityMining() { - let liquidityMiningLogic = await LiquidityMiningLogic.new(); - liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); - liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - - wrapper = await Wrapper.new(liquidityMining.address); - } - - async function upgradeLiquidityMining() { - let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); - liquidityMining = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); - } - - async function deployLiquidityMiningV2() { - let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); - let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); - await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); - liquidityMiningV2 = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); - - wrapper = await Wrapper.new(liquidityMiningV2.address); - } - - async function initializeLiquidityMiningPools() { - for (let i = 0; i < tokens.length; i++) { - await liquidityMining.add(tokens[i].address, allocationPoint, false); - } - } - - async function initializaAccountsTokensBalance() { - let amount = new BN(1000); - await SOVToken.mint(liquidityMining.address, amount); - tokens.forEach((token) => { - accounts.forEach(async (account) => { - await token.mint(account, amount); - await token.approve(liquidityMining.address, amount, { from: account }); - }); - }); - } - - async function initializeLiquidityMiningDeposits() { - accountDeposits.forEach((account) => { - account.deposit.forEach(async (deposit) => { - await liquidityMining.deposit(deposit.token, deposit.amount, ZERO_ADDRESS, { from: account.account }); - }); - }); - } - - function createRandomAccounts(length) { - const randomAccounts = []; - for (let i = 0; i < length; i++) { - let id = crypto.randomBytes(32).toString("hex"); - let privateKey = "0x" + id; - let wallet = new ethers.Wallet(privateKey); - randomAccounts.push(wallet.address); - } - return randomAccounts; - } - - function setAccountsDepositsConstants() { - accountDeposits = [ - { - account: accounts[0], - - deposit: [ - { - token: token1.address, - amount: new BN(100), - }, - { - token: token2.address, - amount: new BN(10), - }, - { - token: token3.address, - amount: new BN(10), - }, - { - token: token4.address, - amount: new BN(10), - }, - { - token: token5.address, - amount: new BN(10), - }, - { - token: token6.address, - amount: new BN(10), - }, - { - token: token7.address, - amount: new BN(10), - }, - { - token: token8.address, - amount: new BN(10), - }, - ], - }, - { - account: accounts[1], - - deposit: [ - { - token: token1.address, - amount: new BN(5), - }, - { - token: token2.address, - amount: new BN(5), - }, - { - token: token3.address, - amount: new BN(5), - }, - { - token: token4.address, - amount: new BN(5), - }, - ], - }, - { - account: accounts[2], - - deposit: [ - { - token: token1.address, - amount: new BN(55), - }, - ], - }, - { - account: accounts[3], - - deposit: [ - { - token: token8.address, - amount: new BN(1000), - }, - ], - }, - { - account: accounts[4], - - deposit: [ - { - token: token6.address, - amount: new BN(25), - }, - { - token: token7.address, - amount: new BN(100), - }, - { - token: token8.address, - amount: new BN(100), - }, - ], - }, - { - account: accounts[5], - - deposit: [ - { - token: token1.address, - amount: new BN(25), - }, - { - token: token3.address, - amount: new BN(100), - }, - { - token: token8.address, - amount: new BN(100), - }, - ], - }, - { - account: accounts[6], - - deposit: [ - { - token: token2.address, - amount: new BN(25), - }, - { - token: token4.address, - amount: new BN(100), - }, - { - token: token6.address, - amount: new BN(100), - }, - ], - }, - { - account: accounts[7], - - deposit: [ - { - token: token3.address, - amount: new BN(25), - }, - { - token: token5.address, - amount: new BN(100), - }, - { - token: token7.address, - amount: new BN(100), - }, - ], - }, - { - account: accounts[8], - - deposit: [ - { - token: token4.address, - amount: new BN(25), - }, - { - token: token5.address, - amount: new BN(100), - }, - { - token: token6.address, - amount: new BN(100), - }, - ], - }, - ]; - } - - async function mineBlocks(blocks) { - for (let i = 0; i < blocks; i++) { - await mineBlock(); - } - } - - async function deployProtocol() { - //Token - underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - testWrbtc = await TestWrbtc.new(); - - const sovrynproxy = await sovrynProtocol.new(); - sovryn = await ISovryn.at(sovrynproxy.address); - - await sovryn.replaceContract((await LoanClosingsBase.new()).address); - await sovryn.replaceContract((await LoanClosingsWith.new()).address); - await sovryn.replaceContract((await ProtocolSettings.new()).address); - await sovryn.replaceContract((await LoanSettings.new()).address); - await sovryn.replaceContract((await LoanMaintenance.new()).address); - await sovryn.replaceContract((await SwapsExternal.new()).address); - await sovryn.replaceContract((await LoanOpenings.new()).address); - - await sovryn.setWrbtcToken(testWrbtc.address); - - feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); - await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); - const swaps = await SwapsImplLocal.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([underlyingToken.address, testWrbtc.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address //priceFeeds - ); - await sovryn.setSwapsImplContract( - swaps.address // swapsImpl - ); - await sovryn.setFeesController(lender); - } - - async function deployLoanTokens() { - loanTokenLogicLM = await LoanTokenLogicLM.new(); - loanToken = await LoanToken.new(lender, loanTokenLogicLM.address, sovryn.address, testWrbtc.address); - await loanToken.initialize(underlyingToken.address, name, symbol); //iToken - loanToken = await LoanTokenLogicLM.at(loanToken.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - underlyingToken.address, // address loanToken; // the token being loaned - testWrbtc.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanToken.setupLoanParams([params], false); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - // --------------- WRBTC -----------------------// - - loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); - loanTokenWRBTC = await LoanToken.new(lender, loanTokenLogicWRBTC.address, sovryn.address, testWrbtc.address); - await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); - loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - testWrbtc.address, // address loanToken; // the token being loaned - underlyingToken.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanTokenWRBTC.setupLoanParams([params], false); - await sovryn.setLoanPool([loanTokenWRBTC.address], [testWrbtc.address]); - - // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// - await testWrbtc.mint(sovryn.address, wei("500", "ether")); - await underlyingToken.mint(sovryn.address, wei("50000", "ether")); - } + const name = "Test SOV Token"; + const symbol = "TST"; + + const PRECISION = 1e12; + + const rewardTokensPerBlock = new BN(3); + const startDelayBlocks = new BN(1); + const numberOfBonusBlocks = new BN(50); + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let sovryn, loanToken, loanTokenWRBTC; + let lender, + account1, + account2, + account3, + account4, + account5, + account6, + account7, + account8, + account9; + let SOVToken, + token1, + token2, + token3, + token4, + token5, + token6, + token7, + token8, + liquidityMiningConfigToken; + let liquidityMiningProxy, liquidityMining, liquidityMiningV2, migrator, wrapper; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + let erc20RewardTransferLogic; + let allocationPoint = new BN(10); + + const MigrationStates = { + MigratingPools: 0, + MigratingUsers: 1, + MigratingFunds: 2, + MigrationFinished: 3, + }; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [lender, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + token4 = await TestToken.new("Test token 4", "TST-4", 18, TOTAL_SUPPLY); + token5 = await TestToken.new("Test token 5", "TST-5", 18, TOTAL_SUPPLY); + token6 = await TestToken.new("Test token 6", "TST-6", 18, TOTAL_SUPPLY); + token7 = await TestToken.new("Test token 7", "TST-7", 18, TOTAL_SUPPLY); + token8 = await TestToken.new("Test token 8", "TST-8", 18, TOTAL_SUPPLY); + + tokens = [token1, token2, token3, token4, token5, token6, token7, token8]; + + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployProtocol(); + await deployLoanTokens(); + + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + //set accounts deposits pools in liquidity mining V1 + setAccountsDepositsConstants(); + //mint some tokens to all the accounts + await initializaAccountsTokensBalance(); + //add all poolTokens to liquidityMining + await initializeLiquidityMiningPools(); + //make deposits from accounts to some pools + await initializeLiquidityMiningDeposits(); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address); + + erc20RewardTransferLogic = await ERC20TransferLogic.new(); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMiningV2.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + }); + + describe("initializeLiquidityMining", () => { + it("should check all user deposits", async () => { + for (let i = 0; i < accountDeposits.length; i++) { + for (let j = 0; j < accountDeposits[i].deposit.length; j++) { + let poolToken = accountDeposits[i].deposit[j].token; + let poolId = await liquidityMining.getPoolId(poolToken); + let { amount } = await liquidityMining.userInfoMap( + poolId, + accountDeposits[i].account + ); + expect(amount).bignumber.equal(accountDeposits[i].deposit[j].amount); + } + } + }); + it("should check all pool have been added", async () => { + const { _poolToken } = await liquidityMining.getPoolInfoListArray(); + for (let i = 0; i < tokens.length; i++) { + expect(_poolToken[i]).equal(tokens[i].address); + } + }); + it("should fail if liquidity mining V2 address is invalid", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + + await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); + }); + it("should fail if SOV address is invalid in migrator contract", async () => { + migrator = await Migrator.new(); + await expectRevert( + migrator.initialize( + ZERO_ADDRESS, + liquidityMining.address, + liquidityMiningV2.address + ), + "invalid token address" + ); + }); + it("should fail if liquidity mining V1 address is invalid in migrator contract", async () => { + migrator = await Migrator.new(); + await expectRevert( + migrator.initialize(SOVToken.address, ZERO_ADDRESS, liquidityMiningV2.address), + "invalid contract address" + ); + }); + it("should fail if liquidity mining V2 address is invalid in migrator contract", async () => { + migrator = await Migrator.new(); + await expectRevert( + migrator.initialize(SOVToken.address, liquidityMining.address, ZERO_ADDRESS), + "invalid contract address" + ); + }); + it("fails if already initialized", async () => { + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + await expectRevert( + migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ), + "Already initialized" + ); + }); + }); + + describe("migratePools", () => { + it("should only allow to migrate pools by the admin", async () => { + await expectRevert(migrator.migratePools({ from: account1 }), "unauthorized"); + }); + it("should fail if migrator contract was not added as admin in liquidity mining V1", async () => { + await expectRevert(migrator.migratePools(), "unauthorized"); + }); + it("should fail if migrator contract was not added as admin in liquidity mining V2", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await expectRevert(migrator.migratePools(), "unauthorized"); + }); + it("should only allow to migrate pools by migrator contract", async () => { + await deployLiquidityMiningV2(); + await liquidityMiningV2.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + + await liquidityMiningV2.initialize(wrapper.address, SOVToken.address); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await expectRevert(migrator.migratePools(), "only allowed to migrator contract"); + }); + it("should only allow to migrate pools if the migrate grace period started", async () => { + await liquidityMining.addAdmin(migrator.address); + await expectRevert( + migrator.migratePools(), + "Forbidden: Migration hasn't started yet or already finished" + ); + }); + it("should only allow to migrate pools once", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await expectRevert(migrator.migratePools(), "Wrong state: should be MigratingPools"); + }); + it("should add pools from liquidityMininigV1", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + for (let i = 0; i < tokens.length; i++) { + let poolToken = await liquidityMiningV2.poolInfoList(i); + expect(poolToken).equal(tokens[i].address); + + let { + allocationPoint: allocationPointV2, + lastRewardBlock: lastRewardBlockV2, + accumulatedRewardPerShare: accumulatedRewardPerShareV2, + } = await liquidityMiningV2.poolInfoRewardTokensMap(i, SOVToken.address); + let { + allocationPoint: allocationPointV1, + lastRewardBlock: lastRewardBlockV1, + accumulatedRewardPerShare: accumulatedRewardPerShareV1, + } = await liquidityMining.poolInfoList(i); + expect(allocationPointV2).bignumber.equal(allocationPointV1); + expect(lastRewardBlockV2).bignumber.equal(lastRewardBlockV1); + expect(accumulatedRewardPerShareV2).bignumber.equal(accumulatedRewardPerShareV1); + + let { startBlock: startBlockV2, totalUsersBalance: totalUsersBalanceV2 } = + await liquidityMiningV2.rewardTokensMap(SOVToken.address); + let startBlockV1 = await liquidityMining.startBlock(); + let totalUsersBalanceV1 = await liquidityMining.totalUsersBalance(); + + expect(startBlockV2).bignumber.equal(startBlockV1); + expect(totalUsersBalanceV2).bignumber.equal(totalUsersBalanceV1); + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingUsers); + } + }); + }); + + describe("migrateUsers", () => { + it("should only allow to migrate users by the admin", async () => { + await expectRevert( + migrator.migrateUsers(accounts, { from: account1 }), + "unauthorized" + ); + }); + it("should fail migrating users if pools were not migrated", async () => { + await expectRevert( + migrator.migrateUsers(accounts), + "Wrong state: should be MigratingUsers" + ); + }); + it("should fail finishing users migration if pools were not migrated", async () => { + await expectRevert( + migrator.finishUsersMigration(), + "Wrong state: should be MigratingUsers" + ); + }); + it("should only allow to finish users migration by the admin", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await expectRevert(migrator.finishUsersMigration({ from: account1 }), "unauthorized"); + }); + it("should only allow to migrate users before finish user migration", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await expectRevert( + migrator.migrateUsers(accounts), + "Wrong state: should be MigratingUsers" + ); + }); + it("should save migrated users", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(accounts); + accounts.forEach(async (account) => { + expect(await migrator.userMigrated(account)); + }); + + await migrator.finishUsersMigration(); + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); + }); + it("should emit user migrated event", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + tx = await migrator.migrateUsers([accounts[0]]); + expectEvent(tx, "UserMigrated", { + user: accounts[0], + }); + }); + it("should be able to migrate users in differents tx", async () => { + let userInfoV1 = []; + for (let i = 0; i < tokens.length; i++) { + userInfoV1[i] = []; + for (let j = 0; j < accountDeposits.length; j++) { + let userInfo = await liquidityMining.getUserInfo( + tokens[i].address, + accountDeposits[j].account + ); + userInfoV1[i][j] = userInfo; + } + } + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + let halfLength = accounts.length / 2; + await migrator.migrateUsers(accounts.slice(0, halfLength)); + + let migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingUsers); + + await migrator.migrateUsers(accounts.slice(-halfLength)); + + await migrator.finishUsersMigration(); + migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); + + for (let i = 0; i < tokens.length; i++) { + for (let j = 0; j < accountDeposits.length; j++) { + let userInfoV2 = await liquidityMiningV2.getUserInfo( + tokens[i].address, + accountDeposits[j].account + ); + + expect(userInfoV2.amount).bignumber.equal(userInfoV1[i][j].amount); + expect(userInfoV2.rewards[0].rewardDebt).bignumber.equal( + userInfoV1[i][j].rewardDebt + ); + expect(userInfoV2.rewards[0].accumulatedReward).bignumber.equal( + userInfoV1[i][j].accumulatedReward + ); + } + } + }); + it("should migrate all accounts with deposits from liquidityMininigV1", async () => { + let userInfoV1 = []; + for (let i = 0; i < tokens.length; i++) { + userInfoV1[i] = []; + for (let j = 0; j < accountDeposits.length; j++) { + let userInfo = await liquidityMining.getUserInfo( + tokens[i].address, + accountDeposits[j].account + ); + userInfoV1[i][j] = userInfo; + } + } + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(accounts); + + await migrator.finishUsersMigration(); + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigratingFunds); + + for (let i = 0; i < tokens.length; i++) { + for (let j = 0; j < accountDeposits.length; j++) { + let userInfoV2 = await liquidityMiningV2.getUserInfo( + tokens[i].address, + accountDeposits[j].account + ); + + expect(userInfoV2.amount).bignumber.equal(userInfoV1[i][j].amount); + expect(userInfoV2.rewards[0].rewardDebt).bignumber.equal( + userInfoV1[i][j].rewardDebt + ); + expect(userInfoV2.rewards[0].accumulatedReward).bignumber.equal( + userInfoV1[i][j].accumulatedReward + ); + } + } + }); + it("should migrate 65 random accounts from liquidityMininigV1", async () => { + let randomAccounts = createRandomAccounts(65); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(randomAccounts); + }); + }); + + describe("migrateFunds", () => { + it("should only allow to migrate funds by the admin", async () => { + await expectRevert(migrator.migrateFunds({ from: account1 }), "unauthorized"); + }); + it("should fail migrating funds if users were not migrated", async () => { + await expectRevert(migrator.migrateFunds(), "Wrong state: should be MigratingFunds"); + }); + it("should fail trying to migrate funds without SOV tokens in liquidityMiningV1", async () => { + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await liquidityMining.transferSOV(liquidityMiningV2.address, balanceSOV); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await expectRevert(migrator.migrateFunds(), "Amount invalid"); + }); + it("should fail trying to migrate funds twice", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + await expectRevert(migrator.migrateFunds(), "Wrong state: should be MigratingFunds"); + }); + it("should fail if liquidity mining V2 is not initialized in liquidity mining V1", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await expectRevert(migrator.migrateFunds(), "Address not initialized"); + }); + it("should migrate funds from liquidityMining", async () => { + let SOVBalanceV1Before = await SOVToken.balanceOf(liquidityMining.address); + let SOVBalanceV2Before = await SOVToken.balanceOf(liquidityMiningV2.address); + let tokenBalancesV1Before = []; + let tokenBalancesV2Before = []; + for (let i = 0; i < tokens.length; i++) { + tokenBalancesV1Before.push(await tokens[i].balanceOf(liquidityMining.address)); + tokenBalancesV2Before.push(await tokens[i].balanceOf(liquidityMiningV2.address)); + expect(tokenBalancesV2Before[i]).bignumber.equal(new BN(0)); + } + expect(SOVBalanceV2Before).bignumber.equal(new BN(0)); + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + const migrationState = await migrator.migrationState(); + expect(migrationState.toNumber()).to.equal(MigrationStates.MigrationFinished); + + let SOVBalanceV1After = await SOVToken.balanceOf(liquidityMining.address); + let SOVBalanceV2After = await SOVToken.balanceOf(liquidityMiningV2.address); + let tokenBalancesV1After = []; + let tokenBalancesV2After = []; + for (let i = 0; i < tokens.length; i++) { + tokenBalancesV1After.push(await tokens[i].balanceOf(liquidityMining.address)); + tokenBalancesV2After.push(await tokens[i].balanceOf(liquidityMiningV2.address)); + expect(tokenBalancesV1After[i]).bignumber.equal(new BN(0)); + expect(tokenBalancesV2After[i]).bignumber.equal(tokenBalancesV1Before[i]); + } + expect(SOVBalanceV1After).bignumber.equal(new BN(0)); + expect(SOVBalanceV2After).bignumber.equal(SOVBalanceV1Before); + }); + }); + + describe("withdraws", () => { + it("should withdraw all before migration and revert trying to withdraw after", async () => { + await liquidityMining.withdraw( + accountDeposits[0].deposit[0].token, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers(accounts); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await expectRevert( + liquidityMiningV2.withdraw( + accountDeposits[0].deposit[0].token, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ), + "Not enough balance" + ); + }); + it("should withdraw half before migration and withdraw the other half after", async () => { + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + tokenBalanceBefore = await token1.balanceOf(accountDeposits[0].account); + await liquidityMining.withdraw( + accountDeposits[0].deposit[0].token, + accountDeposits[0].deposit[0].amount.div(new BN(2)), + ZERO_ADDRESS, + { from: accountDeposits[0].account } + ); + + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + await liquidityMiningV2.withdraw( + accountDeposits[0].deposit[0].token, + accountDeposits[0].deposit[0].amount.div(new BN(2)), + ZERO_ADDRESS, + { from: accountDeposits[0].account } + ); + + tokenBalanceAfter = await token1.balanceOf(accountDeposits[0].account); + + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).bignumber.equal( + accountDeposits[0].deposit[0].amount + ); + }); + it("should withdraw all before migration, migrate, deposit and withdraw all again", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { + from: accountDeposits[0].account, + }); + await liquidityMining.deposit( + token1.address, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + await mineBlocks(20); + + await liquidityMining.withdraw( + token1.address, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + let balanceLockedBefore = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await token1.approve(liquidityMiningV2.address, accountDeposits[0].deposit[0].amount, { + from: accountDeposits[0].account, + }); + await liquidityMiningV2.deposit( + token1.address, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + await mineBlocks(20); + + await liquidityMiningV2.withdraw( + token1.address, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + let balanceLockedAfter = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + expect(balanceLockedAfter).bignumber.equal(balanceLockedBefore.mul(new BN(2))); + }); + it("should get rewards in liquidity mining V2 after migration", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { + from: accountDeposits[0].account, + }); + await liquidityMining.deposit( + token1.address, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + await mineBlocks(1); + + tx = await liquidityMining.withdraw( + token1.address, + accountDeposits[0].deposit[0].amount.div(new BN(2)), + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + let blockStart = tx.receipt.blockNumber; + let balanceLockedBefore = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + + await mineBlocks(10); + + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + tx = await liquidityMiningV2.withdraw( + token1.address, + accountDeposits[0].deposit[0].amount.div(new BN(2)), + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + let blockEnd = tx.receipt.blockNumber; + let passedBlocks = new BN(blockEnd - blockStart); + let reward = passedBlocks.mul(rewardTokensPerBlock); + let balanceLockedAfter = await lockedSOV.getLockedBalance(accountDeposits[0].account); + + expect(balanceLockedAfter).bignumber.equal(balanceLockedBefore.add(reward)); + }); + it("should migrate rewards", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { + from: accountDeposits[0].account, + }); + await liquidityMining.deposit( + token1.address, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + await mineBlocks(10); + + await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + let { rewardDebt: rewardDebtBefore } = await liquidityMining.userInfoMap( + 0, + accountDeposits[0].account + ); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + let userInfoV2 = await liquidityMiningV2.getUserInfo( + token1.address, + accountDeposits[0].account + ); + let rewardDebtAfter = userInfoV2.rewards[0].rewardDebt; + + expect(rewardDebtAfter).bignumber.equal(rewardDebtBefore); + }); + it("should be able to claim rewards after migration", async () => { + //Re-initialization of liquidity mining contracts + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + new BN(0), + wrapper.address, + lockedSOV.address, + new BN(0) + ); + await upgradeLiquidityMining(); + await deployLiquidityMiningV2(); + await liquidityMining.initialize(liquidityMiningV2.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMining.address, + liquidityMiningV2.address + ); + + await liquidityMiningV2.initialize(wrapper.address, migrator.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, new BN(0)); + + await liquidityMiningV2.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + await liquidityMining.add(accountDeposits[0].deposit[0].token, allocationPoint, false); + + await SOVToken.mint(liquidityMining.address, new BN(1000)); + + await token1.approve(liquidityMining.address, accountDeposits[0].deposit[0].amount, { + from: accountDeposits[0].account, + }); + await liquidityMining.deposit( + token1.address, + accountDeposits[0].deposit[0].amount, + ZERO_ADDRESS, + { + from: accountDeposits[0].account, + } + ); + await mineBlocks(10); + + tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + let blockStart = tx.receipt.blockNumber; + let { rewardDebt: rewardDebtBefore } = await liquidityMining.userInfoMap( + 0, + accountDeposits[0].account + ); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + + await mineBlocks(10); + + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([accountDeposits[0].account]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + tx = await liquidityMiningV2.claimRewards(token1.address, ZERO_ADDRESS, { + from: accountDeposits[0].account, + }); + let blockEnd = tx.receipt.blockNumber; + let passedBlocks = new BN(blockEnd - blockStart); + let rewardDebt = passedBlocks.mul(rewardTokensPerBlock); + let userInfoV2 = await liquidityMiningV2.getUserInfo( + token1.address, + accountDeposits[0].account + ); + let rewardDebtAfter = userInfoV2.rewards[0].rewardDebt; + + expect(rewardDebtAfter).bignumber.equal(rewardDebtBefore.add(rewardDebt)); + }); + }); + + describe("Test lending with liquidity mining V2", async () => { + it("Should lend to the pool at the liquidity mining V1 and migrate it", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + const userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); + }); + + it("Should lend to the pool at the liquidity mining V1, lend to liquidity mining V2 and migrate", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await migrator.migrateUsers([lender]); + + const userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN(2))); + }); + + it("Should only allow to burn tokens if migration finished", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + await expectRevert( + loanToken.burn(lender, userInfo.amount, true), + "Migration is not over yet" + ); + }); + + it("Should remove the pool tokens from the liquidity mining V2 pool and burn them after migration", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanToken.address, new BN(10), false); + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await underlyingToken.approve(loanToken.address, depositAmount); + await loanToken.mint(lender, depositAmount, true); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanToken.setLiquidityMiningAddress(liquidityMiningV2.address); + + let userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + + const tx = await loanToken.burn(lender, userInfo.amount, true); + userInfo = await liquidityMiningV2.getUserInfo(loanToken.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal("0"); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + }); + + describe("Test WRBTC lending with liquidity mining", () => { + it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + //expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool at the liquidity mining V1, migrate it and lend to liquidity mining V2", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal( + depositAmount.mul(new BN(2)) + ); + }); + + it("Should lend to the pool at the liquidity mining V1, lend to liquidity mining V2 and migrate", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), true); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await migrator.migrateUsers([lender]); + + const userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount.mul(new BN(2))); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal( + depositAmount.mul(new BN(2)) + ); + }); + + it("Should only allow to burn tokens if migration finished", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + await expectRevert( + loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true), + "Migration is not over yet" + ); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + const depositAmount = new BN(wei("400", "ether")); + await liquidityMining.add(loanTokenWRBTC.address, new BN(10), false); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + + await liquidityMining.addAdmin(migrator.address); + await liquidityMining.startMigrationGracePeriod(); + await liquidityMiningV2.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.migrateUsers([lender]); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMiningV2.address); + let userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); + userInfo = await liquidityMiningV2.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMining = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMiningV2 = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + + wrapper = await Wrapper.new(liquidityMiningV2.address); + } + + async function initializeLiquidityMiningPools() { + for (let i = 0; i < tokens.length; i++) { + await liquidityMining.add(tokens[i].address, allocationPoint, false); + } + } + + async function initializaAccountsTokensBalance() { + let amount = new BN(1000); + await SOVToken.mint(liquidityMining.address, amount); + tokens.forEach((token) => { + accounts.forEach(async (account) => { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + }); + }); + } + + async function initializeLiquidityMiningDeposits() { + accountDeposits.forEach((account) => { + account.deposit.forEach(async (deposit) => { + await liquidityMining.deposit(deposit.token, deposit.amount, ZERO_ADDRESS, { + from: account.account, + }); + }); + }); + } + + function createRandomAccounts(length) { + const randomAccounts = []; + for (let i = 0; i < length; i++) { + let id = crypto.randomBytes(32).toString("hex"); + let privateKey = "0x" + id; + let wallet = new ethers.Wallet(privateKey); + randomAccounts.push(wallet.address); + } + return randomAccounts; + } + + function setAccountsDepositsConstants() { + accountDeposits = [ + { + account: accounts[0], + + deposit: [ + { + token: token1.address, + amount: new BN(100), + }, + { + token: token2.address, + amount: new BN(10), + }, + { + token: token3.address, + amount: new BN(10), + }, + { + token: token4.address, + amount: new BN(10), + }, + { + token: token5.address, + amount: new BN(10), + }, + { + token: token6.address, + amount: new BN(10), + }, + { + token: token7.address, + amount: new BN(10), + }, + { + token: token8.address, + amount: new BN(10), + }, + ], + }, + { + account: accounts[1], + + deposit: [ + { + token: token1.address, + amount: new BN(5), + }, + { + token: token2.address, + amount: new BN(5), + }, + { + token: token3.address, + amount: new BN(5), + }, + { + token: token4.address, + amount: new BN(5), + }, + ], + }, + { + account: accounts[2], + + deposit: [ + { + token: token1.address, + amount: new BN(55), + }, + ], + }, + { + account: accounts[3], + + deposit: [ + { + token: token8.address, + amount: new BN(1000), + }, + ], + }, + { + account: accounts[4], + + deposit: [ + { + token: token6.address, + amount: new BN(25), + }, + { + token: token7.address, + amount: new BN(100), + }, + { + token: token8.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[5], + + deposit: [ + { + token: token1.address, + amount: new BN(25), + }, + { + token: token3.address, + amount: new BN(100), + }, + { + token: token8.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[6], + + deposit: [ + { + token: token2.address, + amount: new BN(25), + }, + { + token: token4.address, + amount: new BN(100), + }, + { + token: token6.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[7], + + deposit: [ + { + token: token3.address, + amount: new BN(25), + }, + { + token: token5.address, + amount: new BN(100), + }, + { + token: token7.address, + amount: new BN(100), + }, + ], + }, + { + account: accounts[8], + + deposit: [ + { + token: token4.address, + amount: new BN(25), + }, + { + token: token5.address, + amount: new BN(100), + }, + { + token: token6.address, + amount: new BN(100), + }, + ], + }, + ]; + } + + async function mineBlocks(blocks) { + for (let i = 0; i < blocks; i++) { + await mineBlock(); + } + } + + async function deployProtocol() { + //Token + underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + testWrbtc = await TestWrbtc.new(); + + const sovrynproxy = await sovrynProtocol.new(); + sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await LoanClosingsBase.new()).address); + await sovryn.replaceContract((await LoanClosingsWith.new()).address); + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + await sovryn.replaceContract((await LoanOpenings.new()).address); + + await sovryn.setWrbtcToken(testWrbtc.address); + + feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); + await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); + const swaps = await SwapsImplLocal.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens( + [underlyingToken.address, testWrbtc.address], + [true, true] + ); + await sovryn.setPriceFeedContract( + feeds.address //priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + } + + async function deployLoanTokens() { + loanTokenLogicLM = await LoanTokenLogicLM.new(); + loanToken = await LoanToken.new( + lender, + loanTokenLogicLM.address, + sovryn.address, + testWrbtc.address + ); + await loanToken.initialize(underlyingToken.address, name, symbol); //iToken + loanToken = await LoanTokenLogicLM.at(loanToken.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + underlyingToken.address, // address loanToken; // the token being loaned + testWrbtc.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + // --------------- WRBTC -----------------------// + + loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); + loanTokenWRBTC = await LoanToken.new( + lender, + loanTokenLogicWRBTC.address, + sovryn.address, + testWrbtc.address + ); + await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); + loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + testWrbtc.address, // address loanToken; // the token being loaned + underlyingToken.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanTokenWRBTC.setupLoanParams([params], false); + await sovryn.setLoanPool([loanTokenWRBTC.address], [testWrbtc.address]); + + // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// + await testWrbtc.mint(sovryn.address, wei("500", "ether")); + await underlyingToken.mint(sovryn.address, wei("50000", "ether")); + } }); diff --git a/tests/farm/LiquidityMiningV1.js b/tests/farm/LiquidityMiningV1.js index aa8deb1f3..0aed6ca7c 100644 --- a/tests/farm/LiquidityMiningV1.js +++ b/tests/farm/LiquidityMiningV1.js @@ -26,2033 +26,2496 @@ const numberOfBonusBlocks = new BN(50); const unlockedImmediatelyPercent = new BN(1000); //10% async function mineBlocks(blocks) { - for (let i = 0; i < blocks; i++) { - await mineBlock(); - } + for (let i = 0; i < blocks; i++) { + await mineBlock(); + } } describe("LiquidityMining", () => { - let accounts; - let root, account1, account2, account3, account4; - let SOVToken, token1, token2, token3, liquidityMiningConfigToken; - let liquidityMining, liquidityMiningProxy, liquidityMiningV2, liquidityMiningProxyV2, wrapper; - let lockedSOVAdmins, lockedSOV; - - before(async () => { - accounts = await web3.eth.getAccounts(); - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); - lockedSOVAdmins = [account1, account2]; - - lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - - await upgradeLiquidityMining(); - - await deployLiquidityMiningV2(); - - await liquidityMining.initialize(liquidityMiningV2.address); - }); - - describe("initialize", () => { - it("sets the expected values", async () => { - await deployLiquidityMining(); - let tx = await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - - let _SOV = await liquidityMining.SOV(); - let _rewardTokensPerBlock = await liquidityMining.rewardTokensPerBlock(); - let _startBlock = await liquidityMining.startBlock(); - let _bonusEndBlock = await liquidityMining.bonusEndBlock(); - let _wrapper = await liquidityMining.wrapper(); - - let blockNumber = new BN(tx.receipt.blockNumber); - - expect(_SOV).equal(SOVToken.address); - expect(_rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); - expect(_startBlock).bignumber.equal(startDelayBlocks.add(blockNumber)); - expect(_bonusEndBlock).bignumber.equal(startDelayBlocks.add(blockNumber).add(numberOfBonusBlocks)); - expect(_wrapper).equal(wrapper.address); - }); - - it("fails if not an owner or an admin", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent, - { from: account1 } - ), - "unauthorized" - ); - - await liquidityMining.addAdmin(account1); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent, - { from: account1 } - ); - }); - - it("fails if _startBlock = 0", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - 0, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ), - "Invalid start block" - ); - }); - - it("fails if already initialized", async () => { - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ), - "Already initialized" - ); - }); - it("fails if already initialized", async () => { - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - await upgradeLiquidityMining(); - await liquidityMining.initialize(liquidityMiningV2.address); - await expectRevert(liquidityMining.initialize(liquidityMiningV2.address), "Already initialized"); - }); - it("fails if the 0 address is passed as token address", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - ZERO_ADDRESS, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ), - "Invalid token address" - ); - }); - it("fails if the 0 address is passed as LiquidityMiningV2 address", async () => { - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - await upgradeLiquidityMining(); - await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); - }); - - it("fails if unlockedImmediatelyPercent >= 10000", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - 12345 - ), - "Unlocked immediately percent has to be less than 10000." - ); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await liquidityMining.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await liquidityMining.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(liquidityMining.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("adds admin", async () => { - await liquidityMining.addAdmin(account1); - let tx = await liquidityMining.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await liquidityMining.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(liquidityMining.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("setLockedSOV", () => { - it("sets the expected values", async () => { - let newLockedSOV = account2; - await liquidityMining.setLockedSOV(newLockedSOV); - - let _lockedSOV = await liquidityMining.lockedSOV(); - expect(_lockedSOV).equal(newLockedSOV); - }); - - it("fails if not an owner and an admin", async () => { - await expectRevert(liquidityMining.setLockedSOV(account2, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.setLockedSOV(account2, { from: account1 }); - }); - - it("fails if zero address passed", async () => { - await expectRevert(liquidityMining.setLockedSOV(ZERO_ADDRESS), "Invalid lockedSOV Address."); - }); - }); - - describe("setUnlockedImmediatelyPercent", () => { - it("sets the expected values", async () => { - let newUnlockedImmediatelyPercent = new BN(2000); - await liquidityMining.setUnlockedImmediatelyPercent(newUnlockedImmediatelyPercent); - - let _unlockedImmediatelyPercent = await liquidityMining.unlockedImmediatelyPercent(); - expect(_unlockedImmediatelyPercent).bignumber.equal(newUnlockedImmediatelyPercent); - }); - - it("fails if not an owner or an admin", async () => { - await expectRevert(liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }); - }); - - it("fails if unlockedImmediatelyPercent >= 10000", async () => { - await expectRevert( - liquidityMining.setUnlockedImmediatelyPercent(100000), - "Unlocked immediately percent has to be less than 10000." - ); - }); - }); - - describe("setWrapper", () => { - it("sets the expected values", async () => { - let newWrapper = account2; - await liquidityMining.setWrapper(newWrapper); - - let _wrapper = await liquidityMining.wrapper(); - expect(_wrapper).equal(newWrapper); - }); - - it("fails if not an owner or an admin", async () => { - await expectRevert(liquidityMining.setWrapper(account2, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.setWrapper(account2, { from: account1 }); - }); - }); - - describe("stopMining", () => { - it("should set end block", async () => { - let tx = await liquidityMining.stopMining(); - - let blockNumber = new BN(tx.receipt.blockNumber); - let _endBlock = await liquidityMining.endBlock(); - expect(_endBlock).bignumber.equal(blockNumber); - }); - - it("fails if not an owner or an admin", async () => { - await expectRevert(liquidityMining.stopMining({ from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.stopMining({ from: account1 }); - }); - - it("fails if already stopped", async () => { - await liquidityMining.stopMining(); - await expectRevert(liquidityMining.stopMining(), "Already stopped"); - }); - }); - - describe("transferSOV", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOVToken.transfer(liquidityMining.address, amount); - - let balanceBefore = await SOVToken.balanceOf(account1); - await liquidityMining.transferSOV(account1, amount); - let balanceAfter = await SOVToken.balanceOf(account1); - - expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner or admin should be able to transfer", async () => { - await expectRevert(liquidityMining.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.transferSOV(account1, 1000, { from: account1 }); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(liquidityMining.transferSOV(ZERO_ADDRESS, 1000), "Receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(liquidityMining.transferSOV(account1, 0), "Amount invalid"); - }); - }); - - describe("add", () => { - it("should be able to add pool token", async () => { - let allocationPoint = new BN(1); - let tx = await liquidityMining.add(token1.address, allocationPoint, false); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); - - let poolInfo = await liquidityMining.poolInfoList(0); - expect(poolInfo.poolToken).equal(token1.address); - expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); - let blockNumber = new BN(tx.receipt.blockNumber); - expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); - expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(new BN(0)); - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); - - expectEvent(tx, "PoolTokenAdded", { - user: root, - poolToken: token1.address, - allocationPoint: allocationPoint, - }); - }); - - it("should be able to add 2 pool tokens and update pools", async () => { - let allocationPoint1 = new BN(1); - let tx1 = await liquidityMining.add(token1.address, allocationPoint1, false); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1); - - expectEvent(tx1, "PoolTokenAdded", { - user: root, - poolToken: token1.address, - allocationPoint: allocationPoint1, - }); - - let allocationPoint2 = new BN(2); - let tx2 = await liquidityMining.add(token2.address, allocationPoint2, true); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1.add(allocationPoint2)); - - expectEvent(tx2, "PoolTokenAdded", { - user: root, - poolToken: token2.address, - allocationPoint: allocationPoint2, - }); - - let poolInfo1 = await liquidityMining.getPoolInfo(token1.address); - let poolInfo2 = await liquidityMining.getPoolInfo(token2.address); - expect(poolInfo1.lastRewardBlock).bignumber.equal(poolInfo2.lastRewardBlock); - }); - - it("fails if the 0 allocation point is passed", async () => { - await expectRevert(liquidityMining.add(token1.address, new BN(0), false), "Invalid allocation point"); - }); - - it("fails if the 0 address is passed as token address", async () => { - await expectRevert(liquidityMining.add(ZERO_ADDRESS, new BN(1), false), "Invalid token address"); - }); - - it("fails if token already added", async () => { - await liquidityMining.add(token1.address, new BN(1), false); - await expectRevert(liquidityMining.add(token1.address, new BN(1), false), "Token already added"); - }); - - it("only owner or admin should be able to add pool token", async () => { - await expectRevert(liquidityMining.add(token2.address, new BN(1), false, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.add(token2.address, new BN(1), false, { from: account1 }); - }); - }); - - describe("update", () => { - it("should be able to update pool token", async () => { - let oldAllocationPoint = new BN(1); - await liquidityMining.add(token1.address, oldAllocationPoint, false); - - let newAllocationPoint = new BN(2); - let tx = await liquidityMining.update(token1.address, newAllocationPoint, false); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(newAllocationPoint); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, newAllocationPoint, blockNumber, new BN(0)); - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); - - expectEvent(tx, "PoolTokenUpdated", { - user: root, - poolToken: token1.address, - newAllocationPoint: newAllocationPoint, - oldAllocationPoint: oldAllocationPoint, - }); - }); - - it("should be able to update pool token and update pools", async () => { - let oldAllocationPoint = new BN(1); - await liquidityMining.add(token1.address, oldAllocationPoint, false); - - await liquidityMining.add(token2.address, oldAllocationPoint, false); - - let newAllocationPoint = new BN(2); - let tx = await liquidityMining.update(token1.address, newAllocationPoint, true); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(oldAllocationPoint.add(newAllocationPoint)); - - let poolInfo = await liquidityMining.getPoolInfo(token2.address); - expect(poolInfo.lastRewardBlock).bignumber.equal(new BN(tx.receipt.blockNumber)); - }); - - it("fails if token wasn't added", async () => { - await expectRevert(liquidityMining.update(token1.address, new BN(1), false), "Pool token not found"); - }); - - it("only owner or admin should be able to update pool token", async () => { - await liquidityMining.add(token2.address, new BN(1), false); - await expectRevert(liquidityMining.update(token2.address, new BN(1), false, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.update(token2.address, new BN(1), false, { from: account1 }); - }); - }); - - describe("updateTokens", () => { - it("should be able to update 2 pool tokens", async () => { - let poolTokens = [token1.address, token2.address, token3.address]; - let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; - - for (let i = 0; i < poolTokens.length; i++) { - await liquidityMining.add(poolTokens[i], oldAllocationPoints[i], false); - } - - let newAllocationPoints = [new BN(101), new BN(102), new BN(3)]; - let tx = await liquidityMining.updateTokens(poolTokens, newAllocationPoints, true); - - let totalAllocationPoint = new BN(0); - for (let i = 0; i < newAllocationPoints.length; i++) { - totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i]); - } - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(totalAllocationPoint); - - let blockNumber = new BN(tx.receipt.blockNumber); - for (let i = 0; i < poolTokens.length - 1; i++) { - let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); - checkPoolInfo(poolInfo, poolTokens[i], newAllocationPoints[i], blockNumber, new BN(0)); - - expectEvent(tx, "PoolTokenUpdated", { - user: root, - poolToken: poolTokens[i], - newAllocationPoint: newAllocationPoints[i], - oldAllocationPoint: oldAllocationPoints[i], - }); - } - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); - - let poolInfo = await liquidityMining.getPoolInfo(poolTokens[poolTokens.length - 1]); - expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); - }); - - it("fails if token wasn't added", async () => { - await expectRevert(liquidityMining.updateTokens([token1.address], [new BN(1)], false), "Pool token not found"); - }); - - it("fails if arrays have different length", async () => { - await liquidityMining.add(token2.address, new BN(1), false); - await expectRevert(liquidityMining.updateTokens([token1.address, token2.address], [new BN(1)], false), "Arrays mismatch"); - }); - - it("only owner or admin should be able to update pool token", async () => { - await liquidityMining.add(token2.address, new BN(1), false); - await expectRevert(liquidityMining.updateTokens([token2.address], [new BN(1)], false, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.updateTokens([token2.address], [new BN(1)], false, { from: account1 }); - }); - }); - - describe("deposit", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to deposit", async () => { - let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - - expectEvent(tx, "Deposit", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - }); - - it("should be able to deposit using wrapper", async () => { - let tx = await liquidityMining.deposit(token1.address, amount, account2, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); - - await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); - - expectEvent(tx, "Deposit", { - user: account2, - poolToken: token1.address, - amount: amount, - }); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - }); - - describe("claimReward", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await expectRevert(liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); - }); - - it("should be able to claim reward (will be claimed with SOV tokens)", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: userReward, - }); - }); - - it("should be able to claim reward using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.claimReward(token1.address, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.claimReward(account1, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - }); - - describe("claimRewardFromAllPools", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, allocationPoint, false); - await liquidityMining.add(token2.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - await token2.mint(account1, amount); - await token2.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await expectRevert(liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); - }); - - it("should be able to claim reward (will be claimed with SOV tokens)", async () => { - let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); - let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); - - let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward1 = await checkUserReward(account1, token1, depositBlockNumber1, latestBlockNumber); - //we have 2 pools with the same allocation points - userReward1 = userReward1.div(new BN(2)); - - await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); - let userReward2 = await checkUserReward(account1, token2, depositBlockNumber2, latestBlockNumber); - //we have 2 pools with the same allocation points - userReward2 = userReward2.div(new BN(2)); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: userReward1, - }); - - expect(userReward1, tx.logs[0].args.amount); - expect(token1.address, tx.logs[0].args.poolToken); - expect(userReward2, tx.logs[1].args.amount); - expect(token2.address, tx.logs[1].args.poolToken); - }); - - it("should be able to claim reward using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - }); - }); - - describe("withdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to withdraw (without claiming reward)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - - // User's balance on lockedSOV vault - let userRewardBalance = await lockedSOV.getLockedBalance(account1); - expect(userRewardBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - }); - - it("should be able to withdraw (with claiming reward)", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was not invoked - let expectedUnlockedBalance = userReward.mul(unlockedImmediatelyPercent).div(new BN(10000)); - let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); - expect(lockedBalance).bignumber.equal(expectedLockedBalance); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: userReward, - }); - }); - - it("should be able to withdraw using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount, wrapper.address); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - - it("fails if token pool token not found", async () => { - await expectRevert( - liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { from: account1 }), - "Not enough balance" - ); - }); - }); - - describe("emergencyWithdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to withdraw", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - - let userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); - expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); - - let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - let expectedAccumulatedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); - expectEvent(tx, "EmergencyWithdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - accumulatedReward: expectedAccumulatedReward, - }); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.emergencyWithdraw(account1, { from: account1 }), "Pool token not found"); - }); - }); - - describe("getPassedBlocksWithBonusMultiplier", () => { - it("check calculation", async () => { - let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - let startBlock = await liquidityMining.startBlock(); - let bonusEndBlock = await liquidityMining.bonusEndBlock(); - let blocks; - - //[startBlock, bonusEndBlock] - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock, bonusEndBlock); - expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); - - //[startBlock - 100, bonusEndBlock] - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock.sub(new BN(100)), bonusEndBlock); - expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); - - //[startBlock, bonusEndBlock + 100] - let blocksAfterBonusPeriod = new BN(100); - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( - startBlock, - bonusEndBlock.add(new BN(blocksAfterBonusPeriod)) - ); - expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier).add(blocksAfterBonusPeriod)); - - //[startBlock, stopMining, ... bonusEndBlock] - await mineBlocks(5); - await liquidityMining.stopMining(); - let endBlock = await liquidityMining.endBlock(); - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock, bonusEndBlock); - expect(blocks).bignumber.equal(endBlock.sub(startBlock).mul(bonusBlockMultiplier)); - }); - }); - - describe("getUserAccumulatedReward", () => { - const amount1 = new BN(1000); - const amount2 = new BN(2000); - const allocationPoint1 = new BN(1); - const allocationPoint2 = new BN(2); - const totalAllocationPoint = allocationPoint1.add(allocationPoint2); - let bonusBlockMultiplier; - let bonusEndBlock; - - beforeEach(async () => { - await liquidityMining.add(token1.address, allocationPoint1, false); - await liquidityMining.add(token2.address, allocationPoint2, false); - - await token1.mint(account1, amount1); - await token2.mint(account2, amount2); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await token2.approve(liquidityMining.address, amount2, { from: account2 }); - - bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - bonusEndBlock = await liquidityMining.bonusEndBlock(); - }); - - it("check calculation for no deposits", async () => { - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - expect(reward1).bignumber.equal("0"); - expect(reward2).bignumber.equal("0"); - }); - - it("check calculation for single user, token 1, bonus period off", async () => { - await advanceBlocks(bonusEndBlock); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - let reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - - // 1 block has passed, bonus period is off - // users are given 3 tokens per share per block. user1 owns 100% of the shares - // token 1 counts as 1/3 of the pool - // reward = 1 * 3 * 1/3 = 1 - const expectedReward = rewardTokensPerBlock.mul(allocationPoint1).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("1"); // sanity check - expect(reward).bignumber.equal(expectedReward); - - await mineBlock(); - reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - expect(reward).bignumber.equal("2"); - }); - - it("check calculation for single user, token 2, bonus period off", async () => { - await advanceBlocks(bonusEndBlock); - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - await mineBlock(); - let reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - - // 1 block has passed, bonus period is off - // users are given 3 tokens per share per block. user2 owns 100% of the shares - // token 2 counts as 2/3 of the pool - // reward = 1 * 3 * 2/3 = 2 - const expectedReward = rewardTokensPerBlock.mul(allocationPoint2).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("2"); // sanity check - expect(reward).bignumber.equal(expectedReward); - - await mineBlock(); - reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - expect(reward).bignumber.equal("4"); - }); - - it("check calculation for single user, token 1, bonus period on", async () => { - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - - // 1 block has passed, bonus period is on so it counts as 10 blocks, - // users are given 3 tokens per share per block. user1 owns 100% of the shares - // token 1 counts as 1/3 of the pool - // reward = 10 * 3 * 1/3 = 10 - const expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier).mul(allocationPoint1).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("10"); // sanity check - expect(reward).bignumber.equal(expectedReward); - }); - - it("check calculation for single user, token 1, bonus period on, smaller amount", async () => { - await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - - // 1 block has passed, bonus period is on so it counts as 10 blocks, - // users are given 3 tokens per share per block. user1 owns 100% of the shares - // token 1 counts as 1/3 of the pool - // reward = 10 * 3 * 1/3 = 10 - // Note that the actual amount deposited plays no role here - expect(reward).bignumber.equal("10"); - }); - - it("check calculation for single user, token 2, bonus period on", async () => { - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - await mineBlock(); - const reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - - // 1 block has passed, bonus period is on so it counts as 10 blocks, - // users are given 3 tokens per share per block. user2 owns 100% of the shares - // token 2 counts as 2/3 of the pool - // reward = 10 * 3 * 2/3 = 20 - const expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier).mul(allocationPoint2).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("20"); // sanity check - expect(reward).bignumber.equal(expectedReward); - }); - - it("check calculation for two users and tokens", async () => { - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - // because automining is on, the following will advance a block - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - // sanity checks - expect(await liquidityMining.getUserAccumulatedReward(token1.address, account1)).bignumber.equal("10"); - expect(await liquidityMining.getUserAccumulatedReward(token2.address, account2)).bignumber.equal("0"); - await mineBlock(); - - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - - // for the first block, user 1 will receive the reward of 10 - // for the second block: - // - user 1 still owns 100% of the shares for token1, so same reward (total 10 + 10 = 20) - // - user 2 owns 100% of the shares for token2, so same reward as in the other cases - expect(reward1).bignumber.equal("20"); - expect(reward2).bignumber.equal("20"); - }); - - it("check calculation for two users, same token (shares taken into account)", async () => { - const token = token1; - const amount = amount1; - await token.mint(account2, amount); - await token.approve(liquidityMining.address, amount, { from: account2 }); - - await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); - // because automining is on, the following will advance a block - await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); - // sanity checks - expect(await liquidityMining.getUserAccumulatedReward(token.address, account1)).bignumber.equal("10"); - expect(await liquidityMining.getUserAccumulatedReward(token.address, account2)).bignumber.equal("0"); - await mineBlock(); - - const reward1 = await liquidityMining.getUserAccumulatedReward(token.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token.address, account2); - - // for the first block, user 1 will receive the reward of 10 (reward given per block for 100% of shares) - // for the second block: - // - user 1 owns 1/2 of the shares => expected reward = 5 (total 10 + 5 = 15) - // - user 2 owns 1/2 of the shares => expected reward = 5 - expect(reward1).bignumber.equal("15"); - expect(reward2).bignumber.equal("5"); - }); - }); - - describe("getEstimatedReward", () => { - const amount1 = new BN(1000); - const amount2 = new BN(2000); - const amount3 = new BN(4000); - const allocationPoint1 = new BN(1); - const allocationPoint2 = new BN(2); - - const totalAllocationPoint = allocationPoint1.add(allocationPoint2); - let bonusBlockMultiplier; - let bonusEndBlock; - let secondsPerBlock; - - beforeEach(async () => { - await liquidityMining.add(token1.address, allocationPoint1, false); - - await token1.mint(account1, amount1); - await token1.mint(account2, amount2); - await token1.mint(account3, amount3); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await token1.approve(liquidityMining.address, amount2, { from: account2 }); - - bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - bonusEndBlock = await liquidityMining.bonusEndBlock(); - - secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); - }); - - it("check calculation for 1 user, period less than 1 block", async () => { - let duration = secondsPerBlock.sub(new BN(1)); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = "0"; - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user, period is 1 block", async () => { - let duration = secondsPerBlock; - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user, period is 30 blocks", async () => { - let blocks = new BN(30); - let duration = secondsPerBlock.mul(blocks); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks).mul(bonusBlockMultiplier); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 2 users, period is 100 blocks", async () => { - //turn off bonus period - await advanceBlocks(bonusEndBlock); - - let blocks = new BN(100); - let duration = secondsPerBlock.mul(blocks); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks); - let totalAmount = amount1.add(amount3); - expectedReward = expectedReward.mul(amount3).div(totalAmount); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { - await liquidityMining.add(token2.address, allocationPoint2, false); - //turn off bonus period - await advanceBlocks(bonusEndBlock); - - let blocks = new BN(1000); - let duration = secondsPerBlock.mul(blocks); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await token1.approve(liquidityMining.address, amount2, { from: account2 }); - await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { from: account2 }); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks); - expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); - let totalAmount = amount1.add(amount2).add(amount3); - expectedReward = expectedReward.mul(amount3).div(totalAmount); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - }); - - describe("deposit/withdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - for (let token of [token1, token2]) { - for (let account of [account1, account2]) { - await token.mint(account, amount); - await token.approve(liquidityMining.address, amount, { from: account }); - } - } - - // make sure the pool has tokens to distribute - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - }); - - it("add, add, deposit, deposit", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 - await liquidityMining.add(token2.address, allocationPoint, false); //weight 1/2 - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); // 1 block passed - - // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); //weight 2/3 - await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit - - const currentBlockNumber = await web3.eth.getBlockNumber(); - - // 3 tokens per share per block, times bonus multiplier (10), times precision (1e12), times weight (1/2), divided by total shares - const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock.mul(new BN(10)).mul(new BN(1e12)).div(new BN(2)).div(amount); - - const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); - expect(poolInfo1.poolToken).equal(token1.address); - expect(poolInfo1.allocationPoint).equal("1"); - expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); - // token1 deposit has been there for 2 blocks because of automining - expect(poolInfo1.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString()); - - const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); - expect(poolInfo2.poolToken).equal(token2.address); - expect(poolInfo2.allocationPoint).equal("1"); - expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); - // token2 deposit has been there for only 1 block - expect(poolInfo2.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.toString()); - }); - - // tricky case 1 - it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 - await liquidityMining.add(token2.address, allocationPoint, false); //weight 1/2 - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await liquidityMining.update(token1.address, new BN("2"), false); // 1 block passed, new weight 2/3 - const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); // 2 blocks passed - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 30 (because of bonus period), 1 block with weight 1/2 = 15, 1 block with weight 2/3 = 20 - const expectedRewardAmount = new BN("35"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: rewardAmount, - }); - }); - - // tricky case 2 - it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 - - // deposit 1: 0 blocks, deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks - await mineBlock(); - - // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account2 }); - - // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) - const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) - const withdrawTx2 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account2 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount1 = await lockedSOV.getLockedBalance(account1); - const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); - const reward1 = lockedAmount1.add(unlockedAmount1); - - const lockedAmount2 = await lockedSOV.getLockedBalance(account2); - const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); - const reward2 = lockedAmount2.add(unlockedAmount2); - - // reward per block 30 (because of bonus period), 2 block with 100% shares = 60, 1 block with 50% shares = 15 - const expectedReward1 = new BN("75"); - - // reward per block 30 (because of bonus period), 1 block with 50% shares = 15, 1 block with 100% shares = 30 - const expectedReward2 = new BN("45"); - - expect(reward1).bignumber.equal(expectedReward1); - expect(reward2).bignumber.equal(expectedReward2); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - await checkUserPoolTokens( - account2, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - - expectEvent(withdrawTx1, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx1, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: reward1, - }); - expectEvent(withdrawTx2, "Withdraw", { - user: account2, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx2, "RewardClaimed", { - user: account2, - poolToken: token1.address, - amount: reward2, - }); - }); - - // tricky case 3a - it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 - - // deposit: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit: 1 blocks, note: pool1 is NOT updated - await liquidityMining.add(token2.address, new BN(2), false); // new weight: 1/3 - - // deposit: 2 blocks - await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 30 (because of bonus period), - // because add was called without updating the pool, the new weight is used for all blocks - // so 2 blocks with weight 1/3 = 20 - const expectedRewardAmount = new BN("20"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - }); - - // tricky case 3b - it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 - - // deposit: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit: 1 blocks, note: pool1 IS updated - await liquidityMining.add(token2.address, new BN(2), true); // new weight: 1/3 - - // deposit: 2 blocks - await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 30 (because of bonus period), - // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block - // so 1 block with weight 1/1 = 30 and 1 block with weight 1/3 = 10 - const expectedRewardAmount = new BN("40"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - }); - - // tricky case 4 - it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 - - // deposit 1: 0 blocks, deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated - await liquidityMining.add(token2.address, allocationPoint, true); //weight 1/2 - - // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account2 }); - - // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) - const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) - const withdrawTx2 = await liquidityMining.withdraw(token2.address, amount, ZERO_ADDRESS, { from: account2 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount1 = await lockedSOV.getLockedBalance(account1); - const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); - const reward1 = lockedAmount1.add(unlockedAmount1); - - const lockedAmount2 = await lockedSOV.getLockedBalance(account2); - const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); - const reward2 = lockedAmount2.add(unlockedAmount2); - - // reward per block 30 (because of bonus period) - // deposit 1 has 1 block with weight 1/1 (30) and 2 blocks with weight 1/2 (15*2 = 30) - const expectedReward1 = new BN("60"); - - // deposit 2 has 2 blocks with weight 1/2 (15 * 2 = 30) - const expectedReward2 = new BN("30"); - - expect(reward1).bignumber.equal(expectedReward1); - expect(reward2).bignumber.equal(expectedReward2); - - for (let account of [account1, account2]) { - for (let token of [token1, token2]) { - await checkUserPoolTokens( - account, - token, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - } - } - - expectEvent(withdrawTx1, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx1, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: reward1, - }); - expectEvent(withdrawTx2, "Withdraw", { - user: account2, - poolToken: token2.address, - amount: amount, - }); - expectEvent(withdrawTx2, "RewardClaimed", { - user: account2, - poolToken: token2.address, - amount: reward2, - }); - }); - }); - - describe("LM configuration", () => { - //Maximum reward per week: 100K SOV (or 100M SOV) - //Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) - - const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); - // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); - - //SOV/BTC pool 40K per week - //ETH/BTC pool 37.5K per week (from second week) - //Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) - - const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); - // const MAX_ALLOCATION_POINT = new BN(100000); - const ALLOCATION_POINT_SOV_BTC = new BN(40000); - const ALLOCATION_POINT_ETH_BTC = new BN(37500); - - const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); - - const amount = new BN(1000); - - beforeEach(async () => { - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - REWARD_TOKENS_PER_BLOCK, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - 0 - ); - - for (let token of [token1, token2]) { - for (let account of [account1, account2]) { - await token.mint(account, amount); - await token.approve(liquidityMining.address, amount, { from: account }); - } - } - - //turn off bonus period - let bonusEndBlock = await liquidityMining.bonusEndBlock(); - await advanceBlocks(bonusEndBlock); - }); - - it("dummy pool + 1 pool", async () => { - let dummyPool = liquidityMiningConfigToken.address; - - let SOVBTCpool = token1.address; - - await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); //weight 40000 / 100000 - await liquidityMining.add(dummyPool, MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC), false); //weight (100000 - 40000) / 100000 - - await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - //reward won't be claimed because liquidityMining doesn't have enough SOV balance - //user reward will be updated - //10 blocks passed since last deposit - await mineBlocks(9); - await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); - //10 blocks passed - let passedBlocks = 10; - let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) - .mul(ALLOCATION_POINT_SOV_BTC) - .div(MAX_ALLOCATION_POINT); - expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); - console.log(expectedUserReward.toString()); - }); - - it("dummy pool + 2 pools", async () => { - let dummyPool = liquidityMiningConfigToken.address; - - let SOVBTCpool = token1.address; - let ETHBTCpoll = token2.address; - - await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); //weight 40000 / 100000 - const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); - await liquidityMining.add(dummyPool, DUMMY_ALLOCATION_POINT, false); //weight (100000 - 40000) / 100000 - - await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - await mineBlocks(9); - await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit - - //update config - //this method will also update pool reward using previous allocation point, - //so this block should be add to calculation with old values - await liquidityMining.update(SOVBTCpool, ALLOCATION_POINT_SOV_BTC_2, false); //weight 30000 / 100000 - - await liquidityMining.add(ETHBTCpoll, ALLOCATION_POINT_ETH_BTC, false); //weight 37500 / 100000 - const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC_2).sub(ALLOCATION_POINT_ETH_BTC); - await liquidityMining.update(dummyPool, DUMMY_ALLOCATION_POINT_2, false); //weight (100000 - 30000 - 37500) / 100000 - await liquidityMining.updateAllPools(); - - //reward won't be claimed because liquidityMining doesn't have enough SOV balance - //user reward will be updated - //10 blocks + 5 blocks passed - await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); - //10 blocks + 5 blocks passed - let passedBlocks = 10 + 1; //block should be add to calculation with old values - let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) - .mul(ALLOCATION_POINT_SOV_BTC) - .div(MAX_ALLOCATION_POINT); - passedBlocks = 5 - 1; //block should be removed from calculation with new values - expectedUserReward = expectedUserReward.add( - REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)).mul(ALLOCATION_POINT_SOV_BTC_2).div(MAX_ALLOCATION_POINT) - ); - expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); - console.log(expectedUserReward.toString()); - }); - }); - - describe("onTokensDeposited", () => { - it("should revert if the sender is not a valid pool token", async () => { - await expectRevert(liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), "Pool token not found"); - }); - }); - - describe("external getters", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - await liquidityMining.add(token1.address, allocationPoint, false); - }); - - it("PRECISION", async () => { - expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); - }); - - it("BONUS_BLOCK_MULTIPLIER", async () => { - expect(await liquidityMining.BONUS_BLOCK_MULTIPLIER()).bignumber.equal("10"); - }); - - it("SVR", async () => { - expect(await liquidityMining.SOV()).equal(SOVToken.address); - }); - - it("rewardTokensPerBlock", async () => { - expect(await liquidityMining.rewardTokensPerBlock()).bignumber.equal(rewardTokensPerBlock); - }); - - it("startBlock", async () => { - expect(await liquidityMining.startBlock()).bignumber.gt("0"); - }); - - it("bonusEndBlock", async () => { - const startBlock = await liquidityMining.startBlock(); - expect(await liquidityMining.bonusEndBlock()).bignumber.equal(startBlock.add(numberOfBonusBlocks)); - }); - - it("endBlock", async () => { - expect(await liquidityMining.endBlock()).bignumber.equal("0"); - }); - - it("wrapper", async () => { - expect(await liquidityMining.wrapper()).equal(wrapper.address); - }); - - it("totalAllocationPoint", async () => { - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); - await liquidityMining.add(token2.address, allocationPoint, false); - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint.mul(new BN(2))); - }); - - it("totalUsersBalance", async () => { - expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); - - await liquidityMining.updateAllPools(); - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); - - await liquidityMining.updateAllPools(); - expect(await liquidityMining.totalUsersBalance()).bignumber.equal("30"); - }); - - // could still test these, but I don't see much point: - // PoolInfo[] public poolInfoList; - // mapping(address => uint256) poolIdList; - // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; - - it("getMissedBalance", async () => { - let missedBalance = await liquidityMining.getMissedBalance(); - expect(missedBalance).bignumber.equal("0"); - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await liquidityMining.updatePool(token1.address); - - missedBalance = await liquidityMining.getMissedBalance(); - expect(missedBalance).bignumber.equal("30"); - }); - - it("getUserAccumulatedReward", async () => { - // real tests are elsewhere in this file - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token1.address, account2); - expect(reward1).bignumber.equal("30"); - expect(reward2).bignumber.equal("0"); - }); - - it("getPoolId", async () => { - const poolId = await liquidityMining.getPoolId(token1.address); - expect(poolId).bignumber.equal("0"); - await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); - await liquidityMining.add(token2.address, allocationPoint, false); - const poolId2 = await liquidityMining.getPoolId(token2.address); - expect(poolId2).bignumber.equal("1"); - }); - - it("getPoolLength", async () => { - let length = await liquidityMining.getPoolLength(); - expect(length).bignumber.equal("1"); - - await liquidityMining.add(token2.address, allocationPoint, false); - length = await liquidityMining.getPoolLength(); - expect(length).bignumber.equal("2"); - }); - - it("getPoolInfoList", async () => { - const infoList = await liquidityMining.getPoolInfoList(); - expect(infoList).to.be.an("array"); - expect(infoList.length).equal(1); - const info = infoList[0]; - expect(info.poolToken).equal(token1.address); - expect(info.allocationPoint).equal(allocationPoint.toString()); - expect(info.accumulatedRewardPerShare).equal("0"); - expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); - }); - - it("getPoolInfo", async () => { - const info = await liquidityMining.getPoolInfo(token1.address); - expect(info.poolToken).equal(token1.address); - expect(info.allocationPoint).equal(allocationPoint.toString()); - expect(info.accumulatedRewardPerShare).equal("0"); - expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); - - await expectRevert(liquidityMining.getPoolInfo(token2.address), "Pool token not found"); - }); - - it("getUserBalanceList", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const balanceList = await liquidityMining.getUserBalanceList(account1); - - expect(balanceList).to.be.an("array"); - expect(balanceList.length).equal(1); - const balanceData = balanceList[0]; - expect(balanceData).to.be.an("array"); - expect(balanceData[0]).bignumber.equal(amount); - expect(balanceData[1]).bignumber.equal("30"); - }); - - it("getUserInfo", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.amount).bignumber.equal("500"); - expect(userInfo.accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? - expect(userInfo.rewardDebt).bignumber.equal("0"); // not yet updated either - - // deposit updates it. - await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); - userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.amount).bignumber.equal("501"); - expect(userInfo.accumulatedReward).bignumber.equal("30"); - expect(userInfo.rewardDebt).bignumber.equal("30"); - }); - - it("getUserInfoList", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let userInfoList = await liquidityMining.getUserInfoList(account1); - expect(userInfoList).to.be.an("array"); - expect(userInfoList.length).equal(1); - const userInfo = userInfoList[0]; - expect(userInfo.amount).bignumber.equal("500"); - expect(userInfo.accumulatedReward).bignumber.equal("0"); - expect(userInfo.rewardDebt).bignumber.equal("0"); - }); - - it("getUserAccumulatedRewardList", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); - expect(rewardList).to.be.an("array"); - expect(rewardList.length).equal(1); - expect(rewardList[0]).bignumber.equal("0"); - }); - - it("getUserPoolTokenBalance", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance(token1.address, account1); - expect(poolTokenBalance).bignumber.equal(new BN(500)); - }); - }); - - describe("Migration", () => { - const MigrationGracePeriodStates = { - None: 0, - Started: 1, - Finished: 2, - }; - - describe("Grace period start", () => { - it("should be deployed with state being before grace period", async () => { - const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); - expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.None); - }); - - it("should only allow grace period to be started by the admin", async () => { - await expectRevert(liquidityMining.startMigrationGracePeriod({ from: account1 }), "unauthorized"); - }); - - it("should properly start grace period", async () => { - await liquidityMining.startMigrationGracePeriod(); - - const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); - expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Started); - - const endBlock = await liquidityMining.endBlock(); - expect(endBlock).not.equal(0); - }); - - it("should fail when trying to start the grace period if already started", async () => { - await liquidityMining.startMigrationGracePeriod(); - // check that the grace period is started - const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); - expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Started); - - await expectRevert(liquidityMining.startMigrationGracePeriod(), "Forbidden: migration already started"); - }); - - it("should not let users to deposit", async () => { - await liquidityMining.startMigrationGracePeriod(); - await expectRevert( - liquidityMining.deposit(token1.address, new BN(10), ZERO_ADDRESS, { from: account1 }), - "Forbidden: migration already started" - ); - }); - - it("should not let lending pools to deposit", async () => { - await liquidityMining.startMigrationGracePeriod(); - await expectRevert( - liquidityMining.onTokensDeposited(token1.address, new BN(10), { from: account1 }), - "Forbidden: migration already started" - ); - }); - - it("should let users to update the pools information", async () => { - let allocationPoint = new BN(1); - await liquidityMining.add(token1.address, allocationPoint, false); - await liquidityMining.startMigrationGracePeriod(); - - await liquidityMining.updatePool(token1.address); - await liquidityMining.updateAllPools(); - }); - }); - - describe("Grace period finish", () => { - it("should only allow admin to finish the grace period", async () => { - await expectRevert(liquidityMining.finishMigrationGracePeriod({ from: account1 }), "unauthorized"); - }); - - it("should fail if grace period hasn't started before finishing", async () => { - await expectRevert( - liquidityMining.finishMigrationGracePeriod(), - "Forbidden: Migration hasn't started yet or already finished" - ); - }); - - it("should fail if grace period has already finished", async () => { - await liquidityMining.startMigrationGracePeriod(); - await liquidityMining.finishMigrationGracePeriod(); - - const migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); - expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Finished); - - await expectRevert( - liquidityMining.finishMigrationGracePeriod(), - "Forbidden: Migration hasn't started yet or already finished" - ); - }); - - it("should properly finish grace period", async () => { - await liquidityMining.startMigrationGracePeriod(); - // check that the grace period is started - let migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); - expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Started); - - await liquidityMining.finishMigrationGracePeriod(); - // check that the grace period is finished - migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); - expect(migrationGracePeriodState.toNumber()).to.equal(MigrationGracePeriodStates.Finished); - }); - - it("should not let users to withdraw", async () => { - await liquidityMining.startMigrationGracePeriod(); - await liquidityMining.finishMigrationGracePeriod(); - await expectRevert(liquidityMining.withdraw(token1.address, new BN(10), account1), "Forbidden: contract deprecated"); - await expectRevert(liquidityMining.emergencyWithdraw(token1.address), "Forbidden: contract deprecated"); - }); - - it("should not let users to claim rewards", async () => { - await liquidityMining.startMigrationGracePeriod(); - await liquidityMining.finishMigrationGracePeriod(); - await expectRevert(liquidityMining.claimRewardFromAllPools(account1), "Forbidden: contract deprecated"); - - await expectRevert(liquidityMining.claimReward(token1.address, account1), "Forbidden: contract deprecated"); - }); - - it("should not let users to update the pools information", async () => { - let allocationPoint = new BN(1); - await liquidityMining.add(token1.address, allocationPoint, false); - - await liquidityMining.startMigrationGracePeriod(); - await liquidityMining.finishMigrationGracePeriod(); - await expectRevert(liquidityMining.updatePool(token1.address), "Forbidden: contract deprecated"); - await expectRevert(liquidityMining.updateAllPools(), "Forbidden: contract deprecated"); - }); - }); - }); - - async function deployLiquidityMining() { - let liquidityMiningLogic = await LiquidityMiningLogic.new(); - liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); - liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - - wrapper = await Wrapper.new(liquidityMining.address); - } - - async function upgradeLiquidityMining() { - let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); - liquidityMining = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); - } - - async function deployLiquidityMiningV2() { - let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); - liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); - await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); - liquidityMiningV2 = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); - } - - async function checkBonusPeriodHasNotEnded() { - expect(await liquidityMining.bonusEndBlock()).bignumber.gt((await web3.eth.getBlockNumber()).toString()); - } - - function checkPoolInfo(poolInfo, token, allocationPoint, lastRewardBlock, accumulatedRewardPerShare) { - expect(poolInfo.poolToken).equal(token); - expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); - expect(poolInfo.lastRewardBlock).bignumber.equal(lastRewardBlock); - if (accumulatedRewardPerShare.toNumber() !== -1) { - expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(accumulatedRewardPerShare); - } - } - - async function checkUserPoolTokens(user, poolToken, _userAmount, _liquidityMiningBalance, _userBalance, wrapper) { - //user balance in pool - let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); - expect(userInfo.amount).bignumber.equal(_userAmount); - //LM balance of pool tokens - let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); - expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); - //user's balance of pool tokens - let userBalance = await poolToken.balanceOf(user); - if (wrapper !== undefined) { - userBalance = await poolToken.balanceOf(wrapper); - } - expect(userBalance).bignumber.equal(_userBalance); - } - - //user's balance of reward token - async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { - let passedBlocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(depositBlockNumber, latestBlockNumber); - let userReward = passedBlocks.mul(rewardTokensPerBlock); - let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); - expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); - return userReward; - } + let accounts; + let root, account1, account2, account3, account4; + let SOVToken, token1, token2, token3, liquidityMiningConfigToken; + let liquidityMining, liquidityMiningProxy, liquidityMiningV2, liquidityMiningProxyV2, wrapper; + let lockedSOVAdmins, lockedSOV; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMining.initialize(liquidityMiningV2.address); + }); + + describe("initialize", () => { + it("sets the expected values", async () => { + await deployLiquidityMining(); + let tx = await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + let _SOV = await liquidityMining.SOV(); + let _rewardTokensPerBlock = await liquidityMining.rewardTokensPerBlock(); + let _startBlock = await liquidityMining.startBlock(); + let _bonusEndBlock = await liquidityMining.bonusEndBlock(); + let _wrapper = await liquidityMining.wrapper(); + + let blockNumber = new BN(tx.receipt.blockNumber); + + expect(_SOV).equal(SOVToken.address); + expect(_rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); + expect(_startBlock).bignumber.equal(startDelayBlocks.add(blockNumber)); + expect(_bonusEndBlock).bignumber.equal( + startDelayBlocks.add(blockNumber).add(numberOfBonusBlocks) + ); + expect(_wrapper).equal(wrapper.address); + }); + + it("fails if not an owner or an admin", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent, + { from: account1 } + ), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent, + { from: account1 } + ); + }); + + it("fails if _startBlock = 0", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + 0, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Invalid start block" + ); + }); + + it("fails if already initialized", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Already initialized" + ); + }); + it("fails if already initialized", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + await liquidityMining.initialize(liquidityMiningV2.address); + await expectRevert( + liquidityMining.initialize(liquidityMiningV2.address), + "Already initialized" + ); + }); + it("fails if the 0 address is passed as token address", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + ZERO_ADDRESS, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Invalid token address" + ); + }); + it("fails if the 0 address is passed as LiquidityMiningV2 address", async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + await upgradeLiquidityMining(); + await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); + }); + + it("fails if unlockedImmediatelyPercent >= 10000", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + 12345 + ), + "Unlocked immediately percent has to be less than 10000." + ); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await liquidityMining.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + liquidityMining.addAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await liquidityMining.addAdmin(account1); + let tx = await liquidityMining.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + liquidityMining.removeAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("setLockedSOV", () => { + it("sets the expected values", async () => { + let newLockedSOV = account2; + await liquidityMining.setLockedSOV(newLockedSOV); + + let _lockedSOV = await liquidityMining.lockedSOV(); + expect(_lockedSOV).equal(newLockedSOV); + }); + + it("fails if not an owner and an admin", async () => { + await expectRevert( + liquidityMining.setLockedSOV(account2, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setLockedSOV(account2, { from: account1 }); + }); + + it("fails if zero address passed", async () => { + await expectRevert( + liquidityMining.setLockedSOV(ZERO_ADDRESS), + "Invalid lockedSOV Address." + ); + }); + }); + + describe("setUnlockedImmediatelyPercent", () => { + it("sets the expected values", async () => { + let newUnlockedImmediatelyPercent = new BN(2000); + await liquidityMining.setUnlockedImmediatelyPercent(newUnlockedImmediatelyPercent); + + let _unlockedImmediatelyPercent = await liquidityMining.unlockedImmediatelyPercent(); + expect(_unlockedImmediatelyPercent).bignumber.equal(newUnlockedImmediatelyPercent); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert( + liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }); + }); + + it("fails if unlockedImmediatelyPercent >= 10000", async () => { + await expectRevert( + liquidityMining.setUnlockedImmediatelyPercent(100000), + "Unlocked immediately percent has to be less than 10000." + ); + }); + }); + + describe("setWrapper", () => { + it("sets the expected values", async () => { + let newWrapper = account2; + await liquidityMining.setWrapper(newWrapper); + + let _wrapper = await liquidityMining.wrapper(); + expect(_wrapper).equal(newWrapper); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert( + liquidityMining.setWrapper(account2, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setWrapper(account2, { from: account1 }); + }); + }); + + describe("stopMining", () => { + it("should set end block", async () => { + let tx = await liquidityMining.stopMining(); + + let blockNumber = new BN(tx.receipt.blockNumber); + let _endBlock = await liquidityMining.endBlock(); + expect(_endBlock).bignumber.equal(blockNumber); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert(liquidityMining.stopMining({ from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.stopMining({ from: account1 }); + }); + + it("fails if already stopped", async () => { + await liquidityMining.stopMining(); + await expectRevert(liquidityMining.stopMining(), "Already stopped"); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOVToken.transfer(liquidityMining.address, amount); + + let balanceBefore = await SOVToken.balanceOf(account1); + await liquidityMining.transferSOV(account1, amount); + let balanceAfter = await SOVToken.balanceOf(account1); + + expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert( + liquidityMining.transferSOV(account1, 1000, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.transferSOV(account1, 1000, { from: account1 }); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + liquidityMining.transferSOV(ZERO_ADDRESS, 1000), + "Receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(liquidityMining.transferSOV(account1, 0), "Amount invalid"); + }); + }); + + describe("add", () => { + it("should be able to add pool token", async () => { + let allocationPoint = new BN(1); + let tx = await liquidityMining.add(token1.address, allocationPoint, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); + + let poolInfo = await liquidityMining.poolInfoList(0); + expect(poolInfo.poolToken).equal(token1.address); + expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); + let blockNumber = new BN(tx.receipt.blockNumber); + expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); + expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + allocationPoint: allocationPoint, + }); + }); + + it("should be able to add 2 pool tokens and update pools", async () => { + let allocationPoint1 = new BN(1); + let tx1 = await liquidityMining.add(token1.address, allocationPoint1, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1); + + expectEvent(tx1, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + allocationPoint: allocationPoint1, + }); + + let allocationPoint2 = new BN(2); + let tx2 = await liquidityMining.add(token2.address, allocationPoint2, true); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + allocationPoint1.add(allocationPoint2) + ); + + expectEvent(tx2, "PoolTokenAdded", { + user: root, + poolToken: token2.address, + allocationPoint: allocationPoint2, + }); + + let poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + let poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo1.lastRewardBlock).bignumber.equal(poolInfo2.lastRewardBlock); + }); + + it("fails if the 0 allocation point is passed", async () => { + await expectRevert( + liquidityMining.add(token1.address, new BN(0), false), + "Invalid allocation point" + ); + }); + + it("fails if the 0 address is passed as token address", async () => { + await expectRevert( + liquidityMining.add(ZERO_ADDRESS, new BN(1), false), + "Invalid token address" + ); + }); + + it("fails if token already added", async () => { + await liquidityMining.add(token1.address, new BN(1), false); + await expectRevert( + liquidityMining.add(token1.address, new BN(1), false), + "Token already added" + ); + }); + + it("only owner or admin should be able to add pool token", async () => { + await expectRevert( + liquidityMining.add(token2.address, new BN(1), false, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.add(token2.address, new BN(1), false, { from: account1 }); + }); + }); + + describe("update", () => { + it("should be able to update pool token", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, oldAllocationPoint, false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, newAllocationPoint, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + newAllocationPoint + ); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, newAllocationPoint, blockNumber, new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: token1.address, + newAllocationPoint: newAllocationPoint, + oldAllocationPoint: oldAllocationPoint, + }); + }); + + it("should be able to update pool token and update pools", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, oldAllocationPoint, false); + + await liquidityMining.add(token2.address, oldAllocationPoint, false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, newAllocationPoint, true); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + oldAllocationPoint.add(newAllocationPoint) + ); + + let poolInfo = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo.lastRewardBlock).bignumber.equal(new BN(tx.receipt.blockNumber)); + }); + + it("fails if token wasn't added", async () => { + await expectRevert( + liquidityMining.update(token1.address, new BN(1), false), + "Pool token not found" + ); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert( + liquidityMining.update(token2.address, new BN(1), false, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.update(token2.address, new BN(1), false, { from: account1 }); + }); + }); + + describe("updateTokens", () => { + it("should be able to update 2 pool tokens", async () => { + let poolTokens = [token1.address, token2.address, token3.address]; + let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; + + for (let i = 0; i < poolTokens.length; i++) { + await liquidityMining.add(poolTokens[i], oldAllocationPoints[i], false); + } + + let newAllocationPoints = [new BN(101), new BN(102), new BN(3)]; + let tx = await liquidityMining.updateTokens(poolTokens, newAllocationPoints, true); + + let totalAllocationPoint = new BN(0); + for (let i = 0; i < newAllocationPoints.length; i++) { + totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i]); + } + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + totalAllocationPoint + ); + + let blockNumber = new BN(tx.receipt.blockNumber); + for (let i = 0; i < poolTokens.length - 1; i++) { + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); + checkPoolInfo( + poolInfo, + poolTokens[i], + newAllocationPoints[i], + blockNumber, + new BN(0) + ); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: poolTokens[i], + newAllocationPoint: newAllocationPoints[i], + oldAllocationPoint: oldAllocationPoints[i], + }); + } + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); + + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[poolTokens.length - 1]); + expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); + }); + + it("fails if token wasn't added", async () => { + await expectRevert( + liquidityMining.updateTokens([token1.address], [new BN(1)], false), + "Pool token not found" + ); + }); + + it("fails if arrays have different length", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert( + liquidityMining.updateTokens([token1.address, token2.address], [new BN(1)], false), + "Arrays mismatch" + ); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert( + liquidityMining.updateTokens([token2.address], [new BN(1)], false, { + from: account1, + }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.updateTokens([token2.address], [new BN(1)], false, { + from: account1, + }); + }); + }); + + describe("deposit", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to deposit", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to deposit using wrapper", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, account2, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("claimReward", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await expectRevert( + liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }), + "Claiming reward failed" + ); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { + from: account1, + }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward, + }); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimReward(token1.address, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.claimReward(account1, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("claimRewardFromAllPools", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await liquidityMining.add(token2.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await expectRevert( + liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), + "Claiming reward failed" + ); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); + let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); + + let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { + from: account1, + }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward1 = await checkUserReward( + account1, + token1, + depositBlockNumber1, + latestBlockNumber + ); + //we have 2 pools with the same allocation points + userReward1 = userReward1.div(new BN(2)); + + await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); + let userReward2 = await checkUserReward( + account1, + token2, + depositBlockNumber2, + latestBlockNumber + ); + //we have 2 pools with the same allocation points + userReward2 = userReward2.div(new BN(2)); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward1, + }); + + expect(userReward1, tx.logs[0].args.amount); + expect(token1.address, tx.logs[0].args.poolToken); + expect(userReward2, tx.logs[1].args.amount); + expect(token2.address, tx.logs[1].args.poolToken); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + }); + + describe("withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw (without claiming reward)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + // User's balance on lockedSOV vault + let userRewardBalance = await lockedSOV.getLockedBalance(account1); + expect(userRewardBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to withdraw (with claiming reward)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + //withdrawAndStakeTokensFrom was not invoked + let expectedUnlockedBalance = userReward + .mul(unlockedImmediatelyPercent) + .div(new BN(10000)); + let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); + expect(lockedBalance).bignumber.equal(expectedLockedBalance); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward, + }); + }); + + it("should be able to withdraw using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), + new BN(0), + amount, + wrapper.address + ); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { + from: account1, + }), + "Not enough balance" + ); + }); + }); + + describe("emergencyWithdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + + let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + let expectedAccumulatedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); + expectEvent(tx, "EmergencyWithdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + accumulatedReward: expectedAccumulatedReward, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.emergencyWithdraw(account1, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("getPassedBlocksWithBonusMultiplier", () => { + it("check calculation", async () => { + let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + let startBlock = await liquidityMining.startBlock(); + let bonusEndBlock = await liquidityMining.bonusEndBlock(); + let blocks; + + //[startBlock, bonusEndBlock] + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock, + bonusEndBlock + ); + expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); + + //[startBlock - 100, bonusEndBlock] + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock.sub(new BN(100)), + bonusEndBlock + ); + expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); + + //[startBlock, bonusEndBlock + 100] + let blocksAfterBonusPeriod = new BN(100); + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock, + bonusEndBlock.add(new BN(blocksAfterBonusPeriod)) + ); + expect(blocks).bignumber.equal( + numberOfBonusBlocks.mul(bonusBlockMultiplier).add(blocksAfterBonusPeriod) + ); + + //[startBlock, stopMining, ... bonusEndBlock] + await mineBlocks(5); + await liquidityMining.stopMining(); + let endBlock = await liquidityMining.endBlock(); + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock, + bonusEndBlock + ); + expect(blocks).bignumber.equal(endBlock.sub(startBlock).mul(bonusBlockMultiplier)); + }); + }); + + describe("getUserAccumulatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let bonusBlockMultiplier; + let bonusEndBlock; + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint1, false); + await liquidityMining.add(token2.address, allocationPoint2, false); + + await token1.mint(account1, amount1); + await token2.mint(account2, amount2); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token2.approve(liquidityMining.address, amount2, { from: account2 }); + + bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + bonusEndBlock = await liquidityMining.bonusEndBlock(); + }); + + it("check calculation for no deposits", async () => { + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token2.address, + account2 + ); + expect(reward1).bignumber.equal("0"); + expect(reward2).bignumber.equal("0"); + }); + + it("check calculation for single user, token 1, bonus period off", async () => { + await advanceBlocks(bonusEndBlock); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + + // 1 block has passed, bonus period is off + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 1 * 3 * 1/3 = 1 + const expectedReward = rewardTokensPerBlock + .mul(allocationPoint1) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("1"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + expect(reward).bignumber.equal("2"); + }); + + it("check calculation for single user, token 2, bonus period off", async () => { + await advanceBlocks(bonusEndBlock); + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + + // 1 block has passed, bonus period is off + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 1 * 3 * 2/3 = 2 + const expectedReward = rewardTokensPerBlock + .mul(allocationPoint2) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("2"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + expect(reward).bignumber.equal("4"); + }); + + it("check calculation for single user, token 1, bonus period on", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 10 * 3 * 1/3 = 10 + const expectedReward = rewardTokensPerBlock + .mul(bonusBlockMultiplier) + .mul(allocationPoint1) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("10"); // sanity check + expect(reward).bignumber.equal(expectedReward); + }); + + it("check calculation for single user, token 1, bonus period on, smaller amount", async () => { + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 10 * 3 * 1/3 = 10 + // Note that the actual amount deposited plays no role here + expect(reward).bignumber.equal("10"); + }); + + it("check calculation for single user, token 2, bonus period on", async () => { + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward( + token2.address, + account2 + ); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 10 * 3 * 2/3 = 20 + const expectedReward = rewardTokensPerBlock + .mul(bonusBlockMultiplier) + .mul(allocationPoint2) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("20"); // sanity check + expect(reward).bignumber.equal(expectedReward); + }); + + it("check calculation for two users and tokens", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + // sanity checks + expect( + await liquidityMining.getUserAccumulatedReward(token1.address, account1) + ).bignumber.equal("10"); + expect( + await liquidityMining.getUserAccumulatedReward(token2.address, account2) + ).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token2.address, + account2 + ); + + // for the first block, user 1 will receive the reward of 10 + // for the second block: + // - user 1 still owns 100% of the shares for token1, so same reward (total 10 + 10 = 20) + // - user 2 owns 100% of the shares for token2, so same reward as in the other cases + expect(reward1).bignumber.equal("20"); + expect(reward2).bignumber.equal("20"); + }); + + it("check calculation for two users, same token (shares taken into account)", async () => { + const token = token1; + const amount = amount1; + await token.mint(account2, amount); + await token.approve(liquidityMining.address, amount, { from: account2 }); + + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); + // sanity checks + expect( + await liquidityMining.getUserAccumulatedReward(token.address, account1) + ).bignumber.equal("10"); + expect( + await liquidityMining.getUserAccumulatedReward(token.address, account2) + ).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward( + token.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token.address, + account2 + ); + + // for the first block, user 1 will receive the reward of 10 (reward given per block for 100% of shares) + // for the second block: + // - user 1 owns 1/2 of the shares => expected reward = 5 (total 10 + 5 = 15) + // - user 2 owns 1/2 of the shares => expected reward = 5 + expect(reward1).bignumber.equal("15"); + expect(reward2).bignumber.equal("5"); + }); + }); + + describe("getEstimatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const amount3 = new BN(4000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let bonusBlockMultiplier; + let bonusEndBlock; + let secondsPerBlock; + + beforeEach(async () => { + await liquidityMining.add(token1.address, allocationPoint1, false); + + await token1.mint(account1, amount1); + await token1.mint(account2, amount2); + await token1.mint(account3, amount3); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + + bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + bonusEndBlock = await liquidityMining.bonusEndBlock(); + + secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); + }); + + it("check calculation for 1 user, period less than 1 block", async () => { + let duration = secondsPerBlock.sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = "0"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 1 block", async () => { + let duration = secondsPerBlock; + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 30 blocks", async () => { + let blocks = new BN(30); + let duration = secondsPerBlock.mul(blocks); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks).mul(bonusBlockMultiplier); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 2 users, period is 100 blocks", async () => { + //turn off bonus period + await advanceBlocks(bonusEndBlock); + + let blocks = new BN(100); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks); + let totalAmount = amount1.add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { + await liquidityMining.add(token2.address, allocationPoint2, false); + //turn off bonus period + await advanceBlocks(bonusEndBlock); + + let blocks = new BN(1000); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks); + expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); + let totalAmount = amount1.add(amount2).add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + }); + + describe("deposit/withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + // make sure the pool has tokens to distribute + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + }); + + it("add, add, deposit, deposit", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + await liquidityMining.add(token2.address, allocationPoint, false); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); // 1 block passed + + // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); //weight 2/3 + await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit + + const currentBlockNumber = await web3.eth.getBlockNumber(); + + // 3 tokens per share per block, times bonus multiplier (10), times precision (1e12), times weight (1/2), divided by total shares + const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock + .mul(new BN(10)) + .mul(new BN(1e12)) + .div(new BN(2)) + .div(amount); + + const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + expect(poolInfo1.poolToken).equal(token1.address); + expect(poolInfo1.allocationPoint).equal("1"); + expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); + // token1 deposit has been there for 2 blocks because of automining + expect(poolInfo1.accumulatedRewardPerShare).equal( + expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString() + ); + + const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo2.poolToken).equal(token2.address); + expect(poolInfo2.allocationPoint).equal("1"); + expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); + // token2 deposit has been there for only 1 block + expect(poolInfo2.accumulatedRewardPerShare).equal( + expectedAccumulatedRewardPerBlock.toString() + ); + }); + + // tricky case 1 + it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + await liquidityMining.add(token2.address, allocationPoint, false); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await liquidityMining.update(token1.address, new BN("2"), false); // 1 block passed, new weight 2/3 + const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); // 2 blocks passed + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), 1 block with weight 1/2 = 15, 1 block with weight 2/3 = 20 + const expectedRewardAmount = new BN("35"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: rewardAmount, + }); + }); + + // tricky case 2 + it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks + await mineBlock(); + + // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account2, + }); + + // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) + const withdrawTx1 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account1 } + ); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) + const withdrawTx2 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account2 } + ); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 30 (because of bonus period), 2 block with 100% shares = 60, 1 block with 50% shares = 15 + const expectedReward1 = new BN("75"); + + // reward per block 30 (because of bonus period), 1 block with 50% shares = 15, 1 block with 100% shares = 30 + const expectedReward2 = new BN("45"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + await checkUserPoolTokens( + account2, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + poolToken: token1.address, + amount: reward2, + }); + }); + + // tricky case 3a + it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit: 1 blocks, note: pool1 is NOT updated + await liquidityMining.add(token2.address, new BN(2), false); // new weight: 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), + // because add was called without updating the pool, the new weight is used for all blocks + // so 2 blocks with weight 1/3 = 20 + const expectedRewardAmount = new BN("20"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 3b + it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit: 1 blocks, note: pool1 IS updated + await liquidityMining.add(token2.address, new BN(2), true); // new weight: 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), + // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block + // so 1 block with weight 1/1 = 30 and 1 block with weight 1/3 = 10 + const expectedRewardAmount = new BN("40"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 4 + it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated + await liquidityMining.add(token2.address, allocationPoint, true); //weight 1/2 + + // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account2, + }); + + // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) + const withdrawTx1 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account1 } + ); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) + const withdrawTx2 = await liquidityMining.withdraw( + token2.address, + amount, + ZERO_ADDRESS, + { from: account2 } + ); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 30 (because of bonus period) + // deposit 1 has 1 block with weight 1/1 (30) and 2 blocks with weight 1/2 (15*2 = 30) + const expectedReward1 = new BN("60"); + + // deposit 2 has 2 blocks with weight 1/2 (15 * 2 = 30) + const expectedReward2 = new BN("30"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + for (let account of [account1, account2]) { + for (let token of [token1, token2]) { + await checkUserPoolTokens( + account, + token, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + } + } + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token2.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + poolToken: token2.address, + amount: reward2, + }); + }); + }); + + describe("LM configuration", () => { + //Maximum reward per week: 100K SOV (or 100M SOV) + //Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) + + const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); + // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); + + //SOV/BTC pool 40K per week + //ETH/BTC pool 37.5K per week (from second week) + //Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) + + const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); + // const MAX_ALLOCATION_POINT = new BN(100000); + const ALLOCATION_POINT_SOV_BTC = new BN(40000); + const ALLOCATION_POINT_ETH_BTC = new BN(37500); + + const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); + + const amount = new BN(1000); + + beforeEach(async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + REWARD_TOKENS_PER_BLOCK, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + 0 + ); + + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + //turn off bonus period + let bonusEndBlock = await liquidityMining.bonusEndBlock(); + await advanceBlocks(bonusEndBlock); + }); + + it("dummy pool + 1 pool", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + + await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); //weight 40000 / 100000 + await liquidityMining.add( + dummyPool, + MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC), + false + ); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks passed since last deposit + await mineBlocks(9); + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks passed + let passedBlocks = 10; + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); + console.log(expectedUserReward.toString()); + }); + + it("dummy pool + 2 pools", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + let ETHBTCpoll = token2.address; + + await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); //weight 40000 / 100000 + const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); + await liquidityMining.add(dummyPool, DUMMY_ALLOCATION_POINT, false); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlocks(9); + await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit + + //update config + //this method will also update pool reward using previous allocation point, + //so this block should be add to calculation with old values + await liquidityMining.update(SOVBTCpool, ALLOCATION_POINT_SOV_BTC_2, false); //weight 30000 / 100000 + + await liquidityMining.add(ETHBTCpoll, ALLOCATION_POINT_ETH_BTC, false); //weight 37500 / 100000 + const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub( + ALLOCATION_POINT_SOV_BTC_2 + ).sub(ALLOCATION_POINT_ETH_BTC); + await liquidityMining.update(dummyPool, DUMMY_ALLOCATION_POINT_2, false); //weight (100000 - 30000 - 37500) / 100000 + await liquidityMining.updateAllPools(); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks + 5 blocks passed + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks + 5 blocks passed + let passedBlocks = 10 + 1; //block should be add to calculation with old values + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + passedBlocks = 5 - 1; //block should be removed from calculation with new values + expectedUserReward = expectedUserReward.add( + REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC_2) + .div(MAX_ALLOCATION_POINT) + ); + expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); + console.log(expectedUserReward.toString()); + }); + }); + + describe("onTokensDeposited", () => { + it("should revert if the sender is not a valid pool token", async () => { + await expectRevert( + liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), + "Pool token not found" + ); + }); + }); + + describe("external getters", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await liquidityMining.add(token1.address, allocationPoint, false); + }); + + it("PRECISION", async () => { + expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); + }); + + it("BONUS_BLOCK_MULTIPLIER", async () => { + expect(await liquidityMining.BONUS_BLOCK_MULTIPLIER()).bignumber.equal("10"); + }); + + it("SVR", async () => { + expect(await liquidityMining.SOV()).equal(SOVToken.address); + }); + + it("rewardTokensPerBlock", async () => { + expect(await liquidityMining.rewardTokensPerBlock()).bignumber.equal( + rewardTokensPerBlock + ); + }); + + it("startBlock", async () => { + expect(await liquidityMining.startBlock()).bignumber.gt("0"); + }); + + it("bonusEndBlock", async () => { + const startBlock = await liquidityMining.startBlock(); + expect(await liquidityMining.bonusEndBlock()).bignumber.equal( + startBlock.add(numberOfBonusBlocks) + ); + }); + + it("endBlock", async () => { + expect(await liquidityMining.endBlock()).bignumber.equal("0"); + }); + + it("wrapper", async () => { + expect(await liquidityMining.wrapper()).equal(wrapper.address); + }); + + it("totalAllocationPoint", async () => { + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); + await liquidityMining.add(token2.address, allocationPoint, false); + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + allocationPoint.mul(new BN(2)) + ); + }); + + it("totalUsersBalance", async () => { + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("30"); + }); + + // could still test these, but I don't see much point: + // PoolInfo[] public poolInfoList; + // mapping(address => uint256) poolIdList; + // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + + it("getMissedBalance", async () => { + let missedBalance = await liquidityMining.getMissedBalance(); + expect(missedBalance).bignumber.equal("0"); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await liquidityMining.updatePool(token1.address); + + missedBalance = await liquidityMining.getMissedBalance(); + expect(missedBalance).bignumber.equal("30"); + }); + + it("getUserAccumulatedReward", async () => { + // real tests are elsewhere in this file + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account2 + ); + expect(reward1).bignumber.equal("30"); + expect(reward2).bignumber.equal("0"); + }); + + it("getPoolId", async () => { + const poolId = await liquidityMining.getPoolId(token1.address); + expect(poolId).bignumber.equal("0"); + await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); + await liquidityMining.add(token2.address, allocationPoint, false); + const poolId2 = await liquidityMining.getPoolId(token2.address); + expect(poolId2).bignumber.equal("1"); + }); + + it("getPoolLength", async () => { + let length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("1"); + + await liquidityMining.add(token2.address, allocationPoint, false); + length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("2"); + }); + + it("getPoolInfoList", async () => { + const infoList = await liquidityMining.getPoolInfoList(); + expect(infoList).to.be.an("array"); + expect(infoList.length).equal(1); + const info = infoList[0]; + expect(info.poolToken).equal(token1.address); + expect(info.allocationPoint).equal(allocationPoint.toString()); + expect(info.accumulatedRewardPerShare).equal("0"); + expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + }); + + it("getPoolInfo", async () => { + const info = await liquidityMining.getPoolInfo(token1.address); + expect(info.poolToken).equal(token1.address); + expect(info.allocationPoint).equal(allocationPoint.toString()); + expect(info.accumulatedRewardPerShare).equal("0"); + expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + + await expectRevert( + liquidityMining.getPoolInfo(token2.address), + "Pool token not found" + ); + }); + + it("getUserBalanceList", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const balanceList = await liquidityMining.getUserBalanceList(account1); + + expect(balanceList).to.be.an("array"); + expect(balanceList.length).equal(1); + const balanceData = balanceList[0]; + expect(balanceData).to.be.an("array"); + expect(balanceData[0]).bignumber.equal(amount); + expect(balanceData[1]).bignumber.equal("30"); + }); + + it("getUserInfo", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? + expect(userInfo.rewardDebt).bignumber.equal("0"); // not yet updated either + + // deposit updates it. + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { + from: account1, + }); + userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("501"); + expect(userInfo.accumulatedReward).bignumber.equal("30"); + expect(userInfo.rewardDebt).bignumber.equal("30"); + }); + + it("getUserInfoList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let userInfoList = await liquidityMining.getUserInfoList(account1); + expect(userInfoList).to.be.an("array"); + expect(userInfoList.length).equal(1); + const userInfo = userInfoList[0]; + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.accumulatedReward).bignumber.equal("0"); + expect(userInfo.rewardDebt).bignumber.equal("0"); + }); + + it("getUserAccumulatedRewardList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); + expect(rewardList).to.be.an("array"); + expect(rewardList.length).equal(1); + expect(rewardList[0]).bignumber.equal("0"); + }); + + it("getUserPoolTokenBalance", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance( + token1.address, + account1 + ); + expect(poolTokenBalance).bignumber.equal(new BN(500)); + }); + }); + + describe("Migration", () => { + const MigrationGracePeriodStates = { + None: 0, + Started: 1, + Finished: 2, + }; + + describe("Grace period start", () => { + it("should be deployed with state being before grace period", async () => { + const migrationGracePeriodState = + await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal( + MigrationGracePeriodStates.None + ); + }); + + it("should only allow grace period to be started by the admin", async () => { + await expectRevert( + liquidityMining.startMigrationGracePeriod({ from: account1 }), + "unauthorized" + ); + }); + + it("should properly start grace period", async () => { + await liquidityMining.startMigrationGracePeriod(); + + const migrationGracePeriodState = + await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal( + MigrationGracePeriodStates.Started + ); + + const endBlock = await liquidityMining.endBlock(); + expect(endBlock).not.equal(0); + }); + + it("should fail when trying to start the grace period if already started", async () => { + await liquidityMining.startMigrationGracePeriod(); + // check that the grace period is started + const migrationGracePeriodState = + await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal( + MigrationGracePeriodStates.Started + ); + + await expectRevert( + liquidityMining.startMigrationGracePeriod(), + "Forbidden: migration already started" + ); + }); + + it("should not let users to deposit", async () => { + await liquidityMining.startMigrationGracePeriod(); + await expectRevert( + liquidityMining.deposit(token1.address, new BN(10), ZERO_ADDRESS, { + from: account1, + }), + "Forbidden: migration already started" + ); + }); + + it("should not let lending pools to deposit", async () => { + await liquidityMining.startMigrationGracePeriod(); + await expectRevert( + liquidityMining.onTokensDeposited(token1.address, new BN(10), { + from: account1, + }), + "Forbidden: migration already started" + ); + }); + + it("should let users to update the pools information", async () => { + let allocationPoint = new BN(1); + await liquidityMining.add(token1.address, allocationPoint, false); + await liquidityMining.startMigrationGracePeriod(); + + await liquidityMining.updatePool(token1.address); + await liquidityMining.updateAllPools(); + }); + }); + + describe("Grace period finish", () => { + it("should only allow admin to finish the grace period", async () => { + await expectRevert( + liquidityMining.finishMigrationGracePeriod({ from: account1 }), + "unauthorized" + ); + }); + + it("should fail if grace period hasn't started before finishing", async () => { + await expectRevert( + liquidityMining.finishMigrationGracePeriod(), + "Forbidden: Migration hasn't started yet or already finished" + ); + }); + + it("should fail if grace period has already finished", async () => { + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + + const migrationGracePeriodState = + await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal( + MigrationGracePeriodStates.Finished + ); + + await expectRevert( + liquidityMining.finishMigrationGracePeriod(), + "Forbidden: Migration hasn't started yet or already finished" + ); + }); + + it("should properly finish grace period", async () => { + await liquidityMining.startMigrationGracePeriod(); + // check that the grace period is started + let migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal( + MigrationGracePeriodStates.Started + ); + + await liquidityMining.finishMigrationGracePeriod(); + // check that the grace period is finished + migrationGracePeriodState = await liquidityMining.migrationGracePeriodState(); + expect(migrationGracePeriodState.toNumber()).to.equal( + MigrationGracePeriodStates.Finished + ); + }); + + it("should not let users to withdraw", async () => { + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + await expectRevert( + liquidityMining.withdraw(token1.address, new BN(10), account1), + "Forbidden: contract deprecated" + ); + await expectRevert( + liquidityMining.emergencyWithdraw(token1.address), + "Forbidden: contract deprecated" + ); + }); + + it("should not let users to claim rewards", async () => { + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + await expectRevert( + liquidityMining.claimRewardFromAllPools(account1), + "Forbidden: contract deprecated" + ); + + await expectRevert( + liquidityMining.claimReward(token1.address, account1), + "Forbidden: contract deprecated" + ); + }); + + it("should not let users to update the pools information", async () => { + let allocationPoint = new BN(1); + await liquidityMining.add(token1.address, allocationPoint, false); + + await liquidityMining.startMigrationGracePeriod(); + await liquidityMining.finishMigrationGracePeriod(); + await expectRevert( + liquidityMining.updatePool(token1.address), + "Forbidden: contract deprecated" + ); + await expectRevert( + liquidityMining.updateAllPools(), + "Forbidden: contract deprecated" + ); + }); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMining = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMiningV2 = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + } + + async function checkBonusPeriodHasNotEnded() { + expect(await liquidityMining.bonusEndBlock()).bignumber.gt( + (await web3.eth.getBlockNumber()).toString() + ); + } + + function checkPoolInfo( + poolInfo, + token, + allocationPoint, + lastRewardBlock, + accumulatedRewardPerShare + ) { + expect(poolInfo.poolToken).equal(token); + expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); + expect(poolInfo.lastRewardBlock).bignumber.equal(lastRewardBlock); + if (accumulatedRewardPerShare.toNumber() !== -1) { + expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(accumulatedRewardPerShare); + } + } + + async function checkUserPoolTokens( + user, + poolToken, + _userAmount, + _liquidityMiningBalance, + _userBalance, + wrapper + ) { + //user balance in pool + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.amount).bignumber.equal(_userAmount); + //LM balance of pool tokens + let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); + expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); + //user's balance of pool tokens + let userBalance = await poolToken.balanceOf(user); + if (wrapper !== undefined) { + userBalance = await poolToken.balanceOf(wrapper); + } + expect(userBalance).bignumber.equal(_userBalance); + } + + //user's balance of reward token + async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { + let passedBlocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + depositBlockNumber, + latestBlockNumber + ); + let userReward = passedBlocks.mul(rewardTokensPerBlock); + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + return userReward; + } }); describe("Contract upgrade", async () => { - let liquidityMiningProxy; - - beforeEach(async () => { - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); - [root, account1, account2] = await web3.eth.getAccounts(); - lockedSOVAdmins = [account1, account2]; - - lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - }); - - it("should be able to withdraw and SOV tokens to be transferred", async () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - - SOVToken.transfer(liquidityMining.address, amount); - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await upgradeLiquidityMining(); - - let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - let userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); - expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); - - expectEvent(tx, "EmergencyWithdraw", { - user: account1, - poolToken: token1.address, - amount, - }); - - tx = await liquidityMining.transferSOV(root, amount, { from: root }); - expectEvent(tx, "SOVTransferred", { - receiver: root, - amount: amount, - }); - }); - - async function deployLiquidityMining() { - let liquidityMiningLogic = await LiquidityMiningLogic.new(); - liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); - liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - - wrapper = await Wrapper.new(liquidityMining.address); - } - - async function upgradeLiquidityMining() { - let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); - } + let liquidityMiningProxy; + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + [root, account1, account2] = await web3.eth.getAccounts(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + }); + + it("should be able to withdraw and SOV tokens to be transferred", async () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + + SOVToken.transfer(liquidityMining.address, amount); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); + + await upgradeLiquidityMining(); + + let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + + expectEvent(tx, "EmergencyWithdraw", { + user: account1, + poolToken: token1.address, + amount, + }); + + tx = await liquidityMining.transferSOV(root, amount, { from: root }); + expectEvent(tx, "SOVTransferred", { + receiver: root, + amount: amount, + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + } }); diff --git a/tests/farm/LiquidityMiningV2.js b/tests/farm/LiquidityMiningV2.js index 9b0f11da4..8aa5ce9b2 100644 --- a/tests/farm/LiquidityMiningV2.js +++ b/tests/farm/LiquidityMiningV2.js @@ -20,1987 +20,2851 @@ const TestPoolToken = artifacts.require("TestPoolToken"); const Migrator = artifacts.require("LMV1toLMV2Migrator"); describe("LiquidityMiningV2", () => { - const name = "Test SOV Token"; - const symbol = "TST"; - - const PRECISION = 1e12; - - const rewardTokensPerBlock = new BN(3); - const startDelayBlocks = new BN(1); - const numberOfBonusBlocks = new BN(50); - - // The % which determines how much will be unlocked immediately. - /// @dev 10000 is 100% - const unlockedImmediatelyPercent = new BN(1000); //10% - - let accounts; - let root, account1, account2, account3, account4; - let SOVToken, token1, token2, token3, liquidityMiningConfigToken; - let liquidityMiningV1, liquidityMining, migrator, wrapper; - let rewardTransferLogic, lockedSOVAdmins, lockedSOV; - let erc20RewardTransferLogic; - - before(async () => { - accounts = await web3.eth.getAccounts(); - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); - lockedSOVAdmins = [account1, account2]; - - lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - - await deployLiquidityMining(); - await liquidityMiningV1.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - - await upgradeLiquidityMining(); - - await deployLiquidityMiningV2(); - - await liquidityMiningV1.initialize(liquidityMining.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); - - await liquidityMining.initialize(wrapper.address, migrator.address); - - erc20RewardTransferLogic = await ERC20TransferLogic.new(); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); - - await liquidityMining.setWrapper(wrapper.address); - await liquidityMining.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - //mint SOVs to lvm1 for migrations - await SOVToken.mint(liquidityMiningV1.address, new BN(10)); - await liquidityMiningV1.addAdmin(migrator.address); - await liquidityMiningV1.startMigrationGracePeriod(); - await liquidityMining.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - //burn SOVs for testing - const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); - await SOVToken.burn(liquidityMining.address, balanceSOV); - }); - - describe("initialize", () => { - it("should fail if migrator address is invalid", async () => { - await deployLiquidityMiningV2(); - await expectRevert(liquidityMining.initialize(wrapper.address, ZERO_ADDRESS), "invalid contract address"); - }); - - it("fails if already initialized", async () => { - await deployLiquidityMiningV2(); - await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); - await expectRevert(liquidityMining.initialize(wrapper.address, liquidityMiningV1.address), "Already initialized"); - }); - - it("sets the expected values", async () => { - await deployLiquidityMiningV2(); - await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); - - let _wrapper = await liquidityMining.wrapper(); - - expect(_wrapper).equal(wrapper.address); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await liquidityMining.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await liquidityMining.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(liquidityMining.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("adds admin", async () => { - await liquidityMining.addAdmin(account1); - let tx = await liquidityMining.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await liquidityMining.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(liquidityMining.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("setWrapper", () => { - it("sets the expected values", async () => { - let newWrapper = account2; - await liquidityMining.setWrapper(newWrapper); - - let _wrapper = await liquidityMining.wrapper(); - expect(_wrapper).equal(newWrapper); - }); - - it("fails if not an owner or an admin", async () => { - await expectRevert(liquidityMining.setWrapper(account2, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.setWrapper(account2, { from: account1 }); - }); - }); - - describe("stopMining", () => { - it("should set end block", async () => { - let tx = await liquidityMining.stopMining(SOVToken.address); - - let blockNumber = new BN(tx.receipt.blockNumber); - - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - let _endBlock = rewardToken.endBlock; - expect(_endBlock).bignumber.equal(blockNumber); - }); - - it("fails if not an owner or an admin", async () => { - await expectRevert(liquidityMining.stopMining(SOVToken.address, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.stopMining(SOVToken.address, { from: account1 }); - }); - - it("fails if already stopped", async () => { - await liquidityMining.stopMining(SOVToken.address); - await expectRevert(liquidityMining.stopMining(SOVToken.address), "Already stopped"); - }); - - it("fails if reward token is not initialized", async () => { - await liquidityMining.stopMining(SOVToken.address); - await expectRevert(liquidityMining.stopMining(token3.address), "Not initialized"); - }); - }); - - describe("transferRewardTokens", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOVToken.transfer(liquidityMining.address, amount); - - let balanceBefore = await SOVToken.balanceOf(account1); - await liquidityMining.transferRewardTokens(SOVToken.address, account1, amount); - let balanceAfter = await SOVToken.balanceOf(account1); - - expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner or admin should be able to transfer", async () => { - await expectRevert(liquidityMining.transferRewardTokens(SOVToken.address, account1, 1000, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.transferRewardTokens(SOVToken.address, account1, 1000, { from: account1 }); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(liquidityMining.transferRewardTokens(SOVToken.address, ZERO_ADDRESS, 1000), "Receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(liquidityMining.transferRewardTokens(SOVToken.address, account1, 0), "Amount invalid"); - }); - - it("fails if the zero address is passed as token", async () => { - await expectRevert(liquidityMining.transferRewardTokens(ZERO_ADDRESS, account1, 0), "Reward address invalid"); - }); - }); - - describe("addRewardToken", () => { - /* + const name = "Test SOV Token"; + const symbol = "TST"; + + const PRECISION = 1e12; + + const rewardTokensPerBlock = new BN(3); + const startDelayBlocks = new BN(1); + const numberOfBonusBlocks = new BN(50); + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let root, account1, account2, account3, account4; + let SOVToken, token1, token2, token3, liquidityMiningConfigToken; + let liquidityMiningV1, liquidityMining, migrator, wrapper; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + let erc20RewardTransferLogic; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMiningV1.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMiningV1.initialize(liquidityMining.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMiningV1.address, + liquidityMining.address + ); + + await liquidityMining.initialize(wrapper.address, migrator.address); + + erc20RewardTransferLogic = await ERC20TransferLogic.new(); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMining.setWrapper(wrapper.address); + await liquidityMining.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + //mint SOVs to lvm1 for migrations + await SOVToken.mint(liquidityMiningV1.address, new BN(10)); + await liquidityMiningV1.addAdmin(migrator.address); + await liquidityMiningV1.startMigrationGracePeriod(); + await liquidityMining.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + //burn SOVs for testing + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await SOVToken.burn(liquidityMining.address, balanceSOV); + }); + + describe("initialize", () => { + it("should fail if migrator address is invalid", async () => { + await deployLiquidityMiningV2(); + await expectRevert( + liquidityMining.initialize(wrapper.address, ZERO_ADDRESS), + "invalid contract address" + ); + }); + + it("fails if already initialized", async () => { + await deployLiquidityMiningV2(); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); + await expectRevert( + liquidityMining.initialize(wrapper.address, liquidityMiningV1.address), + "Already initialized" + ); + }); + + it("sets the expected values", async () => { + await deployLiquidityMiningV2(); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); + + let _wrapper = await liquidityMining.wrapper(); + + expect(_wrapper).equal(wrapper.address); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await liquidityMining.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + liquidityMining.addAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await liquidityMining.addAdmin(account1); + let tx = await liquidityMining.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + liquidityMining.removeAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("setWrapper", () => { + it("sets the expected values", async () => { + let newWrapper = account2; + await liquidityMining.setWrapper(newWrapper); + + let _wrapper = await liquidityMining.wrapper(); + expect(_wrapper).equal(newWrapper); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert( + liquidityMining.setWrapper(account2, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setWrapper(account2, { from: account1 }); + }); + }); + + describe("stopMining", () => { + it("should set end block", async () => { + let tx = await liquidityMining.stopMining(SOVToken.address); + + let blockNumber = new BN(tx.receipt.blockNumber); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + let _endBlock = rewardToken.endBlock; + expect(_endBlock).bignumber.equal(blockNumber); + }); + + it("fails if not an owner or an admin", async () => { + await expectRevert( + liquidityMining.stopMining(SOVToken.address, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.stopMining(SOVToken.address, { from: account1 }); + }); + + it("fails if already stopped", async () => { + await liquidityMining.stopMining(SOVToken.address); + await expectRevert(liquidityMining.stopMining(SOVToken.address), "Already stopped"); + }); + + it("fails if reward token is not initialized", async () => { + await liquidityMining.stopMining(SOVToken.address); + await expectRevert(liquidityMining.stopMining(token3.address), "Not initialized"); + }); + }); + + describe("transferRewardTokens", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOVToken.transfer(liquidityMining.address, amount); + + let balanceBefore = await SOVToken.balanceOf(account1); + await liquidityMining.transferRewardTokens(SOVToken.address, account1, amount); + let balanceAfter = await SOVToken.balanceOf(account1); + + expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert( + liquidityMining.transferRewardTokens(SOVToken.address, account1, 1000, { + from: account1, + }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.transferRewardTokens(SOVToken.address, account1, 1000, { + from: account1, + }); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + liquidityMining.transferRewardTokens(SOVToken.address, ZERO_ADDRESS, 1000), + "Receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert( + liquidityMining.transferRewardTokens(SOVToken.address, account1, 0), + "Amount invalid" + ); + }); + + it("fails if the zero address is passed as token", async () => { + await expectRevert( + liquidityMining.transferRewardTokens(ZERO_ADDRESS, account1, 0), + "Reward address invalid" + ); + }); + }); + + describe("addRewardToken", () => { + /* address _rewardToken, uint256 _rewardTokensPerBlock, uint256 _startDelayBlocks, address _rewardTransferLogic */ - let otherRewardTokensPerBlock = 2; - let otherStartDelayBlocks = 3; - - it("should be able to add a reward token", async () => { - const transferLogic = await ERC20TransferLogic.new(); - await transferLogic.initialize(token1.address); - await liquidityMining.addRewardToken(token1.address, otherRewardTokensPerBlock, otherStartDelayBlocks, transferLogic.address); - const rewardToken = await liquidityMining.getRewardToken(token1.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(new BN(0)); - expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); - expect(rewardToken.rewardTokensPerBlock).bignumber.equal(new BN(2)); - }); - - it("fails if start delay blocks is not greater than 0", async () => { - const addInvalidAddress = liquidityMining.addRewardToken( - token1.address, - otherRewardTokensPerBlock, - 0, - erc20RewardTransferLogic.address - ); - await expectRevert(addInvalidAddress, "Invalid start block"); - }); - - it("fails if reward token address is not valid", async () => { - const addInvalidAddress = liquidityMining.addRewardToken( - ZERO_ADDRESS, - otherRewardTokensPerBlock, - otherStartDelayBlocks, - erc20RewardTransferLogic.address - ); - await expectRevert(addInvalidAddress, "Invalid token address"); - }); - - it("fails if token is already added as reward token", async () => { - const transferLogic = await ERC20TransferLogic.new(); - await transferLogic.initialize(token1.address); - await liquidityMining.addRewardToken(token1.address, otherRewardTokensPerBlock, otherStartDelayBlocks, transferLogic.address); - const addReward = liquidityMining.addRewardToken( - token1.address, - otherRewardTokensPerBlock, - otherStartDelayBlocks, - transferLogic.address - ); - await expectRevert(addReward, "Already added"); - }); - - it("fails if reward transfer logic doesn't correspond to given reward token", async () => { - const addInvalidAddress = liquidityMining.addRewardToken( - token1.address, - otherRewardTokensPerBlock, - otherStartDelayBlocks, - erc20RewardTransferLogic.address - ); - await expectRevert(addInvalidAddress, "Reward token and transfer logic mismatch"); - }); - }); - - describe("add", () => { - it("should be able to add pool token", async () => { - let allocationPoint = new BN(1); - let tx = await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); - - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint); - - let poolInfo = await liquidityMining.poolInfoList(0); - expect(poolInfo).equal(token1.address); - - let poolReward = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - expect(poolReward.allocationPoint).bignumber.equal(allocationPoint); - let blockNumber = new BN(tx.receipt.blockNumber); - expect(poolReward.lastRewardBlock).bignumber.equal(blockNumber); - expect(poolReward.accumulatedRewardPerShare).bignumber.equal(new BN(0)); - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); - - expectEvent(tx, "PoolTokenAdded", { - user: root, - poolToken: token1.address, - rewardTokens: [SOVToken.address], - }); - expect(tx.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint); - }); - - it("should be able to add 2 pool tokens and update pools", async () => { - let allocationPoint1 = new BN(1); - let tx1 = await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint1], false); - - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint1); - - expectEvent(tx1, "PoolTokenAdded", { - user: root, - poolToken: token1.address, - rewardTokens: [SOVToken.address], - }); - expect(tx1.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint1); - - let allocationPoint2 = new BN(2); - let tx2 = await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint2], true); - - rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint1.add(allocationPoint2)); - - expectEvent(tx2, "PoolTokenAdded", { - user: root, - poolToken: token2.address, - rewardTokens: [SOVToken.address], - }); - expect(tx2.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint2); - - let poolRewardInfo1 = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - let poolRewardInfo2 = await liquidityMining.getPoolReward(token2.address, SOVToken.address); - expect(poolRewardInfo1.lastRewardBlock).bignumber.equal(poolRewardInfo2.lastRewardBlock); - }); - - it("fails if the reward token list is empty", async () => { - await expectRevert(liquidityMining.add(token1.address, [], [new BN(0)], false), "Invalid reward tokens length"); - }); - - it("fails if the reward tokens length is different to allocation points", async () => { - await expectRevert( - liquidityMining.add(token1.address, [token2.address, token3.address], [new BN(1)], false), - "Invalid allocation points length" - ); - }); - - it("fails if the reward token is repeated", async () => { - await expectRevert( - liquidityMining.add(token1.address, [SOVToken.address, SOVToken.address], [new BN(1), new BN(1)], false), - "Already associated" - ); - }); - - it("fails if the reward token is not valid", async () => { - await expectRevert(liquidityMining.add(token1.address, [token2.address], [new BN(1)], false), "Not initialized"); - }); - - it("fails if the 0 allocation point is passed", async () => { - await expectRevert(liquidityMining.add(token1.address, [SOVToken.address], [new BN(0)], false), "Invalid allocation point"); - }); - - it("fails if the 0 address is passed as token address", async () => { - await expectRevert(liquidityMining.add(ZERO_ADDRESS, [SOVToken.address], [new BN(0)], false), "Invalid token address"); - }); - - it("fails if token already added", async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [new BN(1)], false); - await expectRevert(liquidityMining.add(token1.address, [SOVToken.address], [new BN(0)], false), "Token already added"); - }); - - it("only owner or admin should be able to add pool token", async () => { - await expectRevert( - liquidityMining.add(token2.address, [SOVToken.address], [new BN(0)], false, { from: account1 }), - "unauthorized" - ); - - await liquidityMining.addAdmin(account1); - await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false, { from: account1 }); - }); - }); - - describe("update", () => { - it("should be able to update pool token", async () => { - let oldAllocationPoint = new BN(1); - await liquidityMining.add(token1.address, [SOVToken.address], [oldAllocationPoint], false); - - let newAllocationPoint = new BN(2); - let tx = await liquidityMining.update(token1.address, [SOVToken.address], [newAllocationPoint], false); - - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(newAllocationPoint); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, newAllocationPoint, blockNumber, new BN(0)); - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); - - expectEvent(tx, "PoolTokenUpdated", { - user: root, - poolToken: token1.address, - rewardToken: SOVToken.address, - newAllocationPoint: newAllocationPoint, - oldAllocationPoint: oldAllocationPoint, - }); - }); - - it("should be able to update pool token and update pools", async () => { - let oldAllocationPoint = new BN(1); - await liquidityMining.add(token1.address, [SOVToken.address], [oldAllocationPoint], false); - - await liquidityMining.add(token2.address, [SOVToken.address], [oldAllocationPoint], false); - - let newAllocationPoint = new BN(2); - let tx = await liquidityMining.update(token1.address, [SOVToken.address], [newAllocationPoint], true); - - const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(oldAllocationPoint.add(newAllocationPoint)); - - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - expect(poolRewardToken.lastRewardBlock).bignumber.equal(new BN(tx.receipt.blockNumber)); - }); - - it("fails if token wasn't added", async () => { - await expectRevert(liquidityMining.update(token1.address, [SOVToken.address], [new BN(1)], false), "Pool token not found"); - }); - - it("only owner or admin should be able to update pool token", async () => { - await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); - await expectRevert( - liquidityMining.update(token2.address, [SOVToken.address], [new BN(1)], false, { from: account1 }), - "unauthorized" - ); - - await liquidityMining.addAdmin(account1); - await liquidityMining.update(token2.address, [SOVToken.address], [new BN(1)], false, { from: account1 }); - }); - }); - - describe("updateTokens", () => { - it("should be able to update 2 pool tokens", async () => { - let poolTokens = [token1.address, token2.address, token3.address]; - let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; - - for (let i = 0; i < poolTokens.length; i++) { - await liquidityMining.add(poolTokens[i], [SOVToken.address], [oldAllocationPoints[i]], false); - } - - let newAllocationPoints = [[new BN(101)], [new BN(102)], [new BN(3)]]; - let rewardTokens = new Array(3).fill([SOVToken.address]); - let tx = await liquidityMining.updateTokens(poolTokens, rewardTokens, newAllocationPoints, true); - - let totalAllocationPoint = new BN(0); - for (let i = 0; i < newAllocationPoints.length; i++) { - totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i][0]); - } - const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(totalAllocationPoint); - - let blockNumber = new BN(tx.receipt.blockNumber); - for (let i = 0; i < poolTokens.length - 1; i++) { - let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); - const poolRewardToken = await liquidityMining.getPoolReward(poolTokens[i], SOVToken.address); - checkPoolRewardInfo(poolInfo, poolTokens[i], poolRewardToken, newAllocationPoints[i][0], blockNumber, new BN(0)); - - expectEvent(tx, "PoolTokenUpdated", { - user: root, - poolToken: poolTokens[i], - rewardToken: SOVToken.address, - newAllocationPoint: newAllocationPoints[i][0], - oldAllocationPoint: oldAllocationPoints[i], - }); - } - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); - - const poolRewardToken = await liquidityMining.getPoolReward(poolTokens[poolTokens.length - 1], SOVToken.address); - expect(poolRewardToken.lastRewardBlock).bignumber.equal(blockNumber); - }); - - it("fails if token wasn't added", async () => { - await expectRevert( - liquidityMining.updateTokens([token1.address], [[SOVToken.address]], [[new BN(1)]], false), - "Pool token not found" - ); - }); - - it("fails if arrays have different length", async () => { - await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); - await expectRevert( - liquidityMining.updateTokens([token1.address, token2.address], [[SOVToken.address]], [[new BN(1)]], false), - "Arrays mismatch" - ); - - await expectRevert( - liquidityMining.updateTokens([token1.address, token2.address], [[SOVToken.address]], [[new BN(1)], [new BN(1)]], false), - "Arrays mismatch" - ); - - await expectRevert( - liquidityMining.updateTokens( - [token1.address, token2.address], - [[SOVToken.address, token3.address], [SOVToken.address]], - [[new BN(1)], [new BN(1)]], - false - ), - "Arrays mismatch" - ); - - await expectRevert( - liquidityMining.updateTokens( - [token1.address, token2.address], - [[SOVToken.address, token3.address]], - [[new BN(1)], [new BN(1)]], - false - ), - "Arrays mismatch" - ); - }); - - it("only owner or admin should be able to update pool token", async () => { - await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); - await expectRevert( - liquidityMining.updateTokens([token2.address], [[SOVToken.address]], [[new BN(1)]], false, { from: account1 }), - "unauthorized" - ); - - await liquidityMining.addAdmin(account1); - await liquidityMining.updateTokens([token2.address], [[SOVToken.address]], [[new BN(1)]], false, { from: account1 }); - }); - }); - - describe("deposit", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - it("should only allow to deposit if migration is finished", async () => { - await deployLiquidityMiningV2(); - await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); - await liquidityMining.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - await expectRevert( - liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }), - "Migration is not over yet" - ); - }); - it("should be able to deposit", async () => { - let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, blockNumber, new BN(0)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - - expectEvent(tx, "Deposit", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - }); - - it("should be able to deposit using wrapper", async () => { - let tx = await liquidityMining.deposit(token1.address, amount, account2, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, blockNumber, new BN(0)); - - await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); - - expectEvent(tx, "Deposit", { - user: account2, - poolToken: token1.address, - amount: amount, - }); - }); - - it("should be able to deposit 0 amount", async () => { - let tx = await liquidityMining.deposit(token1.address, new BN(0), ZERO_ADDRESS, { from: account1 }); - - expectEvent(tx, "Deposit", { - user: account1, - poolToken: token1.address, - amount: new BN(0), - }); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - }); - - describe("claimRewards", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await expectRevert(liquidityMining.claimRewards(token1.address, ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); - }); - - it("should be able to claim reward (will be claimed with SOV tokens)", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await liquidityMining.claimRewards(token1.address, ZERO_ADDRESS, { from: account1 }); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - - const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "RewardClaimed", { - user: account1, - amount: userReward, - rewardToken: SOVToken.address, - }); - }); - - it("should be able to claim reward using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.claimReward(token1.address, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - }); - - it("should not take into account blocks before start delay", async () => { - await token2.mint(account1, amount); - await token2.approve(liquidityMining.address, amount, { from: account1 }); - await token3.transfer(liquidityMining.address, new BN(1000)); - - await erc20RewardTransferLogic.initialize(token3.address); - await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(5), erc20RewardTransferLogic.address); - await liquidityMining.add(token2.address, [token3.address], [allocationPoint], true); - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); - - await mineBlock(); - - await liquidityMining.updateAllPools(); - const reward1 = await liquidityMining.getUserAccumulatedReward(token2.address, token3.address, account1); - expect(reward1).bignumber.equal(new BN(0)); - - await mineBlock(); - - let tx = await liquidityMining.claimRewards(token2.address, ZERO_ADDRESS, { from: account1 }); - // at this point there was 1 mined block after start block - expectEvent(tx, "RewardClaimed", { - user: account1, - amount: new BN(3), - rewardToken: token3.address, - }); - }); - - it("should not take into account blocks after stop mining", async () => { - await token2.mint(account1, amount); - await token2.approve(liquidityMining.address, amount, { from: account1 }); - await token3.transfer(liquidityMining.address, new BN(1000)); - - await erc20RewardTransferLogic.initialize(token3.address); - await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(2), erc20RewardTransferLogic.address); - - await liquidityMining.add(token2.address, [token3.address], [allocationPoint], true); - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); - - await mineBlocks(1); - await liquidityMining.stopMining(token3.address); - await mineBlocks(2); - - let tx = await liquidityMining.claimRewards(token2.address, ZERO_ADDRESS, { from: account1 }); - // last 2 blocks should not accumulate rewards - expectEvent(tx, "RewardClaimed", { - user: account1, - amount: new BN(6), - rewardToken: token3.address, - }); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.claimRewards(account1, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - }); - - describe("claimRewardFromAllPools", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - await token2.mint(account1, amount); - await token2.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await expectRevert(liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); - }); - - it("should be able to claim reward (will be claimed with SOV tokens)", async () => { - let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); - let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); - - let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }); - - const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward1 = await checkUserReward(account1, token1, depositBlockNumber1, latestBlockNumber); - //we have 2 pools with the same allocation points - userReward1 = userReward1.div(new BN(2)); - - await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); - let userReward2 = await checkUserReward(account1, token2, depositBlockNumber2, latestBlockNumber); - //we have 2 pools with the same allocation points - userReward2 = userReward2.div(new BN(2)); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "RewardClaimed", { - user: account1, - rewardToken: SOVToken.address, - amount: userReward1, - }); - - expect(userReward1, tx.logs[0].args.amount); - expect(token1.address, tx.logs[0].args.poolToken); - expect(userReward2, tx.logs[1].args.amount); - expect(token2.address, tx.logs[1].args.poolToken); - }); - - it("should be able to claim reward using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - }); - }); - - describe("withdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to withdraw (without claiming reward)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, blockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - - // User's balance on lockedSOV vault - let userRewardBalance = await lockedSOV.getLockedBalance(account1); - expect(userRewardBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - }); - - it("should be able to withdraw (with claiming reward)", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - //withdrawAndStakeTokensFrom was not invoked - let expectedUnlockedBalance = userReward.mul(unlockedImmediatelyPercent).div(new BN(10000)); - let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); - expect(lockedBalance).bignumber.equal(expectedLockedBalance); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - - expectEvent(tx, "RewardClaimed", { - user: account1, - rewardToken: SOVToken.address, - amount: userReward, - }); - }); - - it("should be able to withdraw using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - const poolRewardToken = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - checkPoolRewardInfo(poolInfo, token1.address, poolRewardToken, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount, wrapper.address); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - }); - - it("fails if withdraw for a user without using wrapper or pool", async () => { - await expectRevert( - liquidityMining.withdraw(token1.address, amount, account1, { from: account3 }), - "only wrapper or pools may withdraw for a user" - ); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - - it("fails if token pool token not found", async () => { - await expectRevert( - liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { from: account1 }), - "Not enough balance" - ); - }); - }); - - describe("emergencyWithdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to withdraw", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); - - const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - - let userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.rewards[0].rewardDebt).bignumber.equal(new BN(0)); - expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(new BN(0)); - - expectEvent(tx, "EmergencyWithdraw", { - user: account1, - poolToken: token1.address, - rewardToken: SOVToken.address, - amount: amount, - accumulatedReward: rewardTokensPerBlock, - }); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.emergencyWithdraw(account1, { from: account1 }), "Pool token not found"); - }); - }); - - describe("getUserAccumulatedReward", () => { - const amount1 = new BN(1000); - const amount2 = new BN(2000); - const allocationPoint1 = new BN(1); - const allocationPoint2 = new BN(2); - const totalAllocationPoint = allocationPoint1.add(allocationPoint2); - - beforeEach(async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint1], false); - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint2], false); - - await token1.mint(account1, amount1); - await token2.mint(account2, amount2); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await token2.approve(liquidityMining.address, amount2, { from: account2 }); - }); - - it("check calculation for no deposits", async () => { - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); - expect(reward1).bignumber.equal("0"); - expect(reward2).bignumber.equal("0"); - }); - - it("check calculation for single user, token 1", async () => { - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - let reward = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); - - // 1 block has passed - // users are given 3 tokens per share per block. user1 owns 100% of the shares - // token 1 counts as 1/3 of the pool - // reward = 1 * 3 * 1/3 = 1 - const expectedReward = rewardTokensPerBlock.mul(allocationPoint1).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("1"); // sanity check - expect(reward).bignumber.equal(expectedReward); - - await mineBlock(); - reward = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); - expect(reward).bignumber.equal("2"); - }); - - it("check calculation for single user, token 2", async () => { - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - await mineBlock(); - let reward = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); - - // 1 block has passed - // users are given 3 tokens per share per block. user2 owns 100% of the shares - // token 2 counts as 2/3 of the pool - // reward = 1 * 3 * 2/3 = 2 - const expectedReward = rewardTokensPerBlock.mul(allocationPoint2).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("2"); // sanity check - expect(reward).bignumber.equal(expectedReward); - - await mineBlock(); - reward = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); - expect(reward).bignumber.equal("4"); - }); - - it("check calculation for two users and tokens", async () => { - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - // because automining is on, the following will advance a block - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - // sanity checks - expect(await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1)).bignumber.equal("1"); - expect(await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2)).bignumber.equal("0"); - await mineBlock(); - - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, SOVToken.address, account2); - - // for the first block, user 1 will receive the reward of 1 - // for the second block: - // - user 1 still owns 100% of the shares for token1, so same reward (total 1 + 1 = 2) - // - user 2 owns 100% of the shares for token2, so same reward as in the other cases - expect(reward1).bignumber.equal("2"); - expect(reward2).bignumber.equal("2"); - }); - - it("check calculation for two users, same token (shares taken into account)", async () => { - const token = token1; - const amount = amount1; - await token.mint(account2, amount); - await token.approve(liquidityMining.address, amount, { from: account2 }); - - await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); - // because automining is on, the following will advance a block - await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); - // sanity checks - expect(await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account1)).bignumber.equal("1"); - expect(await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account2)).bignumber.equal("0"); - await mineBlock(); - await mineBlock(); - - const reward1 = await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token.address, SOVToken.address, account2); - - // for the first block, user 1 will receive the reward of 1 (reward given per block for 100% of shares) - // after 2 blocks: - // - user 1 owns 1/2 of the shares => expected reward = 1 (total 1 + 1 = 2) - // - user 2 owns 1/2 of the shares => expected reward = 1 - expect(reward1).bignumber.equal("2"); - expect(reward2).bignumber.equal("1"); - }); - }); - - describe("getEstimatedReward", () => { - const amount1 = new BN(1000); - const amount2 = new BN(2000); - const amount3 = new BN(4000); - const allocationPoint1 = new BN(1); - const allocationPoint2 = new BN(2); - - const totalAllocationPoint = allocationPoint1.add(allocationPoint2); - let secondsPerBlock; - - beforeEach(async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint1], false); - - await token1.mint(account1, amount1); - await token1.mint(account2, amount2); - await token1.mint(account3, amount3); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await token1.approve(liquidityMining.address, amount2, { from: account2 }); - - secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); - }); - - it("check calculation for 1 user 2 delayed block reward, period less than 2 blocks", async () => { - await erc20RewardTransferLogic.initialize(token3.address); - await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(2), erc20RewardTransferLogic.address); - await liquidityMining.add(token2.address, [token3.address], [allocationPoint1], false); - let duration = secondsPerBlock.mul(new BN(2)).sub(new BN(1)); - - let estimatedReward = await liquidityMining.getEstimatedReward(token2.address, token3.address, amount3, duration); - let expectedReward = "0"; - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user 2 delayed block reward, period less than 3 blocks", async () => { - await erc20RewardTransferLogic.initialize(token3.address); - await liquidityMining.addRewardToken(token3.address, new BN(3), new BN(2), erc20RewardTransferLogic.address); - await liquidityMining.add(token2.address, [token3.address], [allocationPoint1], false); - let duration = secondsPerBlock.mul(new BN(3)).sub(new BN(1)); - - let estimatedReward = await liquidityMining.getEstimatedReward(token2.address, token3.address, amount3, duration); - let expectedReward = "3"; - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user, period less than 1 block", async () => { - let duration = secondsPerBlock.sub(new BN(1)); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); - let expectedReward = "0"; - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user, period is 1 block", async () => { - let duration = secondsPerBlock; - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); - let expectedReward = rewardTokensPerBlock; - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user, period is 40 blocks", async () => { - let blocks = new BN(40); - let duration = secondsPerBlock.mul(blocks); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 2 users, period is 100 blocks", async () => { - let blocks = new BN(100); - let duration = secondsPerBlock.mul(blocks); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks); - let totalAmount = amount1.add(amount3); - expectedReward = expectedReward.mul(amount3).div(totalAmount); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint2], false); - - let blocks = new BN(1000); - let duration = secondsPerBlock.mul(blocks); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await token1.approve(liquidityMining.address, amount2, { from: account2 }); - await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { from: account2 }); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, SOVToken.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks); - expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); - let totalAmount = amount1.add(amount2).add(amount3); - expectedReward = expectedReward.mul(amount3).div(totalAmount); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - }); - - describe("deposit/withdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - for (let token of [token1, token2]) { - for (let account of [account1, account2]) { - await token.mint(account, amount); - await token.approve(liquidityMining.address, amount, { from: account }); - } - } - - // make sure the pool has tokens to distribute - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - }); - - it("add, add, deposit, deposit", async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); //weight 1/2 - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); // 1 block passed - - // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); //weight 2/3 - await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit - - const currentBlockNumber = await web3.eth.getBlockNumber(); - - // 3 tokens per share per block, times precision (1e12), times weight (1/2), divided by total shares - const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock.mul(new BN(1e12)).div(new BN(2)).div(amount); - - const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); - const poolReward = await liquidityMining.getPoolReward(token1.address, SOVToken.address); - expect(poolInfo1.poolToken).equal(token1.address); - expect(poolReward.allocationPoint).equal("1"); - expect(poolReward.lastRewardBlock).equal(currentBlockNumber.toString()); - // token1 deposit has been there for 2 blocks because of automining - expect(poolReward.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString()); - - const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); - const poolReward2 = await liquidityMining.getPoolReward(token2.address, SOVToken.address); - expect(poolInfo2.poolToken).equal(token2.address); - expect(poolReward2.allocationPoint).equal("1"); - expect(poolReward2.lastRewardBlock).equal(currentBlockNumber.toString()); - // token2 deposit has been there for only 1 block - expect(poolReward2.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.toString()); - }); - - // // tricky case 1 - it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); //weight 1/2 - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await liquidityMining.update(token1.address, [SOVToken.address], [new BN("2")], false); // 1 block passed, new weight 2/3 - const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); // 2 blocks passed - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 3 (because of bonus period), 1 block with weight 1/2 = 1, 1 block with weight 2/3 = 2 - const expectedRewardAmount = new BN("3"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - - expectEvent(tx, "RewardClaimed", { - user: account1, - rewardToken: SOVToken.address, - amount: rewardAmount, - }); - }); - - // // tricky case 2 - it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 - - // deposit 1: 0 blocks, deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks - await mineBlock(); - - // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account2 }); - - // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) - const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) - const withdrawTx2 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account2 }); - - const lockedAmount1 = await lockedSOV.getLockedBalance(account1); - const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); - const reward1 = lockedAmount1.add(unlockedAmount1); - - const lockedAmount2 = await lockedSOV.getLockedBalance(account2); - const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); - const reward2 = lockedAmount2.add(unlockedAmount2); - - // reward per block 3, 2 block with 100% shares = 6, 1 block with 50% shares = 1 - const expectedReward1 = new BN("7"); - - // reward per block 3, 1 block with 50% shares = 1, 1 block with 100% shares = 3 - const expectedReward2 = new BN("4"); - - expect(reward1).bignumber.equal(expectedReward1); - expect(reward2).bignumber.equal(expectedReward2); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - await checkUserPoolTokens( - account2, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - - expectEvent(withdrawTx1, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx1, "RewardClaimed", { - user: account1, - rewardToken: SOVToken.address, - amount: reward1, - }); - expectEvent(withdrawTx2, "Withdraw", { - user: account2, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx2, "RewardClaimed", { - user: account2, - rewardToken: SOVToken.address, - amount: reward2, - }); - }); - - // tricky case 3a - it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 - - // deposit: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit: 1 blocks, note: pool1 is NOT updated - await liquidityMining.add(token2.address, [SOVToken.address], [new BN(2)], false); //weight 1/3 - - // deposit: 2 blocks - await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 3, - // because add was called without updating the pool, the new weight is used for all blocks - // so 2 blocks with weight 1/3 = 2 - const expectedRewardAmount = new BN("2"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - }); - - // tricky case 3b - it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 - - // deposit: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit: 1 blocks, note: pool1 IS updated - await liquidityMining.add(token2.address, [SOVToken.address], [new BN(2)], true); //weight 1/3 - - // deposit: 2 blocks - await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 3, - // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block - // so 1 block with weight 1/1 = 3 and 1 block with weight 1/3 = 1 - const expectedRewardAmount = new BN("4"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - }); - - // tricky case 4 - it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); //weight 1/1 - - // deposit 1: 0 blocks, deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], true); //weight 1/2 - - // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account2 }); - - // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) - const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) - const withdrawTx2 = await liquidityMining.withdraw(token2.address, amount, ZERO_ADDRESS, { from: account2 }); - - const lockedAmount1 = await lockedSOV.getLockedBalance(account1); - const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); - const reward1 = lockedAmount1.add(unlockedAmount1); - - const lockedAmount2 = await lockedSOV.getLockedBalance(account2); - const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); - const reward2 = lockedAmount2.add(unlockedAmount2); - - // reward per block 3 - // deposit 1 has 1 block with weight 1/1 (3) and 2 blocks with weight 1/2 - const expectedReward1 = new BN("6"); - - // deposit 2 has 2 blocks with weight 1/2 - const expectedReward2 = new BN("3"); - - expect(reward1).bignumber.equal(expectedReward1); - expect(reward2).bignumber.equal(expectedReward2); - - for (let account of [account1, account2]) { - for (let token of [token1, token2]) { - await checkUserPoolTokens( - account, - token, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - } - } - - expectEvent(withdrawTx1, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx1, "RewardClaimed", { - user: account1, - rewardToken: SOVToken.address, - amount: reward1, - }); - expectEvent(withdrawTx2, "Withdraw", { - user: account2, - poolToken: token2.address, - amount: amount, - }); - expectEvent(withdrawTx2, "RewardClaimed", { - user: account2, - rewardToken: SOVToken.address, - amount: reward2, - }); - }); - }); - - describe("LM configuration", () => { - //Maximum reward per week: 100K SOV (or 100M SOV) - //Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) - - const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); - // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); - - //SOV/BTC pool 40K per week - //ETH/BTC pool 37.5K per week (from second week) - //Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) - - const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); - // const MAX_ALLOCATION_POINT = new BN(100000); - const ALLOCATION_POINT_SOV_BTC = new BN(40000); - const ALLOCATION_POINT_ETH_BTC = new BN(37500); - - const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); - - const amount = new BN(1000); - - beforeEach(async () => { - await deployLiquidityMining(); - await liquidityMiningV1.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - - await upgradeLiquidityMining(); - - await deployLiquidityMiningV2(); - - await liquidityMiningV1.initialize(liquidityMining.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); - - await liquidityMining.initialize(wrapper.address, migrator.address); - - for (let token of [token1, token2]) { - for (let account of [account1, account2]) { - await token.mint(account, amount); - await token.approve(liquidityMining.address, amount, { from: account }); - } - } - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); - - await liquidityMining.addRewardToken(SOVToken.address, REWARD_TOKENS_PER_BLOCK, startDelayBlocks, rewardTransferLogic.address); - - //mint SOVs to lvm1 for migrations - await SOVToken.mint(liquidityMiningV1.address, new BN(10)); - await liquidityMiningV1.addAdmin(migrator.address); - await liquidityMiningV1.startMigrationGracePeriod(); - await liquidityMining.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - //burn SOVs for testing - const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); - await SOVToken.burn(liquidityMining.address, balanceSOV); - }); - - it("dummy pool + 1 pool", async () => { - let dummyPool = liquidityMiningConfigToken.address; - - let SOVBTCpool = token1.address; - - await liquidityMining.add(SOVBTCpool, [SOVToken.address], [ALLOCATION_POINT_SOV_BTC], false); //weight 40000 / 100000 - await liquidityMining.add(dummyPool, [SOVToken.address], [MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC)], false); //weight (100000 - 40000) / 100000 - - await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - //reward won't be claimed because liquidityMining doesn't have enough SOV balance - //user reward will be updated - //10 blocks passed since last deposit - await mineBlocks(9); - await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); - //10 blocks passed - let passedBlocks = 10; - let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) - .mul(ALLOCATION_POINT_SOV_BTC) - .div(MAX_ALLOCATION_POINT); - expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(expectedUserReward); - }); - - it("dummy pool + 2 pools", async () => { - let dummyPool = liquidityMiningConfigToken.address; - - let SOVBTCpool = token1.address; - let ETHBTCpoll = token2.address; - - await liquidityMining.add(SOVBTCpool, [SOVToken.address], [ALLOCATION_POINT_SOV_BTC], false); //weight 40000 / 100000 - const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); - await liquidityMining.add(dummyPool, [SOVToken.address], [DUMMY_ALLOCATION_POINT], false); //weight (100000 - 40000) / 100000 - - await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - await mineBlocks(9); - await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit - - //update config - //this method will also update pool reward using previous allocation point, - //so this block should be add to calculation with old values - await liquidityMining.update(SOVBTCpool, [SOVToken.address], [ALLOCATION_POINT_SOV_BTC_2], false); //weight 30000 / 100000 - - await liquidityMining.add(ETHBTCpoll, [SOVToken.address], [ALLOCATION_POINT_ETH_BTC], false); //weight 37500 / 100000 - const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC_2).sub(ALLOCATION_POINT_ETH_BTC); - await liquidityMining.update(dummyPool, [SOVToken.address], [DUMMY_ALLOCATION_POINT_2], false); //weight (100000 - 30000 - 37500) / 100000 - await liquidityMining.updateAllPools(); - - //reward won't be claimed because liquidityMining doesn't have enough SOV balance - //user reward will be updated - //10 blocks + 5 blocks passed - await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); - //10 blocks + 5 blocks passed - let passedBlocks = 10 + 1; //block should be add to calculation with old values - let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) - .mul(ALLOCATION_POINT_SOV_BTC) - .div(MAX_ALLOCATION_POINT); - passedBlocks = 5 - 1; //block should be removed from calculation with new values - expectedUserReward = expectedUserReward.add( - REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)).mul(ALLOCATION_POINT_SOV_BTC_2).div(MAX_ALLOCATION_POINT) - ); - expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(expectedUserReward); - }); - }); - - describe("multiple rewards tokens per pool", () => { - let rewardToken1; - let rewardToken2; - let transferLogic1; - let transferLogic2; - const otherRewardTokensPerBlock = new BN(6); - const allocationPoint = new BN(1); - - beforeEach(async () => { - // add other reward token - rewardToken1 = await TestToken.new("Reward token 1", "RWT-1", 18, TOTAL_SUPPLY); - rewardToken2 = await TestToken.new("Reward token 2", "RWT-2", 18, TOTAL_SUPPLY); - - transferLogic1 = await ERC20TransferLogic.new(); - transferLogic2 = await ERC20TransferLogic.new(); - await transferLogic1.initialize(rewardToken1.address); - await transferLogic2.initialize(rewardToken2.address); - - await liquidityMining.addRewardToken(rewardToken1.address, rewardTokensPerBlock, startDelayBlocks, transferLogic1.address); - await liquidityMining.addRewardToken(rewardToken2.address, otherRewardTokensPerBlock, startDelayBlocks, transferLogic2.address); - - await rewardToken1.transfer(liquidityMining.address, new BN(1000)); - await rewardToken2.transfer(liquidityMining.address, new BN(1000)); - }); - - it("add 2 reward tokens to one pool", async () => { - await liquidityMining.add( - token1.address, - [rewardToken1.address, rewardToken2.address], - [allocationPoint, allocationPoint], - false - ); - - const poolRewards = await liquidityMining.getPoolRewards(token1.address); - expect(poolRewards).to.be.an("array"); - expect(poolRewards).to.have.length(2); - expect(poolRewards[0].allocationPoint).bignumber.equal(allocationPoint); - expect(poolRewards[1].allocationPoint).bignumber.equal(allocationPoint); - }); - - it("update 2 reward tokens with new allocation points", async () => { - await liquidityMining.add( - token1.address, - [rewardToken1.address, rewardToken2.address], - [allocationPoint, allocationPoint], - false - ); - - let poolRewards = await liquidityMining.getPoolRewards(token1.address); - expect(poolRewards).to.be.an("array"); - expect(poolRewards).to.have.length(2); - expect(poolRewards[0].allocationPoint).bignumber.equal(allocationPoint); - expect(poolRewards[1].allocationPoint).bignumber.equal(allocationPoint); - - const newAllocationPoints = [new BN(3), new BN(4)]; - - await liquidityMining.update(token1.address, [rewardToken1.address, rewardToken2.address], newAllocationPoints, false); - - poolRewards = await liquidityMining.getPoolRewards(token1.address); - expect(poolRewards).to.be.an("array"); - expect(poolRewards).to.have.length(2); - expect(poolRewards[0].allocationPoint).bignumber.equal(newAllocationPoints[0]); - expect(poolRewards[1].allocationPoint).bignumber.equal(newAllocationPoints[1]); - }); - - it("check rewards for two reward tokens and one pool", async () => { - const amount = new BN(1000); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - await liquidityMining.add( - token1.address, - [rewardToken1.address, rewardToken2.address], - [allocationPoint, allocationPoint], - false - ); - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await mineBlock(); - - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, rewardToken1.address, account1); - expect(reward1).bignumber.equal(rewardTokensPerBlock); - - const claimRewardTx1 = await liquidityMining.claimReward(token1.address, rewardToken1.address, ZERO_ADDRESS, { - from: account1, - }); - expectEvent(claimRewardTx1, "RewardClaimed", { - user: account1, - amount: new BN(6), - rewardToken: rewardToken1.address, - }); - - const reward2 = await liquidityMining.getUserAccumulatedReward(token1.address, rewardToken2.address, account1); - expect(reward2).bignumber.equal(new BN(12)); - - const claimRewardTx2 = await liquidityMining.claimReward(token1.address, rewardToken2.address, ZERO_ADDRESS, { - from: account1, - }); - expectEvent(claimRewardTx2, "RewardClaimed", { - user: account1, - amount: new BN(18), - rewardToken: rewardToken2.address, - }); - }); - }); - - describe("onTokensDeposited", () => { - it("a pool should be able to deposit for a user", async () => { - const poolToken = await TestPoolToken.new("Test Pool Token", "TPT", 18, TOTAL_SUPPLY, liquidityMining.address); - - await liquidityMining.add(poolToken.address, [SOVToken.address], [new BN(1)], false); - const tx = await poolToken.depositFor(account1, new BN(1000)); - - const userInfo = await liquidityMining.getUserInfo(poolToken.address, account1); - expect(userInfo.amount).bignumber.equal(new BN(1000)); - }); - it("should revert if the sender is not a valid pool token", async () => { - await expectRevert(liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), "Pool token not found"); - }); - }); - - describe("external getters", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - await liquidityMining.add(token1.address, [SOVToken.address], [allocationPoint], false); - }); - - it("PRECISION", async () => { - expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); - }); - - it("rewardTokensPerBlock", async () => { - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); - }); - - it("startBlock", async () => { - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.startBlock).bignumber.gt("0"); - }); - - it("endBlock", async () => { - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.endBlock).bignumber.equal("0"); - }); - - it("wrapper", async () => { - expect(await liquidityMining.wrapper()).equal(wrapper.address); - }); - - it("totalAllocationPoint", async () => { - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint); - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); - rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint.mul(new BN(2))); - }); - - it("totalUsersBalance", async () => { - let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalUsersBalance).bignumber.equal("0"); - - await liquidityMining.updateAllPools(); - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalUsersBalance).bignumber.equal("0"); - - await liquidityMining.updateAllPools(); - rewardToken = await liquidityMining.getRewardToken(SOVToken.address); - expect(rewardToken.totalUsersBalance).bignumber.equal("3"); - }); - - // could still test these, but I don't see much point: - // PoolInfo[] public poolInfoList; - // mapping(address => uint256) poolIdList; - // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; - - it("getMissedBalance", async () => { - let missedBalance = await liquidityMining.getMissedBalance(SOVToken.address); - expect(missedBalance).bignumber.equal("0"); - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await liquidityMining.updatePool(token1.address); - - missedBalance = await liquidityMining.getMissedBalance(SOVToken.address); - expect(missedBalance).bignumber.equal("3"); - }); - - it("getUserAccumulatedReward", async () => { - // real tests are elsewhere in this file - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token1.address, SOVToken.address, account2); - expect(reward1).bignumber.equal("3"); - expect(reward2).bignumber.equal("0"); - }); - - it("getPoolId", async () => { - const poolId = await liquidityMining.getPoolId(token1.address); - expect(poolId).bignumber.equal("0"); - await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); - const poolId2 = await liquidityMining.getPoolId(token2.address); - expect(poolId2).bignumber.equal("1"); - }); - - it("getPoolLength", async () => { - let length = await liquidityMining.getPoolLength(); - expect(length).bignumber.equal("1"); - - await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], false); - length = await liquidityMining.getPoolLength(); - expect(length).bignumber.equal("2"); - }); - - it("getPoolInfoList", async () => { - const infoList = await liquidityMining.getPoolInfoList(); - expect(infoList).to.be.an("array"); - expect(infoList.length).equal(1); - const info = infoList[0]; - expect(info.poolToken).equal(token1.address); - expect(info.rewardTokens[0]).equal(SOVToken.address); - - const poolRewardInfo = await liquidityMining.getPoolReward(token1.address, info.rewardTokens[0]); - expect(poolRewardInfo.allocationPoint).equal(allocationPoint.toString()); - expect(poolRewardInfo.accumulatedRewardPerShare).equal("0"); - expect(poolRewardInfo.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); - }); - - it("getPoolInfo", async () => { - const info = await liquidityMining.getPoolInfo(token1.address); - expect(info.poolToken).equal(token1.address); - - const poolRewardInfo = await liquidityMining.getPoolReward(token1.address, info.rewardTokens[0]); - expect(poolRewardInfo.allocationPoint).equal(allocationPoint.toString()); - expect(poolRewardInfo.accumulatedRewardPerShare).equal("0"); - expect(poolRewardInfo.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); - - await expectRevert(liquidityMining.getPoolInfo(token2.address), "Pool token not found"); - }); - - it("getUserBalanceList", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const balanceList = await liquidityMining.getUserBalanceList(account1); - - expect(balanceList).to.be.an("array"); - expect(balanceList.length).equal(1); - const balanceData = balanceList[0]; - expect(balanceData).to.be.an("array"); - expect(balanceData[0].amount).bignumber.equal(amount); - expect(balanceData[0].accumulatedReward).bignumber.equal("3"); - }); - - it("getUserInfo", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.amount).bignumber.equal("500"); - expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? - expect(userInfo.rewards[0].rewardDebt).bignumber.equal("0"); // not yet updated either - - // deposit updates it. - await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); - userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.amount).bignumber.equal("501"); - expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("3"); - expect(userInfo.rewards[0].rewardDebt).bignumber.equal("3"); - }); - - it("getUserInfoList", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let userInfoList = await liquidityMining.getUserInfoList(account1); - expect(userInfoList).to.be.an("array"); - expect(userInfoList.length).equal(1); - const userInfo = userInfoList[0]; - expect(userInfo.amount).bignumber.equal("500"); - expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("0"); - expect(userInfo.rewards[0].rewardDebt).bignumber.equal("0"); - }); - - it("getUserAccumulatedRewardList", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); - expect(rewardList).to.be.an("array"); - expect(rewardList.length).equal(1); - expect(rewardList[0]).to.be.an("array"); - expect(rewardList[0].length).equal(1); - expect(rewardList[0][0].accumulatedReward).bignumber.equal("0"); - }); - - it("getUserPoolTokenBalance", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance(token1.address, account1); - expect(poolTokenBalance).bignumber.equal(new BN(500)); - }); - }); - - async function deployLiquidityMining() { - let liquidityMiningLogicV1 = await LiquidityMiningLogic.new(); - liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); - liquidityMiningV1 = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - - wrapper = await Wrapper.new(liquidityMiningV1.address); - } - - async function upgradeLiquidityMining() { - let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); - liquidityMiningV1 = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); - } - - async function deployLiquidityMiningV2() { - let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); - let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); - await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); - liquidityMining = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); - - wrapper = await Wrapper.new(liquidityMining.address); - } - - async function mineBlocks(blocks) { - for (let i = 0; i < blocks; i++) { - await mineBlock(); - } - } - - function checkPoolRewardInfo(poolInfo, token, rewardToken, allocationPoint, lastRewardBlock, accumulatedRewardPerShare) { - expect(poolInfo.poolToken).equal(token); - expect(rewardToken.allocationPoint).bignumber.equal(allocationPoint); - expect(rewardToken.lastRewardBlock).bignumber.equal(lastRewardBlock); - if (accumulatedRewardPerShare.toNumber() !== -1) { - expect(rewardToken.accumulatedRewardPerShare).bignumber.equal(accumulatedRewardPerShare); - } - } - - async function checkUserPoolTokens(user, poolToken, _userAmount, _liquidityMiningBalance, _userBalance, wrapper) { - //user balance in pool - let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); - expect(userInfo.amount).bignumber.equal(_userAmount); - //LM balance of pool tokens - let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); - expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); - //user's balance of pool tokens - let userBalance = await poolToken.balanceOf(user); - if (wrapper !== undefined) { - userBalance = await poolToken.balanceOf(wrapper); - } - expect(userBalance).bignumber.equal(_userBalance); - } - - //user's balance of reward token - async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { - let passedBlocks = latestBlockNumber.sub(depositBlockNumber); - let userReward = passedBlocks.mul(rewardTokensPerBlock); - let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); - expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(new BN(0)); - return userReward; - } + let otherRewardTokensPerBlock = 2; + let otherStartDelayBlocks = 3; + + it("should be able to add a reward token", async () => { + const transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + await liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + transferLogic.address + ); + const rewardToken = await liquidityMining.getRewardToken(token1.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(new BN(0)); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + expect(rewardToken.rewardTokensPerBlock).bignumber.equal(new BN(2)); + }); + + it("fails if start delay blocks is not greater than 0", async () => { + const addInvalidAddress = liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + 0, + erc20RewardTransferLogic.address + ); + await expectRevert(addInvalidAddress, "Invalid start block"); + }); + + it("fails if reward token address is not valid", async () => { + const addInvalidAddress = liquidityMining.addRewardToken( + ZERO_ADDRESS, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + erc20RewardTransferLogic.address + ); + await expectRevert(addInvalidAddress, "Invalid token address"); + }); + + it("fails if token is already added as reward token", async () => { + const transferLogic = await ERC20TransferLogic.new(); + await transferLogic.initialize(token1.address); + await liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + transferLogic.address + ); + const addReward = liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + transferLogic.address + ); + await expectRevert(addReward, "Already added"); + }); + + it("fails if reward transfer logic doesn't correspond to given reward token", async () => { + const addInvalidAddress = liquidityMining.addRewardToken( + token1.address, + otherRewardTokensPerBlock, + otherStartDelayBlocks, + erc20RewardTransferLogic.address + ); + await expectRevert(addInvalidAddress, "Reward token and transfer logic mismatch"); + }); + }); + + describe("add", () => { + it("should be able to add pool token", async () => { + let allocationPoint = new BN(1); + let tx = await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint); + + let poolInfo = await liquidityMining.poolInfoList(0); + expect(poolInfo).equal(token1.address); + + let poolReward = await liquidityMining.getPoolReward(token1.address, SOVToken.address); + expect(poolReward.allocationPoint).bignumber.equal(allocationPoint); + let blockNumber = new BN(tx.receipt.blockNumber); + expect(poolReward.lastRewardBlock).bignumber.equal(blockNumber); + expect(poolReward.accumulatedRewardPerShare).bignumber.equal(new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + rewardTokens: [SOVToken.address], + }); + expect(tx.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint); + }); + + it("should be able to add 2 pool tokens and update pools", async () => { + let allocationPoint1 = new BN(1); + let tx1 = await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint1], + false + ); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint1); + + expectEvent(tx1, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + rewardTokens: [SOVToken.address], + }); + expect(tx1.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint1); + + let allocationPoint2 = new BN(2); + let tx2 = await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint2], + true + ); + + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal( + allocationPoint1.add(allocationPoint2) + ); + + expectEvent(tx2, "PoolTokenAdded", { + user: root, + poolToken: token2.address, + rewardTokens: [SOVToken.address], + }); + expect(tx2.logs[1].args.allocationPoints[0]).bignumber.equal(allocationPoint2); + + let poolRewardInfo1 = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + let poolRewardInfo2 = await liquidityMining.getPoolReward( + token2.address, + SOVToken.address + ); + expect(poolRewardInfo1.lastRewardBlock).bignumber.equal( + poolRewardInfo2.lastRewardBlock + ); + }); + + it("fails if the reward token list is empty", async () => { + await expectRevert( + liquidityMining.add(token1.address, [], [new BN(0)], false), + "Invalid reward tokens length" + ); + }); + + it("fails if the reward tokens length is different to allocation points", async () => { + await expectRevert( + liquidityMining.add( + token1.address, + [token2.address, token3.address], + [new BN(1)], + false + ), + "Invalid allocation points length" + ); + }); + + it("fails if the reward token is repeated", async () => { + await expectRevert( + liquidityMining.add( + token1.address, + [SOVToken.address, SOVToken.address], + [new BN(1), new BN(1)], + false + ), + "Already associated" + ); + }); + + it("fails if the reward token is not valid", async () => { + await expectRevert( + liquidityMining.add(token1.address, [token2.address], [new BN(1)], false), + "Not initialized" + ); + }); + + it("fails if the 0 allocation point is passed", async () => { + await expectRevert( + liquidityMining.add(token1.address, [SOVToken.address], [new BN(0)], false), + "Invalid allocation point" + ); + }); + + it("fails if the 0 address is passed as token address", async () => { + await expectRevert( + liquidityMining.add(ZERO_ADDRESS, [SOVToken.address], [new BN(0)], false), + "Invalid token address" + ); + }); + + it("fails if token already added", async () => { + await liquidityMining.add(token1.address, [SOVToken.address], [new BN(1)], false); + await expectRevert( + liquidityMining.add(token1.address, [SOVToken.address], [new BN(0)], false), + "Token already added" + ); + }); + + it("only owner or admin should be able to add pool token", async () => { + await expectRevert( + liquidityMining.add(token2.address, [SOVToken.address], [new BN(0)], false, { + from: account1, + }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false, { + from: account1, + }); + }); + }); + + describe("update", () => { + it("should be able to update pool token", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add( + token1.address, + [SOVToken.address], + [oldAllocationPoint], + false + ); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update( + token1.address, + [SOVToken.address], + [newAllocationPoint], + false + ); + + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(newAllocationPoint); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + newAllocationPoint, + blockNumber, + new BN(0) + ); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: token1.address, + rewardToken: SOVToken.address, + newAllocationPoint: newAllocationPoint, + oldAllocationPoint: oldAllocationPoint, + }); + }); + + it("should be able to update pool token and update pools", async () => { + let oldAllocationPoint = new BN(1); + await liquidityMining.add( + token1.address, + [SOVToken.address], + [oldAllocationPoint], + false + ); + + await liquidityMining.add( + token2.address, + [SOVToken.address], + [oldAllocationPoint], + false + ); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update( + token1.address, + [SOVToken.address], + [newAllocationPoint], + true + ); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal( + oldAllocationPoint.add(newAllocationPoint) + ); + + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + expect(poolRewardToken.lastRewardBlock).bignumber.equal( + new BN(tx.receipt.blockNumber) + ); + }); + + it("fails if token wasn't added", async () => { + await expectRevert( + liquidityMining.update(token1.address, [SOVToken.address], [new BN(1)], false), + "Pool token not found" + ); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); + await expectRevert( + liquidityMining.update(token2.address, [SOVToken.address], [new BN(1)], false, { + from: account1, + }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.update(token2.address, [SOVToken.address], [new BN(1)], false, { + from: account1, + }); + }); + }); + + describe("updateTokens", () => { + it("should be able to update 2 pool tokens", async () => { + let poolTokens = [token1.address, token2.address, token3.address]; + let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; + + for (let i = 0; i < poolTokens.length; i++) { + await liquidityMining.add( + poolTokens[i], + [SOVToken.address], + [oldAllocationPoints[i]], + false + ); + } + + let newAllocationPoints = [[new BN(101)], [new BN(102)], [new BN(3)]]; + let rewardTokens = new Array(3).fill([SOVToken.address]); + let tx = await liquidityMining.updateTokens( + poolTokens, + rewardTokens, + newAllocationPoints, + true + ); + + let totalAllocationPoint = new BN(0); + for (let i = 0; i < newAllocationPoints.length; i++) { + totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i][0]); + } + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(totalAllocationPoint); + + let blockNumber = new BN(tx.receipt.blockNumber); + for (let i = 0; i < poolTokens.length - 1; i++) { + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); + const poolRewardToken = await liquidityMining.getPoolReward( + poolTokens[i], + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + poolTokens[i], + poolRewardToken, + newAllocationPoints[i][0], + blockNumber, + new BN(0) + ); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: poolTokens[i], + rewardToken: SOVToken.address, + newAllocationPoint: newAllocationPoints[i][0], + oldAllocationPoint: oldAllocationPoints[i], + }); + } + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); + + const poolRewardToken = await liquidityMining.getPoolReward( + poolTokens[poolTokens.length - 1], + SOVToken.address + ); + expect(poolRewardToken.lastRewardBlock).bignumber.equal(blockNumber); + }); + + it("fails if token wasn't added", async () => { + await expectRevert( + liquidityMining.updateTokens( + [token1.address], + [[SOVToken.address]], + [[new BN(1)]], + false + ), + "Pool token not found" + ); + }); + + it("fails if arrays have different length", async () => { + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); + await expectRevert( + liquidityMining.updateTokens( + [token1.address, token2.address], + [[SOVToken.address]], + [[new BN(1)]], + false + ), + "Arrays mismatch" + ); + + await expectRevert( + liquidityMining.updateTokens( + [token1.address, token2.address], + [[SOVToken.address]], + [[new BN(1)], [new BN(1)]], + false + ), + "Arrays mismatch" + ); + + await expectRevert( + liquidityMining.updateTokens( + [token1.address, token2.address], + [[SOVToken.address, token3.address], [SOVToken.address]], + [[new BN(1)], [new BN(1)]], + false + ), + "Arrays mismatch" + ); + + await expectRevert( + liquidityMining.updateTokens( + [token1.address, token2.address], + [[SOVToken.address, token3.address]], + [[new BN(1)], [new BN(1)]], + false + ), + "Arrays mismatch" + ); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(1)], false); + await expectRevert( + liquidityMining.updateTokens( + [token2.address], + [[SOVToken.address]], + [[new BN(1)]], + false, + { from: account1 } + ), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.updateTokens( + [token2.address], + [[SOVToken.address]], + [[new BN(1)]], + false, + { from: account1 } + ); + }); + }); + + describe("deposit", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + it("should only allow to deposit if migration is finished", async () => { + await deployLiquidityMiningV2(); + await liquidityMining.initialize(wrapper.address, liquidityMiningV1.address); + await liquidityMining.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + await expectRevert( + liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }), + "Migration is not over yet" + ); + }); + it("should be able to deposit", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + blockNumber, + new BN(0) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to deposit using wrapper", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, account2, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + blockNumber, + new BN(0) + ); + + await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to deposit 0 amount", async () => { + let tx = await liquidityMining.deposit(token1.address, new BN(0), ZERO_ADDRESS, { + from: account1, + }); + + expectEvent(tx, "Deposit", { + user: account1, + poolToken: token1.address, + amount: new BN(0), + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("claimRewards", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await expectRevert( + liquidityMining.claimRewards(token1.address, ZERO_ADDRESS, { from: account1 }), + "Claiming reward failed" + ); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.claimRewards(token1.address, ZERO_ADDRESS, { + from: account1, + }); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + amount: userReward, + rewardToken: SOVToken.address, + }); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimReward(token1.address, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + + it("should not take into account blocks before start delay", async () => { + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + await token3.transfer(liquidityMining.address, new BN(1000)); + + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken( + token3.address, + new BN(3), + new BN(5), + erc20RewardTransferLogic.address + ); + await liquidityMining.add(token2.address, [token3.address], [allocationPoint], true); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await mineBlock(); + + await liquidityMining.updateAllPools(); + const reward1 = await liquidityMining.getUserAccumulatedReward( + token2.address, + token3.address, + account1 + ); + expect(reward1).bignumber.equal(new BN(0)); + + await mineBlock(); + + let tx = await liquidityMining.claimRewards(token2.address, ZERO_ADDRESS, { + from: account1, + }); + // at this point there was 1 mined block after start block + expectEvent(tx, "RewardClaimed", { + user: account1, + amount: new BN(3), + rewardToken: token3.address, + }); + }); + + it("should not take into account blocks after stop mining", async () => { + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + await token3.transfer(liquidityMining.address, new BN(1000)); + + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken( + token3.address, + new BN(3), + new BN(2), + erc20RewardTransferLogic.address + ); + + await liquidityMining.add(token2.address, [token3.address], [allocationPoint], true); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await mineBlocks(1); + await liquidityMining.stopMining(token3.address); + await mineBlocks(2); + + let tx = await liquidityMining.claimRewards(token2.address, ZERO_ADDRESS, { + from: account1, + }); + // last 2 blocks should not accumulate rewards + expectEvent(tx, "RewardClaimed", { + user: account1, + amount: new BN(6), + rewardToken: token3.address, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.claimRewards(account1, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("claimRewardFromAllPools", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint], + false + ); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await expectRevert( + liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), + "Claiming reward failed" + ); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); + let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); + + let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { + from: account1, + }); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward1 = await checkUserReward( + account1, + token1, + depositBlockNumber1, + latestBlockNumber + ); + //we have 2 pools with the same allocation points + userReward1 = userReward1.div(new BN(2)); + + await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); + let userReward2 = await checkUserReward( + account1, + token2, + depositBlockNumber2, + latestBlockNumber + ); + //we have 2 pools with the same allocation points + userReward2 = userReward2.div(new BN(2)); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: userReward1, + }); + + expect(userReward1, tx.logs[0].args.amount); + expect(token1.address, tx.logs[0].args.poolToken); + expect(userReward2, tx.logs[1].args.amount); + expect(token2.address, tx.logs[1].args.poolToken); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + //withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + }); + + describe("withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw (without claiming reward)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + blockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + // User's balance on lockedSOV vault + let userRewardBalance = await lockedSOV.getLockedBalance(account1); + expect(userRewardBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to withdraw (with claiming reward)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + //withdrawAndStakeTokensFrom was not invoked + let expectedUnlockedBalance = userReward + .mul(unlockedImmediatelyPercent) + .div(new BN(10000)); + let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); + expect(lockedBalance).bignumber.equal(expectedLockedBalance); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: userReward, + }); + }); + + it("should be able to withdraw using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + const poolRewardToken = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + checkPoolRewardInfo( + poolInfo, + token1.address, + poolRewardToken, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), + new BN(0), + amount, + wrapper.address + ); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + }); + + it("fails if withdraw for a user without using wrapper or pool", async () => { + await expectRevert( + liquidityMining.withdraw(token1.address, amount, account1, { from: account3 }), + "only wrapper or pools may withdraw for a user" + ); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { + from: account1, + }), + "Not enough balance" + ); + }); + }); + + describe("emergencyWithdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); + + const rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal(new BN(0)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.rewards[0].rewardDebt).bignumber.equal(new BN(0)); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(new BN(0)); + + expectEvent(tx, "EmergencyWithdraw", { + user: account1, + poolToken: token1.address, + rewardToken: SOVToken.address, + amount: amount, + accumulatedReward: rewardTokensPerBlock, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.emergencyWithdraw(account1, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("getUserAccumulatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + + beforeEach(async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint1], + false + ); + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint2], + false + ); + + await token1.mint(account1, amount1); + await token2.mint(account2, amount2); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token2.approve(liquidityMining.address, amount2, { from: account2 }); + }); + + it("check calculation for no deposits", async () => { + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + SOVToken.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token2.address, + SOVToken.address, + account2 + ); + expect(reward1).bignumber.equal("0"); + expect(reward2).bignumber.equal("0"); + }); + + it("check calculation for single user, token 1", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward( + token1.address, + SOVToken.address, + account1 + ); + + // 1 block has passed + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 1 * 3 * 1/3 = 1 + const expectedReward = rewardTokensPerBlock + .mul(allocationPoint1) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("1"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward( + token1.address, + SOVToken.address, + account1 + ); + expect(reward).bignumber.equal("2"); + }); + + it("check calculation for single user, token 2", async () => { + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward( + token2.address, + SOVToken.address, + account2 + ); + + // 1 block has passed + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 1 * 3 * 2/3 = 2 + const expectedReward = rewardTokensPerBlock + .mul(allocationPoint2) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("2"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward( + token2.address, + SOVToken.address, + account2 + ); + expect(reward).bignumber.equal("4"); + }); + + it("check calculation for two users and tokens", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + // sanity checks + expect( + await liquidityMining.getUserAccumulatedReward( + token1.address, + SOVToken.address, + account1 + ) + ).bignumber.equal("1"); + expect( + await liquidityMining.getUserAccumulatedReward( + token2.address, + SOVToken.address, + account2 + ) + ).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + SOVToken.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token2.address, + SOVToken.address, + account2 + ); + + // for the first block, user 1 will receive the reward of 1 + // for the second block: + // - user 1 still owns 100% of the shares for token1, so same reward (total 1 + 1 = 2) + // - user 2 owns 100% of the shares for token2, so same reward as in the other cases + expect(reward1).bignumber.equal("2"); + expect(reward2).bignumber.equal("2"); + }); + + it("check calculation for two users, same token (shares taken into account)", async () => { + const token = token1; + const amount = amount1; + await token.mint(account2, amount); + await token.approve(liquidityMining.address, amount, { from: account2 }); + + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); + // sanity checks + expect( + await liquidityMining.getUserAccumulatedReward( + token.address, + SOVToken.address, + account1 + ) + ).bignumber.equal("1"); + expect( + await liquidityMining.getUserAccumulatedReward( + token.address, + SOVToken.address, + account2 + ) + ).bignumber.equal("0"); + await mineBlock(); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward( + token.address, + SOVToken.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token.address, + SOVToken.address, + account2 + ); + + // for the first block, user 1 will receive the reward of 1 (reward given per block for 100% of shares) + // after 2 blocks: + // - user 1 owns 1/2 of the shares => expected reward = 1 (total 1 + 1 = 2) + // - user 2 owns 1/2 of the shares => expected reward = 1 + expect(reward1).bignumber.equal("2"); + expect(reward2).bignumber.equal("1"); + }); + }); + + describe("getEstimatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const amount3 = new BN(4000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let secondsPerBlock; + + beforeEach(async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint1], + false + ); + + await token1.mint(account1, amount1); + await token1.mint(account2, amount2); + await token1.mint(account3, amount3); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + + secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); + }); + + it("check calculation for 1 user 2 delayed block reward, period less than 2 blocks", async () => { + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken( + token3.address, + new BN(3), + new BN(2), + erc20RewardTransferLogic.address + ); + await liquidityMining.add(token2.address, [token3.address], [allocationPoint1], false); + let duration = secondsPerBlock.mul(new BN(2)).sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token2.address, + token3.address, + amount3, + duration + ); + let expectedReward = "0"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user 2 delayed block reward, period less than 3 blocks", async () => { + await erc20RewardTransferLogic.initialize(token3.address); + await liquidityMining.addRewardToken( + token3.address, + new BN(3), + new BN(2), + erc20RewardTransferLogic.address + ); + await liquidityMining.add(token2.address, [token3.address], [allocationPoint1], false); + let duration = secondsPerBlock.mul(new BN(3)).sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token2.address, + token3.address, + amount3, + duration + ); + let expectedReward = "3"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period less than 1 block", async () => { + let duration = secondsPerBlock.sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + SOVToken.address, + amount3, + duration + ); + let expectedReward = "0"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 1 block", async () => { + let duration = secondsPerBlock; + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + SOVToken.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 40 blocks", async () => { + let blocks = new BN(40); + let duration = secondsPerBlock.mul(blocks); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + SOVToken.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 2 users, period is 100 blocks", async () => { + let blocks = new BN(100); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + SOVToken.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks); + let totalAmount = amount1.add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint2], + false + ); + + let blocks = new BN(1000); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + SOVToken.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks); + expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); + let totalAmount = amount1.add(amount2).add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + }); + + describe("deposit/withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + // make sure the pool has tokens to distribute + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + }); + + it("add, add, deposit, deposit", async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/1 + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); // 1 block passed + + // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); //weight 2/3 + await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit + + const currentBlockNumber = await web3.eth.getBlockNumber(); + + // 3 tokens per share per block, times precision (1e12), times weight (1/2), divided by total shares + const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock + .mul(new BN(1e12)) + .div(new BN(2)) + .div(amount); + + const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + const poolReward = await liquidityMining.getPoolReward( + token1.address, + SOVToken.address + ); + expect(poolInfo1.poolToken).equal(token1.address); + expect(poolReward.allocationPoint).equal("1"); + expect(poolReward.lastRewardBlock).equal(currentBlockNumber.toString()); + // token1 deposit has been there for 2 blocks because of automining + expect(poolReward.accumulatedRewardPerShare).equal( + expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString() + ); + + const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + const poolReward2 = await liquidityMining.getPoolReward( + token2.address, + SOVToken.address + ); + expect(poolInfo2.poolToken).equal(token2.address); + expect(poolReward2.allocationPoint).equal("1"); + expect(poolReward2.lastRewardBlock).equal(currentBlockNumber.toString()); + // token2 deposit has been there for only 1 block + expect(poolReward2.accumulatedRewardPerShare).equal( + expectedAccumulatedRewardPerBlock.toString() + ); + }); + + // // tricky case 1 + it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/1 + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await liquidityMining.update(token1.address, [SOVToken.address], [new BN("2")], false); // 1 block passed, new weight 2/3 + const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); // 2 blocks passed + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 3 (because of bonus period), 1 block with weight 1/2 = 1, 1 block with weight 2/3 = 2 + const expectedRewardAmount = new BN("3"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: rewardAmount, + }); + }); + + // // tricky case 2 + it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks + await mineBlock(); + + // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account2, + }); + + // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) + const withdrawTx1 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account1 } + ); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) + const withdrawTx2 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account2 } + ); + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 3, 2 block with 100% shares = 6, 1 block with 50% shares = 1 + const expectedReward1 = new BN("7"); + + // reward per block 3, 1 block with 50% shares = 1, 1 block with 100% shares = 3 + const expectedReward2 = new BN("4"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + await checkUserPoolTokens( + account2, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + rewardToken: SOVToken.address, + amount: reward2, + }); + }); + + // tricky case 3a + it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit: 1 blocks, note: pool1 is NOT updated + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(2)], false); //weight 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 3, + // because add was called without updating the pool, the new weight is used for all blocks + // so 2 blocks with weight 1/3 = 2 + const expectedRewardAmount = new BN("2"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 3b + it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit: 1 blocks, note: pool1 IS updated + await liquidityMining.add(token2.address, [SOVToken.address], [new BN(2)], true); //weight 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 3, + // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block + // so 1 block with weight 1/1 = 3 and 1 block with weight 1/3 = 1 + const expectedRewardAmount = new BN("4"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 4 + it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); //weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated + await liquidityMining.add(token2.address, [SOVToken.address], [allocationPoint], true); //weight 1/2 + + // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account2, + }); + + // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) + const withdrawTx1 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account1 } + ); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) + const withdrawTx2 = await liquidityMining.withdraw( + token2.address, + amount, + ZERO_ADDRESS, + { from: account2 } + ); + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 3 + // deposit 1 has 1 block with weight 1/1 (3) and 2 blocks with weight 1/2 + const expectedReward1 = new BN("6"); + + // deposit 2 has 2 blocks with weight 1/2 + const expectedReward2 = new BN("3"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + for (let account of [account1, account2]) { + for (let token of [token1, token2]) { + await checkUserPoolTokens( + account, + token, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + } + } + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + rewardToken: SOVToken.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token2.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + rewardToken: SOVToken.address, + amount: reward2, + }); + }); + }); + + describe("LM configuration", () => { + //Maximum reward per week: 100K SOV (or 100M SOV) + //Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) + + const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); + // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); + + //SOV/BTC pool 40K per week + //ETH/BTC pool 37.5K per week (from second week) + //Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) + + const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); + // const MAX_ALLOCATION_POINT = new BN(100000); + const ALLOCATION_POINT_SOV_BTC = new BN(40000); + const ALLOCATION_POINT_ETH_BTC = new BN(37500); + + const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); + + const amount = new BN(1000); + + beforeEach(async () => { + await deployLiquidityMining(); + await liquidityMiningV1.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMiningV1.initialize(liquidityMining.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMiningV1.address, + liquidityMining.address + ); + + await liquidityMining.initialize(wrapper.address, migrator.address); + + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMining.addRewardToken( + SOVToken.address, + REWARD_TOKENS_PER_BLOCK, + startDelayBlocks, + rewardTransferLogic.address + ); + + //mint SOVs to lvm1 for migrations + await SOVToken.mint(liquidityMiningV1.address, new BN(10)); + await liquidityMiningV1.addAdmin(migrator.address); + await liquidityMiningV1.startMigrationGracePeriod(); + await liquidityMining.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + //burn SOVs for testing + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await SOVToken.burn(liquidityMining.address, balanceSOV); + }); + + it("dummy pool + 1 pool", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + + await liquidityMining.add( + SOVBTCpool, + [SOVToken.address], + [ALLOCATION_POINT_SOV_BTC], + false + ); //weight 40000 / 100000 + await liquidityMining.add( + dummyPool, + [SOVToken.address], + [MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC)], + false + ); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks passed since last deposit + await mineBlocks(9); + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks passed + let passedBlocks = 10; + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(expectedUserReward); + }); + + it("dummy pool + 2 pools", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + let ETHBTCpoll = token2.address; + + await liquidityMining.add( + SOVBTCpool, + [SOVToken.address], + [ALLOCATION_POINT_SOV_BTC], + false + ); //weight 40000 / 100000 + const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); + await liquidityMining.add( + dummyPool, + [SOVToken.address], + [DUMMY_ALLOCATION_POINT], + false + ); //weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlocks(9); + await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit + + //update config + //this method will also update pool reward using previous allocation point, + //so this block should be add to calculation with old values + await liquidityMining.update( + SOVBTCpool, + [SOVToken.address], + [ALLOCATION_POINT_SOV_BTC_2], + false + ); //weight 30000 / 100000 + + await liquidityMining.add( + ETHBTCpoll, + [SOVToken.address], + [ALLOCATION_POINT_ETH_BTC], + false + ); //weight 37500 / 100000 + const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub( + ALLOCATION_POINT_SOV_BTC_2 + ).sub(ALLOCATION_POINT_ETH_BTC); + await liquidityMining.update( + dummyPool, + [SOVToken.address], + [DUMMY_ALLOCATION_POINT_2], + false + ); //weight (100000 - 30000 - 37500) / 100000 + await liquidityMining.updateAllPools(); + + //reward won't be claimed because liquidityMining doesn't have enough SOV balance + //user reward will be updated + //10 blocks + 5 blocks passed + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + //10 blocks + 5 blocks passed + let passedBlocks = 10 + 1; //block should be add to calculation with old values + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + passedBlocks = 5 - 1; //block should be removed from calculation with new values + expectedUserReward = expectedUserReward.add( + REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC_2) + .div(MAX_ALLOCATION_POINT) + ); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(expectedUserReward); + }); + }); + + describe("multiple rewards tokens per pool", () => { + let rewardToken1; + let rewardToken2; + let transferLogic1; + let transferLogic2; + const otherRewardTokensPerBlock = new BN(6); + const allocationPoint = new BN(1); + + beforeEach(async () => { + // add other reward token + rewardToken1 = await TestToken.new("Reward token 1", "RWT-1", 18, TOTAL_SUPPLY); + rewardToken2 = await TestToken.new("Reward token 2", "RWT-2", 18, TOTAL_SUPPLY); + + transferLogic1 = await ERC20TransferLogic.new(); + transferLogic2 = await ERC20TransferLogic.new(); + await transferLogic1.initialize(rewardToken1.address); + await transferLogic2.initialize(rewardToken2.address); + + await liquidityMining.addRewardToken( + rewardToken1.address, + rewardTokensPerBlock, + startDelayBlocks, + transferLogic1.address + ); + await liquidityMining.addRewardToken( + rewardToken2.address, + otherRewardTokensPerBlock, + startDelayBlocks, + transferLogic2.address + ); + + await rewardToken1.transfer(liquidityMining.address, new BN(1000)); + await rewardToken2.transfer(liquidityMining.address, new BN(1000)); + }); + + it("add 2 reward tokens to one pool", async () => { + await liquidityMining.add( + token1.address, + [rewardToken1.address, rewardToken2.address], + [allocationPoint, allocationPoint], + false + ); + + const poolRewards = await liquidityMining.getPoolRewards(token1.address); + expect(poolRewards).to.be.an("array"); + expect(poolRewards).to.have.length(2); + expect(poolRewards[0].allocationPoint).bignumber.equal(allocationPoint); + expect(poolRewards[1].allocationPoint).bignumber.equal(allocationPoint); + }); + + it("update 2 reward tokens with new allocation points", async () => { + await liquidityMining.add( + token1.address, + [rewardToken1.address, rewardToken2.address], + [allocationPoint, allocationPoint], + false + ); + + let poolRewards = await liquidityMining.getPoolRewards(token1.address); + expect(poolRewards).to.be.an("array"); + expect(poolRewards).to.have.length(2); + expect(poolRewards[0].allocationPoint).bignumber.equal(allocationPoint); + expect(poolRewards[1].allocationPoint).bignumber.equal(allocationPoint); + + const newAllocationPoints = [new BN(3), new BN(4)]; + + await liquidityMining.update( + token1.address, + [rewardToken1.address, rewardToken2.address], + newAllocationPoints, + false + ); + + poolRewards = await liquidityMining.getPoolRewards(token1.address); + expect(poolRewards).to.be.an("array"); + expect(poolRewards).to.have.length(2); + expect(poolRewards[0].allocationPoint).bignumber.equal(newAllocationPoints[0]); + expect(poolRewards[1].allocationPoint).bignumber.equal(newAllocationPoints[1]); + }); + + it("check rewards for two reward tokens and one pool", async () => { + const amount = new BN(1000); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await liquidityMining.add( + token1.address, + [rewardToken1.address, rewardToken2.address], + [allocationPoint, allocationPoint], + false + ); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + rewardToken1.address, + account1 + ); + expect(reward1).bignumber.equal(rewardTokensPerBlock); + + const claimRewardTx1 = await liquidityMining.claimReward( + token1.address, + rewardToken1.address, + ZERO_ADDRESS, + { + from: account1, + } + ); + expectEvent(claimRewardTx1, "RewardClaimed", { + user: account1, + amount: new BN(6), + rewardToken: rewardToken1.address, + }); + + const reward2 = await liquidityMining.getUserAccumulatedReward( + token1.address, + rewardToken2.address, + account1 + ); + expect(reward2).bignumber.equal(new BN(12)); + + const claimRewardTx2 = await liquidityMining.claimReward( + token1.address, + rewardToken2.address, + ZERO_ADDRESS, + { + from: account1, + } + ); + expectEvent(claimRewardTx2, "RewardClaimed", { + user: account1, + amount: new BN(18), + rewardToken: rewardToken2.address, + }); + }); + }); + + describe("onTokensDeposited", () => { + it("a pool should be able to deposit for a user", async () => { + const poolToken = await TestPoolToken.new( + "Test Pool Token", + "TPT", + 18, + TOTAL_SUPPLY, + liquidityMining.address + ); + + await liquidityMining.add(poolToken.address, [SOVToken.address], [new BN(1)], false); + const tx = await poolToken.depositFor(account1, new BN(1000)); + + const userInfo = await liquidityMining.getUserInfo(poolToken.address, account1); + expect(userInfo.amount).bignumber.equal(new BN(1000)); + }); + it("should revert if the sender is not a valid pool token", async () => { + await expectRevert( + liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), + "Pool token not found" + ); + }); + }); + + describe("external getters", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await liquidityMining.add( + token1.address, + [SOVToken.address], + [allocationPoint], + false + ); + }); + + it("PRECISION", async () => { + expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); + }); + + it("rewardTokensPerBlock", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); + }); + + it("startBlock", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.startBlock).bignumber.gt("0"); + }); + + it("endBlock", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.endBlock).bignumber.equal("0"); + }); + + it("wrapper", async () => { + expect(await liquidityMining.wrapper()).equal(wrapper.address); + }); + + it("totalAllocationPoint", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal(allocationPoint); + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint], + false + ); + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalAllocationPoint).bignumber.equal( + allocationPoint.mul(new BN(2)) + ); + }); + + it("totalUsersBalance", async () => { + let rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + rewardToken = await liquidityMining.getRewardToken(SOVToken.address); + expect(rewardToken.totalUsersBalance).bignumber.equal("3"); + }); + + // could still test these, but I don't see much point: + // PoolInfo[] public poolInfoList; + // mapping(address => uint256) poolIdList; + // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + + it("getMissedBalance", async () => { + let missedBalance = await liquidityMining.getMissedBalance(SOVToken.address); + expect(missedBalance).bignumber.equal("0"); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await liquidityMining.updatePool(token1.address); + + missedBalance = await liquidityMining.getMissedBalance(SOVToken.address); + expect(missedBalance).bignumber.equal("3"); + }); + + it("getUserAccumulatedReward", async () => { + // real tests are elsewhere in this file + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + SOVToken.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token1.address, + SOVToken.address, + account2 + ); + expect(reward1).bignumber.equal("3"); + expect(reward2).bignumber.equal("0"); + }); + + it("getPoolId", async () => { + const poolId = await liquidityMining.getPoolId(token1.address); + expect(poolId).bignumber.equal("0"); + await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint], + false + ); + const poolId2 = await liquidityMining.getPoolId(token2.address); + expect(poolId2).bignumber.equal("1"); + }); + + it("getPoolLength", async () => { + let length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("1"); + + await liquidityMining.add( + token2.address, + [SOVToken.address], + [allocationPoint], + false + ); + length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("2"); + }); + + it("getPoolInfoList", async () => { + const infoList = await liquidityMining.getPoolInfoList(); + expect(infoList).to.be.an("array"); + expect(infoList.length).equal(1); + const info = infoList[0]; + expect(info.poolToken).equal(token1.address); + expect(info.rewardTokens[0]).equal(SOVToken.address); + + const poolRewardInfo = await liquidityMining.getPoolReward( + token1.address, + info.rewardTokens[0] + ); + expect(poolRewardInfo.allocationPoint).equal(allocationPoint.toString()); + expect(poolRewardInfo.accumulatedRewardPerShare).equal("0"); + expect(poolRewardInfo.lastRewardBlock).equal( + (await web3.eth.getBlockNumber()).toString() + ); + }); + + it("getPoolInfo", async () => { + const info = await liquidityMining.getPoolInfo(token1.address); + expect(info.poolToken).equal(token1.address); + + const poolRewardInfo = await liquidityMining.getPoolReward( + token1.address, + info.rewardTokens[0] + ); + expect(poolRewardInfo.allocationPoint).equal(allocationPoint.toString()); + expect(poolRewardInfo.accumulatedRewardPerShare).equal("0"); + expect(poolRewardInfo.lastRewardBlock).equal( + (await web3.eth.getBlockNumber()).toString() + ); + + await expectRevert( + liquidityMining.getPoolInfo(token2.address), + "Pool token not found" + ); + }); + + it("getUserBalanceList", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const balanceList = await liquidityMining.getUserBalanceList(account1); + + expect(balanceList).to.be.an("array"); + expect(balanceList.length).equal(1); + const balanceData = balanceList[0]; + expect(balanceData).to.be.an("array"); + expect(balanceData[0].amount).bignumber.equal(amount); + expect(balanceData[0].accumulatedReward).bignumber.equal("3"); + }); + + it("getUserInfo", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? + expect(userInfo.rewards[0].rewardDebt).bignumber.equal("0"); // not yet updated either + + // deposit updates it. + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { + from: account1, + }); + userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("501"); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("3"); + expect(userInfo.rewards[0].rewardDebt).bignumber.equal("3"); + }); + + it("getUserInfoList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let userInfoList = await liquidityMining.getUserInfoList(account1); + expect(userInfoList).to.be.an("array"); + expect(userInfoList.length).equal(1); + const userInfo = userInfoList[0]; + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal("0"); + expect(userInfo.rewards[0].rewardDebt).bignumber.equal("0"); + }); + + it("getUserAccumulatedRewardList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); + expect(rewardList).to.be.an("array"); + expect(rewardList.length).equal(1); + expect(rewardList[0]).to.be.an("array"); + expect(rewardList[0].length).equal(1); + expect(rewardList[0][0].accumulatedReward).bignumber.equal("0"); + }); + + it("getUserPoolTokenBalance", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance( + token1.address, + account1 + ); + expect(poolTokenBalance).bignumber.equal(new BN(500)); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMiningV1.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMining = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function mineBlocks(blocks) { + for (let i = 0; i < blocks; i++) { + await mineBlock(); + } + } + + function checkPoolRewardInfo( + poolInfo, + token, + rewardToken, + allocationPoint, + lastRewardBlock, + accumulatedRewardPerShare + ) { + expect(poolInfo.poolToken).equal(token); + expect(rewardToken.allocationPoint).bignumber.equal(allocationPoint); + expect(rewardToken.lastRewardBlock).bignumber.equal(lastRewardBlock); + if (accumulatedRewardPerShare.toNumber() !== -1) { + expect(rewardToken.accumulatedRewardPerShare).bignumber.equal( + accumulatedRewardPerShare + ); + } + } + + async function checkUserPoolTokens( + user, + poolToken, + _userAmount, + _liquidityMiningBalance, + _userBalance, + wrapper + ) { + //user balance in pool + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.amount).bignumber.equal(_userAmount); + //LM balance of pool tokens + let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); + expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); + //user's balance of pool tokens + let userBalance = await poolToken.balanceOf(user); + if (wrapper !== undefined) { + userBalance = await poolToken.balanceOf(wrapper); + } + expect(userBalance).bignumber.equal(_userBalance); + } + + //user's balance of reward token + async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { + let passedBlocks = latestBlockNumber.sub(depositBlockNumber); + let userReward = passedBlocks.mul(rewardTokensPerBlock); + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.rewards[0].accumulatedReward).bignumber.equal(new BN(0)); + return userReward; + } }); diff --git a/tests/farm/LockedSOVRewardTransferLogic.js b/tests/farm/LockedSOVRewardTransferLogic.js index eb011192a..6447b422a 100644 --- a/tests/farm/LockedSOVRewardTransferLogic.js +++ b/tests/farm/LockedSOVRewardTransferLogic.js @@ -10,215 +10,264 @@ const TestLockedSOV = artifacts.require("LockedSOVMockup"); const LockedSOVRewardTransferLogic = artifacts.require("LockedSOVRewardTransferLogic"); describe("LockedSOVRewardTransferLogic", () => { - const name = "Test SOV Token"; - const symbol = "TST"; - - // The % which determines how much will be unlocked immediately. - /// @dev 10000 is 100% - const unlockedImmediatelyPercent = new BN(1000); //10% - - let accounts; - let root, account1, account2, account3, account4; - let SOVToken, token1, token2, token3; - let rewardTransferLogic, lockedSOVAdmins, lockedSOV; - - before(async () => { - accounts = await web3.eth.getAccounts(); - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - - lockedSOVAdmins = [account1, account2]; - - lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); - }); - - describe("initialize", () => { - it("fails if not an owner or admin", async () => { - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await expectRevert( - rewardTransferLogic.initialize(SOVToken.address, unlockedImmediatelyPercent, { from: account1 }), - "unauthorized" - ); - - await rewardTransferLogic.addAdmin(account1); - await rewardTransferLogic.initialize(SOVToken.address, unlockedImmediatelyPercent, { from: account1 }); - }); - - it("sets the expected values", async () => { - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); - let _lockedSOVAddress = await rewardTransferLogic.lockedSOV(); - let _unlockedImmediatelyPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); - expect(_lockedSOVAddress).equal(lockedSOV.address); - expect(_unlockedImmediatelyPercent).bignumber.equal(unlockedImmediatelyPercent); - }); - }); - - describe("changeLockedSOV", () => { - it("fails if not an owner or admin", async () => { - await expectRevert(rewardTransferLogic.changeLockedSOV(SOVToken.address, { from: account1 }), "unauthorized"); - - await rewardTransferLogic.addAdmin(account1); - await rewardTransferLogic.changeLockedSOV(SOVToken.address, { from: account1 }); - }); - - it("fails if invalid address", async () => { - await rewardTransferLogic.addAdmin(account1); - await expectRevert(rewardTransferLogic.changeLockedSOV(ZERO_ADDRESS, { from: account1 }), "Invalid address"); - }); - - it("should set a new LockedSOV", async () => { - //first check original lockedSOV address - let lockedSOVAddress = await rewardTransferLogic.lockedSOV(); - expect(lockedSOV.address).equal(lockedSOVAddress); - - newLockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - await rewardTransferLogic.addAdmin(account1); - tx = await rewardTransferLogic.changeLockedSOV(newLockedSOV.address, { from: account1 }); - - //then check new lockedSOV address - let newLockedSOVAddress = await rewardTransferLogic.lockedSOV(); - expect(newLockedSOV.address).equal(newLockedSOVAddress); - - expectEvent(tx, "LockedSOVChanged", { - _newAddress: newLockedSOVAddress, - }); - }); - }); - - describe("changeUnlockedImmediatelyPercent", async () => { - it("fails if not an owner or admin", async () => { - await expectRevert( - rewardTransferLogic.changeUnlockedImmediatelyPercent(unlockedImmediatelyPercent, { from: account1 }), - "unauthorized" - ); - - await rewardTransferLogic.addAdmin(account1); - await rewardTransferLogic.changeUnlockedImmediatelyPercent(unlockedImmediatelyPercent, { from: account1 }); - }); - - it("fails if invalid unlocked percent", async () => { - await rewardTransferLogic.addAdmin(account1); - await expectRevert( - rewardTransferLogic.changeUnlockedImmediatelyPercent(new BN(10000), { from: account1 }), - "Unlocked immediately percent has to be less than 10000." - ); - }); - - it("should set a new unlocked percent", async () => { - //first check origin unlocked percent - let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); - expect(unlockedPercent).bignumber.equal(unlockedImmediatelyPercent); - - const newUnlockedPercent = new BN(10); - await rewardTransferLogic.addAdmin(account1); - tx = await rewardTransferLogic.changeUnlockedImmediatelyPercent(newUnlockedPercent, { from: account1 }); - - //then check new unlocked percent - let newUnlockedPercentAmount = await rewardTransferLogic.unlockedImmediatelyPercent(); - expect(newUnlockedPercentAmount).bignumber.equal(newUnlockedPercent); - - expectEvent(tx, "UnlockImmediatelyPercentChanged", { - _newAmount: newUnlockedPercentAmount, - }); - }); - }); - - describe("getRewardTokenAddress", async () => { - it("should return SOVToken address", async () => { - let SOVTokenAddress = await rewardTransferLogic.getRewardTokenAddress(); - expect(SOVTokenAddress).equal(SOVToken.address); - }); - - it("should change lockedSOV and return new token address", async () => { - let newLockedSOV = await TestLockedSOV.new(token1.address, lockedSOVAdmins); - await rewardTransferLogic.addAdmin(account1); - await rewardTransferLogic.changeLockedSOV(newLockedSOV.address, { from: account1 }); - - let token1Address = await rewardTransferLogic.getRewardTokenAddress(); - expect(token1Address).equal(token1.address); - }); - }); - - describe("senderToAuthorize", async () => { - it("should return contract address", async () => { - let rewardTransferLogicAddress = await rewardTransferLogic.senderToAuthorize(); - expect(rewardTransferLogicAddress).equal(rewardTransferLogic.address); - }); - }); - - describe("transferReward", async () => { - const account1InitialBalance = new BN(100); - const amountToTransfer = new BN(50); - - it("fails if account doesn't have reward tokens", async () => { - await expectRevert(rewardTransferLogic.transferReward(account2, new BN(5), false, { from: account1 }), "invalid transfer"); - }); - - it("fails if account didn't approve before", async () => { - //send some SOVTokens to account1 to be able to transfer - await SOVToken.mint(account1, new BN(10)); - await expectRevert(rewardTransferLogic.transferReward(account2, new BN(5), false, { from: account1 }), "invalid transfer"); - }); - - it("fails if invalid address to transfer", async () => { - //send some SOVTokens to account1 to be able to transfer - await SOVToken.mint(account1, account1InitialBalance); - await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); - await expectRevert(rewardTransferLogic.transferReward(ZERO_ADDRESS, new BN(5), false, { from: account1 }), "invalid transfer"); - }); - - it("should account1 transfer reward to account2 without withdraw", async () => { - //send some SOVTokens to account1 to be able to transfer - await SOVToken.mint(account1, account1InitialBalance); - await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); - - await rewardTransferLogic.transferReward(account2, amountToTransfer, false, { from: account1 }); - let account1FinalBalance = await SOVToken.balanceOf(account1); - expect(account1FinalBalance).bignumber.equal(account1InitialBalance.sub(amountToTransfer)); - }); - - it("should account2 receive unlocked balance after transfer without withdraw", async () => { - //send some SOVTokens to account1 to be able to transfer - await SOVToken.mint(account1, account1InitialBalance); - await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); - - await rewardTransferLogic.transferReward(account2, amountToTransfer, false, { from: account1 }); - let lockedBalance = await lockedSOV.getLockedBalance(account2); - let unlockedBalance = await lockedSOV.getUnlockedBalance(account2); - - let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); - let balancePercent = amountToTransfer.mul(unlockedPercent).div(new BN(10000)); - let balanceAccount2 = await SOVToken.balanceOf(account2); - - expect(balanceAccount2).bignumber.equal(balancePercent); - expect(lockedBalance).bignumber.equal(new BN(0)); - expect(unlockedBalance).bignumber.equal(new BN(0)); - }); - - it("should should account2 have locked and unlocked balance after transfer with withdraw", async () => { - //send some SOVTokens to account1 to be able to transfer - await SOVToken.mint(account1, account1InitialBalance); - await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { from: account1 }); - - await rewardTransferLogic.transferReward(account2, amountToTransfer, true, { from: account1 }); - let lockedBalance = await lockedSOV.getLockedBalance(account2); - let unlockedBalance = await lockedSOV.getUnlockedBalance(account2); - - let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); - let balancePercent = amountToTransfer.mul(unlockedPercent).div(new BN(10000)); - - expect(lockedBalance).bignumber.equal(amountToTransfer.sub(balancePercent)); - expect(unlockedBalance).bignumber.equal(balancePercent); - }); - }); + const name = "Test SOV Token"; + const symbol = "TST"; + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let accounts; + let root, account1, account2, account3, account4; + let SOVToken, token1, token2, token3; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + }); + + describe("initialize", () => { + it("fails if not an owner or admin", async () => { + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await expectRevert( + rewardTransferLogic.initialize(SOVToken.address, unlockedImmediatelyPercent, { + from: account1, + }), + "unauthorized" + ); + + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.initialize(SOVToken.address, unlockedImmediatelyPercent, { + from: account1, + }); + }); + + it("sets the expected values", async () => { + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + let _lockedSOVAddress = await rewardTransferLogic.lockedSOV(); + let _unlockedImmediatelyPercent = + await rewardTransferLogic.unlockedImmediatelyPercent(); + expect(_lockedSOVAddress).equal(lockedSOV.address); + expect(_unlockedImmediatelyPercent).bignumber.equal(unlockedImmediatelyPercent); + }); + }); + + describe("changeLockedSOV", () => { + it("fails if not an owner or admin", async () => { + await expectRevert( + rewardTransferLogic.changeLockedSOV(SOVToken.address, { from: account1 }), + "unauthorized" + ); + + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.changeLockedSOV(SOVToken.address, { from: account1 }); + }); + + it("fails if invalid address", async () => { + await rewardTransferLogic.addAdmin(account1); + await expectRevert( + rewardTransferLogic.changeLockedSOV(ZERO_ADDRESS, { from: account1 }), + "Invalid address" + ); + }); + + it("should set a new LockedSOV", async () => { + //first check original lockedSOV address + let lockedSOVAddress = await rewardTransferLogic.lockedSOV(); + expect(lockedSOV.address).equal(lockedSOVAddress); + + newLockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + await rewardTransferLogic.addAdmin(account1); + tx = await rewardTransferLogic.changeLockedSOV(newLockedSOV.address, { + from: account1, + }); + + //then check new lockedSOV address + let newLockedSOVAddress = await rewardTransferLogic.lockedSOV(); + expect(newLockedSOV.address).equal(newLockedSOVAddress); + + expectEvent(tx, "LockedSOVChanged", { + _newAddress: newLockedSOVAddress, + }); + }); + }); + + describe("changeUnlockedImmediatelyPercent", async () => { + it("fails if not an owner or admin", async () => { + await expectRevert( + rewardTransferLogic.changeUnlockedImmediatelyPercent(unlockedImmediatelyPercent, { + from: account1, + }), + "unauthorized" + ); + + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.changeUnlockedImmediatelyPercent( + unlockedImmediatelyPercent, + { from: account1 } + ); + }); + + it("fails if invalid unlocked percent", async () => { + await rewardTransferLogic.addAdmin(account1); + await expectRevert( + rewardTransferLogic.changeUnlockedImmediatelyPercent(new BN(10000), { + from: account1, + }), + "Unlocked immediately percent has to be less than 10000." + ); + }); + + it("should set a new unlocked percent", async () => { + //first check origin unlocked percent + let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); + expect(unlockedPercent).bignumber.equal(unlockedImmediatelyPercent); + + const newUnlockedPercent = new BN(10); + await rewardTransferLogic.addAdmin(account1); + tx = await rewardTransferLogic.changeUnlockedImmediatelyPercent(newUnlockedPercent, { + from: account1, + }); + + //then check new unlocked percent + let newUnlockedPercentAmount = await rewardTransferLogic.unlockedImmediatelyPercent(); + expect(newUnlockedPercentAmount).bignumber.equal(newUnlockedPercent); + + expectEvent(tx, "UnlockImmediatelyPercentChanged", { + _newAmount: newUnlockedPercentAmount, + }); + }); + }); + + describe("getRewardTokenAddress", async () => { + it("should return SOVToken address", async () => { + let SOVTokenAddress = await rewardTransferLogic.getRewardTokenAddress(); + expect(SOVTokenAddress).equal(SOVToken.address); + }); + + it("should change lockedSOV and return new token address", async () => { + let newLockedSOV = await TestLockedSOV.new(token1.address, lockedSOVAdmins); + await rewardTransferLogic.addAdmin(account1); + await rewardTransferLogic.changeLockedSOV(newLockedSOV.address, { from: account1 }); + + let token1Address = await rewardTransferLogic.getRewardTokenAddress(); + expect(token1Address).equal(token1.address); + }); + }); + + describe("senderToAuthorize", async () => { + it("should return contract address", async () => { + let rewardTransferLogicAddress = await rewardTransferLogic.senderToAuthorize(); + expect(rewardTransferLogicAddress).equal(rewardTransferLogic.address); + }); + }); + + describe("transferReward", async () => { + const account1InitialBalance = new BN(100); + const amountToTransfer = new BN(50); + + it("fails if account doesn't have reward tokens", async () => { + await expectRevert( + rewardTransferLogic.transferReward(account2, new BN(5), false, { from: account1 }), + "invalid transfer" + ); + }); + + it("fails if account didn't approve before", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, new BN(10)); + await expectRevert( + rewardTransferLogic.transferReward(account2, new BN(5), false, { from: account1 }), + "invalid transfer" + ); + }); + + it("fails if invalid address to transfer", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { + from: account1, + }); + await expectRevert( + rewardTransferLogic.transferReward(ZERO_ADDRESS, new BN(5), false, { + from: account1, + }), + "invalid transfer" + ); + }); + + it("should account1 transfer reward to account2 without withdraw", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { + from: account1, + }); + + await rewardTransferLogic.transferReward(account2, amountToTransfer, false, { + from: account1, + }); + let account1FinalBalance = await SOVToken.balanceOf(account1); + expect(account1FinalBalance).bignumber.equal( + account1InitialBalance.sub(amountToTransfer) + ); + }); + + it("should account2 receive unlocked balance after transfer without withdraw", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { + from: account1, + }); + + await rewardTransferLogic.transferReward(account2, amountToTransfer, false, { + from: account1, + }); + let lockedBalance = await lockedSOV.getLockedBalance(account2); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account2); + + let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); + let balancePercent = amountToTransfer.mul(unlockedPercent).div(new BN(10000)); + let balanceAccount2 = await SOVToken.balanceOf(account2); + + expect(balanceAccount2).bignumber.equal(balancePercent); + expect(lockedBalance).bignumber.equal(new BN(0)); + expect(unlockedBalance).bignumber.equal(new BN(0)); + }); + + it("should should account2 have locked and unlocked balance after transfer with withdraw", async () => { + //send some SOVTokens to account1 to be able to transfer + await SOVToken.mint(account1, account1InitialBalance); + await SOVToken.approve(rewardTransferLogic.address, account1InitialBalance, { + from: account1, + }); + + await rewardTransferLogic.transferReward(account2, amountToTransfer, true, { + from: account1, + }); + let lockedBalance = await lockedSOV.getLockedBalance(account2); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account2); + + let unlockedPercent = await rewardTransferLogic.unlockedImmediatelyPercent(); + let balancePercent = amountToTransfer.mul(unlockedPercent).div(new BN(10000)); + + expect(lockedBalance).bignumber.equal(amountToTransfer.sub(balancePercent)); + expect(unlockedBalance).bignumber.equal(balancePercent); + }); + }); }); diff --git a/tests/loan-token/LendingWithLMV2.test.js b/tests/loan-token/LendingWithLMV2.test.js index 41dbe209d..91d0b858b 100644 --- a/tests/loan-token/LendingWithLMV2.test.js +++ b/tests/loan-token/LendingWithLMV2.test.js @@ -38,305 +38,337 @@ const Migrator = artifacts.require("LMV1toLMV2Migrator"); const TOTAL_SUPPLY = web3.utils.toWei("1000", "ether"); //const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_more_of_lender_balance_should_not_fail } = require("./helpers"); -const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_uint256_max_should_withdraw_total_balance } = require("./helpers"); +const { + lend_to_the_pool, + cash_out_from_the_pool, + cash_out_from_the_pool_uint256_max_should_withdraw_total_balance, +} = require("./helpers"); const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); const wei = web3.utils.toWei; contract("LoanTokenLogicLM", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - const depositAmount = new BN(wei("400", "ether")); - - const rewardTokensPerBlock = new BN(3); - const startDelayBlocks = new BN(1); - const numberOfBonusBlocks = new BN(50); - - // The % which determines how much will be unlocked immediately. - /// @dev 10000 is 100% - const unlockedImmediatelyPercent = new BN(1000); //10% - - let lender, account1, account2, account3, account4; - let underlyingToken, testWrbtc; - let SOVToken, token1, token2, token3, liquidityMiningConfigToken; - let sovryn, loanToken, loanTokenWRBTC; - let liquidityMiningV1, liquidityMining, migrator, wrapper; - let rewardTransferLogic, lockedSOVAdmins, lockedSOV; - - before(async () => { - [lender, account1, account2, account3, account4, ...accounts] = accounts; - await deployProtocol(); - await deployLoanTokens(); - - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); - lockedSOVAdmins = [account1, account2]; - - lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - - await deployLiquidityMining(); - await liquidityMiningV1.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - - await upgradeLiquidityMining(); - - await deployLiquidityMiningV2(); - - await liquidityMiningV1.initialize(liquidityMining.address); - - migrator = await Migrator.new(); - await migrator.initialize(SOVToken.address, liquidityMiningV1.address, liquidityMining.address); - - await liquidityMining.initialize(wrapper.address, migrator.address); - - rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); - await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); - - await liquidityMining.setWrapper(wrapper.address); - await liquidityMining.addRewardToken(SOVToken.address, rewardTokensPerBlock, startDelayBlocks, rewardTransferLogic.address); - - //mint SOVs to lvm1 for migrations - await SOVToken.mint(liquidityMiningV1.address, new BN(10)); - await liquidityMiningV1.addAdmin(migrator.address); - await liquidityMiningV1.startMigrationGracePeriod(); - await liquidityMining.addAdmin(migrator.address); - await migrator.migratePools(); - await migrator.finishUsersMigration(); - await migrator.migrateFunds(); - //burn SOVs for testing - const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); - await SOVToken.burn(liquidityMining.address, balanceSOV); - - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - - await liquidityMining.add(loanToken.address, [SOVToken.address], [new BN(10)], false); - await liquidityMining.add(loanTokenWRBTC.address, [SOVToken.address], [new BN(10)], true); - }); - - describe("Test lending with liquidity mining", () => { - it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { - //await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); - await underlyingToken.approve(loanToken.address, depositAmount); - const tx = await loanToken.mint(lender, depositAmount, true); - const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - //expected: user pool token balance is 0, but balance of LM contract increased - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); - //expect the Mint event to mention the lender - expectEvent(tx, "Mint", { - minter: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { - await underlyingToken.approve(loanToken.address, depositAmount); - const tx = await loanToken.mint(lender, depositAmount, false); - const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged - expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); - }); - - it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { - let userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - const tx = await loanToken.burn(lender, userInfo.amount, true); - userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - //expected: user pool token balance stayed the same but LM balance is 0 - expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); - //expect the Burn event to mention the lender - expectEvent(tx, "Burn", { - burner: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should burn pool tokens without removing them from the LM pool", async () => { - await loanToken.burn(lender, depositAmount, false); - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(await loanToken.totalSupply()).bignumber.equal("0"); - }); - }); - - describe("Test WRBTC lending with liquidity mining", () => { - it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { - //await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); - const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance is 0, but balance of LM contract increased - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); - //expect the Mint event to mention the lender - expectEvent(tx, "Mint", { - minter: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { - await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); - const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); - }); - - it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { - let userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); - userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - //expected: user pool token balance stayed the same but LM balance is 0 - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); - //expect the Burn event to mention the lender - expectEvent(tx, "Burn", { - burner: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should burn pool tokens without removing them from the LM pool", async () => { - await loanTokenWRBTC.burnToBTC(lender, depositAmount, false); - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); - }); - }); - - describe("Test setting the liquidity mining address", () => { - it("Should be able to set the liquidity mining address", async () => { - await loanToken.setLiquidityMiningAddress(account2); - expect(await loanToken.liquidityMiningAddress()).to.be.equal(account2); - }); - - it("Should fail to set the liquidity mining address with an unauthorized wallet", async () => { - await expectRevert(loanToken.setLiquidityMiningAddress(account2, { from: account1 }), "unauthorized"); - }); - }); - - async function deployLiquidityMining() { - let liquidityMiningLogicV1 = await LiquidityMiningLogic.new(); - liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); - liquidityMiningV1 = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - - wrapper = await Wrapper.new(liquidityMiningV1.address); - } - - async function upgradeLiquidityMining() { - let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); - liquidityMiningV1 = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); - } - - async function deployLiquidityMiningV2() { - let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); - let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); - await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); - liquidityMining = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); - - wrapper = await Wrapper.new(liquidityMining.address); - } - - async function deployProtocol() { - //Token - underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - testWrbtc = await TestWrbtc.new(); - - const sovrynproxy = await sovrynProtocol.new(); - sovryn = await ISovryn.at(sovrynproxy.address); - - await sovryn.replaceContract((await LoanClosingsBase.new()).address); - await sovryn.replaceContract((await LoanClosingsWith.new()).address); - await sovryn.replaceContract((await ProtocolSettings.new()).address); - await sovryn.replaceContract((await LoanSettings.new()).address); - await sovryn.replaceContract((await LoanMaintenance.new()).address); - await sovryn.replaceContract((await SwapsExternal.new()).address); - await sovryn.replaceContract((await LoanOpenings.new()).address); - - await sovryn.setWrbtcToken(testWrbtc.address); - - feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); - await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); - const swaps = await SwapsImplLocal.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([underlyingToken.address, testWrbtc.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address //priceFeeds - ); - await sovryn.setSwapsImplContract( - swaps.address // swapsImpl - ); - await sovryn.setFeesController(lender); - } - - async function deployLoanTokens() { - loanTokenLogicLM = await LoanTokenLogicLM.new(); - loanToken = await LoanToken.new(lender, loanTokenLogicLM.address, sovryn.address, testWrbtc.address); - await loanToken.initialize(underlyingToken.address, name, symbol); //iToken - loanToken = await LoanTokenLogicLM.at(loanToken.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - underlyingToken.address, // address loanToken; // the token being loaned - testWrbtc.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanToken.setupLoanParams([params], false); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - // --------------- WRBTC -----------------------// - - loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); - loanTokenWRBTC = await LoanToken.new(lender, loanTokenLogicWRBTC.address, sovryn.address, testWrbtc.address); - await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); - loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - testWrbtc.address, // address loanToken; // the token being loaned - underlyingToken.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanTokenWRBTC.setupLoanParams([params], false); - await sovryn.setLoanPool([loanTokenWRBTC.address], [testWrbtc.address]); - - // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// - await testWrbtc.mint(sovryn.address, wei("500", "ether")); - await underlyingToken.mint(sovryn.address, wei("50000", "ether")); - } + const name = "Test token"; + const symbol = "TST"; + const depositAmount = new BN(wei("400", "ether")); + + const rewardTokensPerBlock = new BN(3); + const startDelayBlocks = new BN(1); + const numberOfBonusBlocks = new BN(50); + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); //10% + + let lender, account1, account2, account3, account4; + let underlyingToken, testWrbtc; + let SOVToken, token1, token2, token3, liquidityMiningConfigToken; + let sovryn, loanToken, loanTokenWRBTC; + let liquidityMiningV1, liquidityMining, migrator, wrapper; + let rewardTransferLogic, lockedSOVAdmins, lockedSOV; + + before(async () => { + [lender, account1, account2, account3, account4, ...accounts] = accounts; + await deployProtocol(); + await deployLoanTokens(); + + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMiningV1.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + await upgradeLiquidityMining(); + + await deployLiquidityMiningV2(); + + await liquidityMiningV1.initialize(liquidityMining.address); + + migrator = await Migrator.new(); + await migrator.initialize( + SOVToken.address, + liquidityMiningV1.address, + liquidityMining.address + ); + + await liquidityMining.initialize(wrapper.address, migrator.address); + + rewardTransferLogic = await LockedSOVRewardTransferLogic.new(); + await rewardTransferLogic.initialize(lockedSOV.address, unlockedImmediatelyPercent); + + await liquidityMining.setWrapper(wrapper.address); + await liquidityMining.addRewardToken( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + rewardTransferLogic.address + ); + + //mint SOVs to lvm1 for migrations + await SOVToken.mint(liquidityMiningV1.address, new BN(10)); + await liquidityMiningV1.addAdmin(migrator.address); + await liquidityMiningV1.startMigrationGracePeriod(); + await liquidityMining.addAdmin(migrator.address); + await migrator.migratePools(); + await migrator.finishUsersMigration(); + await migrator.migrateFunds(); + //burn SOVs for testing + const balanceSOV = await SOVToken.balanceOf(liquidityMining.address); + await SOVToken.burn(liquidityMining.address, balanceSOV); + + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + + await liquidityMining.add(loanToken.address, [SOVToken.address], [new BN(10)], false); + await liquidityMining.add(loanTokenWRBTC.address, [SOVToken.address], [new BN(10)], true); + }); + + describe("Test lending with liquidity mining", () => { + it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { + //await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); + await underlyingToken.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, true); + const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + //expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + await underlyingToken.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, false); + const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + let userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + const tx = await loanToken.burn(lender, userInfo.amount, true); + userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + await loanToken.burn(lender, depositAmount, false); + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal("0"); + }); + }); + + describe("Test WRBTC lending with liquidity mining", () => { + it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { + //await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); + const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + //expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); + const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal( + depositAmount.mul(new BN("2")) + ); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + let userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); + userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + //expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + //expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + await loanTokenWRBTC.burnToBTC(lender, depositAmount, false); + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); + }); + }); + + describe("Test setting the liquidity mining address", () => { + it("Should be able to set the liquidity mining address", async () => { + await loanToken.setLiquidityMiningAddress(account2); + expect(await loanToken.liquidityMiningAddress()).to.be.equal(account2); + }); + + it("Should fail to set the liquidity mining address with an unauthorized wallet", async () => { + await expectRevert( + loanToken.setLiquidityMiningAddress(account2, { from: account1 }), + "unauthorized" + ); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogic.new(); + liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMiningV1.address); + } + + async function upgradeLiquidityMining() { + let liquidityMiningLogicV1 = await LiquidityMiningLogicV1.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogicV1.address); + liquidityMiningV1 = await LiquidityMiningLogicV1.at(liquidityMiningProxy.address); + } + + async function deployLiquidityMiningV2() { + let liquidityMiningLogicV2 = await LiquidityMiningLogicV2.new(); + let liquidityMiningProxyV2 = await LiquidityMiningProxyV2.new(); + await liquidityMiningProxyV2.setImplementation(liquidityMiningLogicV2.address); + liquidityMining = await LiquidityMiningLogicV2.at(liquidityMiningProxyV2.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function deployProtocol() { + //Token + underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + testWrbtc = await TestWrbtc.new(); + + const sovrynproxy = await sovrynProtocol.new(); + sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await LoanClosingsBase.new()).address); + await sovryn.replaceContract((await LoanClosingsWith.new()).address); + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + await sovryn.replaceContract((await LoanOpenings.new()).address); + + await sovryn.setWrbtcToken(testWrbtc.address); + + feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); + await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); + const swaps = await SwapsImplLocal.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens( + [underlyingToken.address, testWrbtc.address], + [true, true] + ); + await sovryn.setPriceFeedContract( + feeds.address //priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + } + + async function deployLoanTokens() { + loanTokenLogicLM = await LoanTokenLogicLM.new(); + loanToken = await LoanToken.new( + lender, + loanTokenLogicLM.address, + sovryn.address, + testWrbtc.address + ); + await loanToken.initialize(underlyingToken.address, name, symbol); //iToken + loanToken = await LoanTokenLogicLM.at(loanToken.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + underlyingToken.address, // address loanToken; // the token being loaned + testWrbtc.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + // --------------- WRBTC -----------------------// + + loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); + loanTokenWRBTC = await LoanToken.new( + lender, + loanTokenLogicWRBTC.address, + sovryn.address, + testWrbtc.address + ); + await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); + loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + testWrbtc.address, // address loanToken; // the token being loaned + underlyingToken.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanTokenWRBTC.setupLoanParams([params], false); + await sovryn.setLoanPool([loanTokenWRBTC.address], [testWrbtc.address]); + + // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// + await testWrbtc.mint(sovryn.address, wei("500", "ether")); + await underlyingToken.mint(sovryn.address, wei("50000", "ether")); + } }); From 3727c5a3de9a1ce862366f612efb3142a18e6563 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 4 May 2022 22:27:20 +0530 Subject: [PATCH 10/17] Fixed tests - need to optimise --- tests/farm/LiquidityMiningMigration.js | 31 ++++++++++++++++----- tests/loan-token/LendingWithLMV2.test.js | 34 ++++++++++++++++++------ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index b30d0f5b6..f3b9faff3 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -27,12 +27,13 @@ const ProtocolSettings = artifacts.require("ProtocolSettings"); const ISovryn = artifacts.require("ISovryn"); const LoanToken = artifacts.require("LoanToken"); +const ILoanTokenLogicProxy = artifacts.require("ILoanTokenLogicProxy"); +const ILoanTokenModules = artifacts.require("ILoanTokenModules"); const LoanTokenLogicLM = artifacts.require("LoanTokenLogicLM"); const LoanTokenLogicWRBTC = artifacts.require("LoanTokenLogicWrbtc"); const LoanSettings = artifacts.require("LoanSettings"); const LoanMaintenance = artifacts.require("LoanMaintenance"); const LoanOpenings = artifacts.require("LoanOpenings"); -const LoanClosingsBase = artifacts.require("LoanClosingsBase"); const LoanClosingsWith = artifacts.require("LoanClosingsWith"); const SwapsExternal = artifacts.require("SwapsExternal"); @@ -41,6 +42,10 @@ const TestSovrynSwap = artifacts.require("TestSovrynSwap"); const SwapsImplLocal = artifacts.require("SwapsImplLocal"); const wei = web3.utils.toWei; +const { + getLoanTokenLogic, + getLoanTokenLogicWrbtc, +} = require("../Utils/initializer.js"); describe("LiquidityMiningMigration", () => { const name = "Test SOV Token"; @@ -1502,7 +1507,6 @@ describe("LiquidityMiningMigration", () => { const sovrynproxy = await sovrynProtocol.new(); sovryn = await ISovryn.at(sovrynproxy.address); - await sovryn.replaceContract((await LoanClosingsBase.new()).address); await sovryn.replaceContract((await LoanClosingsWith.new()).address); await sovryn.replaceContract((await ProtocolSettings.new()).address); await sovryn.replaceContract((await LoanSettings.new()).address); @@ -1531,7 +1535,9 @@ describe("LiquidityMiningMigration", () => { } async function deployLoanTokens() { - loanTokenLogicLM = await LoanTokenLogicLM.new(); + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; + loanTokenLogicBeaconLM = initLoanTokenLogic[1]; loanToken = await LoanToken.new( lender, loanTokenLogicLM.address, @@ -1539,7 +1545,12 @@ describe("LiquidityMiningMigration", () => { testWrbtc.address ); await loanToken.initialize(underlyingToken.address, name, symbol); //iToken - loanToken = await LoanTokenLogicLM.at(loanToken.address); + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); params = [ "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object @@ -1560,15 +1571,21 @@ describe("LiquidityMiningMigration", () => { // --------------- WRBTC -----------------------// - loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); + const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; + loanTokenLogicBeaconWrbtc = initLoanTokenLogicWrbtc[1]; loanTokenWRBTC = await LoanToken.new( lender, - loanTokenLogicWRBTC.address, + loanTokenLogicWrbtc.address, sovryn.address, testWrbtc.address ); await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); - loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); + /** Initialize the loan token logic proxy */ + loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); + await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); + /** Use interface of LoanTokenModules */ + loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); params = [ "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object diff --git a/tests/loan-token/LendingWithLMV2.test.js b/tests/loan-token/LendingWithLMV2.test.js index 91d0b858b..ecf49ee90 100644 --- a/tests/loan-token/LendingWithLMV2.test.js +++ b/tests/loan-token/LendingWithLMV2.test.js @@ -11,12 +11,13 @@ const ProtocolSettings = artifacts.require("ProtocolSettings"); const ISovryn = artifacts.require("ISovryn"); const LoanToken = artifacts.require("LoanToken"); +const ILoanTokenLogicProxy = artifacts.require("ILoanTokenLogicProxy"); +const ILoanTokenModules = artifacts.require("ILoanTokenModules"); const LoanTokenLogicLM = artifacts.require("LoanTokenLogicLM"); const LoanTokenLogicWRBTC = artifacts.require("LoanTokenLogicWrbtc"); const LoanSettings = artifacts.require("LoanSettings"); const LoanMaintenance = artifacts.require("LoanMaintenance"); const LoanOpenings = artifacts.require("LoanOpenings"); -const LoanClosingsBase = artifacts.require("LoanClosingsBase"); const LoanClosingsWith = artifacts.require("LoanClosingsWith"); const SwapsExternal = artifacts.require("SwapsExternal"); @@ -47,6 +48,11 @@ const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); const wei = web3.utils.toWei; +const { + getLoanTokenLogic, + getLoanTokenLogicWrbtc, +} = require("../Utils/initializer.js"); + contract("LoanTokenLogicLM", (accounts) => { const name = "Test token"; const symbol = "TST"; @@ -242,7 +248,7 @@ contract("LoanTokenLogicLM", (accounts) => { describe("Test setting the liquidity mining address", () => { it("Should be able to set the liquidity mining address", async () => { await loanToken.setLiquidityMiningAddress(account2); - expect(await loanToken.liquidityMiningAddress()).to.be.equal(account2); + expect(await loanToken.getLiquidityMiningAddress()).to.be.equal(account2); }); it("Should fail to set the liquidity mining address with an unauthorized wallet", async () => { @@ -285,7 +291,6 @@ contract("LoanTokenLogicLM", (accounts) => { const sovrynproxy = await sovrynProtocol.new(); sovryn = await ISovryn.at(sovrynproxy.address); - await sovryn.replaceContract((await LoanClosingsBase.new()).address); await sovryn.replaceContract((await LoanClosingsWith.new()).address); await sovryn.replaceContract((await ProtocolSettings.new()).address); await sovryn.replaceContract((await LoanSettings.new()).address); @@ -314,7 +319,9 @@ contract("LoanTokenLogicLM", (accounts) => { } async function deployLoanTokens() { - loanTokenLogicLM = await LoanTokenLogicLM.new(); + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; + loanTokenLogicBeaconLM = initLoanTokenLogic[1]; loanToken = await LoanToken.new( lender, loanTokenLogicLM.address, @@ -322,7 +329,12 @@ contract("LoanTokenLogicLM", (accounts) => { testWrbtc.address ); await loanToken.initialize(underlyingToken.address, name, symbol); //iToken - loanToken = await LoanTokenLogicLM.at(loanToken.address); + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); params = [ "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object @@ -343,15 +355,21 @@ contract("LoanTokenLogicLM", (accounts) => { // --------------- WRBTC -----------------------// - loanTokenLogicWRBTC = await LoanTokenLogicWRBTC.new(); + const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; + loanTokenLogicBeaconWrbtc = initLoanTokenLogicWrbtc[1]; loanTokenWRBTC = await LoanToken.new( lender, - loanTokenLogicWRBTC.address, + loanTokenLogicWrbtc.address, sovryn.address, testWrbtc.address ); await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); - loanTokenWRBTC = await LoanTokenLogicWRBTC.at(loanTokenWRBTC.address); + /** Initialize the loan token logic proxy */ + loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); + await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); + /** Use interface of LoanTokenModules */ + loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); params = [ "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object From 68a20eeeb27b4c24860d37df84858f8780aab001 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 4 May 2022 22:28:11 +0530 Subject: [PATCH 11/17] Ran prettier --- tests/farm/LiquidityMiningMigration.js | 21 +++++++++------------ tests/loan-token/LendingWithLMV2.test.js | 21 +++++++++------------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index f3b9faff3..e1e587ebc 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -42,10 +42,7 @@ const TestSovrynSwap = artifacts.require("TestSovrynSwap"); const SwapsImplLocal = artifacts.require("SwapsImplLocal"); const wei = web3.utils.toWei; -const { - getLoanTokenLogic, - getLoanTokenLogicWrbtc, -} = require("../Utils/initializer.js"); +const { getLoanTokenLogic, getLoanTokenLogicWrbtc } = require("../Utils/initializer.js"); describe("LiquidityMiningMigration", () => { const name = "Test SOV Token"; @@ -1535,8 +1532,8 @@ describe("LiquidityMiningMigration", () => { } async function deployLoanTokens() { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicLM = initLoanTokenLogic[0]; + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; loanTokenLogicBeaconLM = initLoanTokenLogic[1]; loanToken = await LoanToken.new( lender, @@ -1571,8 +1568,8 @@ describe("LiquidityMiningMigration", () => { // --------------- WRBTC -----------------------// - const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; + const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; loanTokenLogicBeaconWrbtc = initLoanTokenLogicWrbtc[1]; loanTokenWRBTC = await LoanToken.new( lender, @@ -1581,10 +1578,10 @@ describe("LiquidityMiningMigration", () => { testWrbtc.address ); await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); - /** Initialize the loan token logic proxy */ - loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); - await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); - /** Use interface of LoanTokenModules */ + /** Initialize the loan token logic proxy */ + loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); + await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); + /** Use interface of LoanTokenModules */ loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); params = [ diff --git a/tests/loan-token/LendingWithLMV2.test.js b/tests/loan-token/LendingWithLMV2.test.js index ecf49ee90..4b8342d01 100644 --- a/tests/loan-token/LendingWithLMV2.test.js +++ b/tests/loan-token/LendingWithLMV2.test.js @@ -48,10 +48,7 @@ const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); const wei = web3.utils.toWei; -const { - getLoanTokenLogic, - getLoanTokenLogicWrbtc, -} = require("../Utils/initializer.js"); +const { getLoanTokenLogic, getLoanTokenLogicWrbtc } = require("../Utils/initializer.js"); contract("LoanTokenLogicLM", (accounts) => { const name = "Test token"; @@ -319,8 +316,8 @@ contract("LoanTokenLogicLM", (accounts) => { } async function deployLoanTokens() { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicLM = initLoanTokenLogic[0]; + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; loanTokenLogicBeaconLM = initLoanTokenLogic[1]; loanToken = await LoanToken.new( lender, @@ -355,8 +352,8 @@ contract("LoanTokenLogicLM", (accounts) => { // --------------- WRBTC -----------------------// - const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; + const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; loanTokenLogicBeaconWrbtc = initLoanTokenLogicWrbtc[1]; loanTokenWRBTC = await LoanToken.new( lender, @@ -365,10 +362,10 @@ contract("LoanTokenLogicLM", (accounts) => { testWrbtc.address ); await loanTokenWRBTC.initialize(testWrbtc.address, "iRBTC", "iRBTC"); - /** Initialize the loan token logic proxy */ - loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); - await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); - /** Use interface of LoanTokenModules */ + /** Initialize the loan token logic proxy */ + loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); + await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); + /** Use interface of LoanTokenModules */ loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); params = [ From bfe8a83d112b95073b07e4a2020d83d258e4b78b Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 19 May 2022 17:03:18 +0530 Subject: [PATCH 12/17] Minor changes --- contracts/farm/ERC20TransferLogic.sol | 1 - contracts/farm/IRewardTransferLogic.sol | 2 +- contracts/farm/LMV1toLMV2Migrator.sol | 7 +- contracts/farm/LiquidityMiningV1.sol | 35 +++++---- contracts/farm/LiquidityMiningV2.sol | 77 ++++++++++--------- .../liquidity_mining_V1toV2_migrator.py | 2 +- tests/farm/LiquidityMiningMigration.js | 12 +-- tests/farm/LiquidityMiningV1.js | 8 +- tests/farm/LiquidityMiningV2.js | 4 +- tests/loan-token/LendingWithLMV2.test.js | 2 +- 10 files changed, 83 insertions(+), 67 deletions(-) diff --git a/contracts/farm/ERC20TransferLogic.sol b/contracts/farm/ERC20TransferLogic.sol index 12a0e4ea0..7333f80e8 100644 --- a/contracts/farm/ERC20TransferLogic.sol +++ b/contracts/farm/ERC20TransferLogic.sol @@ -1,6 +1,5 @@ pragma solidity 0.5.17; -import "./IRewardTransferLogic.sol"; import "./ERC20TransferLogicStorage.sol"; import "../interfaces/IERC20.sol"; import "../openzeppelin/SafeERC20.sol"; diff --git a/contracts/farm/IRewardTransferLogic.sol b/contracts/farm/IRewardTransferLogic.sol index de562b812..82969464e 100644 --- a/contracts/farm/IRewardTransferLogic.sol +++ b/contracts/farm/IRewardTransferLogic.sol @@ -13,7 +13,7 @@ interface IRewardTransferLogic { /// @notice Transfers the reward amount to the specified address /// @param _to The address to transfer the reward to /// @param _value The amount of the reward to transfer - /// @param _isWithdrawal If true, means that the reward and the LP deposited tokens are being compeltely withdrawn + /// @param _isWithdrawal If true, the LP tokens are withdrawn and rewards are deposited to LockedSOV(not vested) function transferReward( address _to, uint256 _value, diff --git a/contracts/farm/LMV1toLMV2Migrator.sol b/contracts/farm/LMV1toLMV2Migrator.sol index bc2fe271e..1aa6cecb5 100644 --- a/contracts/farm/LMV1toLMV2Migrator.sol +++ b/contracts/farm/LMV1toLMV2Migrator.sol @@ -11,7 +11,12 @@ import "./ILiquidityMiningV2.sol"; contract LMV1toLMV2Migrator is AdminRole { using SafeMath for uint256; using SafeERC20 for IERC20; - enum MigrationStates { MigratingPools, MigratingUsers, MigratingFunds, MigrationFinished } + enum MigrationStates { + MigratingPools, + MigratingUsers, + MigratingFunds, + MigrationFinished + } //represents de migration state from LiquidityMiningV1 to LiquidityMiningV2 MigrationStates public migrationState; diff --git a/contracts/farm/LiquidityMiningV1.sol b/contracts/farm/LiquidityMiningV1.sol index e59c34afb..7f1e84625 100644 --- a/contracts/farm/LiquidityMiningV1.sol +++ b/contracts/farm/LiquidityMiningV1.sol @@ -76,11 +76,11 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { /* Functions */ /** - * @notice Initialize mining. + * @notice Set LiquidityMiningV2 address. * * @param _liquidityMiningV2 The LiquidityMiningV2 contract address */ - function initialize(address _liquidityMiningV2) external onlyAuthorized { + function setLiquidityMiningV2Address(address _liquidityMiningV2) external onlyAuthorized { /// @dev Non-idempotent function. Must be called just once. require(_liquidityMiningV2 != address(0), "Invalid address"); require(liquidityMiningV2 == address(0), "Already initialized"); @@ -344,8 +344,12 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { PoolInfo storage pool = poolInfoList[poolId]; uint256 start = block.number; uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = - _getPoolAccumulatedReward(pool, _amount, start, end); + (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward( + pool, + _amount, + start, + end + ); return _amount.mul(accumulatedRewardPerShare).div(PRECISION); } @@ -383,8 +387,10 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { return; } - (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = - _getPoolAccumulatedReward(pool); + ( + uint256 accumulatedReward_, + uint256 accumulatedRewardPerShare_ + ) = _getPoolAccumulatedReward(pool); pool.accumulatedRewardPerShare = pool.accumulatedRewardPerShare.add( accumulatedRewardPerShare_ ); @@ -408,10 +414,10 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { uint256 _endBlock ) internal view returns (uint256, uint256) { uint256 passedBlocks = _getPassedBlocksWithBonusMultiplier(_startBlock, _endBlock); - uint256 accumulatedReward = - passedBlocks.mul(rewardTokensPerBlock).mul(_pool.allocationPoint).div( - totalAllocationPoint - ); + uint256 accumulatedReward = passedBlocks + .mul(rewardTokensPerBlock) + .mul(_pool.allocationPoint) + .div(totalAllocationPoint); uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); poolTokenBalance = poolTokenBalance.add(_additionalAmount); @@ -583,10 +589,11 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { //update user accumulated reward if (user.amount > 0) { //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = - user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION).sub( - user.rewardDebt - ); + uint256 accumulatedReward = user + .amount + .mul(pool.accumulatedRewardPerShare) + .div(PRECISION) + .sub(user.rewardDebt); user.accumulatedReward = user.accumulatedReward.add(accumulatedReward); } } diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index f7122acce..6e0df3431 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -218,7 +218,7 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _poolToken, address _rewardToken, uint96 _allocationPoint - ) internal { + ) public onlyAuthorized { uint256 poolId = _getPoolId(_poolToken); // Allocation point checks @@ -286,8 +286,9 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { ) internal { uint256 poolId = _getPoolId(_poolToken); RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - PoolInfoRewardToken storage poolInfoRewardToken = - poolInfoRewardTokensMap[poolId][_rewardToken]; + PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ + _rewardToken + ]; uint96 previousAllocationPoint = poolInfoRewardToken.allocationPoint; rewardToken.totalAllocationPoint = rewardToken @@ -361,15 +362,19 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _user ) internal view returns (uint256) { PoolInfo storage pool = poolInfoList[_poolId]; - PoolInfoRewardToken storage poolRewardToken = - poolInfoRewardTokensMap[_poolId][_rewardToken]; + PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ + _rewardToken + ]; UserInfo storage user = userInfoMap[_poolId][_user]; uint256 accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare; uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); if (block.number > poolRewardToken.lastRewardBlock && poolTokenBalance != 0) { - (, uint256 accumulatedRewardPerShare_) = - _getPoolAccumulatedReward(pool, poolRewardToken, rewardTokensMap[_rewardToken]); + (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward( + pool, + poolRewardToken, + rewardTokensMap[_rewardToken] + ); accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); } return @@ -410,15 +415,14 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { PoolInfo storage pool = poolInfoList[poolId]; uint256 start = block.number; uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = - _getPoolAccumulatedReward( - pool, - _amount, - rewardTokensMap[_rewardToken], - poolInfoRewardTokensMap[poolId][_rewardToken], - start, - end - ); + (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward( + pool, + _amount, + rewardTokensMap[_rewardToken], + poolInfoRewardTokensMap[poolId][_rewardToken], + start, + end + ); return _amount.mul(accumulatedRewardPerShare).div(PRECISION); } @@ -456,8 +460,9 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _poolId, address _rewardToken ) internal { - PoolInfoRewardToken storage poolRewardToken = - poolInfoRewardTokensMap[_poolId][_rewardToken]; + PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ + _rewardToken + ]; // this pool has been updated recently if (block.number <= poolRewardToken.lastRewardBlock) { return; @@ -470,8 +475,10 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { } RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = - _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + ( + uint256 accumulatedReward_, + uint256 accumulatedRewardPerShare_ + ) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); poolRewardToken.accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare.add( accumulatedRewardPerShare_ ); @@ -515,12 +522,11 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _endBlock ) internal view returns (uint256, uint256) { uint256 passedBlocks = _getPassedBlocks(_rewardToken, _startBlock, _endBlock); - uint256 accumulatedReward = - passedBlocks - .mul(_rewardToken.rewardTokensPerBlock) - .mul(PRECISION) - .mul(_poolRewardToken.allocationPoint) - .div(_rewardToken.totalAllocationPoint); + uint256 accumulatedReward = passedBlocks + .mul(_rewardToken.rewardTokensPerBlock) + .mul(PRECISION) + .mul(_poolRewardToken.allocationPoint) + .div(_rewardToken.totalAllocationPoint); uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); poolTokenBalance = poolTokenBalance.add(_additionalAmount); @@ -729,15 +735,13 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { //update user accumulated reward if (user.amount > 0) { //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = - user - .amount - .mul( - poolInfoRewardTokensMap[_poolId][_rewardTokenAddress] - .accumulatedRewardPerShare + uint256 accumulatedReward = user + .amount + .mul( + poolInfoRewardTokensMap[_poolId][_rewardTokenAddress].accumulatedRewardPerShare ) - .div(PRECISION) - .sub(reward.rewardDebt); + .div(PRECISION) + .sub(reward.rewardDebt); reward.accumulatedReward = reward.accumulatedReward.add(accumulatedReward); } } @@ -1041,8 +1045,9 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _accumulatedRewardPerShare ) external onlyMigrator { uint256 poolId = _getPoolId(_poolToken); - PoolInfoRewardToken storage poolInfoRewardToken = - poolInfoRewardTokensMap[poolId][_rewardToken]; + PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ + _rewardToken + ]; poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; poolInfoRewardToken.accumulatedRewardPerShare = _accumulatedRewardPerShare; } diff --git a/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py b/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py index 06891454f..040f7326f 100644 --- a/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py +++ b/scripts/contractInteraction/liquidity_mining_V1toV2_migrator.py @@ -9,7 +9,7 @@ def initializeLiquidityMiningV1(): liquidityMiningV1 = Contract.from_abi("LiquidityMiningV1", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMiningV1.abi, owner = conf.acct) - data = liquidityMiningV1.initialize.encode_input(conf.contracts['LiquidityMiningProxyV2']) + data = liquidityMiningV1.setLiquidityMiningV2Address.encode_input(conf.contracts['LiquidityMiningProxyV2']) sendWithMultisig(conf.contracts['multisig'], liquidityMiningV1.address, data, conf.acct) def initializeLiquidityMiningV2(): diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index e1e587ebc..5bd4b47c1 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -142,7 +142,7 @@ describe("LiquidityMiningMigration", () => { await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); + await liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address); migrator = await Migrator.new(); await migrator.initialize( @@ -199,7 +199,7 @@ describe("LiquidityMiningMigration", () => { ); await upgradeLiquidityMining(); - await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); + await expectRevert(liquidityMining.setLiquidityMiningV2Address(ZERO_ADDRESS), "Invalid address"); }); it("should fail if SOV address is invalid in migrator contract", async () => { migrator = await Migrator.new(); @@ -651,7 +651,7 @@ describe("LiquidityMiningMigration", () => { ); await upgradeLiquidityMining(); await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); + await liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address); migrator = await Migrator.new(); await migrator.initialize( @@ -746,7 +746,7 @@ describe("LiquidityMiningMigration", () => { ); await upgradeLiquidityMining(); await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); + await liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address); migrator = await Migrator.new(); await migrator.initialize( @@ -835,7 +835,7 @@ describe("LiquidityMiningMigration", () => { ); await upgradeLiquidityMining(); await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); + await liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address); migrator = await Migrator.new(); await migrator.initialize( @@ -911,7 +911,7 @@ describe("LiquidityMiningMigration", () => { ); await upgradeLiquidityMining(); await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); + await liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address); migrator = await Migrator.new(); await migrator.initialize( diff --git a/tests/farm/LiquidityMiningV1.js b/tests/farm/LiquidityMiningV1.js index 0aed6ca7c..d0da1d42c 100644 --- a/tests/farm/LiquidityMiningV1.js +++ b/tests/farm/LiquidityMiningV1.js @@ -68,7 +68,7 @@ describe("LiquidityMining", () => { await deployLiquidityMiningV2(); - await liquidityMining.initialize(liquidityMiningV2.address); + await liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address); }); describe("initialize", () => { @@ -182,9 +182,9 @@ describe("LiquidityMining", () => { unlockedImmediatelyPercent ); await upgradeLiquidityMining(); - await liquidityMining.initialize(liquidityMiningV2.address); + await liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address); await expectRevert( - liquidityMining.initialize(liquidityMiningV2.address), + liquidityMining.setLiquidityMiningV2Address(liquidityMiningV2.address), "Already initialized" ); }); @@ -215,7 +215,7 @@ describe("LiquidityMining", () => { unlockedImmediatelyPercent ); await upgradeLiquidityMining(); - await expectRevert(liquidityMining.initialize(ZERO_ADDRESS), "Invalid address"); + await expectRevert(liquidityMining.setLiquidityMiningV2Address(ZERO_ADDRESS), "Invalid address"); }); it("fails if unlockedImmediatelyPercent >= 10000", async () => { diff --git a/tests/farm/LiquidityMiningV2.js b/tests/farm/LiquidityMiningV2.js index 8aa5ce9b2..d2b7a0026 100644 --- a/tests/farm/LiquidityMiningV2.js +++ b/tests/farm/LiquidityMiningV2.js @@ -70,7 +70,7 @@ describe("LiquidityMiningV2", () => { await deployLiquidityMiningV2(); - await liquidityMiningV1.initialize(liquidityMining.address); + await liquidityMiningV1.setLiquidityMiningV2Address(liquidityMining.address); migrator = await Migrator.new(); await migrator.initialize( @@ -2225,7 +2225,7 @@ describe("LiquidityMiningV2", () => { await deployLiquidityMiningV2(); - await liquidityMiningV1.initialize(liquidityMining.address); + await liquidityMiningV1.setLiquidityMiningV2Address(liquidityMining.address); migrator = await Migrator.new(); await migrator.initialize( diff --git a/tests/loan-token/LendingWithLMV2.test.js b/tests/loan-token/LendingWithLMV2.test.js index 4b8342d01..95a384ae0 100644 --- a/tests/loan-token/LendingWithLMV2.test.js +++ b/tests/loan-token/LendingWithLMV2.test.js @@ -99,7 +99,7 @@ contract("LoanTokenLogicLM", (accounts) => { await deployLiquidityMiningV2(); - await liquidityMiningV1.initialize(liquidityMining.address); + await liquidityMiningV1.setLiquidityMiningV2Address(liquidityMining.address); migrator = await Migrator.new(); await migrator.initialize( From bc4b08cd3b468d93b840474ad65d8d5370a24d08 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 19 May 2022 17:04:06 +0530 Subject: [PATCH 13/17] Ran prettier --- contracts/farm/LMV1toLMV2Migrator.sol | 7 +-- contracts/farm/LiquidityMiningV1.sol | 31 +++++------ contracts/farm/LiquidityMiningV2.sol | 75 ++++++++++++-------------- tests/farm/LiquidityMiningMigration.js | 5 +- tests/farm/LiquidityMiningV1.js | 5 +- 5 files changed, 56 insertions(+), 67 deletions(-) diff --git a/contracts/farm/LMV1toLMV2Migrator.sol b/contracts/farm/LMV1toLMV2Migrator.sol index 1aa6cecb5..bc2fe271e 100644 --- a/contracts/farm/LMV1toLMV2Migrator.sol +++ b/contracts/farm/LMV1toLMV2Migrator.sol @@ -11,12 +11,7 @@ import "./ILiquidityMiningV2.sol"; contract LMV1toLMV2Migrator is AdminRole { using SafeMath for uint256; using SafeERC20 for IERC20; - enum MigrationStates { - MigratingPools, - MigratingUsers, - MigratingFunds, - MigrationFinished - } + enum MigrationStates { MigratingPools, MigratingUsers, MigratingFunds, MigrationFinished } //represents de migration state from LiquidityMiningV1 to LiquidityMiningV2 MigrationStates public migrationState; diff --git a/contracts/farm/LiquidityMiningV1.sol b/contracts/farm/LiquidityMiningV1.sol index 7f1e84625..e6891ef7e 100644 --- a/contracts/farm/LiquidityMiningV1.sol +++ b/contracts/farm/LiquidityMiningV1.sol @@ -344,12 +344,8 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { PoolInfo storage pool = poolInfoList[poolId]; uint256 start = block.number; uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward( - pool, - _amount, - start, - end - ); + (, uint256 accumulatedRewardPerShare) = + _getPoolAccumulatedReward(pool, _amount, start, end); return _amount.mul(accumulatedRewardPerShare).div(PRECISION); } @@ -387,10 +383,8 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { return; } - ( - uint256 accumulatedReward_, - uint256 accumulatedRewardPerShare_ - ) = _getPoolAccumulatedReward(pool); + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool); pool.accumulatedRewardPerShare = pool.accumulatedRewardPerShare.add( accumulatedRewardPerShare_ ); @@ -414,10 +408,10 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { uint256 _endBlock ) internal view returns (uint256, uint256) { uint256 passedBlocks = _getPassedBlocksWithBonusMultiplier(_startBlock, _endBlock); - uint256 accumulatedReward = passedBlocks - .mul(rewardTokensPerBlock) - .mul(_pool.allocationPoint) - .div(totalAllocationPoint); + uint256 accumulatedReward = + passedBlocks.mul(rewardTokensPerBlock).mul(_pool.allocationPoint).div( + totalAllocationPoint + ); uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); poolTokenBalance = poolTokenBalance.add(_additionalAmount); @@ -589,11 +583,10 @@ contract LiquidityMiningV1 is ILiquidityMiningV1, LiquidityMiningStorageV1 { //update user accumulated reward if (user.amount > 0) { //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = user - .amount - .mul(pool.accumulatedRewardPerShare) - .div(PRECISION) - .sub(user.rewardDebt); + uint256 accumulatedReward = + user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION).sub( + user.rewardDebt + ); user.accumulatedReward = user.accumulatedReward.add(accumulatedReward); } } diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index 6e0df3431..7408081c9 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -286,9 +286,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { ) internal { uint256 poolId = _getPoolId(_poolToken); RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolInfoRewardToken = + poolInfoRewardTokensMap[poolId][_rewardToken]; uint96 previousAllocationPoint = poolInfoRewardToken.allocationPoint; rewardToken.totalAllocationPoint = rewardToken @@ -362,19 +361,15 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _user ) internal view returns (uint256) { PoolInfo storage pool = poolInfoList[_poolId]; - PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolRewardToken = + poolInfoRewardTokensMap[_poolId][_rewardToken]; UserInfo storage user = userInfoMap[_poolId][_user]; uint256 accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare; uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); if (block.number > poolRewardToken.lastRewardBlock && poolTokenBalance != 0) { - (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward( - pool, - poolRewardToken, - rewardTokensMap[_rewardToken] - ); + (, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool, poolRewardToken, rewardTokensMap[_rewardToken]); accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); } return @@ -415,14 +410,15 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { PoolInfo storage pool = poolInfoList[poolId]; uint256 start = block.number; uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward( - pool, - _amount, - rewardTokensMap[_rewardToken], - poolInfoRewardTokensMap[poolId][_rewardToken], - start, - end - ); + (, uint256 accumulatedRewardPerShare) = + _getPoolAccumulatedReward( + pool, + _amount, + rewardTokensMap[_rewardToken], + poolInfoRewardTokensMap[poolId][_rewardToken], + start, + end + ); return _amount.mul(accumulatedRewardPerShare).div(PRECISION); } @@ -460,9 +456,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _poolId, address _rewardToken ) internal { - PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolRewardToken = + poolInfoRewardTokensMap[_poolId][_rewardToken]; // this pool has been updated recently if (block.number <= poolRewardToken.lastRewardBlock) { return; @@ -475,10 +470,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { } RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - ( - uint256 accumulatedReward_, - uint256 accumulatedRewardPerShare_ - ) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); poolRewardToken.accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare.add( accumulatedRewardPerShare_ ); @@ -522,11 +515,12 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _endBlock ) internal view returns (uint256, uint256) { uint256 passedBlocks = _getPassedBlocks(_rewardToken, _startBlock, _endBlock); - uint256 accumulatedReward = passedBlocks - .mul(_rewardToken.rewardTokensPerBlock) - .mul(PRECISION) - .mul(_poolRewardToken.allocationPoint) - .div(_rewardToken.totalAllocationPoint); + uint256 accumulatedReward = + passedBlocks + .mul(_rewardToken.rewardTokensPerBlock) + .mul(PRECISION) + .mul(_poolRewardToken.allocationPoint) + .div(_rewardToken.totalAllocationPoint); uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); poolTokenBalance = poolTokenBalance.add(_additionalAmount); @@ -735,13 +729,15 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { //update user accumulated reward if (user.amount > 0) { //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = user - .amount - .mul( - poolInfoRewardTokensMap[_poolId][_rewardTokenAddress].accumulatedRewardPerShare + uint256 accumulatedReward = + user + .amount + .mul( + poolInfoRewardTokensMap[_poolId][_rewardTokenAddress] + .accumulatedRewardPerShare ) - .div(PRECISION) - .sub(reward.rewardDebt); + .div(PRECISION) + .sub(reward.rewardDebt); reward.accumulatedReward = reward.accumulatedReward.add(accumulatedReward); } } @@ -1045,9 +1041,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _accumulatedRewardPerShare ) external onlyMigrator { uint256 poolId = _getPoolId(_poolToken); - PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolInfoRewardToken = + poolInfoRewardTokensMap[poolId][_rewardToken]; poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; poolInfoRewardToken.accumulatedRewardPerShare = _accumulatedRewardPerShare; } diff --git a/tests/farm/LiquidityMiningMigration.js b/tests/farm/LiquidityMiningMigration.js index 5bd4b47c1..4633231a1 100644 --- a/tests/farm/LiquidityMiningMigration.js +++ b/tests/farm/LiquidityMiningMigration.js @@ -199,7 +199,10 @@ describe("LiquidityMiningMigration", () => { ); await upgradeLiquidityMining(); - await expectRevert(liquidityMining.setLiquidityMiningV2Address(ZERO_ADDRESS), "Invalid address"); + await expectRevert( + liquidityMining.setLiquidityMiningV2Address(ZERO_ADDRESS), + "Invalid address" + ); }); it("should fail if SOV address is invalid in migrator contract", async () => { migrator = await Migrator.new(); diff --git a/tests/farm/LiquidityMiningV1.js b/tests/farm/LiquidityMiningV1.js index d0da1d42c..a4b090e3f 100644 --- a/tests/farm/LiquidityMiningV1.js +++ b/tests/farm/LiquidityMiningV1.js @@ -215,7 +215,10 @@ describe("LiquidityMining", () => { unlockedImmediatelyPercent ); await upgradeLiquidityMining(); - await expectRevert(liquidityMining.setLiquidityMiningV2Address(ZERO_ADDRESS), "Invalid address"); + await expectRevert( + liquidityMining.setLiquidityMiningV2Address(ZERO_ADDRESS), + "Invalid address" + ); }); it("fails if unlockedImmediatelyPercent >= 10000", async () => { From 99c5b52a66009b1e3ecbf19c365e5fe2f8f67d97 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 23 May 2022 15:41:02 +0530 Subject: [PATCH 14/17] Added a new function and fixed coverage issues --- .github/workflows/node.js.yml | 6 +- contracts/farm/LiquidityMiningV2.sol | 124 +++++++++++++++++++-------- package.json | 4 +- 3 files changed, 91 insertions(+), 43 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 6f7eb348e..38cf5abc3 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -14,8 +14,7 @@ jobs: matrix: node-version: [12.x, 14.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - env: - NODE_OPTIONS: "--max-old-space-size=8192" + steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -48,7 +47,8 @@ jobs: run: npm run lint && npm run prettier && npm run prettier-check - name: Running Test run: npm run test - + # - name: Increasing the memory + # run: export NODE_OPTIONS="--max-old-space-size=7168" # - name: Code Coverage # run: npm run coverage # - name: Coveralls GitHub Action diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index 7408081c9..fc8503dca 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -208,17 +208,60 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { poolIdList[_poolToken] = poolInfoList.length; for (uint256 i = 0; i < _rewardTokens.length; i++) { - associatePoolToRewardToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); + _associatePoolToRewardToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); } emit PoolTokenAdded(msg.sender, _poolToken, _rewardTokens, _allocationPoints); } - function associatePoolToRewardToken( + /** + * @notice adds reward tokens to an existing pool. Can only be called by the owner or an admin. + * @param _poolToken the address of pool token + * @param _rewardTokens the addresses of reward tokens for given pool + * @param _allocationPoints the allocation points (weight) for the given pool and each reward token + */ + function addTokensToExistingPool( + address _poolToken, + address[] calldata _rewardTokens, + uint96[] calldata _allocationPoints, + bool _withUpdate + ) external onlyAuthorized { + require(_rewardTokens.length > 0, "Invalid reward tokens length"); + require( + _rewardTokens.length == _allocationPoints.length, + "Invalid allocation points length" + ); + require(_poolToken != address(0), "Invalid token address"); + require(poolIdList[_poolToken] == 0, "Token already added"); + + if (_withUpdate) { + updateAllPools(); + } + + uint256 _poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[_poolId]; + //indexing starts from 1 in order to check whether token was already added + poolIdList[_poolToken] = poolInfoList.length; + + for (uint256 i = 0; i < _rewardTokens.length; i++) { + pool.rewardTokens.push(_rewardTokens[i]); + _associatePoolToRewardToken(_poolToken, _rewardTokens[i], _allocationPoints[i]); + } + + emit PoolTokenAdded(msg.sender, _poolToken, _rewardTokens, _allocationPoints); + } + + /** + * @notice associate Pool to RewardToken. Can only be called by the owner or an admin + * @param _poolToken the address of pool token + * @param _rewardToken the addresses of reward tokens for given pool + * @param _allocationPoint the allocation points (weight) for the given pool and each reward token + */ + function _associatePoolToRewardToken( address _poolToken, address _rewardToken, uint96 _allocationPoint - ) public onlyAuthorized { + ) internal { uint256 poolId = _getPoolId(_poolToken); // Allocation point checks @@ -286,8 +329,9 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { ) internal { uint256 poolId = _getPoolId(_poolToken); RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - PoolInfoRewardToken storage poolInfoRewardToken = - poolInfoRewardTokensMap[poolId][_rewardToken]; + PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ + _rewardToken + ]; uint96 previousAllocationPoint = poolInfoRewardToken.allocationPoint; rewardToken.totalAllocationPoint = rewardToken @@ -361,15 +405,19 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _user ) internal view returns (uint256) { PoolInfo storage pool = poolInfoList[_poolId]; - PoolInfoRewardToken storage poolRewardToken = - poolInfoRewardTokensMap[_poolId][_rewardToken]; + PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ + _rewardToken + ]; UserInfo storage user = userInfoMap[_poolId][_user]; uint256 accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare; uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); if (block.number > poolRewardToken.lastRewardBlock && poolTokenBalance != 0) { - (, uint256 accumulatedRewardPerShare_) = - _getPoolAccumulatedReward(pool, poolRewardToken, rewardTokensMap[_rewardToken]); + (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward( + pool, + poolRewardToken, + rewardTokensMap[_rewardToken] + ); accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); } return @@ -410,15 +458,14 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { PoolInfo storage pool = poolInfoList[poolId]; uint256 start = block.number; uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = - _getPoolAccumulatedReward( - pool, - _amount, - rewardTokensMap[_rewardToken], - poolInfoRewardTokensMap[poolId][_rewardToken], - start, - end - ); + (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward( + pool, + _amount, + rewardTokensMap[_rewardToken], + poolInfoRewardTokensMap[poolId][_rewardToken], + start, + end + ); return _amount.mul(accumulatedRewardPerShare).div(PRECISION); } @@ -456,8 +503,9 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _poolId, address _rewardToken ) internal { - PoolInfoRewardToken storage poolRewardToken = - poolInfoRewardTokensMap[_poolId][_rewardToken]; + PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ + _rewardToken + ]; // this pool has been updated recently if (block.number <= poolRewardToken.lastRewardBlock) { return; @@ -470,8 +518,10 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { } RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = - _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + ( + uint256 accumulatedReward_, + uint256 accumulatedRewardPerShare_ + ) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); poolRewardToken.accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare.add( accumulatedRewardPerShare_ ); @@ -515,12 +565,11 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _endBlock ) internal view returns (uint256, uint256) { uint256 passedBlocks = _getPassedBlocks(_rewardToken, _startBlock, _endBlock); - uint256 accumulatedReward = - passedBlocks - .mul(_rewardToken.rewardTokensPerBlock) - .mul(PRECISION) - .mul(_poolRewardToken.allocationPoint) - .div(_rewardToken.totalAllocationPoint); + uint256 accumulatedReward = passedBlocks + .mul(_rewardToken.rewardTokensPerBlock) + .mul(PRECISION) + .mul(_poolRewardToken.allocationPoint) + .div(_rewardToken.totalAllocationPoint); uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); poolTokenBalance = poolTokenBalance.add(_additionalAmount); @@ -729,15 +778,13 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { //update user accumulated reward if (user.amount > 0) { //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = - user - .amount - .mul( - poolInfoRewardTokensMap[_poolId][_rewardTokenAddress] - .accumulatedRewardPerShare + uint256 accumulatedReward = user + .amount + .mul( + poolInfoRewardTokensMap[_poolId][_rewardTokenAddress].accumulatedRewardPerShare ) - .div(PRECISION) - .sub(reward.rewardDebt); + .div(PRECISION) + .sub(reward.rewardDebt); reward.accumulatedReward = reward.accumulatedReward.add(accumulatedReward); } } @@ -1041,8 +1088,9 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _accumulatedRewardPerShare ) external onlyMigrator { uint256 poolId = _getPoolId(_poolToken); - PoolInfoRewardToken storage poolInfoRewardToken = - poolInfoRewardTokensMap[poolId][_rewardToken]; + PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ + _rewardToken + ]; poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; poolInfoRewardToken.accumulatedRewardPerShare = _accumulatedRewardPerShare; } diff --git a/package.json b/package.json index 1ce1368d8..98baf839d 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "sol2uml": "^1.1.17", "solhint": "^3.0.0", "solhint-plugin-prettier": "^0.0.5", - "solidity-coverage": "^0.7.13", + "solidity-coverage": "^0.7.17", "solparse": "^2.2.8", "truffle": "^5.1.60", "web3": "^1.3.1" @@ -68,7 +68,7 @@ "scripts": { "analyze-contracts": "slither .", "contract-size": "yarn run hardhat size-contracts", - "coverage": "env COVERAGE=true npx hardhat coverage", + "coverage": "npx hardhat coverage", "doc": "yarn run hardhat docgen", "lint": "npm run lint-sol && npm run lint-js", "lint-sol": "solhint contracts/{*,**/*,**/**/*,**/**/**/*,**/**/**/**/*}.sol", From cd27d298651c8d48c0faad904ea996078c2b37c8 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 23 May 2022 15:42:00 +0530 Subject: [PATCH 15/17] Ran prettier --- contracts/farm/LiquidityMiningV2.sol | 75 +++++++++++++--------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/contracts/farm/LiquidityMiningV2.sol b/contracts/farm/LiquidityMiningV2.sol index fc8503dca..24456e084 100644 --- a/contracts/farm/LiquidityMiningV2.sol +++ b/contracts/farm/LiquidityMiningV2.sol @@ -329,9 +329,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { ) internal { uint256 poolId = _getPoolId(_poolToken); RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolInfoRewardToken = + poolInfoRewardTokensMap[poolId][_rewardToken]; uint96 previousAllocationPoint = poolInfoRewardToken.allocationPoint; rewardToken.totalAllocationPoint = rewardToken @@ -405,19 +404,15 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { address _user ) internal view returns (uint256) { PoolInfo storage pool = poolInfoList[_poolId]; - PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolRewardToken = + poolInfoRewardTokensMap[_poolId][_rewardToken]; UserInfo storage user = userInfoMap[_poolId][_user]; uint256 accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare; uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); if (block.number > poolRewardToken.lastRewardBlock && poolTokenBalance != 0) { - (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward( - pool, - poolRewardToken, - rewardTokensMap[_rewardToken] - ); + (, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool, poolRewardToken, rewardTokensMap[_rewardToken]); accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); } return @@ -458,14 +453,15 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { PoolInfo storage pool = poolInfoList[poolId]; uint256 start = block.number; uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward( - pool, - _amount, - rewardTokensMap[_rewardToken], - poolInfoRewardTokensMap[poolId][_rewardToken], - start, - end - ); + (, uint256 accumulatedRewardPerShare) = + _getPoolAccumulatedReward( + pool, + _amount, + rewardTokensMap[_rewardToken], + poolInfoRewardTokensMap[poolId][_rewardToken], + start, + end + ); return _amount.mul(accumulatedRewardPerShare).div(PRECISION); } @@ -503,9 +499,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _poolId, address _rewardToken ) internal { - PoolInfoRewardToken storage poolRewardToken = poolInfoRewardTokensMap[_poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolRewardToken = + poolInfoRewardTokensMap[_poolId][_rewardToken]; // this pool has been updated recently if (block.number <= poolRewardToken.lastRewardBlock) { return; @@ -518,10 +513,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { } RewardToken storage rewardToken = rewardTokensMap[_rewardToken]; - ( - uint256 accumulatedReward_, - uint256 accumulatedRewardPerShare_ - ) = _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool, poolRewardToken, rewardToken); poolRewardToken.accumulatedRewardPerShare = poolRewardToken.accumulatedRewardPerShare.add( accumulatedRewardPerShare_ ); @@ -565,11 +558,12 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _endBlock ) internal view returns (uint256, uint256) { uint256 passedBlocks = _getPassedBlocks(_rewardToken, _startBlock, _endBlock); - uint256 accumulatedReward = passedBlocks - .mul(_rewardToken.rewardTokensPerBlock) - .mul(PRECISION) - .mul(_poolRewardToken.allocationPoint) - .div(_rewardToken.totalAllocationPoint); + uint256 accumulatedReward = + passedBlocks + .mul(_rewardToken.rewardTokensPerBlock) + .mul(PRECISION) + .mul(_poolRewardToken.allocationPoint) + .div(_rewardToken.totalAllocationPoint); uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); poolTokenBalance = poolTokenBalance.add(_additionalAmount); @@ -778,13 +772,15 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { //update user accumulated reward if (user.amount > 0) { //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = user - .amount - .mul( - poolInfoRewardTokensMap[_poolId][_rewardTokenAddress].accumulatedRewardPerShare + uint256 accumulatedReward = + user + .amount + .mul( + poolInfoRewardTokensMap[_poolId][_rewardTokenAddress] + .accumulatedRewardPerShare ) - .div(PRECISION) - .sub(reward.rewardDebt); + .div(PRECISION) + .sub(reward.rewardDebt); reward.accumulatedReward = reward.accumulatedReward.add(accumulatedReward); } } @@ -1088,9 +1084,8 @@ contract LiquidityMiningV2 is ILiquidityMiningV2, LiquidityMiningStorageV2 { uint256 _accumulatedRewardPerShare ) external onlyMigrator { uint256 poolId = _getPoolId(_poolToken); - PoolInfoRewardToken storage poolInfoRewardToken = poolInfoRewardTokensMap[poolId][ - _rewardToken - ]; + PoolInfoRewardToken storage poolInfoRewardToken = + poolInfoRewardTokensMap[poolId][_rewardToken]; poolInfoRewardToken.lastRewardBlock = _lastRewardBlock; poolInfoRewardToken.accumulatedRewardPerShare = _accumulatedRewardPerShare; } From 97f2098a9505af9c8ec676b5d859a62197f79a89 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 23 May 2022 15:53:48 +0530 Subject: [PATCH 16/17] Modified lock file --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f3b5908b..9be85a57e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "sol2uml": "^1.1.17", "solhint": "^3.0.0", "solhint-plugin-prettier": "^0.0.5", - "solidity-coverage": "^0.7.13", + "solidity-coverage": "^0.7.17", "solparse": "^2.2.8", "truffle": "^5.1.60", "web3": "^1.3.1" @@ -82525,4 +82525,4 @@ } } } -} +} \ No newline at end of file From 5add9b2b83f3a6f0f5ed4dcfdd60964c61e958a7 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 23 May 2022 15:58:03 +0530 Subject: [PATCH 17/17] Modified all versions for coverage --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9be85a57e..7845f5180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39811,8 +39811,8 @@ "dev": true }, "node_modules/solidity-coverage": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.7.16.tgz", + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.7.17.tgz", "integrity": "sha512-ttBOStywE6ZOTJmmABSg4b8pwwZfYKG8zxu40Nz+sRF5bQX7JULXWj/XbX0KXps3Fsp8CJXg8P29rH3W54ipxw==", "dev": true, "dependencies": { @@ -78319,8 +78319,8 @@ "dev": true }, "solidity-coverage": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.7.16.tgz", + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.7.17.tgz", "integrity": "sha512-ttBOStywE6ZOTJmmABSg4b8pwwZfYKG8zxu40Nz+sRF5bQX7JULXWj/XbX0KXps3Fsp8CJXg8P29rH3W54ipxw==", "dev": true, "requires": {