From 078fb40f1f50cc98120a2b4abc72d22c36dfae3d Mon Sep 17 00:00:00 2001 From: owen05 Date: Wed, 24 Mar 2021 18:08:20 +0800 Subject: [PATCH] dodomine v2 --- contracts/DODOToken/DODOMineV2/BaseMine.sol | 216 ++++++++++++++++++ contracts/DODOToken/DODOMineV2/LpMine.sol | 67 ++++++ .../DODOToken/DODOMineV2/RewardVault.sol | 31 +++ contracts/DODOToken/DODOMineV2/vDODOMine.sol | 59 +++++ contracts/DODOToken/Governance.sol | 47 +++- contracts/DODOToken/LockedTokenVault.sol | 2 +- contracts/intf/IDODOApproveProxy.sol | 13 ++ deploy-detail-periphery.txt | 3 +- migrations/4_deploy_periphery.js | 17 ++ truffle-config.js | 3 +- 10 files changed, 451 insertions(+), 7 deletions(-) create mode 100644 contracts/DODOToken/DODOMineV2/BaseMine.sol create mode 100644 contracts/DODOToken/DODOMineV2/LpMine.sol create mode 100644 contracts/DODOToken/DODOMineV2/RewardVault.sol create mode 100644 contracts/DODOToken/DODOMineV2/vDODOMine.sol create mode 100644 contracts/intf/IDODOApproveProxy.sol diff --git a/contracts/DODOToken/DODOMineV2/BaseMine.sol b/contracts/DODOToken/DODOMineV2/BaseMine.sol new file mode 100644 index 0000000..2d1d96b --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/BaseMine.sol @@ -0,0 +1,216 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {SafeERC20} from "../../lib/SafeERC20.sol"; +import {IERC20} from "../../intf/IERC20.sol"; +import {SafeMath} from "../../lib/SafeMath.sol"; +import {DecimalMath} from "../../lib/DecimalMath.sol"; +import {InitializableOwnable} from "../../lib/InitializableOwnable.sol"; +import {IRewardVault, RewardVault} from "./RewardVault.sol"; + + +contract BaseMine is InitializableOwnable { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + // ============ Storage ============ + + struct RewardTokenInfo { + address rewardToken; + uint256 startBlock; + uint256 endBlock; + address rewardVault; + address rewardDistributor; + + uint256 rewardPerBlock; + uint256 accRewardPerShare; + uint256 lastRewardBlock; + + mapping(address => uint256) userRewardPerTokenPaid; + mapping(address => uint256) userRewards; + } + + RewardTokenInfo[] public rewardTokenInfos; + + uint256 internal _totalSupply; + mapping(address => uint256) internal _balances; + + // ============ Event ============= + + event Claim(uint256 indexed i, address indexed user, uint256 reward); + event UpdateReward(uint256 indexed i, uint256 rewardPerBlock); + event UpdateEndBlock(uint256 indexed i, uint256 endBlock); + event RewardDistributorChanged(uint256 indexed i, address rewardDistributor); + event NewRewardToken(uint256 indexed i, address rewardToken); + event RemoveRewardToken(address rewardToken); + + // ============ Modifier ========== + + modifier onlyRewardDistributor(uint i) { + require(msg.sender == rewardTokenInfos[i].rewardDistributor, "DODOMineV2: ACCESS_RESTRICTED"); + _; + } + + modifier updateReward(address user) { + uint256 len = rewardTokenInfos.length; + for (uint i = 0; i < len; i++) { + RewardTokenInfo storage rt = rewardTokenInfos[i]; + rt.accRewardPerShare = rewardPerLpToken(i); + rt.lastRewardBlock = lastBlockRewardApplicable(i); + if (user != address(0)) { + rt.userRewards[user] = getPendingReward(i, user); + rt.userRewardPerTokenPaid[user] = rt.accRewardPerShare; + } + } + _; + } + + // ============ View ============ + + function lastBlockRewardApplicable(uint i) public view returns (uint256) { + uint256 startBlock = rewardTokenInfos[i].startBlock; + uint256 endBlock = rewardTokenInfos[i].endBlock; + if(block.number < endBlock) { + if(block.number < startBlock) + return startBlock; + else + return block.number; + }else { + return endBlock; + } + } + + function rewardPerLpToken(uint i) public view returns (uint256) { + RewardTokenInfo memory rt = rewardTokenInfos[i]; + if (totalSupply() == 0) { + return rt.accRewardPerShare; + } + return rt.accRewardPerShare.add( + DecimalMath.divFloor( + lastBlockRewardApplicable(i).sub(rt.lastRewardBlock).mul(rt.rewardPerBlock), + totalSupply() + ) + ); + } + + function getPendingReward(uint i, address user) public view returns (uint256) { + RewardTokenInfo storage rt = rewardTokenInfos[i]; + return DecimalMath.mulFloor( + balanceOf(user), + rewardPerLpToken(i).sub(rt.userRewardPerTokenPaid[user]) + ).add(rt.userRewards[user]); + } + + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address user) public view returns (uint256) { + return _balances[user]; + } + + // ============ Claim ============ + + function getReward(uint i) public updateReward(msg.sender) { + RewardTokenInfo storage rt = rewardTokenInfos[i]; + uint256 reward = rt.userRewards[msg.sender]; + if (reward > 0) { + rt.userRewards[msg.sender] = 0; + IRewardVault(rt.rewardVault).reward(msg.sender, reward); + emit Claim(i, msg.sender, reward); + } + } + + function getAllRewards() public { + uint256 len = rewardTokenInfos.length; + for (uint i = 0; i < len; i++) { + getReward(i); + } + } + + // =============== Ownable ================ + + function setRewardDistribution(uint i, address rewardDistributor) external onlyOwner { + RewardTokenInfo storage rt = rewardTokenInfos[i]; + rt.rewardDistributor = rewardDistributor; + emit RewardDistributorChanged(i, rewardDistributor); + } + + function addRewardToken(address rewardToken, address rewardDistributor,uint256 startBlock, uint256 endBlock) external onlyOwner { + require(rewardToken != address(0),"DODOMineV2: TOKEN_INVALID"); + require(startBlock > block.number, "DODOMineV2: START_BLOCK_INVALID"); + require(endBlock > startBlock ,"DODOMineV2: DURATION_INVALID"); + require(rewardDistributor != address(0), "DODOMineV2: REWARD_DISTRIBUTOR_INVALID"); + + uint256 len = rewardTokenInfos.length; + for (uint i = 0; i < len; i++) { + require(rewardToken != rewardTokenInfos[i].rewardToken, "DODOMineV2: TOKEN_ALREADY_ADDED"); + } + + RewardTokenInfo storage rt = rewardTokenInfos.push(); + rt.rewardToken = rewardToken; + rt.startBlock = startBlock; + rt.endBlock = endBlock; + rt.rewardDistributor = rewardDistributor; + rt.rewardVault = address(new RewardVault(rewardToken)); + + emit NewRewardToken(len, rewardToken); + emit RewardDistributorChanged(len, rewardDistributor); + } + + function removeRewardToken(address rewardToken) external onlyOwner { + uint256 len = rewardTokenInfos.length; + for (uint256 i = 0; i < len; i++) { + if (rewardToken == rewardTokenInfos[i].rewardToken) { + rewardTokenInfos[i] = rewardTokenInfos[len - 1]; + rewardTokenInfos.pop(); + emit RemoveRewardToken(rewardToken); + break; + } + } + } + + function setEndBlock(uint i, uint256 newEndBlock) external onlyRewardDistributor(i) updateReward(address(0)) { + require(block.number < newEndBlock, "DODOMineV2: END_BLOCK_INVALID"); + RewardTokenInfo storage rt = rewardTokenInfos[i]; + require(block.number > rt.startBlock, "DODOMineV2: NOT_START"); + require(block.number < rt.endBlock, "DODOMineV2: ALREADY_CLOSE"); + + //TODO: gas ?需要额外维护 已分发reward变量,总reward 两个storage + // uint256 vaultBalance = IERC20(rt.rewardToken).balanceOf(rt.rewardVault); + // uint256 preReward = DecimalMath.mulFloor(newEndBlock.sub(block.number), rt.rewardPerBlock); + // require(preReward <= vaultBalance, "DODOMineV2: REWARD_NOT_ENOUGH"); + + rt.endBlock = newEndBlock; + rt.lastRewardBlock = block.number; + + emit UpdateEndBlock(i, newEndBlock); + } + + function setReward(uint i, uint256 newRewardPerBlock) external onlyRewardDistributor(i) updateReward(address(0)) { + RewardTokenInfo storage rt = rewardTokenInfos[i]; + uint256 endBlock = rt.endBlock; + + require(block.number < endBlock, "DODOMineV2: ALREADY_FINISHED"); + + //TODO: + // uint256 vaultBalance = IERC20(rt.rewardToken).balanceOf(rt.rewardVault); + // uint256 startBlock = rt.startBlock; + // uint256 preReward; + // if(startBlock > block.number) { + // preReward = DecimalMath.mulFloor(endBlock.sub(startBlock), newRewardPerBlock); + // }else { + // preReward = DecimalMath.mulFloor(endBlock.sub(block.number), newRewardPerBlock); + // } + // require(preReward <= vaultBalance, "DODOMineV2: REWARD_NOT_ENOUGH"); + + rt.rewardPerBlock = newRewardPerBlock; + emit UpdateReward(i, newRewardPerBlock); + } +} diff --git a/contracts/DODOToken/DODOMineV2/LpMine.sol b/contracts/DODOToken/DODOMineV2/LpMine.sol new file mode 100644 index 0000000..489a025 --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/LpMine.sol @@ -0,0 +1,67 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {SafeERC20} from "../../lib/SafeERC20.sol"; +import {IERC20} from "../../intf/IERC20.sol"; +import {SafeMath} from "../../lib/SafeMath.sol"; +import {IDODOApproveProxy} from "../../intf/IDODOApproveProxy.sol"; +import {BaseMine} from "./BaseMine.sol"; + + +contract LpMine is BaseMine { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + // ============ Storage ============ + + address public immutable _LP_TOKEN_; + address public immutable _DODO_APPROVE_PROXY_; + + constructor(address lpToken, address dodoApproveProxy) public { + _LP_TOKEN_ = lpToken; + _DODO_APPROVE_PROXY_ = dodoApproveProxy; + } + + // ============ Event ============ + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + + + // ============ Deposit && Withdraw && Exit ============ + + function deposit(uint256 amount) virtual public updateReward(msg.sender) { + require(amount > 0, "DODOMineV2: CANNOT_DEPOSIT_ZERO"); + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + IDODOApproveProxy(_DODO_APPROVE_PROXY_).claimTokens( + _LP_TOKEN_, + msg.sender, + address(this), + amount + ); + emit Deposit(msg.sender, amount); + } + + function withdraw(uint256 amount) virtual public updateReward(msg.sender) { + require(amount > 0, "DODOMineV2: CANNOT_WITHDRAW_ZERO"); + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + IERC20(_LP_TOKEN_).safeTransfer(msg.sender, amount); + emit Withdraw(msg.sender, amount); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + function exit() external { + withdraw(balanceOf(msg.sender)); + getAllRewards(); + } +} diff --git a/contracts/DODOToken/DODOMineV2/RewardVault.sol b/contracts/DODOToken/DODOMineV2/RewardVault.sol new file mode 100644 index 0000000..33bacf8 --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/RewardVault.sol @@ -0,0 +1,31 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; + +import {Ownable} from "../../lib/Ownable.sol"; +import {SafeERC20} from "../../lib/SafeERC20.sol"; +import {IERC20} from "../../intf/IERC20.sol"; + + +interface IRewardVault { + function reward(address to, uint256 amount) external; +} + +contract RewardVault is Ownable { + using SafeERC20 for IERC20; + + address public rewardToken; + + constructor(address _rewardToken) public { + rewardToken = _rewardToken; + } + + function reward(address to, uint256 amount) external onlyOwner { + IERC20(rewardToken).safeTransfer(to, amount); + } +} diff --git a/contracts/DODOToken/DODOMineV2/vDODOMine.sol b/contracts/DODOToken/DODOMineV2/vDODOMine.sol new file mode 100644 index 0000000..f5dd926 --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/vDODOMine.sol @@ -0,0 +1,59 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; + +import {SafeERC20} from "../../lib/SafeERC20.sol"; +import {IERC20} from "../../intf/IERC20.sol"; +import {SafeMath} from "../../lib/SafeMath.sol"; +import {BaseMine} from "./BaseMine.sol"; + +interface IVDODOToken { + function availableBalanceOf(address account) external view returns (uint256); +} + +contract vDODOMine is BaseMine { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + // ============ Storage ============ + address public immutable _vDODO_TOKEN_; + + constructor(address vDODOToken) public { + _vDODO_TOKEN_ = vDODOToken; + } + + // ============ Event ============= + + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + + + function deposit(uint256 amount) public { + require(amount > 0, "vDODOMineETH: CANNOT_DEPOSIT_ZERO"); + require(IVDODOToken(_vDODO_TOKEN_).availableBalanceOf(msg.sender) >= amount, "vDODOMineETH: vDODO_NOT_ENOUGH"); + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + emit Deposit(msg.sender, amount); + } + + function withdraw(uint256 amount) public { + require(amount > 0, "DODOMineV2: CANNOT_WITHDRAW_ZERO"); + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + emit Withdraw(msg.sender, amount); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + function exit() external { + withdraw(balanceOf(msg.sender)); + getAllRewards(); + } +} \ No newline at end of file diff --git a/contracts/DODOToken/Governance.sol b/contracts/DODOToken/Governance.sol index c177d7e..5c86b1e 100644 --- a/contracts/DODOToken/Governance.sol +++ b/contracts/DODOToken/Governance.sol @@ -5,14 +5,53 @@ */ pragma solidity 0.6.9; -pragma experimental ABIEncoderV2; import {InitializableOwnable} from "../lib/InitializableOwnable.sol"; +import {SafeMath} from "../lib/SafeMath.sol"; + +interface IVDODOMine { + function balanceOf(address account) external view returns (uint256); +} -//todo contract Governance is InitializableOwnable { + using SafeMath for uint256; - function getLockedvDODO(address account) external pure returns (uint256 lockedvDODO) { - lockedvDODO = 0;//todo for test + // ============ Storage ============ + address[] public _VDODO_MINE_LIST_; + + + // ============ Event ============= + event AddMineContract(address mineContract); + event RemoveMineContract(address mineContract); + + + function getLockedvDODO(address account) external view returns (uint256 lockedvDODO) { + uint256 len = _VDODO_MINE_LIST_.length; + for(uint i = 0; i < len; i++){ + uint256 curLocked = IVDODOMine(_VDODO_MINE_LIST_[i]).balanceOf(account); + lockedvDODO = lockedvDODO.add(curLocked); + } + } + + // =============== Ownable ================ + + function addMineContract(address[] memory mineContracts) external onlyOwner { + for(uint i = 0; i < mineContracts.length; i++){ + require(mineContracts[i] != address(0),"ADDRESS_INVALID"); + _VDODO_MINE_LIST_.push(mineContracts[i]); + emit AddMineContract(mineContracts[i]); + } + } + + function removeMineContract(address mineContract) external onlyOwner { + uint256 len = _VDODO_MINE_LIST_.length; + for (uint256 i = 0; i < len; i++) { + if (mineContract == _VDODO_MINE_LIST_[i]) { + _VDODO_MINE_LIST_[i] = _VDODO_MINE_LIST_[len - 1]; + _VDODO_MINE_LIST_.pop(); + emit RemoveMineContract(mineContract); + break; + } + } } } diff --git a/contracts/DODOToken/LockedTokenVault.sol b/contracts/DODOToken/LockedTokenVault.sol index aea1bd3..706204d 100644 --- a/contracts/DODOToken/LockedTokenVault.sol +++ b/contracts/DODOToken/LockedTokenVault.sol @@ -25,7 +25,7 @@ contract LockedTokenVault is Ownable { using SafeMath for uint256; using SafeERC20 for IERC20; - address _TOKEN_; + address public immutable _TOKEN_; mapping(address => uint256) internal originBalances; mapping(address => uint256) internal claimedBalances; diff --git a/contracts/intf/IDODOApproveProxy.sol b/contracts/intf/IDODOApproveProxy.sol new file mode 100644 index 0000000..39e13d8 --- /dev/null +++ b/contracts/intf/IDODOApproveProxy.sol @@ -0,0 +1,13 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; + +interface IDODOApproveProxy { + function isAllowedProxy(address _proxy) external view returns (bool); + function claimTokens(address token,address who,address dest,uint256 amount) external; +} diff --git a/deploy-detail-periphery.txt b/deploy-detail-periphery.txt index a9fd245..a942417 100644 --- a/deploy-detail-periphery.txt +++ b/deploy-detail-periphery.txt @@ -197,7 +197,8 @@ network type: kovan Deploy time: 2021/3/10 下午11:27:00 Deploy type: UpCrowdPoolingFactory UpCrowdPoolingFactory address: 0xe1C4300B47ccE8B162D8d036Db356c563a904757 -Init UpCpFactory Tx: 0x97ad372abbb2321e24e3e388585d47f0b1120980566abc3268c8305b04dc61f7==================================================== +Init UpCpFactory Tx: 0x97ad372abbb2321e24e3e388585d47f0b1120980566abc3268c8305b04dc61f7 +==================================================== network type: bsclive Deploy time: 2021/3/13 下午11:36:30 Deploy type: UpCrowdPoolingFactory diff --git a/migrations/4_deploy_periphery.js b/migrations/4_deploy_periphery.js index 48c5fd6..597667e 100644 --- a/migrations/4_deploy_periphery.js +++ b/migrations/4_deploy_periphery.js @@ -16,6 +16,7 @@ const DODOToken = artifacts.require("DODOToken"); const UpCrowdPoolingFactory = artifacts.require("UpCrowdPoolingFactory"); const CpFactory = artifacts.require("CrowdPoolingFactory"); const MultiCall = artifacts.require("Multicall"); +const LockedTokenVault = artifacts.require("LockedTokenVault"); module.exports = async (deployer, network, accounts) => { let CONFIG = GetConfig(network, accounts) @@ -39,6 +40,22 @@ module.exports = async (deployer, network, accounts) => { let multiSigAddress = CONFIG.multiSigAddress; let defaultMaintainer = CONFIG.defaultMaintainer; + if(deploySwitch.LockedVault) { + logger.log("===================================================="); + logger.log("network type: " + network); + logger.log("Deploy time: " + new Date().toLocaleString()); + logger.log("Deploy type: LockedVault"); + await deployer.deploy( + LockedTokenVault, + "0xd8C30a4E866B188F16aD266dC3333BD47F34ebaE", + 1616468400, + 2592000, + "100000000000000000" + ); + logger.log("LockedVault address: ", LockedTokenVault.address); + //TODO: approve && deposit + } + if (deploySwitch.UpCP) { logger.log("===================================================="); logger.log("network type: " + network); diff --git a/truffle-config.js b/truffle-config.js index 4249cf0..c88c751 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -54,7 +54,8 @@ module.exports = { DVM: false, CP: false, CPFactory: false, - MultiCall: false + MultiCall: false, + LockedVault: false }, networks: {