From 2cde28e424fb8a850443da995d123542f40eae86 Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:06:50 +0800 Subject: [PATCH] add contract for staking --- contracts/activity/TokenClaim.sol | 17 +- contracts/core/Governable.sol | 19 + contracts/interfaces/IMintable.sol | 9 + contracts/staking/RewardDistributor.sol | 91 +++ contracts/staking/RewardRouter.sol | 544 ++++++++++++++++++ contracts/staking/RewardTracker.sol | 329 +++++++++++ contracts/staking/Vester.sol | 387 +++++++++++++ .../staking/interfaces/IRewardDistributor.sol | 10 + .../staking/interfaces/IRewardRouter.sol | 7 + .../staking/interfaces/IRewardTracker.sol | 18 + contracts/staking/interfaces/IVester.sol | 27 + contracts/test/TestSth.sol | 8 + out/bsc_main_release.json | 6 + test/testTest.ts | 37 ++ 14 files changed, 1494 insertions(+), 15 deletions(-) create mode 100644 contracts/core/Governable.sol create mode 100644 contracts/interfaces/IMintable.sol create mode 100644 contracts/staking/RewardDistributor.sol create mode 100644 contracts/staking/RewardRouter.sol create mode 100644 contracts/staking/RewardTracker.sol create mode 100644 contracts/staking/Vester.sol create mode 100644 contracts/staking/interfaces/IRewardDistributor.sol create mode 100644 contracts/staking/interfaces/IRewardRouter.sol create mode 100644 contracts/staking/interfaces/IRewardTracker.sol create mode 100644 contracts/staking/interfaces/IVester.sol create mode 100644 contracts/test/TestSth.sol create mode 100644 test/testTest.ts diff --git a/contracts/activity/TokenClaim.sol b/contracts/activity/TokenClaim.sol index 9067595..33e7473 100644 --- a/contracts/activity/TokenClaim.sol +++ b/contracts/activity/TokenClaim.sol @@ -83,14 +83,7 @@ contract TokenClaim is HasSignature, ReentrancyGuard, Pausable, TimeChecker { uint256 current = claimedBitMap[account][token]; require(current & vals[1] == 0, "TokenClaim: condition check failed"); address user = _msgSender(); - bytes32 criteriaMessageHash = getMessageHash( - user, - account, - token, - _CACHED_THIS, - _CACHED_CHAIN_ID, - vals - ); + bytes32 criteriaMessageHash = getMessageHash(user, account, token, _CACHED_THIS, _CACHED_CHAIN_ID, vals); checkSigner(verifier, criteriaMessageHash, signature); _useSignature(signature); claimedBitMap[account][token] = current | vals[1]; @@ -106,13 +99,7 @@ contract TokenClaim is HasSignature, ReentrancyGuard, Pausable, TimeChecker { uint256 _chainId, uint256[4] calldata _vals ) public pure returns (bytes32) { - bytes memory encoded = abi.encodePacked( - _user, - _account, - _token, - _contract, - _chainId - ); + bytes memory encoded = abi.encodePacked(_user, _account, _token, _contract, _chainId); for (uint256 i = 0; i < _vals.length; i++) { encoded = bytes.concat(encoded, abi.encodePacked(_vals[i])); } diff --git a/contracts/core/Governable.sol b/contracts/core/Governable.sol new file mode 100644 index 0000000..bb0ae2e --- /dev/null +++ b/contracts/core/Governable.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +contract Governable { + address public gov; + + constructor() { + gov = msg.sender; + } + + modifier onlyGov() { + require(msg.sender == gov, "Governable: forbidden"); + _; + } + + function setGov(address _gov) external onlyGov { + gov = _gov; + } +} diff --git a/contracts/interfaces/IMintable.sol b/contracts/interfaces/IMintable.sol new file mode 100644 index 0000000..3b8de31 --- /dev/null +++ b/contracts/interfaces/IMintable.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IMintable { + function isMinter(address _account) external returns (bool); + function setMinter(address _minter, bool _isActive) external; + function mint(address _account, uint256 _amount) external; + function burn(address _account, uint256 _amount) external; +} \ No newline at end of file diff --git a/contracts/staking/RewardDistributor.sol b/contracts/staking/RewardDistributor.sol new file mode 100644 index 0000000..d428381 --- /dev/null +++ b/contracts/staking/RewardDistributor.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IRewardDistributor} from "./interfaces/IRewardDistributor.sol"; +import {IRewardTracker} from "./interfaces/IRewardTracker.sol"; +import {Governable} from "../core/Governable.sol"; + +contract RewardDistributor is IRewardDistributor, ReentrancyGuard, Governable { + using SafeERC20 for IERC20; + + address public override rewardToken; + // 1个token, 在指定周期内, 每秒可获得的收益 + // 比如收益是150%, 周期是1年, 那么 1.5 / (365 * 24 * 3600) + uint256 public override tokensPerInterval; + uint256 public lastDistributionTime; + // RewardTracker合约地址 + address public rewardTracker; + + address public admin; + + event Distribute(uint256 amount); + event TokensPerIntervalChange(uint256 amount); + + modifier onlyAdmin() { + require(msg.sender == admin, "RewardDistributor: forbidden"); + _; + } + + constructor(address _rewardToken, address _rewardTracker) { + rewardToken = _rewardToken; + rewardTracker = _rewardTracker; + admin = msg.sender; + } + + function setAdmin(address _admin) external onlyGov { + admin = _admin; + } + + // to help users who accidentally send their tokens to this contract + function withdrawToken(address _token, address _account, uint256 _amount) external onlyGov { + IERC20(_token).safeTransfer(_account, _amount); + } + + function updateLastDistributionTime() external onlyAdmin { + lastDistributionTime = block.timestamp; + } + + function setTokensPerInterval(uint256 _amount) external onlyAdmin { + require(lastDistributionTime != 0, "RewardDistributor: invalid lastDistributionTime"); + IRewardTracker(rewardTracker).updateRewards(); + tokensPerInterval = _amount; + emit TokensPerIntervalChange(_amount); + } + + function pendingRewards() public view override returns (uint256) { + if (block.timestamp == lastDistributionTime) { + return 0; + } + + uint256 timeDiff = block.timestamp - lastDistributionTime; + return tokensPerInterval * timeDiff; + } + // changed + // 从RewardTracker合约中调用 + // 由于我们是每个用户单独计算收益, 所以这里需要传入具体数量, 用于计算需要transafer的数量 + function distribute(uint256 _amount) external override returns (uint256) { + require(msg.sender == rewardTracker, "RewardDistributor: invalid msg.sender"); + uint256 amount = pendingRewards(); + if (amount == 0) { + return 0; + } + + lastDistributionTime = block.timestamp; + + uint256 tokenAmount = amount * _amount; + + uint256 balance = IERC20(rewardToken).balanceOf(address(this)); + if (tokenAmount > balance) { + tokenAmount = balance; + } + + IERC20(rewardToken).safeTransfer(msg.sender, tokenAmount); + + emit Distribute(tokenAmount); + return amount; + } +} diff --git a/contracts/staking/RewardRouter.sol b/contracts/staking/RewardRouter.sol new file mode 100644 index 0000000..fc946f2 --- /dev/null +++ b/contracts/staking/RewardRouter.sol @@ -0,0 +1,544 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IRewardTracker} from "./interfaces/IRewardTracker.sol"; +import {IRewardRouter} from "./interfaces/IRewardRouter.sol"; +import {IVester} from "./interfaces/IVester.sol"; +import {IMintable} from "../interfaces/IMintable.sol"; +import "../core/interfaces/IGlpManager.sol"; +import {Governable} from "../core/Governable.sol"; + +contract RewardRouter is IRewardRouterV2, ReentrancyGuard, Governable { + using SafeERC20 for IERC20; + using Address for address payable; + + enum VotingPowerType { + None, + BaseStakedAmount, + BaseAndBonusStakedAmount + } + + uint256 public constant BASIS_POINTS_DIVISOR = 10000; + + bool public isInitialized; + + address public weth; + + address public gmx; + address public esGmx; + address public bnGmx; + + address public glp; // GMX Liquidity Provider token + + address public stakedGmxTracker; + address public bonusGmxTracker; + address public feeGmxTracker; + + address public override stakedGlpTracker; + address public override feeGlpTracker; + + address public glpManager; + + address public gmxVester; + address public glpVester; + + uint256 public maxBoostBasisPoints; + bool public inStrictTransferMode; + + address public govToken; + VotingPowerType public votingPowerType; + + mapping (address => address) public pendingReceivers; + + event StakeGmx(address account, address token, uint256 amount); + event UnstakeGmx(address account, address token, uint256 amount); + + event StakeGlp(address account, uint256 amount); + event UnstakeGlp(address account, uint256 amount); + + receive() external payable { + require(msg.sender == weth, "Router: invalid sender"); + } + + function initialize( + address _weth, + address _gmx, + address _esGmx, + address _bnGmx, + address _glp, + address _stakedGmxTracker, + address _bonusGmxTracker, + address _feeGmxTracker, + address _feeGlpTracker, + address _stakedGlpTracker, + address _glpManager, + address _gmxVester, + address _glpVester, + address _govToken + ) external onlyGov { + require(!isInitialized, "already initialized"); + isInitialized = true; + + weth = _weth; + + gmx = _gmx; + esGmx = _esGmx; + bnGmx = _bnGmx; + + glp = _glp; + + stakedGmxTracker = _stakedGmxTracker; + bonusGmxTracker = _bonusGmxTracker; + feeGmxTracker = _feeGmxTracker; + + feeGlpTracker = _feeGlpTracker; + stakedGlpTracker = _stakedGlpTracker; + + glpManager = _glpManager; + + gmxVester = _gmxVester; + glpVester = _glpVester; + + govToken = _govToken; + } + + function setInStrictTransferMode(bool _inStrictTransferMode) external onlyGov { + inStrictTransferMode = _inStrictTransferMode; + } + + function setMaxBoostBasisPoints(uint256 _maxBoostBasisPoints) external onlyGov { + maxBoostBasisPoints = _maxBoostBasisPoints; + } + + function setVotingPowerType(VotingPowerType _votingPowerType) external onlyGov { + votingPowerType = _votingPowerType; + } + + // to help users who accidentally send their tokens to this contract + function withdrawToken(address _token, address _account, uint256 _amount) external onlyGov { + IERC20(_token).safeTransfer(_account, _amount); + } + + function batchStakeGmxForAccount(address[] memory _accounts, uint256[] memory _amounts) external nonReentrant onlyGov { + address _gmx = gmx; + for (uint256 i = 0; i < _accounts.length; i++) { + _stakeGmx(msg.sender, _accounts[i], _gmx, _amounts[i]); + } + } + + function stakeGmxForAccount(address _account, uint256 _amount) external nonReentrant onlyGov { + _stakeGmx(msg.sender, _account, gmx, _amount); + } + + function stakeGmx(uint256 _amount) external nonReentrant { + _stakeGmx(msg.sender, msg.sender, gmx, _amount); + } + + function stakeEsGmx(uint256 _amount) external nonReentrant { + _stakeGmx(msg.sender, msg.sender, esGmx, _amount); + } + + function unstakeGmx(uint256 _amount) external nonReentrant { + _unstakeGmx(msg.sender, gmx, _amount, true); + } + + function unstakeEsGmx(uint256 _amount) external nonReentrant { + _unstakeGmx(msg.sender, esGmx, _amount, true); + } + + function mintAndStakeGlp(address _token, uint256 _amount, uint256 _minUsdg, uint256 _minGlp) external nonReentrant returns (uint256) { + require(_amount > 0, "invalid _amount"); + + address account = msg.sender; + uint256 glpAmount = IGlpManager(glpManager).addLiquidityForAccount(account, account, _token, _amount, _minUsdg, _minGlp); + IRewardTracker(feeGlpTracker).stakeForAccount(account, account, glp, glpAmount); + IRewardTracker(stakedGlpTracker).stakeForAccount(account, account, feeGlpTracker, glpAmount); + + emit StakeGlp(account, glpAmount); + + return glpAmount; + } + + function mintAndStakeGlpETH(uint256 _minUsdg, uint256 _minGlp) external payable nonReentrant returns (uint256) { + require(msg.value > 0, "invalid msg.value"); + + IWETH(weth).deposit{value: msg.value}(); + IERC20(weth).approve(glpManager, msg.value); + + address account = msg.sender; + uint256 glpAmount = IGlpManager(glpManager).addLiquidityForAccount(address(this), account, weth, msg.value, _minUsdg, _minGlp); + + IRewardTracker(feeGlpTracker).stakeForAccount(account, account, glp, glpAmount); + IRewardTracker(stakedGlpTracker).stakeForAccount(account, account, feeGlpTracker, glpAmount); + + emit StakeGlp(account, glpAmount); + + return glpAmount; + } + + function unstakeAndRedeemGlp(address _tokenOut, uint256 _glpAmount, uint256 _minOut, address _receiver) external nonReentrant returns (uint256) { + require(_glpAmount > 0, "invalid _glpAmount"); + + address account = msg.sender; + IRewardTracker(stakedGlpTracker).unstakeForAccount(account, feeGlpTracker, _glpAmount, account); + IRewardTracker(feeGlpTracker).unstakeForAccount(account, glp, _glpAmount, account); + uint256 amountOut = IGlpManager(glpManager).removeLiquidityForAccount(account, _tokenOut, _glpAmount, _minOut, _receiver); + + emit UnstakeGlp(account, _glpAmount); + + return amountOut; + } + + function unstakeAndRedeemGlpETH(uint256 _glpAmount, uint256 _minOut, address payable _receiver) external nonReentrant returns (uint256) { + require(_glpAmount > 0, "invalid _glpAmount"); + + address account = msg.sender; + IRewardTracker(stakedGlpTracker).unstakeForAccount(account, feeGlpTracker, _glpAmount, account); + IRewardTracker(feeGlpTracker).unstakeForAccount(account, glp, _glpAmount, account); + uint256 amountOut = IGlpManager(glpManager).removeLiquidityForAccount(account, weth, _glpAmount, _minOut, address(this)); + + IWETH(weth).withdraw(amountOut); + + _receiver.sendValue(amountOut); + + emit UnstakeGlp(account, _glpAmount); + + return amountOut; + } + + function claim() external nonReentrant { + address account = msg.sender; + + IRewardTracker(feeGmxTracker).claimForAccount(account, account); + IRewardTracker(feeGlpTracker).claimForAccount(account, account); + + IRewardTracker(stakedGmxTracker).claimForAccount(account, account); + IRewardTracker(stakedGlpTracker).claimForAccount(account, account); + } + + function claimEsGmx() external nonReentrant { + address account = msg.sender; + + IRewardTracker(stakedGmxTracker).claimForAccount(account, account); + IRewardTracker(stakedGlpTracker).claimForAccount(account, account); + } + + function claimFees() external nonReentrant { + address account = msg.sender; + + IRewardTracker(feeGmxTracker).claimForAccount(account, account); + IRewardTracker(feeGlpTracker).claimForAccount(account, account); + } + + function compound() external nonReentrant { + _compound(msg.sender); + } + + function compoundForAccount(address _account) external nonReentrant onlyGov { + _compound(_account); + } + + function handleRewards( + bool _shouldClaimGmx, + bool _shouldStakeGmx, + bool _shouldClaimEsGmx, + bool _shouldStakeEsGmx, + bool _shouldStakeMultiplierPoints, + bool _shouldClaimWeth, + bool _shouldConvertWethToEth + ) external nonReentrant { + address account = msg.sender; + + uint256 gmxAmount = 0; + if (_shouldClaimGmx) { + uint256 gmxAmount0 = IVester(gmxVester).claimForAccount(account, account); + uint256 gmxAmount1 = IVester(glpVester).claimForAccount(account, account); + gmxAmount = gmxAmount0.add(gmxAmount1); + } + + if (_shouldStakeGmx && gmxAmount > 0) { + _stakeGmx(account, account, gmx, gmxAmount); + } + + uint256 esGmxAmount = 0; + if (_shouldClaimEsGmx) { + uint256 esGmxAmount0 = IRewardTracker(stakedGmxTracker).claimForAccount(account, account); + uint256 esGmxAmount1 = IRewardTracker(stakedGlpTracker).claimForAccount(account, account); + esGmxAmount = esGmxAmount0.add(esGmxAmount1); + } + + if (_shouldStakeEsGmx && esGmxAmount > 0) { + _stakeGmx(account, account, esGmx, esGmxAmount); + } + + if (_shouldStakeMultiplierPoints) { + _stakeBnGmx(account); + } + + if (_shouldClaimWeth) { + if (_shouldConvertWethToEth) { + uint256 weth0 = IRewardTracker(feeGmxTracker).claimForAccount(account, address(this)); + uint256 weth1 = IRewardTracker(feeGlpTracker).claimForAccount(account, address(this)); + + uint256 wethAmount = weth0.add(weth1); + IWETH(weth).withdraw(wethAmount); + + payable(account).sendValue(wethAmount); + } else { + IRewardTracker(feeGmxTracker).claimForAccount(account, account); + IRewardTracker(feeGlpTracker).claimForAccount(account, account); + } + } + + _syncVotingPower(account); + } + + function batchCompoundForAccounts(address[] memory _accounts) external nonReentrant onlyGov { + for (uint256 i = 0; i < _accounts.length; i++) { + _compound(_accounts[i]); + } + } + + // the _validateReceiver function checks that the averageStakedAmounts and cumulativeRewards + // values of an account are zero, this is to help ensure that vesting calculations can be + // done correctly + // averageStakedAmounts and cumulativeRewards are updated if the claimable reward for an account + // is more than zero + // it is possible for multiple transfers to be sent into a single account, using signalTransfer and + // acceptTransfer, if those values have not been updated yet + // for GLP transfers it is also possible to transfer GLP into an account using the StakedGlp contract + function signalTransfer(address _receiver) external nonReentrant { + require(IERC20(gmxVester).balanceOf(msg.sender) == 0, "sender has vested tokens"); + require(IERC20(glpVester).balanceOf(msg.sender) == 0, "sender has vested tokens"); + + _validateReceiver(_receiver); + + if (inStrictTransferMode) { + uint256 balance = IRewardTracker(feeGmxTracker).stakedAmounts(msg.sender); + uint256 allowance = IERC20(feeGmxTracker).allowance(msg.sender, _receiver); + require(allowance >= balance, "insufficient allowance"); + } + + pendingReceivers[msg.sender] = _receiver; + } + + function acceptTransfer(address _sender) external nonReentrant { + require(IERC20(gmxVester).balanceOf(_sender) == 0, "sender has vested tokens"); + require(IERC20(glpVester).balanceOf(_sender) == 0, "sender has vested tokens"); + + address receiver = msg.sender; + require(pendingReceivers[_sender] == receiver, "transfer not signalled"); + delete pendingReceivers[_sender]; + + _validateReceiver(receiver); + _compound(_sender); + + uint256 stakedGmx = IRewardTracker(stakedGmxTracker).depositBalances(_sender, gmx); + if (stakedGmx > 0) { + _unstakeGmx(_sender, gmx, stakedGmx, false); + _stakeGmx(_sender, receiver, gmx, stakedGmx); + } + + uint256 stakedEsGmx = IRewardTracker(stakedGmxTracker).depositBalances(_sender, esGmx); + if (stakedEsGmx > 0) { + _unstakeGmx(_sender, esGmx, stakedEsGmx, false); + _stakeGmx(_sender, receiver, esGmx, stakedEsGmx); + } + + uint256 stakedBnGmx = IRewardTracker(feeGmxTracker).depositBalances(_sender, bnGmx); + if (stakedBnGmx > 0) { + IRewardTracker(feeGmxTracker).unstakeForAccount(_sender, bnGmx, stakedBnGmx, _sender); + IRewardTracker(feeGmxTracker).stakeForAccount(_sender, receiver, bnGmx, stakedBnGmx); + } + + uint256 esGmxBalance = IERC20(esGmx).balanceOf(_sender); + if (esGmxBalance > 0) { + IERC20(esGmx).transferFrom(_sender, receiver, esGmxBalance); + } + + uint256 bnGmxBalance = IERC20(bnGmx).balanceOf(_sender); + if (bnGmxBalance > 0) { + IMintable(bnGmx).burn(_sender, bnGmxBalance); + IMintable(bnGmx).mint(receiver, bnGmxBalance); + } + + uint256 glpAmount = IRewardTracker(feeGlpTracker).depositBalances(_sender, glp); + if (glpAmount > 0) { + IRewardTracker(stakedGlpTracker).unstakeForAccount(_sender, feeGlpTracker, glpAmount, _sender); + IRewardTracker(feeGlpTracker).unstakeForAccount(_sender, glp, glpAmount, _sender); + + IRewardTracker(feeGlpTracker).stakeForAccount(_sender, receiver, glp, glpAmount); + IRewardTracker(stakedGlpTracker).stakeForAccount(receiver, receiver, feeGlpTracker, glpAmount); + } + + IVester(gmxVester).transferStakeValues(_sender, receiver); + IVester(glpVester).transferStakeValues(_sender, receiver); + + _syncVotingPower(_sender); + _syncVotingPower(receiver); + } + + function _validateReceiver(address _receiver) private view { + require(IRewardTracker(stakedGmxTracker).averageStakedAmounts(_receiver) == 0, "stakedGmxTracker.averageStakedAmounts > 0"); + require(IRewardTracker(stakedGmxTracker).cumulativeRewards(_receiver) == 0, "stakedGmxTracker.cumulativeRewards > 0"); + + require(IRewardTracker(bonusGmxTracker).averageStakedAmounts(_receiver) == 0, "bonusGmxTracker.averageStakedAmounts > 0"); + require(IRewardTracker(bonusGmxTracker).cumulativeRewards(_receiver) == 0, "bonusGmxTracker.cumulativeRewards > 0"); + + require(IRewardTracker(feeGmxTracker).averageStakedAmounts(_receiver) == 0, "feeGmxTracker.averageStakedAmounts > 0"); + require(IRewardTracker(feeGmxTracker).cumulativeRewards(_receiver) == 0, "feeGmxTracker.cumulativeRewards > 0"); + + require(IVester(gmxVester).transferredAverageStakedAmounts(_receiver) == 0, "gmxVester.transferredAverageStakedAmounts > 0"); + require(IVester(gmxVester).transferredCumulativeRewards(_receiver) == 0, "gmxVester.transferredCumulativeRewards > 0"); + + require(IRewardTracker(stakedGlpTracker).averageStakedAmounts(_receiver) == 0, "stakedGlpTracker.averageStakedAmounts > 0"); + require(IRewardTracker(stakedGlpTracker).cumulativeRewards(_receiver) == 0, "stakedGlpTracker.cumulativeRewards > 0"); + + require(IRewardTracker(feeGlpTracker).averageStakedAmounts(_receiver) == 0, "feeGlpTracker.averageStakedAmounts > 0"); + require(IRewardTracker(feeGlpTracker).cumulativeRewards(_receiver) == 0, "feeGlpTracker.cumulativeRewards > 0"); + + require(IVester(glpVester).transferredAverageStakedAmounts(_receiver) == 0, "gmxVester.transferredAverageStakedAmounts > 0"); + require(IVester(glpVester).transferredCumulativeRewards(_receiver) == 0, "gmxVester.transferredCumulativeRewards > 0"); + + require(IERC20(gmxVester).balanceOf(_receiver) == 0, "gmxVester.balance > 0"); + require(IERC20(glpVester).balanceOf(_receiver) == 0, "glpVester.balance > 0"); + } + + function _compound(address _account) private { + _compoundGmx(_account); + _compoundGlp(_account); + _syncVotingPower(_account); + } + + function _compoundGmx(address _account) private { + uint256 esGmxAmount = IRewardTracker(stakedGmxTracker).claimForAccount(_account, _account); + if (esGmxAmount > 0) { + _stakeGmx(_account, _account, esGmx, esGmxAmount); + } + + _stakeBnGmx(_account); + } + + function _compoundGlp(address _account) private { + uint256 esGmxAmount = IRewardTracker(stakedGlpTracker).claimForAccount(_account, _account); + if (esGmxAmount > 0) { + _stakeGmx(_account, _account, esGmx, esGmxAmount); + } + } + + function _stakeGmx(address _fundingAccount, address _account, address _token, uint256 _amount) private { + require(_amount > 0, "invalid _amount"); + + IRewardTracker(stakedGmxTracker).stakeForAccount(_fundingAccount, _account, _token, _amount); + IRewardTracker(bonusGmxTracker).stakeForAccount(_account, _account, stakedGmxTracker, _amount); + IRewardTracker(feeGmxTracker).stakeForAccount(_account, _account, bonusGmxTracker, _amount); + + _syncVotingPower(_account); + + emit StakeGmx(_account, _token, _amount); + } + + // note that _syncVotingPower is not called here, in functions which + // call _stakeBnGmx it should be ensured that _syncVotingPower is called + // after + function _stakeBnGmx(address _account) private { + IRewardTracker(bonusGmxTracker).claimForAccount(_account, _account); + + // get the bnGmx balance of the user, this would be the amount of + // bnGmx that has not been staked + uint256 bnGmxAmount = IERC20(bnGmx).balanceOf(_account); + if (bnGmxAmount == 0) { return; } + + // get the baseStakedAmount which would be the sum of staked gmx and staked esGmx tokens + uint256 baseStakedAmount = IRewardTracker(stakedGmxTracker).stakedAmounts(_account); + uint256 maxAllowedBnGmxAmount = baseStakedAmount.mul(maxBoostBasisPoints).div(BASIS_POINTS_DIVISOR); + uint256 currentBnGmxAmount = IRewardTracker(feeGmxTracker).depositBalances(_account, bnGmx); + if (currentBnGmxAmount == maxAllowedBnGmxAmount) { return; } + + // if the currentBnGmxAmount is more than the maxAllowedBnGmxAmount + // unstake the excess tokens + if (currentBnGmxAmount > maxAllowedBnGmxAmount) { + uint256 amountToUnstake = currentBnGmxAmount.sub(maxAllowedBnGmxAmount); + IRewardTracker(feeGmxTracker).unstakeForAccount(_account, bnGmx, amountToUnstake, _account); + return; + } + + uint256 maxStakeableBnGmxAmount = maxAllowedBnGmxAmount.sub(currentBnGmxAmount); + if (bnGmxAmount > maxStakeableBnGmxAmount) { + bnGmxAmount = maxStakeableBnGmxAmount; + } + + IRewardTracker(feeGmxTracker).stakeForAccount(_account, _account, bnGmx, bnGmxAmount); + } + + function _unstakeGmx(address _account, address _token, uint256 _amount, bool _shouldReduceBnGmx) private { + require(_amount > 0, "invalid _amount"); + + uint256 balance = IRewardTracker(stakedGmxTracker).stakedAmounts(_account); + + IRewardTracker(feeGmxTracker).unstakeForAccount(_account, bonusGmxTracker, _amount, _account); + IRewardTracker(bonusGmxTracker).unstakeForAccount(_account, stakedGmxTracker, _amount, _account); + IRewardTracker(stakedGmxTracker).unstakeForAccount(_account, _token, _amount, _account); + + if (_shouldReduceBnGmx) { + IRewardTracker(bonusGmxTracker).claimForAccount(_account, _account); + + // unstake and burn staked bnGmx tokens + uint256 stakedBnGmx = IRewardTracker(feeGmxTracker).depositBalances(_account, bnGmx); + if (stakedBnGmx > 0) { + uint256 reductionAmount = stakedBnGmx.mul(_amount).div(balance); + IRewardTracker(feeGmxTracker).unstakeForAccount(_account, bnGmx, reductionAmount, _account); + IMintable(bnGmx).burn(_account, reductionAmount); + } + + // burn bnGmx tokens from user's balance + uint256 bnGmxBalance = IERC20(bnGmx).balanceOf(_account); + if (bnGmxBalance > 0) { + uint256 amountToBurn = bnGmxBalance.mul(_amount).div(balance); + IMintable(bnGmx).burn(_account, amountToBurn); + } + } + + _syncVotingPower(_account); + + emit UnstakeGmx(_account, _token, _amount); + } + + function _syncVotingPower(address _account) private { + if (votingPowerType == VotingPowerType.None) { + return; + } + + if (votingPowerType == VotingPowerType.BaseStakedAmount) { + uint256 baseStakedAmount = IRewardTracker(stakedGmxTracker).stakedAmounts(_account); + _syncVotingPower(_account, baseStakedAmount); + return; + } + + if (votingPowerType == VotingPowerType.BaseAndBonusStakedAmount) { + uint256 stakedAmount = IRewardTracker(feeGmxTracker).stakedAmounts(_account); + _syncVotingPower(_account, stakedAmount); + return; + } + + revert("unsupported votingPowerType"); + } + + function _syncVotingPower(address _account, uint256 _amount) private { + uint256 currentVotingPower = IERC20(govToken).balanceOf(_account); + if (currentVotingPower == _amount) { return; } + + if (currentVotingPower > _amount) { + uint256 amountToBurn = currentVotingPower.sub(_amount); + IMintable(govToken).burn(_account, amountToBurn); + return; + } + + uint256 amountToMint = _amount.sub(currentVotingPower); + IMintable(govToken).mint(_account, amountToMint); + } +} diff --git a/contracts/staking/RewardTracker.sol b/contracts/staking/RewardTracker.sol new file mode 100644 index 0000000..6b751e1 --- /dev/null +++ b/contracts/staking/RewardTracker.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IRewardDistributor} from "./interfaces/IRewardDistributor.sol"; +import {IRewardTracker} from "./interfaces/IRewardTracker.sol"; +import {Governable} from "../core/Governable.sol"; + +contract RewardTracker is IERC20, ReentrancyGuard, IRewardTracker, Governable { + using SafeERC20 for IERC20; + + uint256 public constant BASIS_POINTS_DIVISOR = 10000; + uint256 public constant PRECISION = 1e30; + + bool public isInitialized; + + string public name; + string public symbol; + uint8 public decimals = 18; + uint256 public override totalSupply; + mapping(address account => uint256 amount) public balances; + mapping(address owner => mapping(address spender => uint256 amount)) public allowances; + + address public distributor; + mapping(address token => bool status) public isDepositToken; + // 用户存入的token数量 + mapping(address account => mapping(address token => uint256 amount)) public override depositBalances; + // token总存入数量, 所有用户 + mapping(address token => uint256 amount) public totalDepositSupply; + + uint256 public cumulativeRewardPerToken; + // 用户stake的token数量 + mapping(address account => uint256 amount) public override stakedAmounts; + mapping(address account => uint256 amount) public claimableReward; + mapping(address account => uint256 amount) public previousCumulatedRewardPerToken; + // 用户累计收益 + mapping(address account => uint256 amount) public override cumulativeRewards; + // 平均 + mapping(address account => uint256 amount) public override averageStakedAmounts; + + bool public inPrivateTransferMode; + bool public inPrivateStakingMode; + bool public inPrivateClaimingMode; + mapping(address handler => bool status) public isHandler; + + event Claim(address receiver, uint256 amount); + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + } + + function initialize(address[] memory _depositTokens, address _distributor) external onlyGov { + require(!isInitialized, "RewardTracker: already initialized"); + isInitialized = true; + + for (uint256 i = 0; i < _depositTokens.length; i++) { + address depositToken = _depositTokens[i]; + isDepositToken[depositToken] = true; + } + + distributor = _distributor; + } + + function setDepositToken(address _depositToken, bool _isDepositToken) external onlyGov { + isDepositToken[_depositToken] = _isDepositToken; + } + + function setInPrivateTransferMode(bool _inPrivateTransferMode) external onlyGov { + inPrivateTransferMode = _inPrivateTransferMode; + } + + function setInPrivateStakingMode(bool _inPrivateStakingMode) external onlyGov { + inPrivateStakingMode = _inPrivateStakingMode; + } + + function setInPrivateClaimingMode(bool _inPrivateClaimingMode) external onlyGov { + inPrivateClaimingMode = _inPrivateClaimingMode; + } + + function setHandler(address _handler, bool _isActive) external onlyGov { + isHandler[_handler] = _isActive; + } + + // to help users who accidentally send their tokens to this contract + function withdrawToken(address _token, address _account, uint256 _amount) external onlyGov { + IERC20(_token).safeTransfer(_account, _amount); + } + + function balanceOf(address _account) external view override returns (uint256) { + return balances[_account]; + } + + function stake(address _depositToken, uint256 _amount) external override nonReentrant { + if (inPrivateStakingMode) { + revert("RewardTracker: action not enabled"); + } + _stake(msg.sender, msg.sender, _depositToken, _amount); + } + + function stakeForAccount( + address _fundingAccount, + address _account, + address _depositToken, + uint256 _amount + ) external override nonReentrant { + _validateHandler(); + _stake(_fundingAccount, _account, _depositToken, _amount); + } + + function unstake(address _depositToken, uint256 _amount) external override nonReentrant { + if (inPrivateStakingMode) { + revert("RewardTracker: action not enabled"); + } + _unstake(msg.sender, _depositToken, _amount, msg.sender); + } + + function unstakeForAccount( + address _account, + address _depositToken, + uint256 _amount, + address _receiver + ) external override nonReentrant { + _validateHandler(); + _unstake(_account, _depositToken, _amount, _receiver); + } + + function transfer(address _recipient, uint256 _amount) external override returns (bool) { + _transfer(msg.sender, _recipient, _amount); + return true; + } + + function allowance(address _owner, address _spender) external view override returns (uint256) { + return allowances[_owner][_spender]; + } + + function approve(address _spender, uint256 _amount) external override returns (bool) { + _approve(msg.sender, _spender, _amount); + return true; + } + + function transferFrom(address _sender, address _recipient, uint256 _amount) external override returns (bool) { + if (isHandler[msg.sender]) { + _transfer(_sender, _recipient, _amount); + return true; + } + require(allowances[_sender][msg.sender] >= _amount, "RewardTracker: transfer amount exceeds allowance"); + uint256 nextAllowance = allowances[_sender][msg.sender] - _amount; + _approve(_sender, msg.sender, nextAllowance); + _transfer(_sender, _recipient, _amount); + return true; + } + + function tokensPerInterval() external view override returns (uint256) { + return IRewardDistributor(distributor).tokensPerInterval(); + } + + function updateRewards() external override nonReentrant { + _updateRewards(address(0)); + } + + function claim(address _receiver) external override nonReentrant returns (uint256) { + if (inPrivateClaimingMode) { + revert("RewardTracker: action not enabled"); + } + return _claim(msg.sender, _receiver); + } + + function claimForAccount(address _account, address _receiver) external override nonReentrant returns (uint256) { + _validateHandler(); + return _claim(_account, _receiver); + } + + // changed + function claimable(address _account) public view override returns (uint256) { + uint256 stakedAmount = stakedAmounts[_account]; + if (stakedAmount == 0) { + return claimableReward[_account]; + } + uint256 pendingRewards = IRewardDistributor(distributor).pendingRewards() * PRECISION; + // 这里是pendingRewards是总的奖励,需要除以总的token数量, 我们是固定的比例的收益, 所以不需要再除以总数 + uint256 nextCumulativeRewardPerToken = cumulativeRewardPerToken + pendingRewards; + return + claimableReward[_account] + + (stakedAmount * (nextCumulativeRewardPerToken - previousCumulatedRewardPerToken[_account])) / + PRECISION; + } + + function rewardToken() public view returns (address) { + return IRewardDistributor(distributor).rewardToken(); + } + + function _claim(address _account, address _receiver) private returns (uint256) { + _updateRewards(_account); + + uint256 tokenAmount = claimableReward[_account]; + claimableReward[_account] = 0; + + if (tokenAmount > 0) { + IERC20(rewardToken()).safeTransfer(_receiver, tokenAmount); + emit Claim(_account, tokenAmount); + } + + return tokenAmount; + } + + function _mint(address _account, uint256 _amount) internal { + require(_account != address(0), "RewardTracker: mint to the zero address"); + + totalSupply = totalSupply + _amount; + balances[_account] = balances[_account] + _amount; + + emit Transfer(address(0), _account, _amount); + } + + function _burn(address _account, uint256 _amount) internal { + require(_account != address(0), "RewardTracker: burn from the zero address"); + require(balances[_account] >= _amount, "RewardTracker: burn amount exceeds balance"); + balances[_account] = balances[_account] - _amount; + totalSupply = totalSupply / _amount; + + emit Transfer(_account, address(0), _amount); + } + + function _transfer(address _sender, address _recipient, uint256 _amount) private { + require(_sender != address(0), "RewardTracker: transfer from the zero address"); + require(_recipient != address(0), "RewardTracker: transfer to the zero address"); + + if (inPrivateTransferMode) { + _validateHandler(); + } + require(balances[_sender] >= _amount, "RewardTracker: transfer amount exceeds balance"); + balances[_sender] = balances[_sender] - _amount; + balances[_recipient] = balances[_recipient] + _amount; + + emit Transfer(_sender, _recipient, _amount); + } + + function _approve(address _owner, address _spender, uint256 _amount) private { + require(_owner != address(0), "RewardTracker: approve from the zero address"); + require(_spender != address(0), "RewardTracker: approve to the zero address"); + + allowances[_owner][_spender] = _amount; + + emit Approval(_owner, _spender, _amount); + } + + function _validateHandler() private view { + require(isHandler[msg.sender], "RewardTracker: forbidden"); + } + + function _stake(address _fundingAccount, address _account, address _depositToken, uint256 _amount) private { + require(_amount > 0, "RewardTracker: invalid _amount"); + require(isDepositToken[_depositToken], "RewardTracker: invalid _depositToken"); + + IERC20(_depositToken).safeTransferFrom(_fundingAccount, address(this), _amount); + + _updateRewards(_account); + + stakedAmounts[_account] = stakedAmounts[_account] + _amount; + depositBalances[_account][_depositToken] = depositBalances[_account][_depositToken] + _amount; + totalDepositSupply[_depositToken] = totalDepositSupply[_depositToken] + _amount; + + _mint(_account, _amount); + } + + function _unstake(address _account, address _depositToken, uint256 _amount, address _receiver) private { + require(_amount > 0, "RewardTracker: invalid _amount"); + require(isDepositToken[_depositToken], "RewardTracker: invalid _depositToken"); + + _updateRewards(_account); + + uint256 stakedAmount = stakedAmounts[_account]; + require(stakedAmounts[_account] >= _amount, "RewardTracker: _amount exceeds stakedAmount"); + + stakedAmounts[_account] = stakedAmount - _amount; + + uint256 depositBalance = depositBalances[_account][_depositToken]; + require(depositBalance >= _amount, "RewardTracker: _amount exceeds depositBalance"); + depositBalances[_account][_depositToken] = depositBalance - _amount; + totalDepositSupply[_depositToken] = totalDepositSupply[_depositToken] - _amount; + + _burn(_account, _amount); + IERC20(_depositToken).safeTransfer(_receiver, _amount); + } + // changed + function _updateRewards(address _account) private { + uint256 supply = totalSupply; + uint256 blockReward = IRewardDistributor(distributor).distribute(supply); + + + uint256 _cumulativeRewardPerToken = cumulativeRewardPerToken; + if (supply > 0 && blockReward > 0) { + _cumulativeRewardPerToken = _cumulativeRewardPerToken + blockReward * PRECISION; + cumulativeRewardPerToken = _cumulativeRewardPerToken; + } + + // cumulativeRewardPerToken can only increase + // so if cumulativeRewardPerToken is zero, it means there are no rewards yet + if (_cumulativeRewardPerToken == 0) { + return; + } + + if (_account != address(0)) { + uint256 stakedAmount = stakedAmounts[_account]; + uint256 accountReward = (stakedAmount * (_cumulativeRewardPerToken - previousCumulatedRewardPerToken[_account])) / + PRECISION; + uint256 _claimableReward = claimableReward[_account] + accountReward; + + claimableReward[_account] = _claimableReward; + previousCumulatedRewardPerToken[_account] = _cumulativeRewardPerToken; + + if (_claimableReward > 0 && stakedAmounts[_account] > 0) { + uint256 nextCumulativeReward = cumulativeRewards[_account] + accountReward; + + averageStakedAmounts[_account] = + (averageStakedAmounts[_account] * cumulativeRewards[_account]) / + nextCumulativeReward + + (stakedAmount * accountReward) / + nextCumulativeReward; + + cumulativeRewards[_account] = nextCumulativeReward; + } + } + } +} diff --git a/contracts/staking/Vester.sol b/contracts/staking/Vester.sol new file mode 100644 index 0000000..a5a54e5 --- /dev/null +++ b/contracts/staking/Vester.sol @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IVester} from "./interfaces/IVester.sol"; +import {IRewardTracker} from "./interfaces/IRewardTracker.sol"; +import {Governable} from "../core/Governable.sol"; +import {IMintable} from "../interfaces/IMintable.sol"; + +contract Vester is IVester, IERC20, ReentrancyGuard, Governable { + using SafeERC20 for IERC20; + + string public name; + string public symbol; + uint8 public decimals = 18; + // remove? + uint256 public vestingDuration; + address public esToken; + address public pairToken; + address public claimableToken; + + address public override rewardTracker; + + uint256 public override totalSupply; + uint256 public pairSupply; + + mapping(address account => uint256 amount) public balances; + mapping(address account => uint256 amount) public override pairAmounts; + // 累积claim的数量, 每次计算后, 将可claim的数量从balances中转移到cumulativeClaimAmounts中, + // 在claim和deposit时, 会更新cumulativeClaimAmounts + // 然后减去claimedAmounts, 就是可claim的数量 + // claim以后, cumulativeClaimAmounts的值和claimedAmounts的值都会增加, 且相等 + mapping(address account => uint256 amount) public override cumulativeClaimAmounts; + // 已经claim了的数量, 只有在claim以后才会更新 + mapping(address account => uint256 amount) public override claimedAmounts; + // 最后claim的时间 + mapping(address account => uint256 time) public lastVestingTimes; + + mapping(address account => uint256 amount) public override transferredAverageStakedAmounts; + mapping(address account => uint256 amount) public override transferredCumulativeRewards; + mapping(address account => uint256 amount) public override cumulativeRewardDeductions; + mapping(address account => uint256 amount) public override bonusRewards; + + mapping(address handler => bool status) public isHandler; + + event Claim(address receiver, uint256 amount); + event Deposit(address account, uint256 amount); + event Withdraw(address account, uint256 claimedAmount, uint256 balance); + event PairTransfer(address indexed from, address indexed to, uint256 value); + + constructor( + string memory _name, + string memory _symbol, + uint256 _vestingDuration, + address _esToken, + address _pairToken, + address _claimableToken, + address _rewardTracker + ) { + name = _name; + symbol = _symbol; + + vestingDuration = _vestingDuration; + + esToken = _esToken; + pairToken = _pairToken; + claimableToken = _claimableToken; + + rewardTracker = _rewardTracker; + } + + function setHandler(address _handler, bool _isActive) external onlyGov { + isHandler[_handler] = _isActive; + } + + function deposit(uint256 _amount) external nonReentrant { + _deposit(msg.sender, _amount); + } + + function depositForAccount(address _account, uint256 _amount) external nonReentrant { + _validateHandler(); + _deposit(_account, _amount); + } + + function claim() external nonReentrant returns (uint256) { + return _claim(msg.sender, msg.sender); + } + + function claimForAccount(address _account, address _receiver) external override nonReentrant returns (uint256) { + _validateHandler(); + return _claim(_account, _receiver); + } + + // to help users who accidentally send their tokens to this contract + function withdrawToken(address _token, address _account, uint256 _amount) external onlyGov { + IERC20(_token).safeTransfer(_account, _amount); + } + + function withdraw() external nonReentrant { + address account = msg.sender; + address _receiver = account; + _claim(account, _receiver); + + uint256 claimedAmount = cumulativeClaimAmounts[account]; + uint256 balance = balances[account]; + uint256 totalVested = balance + claimedAmount; + require(totalVested > 0, "Vester: vested amount is zero"); + + if (hasPairToken()) { + uint256 pairAmount = pairAmounts[account]; + _burnPair(account, pairAmount); + IERC20(pairToken).safeTransfer(_receiver, pairAmount); + } + + IERC20(esToken).safeTransfer(_receiver, balance); + _burn(account, balance); + + delete cumulativeClaimAmounts[account]; + delete claimedAmounts[account]; + delete lastVestingTimes[account]; + + emit Withdraw(account, claimedAmount, balance); + } + + function transferStakeValues(address _sender, address _receiver) external override nonReentrant { + _validateHandler(); + + transferredAverageStakedAmounts[_receiver] = getCombinedAverageStakedAmount(_sender); + transferredAverageStakedAmounts[_sender] = 0; + + uint256 transferredCumulativeReward = transferredCumulativeRewards[_sender]; + uint256 cumulativeReward = hasRewardTracker() ? IRewardTracker(rewardTracker).cumulativeRewards(_sender) : 0; + + transferredCumulativeRewards[_receiver] = transferredCumulativeReward + cumulativeReward; + cumulativeRewardDeductions[_sender] = cumulativeReward; + transferredCumulativeRewards[_sender] = 0; + + bonusRewards[_receiver] = bonusRewards[_sender]; + bonusRewards[_sender] = 0; + } + + function setTransferredAverageStakedAmounts(address _account, uint256 _amount) external override nonReentrant { + _validateHandler(); + transferredAverageStakedAmounts[_account] = _amount; + } + + function setTransferredCumulativeRewards(address _account, uint256 _amount) external override nonReentrant { + _validateHandler(); + transferredCumulativeRewards[_account] = _amount; + } + + function setCumulativeRewardDeductions(address _account, uint256 _amount) external override nonReentrant { + _validateHandler(); + cumulativeRewardDeductions[_account] = _amount; + } + + function setBonusRewards(address _account, uint256 _amount) external override nonReentrant { + _validateHandler(); + bonusRewards[_account] = _amount; + } + + + + function getMaxVestableAmount(address _account) public view override returns (uint256) { + uint256 transferredCumulativeReward = transferredCumulativeRewards[_account]; + uint256 bonusReward = bonusRewards[_account]; + uint256 maxVestableAmount = transferredCumulativeReward + bonusReward; + + if (hasRewardTracker()) { + uint256 cumulativeReward = IRewardTracker(rewardTracker).cumulativeRewards(_account); + maxVestableAmount = maxVestableAmount + cumulativeReward; + } + + uint256 cumulativeRewardDeduction = cumulativeRewardDeductions[_account]; + + if (maxVestableAmount < cumulativeRewardDeduction) { + return 0; + } + + return maxVestableAmount - cumulativeRewardDeduction; + } + + function getCombinedAverageStakedAmount(address _account) public view override returns (uint256) { + if (!hasRewardTracker()) { + return 0; + } + + uint256 cumulativeReward = IRewardTracker(rewardTracker).cumulativeRewards(_account); + uint256 transferredCumulativeReward = transferredCumulativeRewards[_account]; + uint256 totalCumulativeReward = cumulativeReward + transferredCumulativeReward; + if (totalCumulativeReward == 0) { + return 0; + } + + uint256 averageStakedAmount = IRewardTracker(rewardTracker).averageStakedAmounts(_account); + uint256 transferredAverageStakedAmount = transferredAverageStakedAmounts[_account]; + + return + (averageStakedAmount * cumulativeReward) / + totalCumulativeReward + + (transferredAverageStakedAmount * transferredCumulativeReward) / + totalCumulativeReward; + } + + function getPairAmount(address _account, uint256 _esAmount) public view returns (uint256) { + if (!hasRewardTracker()) { + return 0; + } + + uint256 combinedAverageStakedAmount = getCombinedAverageStakedAmount(_account); + if (combinedAverageStakedAmount == 0) { + return 0; + } + + uint256 maxVestableAmount = getMaxVestableAmount(_account); + if (maxVestableAmount == 0) { + return 0; + } + + return (_esAmount * combinedAverageStakedAmount) / maxVestableAmount; + } + + function hasRewardTracker() public view returns (bool) { + return rewardTracker != address(0); + } + + function hasPairToken() public view returns (bool) { + return pairToken != address(0); + } + + function getTotalVested(address _account) public view returns (uint256) { + return balances[_account] + cumulativeClaimAmounts[_account]; + } + + function balanceOf(address _account) public view override returns (uint256) { + return balances[_account]; + } + + // empty implementation, tokens are non-transferrable + function transfer(address /* recipient */, uint256 /* amount */) public virtual override returns (bool) { + revert("Vester: non-transferrable"); + } + + // empty implementation, tokens are non-transferrable + function allowance(address /* owner */, address /* spender */) public view virtual override returns (uint256) { + return 0; + } + + // empty implementation, tokens are non-transferrable + function approve(address /* spender */, uint256 /* amount */) public virtual override returns (bool) { + revert("Vester: non-transferrable"); + } + + // empty implementation, tokens are non-transferrable + function transferFrom( + address /* sender */, + address /* recipient */, + uint256 /* amount */ + ) public virtual override returns (bool) { + revert("Vester: non-transferrable"); + } + + function getVestedAmount(address _account) public view override returns (uint256) { + uint256 balance = balances[_account]; + uint256 cumulativeClaimAmount = cumulativeClaimAmounts[_account]; + return balance + cumulativeClaimAmount; + } + + function _mint(address _account, uint256 _amount) private { + require(_account != address(0), "Vester: mint to the zero address"); + + totalSupply = totalSupply + _amount; + balances[_account] = balances[_account] + _amount; + + emit Transfer(address(0), _account, _amount); + } + + function _mintPair(address _account, uint256 _amount) private { + require(_account != address(0), "Vester: mint to the zero address"); + + pairSupply = pairSupply + _amount; + pairAmounts[_account] = pairAmounts[_account] + _amount; + + emit PairTransfer(address(0), _account, _amount); + } + + function _burn(address _account, uint256 _amount) private { + require(_account != address(0), "Vester: burn from the zero address"); + require(balances[_account] >= _amount, "Vester: balance is not enough"); + balances[_account] = balances[_account] - _amount; + totalSupply = totalSupply - _amount; + + emit Transfer(_account, address(0), _amount); + } + + function _burnPair(address _account, uint256 _amount) private { + require(_account != address(0), "Vester: burn from the zero address"); + require(pairAmounts[_account] >= _amount, "Vester: balance is not enough"); + pairAmounts[_account] = pairAmounts[_account] - _amount; + pairSupply = pairSupply - _amount; + + emit PairTransfer(_account, address(0), _amount); + } + /** + * @dev Deposit ES tokens to the contract + * 将esToken transfer到合约, 更新cumulativeClaimAmounts, lastVestingTimes, balances + */ + function _deposit(address _account, uint256 _amount) private { + require(_amount > 0, "Vester: invalid _amount"); + + _updateVesting(_account); + + IERC20(esToken).safeTransferFrom(_account, address(this), _amount); + + _mint(_account, _amount); + + if (hasPairToken()) { + uint256 pairAmount = pairAmounts[_account]; + uint256 nextPairAmount = getPairAmount(_account, balances[_account]); + if (nextPairAmount > pairAmount) { + uint256 pairAmountDiff = nextPairAmount - pairAmount; + IERC20(pairToken).safeTransferFrom(_account, address(this), pairAmountDiff); + _mintPair(_account, pairAmountDiff); + } + } + + uint256 maxAmount = getMaxVestableAmount(_account); + require(getTotalVested(_account) <= maxAmount, "Vester: max vestable amount exceeded"); + + emit Deposit(_account, _amount); + } + + function _updateVesting(address _account) private { + uint256 amount = _getNextClaimableAmount(_account); + lastVestingTimes[_account] = block.timestamp; + + if (amount == 0) { + return; + } + + // transfer claimableAmount from balances to cumulativeClaimAmounts + _burn(_account, amount); + cumulativeClaimAmounts[_account] = cumulativeClaimAmounts[_account] + amount; + + IMintable(esToken).burn(address(this), amount); + } + + function _getNextClaimableAmount(address _account) private view returns (uint256) { + uint256 timeDiff = block.timestamp - lastVestingTimes[_account]; + + uint256 balance = balances[_account]; + if (balance == 0) { + return 0; + } + // vestedAmount = balance + cumulativeClaimAmount + 最后一次vesting 至今的可提取金额 + uint256 vestedAmount = getVestedAmount(_account); + uint256 claimableAmount = (vestedAmount * timeDiff) / vestingDuration; + // 如果claimableAmount < balance, 返回claimableAmount + // 否则返回balance, 保证claimableAmount <= balance + if (claimableAmount < balance) { + return claimableAmount; + } + + return balance; + } + + function claimable(address _account) public view override returns (uint256) { + uint256 amount = cumulativeClaimAmounts[_account] - claimedAmounts[_account]; + uint256 nextClaimable = _getNextClaimableAmount(_account); + return amount + nextClaimable; + } + + function _claim(address _account, address _receiver) private returns (uint256) { + _updateVesting(_account); + uint256 amount = claimable(_account); + claimedAmounts[_account] = claimedAmounts[_account] + amount; + IERC20(claimableToken).safeTransfer(_receiver, amount); + emit Claim(_account, amount); + return amount; + } + + function _validateHandler() private view { + require(isHandler[msg.sender], "Vester: forbidden"); + } +} diff --git a/contracts/staking/interfaces/IRewardDistributor.sol b/contracts/staking/interfaces/IRewardDistributor.sol new file mode 100644 index 0000000..5cc293e --- /dev/null +++ b/contracts/staking/interfaces/IRewardDistributor.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IRewardDistributor { + function rewardToken() external view returns (address); + function tokensPerInterval() external view returns (uint256); + function pendingRewards() external view returns (uint256); + // changed + function distribute(uint256 _amount) external returns (uint256); +} diff --git a/contracts/staking/interfaces/IRewardRouter.sol b/contracts/staking/interfaces/IRewardRouter.sol new file mode 100644 index 0000000..472edcc --- /dev/null +++ b/contracts/staking/interfaces/IRewardRouter.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IRewardRouter { + function feeGlpTracker() external view returns (address); + function stakedGlpTracker() external view returns (address); +} diff --git a/contracts/staking/interfaces/IRewardTracker.sol b/contracts/staking/interfaces/IRewardTracker.sol new file mode 100644 index 0000000..d8470be --- /dev/null +++ b/contracts/staking/interfaces/IRewardTracker.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IRewardTracker { + function depositBalances(address _account, address _depositToken) external view returns (uint256); + function stakedAmounts(address _account) external view returns (uint256); + function updateRewards() external; + function stake(address _depositToken, uint256 _amount) external; + function stakeForAccount(address _fundingAccount, address _account, address _depositToken, uint256 _amount) external; + function unstake(address _depositToken, uint256 _amount) external; + function unstakeForAccount(address _account, address _depositToken, uint256 _amount, address _receiver) external; + function tokensPerInterval() external view returns (uint256); + function claim(address _receiver) external returns (uint256); + function claimForAccount(address _account, address _receiver) external returns (uint256); + function claimable(address _account) external view returns (uint256); + function averageStakedAmounts(address _account) external view returns (uint256); + function cumulativeRewards(address _account) external view returns (uint256); +} diff --git a/contracts/staking/interfaces/IVester.sol b/contracts/staking/interfaces/IVester.sol new file mode 100644 index 0000000..3cf995a --- /dev/null +++ b/contracts/staking/interfaces/IVester.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IVester { + function rewardTracker() external view returns (address); + + function claimForAccount(address _account, address _receiver) external returns (uint256); + + function claimable(address _account) external view returns (uint256); + function cumulativeClaimAmounts(address _account) external view returns (uint256); + function claimedAmounts(address _account) external view returns (uint256); + function pairAmounts(address _account) external view returns (uint256); + function getVestedAmount(address _account) external view returns (uint256); + function transferredAverageStakedAmounts(address _account) external view returns (uint256); + function transferredCumulativeRewards(address _account) external view returns (uint256); + function cumulativeRewardDeductions(address _account) external view returns (uint256); + function bonusRewards(address _account) external view returns (uint256); + + function transferStakeValues(address _sender, address _receiver) external; + function setTransferredAverageStakedAmounts(address _account, uint256 _amount) external; + function setTransferredCumulativeRewards(address _account, uint256 _amount) external; + function setCumulativeRewardDeductions(address _account, uint256 _amount) external; + function setBonusRewards(address _account, uint256 _amount) external; + + function getMaxVestableAmount(address _account) external view returns (uint256); + function getCombinedAverageStakedAmount(address _account) external view returns (uint256); +} diff --git a/contracts/test/TestSth.sol b/contracts/test/TestSth.sol new file mode 100644 index 0000000..a7bc4b0 --- /dev/null +++ b/contracts/test/TestSth.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +contract TestSth { + function test(uint256 a, uint256 b) public view returns (uint256) { + return a / b; + } +} diff --git a/out/bsc_main_release.json b/out/bsc_main_release.json index 7239a75..234aa6e 100644 --- a/out/bsc_main_release.json +++ b/out/bsc_main_release.json @@ -4,5 +4,11 @@ "type": "logic", "json": "assets/contracts/GameItemMall.json", "address": "0x3A85cA6615953c683826FBe54fA5e2a770ee8bA2" + }, + { + "name": "CEC", + "type": "erc20", + "json": "assets/contracts/FT.json", + "address": "0x111111267109489dc6f350608d5113B10c0C5cd7" } ] \ No newline at end of file diff --git a/test/testTest.ts b/test/testTest.ts new file mode 100644 index 0000000..b8db6e2 --- /dev/null +++ b/test/testTest.ts @@ -0,0 +1,37 @@ +import { expect } from 'chai' +import hre from "hardhat"; +import { + getBytes, + solidityPackedKeccak256, +} from 'ethers' +import { + loadFixture, +} from "@nomicfoundation/hardhat-toolbox/network-helpers"; + +describe('Test', function() { + async function deployOneContract() { + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await hre.ethers.getSigners(); + const verifier = owner.address; + + const TestBit = await hre.ethers.getContractFactory("TestSth"); + + const contract = await TestBit.deploy( ); + const chainId = hre.network.config.chainId + + return { contract, owner, otherAccount, verifier, chainId }; + } + describe("Deployment", function () { + it('should deploy TestSth', async function() { + const { contract } = await loadFixture(deployOneContract); + + const result = await contract.test(2, 3); + console.log(result); + expect(result).to.equal(1); + + }); + + }) + + +}) \ No newline at end of file