// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.7.6; pragma abicoder v2; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../../periphery/WithdrawableV2.sol"; import "../../interfaces/IPendleLiquidityMiningV2Multi.sol"; import "../../interfaces/IPendlePausingManager.sol"; import "../../interfaces/IPendleWhitelist.sol"; import "../../libraries/MathLib.sol"; import "../../libraries/TokenUtilsLib.sol"; import "../PendleSimpleERC20TokenHolder.sol"; /* - stakeToken is the token to be used to stake into this contract to receive rewards - yieldTokens are tokens generated by stakeToken while it's being staked. For example, Sushi's LP token generates SUSHI(if it's in Onsen program), or Pendle's Aave LP generates aToken - If there is no yieldTokens, it should be set to address(0) to save gas */ contract PendleLiquidityMiningBaseV2Multi is IPendleLiquidityMiningV2Multi, WithdrawableV2, ReentrancyGuard { using Math for uint256; using SafeMath for uint256; using PairTokensLib for PairUints; using PairTokensLib for PairTokens; using TokenUtils for IERC20; struct ConstructorArgs { address governanceManager; address pausingManager; address whitelist; PairTokens rewardTokens; address stakeToken; PairTokens yieldTokens; uint256 startTime; uint256 epochDuration; uint256 vestingEpochs; IWETH weth; } struct EpochData { uint256 totalStakeUnits; PairUints totalRewards; uint256 lastUpdated; mapping(address => uint256) stakeUnitsForUser; mapping(address => PairUints) availableRewardsForUser; } IPendleWhitelist public immutable whitelist; IPendlePausingManager public immutable pausingManager; IWETH public immutable weth; uint256 public override numberOfEpochs; uint256 public override totalStake; mapping(uint256 => EpochData) internal epochData; mapping(address => uint256) public override balances; mapping(address => uint256) public lastTimeUserStakeUpdated; mapping(address => uint256) public lastEpochClaimed; PairTokens public rewardTokens; address public immutable override stakeToken; PairTokens public yieldTokens; address public immutable rewardTokensHolder; uint256 public immutable override startTime; uint256 public immutable override epochDuration; uint256 public immutable override vestingEpochs; uint256 public constant MULTIPLIER = 10**20; // yieldTokens-related mapping(address => PairUints) public dueInterests; mapping(address => PairUints) public lastParamL; PairUints public lastNYield; PairUints public paramL; modifier hasStarted() { require(getCurrentEpochId() > 0, "NOT_STARTED"); _; } modifier nonContractOrWhitelisted() { bool isEOA = !Address.isContract(msg.sender) && tx.origin == msg.sender; require(isEOA || whitelist.whitelisted(msg.sender), "CONTRACT_NOT_WHITELISTED"); _; } modifier isUserAllowedToUse() { (bool paused, ) = pausingManager.checkLiqMiningStatus(address(this)); require(!paused, "LIQ_MINING_PAUSED"); require(numberOfEpochs > 0, "NOT_FUNDED"); require(getCurrentEpochId() > 0, "NOT_STARTED"); _; } constructor(ConstructorArgs memory args) PermissionsV2(args.governanceManager) { require(args.startTime > block.timestamp, "INVALID_START_TIME"); require(args.vestingEpochs > 0, "INVALID_VESTING_EPOCHS"); args.rewardTokens.verify(); args.yieldTokens.verify(); weth = args.weth; pausingManager = IPendlePausingManager(args.pausingManager); whitelist = IPendleWhitelist(args.whitelist); rewardTokens = args.rewardTokens; stakeToken = args.stakeToken; yieldTokens = args.yieldTokens; startTime = args.startTime; epochDuration = args.epochDuration; vestingEpochs = args.vestingEpochs; paramL = PairUints(1, 1); rewardTokensHolder = address( new PendleSimpleERC20TokenHolder(args.rewardTokens.toArr(), address(this)) ); } /** @notice set up emergencyMode by pulling all tokens back to this contract & approve spender to spend infinity amount */ function setUpEmergencyMode(address spender, bool) external virtual override { (, bool emergencyMode) = pausingManager.checkLiqMiningStatus(address(this)); require(emergencyMode, "NOT_EMERGENCY"); (address liqMiningEmergencyHandler, , ) = pausingManager.liqMiningEmergencyHandler(); require(msg.sender == liqMiningEmergencyHandler, "NOT_EMERGENCY_HANDLER"); //pulling our rewardTokens back rewardTokens.safeTransferFrom( rewardTokensHolder, address(this), rewardTokens.balanceOf(rewardTokensHolder) ); rewardTokens.infinityApprove(spender); IERC20(stakeToken).safeApprove(spender, type(uint256).max); yieldTokens.infinityApprove(spender); } /** @notice create new epochs & fund rewards for them @dev same logic as the function in V1 */ function fund(PairUints[] calldata rewards) external virtual override onlyGovernance { // Once the program is over, it cannot be extended require(getCurrentEpochId() <= numberOfEpochs, "LAST_EPOCH_OVER"); uint256 nNewEpochs = rewards.length; PairUints memory totalFunded; // all the funding will be used for new epochs for (uint256 i = 0; i < nNewEpochs; i++) { totalFunded = totalFunded.add(rewards[i]); epochData[numberOfEpochs + i + 1].totalRewards = rewards[i]; } numberOfEpochs = numberOfEpochs.add(nNewEpochs); rewardTokens.safeTransferFrom(msg.sender, rewardTokensHolder, totalFunded); emit Funded(rewards, numberOfEpochs); } /** @notice top up rewards of exisiting epochs @dev almost same logic as the function in V1 without the redundant isFunded check */ function topUpRewards(uint256[] calldata epochIds, PairUints[] calldata rewards) external virtual override onlyGovernance { require(epochIds.length == rewards.length, "INVALID_ARRAYS"); uint256 curEpoch = getCurrentEpochId(); uint256 endEpoch = numberOfEpochs; PairUints memory totalTopUp; for (uint256 i = 0; i < epochIds.length; i++) { require(curEpoch < epochIds[i] && epochIds[i] <= endEpoch, "INVALID_EPOCH_ID"); totalTopUp = totalTopUp.add(rewards[i]); epochData[epochIds[i]].totalRewards = epochData[epochIds[i]].totalRewards.add( rewards[i] ); } rewardTokens.safeTransferFrom(msg.sender, rewardTokensHolder, totalTopUp); emit RewardsToppedUp(epochIds, rewards); } /** @notice stake tokens in to receive rewards. It's allowed to stake for others @param forAddr the address to stake for @dev all staking data will be updated for `forAddr`, but msg.sender will be the one transferring tokens in */ function stake(address forAddr, uint256 amount) external virtual override nonReentrant nonContractOrWhitelisted isUserAllowedToUse { require(forAddr != address(0), "ZERO_ADDRESS"); require(amount != 0, "ZERO_AMOUNT"); require(getCurrentEpochId() <= numberOfEpochs, "INCENTIVES_PERIOD_OVER"); _settleStake(forAddr, msg.sender, amount); emit Staked(forAddr, amount); } /** @notice withdraw tokens from the staking contract. It's allowed to withdraw to an address different from msg.sender @param toAddr the address to receive all tokens @dev all staking data will be updated for msg.sender, but `toAddr` will be the one receiving all tokens */ function withdraw(address toAddr, uint256 amount) external virtual override nonReentrant isUserAllowedToUse { require(amount != 0, "ZERO_AMOUNT"); require(toAddr != address(0), "ZERO_ADDRESS"); _settleWithdraw(msg.sender, toAddr, amount); emit Withdrawn(msg.sender, amount); } /** @notice redeem all available rewards from expired epochs. It's allowed to redeem for others @param user the address whose data will be updated & receive rewards */ function redeemRewards(address user) external virtual override nonReentrant isUserAllowedToUse returns (PairUints memory rewards) { require(user != address(0), "ZERO_ADDRESS"); rewards = _beforeTransferPendingRewards(user); rewardTokens.safeTransferFrom(rewardTokensHolder, user, rewards); } /** @notice redeem all due interests. It's allowed to redeem for others @param user the address whose data will be updated & receive due interests */ function redeemDueInterests(address user) external virtual override nonReentrant isUserAllowedToUse returns (PairUints memory amountOut) { if (yieldTokens.allZero()) return amountOut; require(user != address(0), "ZERO_ADDRESS"); amountOut = _beforeTransferDueInterests(user); amountOut = _pushYieldToken(user, amountOut); } function updateAndReadEpochData(uint256 epochId, address user) external override nonReentrant isUserAllowedToUse returns ( uint256 totalStakeUnits, PairUints memory totalRewards, uint256 lastUpdated, uint256 stakeUnitsForUser, PairUints memory availableRewardsForUser ) { _updatePendingRewards(user); return readEpochData(epochId, user); } function readYieldTokens() external view override returns (PairTokens memory tokens) { tokens = yieldTokens; } function readRewardTokens() external view override returns (PairTokens memory tokens) { tokens = rewardTokens; } function readEpochData(uint256 epochId, address user) public view override returns ( uint256 totalStakeUnits, PairUints memory totalRewards, uint256 lastUpdated, uint256 stakeUnitsForUser, PairUints memory availableRewardsForUser ) { totalStakeUnits = epochData[epochId].totalStakeUnits; totalRewards = epochData[epochId].totalRewards; lastUpdated = epochData[epochId].lastUpdated; stakeUnitsForUser = epochData[epochId].stakeUnitsForUser[user]; availableRewardsForUser = epochData[epochId].availableRewardsForUser[user]; } function getCurrentEpochId() public view returns (uint256) { return _epochOfTimestamp(block.timestamp); } /** @notice update all reward-related data for user @dev to be called before user's stakeToken balance changes @dev same logic as the function in V1 */ function _updatePendingRewards(address user) internal virtual { _updateStakeData(); // user has not staked before, no need to do anything if (lastTimeUserStakeUpdated[user] == 0) { lastTimeUserStakeUpdated[user] = block.timestamp; return; } uint256 _curEpoch = getCurrentEpochId(); uint256 _endEpoch = Math.min(numberOfEpochs, _curEpoch); // if _curEpoch<=numberOfEpochs => the endEpoch hasn't ended yet (since endEpoch=curEpoch) bool _isEndEpochOver = (_curEpoch > numberOfEpochs); // caching uint256 _balance = balances[user]; uint256 _lastTimeUserStakeUpdated = lastTimeUserStakeUpdated[user]; uint256 _totalStake = totalStake; uint256 _startEpoch = _epochOfTimestamp(_lastTimeUserStakeUpdated); // Go through all epochs until now to update stakeUnitsForUser and availableRewardsForEpoch for (uint256 epochId = _startEpoch; epochId <= _endEpoch; epochId++) { if (epochData[epochId].totalStakeUnits == 0) { // in the extreme case of zero staked tokens for this expiry even now, // => nothing to do from this epoch onwards if (_totalStake == 0) break; // nobody stakes anything in this epoch continue; } // updating stakeUnits for users. The logic of this is similar to _updateStakeDataForExpiry epochData[epochId].stakeUnitsForUser[user] = epochData[epochId] .stakeUnitsForUser[user] .add(_calcUnitsStakeInEpoch(_balance, _lastTimeUserStakeUpdated, epochId)); // all epochs prior to the endEpoch must have ended // if epochId == _endEpoch, we must check if the epoch has ended or not if (epochId == _endEpoch && !_isEndEpochOver) { break; } // Now this epoch has ended,let's distribute its reward to this user // calc the amount of rewards the user is eligible to receive from this epoch PairUints memory rewardsPerVestingEpoch = _calcAmountRewardsForUserInEpoch( user, epochId ); // Now we distribute this rewards over the vestingEpochs starting from epochId + 1 // to epochId + vestingEpochs for (uint256 i = epochId + 1; i <= epochId + vestingEpochs; i++) { epochData[i].availableRewardsForUser[user] = epochData[i] .availableRewardsForUser[user] .add(rewardsPerVestingEpoch); } } lastTimeUserStakeUpdated[user] = block.timestamp; } /** @notice update staking data for the current epoch @dev same logic as the function in V1 */ function _updateStakeData() internal virtual { uint256 _curEpoch = getCurrentEpochId(); // loop through all epochData in descending order for (uint256 i = Math.min(_curEpoch, numberOfEpochs); i > 0; i--) { uint256 epochEndTime = _endTimeOfEpoch(i); uint256 lastUpdatedForEpoch = epochData[i].lastUpdated; if (lastUpdatedForEpoch == epochEndTime) { break; // its already updated until this epoch, our job here is done } // if the epoch hasn't been fully updated yet, we will update it // just add the amount of units contributed by users since lastUpdatedForEpoch -> now // by calling _calcUnitsStakeInEpoch epochData[i].totalStakeUnits = epochData[i].totalStakeUnits.add( _calcUnitsStakeInEpoch(totalStake, lastUpdatedForEpoch, i) ); // If the epoch has ended, lastUpdated = epochEndTime // If not yet, lastUpdated = block.timestamp (aka now) epochData[i].lastUpdated = Math.min(block.timestamp, epochEndTime); } } /** @notice update all interest-related data for user @dev to be called before user's stakeToken balance changes or when user wants to update his interests @dev same logic as the function in CompoundLiquidityMiningV1 */ function _updateDueInterests(address user) internal virtual { if (yieldTokens.allZero()) return; _updateParamL(); if (lastParamL[user].allZero()) { lastParamL[user] = paramL; return; } uint256 principal = balances[user]; PairUints memory interestValuePerStakeToken = paramL.sub(lastParamL[user]); PairUints memory interestFromStakeToken = interestValuePerStakeToken.mul(principal).div( MULTIPLIER ); dueInterests[user] = dueInterests[user].add(interestFromStakeToken); lastParamL[user] = paramL; } /** @notice update paramL, lastNYield & redeem interest from external sources @dev to be called only from _updateDueInterests @dev same logic as the function in V1 */ function _updateParamL() internal virtual { if (yieldTokens.allZero() || !_checkNeedUpdateParamL()) return; _redeemExternalInterests(); PairUints memory currentNYield = yieldTokens.balanceOf(address(this)); (PairUints memory firstTerm, PairUints memory paramR) = _getFirstTermAndParamR( currentNYield ); PairUints memory secondTerm; if (totalStake != 0) secondTerm = paramR.mul(MULTIPLIER).div(totalStake); // Update new states paramL = firstTerm.add(secondTerm); lastNYield = currentNYield; } /** @dev same logic as the function in CompoundLiquidityMining @dev to be called only from _updateParamL */ function _getFirstTermAndParamR(PairUints memory currentNYield) internal virtual returns (PairUints memory firstTerm, PairUints memory paramR) { firstTerm = paramL; paramR = currentNYield.sub(lastNYield); } /** @dev function is empty because by default yieldTokens==0 */ function _checkNeedUpdateParamL() internal virtual returns (bool) {} /** @dev function is empty because by default yieldTokens==0 */ function _redeemExternalInterests() internal virtual {} /** @notice Calc the amount of rewards that the user can receive now & clear all the pending rewards @dev To be called before any rewards is transferred out @dev same logic as the function in V1 */ function _beforeTransferPendingRewards(address user) internal virtual returns (PairUints memory amountOut) { _updatePendingRewards(user); uint256 _lastEpoch = Math.min(getCurrentEpochId(), numberOfEpochs + vestingEpochs); for (uint256 i = lastEpochClaimed[user]; i <= _lastEpoch; i++) { if (!epochData[i].availableRewardsForUser[user].allZero()) { amountOut = amountOut.add(epochData[i].availableRewardsForUser[user]); epochData[i].availableRewardsForUser[user] = PairUints(0, 0); } } lastEpochClaimed[user] = _lastEpoch; emit PendleRewardsSettled(user, amountOut); } /** @notice Calc the amount of interests that the user can receive now & clear all the due interests @dev To be called before any interests is transferred out @dev same logic as the function in V1 */ function _beforeTransferDueInterests(address user) internal virtual returns (PairUints memory amountOut) { if (yieldTokens.allZero()) return amountOut; _updateDueInterests(user); amountOut = dueInterests[user].min(lastNYield); dueInterests[user] = PairUints(0, 0); lastNYield = lastNYield.sub(amountOut); } /** @param user the address whose all stake data will be updated @param payer the address which tokens will be pulled from @param amount amount of tokens to be staked @dev payer is only used to pass to _pullStakeToken */ function _settleStake( address user, address payer, uint256 amount ) internal virtual { _updatePendingRewards(user); _updateDueInterests(user); balances[user] = balances[user].add(amount); totalStake = totalStake.add(amount); _pullStakeToken(payer, amount); } /** @param user the address whose all stake data will be updated @param receiver the address which tokens will be pushed to @param amount amount of tokens to be withdrawn @dev receiver is only used to pass to _pullStakeToken */ function _settleWithdraw( address user, address receiver, uint256 amount ) internal virtual { _updatePendingRewards(user); _updateDueInterests(user); balances[user] = balances[user].sub(amount); totalStake = totalStake.sub(amount); _pushStakeToken(receiver, amount); } function _pullStakeToken(address from, uint256 amount) internal virtual { // For the case that we don't need to stake the stakeToken anywhere else, just pull it // into the current contract IERC20(stakeToken).safeTransferFrom(from, address(this), amount); } function _pushStakeToken(address to, uint256 amount) internal virtual { // For the case that we don't need to stake the stakeToken anywhere else, just transfer out // from the current contract if (amount != 0) IERC20(stakeToken).safeTransfer(to, amount); } function _pushYieldToken(address to, PairUints memory amount) internal virtual returns (PairUints memory outAmount) { outAmount = amount.min(yieldTokens.balanceOf(address(this))); yieldTokens.safeTransfer(to, outAmount); } /** @notice returns the stakeUnits in the _epochId(th) epoch of an user if he stake from _startTime to now @dev to calculate durationStakeThisEpoch: user will stake from _startTime -> _endTime, while the epoch last from _startTimeOfEpoch -> _endTimeOfEpoch => the stakeDuration of user will be min(_endTime,_endTimeOfEpoch) - max(_startTime,_startTimeOfEpoch) @dev same logic as in V1 */ function _calcUnitsStakeInEpoch( uint256 _tokenAmount, uint256 _startTime, uint256 _epochId ) internal view returns (uint256 stakeUnitsForUser) { uint256 _endTime = block.timestamp; uint256 _l = Math.max(_startTime, _startTimeOfEpoch(_epochId)); uint256 _r = Math.min(_endTime, _endTimeOfEpoch(_epochId)); uint256 durationStakeThisEpoch = _r.subMax0(_l); return _tokenAmount.mul(durationStakeThisEpoch); } /** @notice calc the amount of rewards the user is eligible to receive from this epoch, but we will return the amount per vestingEpoch instead @dev same logic as in V1 */ function _calcAmountRewardsForUserInEpoch(address user, uint256 epochId) internal view returns (PairUints memory rewardsPerVestingEpoch) { rewardsPerVestingEpoch = epochData[epochId] .totalRewards .mul(epochData[epochId].stakeUnitsForUser[user]) .div(epochData[epochId].totalStakeUnits) .div(vestingEpochs); } function _startTimeOfEpoch(uint256 t) internal view returns (uint256) { // epoch id starting from 1 return startTime.add((t.sub(1)).mul(epochDuration)); } function _epochOfTimestamp(uint256 t) internal view returns (uint256) { if (t < startTime) return 0; return (t.sub(startTime)).div(epochDuration).add(1); } // Although the name of this function is endTimeOfEpoch, it's actually the beginning of the next epoch function _endTimeOfEpoch(uint256 t) internal view returns (uint256) { // epoch id starting from 1 return startTime.add(t.mul(epochDuration)); } function _allowedToWithdraw(address _token) internal view override returns (bool allowed) { allowed = (rewardTokens.contains(_token) == false) && stakeToken != _token && (yieldTokens.contains(_token) == false); } }