diff --git a/contracts/DODOStablePool/impl/DSP.sol b/contracts/DODOStablePool/impl/DSP.sol new file mode 100644 index 0000000..502bad3 --- /dev/null +++ b/contracts/DODOStablePool/impl/DSP.sol @@ -0,0 +1,96 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {IFeeRateModel} from "../../lib/FeeRateModel.sol"; +import {IERC20} from "../../intf/IERC20.sol"; +import {DSPTrader} from "./DSPTrader.sol"; +import {DSPFunding} from "./DSPFunding.sol"; +import {DSPVault} from "./DSPVault.sol"; + +/** + * @title DODO StablePool + * @author DODO Breeder + * + * @notice DODOStablePool initialization + */ +contract DSP is DSPTrader, DSPFunding { + function init( + address maintainer, + address baseTokenAddress, + address quoteTokenAddress, + uint256 lpFeeRate, + address mtFeeRateModel, + uint256 i, + uint256 k, + bool isOpenTWAP + ) external { + require(!_DSP_INITIALIZED_, "DSP_INITIALIZED"); + _DSP_INITIALIZED_ = true; + + require(baseTokenAddress != quoteTokenAddress, "BASE_QUOTE_CAN_NOT_BE_SAME"); + _BASE_TOKEN_ = IERC20(baseTokenAddress); + _QUOTE_TOKEN_ = IERC20(quoteTokenAddress); + + require(i > 0 && i <= 10**36); + _I_ = i; + + require(k <= 10**18); + _K_ = k; + + _LP_FEE_RATE_ = lpFeeRate; + _MT_FEE_RATE_MODEL_ = IFeeRateModel(mtFeeRateModel); + _MAINTAINER_ = maintainer; + + _IS_OPEN_TWAP_ = isOpenTWAP; + if (isOpenTWAP) _BLOCK_TIMESTAMP_LAST_ = uint32(block.timestamp % 2**32); + + string memory connect = "_"; + string memory suffix = "DLP"; + + name = string(abi.encodePacked(suffix, connect, addressToShortString(address(this)))); + symbol = "DLP"; + decimals = _BASE_TOKEN_.decimals(); + + // ============================== Permit ==================================== + uint256 chainId; + assembly { + chainId := chainid() + } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, + keccak256(bytes(name)), + keccak256(bytes("1")), + chainId, + address(this) + ) + ); + // ========================================================================== + } + + function addressToShortString(address _addr) public pure returns (string memory) { + bytes32 value = bytes32(uint256(_addr)); + bytes memory alphabet = "0123456789abcdef"; + + bytes memory str = new bytes(8); + for (uint256 i = 0; i < 4; i++) { + str[i * 2] = alphabet[uint8(value[i + 12] >> 4)]; + str[1 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)]; + } + return string(str); + } + + // ============ Version Control ============ + + function version() external pure returns (string memory) { + return "DSP 1.0.0"; + } +} diff --git a/contracts/DODOStablePool/impl/DSPFunding.sol b/contracts/DODOStablePool/impl/DSPFunding.sol new file mode 100644 index 0000000..9c1c47b --- /dev/null +++ b/contracts/DODOStablePool/impl/DSPFunding.sol @@ -0,0 +1,112 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {DSPVault} from "./DSPVault.sol"; +import {DecimalMath} from "../../lib/DecimalMath.sol"; +import {IDODOCallee} from "../../intf/IDODOCallee.sol"; + +contract DSPFunding is DSPVault { + // ============ Events ============ + + event BuyShares(address to, uint256 increaseShares, uint256 totalShares); + + event SellShares(address payer, address to, uint256 decreaseShares, uint256 totalShares); + + // ============ Buy & Sell Shares ============ + + // buy shares [round down] + function buyShares(address to) + external + preventReentrant + returns ( + uint256 shares, + uint256 baseInput, + uint256 quoteInput + ) + { + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)); + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)); + uint256 baseReserve = _BASE_RESERVE_; + uint256 quoteReserve = _QUOTE_RESERVE_; + + baseInput = baseBalance.sub(baseReserve); + quoteInput = quoteBalance.sub(quoteReserve); + require(baseInput > 0, "NO_BASE_INPUT"); + + // Round down when withdrawing. Therefore, never be a situation occuring balance is 0 but totalsupply is not 0 + // But May Happen,reserve >0 But totalSupply = 0 + if (totalSupply == 0) { + // case 1. initial supply + shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) + ? DecimalMath.divFloor(quoteBalance, _I_) + : baseBalance; + _BASE_TARGET_ = uint112(shares); + _QUOTE_TARGET_ = uint112(DecimalMath.mulFloor(shares, _I_)); + } else if (baseReserve > 0 && quoteReserve > 0) { + // case 2. normal case + uint256 baseInputRatio = DecimalMath.divFloor(baseInput, baseReserve); + uint256 quoteInputRatio = DecimalMath.divFloor(quoteInput, quoteReserve); + uint256 mintRatio = quoteInputRatio < baseInputRatio ? quoteInputRatio : baseInputRatio; + shares = DecimalMath.mulFloor(totalSupply, mintRatio); + + _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_).add(DecimalMath.mulFloor(uint256(_BASE_TARGET_), mintRatio))); + _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_).add(DecimalMath.mulFloor(uint256(_QUOTE_TARGET_), mintRatio))); + } + _mint(to, shares); + _setReserve(baseBalance, quoteBalance); + emit BuyShares(to, shares, _SHARES_[to]); + } + + // sell shares [round down] + function sellShares( + uint256 shareAmount, + address to, + uint256 baseMinAmount, + uint256 quoteMinAmount, + bytes calldata data, + uint256 deadline + ) external preventReentrant returns (uint256 baseAmount, uint256 quoteAmount) { + require(deadline >= block.timestamp, "TIME_EXPIRED"); + require(shareAmount <= _SHARES_[msg.sender], "DLP_NOT_ENOUGH"); + + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)); + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)); + uint256 totalShares = totalSupply; + + baseAmount = baseBalance.mul(shareAmount).div(totalShares); + quoteAmount = quoteBalance.mul(shareAmount).div(totalShares); + + _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_).sub(uint256(_BASE_TARGET_).mul(shareAmount).divCeil(totalShares))); + _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_).sub(uint256(_QUOTE_TARGET_).mul(shareAmount).divCeil(totalShares))); + + require( + baseAmount >= baseMinAmount && quoteAmount >= quoteMinAmount, + "WITHDRAW_NOT_ENOUGH" + ); + + _burn(msg.sender, shareAmount); + _transferBaseOut(to, baseAmount); + _transferQuoteOut(to, quoteAmount); + _sync(); + + if (data.length > 0) { + //Same as DVM + IDODOCallee(to).DVMSellShareCall( + msg.sender, + shareAmount, + baseAmount, + quoteAmount, + data + ); + } + + emit SellShares(msg.sender, to, shareAmount, _SHARES_[msg.sender]); + } +} diff --git a/contracts/DODOStablePool/impl/DSPStorage.sol b/contracts/DODOStablePool/impl/DSPStorage.sol new file mode 100644 index 0000000..96afb2e --- /dev/null +++ b/contracts/DODOStablePool/impl/DSPStorage.sol @@ -0,0 +1,107 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {ReentrancyGuard} from "../../lib/ReentrancyGuard.sol"; +import {SafeMath} from "../../lib/SafeMath.sol"; +import {DODOMath} from "../../lib/DODOMath.sol"; +import {DecimalMath} from "../../lib/DecimalMath.sol"; +import {IFeeRateModel} from "../../lib/FeeRateModel.sol"; +import {IERC20} from "../../intf/IERC20.sol"; +import {PMMPricing} from "../../lib/PMMPricing.sol"; + +contract DSPStorage is ReentrancyGuard { + using SafeMath for uint256; + + bool internal _DSP_INITIALIZED_; + bool public _IS_OPEN_TWAP_ = false; + + // ============ Core Address ============ + + address public _MAINTAINER_; + + IERC20 public _BASE_TOKEN_; + IERC20 public _QUOTE_TOKEN_; + + uint112 public _BASE_RESERVE_; + uint112 public _QUOTE_RESERVE_; + uint32 public _BLOCK_TIMESTAMP_LAST_; + + uint256 public _BASE_PRICE_CUMULATIVE_LAST_; + + uint112 public _BASE_TARGET_; + uint112 public _QUOTE_TARGET_; + uint32 public _RState_; + + // ============ Shares (ERC20) ============ + + string public symbol; + uint8 public decimals; + string public name; + + uint256 public totalSupply; + mapping(address => uint256) internal _SHARES_; + mapping(address => mapping(address => uint256)) internal _ALLOWED_; + + // ================= Permit ====================== + + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + mapping(address => uint256) public nonces; + + // ============ Variables for Pricing ============ + + IFeeRateModel public _MT_FEE_RATE_MODEL_; + + uint256 public _LP_FEE_RATE_; + uint256 public _K_; + uint256 public _I_; + + // ============ Helper Functions ============ + + function getPMMState() public view returns (PMMPricing.PMMState memory state) { + state.i = _I_; + state.K = _K_; + state.B = _BASE_RESERVE_; + state.Q = _QUOTE_RESERVE_; + state.B0 = _BASE_TARGET_; // will be calculated in adjustedTarget + state.Q0 = _QUOTE_TARGET_; + state.R = PMMPricing.RState(_RState_); + PMMPricing.adjustedTarget(state); + } + + function getPMMStateForCall() + external + view + returns ( + uint256 i, + uint256 K, + uint256 B, + uint256 Q, + uint256 B0, + uint256 Q0, + uint256 R + ) + { + PMMPricing.PMMState memory state = getPMMState(); + i = state.i; + K = state.K; + B = state.B; + Q = state.Q; + B0 = state.B0; + Q0 = state.Q0; + R = uint256(state.R); + } + + function getMidPrice() public view returns (uint256 midPrice) { + return PMMPricing.getMidPrice(getPMMState()); + } +} diff --git a/contracts/DODOStablePool/impl/DSPTrader.sol b/contracts/DODOStablePool/impl/DSPTrader.sol new file mode 100644 index 0000000..82a832e --- /dev/null +++ b/contracts/DODOStablePool/impl/DSPTrader.sol @@ -0,0 +1,237 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {DSPVault} from "./DSPVault.sol"; +import {SafeMath} from "../../lib/SafeMath.sol"; +import {DecimalMath} from "../../lib/DecimalMath.sol"; +import {PMMPricing} from "../../lib/PMMPricing.sol"; +import {IDODOCallee} from "../../intf/IDODOCallee.sol"; + +contract DSPTrader is DSPVault { + using SafeMath for uint256; + + // ============ Events ============ + + event DODOSwap( + address fromToken, + address toToken, + uint256 fromAmount, + uint256 toAmount, + address trader, + address receiver + ); + + event DODOFlashLoan(address borrower, address assetTo, uint256 baseAmount, uint256 quoteAmount); + + event RChange(PMMPricing.RState newRState); + + // ============ Trade Functions ============ + + function sellBase(address to) external preventReentrant returns (uint256 receiveQuoteAmount) { + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)); + uint256 baseInput = baseBalance.sub(uint256(_BASE_RESERVE_)); + uint256 mtFee; + uint256 newBaseTarget; + PMMPricing.RState newRState; + (receiveQuoteAmount, mtFee, newRState, newBaseTarget) = querySellBase(tx.origin, baseInput); + + _transferQuoteOut(to, receiveQuoteAmount); + _transferQuoteOut(_MAINTAINER_, mtFee); + + // update TARGET + if (_RState_ != uint32(newRState)) { + require(newBaseTarget <= uint112(-1), "OVERFLOW"); + _BASE_TARGET_ = uint112(newBaseTarget); + _RState_ = uint32(newRState); + emit RChange(newRState); + } + + _setReserve(baseBalance, _QUOTE_TOKEN_.balanceOf(address(this))); + + emit DODOSwap( + address(_BASE_TOKEN_), + address(_QUOTE_TOKEN_), + baseInput, + receiveQuoteAmount, + msg.sender, + to + ); + } + + function sellQuote(address to) external preventReentrant returns (uint256 receiveBaseAmount) { + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)); + uint256 quoteInput = quoteBalance.sub(uint256(_QUOTE_RESERVE_)); + uint256 mtFee; + uint256 newQuoteTarget; + PMMPricing.RState newRState; + (receiveBaseAmount, mtFee, newRState, newQuoteTarget) = querySellQuote( + tx.origin, + quoteInput + ); + + _transferBaseOut(to, receiveBaseAmount); + _transferBaseOut(_MAINTAINER_, mtFee); + + // update TARGET + if (_RState_ != uint32(newRState)) { + require(newQuoteTarget <= uint112(-1), "OVERFLOW"); + _QUOTE_TARGET_ = uint112(newQuoteTarget); + _RState_ = uint32(newRState); + emit RChange(newRState); + } + + _setReserve(_BASE_TOKEN_.balanceOf(address(this)), quoteBalance); + + emit DODOSwap( + address(_QUOTE_TOKEN_), + address(_BASE_TOKEN_), + quoteInput, + receiveBaseAmount, + msg.sender, + to + ); + } + + function flashLoan( + uint256 baseAmount, + uint256 quoteAmount, + address assetTo, + bytes calldata data + ) external preventReentrant { + _transferBaseOut(assetTo, baseAmount); + _transferQuoteOut(assetTo, quoteAmount); + + if (data.length > 0) + IDODOCallee(assetTo).DSPFlashLoanCall(msg.sender, baseAmount, quoteAmount, data); + + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)); + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)); + + // no input -> pure loss + require( + baseBalance >= _BASE_RESERVE_ || quoteBalance >= _QUOTE_RESERVE_, + "FLASH_LOAN_FAILED" + ); + + // sell quote case + // quote input + base output + if (baseBalance < _BASE_RESERVE_) { + uint256 quoteInput = quoteBalance.sub(uint256(_QUOTE_RESERVE_)); + ( + uint256 receiveBaseAmount, + uint256 mtFee, + PMMPricing.RState newRState, + uint256 newQuoteTarget + ) = querySellQuote(tx.origin, quoteInput); // revert if quoteBalance 0 && _BASE_RESERVE_ != 0 && _QUOTE_RESERVE_ != 0) { + _BASE_PRICE_CUMULATIVE_LAST_ += getMidPrice() * timeElapsed; + } + _BLOCK_TIMESTAMP_LAST_ = blockTimestamp; + } + + // ============ Set States ============ + + function _setReserve(uint256 baseReserve, uint256 quoteReserve) internal { + require(baseReserve <= uint112(-1) && quoteReserve <= uint112(-1), "OVERFLOW"); + _BASE_RESERVE_ = uint112(baseReserve); + _QUOTE_RESERVE_ = uint112(quoteReserve); + + if (_IS_OPEN_TWAP_) _twapUpdate(); + } + + function _sync() internal { + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)); + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)); + require(baseBalance <= uint112(-1) && quoteBalance <= uint112(-1), "OVERFLOW"); + if (baseBalance != _BASE_RESERVE_) { + _BASE_RESERVE_ = uint112(baseBalance); + } + if (quoteBalance != _QUOTE_RESERVE_) { + _QUOTE_RESERVE_ = uint112(quoteBalance); + } + + if (_IS_OPEN_TWAP_) _twapUpdate(); + } + + function sync() external preventReentrant { + _sync(); + } + + // ============ Asset Out ============ + + function _transferBaseOut(address to, uint256 amount) internal { + if (amount > 0) { + _BASE_TOKEN_.safeTransfer(to, amount); + } + } + + function _transferQuoteOut(address to, uint256 amount) internal { + if (amount > 0) { + _QUOTE_TOKEN_.safeTransfer(to, amount); + } + } + + // ============ Shares (ERC20) ============ + + /** + * @dev transfer token for a specified address + * @param to The address to transfer to. + * @param amount The amount to be transferred. + */ + function transfer(address to, uint256 amount) public returns (bool) { + require(amount <= _SHARES_[msg.sender], "BALANCE_NOT_ENOUGH"); + + _SHARES_[msg.sender] = _SHARES_[msg.sender].sub(amount); + _SHARES_[to] = _SHARES_[to].add(amount); + emit Transfer(msg.sender, to, amount); + return true; + } + + /** + * @dev Gets the balance of the specified address. + * @param owner The address to query the the balance of. + * @return balance An uint256 representing the amount owned by the passed address. + */ + function balanceOf(address owner) external view returns (uint256 balance) { + return _SHARES_[owner]; + } + + /** + * @dev Transfer tokens from one address to another + * @param from address The address which you want to send tokens from + * @param to address The address which you want to transfer to + * @param amount uint256 the amount of tokens to be transferred + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public returns (bool) { + require(amount <= _SHARES_[from], "BALANCE_NOT_ENOUGH"); + require(amount <= _ALLOWED_[from][msg.sender], "ALLOWANCE_NOT_ENOUGH"); + + _SHARES_[from] = _SHARES_[from].sub(amount); + _SHARES_[to] = _SHARES_[to].add(amount); + _ALLOWED_[from][msg.sender] = _ALLOWED_[from][msg.sender].sub(amount); + emit Transfer(from, to, amount); + return true; + } + + /** + * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + * @param spender The address which will spend the funds. + * @param amount The amount of tokens to be spent. + */ + function approve(address spender, uint256 amount) public returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + function _approve( + address owner, + address spender, + uint256 amount + ) private { + _ALLOWED_[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Function to check the amount of tokens that an owner _ALLOWED_ to a spender. + * @param owner address The address which owns the funds. + * @param spender address The address which will spend the funds. + * @return A uint256 specifying the amount of tokens still available for the spender. + */ + function allowance(address owner, address spender) public view returns (uint256) { + return _ALLOWED_[owner][spender]; + } + + function _mint(address user, uint256 value) internal { + require(value > 1000, "MINT_AMOUNT_NOT_ENOUGH"); + _SHARES_[user] = _SHARES_[user].add(value); + totalSupply = totalSupply.add(value); + emit Mint(user, value); + emit Transfer(address(0), user, value); + } + + function _burn(address user, uint256 value) internal { + _SHARES_[user] = _SHARES_[user].sub(value); + totalSupply = totalSupply.sub(value); + emit Burn(user, value); + emit Transfer(user, address(0), value); + } + + // ============================ Permit ====================================== + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + require(deadline >= block.timestamp, "DODO_DSP_LP: EXPIRED"); + bytes32 digest = + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require( + recoveredAddress != address(0) && recoveredAddress == owner, + "DODO_DSP_LP: INVALID_SIGNATURE" + ); + _approve(owner, spender, value); + } +} diff --git a/contracts/DODOStablePool/intf/IDSP.sol b/contracts/DODOStablePool/intf/IDSP.sol new file mode 100644 index 0000000..0cfc8bf --- /dev/null +++ b/contracts/DODOStablePool/intf/IDSP.sol @@ -0,0 +1,36 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +interface IDSP { + function init( + address maintainer, + address baseTokenAddress, + address quoteTokenAddress, + uint256 lpFeeRate, + address mtFeeRateModel, + uint256 i, + uint256 k, + bool isOpenTWAP + ) external; + + function _BASE_TOKEN_() external returns (address); + + function _QUOTE_TOKEN_() external returns (address); + + function _MT_FEE_RATE_MODEL_() external returns (address); + + function getVaultReserve() external returns (uint256 baseReserve, uint256 quoteReserve); + + function sellBase(address to) external returns (uint256); + + function sellQuote(address to) external returns (uint256); + + function buyShares(address to) external returns (uint256); +} diff --git a/contracts/DODOToken/DODOMineV2/BaseMine.sol b/contracts/DODOToken/DODOMineV2/BaseMine.sol new file mode 100644 index 0000000..a8d6197 --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/BaseMine.sol @@ -0,0 +1,238 @@ +/* + + Copyright 2021 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; + uint256 rewardPerBlock; + uint256 accRewardPerShare; + uint256 lastRewardBlock; + mapping(address => uint256) userRewardPerSharePaid; + 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 NewRewardToken(uint256 indexed i, address rewardToken); + event RemoveRewardToken(address rewardToken); + event WithdrawLeftOver(address owner, uint256 i); + + // ============ View ============ + + function getPendingReward(address user, uint256 i) public view returns (uint256) { + require(i 0) { + rt.userRewards[msg.sender] = 0; + IRewardVault(rt.rewardVault).reward(msg.sender, reward); + emit Claim(i, msg.sender, reward); + } + } + + function claimAllRewards() external { + uint256 len = rewardTokenInfos.length; + for (uint256 i = 0; i < len; i++) { + claimReward(i); + } + } + + // =============== Ownable ================ + + function addRewardToken( + address rewardToken, + uint256 rewardPerBlock, + 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"); + + uint256 len = rewardTokenInfos.length; + for (uint256 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.rewardPerBlock = rewardPerBlock; + rt.rewardVault = address(new RewardVault(rewardToken)); + + emit NewRewardToken(len, rewardToken); + } + + function removeRewardToken(address rewardToken) external onlyOwner { + uint256 len = rewardTokenInfos.length; + for (uint256 i = 0; i < len; i++) { + if (rewardToken == rewardTokenInfos[i].rewardToken) { + if(i != len - 1) { + rewardTokenInfos[i] = rewardTokenInfos[len - 1]; + } + rewardTokenInfos.pop(); + emit RemoveRewardToken(rewardToken); + break; + } + } + } + + function setEndBlock(uint256 i, uint256 newEndBlock) + external + onlyOwner + { + _updateReward(address(0), i); + RewardTokenInfo storage rt = rewardTokenInfos[i]; + + require(block.number < newEndBlock, "DODOMineV2: END_BLOCK_INVALID"); + require(block.number > rt.startBlock, "DODOMineV2: NOT_START"); + require(block.number < rt.endBlock, "DODOMineV2: ALREADY_CLOSE"); + + rt.endBlock = newEndBlock; + emit UpdateEndBlock(i, newEndBlock); + } + + function setReward(uint256 i, uint256 newRewardPerBlock) + external + onlyOwner + { + _updateReward(address(0), i); + RewardTokenInfo storage rt = rewardTokenInfos[i]; + + require(block.number < rt.endBlock, "DODOMineV2: ALREADY_CLOSE"); + + rt.rewardPerBlock = newRewardPerBlock; + emit UpdateReward(i, newRewardPerBlock); + } + + function withdrawLeftOver(uint256 i, uint256 amount) external onlyOwner { + RewardTokenInfo storage rt = rewardTokenInfos[i]; + require(block.number > rt.endBlock, "DODOMineV2: MINING_NOT_FINISHED"); + + IRewardVault(rt.rewardVault).withdrawLeftOver(msg.sender,amount); + + emit WithdrawLeftOver(msg.sender, i); + } + + + // ============ Internal ============ + + function _updateReward(address user, uint256 i) internal { + RewardTokenInfo storage rt = rewardTokenInfos[i]; + if (rt.lastRewardBlock != block.number){ + rt.accRewardPerShare = _getAccRewardPerShare(i); + rt.lastRewardBlock = block.number; + } + if (user != address(0)) { + rt.userRewards[user] = getPendingReward(user, i); + rt.userRewardPerSharePaid[user] = rt.accRewardPerShare; + } + } + + function _updateAllReward(address user) internal { + uint256 len = rewardTokenInfos.length; + for (uint256 i = 0; i < len; i++) { + _updateReward(user, i); + } + } + + function _getUnrewardBlockNum(uint256 i) internal view returns (uint256) { + RewardTokenInfo memory rt = rewardTokenInfos[i]; + if (block.number < rt.startBlock || rt.lastRewardBlock > rt.endBlock) { + return 0; + } + uint256 start = rt.lastRewardBlock < rt.startBlock ? rt.startBlock : rt.lastRewardBlock; + uint256 end = rt.endBlock < block.number ? rt.endBlock : block.number; + return end.sub(start); + } + + function _getAccRewardPerShare(uint256 i) internal view returns (uint256) { + RewardTokenInfo memory rt = rewardTokenInfos[i]; + if (totalSupply() == 0) { + return rt.accRewardPerShare; + } + return + rt.accRewardPerShare.add( + DecimalMath.divFloor(_getUnrewardBlockNum(i).mul(rt.rewardPerBlock), totalSupply()) + ); + } + +} diff --git a/contracts/DODOToken/DODOMineV2/ERC20Mine.sol b/contracts/DODOToken/DODOMineV2/ERC20Mine.sol new file mode 100644 index 0000000..28f36c0 --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/ERC20Mine.sol @@ -0,0 +1,59 @@ +/* + + Copyright 2021 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 {BaseMine} from "./BaseMine.sol"; + +contract ERC20Mine is BaseMine { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + // ============ Storage ============ + + address public immutable _TOKEN_; + + constructor(address token) public { + _TOKEN_ = token; + } + + // ============ Event ============ + + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + + // ============ Deposit && Withdraw && Exit ============ + + function deposit(uint256 amount) external { + require(amount > 0, "DODOMineV2: CANNOT_DEPOSIT_ZERO"); + + _updateAllReward(msg.sender); + + uint256 erc20OriginBalance = IERC20(_TOKEN_).balanceOf(address(this)); + IERC20(_TOKEN_).safeTransferFrom(msg.sender, address(this), amount); + uint256 actualStakeAmount = IERC20(_TOKEN_).balanceOf(address(this)).sub(erc20OriginBalance); + + _totalSupply = _totalSupply.add(actualStakeAmount); + _balances[msg.sender] = _balances[msg.sender].add(actualStakeAmount); + + emit Deposit(msg.sender, actualStakeAmount); + } + + function withdraw(uint256 amount) external { + require(amount > 0, "DODOMineV2: CANNOT_WITHDRAW_ZERO"); + + _updateAllReward(msg.sender); + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + IERC20(_TOKEN_).safeTransfer(msg.sender, amount); + + emit Withdraw(msg.sender, amount); + } +} diff --git a/contracts/DODOToken/DODOMineV2/RewardVault.sol b/contracts/DODOToken/DODOMineV2/RewardVault.sol new file mode 100644 index 0000000..e61dd17 --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/RewardVault.sol @@ -0,0 +1,38 @@ +/* + + 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; + function withdrawLeftOver(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); + } + + function withdrawLeftOver(address to,uint256 amount) external onlyOwner { + uint256 leftover = IERC20(rewardToken).balanceOf(address(this)); + require(amount <= leftover, "VAULT_NOT_ENOUGH"); + 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..1179700 --- /dev/null +++ b/contracts/DODOToken/DODOMineV2/vDODOMine.sol @@ -0,0 +1,78 @@ +/* + + Copyright 2021 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); + event SyncBalance(); + + // ============ Deposit && Withdraw && Exit ============ + + function deposit(uint256 amount) external { + require(amount > 0, "DODOMineV2: CANNOT_DEPOSIT_ZERO"); + require( + amount <= IVDODOToken(_vDODO_TOKEN_).availableBalanceOf(msg.sender), + "DODOMineV2: vDODO_NOT_ENOUGH" + ); + _updateAllReward(msg.sender); + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + emit Deposit(msg.sender, amount); + } + + function withdraw(uint256 amount) external { + require(amount > 0, "DODOMineV2: CANNOT_WITHDRAW_ZERO"); + require(amount <= _balances[msg.sender], "DODOMineV2: WITHDRAW_BALANCE_NOT_ENOUGH"); + _updateAllReward(msg.sender); + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + emit Withdraw(msg.sender, amount); + } + + function syncBalance(address[] calldata userList) external { + for (uint256 i = 0; i < userList.length; ++i) { + address user = userList[i]; + uint256 curBalance = balanceOf(user); + uint256 vDODOBalance = IERC20(_vDODO_TOKEN_).balanceOf(user); + if (curBalance > vDODOBalance) { + _updateAllReward(user); + _totalSupply = _totalSupply.add(vDODOBalance).sub(curBalance); + _balances[user] = vDODOBalance; + } + } + emit SyncBalance(); + } + + // ============ View ============ + + function getLockedvDODO(address account) external view returns (uint256) { + return balanceOf(account); + } +} 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/Factory/DSPFactory.sol b/contracts/Factory/DSPFactory.sol new file mode 100644 index 0000000..a4e3348 --- /dev/null +++ b/contracts/Factory/DSPFactory.sol @@ -0,0 +1,158 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {InitializableOwnable} from "../lib/InitializableOwnable.sol"; +import {ICloneFactory} from "../lib/CloneFactory.sol"; +import {IDSP} from "../DODOStablePool/intf/IDSP.sol"; + +interface IDSPFactory { + function createDODOStablePool( + address baseToken, + address quoteToken, + uint256 lpFeeRate, + uint256 i, + uint256 k, + bool isOpenTWAP + ) external returns (address newStablePool); +} + +/** + * @title DODO StablePool Factory + * @author DODO Breeder + * + * @notice Create And Register DSP Pools + */ +contract DSPFactory is InitializableOwnable { + // ============ Templates ============ + + address public immutable _CLONE_FACTORY_; + address public immutable _DEFAULT_MAINTAINER_; + address public immutable _DEFAULT_MT_FEE_RATE_MODEL_; + address public _DSP_TEMPLATE_; + + // ============ Registry ============ + + // base -> quote -> DSP address list + mapping(address => mapping(address => address[])) public _REGISTRY_; + // creator -> DSP address list + mapping(address => address[]) public _USER_REGISTRY_; + + // ============ Events ============ + + event NewDSP(address baseToken, address quoteToken, address creator, address DSP); + + event RemoveDSP(address DSP); + + // ============ Functions ============ + + constructor( + address cloneFactory, + address DSPTemplate, + address defaultMaintainer, + address defaultMtFeeRateModel + ) public { + _CLONE_FACTORY_ = cloneFactory; + _DSP_TEMPLATE_ = DSPTemplate; + _DEFAULT_MAINTAINER_ = defaultMaintainer; + _DEFAULT_MT_FEE_RATE_MODEL_ = defaultMtFeeRateModel; + } + + function createDODOStablePool( + address baseToken, + address quoteToken, + uint256 lpFeeRate, + uint256 i, + uint256 k, + bool isOpenTWAP + ) external returns (address newStablePool) { + newStablePool = ICloneFactory(_CLONE_FACTORY_).clone(_DSP_TEMPLATE_); + { + IDSP(newStablePool).init( + _DEFAULT_MAINTAINER_, + baseToken, + quoteToken, + lpFeeRate, + _DEFAULT_MT_FEE_RATE_MODEL_, + i, + k, + isOpenTWAP + ); + } + _REGISTRY_[baseToken][quoteToken].push(newStablePool); + _USER_REGISTRY_[tx.origin].push(newStablePool); + emit NewDSP(baseToken, quoteToken, tx.origin, newStablePool); + } + + // ============ Admin Operation Functions ============ + + function updateDSPTemplate(address _newDSPTemplate) external onlyOwner { + _DSP_TEMPLATE_ = _newDSPTemplate; + } + + function addPoolByAdmin( + address creator, + address baseToken, + address quoteToken, + address pool + ) external onlyOwner { + _REGISTRY_[baseToken][quoteToken].push(pool); + _USER_REGISTRY_[creator].push(pool); + emit NewDSP(baseToken, quoteToken, creator, pool); + } + + function removePoolByAdmin( + address creator, + address baseToken, + address quoteToken, + address pool + ) external onlyOwner { + address[] memory registryList = _REGISTRY_[baseToken][quoteToken]; + for (uint256 i = 0; i < registryList.length; i++) { + if (registryList[i] == pool) { + registryList[i] = registryList[registryList.length - 1]; + break; + } + } + _REGISTRY_[baseToken][quoteToken] = registryList; + _REGISTRY_[baseToken][quoteToken].pop(); + address[] memory userRegistryList = _USER_REGISTRY_[creator]; + for (uint256 i = 0; i < userRegistryList.length; i++) { + if (userRegistryList[i] == pool) { + userRegistryList[i] = userRegistryList[userRegistryList.length - 1]; + break; + } + } + _USER_REGISTRY_[creator] = userRegistryList; + _USER_REGISTRY_[creator].pop(); + emit RemoveDSP(pool); + } + + // ============ View Functions ============ + + function getDODOPool(address baseToken, address quoteToken) + external + view + returns (address[] memory machines) + { + return _REGISTRY_[baseToken][quoteToken]; + } + + function getDODOPoolBidirection(address token0, address token1) + external + view + returns (address[] memory baseToken0Machines, address[] memory baseToken1Machines) + { + return (_REGISTRY_[token0][token1], _REGISTRY_[token1][token0]); + } + + function getDODOPoolByUser(address user) external view returns (address[] memory machines) { + return _USER_REGISTRY_[user]; + } +} 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/contracts/intf/IDODOCallee.sol b/contracts/intf/IDODOCallee.sol index 7118809..a668917 100644 --- a/contracts/intf/IDODOCallee.sol +++ b/contracts/intf/IDODOCallee.sol @@ -31,6 +31,13 @@ interface IDODOCallee { bytes calldata data ) external; + function DSPFlashLoanCall( + address sender, + uint256 baseAmount, + uint256 quoteAmount, + bytes calldata data + ) external; + function CPCancelCall( address sender, uint256 amount, 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/test/DODOMineV2/erc20Mine.test.ts b/test/DODOMineV2/erc20Mine.test.ts new file mode 100644 index 0000000..b11bc33 --- /dev/null +++ b/test/DODOMineV2/erc20Mine.test.ts @@ -0,0 +1,357 @@ +/* + + Copyright 2021 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +import { decimalStr, fromWei } from '../utils/Converter'; +import { logGas } from '../utils/Log'; +import { DODOMineV2Context, getDODOMineContext } from '../utils/DODOMineV2Context'; +import { assert } from 'chai'; +import { Contract } from 'web3-eth-contract'; + +let account0: string; +let account1: string; +let projector: string; + +async function init(ctx: DODOMineV2Context): Promise { + projector = ctx.Deployer; + account0 = ctx.SpareAccounts[0]; + account1 = ctx.SpareAccounts[1]; + + //For User + await ctx.mintTestToken(account0, ctx.ERC20, decimalStr("1000")); + await ctx.mintTestToken(account1, ctx.ERC20, decimalStr("500")); + + //For Project + await ctx.mintTestToken(projector, ctx.REWARD_1, decimalStr("1000000")); + await ctx.mintTestToken(projector, ctx.REWARD_2, decimalStr("1000000")); + + await ctx.approveProxy(account0, ctx.ERC20Mine.options.address, ctx.ERC20); + await ctx.approveProxy(account1, ctx.ERC20Mine.options.address, ctx.ERC20); +} + +async function addRewardToken(ctx: DODOMineV2Context, token: Contract, start: number, end: number, rewardPerBlock: string) { + await ctx.ERC20Mine.methods.addRewardToken( + token.options.address, + rewardPerBlock, + start, + end + ).send(ctx.sendParam(projector)); + + let idx = await ctx.ERC20Mine.methods.getIdByRewardToken(token.options.address).call(); + let rewardInfo = await ctx.ERC20Mine.methods.rewardTokenInfos(idx).call(); + await token.methods.transfer(rewardInfo.rewardVault, decimalStr("10000")).send(ctx.sendParam(projector)); +} + +async function stakeInfo(ctx: DODOMineV2Context, user: string, logInfo?: string) { + console.log(logInfo) + let totalSupply = await ctx.ERC20Mine.methods.totalSupply().call(); + let balance = await ctx.ERC20Mine.methods.balanceOf(user).call(); + console.log("totalSupply:" + fromWei(totalSupply, "ether") + " balance:" + fromWei(balance, "ether")); +} + +async function getRewardInfo(ctx: DODOMineV2Context, idx: number, user: string, logInfo?: string) { + let erc20Mine = ctx.ERC20Mine + let obj = await erc20Mine.methods.rewardTokenInfos(idx).call(); + let curBlock = await ctx.Web3.eth.getBlockNumber(); + console.log(logInfo); + // console.log("Static-Data: rewardToken:" + obj.rewardToken + " rewardVault:" + obj.rewardVault + " rewardPerBlock:" + fromWei(obj.rewardPerBlock, "ether")); + console.log("Dynamic-Data: start:" + obj.startBlock + " end:" + obj.endBlock + " accRewardPerShare:" + fromWei(obj.accRewardPerShare, "ether") + " lastRewardBlock:" + obj.lastRewardBlock + " curBlock:" + curBlock); + var pendingReward = null; + if (user != null) { + pendingReward = await erc20Mine.methods.getPendingReward(user, idx).call(); + console.log("User-pendingReward:" + fromWei(pendingReward, "ether")); + } + return [obj, pendingReward]; +} + +describe("erc20Mine", () => { + let snapshotId: string; + let ctx: DODOMineV2Context; + + before(async () => { + ctx = await getDODOMineContext(null); + await init(ctx); + }); + + beforeEach(async () => { + snapshotId = await ctx.EVM.snapshot(); + }); + + afterEach(async () => { + await ctx.EVM.reset(snapshotId); + }); + + + describe("baseMine", () => { + // ======= Ownable ========= + it("addRewardToken", async () => { + let erc20Mine = ctx.ERC20Mine; + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await erc20Mine.methods.addRewardToken( + ctx.REWARD_1.options.address, + decimalStr("0"), + curBlock + 2, + curBlock + 1000 + ).send(ctx.sendParam(projector)); + let [rewardTokenInfo,] = await getRewardInfo(ctx, 0, null, ""); + assert(rewardTokenInfo.rewardPerBlock, decimalStr("0")) + }); + + it("removeRewardToken", async () => { + let erc20Mine = ctx.ERC20Mine; + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 10, curBlock + 110, decimalStr("0")); + await addRewardToken(ctx, ctx.REWARD_2, curBlock + 10, curBlock + 110, decimalStr("0")); + let [rewardTokenInfo,] = await getRewardInfo(ctx, 0, null, ""); + await erc20Mine.methods.removeRewardToken( + rewardTokenInfo.rewardToken + ).send(ctx.sendParam(projector)); + [rewardTokenInfo,] = await getRewardInfo(ctx, 0, null, ""); + assert(rewardTokenInfo.rewardToken, ctx.REWARD_2.options.address) + }); + // =========================== + + }) + + describe("erc20Mine", () => { + + it("deposit", async () => { + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 102, decimalStr("10")); + await stakeInfo(ctx, account0, "UserStakeInfo - Before"); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - Before"); + //增加区块 + await ctx.increBlock(3); + + // curBlock = await ctx.Web3.eth.getBlockNumber(); + // console.log("deposit curBlock:", curBlock) + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("5") + ), ctx.sendParam(account0), "deposit"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("5") + ), ctx.sendParam(account0), "deposit"); + + await stakeInfo(ctx, account0, "UserStakeInfo - After"); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After - 1"); + //增加区块 + await ctx.increBlock(3); + + let [obj, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After - 2"); + + assert.equal(obj.accRewardPerShare, "2666666666666666666"); + assert.equal(pendingReward, "49999999999999999990"); + }); + + + it("withdraw", async () => { + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 102, decimalStr("10")); + // await stakeInfo(ctx, account0, "UserStakeInfo - Before"); + // await getRewardInfo(ctx, 0, account0, "UserRewardInfo - Before"); + //增加区块 + await ctx.increBlock(3); + + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("20") + ), ctx.sendParam(account0), "deposit - account0"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account1), "deposit - account1"); + + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After - 1"); + await ctx.increBlock(3); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After - 2"); + + await logGas(await ctx.ERC20Mine.methods.withdraw( + decimalStr("10") + ), ctx.sendParam(account0), "withdraw"); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After - 3"); + await logGas(await ctx.ERC20Mine.methods.withdraw( + decimalStr("10") + ), ctx.sendParam(account0), "withdraw"); + + //增加区块 + await ctx.increBlock(3); + let [obj, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After - 4"); + + assert.equal(obj.accRewardPerShare, "2333333333333333333"); + assert.equal(pendingReward, "41666666666666666660"); + }); + + + it("getReward", async () => { + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 102, decimalStr("10")); + await stakeInfo(ctx, account0, "UserStakeInfo - Before"); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - Before"); + + //增加区块 + await ctx.increBlock(3); + + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account1), "deposit"); + + //增加区块 + await ctx.increBlock(3); + + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + await logGas(await ctx.ERC20Mine.methods.claimReward(0), ctx.sendParam(account0), "claimReward - 0"); + + let rewardBalance = await ctx.REWARD_1.methods.balanceOf(account0).call(); + assert.equal(rewardBalance, "30000000000000000000"); + + }); + + it("getRewardAll", async () => { + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 5, curBlock + 103, decimalStr("10")); + await addRewardToken(ctx, ctx.REWARD_2, curBlock + 5, curBlock + 103, decimalStr("5")); + await stakeInfo(ctx, account0, "UserStakeInfo - Before"); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - Before"); + + //增加区块 + await ctx.increBlock(10); + + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account1), "deposit"); + + //增加区块 + await ctx.increBlock(3); + + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + await logGas(await ctx.ERC20Mine.methods.claimAllRewards(), ctx.sendParam(account0), "claimReward - 0"); + + let rewardBalance0 = await ctx.REWARD_1.methods.balanceOf(account0).call(); + let rewardBalance1 = await ctx.REWARD_2.methods.balanceOf(account0).call(); + assert.equal(rewardBalance0, "30000000000000000000"); + assert.equal(rewardBalance1, "15000000000000000000"); + }); + + it("setReward - beforeStart", async () => { + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 10, curBlock + 100, decimalStr("10")); + await ctx.ERC20Mine.methods.setReward(0, decimalStr("5")).send(ctx.sendParam(projector)); + + //增加区块 + await ctx.increBlock(10); + + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account1), "deposit"); + + //增加区块 + await ctx.increBlock(3); + + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + await logGas(await ctx.ERC20Mine.methods.claimReward(0), ctx.sendParam(account0), "claimReward - 0"); + + let rewardBalance = await ctx.REWARD_1.methods.balanceOf(account0).call(); + assert.equal(rewardBalance, "15000000000000000000"); + }) + + it("setReward - ing", async () => { + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 100, decimalStr("10")); + + //增加区块 + await ctx.increBlock(3); + + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account1), "deposit"); + + //增加区块 + await ctx.increBlock(3); + + let [, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + assert.equal(pendingReward, "25000000000000000000"); + + await ctx.ERC20Mine.methods.setReward(0, decimalStr("5")).send(ctx.sendParam(projector)); + + //增加区块 + await ctx.increBlock(3); + + [, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + await logGas(await ctx.ERC20Mine.methods.claimReward(0), ctx.sendParam(account0), "claimReward - 0"); + + let rewardBalance = await ctx.REWARD_1.methods.balanceOf(account0).call(); + assert.equal(rewardBalance, "40000000000000000000"); + }) + + // it("setReward - after", async () => { + // var curBlock = await ctx.Web3.eth.getBlockNumber(); + // await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 10, decimalStr("10")); + + // //增加区块 + // await ctx.increBlock(3); + + // await logGas(await ctx.ERC20Mine.methods.deposit( + // decimalStr("10") + // ), ctx.sendParam(account0), "deposit"); + // await logGas(await ctx.ERC20Mine.methods.deposit( + // decimalStr("10") + // ), ctx.sendParam(account1), "deposit"); + + // //增加区块 + // await ctx.increBlock(3); + + // let [, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + // assert.equal(pendingReward, "25000000000000000000"); + + // await ctx.ERC20Mine.methods.setReward(0, decimalStr("5")).send(ctx.sendParam(projector)); + + // //增加区块 + // await ctx.increBlock(3); + + // [, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + // await logGas(await ctx.ERC20Mine.methods.claimReward(0), ctx.sendParam(account0), "claimReward - 0"); + + // let rewardBalance = await ctx.REWARD_1.methods.balanceOf(account0).call(); + // assert.equal(rewardBalance, "25000000000000000000"); + // }) + + it("setEndBlock", async () => { + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 100, decimalStr("10")); + + //增加区块 + await ctx.increBlock(3); + + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit"); + await logGas(await ctx.ERC20Mine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account1), "deposit"); + + //增加区块 + await ctx.increBlock(3); + + await ctx.ERC20Mine.methods.setEndBlock(0, curBlock + 120).send(ctx.sendParam(projector)); + let [obj,] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + assert(obj.endBlock - curBlock - 100, "20"); + }) + + }) +}); diff --git a/test/DODOMineV2/vDODOMine.test.ts b/test/DODOMineV2/vDODOMine.test.ts new file mode 100644 index 0000000..9fb1ac2 --- /dev/null +++ b/test/DODOMineV2/vDODOMine.test.ts @@ -0,0 +1,194 @@ +/* + + Copyright 2021 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +import { decimalStr, fromWei } from '../utils/Converter'; +import { logGas } from '../utils/Log'; +import { DODOMineV2Context, getDODOMineContext } from '../utils/DODOMineV2Context'; +import { VDODOContext, getVDODOContext } from '../utils/VDODOContext'; +import { assert } from 'chai'; +import { Contract } from 'web3-eth-contract'; +const truffleAssert = require('truffle-assertions'); + +let account0: string; +let account1: string; +let projector: string; +let dodoTeam: string; + +async function init(ctx: DODOMineV2Context): Promise { + projector = ctx.Deployer; + account0 = ctx.SpareAccounts[0]; + account1 = ctx.SpareAccounts[1]; + + //For Project + await ctx.mintTestToken(projector, ctx.REWARD_1, decimalStr("1000000")); + await ctx.mintTestToken(projector, ctx.REWARD_2, decimalStr("1000000")); + + await ctx.approveProxy(account0, ctx.VDODOMine.options.address, ctx.ERC20); + await ctx.approveProxy(account1, ctx.VDODOMine.options.address, ctx.ERC20); +} + +async function initVdodo(ctx: VDODOContext): Promise { + dodoTeam = ctx.Deployer; + await ctx.mintTestToken(account0, decimalStr("10000")); + await ctx.mintTestToken(account1, decimalStr("10000")); + + await ctx.approveProxy(account0); + await ctx.approveProxy(account1); +} + +async function mint(ctx: VDODOContext, user: string, mintAmount: string, superior: string) { + await ctx.VDODO.methods.mint( + mintAmount, + superior + ).send(ctx.sendParam(user)); +} + +async function addRewardToken(ctx: DODOMineV2Context, token: Contract, start: number, end: number, rewardPerBlock: string) { + await ctx.VDODOMine.methods.addRewardToken( + token.options.address, + rewardPerBlock, + start, + end + ).send(ctx.sendParam(projector)); + + let idx = await ctx.VDODOMine.methods.getIdByRewardToken(token.options.address).call(); + let rewardInfo = await ctx.VDODOMine.methods.rewardTokenInfos(idx).call(); + await token.methods.transfer(rewardInfo.rewardVault, decimalStr("10000")).send(ctx.sendParam(projector)); +} + +async function stakeInfo(ctx: DODOMineV2Context, user: string, logInfo?: string) { + console.log(logInfo) + let totalSupply = await ctx.VDODOMine.methods.totalSupply().call(); + let balance = await ctx.VDODOMine.methods.balanceOf(user).call(); + console.log("totalSupply:" + fromWei(totalSupply, "ether") + " balance:" + fromWei(balance, "ether")); +} + +async function vdodoBalance(ctx: VDODOContext, user: string, logInfo?: string) { + console.log(logInfo) + let dodoBalance = await ctx.VDODO.methods.dodoBalanceOf(user).call(); + let availableBalance = await ctx.VDODO.methods.availableBalanceOf(user).call(); + console.log("dodoBalance:" + fromWei(dodoBalance, "ether") + " availableBalance:" + fromWei(availableBalance, "ether")); + return [dodoBalance, availableBalance] +} + +async function getRewardInfo(ctx: DODOMineV2Context, idx: number, user: string, logInfo?: string) { + let VDODOMine = ctx.VDODOMine + let obj = await VDODOMine.methods.rewardTokenInfos(idx).call(); + let curBlock = await ctx.Web3.eth.getBlockNumber(); + console.log(logInfo); + // console.log("Static-Data: rewardToken:" + obj.rewardToken + " rewardVault:" + obj.rewardVault + " rewardPerBlock:" + fromWei(obj.rewardPerBlock, "ether")); + console.log("Dynamic-Data: start:" + obj.startBlock + " end:" + obj.endBlock + " accRewardPerShare:" + fromWei(obj.accRewardPerShare, "ether") + " lastRewardBlock:" + obj.lastRewardBlock + " curBlock:" + curBlock); + var pendingReward = null; + if (user != null) { + pendingReward = await VDODOMine.methods.getPendingReward(user, idx).call(); + console.log("User-pendingReward:" + fromWei(pendingReward, "ether")); + } + return [obj, pendingReward]; +} + +describe("VDODOMine", () => { + let snapshotId: string; + let ctx: DODOMineV2Context; + let ctxVdodo: VDODOContext; + + before(async () => { + ctxVdodo = await getVDODOContext(); + ctx = await getDODOMineContext(ctxVdodo.VDODO.options.address); + await init(ctx); + await initVdodo(ctxVdodo); + await ctxVdodo.VDODO.methods.updateGovernance(ctx.VDODOMine.options.address).send(ctxVdodo.sendParam(ctxVdodo.Deployer)); + }); + + beforeEach(async () => { + snapshotId = await ctx.EVM.snapshot(); + }); + + afterEach(async () => { + await ctx.EVM.reset(snapshotId); + }); + + describe("VDODOMine", () => { + + it("deposit", async () => { + await mint(ctxVdodo, account0, decimalStr("10000"), dodoTeam); + await vdodoBalance(ctxVdodo, account0, "vDODOBalance - before"); + + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 102, decimalStr("10")); + await stakeInfo(ctx, account0, "UserStakeInfo - Before"); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - Before"); + + //增加区块 + await ctx.increBlock(3); + + await logGas(await ctx.VDODOMine.methods.deposit( + decimalStr("5") + ), ctx.sendParam(account0), "deposit - 0"); + + await logGas(await ctx.VDODOMine.methods.deposit( + decimalStr("5") + ), ctx.sendParam(account0), "deposit - 1"); + + await stakeInfo(ctx, account0, "UserStakeInfo - After"); + let [, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After - 1"); + let [, availableBalance] = await vdodoBalance(ctxVdodo, account0, "vDODOBalance - after"); + + assert.equal(pendingReward, decimalStr("10")); + assert.equal(availableBalance, "90063636363636363600"); + }); + + + it("withdraw", async () => { + await mint(ctxVdodo, account0, decimalStr("10000"), dodoTeam); + await vdodoBalance(ctxVdodo, account0, "vDODOBalance - before"); + + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 102, decimalStr("10")); + await stakeInfo(ctx, account0, "UserStakeInfo - Before"); + await getRewardInfo(ctx, 0, account0, "UserRewardInfo - Before"); + + await ctx.increBlock(3); + + await logGas(await ctx.VDODOMine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit"); + + await ctx.increBlock(3); + + await logGas(await ctx.VDODOMine.methods.withdraw( + decimalStr("5") + ), ctx.sendParam(account0), "withdraw"); + + await stakeInfo(ctx, account0, "UserStakeInfo - After"); + let [, pendingReward] = await getRewardInfo(ctx, 0, account0, "UserRewardInfo - After"); + let [, availableBalance] = await vdodoBalance(ctxVdodo, account0, "vDODOBalance - after"); + + assert.equal(pendingReward, decimalStr("40")); + assert.equal(availableBalance, "95090909090909090900"); + }); + + + it("revert case", async () => { + await mint(ctxVdodo, account0, decimalStr("10000"), dodoTeam); + + var curBlock = await ctx.Web3.eth.getBlockNumber(); + await addRewardToken(ctx, ctx.REWARD_1, curBlock + 2, curBlock + 102, decimalStr("10")); + + //增加区块 + await ctx.increBlock(3); + + await logGas(await ctx.VDODOMine.methods.deposit( + decimalStr("10") + ), ctx.sendParam(account0), "deposit - 0"); + + await truffleAssert.reverts( + ctxVdodo.VDODO.methods.redeem(decimalStr("95"), false).send(ctxVdodo.sendParam(account0)), + "vDODOToken: available amount not enough" + ) + }) + }) +}); diff --git a/test/DSP/funding.test.ts b/test/DSP/funding.test.ts new file mode 100644 index 0000000..7017ec7 --- /dev/null +++ b/test/DSP/funding.test.ts @@ -0,0 +1,188 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +// import * as assert from 'assert'; + +import { decimalStr, MAX_UINT256 } from '../utils/Converter'; +import { logGas } from '../utils/Log'; +import { DSPContext, getDSPContext } from '../utils/DSPContext'; +import { assert } from 'chai'; +import BigNumber from 'bignumber.js'; +const truffleAssert = require('truffle-assertions'); + +let lp: string; +let trader: string; + +async function init(ctx: DSPContext): Promise { + lp = ctx.SpareAccounts[0]; + trader = ctx.SpareAccounts[1]; + + await ctx.mintTestToken(lp, decimalStr("1000"), decimalStr("1000")); + await ctx.mintTestToken(trader, decimalStr("1000"), decimalStr("1000")); +} + +describe("Funding", () => { + let snapshotId: string; + let ctx: DSPContext; + + before(async () => { + ctx = await getDSPContext(); + await init(ctx); + }); + + beforeEach(async () => { + snapshotId = await ctx.EVM.snapshot(); + }); + + afterEach(async () => { + await ctx.EVM.reset(snapshotId); + }); + + describe("buy shares", () => { + + it("revert cases", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("10")) + await truffleAssert.reverts( + ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)), + "MINT_AMOUNT_NOT_ENOUGH" + ) + }) + + it("buy shares from init states", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("100")) + await ctx.transferQuoteToDSP(lp, decimalStr("100")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)); + assert.equal(await ctx.DSP.methods.balanceOf(lp).call(), decimalStr("100")) + assert.equal(await ctx.DSP.methods.getMidPrice().call(), decimalStr("1")) + }) + + it("buy shares with balanced input", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("100")) + await ctx.transferQuoteToDSP(lp, decimalStr("100")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)) + + await ctx.transferQuoteToDSP(trader, decimalStr("20")) + await ctx.DSP.methods.sellQuote(trader).send(ctx.sendParam(trader)) + + var vaultBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(ctx.DSP.options.address).call()) + var vaultQuoteBalance = new BigNumber(await ctx.QUOTE.methods.balanceOf(ctx.DSP.options.address).call()) + var increaseRatio = new BigNumber("0.1") + + await ctx.transferBaseToDSP(trader, vaultBaseBalance.multipliedBy(increaseRatio).toFixed(0)) + await ctx.transferQuoteToDSP(trader, vaultQuoteBalance.multipliedBy(increaseRatio).toFixed(0)) + await ctx.DSP.methods.buyShares(trader).send(ctx.sendParam(trader)) + + assert.equal( + await ctx.BASE.methods.balanceOf(ctx.DSP.options.address).call(), + "88521163953680151790" + ); + assert.equal( + await ctx.QUOTE.methods.balanceOf(ctx.DSP.options.address).call(), + "132000000000000000000" + ); + + assert.equal(await ctx.DSP.methods.balanceOf(trader).call(), decimalStr("10")) + }) + + it("buy shares with unbalanced input (less quote)", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("100")) + await ctx.transferQuoteToDSP(lp, decimalStr("100")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)) + + await ctx.transferQuoteToDSP(trader, decimalStr("20")) + await ctx.DSP.methods.sellQuote(trader).send(ctx.sendParam(trader)) + + var vaultBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(ctx.DSP.options.address).call()) + var vaultQuoteBalance = new BigNumber(await ctx.QUOTE.methods.balanceOf(ctx.DSP.options.address).call()) + var increaseRatio = new BigNumber("0.1") + + await ctx.transferBaseToDSP(trader, vaultBaseBalance.multipliedBy(increaseRatio).toFixed(0)) + await ctx.transferQuoteToDSP(trader, vaultQuoteBalance.multipliedBy(increaseRatio).div(2).toFixed(0)) + await ctx.DSP.methods.buyShares(trader).send(ctx.sendParam(trader)) + + assert.equal(await ctx.DSP.methods.balanceOf(trader).call(), decimalStr("5")) + }) + + it("buy shares with unbalanced input (less base)", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("100")) + await ctx.transferQuoteToDSP(lp, decimalStr("100")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)) + + await ctx.transferQuoteToDSP(trader, decimalStr("20")) + await ctx.DSP.methods.sellQuote(trader).send(ctx.sendParam(trader)) + + var vaultBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(ctx.DSP.options.address).call()) + var vaultQuoteBalance = new BigNumber(await ctx.QUOTE.methods.balanceOf(ctx.DSP.options.address).call()) + var increaseRatio = new BigNumber("0.1") + + await ctx.transferBaseToDSP(trader, vaultBaseBalance.multipliedBy(increaseRatio).div(2).toFixed(0)) + await ctx.transferQuoteToDSP(trader, vaultQuoteBalance.multipliedBy(increaseRatio).toFixed(0)) + await ctx.DSP.methods.buyShares(trader).send(ctx.sendParam(trader)) + + assert.equal(await ctx.DSP.methods.balanceOf(trader).call(), "4999999999999999900") + }) + }); + + describe("sell shares", () => { + it("not the last one sell shares", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("100")) + await ctx.transferQuoteToDSP(lp, decimalStr("100")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)) + + await ctx.transferBaseToDSP(trader, decimalStr("10")) + await ctx.transferQuoteToDSP(trader, decimalStr("10")) + await ctx.DSP.methods.buyShares(trader).send(ctx.sendParam(trader)) + + var vaultShares = new BigNumber(await ctx.DSP.methods.balanceOf(lp).call()) + var bob = ctx.SpareAccounts[5] + await ctx.DSP.methods.sellShares(vaultShares.div(2).toFixed(0), bob, 0, 0, "0x", MAX_UINT256).send(ctx.sendParam(lp)) + assert.equal(await ctx.BASE.methods.balanceOf(bob).call(), decimalStr("50")) + assert.equal(await ctx.QUOTE.methods.balanceOf(bob).call(), decimalStr("50")) + + await ctx.DSP.methods.sellShares(vaultShares.div(2).toFixed(0), bob, 0, 0, "0x", MAX_UINT256).send(ctx.sendParam(lp)) + assert.equal(await ctx.BASE.methods.balanceOf(bob).call(), decimalStr("100")) + assert.equal(await ctx.QUOTE.methods.balanceOf(bob).call(), decimalStr("100")) + }) + + it("the last one sell shares", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("100")) + await ctx.transferQuoteToDSP(lp, decimalStr("100")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)) + + var vaultShares = await ctx.DSP.methods.balanceOf(lp).call() + var bob = ctx.SpareAccounts[5] + await ctx.DSP.methods.sellShares(vaultShares, bob, 0, 0, "0x", MAX_UINT256).send(ctx.sendParam(lp)) + assert.equal(await ctx.BASE.methods.balanceOf(bob).call(), decimalStr("100")) + assert.equal(await ctx.QUOTE.methods.balanceOf(bob).call(), decimalStr("100")) + }) + + it("revert cases", async () => { + await ctx.transferBaseToDSP(lp, decimalStr("100")) + await ctx.transferQuoteToDSP(lp, decimalStr("100")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)) + + var vaultShares = await ctx.DSP.methods.balanceOf(lp).call() + var bob = ctx.SpareAccounts[5] + await truffleAssert.reverts( + ctx.DSP.methods.sellShares(new BigNumber(vaultShares).multipliedBy(2), bob, 0, 0, "0x", MAX_UINT256).send(ctx.sendParam(lp)), + "DLP_NOT_ENOUGH" + ) + await truffleAssert.reverts( + ctx.DSP.methods.sellShares(vaultShares, bob, decimalStr("1000"), 0, "0x", MAX_UINT256).send(ctx.sendParam(lp)), + "WITHDRAW_NOT_ENOUGH" + ) + await truffleAssert.reverts( + ctx.DSP.methods.sellShares(vaultShares, bob, 0, decimalStr("1000"), "0x", MAX_UINT256).send(ctx.sendParam(lp)), + "WITHDRAW_NOT_ENOUGH" + ) + await truffleAssert.reverts( + ctx.DSP.methods.sellShares(vaultShares, bob, 0, decimalStr("10000"), "0x", "0").send(ctx.sendParam(lp)), + "TIME_EXPIRED" + ) + }) + }) +}); diff --git a/test/DSP/trader.test.ts b/test/DSP/trader.test.ts new file mode 100644 index 0000000..1c0ec17 --- /dev/null +++ b/test/DSP/trader.test.ts @@ -0,0 +1,192 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +// import * as assert from 'assert'; + +import { decimalStr, gweiStr } from '../utils/Converter'; +import { logGas } from '../utils/Log'; +import { DSPContext, getDSPContext } from '../utils/DSPContext'; +import { assert } from 'chai'; +const truffleAssert = require('truffle-assertions'); + +let lp: string; +let trader: string; + +async function init(ctx: DSPContext): Promise { + lp = ctx.Deployer + trader = ctx.SpareAccounts[1]; + + await ctx.mintTestToken(lp, decimalStr("1000"), decimalStr("1000")); + await ctx.mintTestToken(trader, decimalStr("1000"), decimalStr("1000")); + + await ctx.transferBaseToDSP(lp, decimalStr("1000")) + await ctx.transferQuoteToDSP(lp, decimalStr("1000")) + await ctx.DSP.methods.buyShares(lp).send(ctx.sendParam(lp)) +} + +describe("DSP Trader", () => { + let snapshotId: string; + let ctx: DSPContext; + + before(async () => { + ctx = await getDSPContext(); + await init(ctx); + }); + + beforeEach(async () => { + snapshotId = await ctx.EVM.snapshot(); + }); + + afterEach(async () => { + await ctx.EVM.reset(snapshotId); + }); + + describe("trade", () => { + + it("first buy and then sell", async () => { + // buy at R=1 + await ctx.transferQuoteToDSP(trader, decimalStr("100")) + await logGas(ctx.DSP.methods.sellQuote(trader), ctx.sendParam(trader), "sellQuote - buy at R=1") + var balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, "1098914196817061816111") + assert.equal(balances.traderQuote, decimalStr("900")) + assert.equal(balances.DSPBase, "901085803182938183889") + assert.equal(balances.DSPQuote, decimalStr("1100")) + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + // buy at R>1 + await ctx.transferQuoteToDSP(trader, decimalStr("100")) + await logGas(ctx.DSP.methods.sellQuote(trader), ctx.sendParam(trader), "sellQuote - buy at R>1") + balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, "1195262145875634983260") + assert.equal(balances.traderQuote, decimalStr("800")) + assert.equal(balances.DSPBase, "804737854124365016740") + assert.equal(balances.DSPQuote, decimalStr("1200")) + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + // sell at R>1 and R not change state + await ctx.transferBaseToDSP(trader, decimalStr("100")) + await logGas(ctx.DSP.methods.sellBase(trader), ctx.sendParam(trader), "sellBase - sell at R>1 and R not change state") + balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, "1095262145875634983260") + assert.equal(balances.traderQuote, "903734814802481693100") + assert.equal(balances.DSPBase, "904737854124365016740") + assert.equal(balances.DSPQuote, "1096265185197518306900") + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + + // sell at R>1 and R change state + await ctx.transferBaseToDSP(trader, decimalStr("200")) + + await logGas(ctx.DSP.methods.sellBase(trader), ctx.sendParam(trader), "sellBase - sell at R>1 and R change state") + balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, "895262145875634983260") + assert.equal(balances.traderQuote, "1103541932946094354686") + assert.equal(balances.DSPBase, "1104737854124365016740") + assert.equal(balances.DSPQuote, "896458067053905645314") + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + var PMMStat = await ctx.DSP.methods.getPMMState().call() + assert.equal(PMMStat.R, "2") + assert.equal(PMMStat.B0, "999999999999999996713") + }); + + it("first sell and then buy", async () => { + // sell at R=1 + await ctx.transferBaseToDSP(trader, decimalStr("1")) + await logGas(ctx.DSP.methods.sellBase(trader), ctx.sendParam(trader), "sellBase - sell at R=1") + var balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, decimalStr("999")) + assert.equal(balances.traderQuote, "1000999899919944970392") + assert.equal(balances.DSPBase, decimalStr("1001")) + assert.equal(balances.DSPQuote, "999000100080055029608") + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + // buy at R>1 + await ctx.transferBaseToDSP(trader, decimalStr("1")) + await logGas(ctx.DSP.methods.sellBase(trader), ctx.sendParam(trader), "sellBase - buy at R>1") + balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, decimalStr("998")) + assert.equal(balances.traderQuote, "1001999599359119051790") + assert.equal(balances.DSPBase, decimalStr("1002")) + assert.equal(balances.DSPQuote, "998000400640880948210") + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + // sell at R>1 and R not change state + await ctx.transferQuoteToDSP(trader, decimalStr("1")) + await logGas(ctx.DSP.methods.sellQuote(trader), ctx.sendParam(trader), "sell at R>1 and R not change state") + balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, "999000300621013276966") + assert.equal(balances.traderQuote, "1000999599359119051790") + assert.equal(balances.DSPBase, "1000999699378986723034") + assert.equal(balances.DSPQuote, "999000400640880948210") + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + // sell at R>1 and R change state + await ctx.transferQuoteToDSP(trader, decimalStr("2")) + await logGas(ctx.DSP.methods.sellQuote(trader), ctx.sendParam(trader), "sell at R>1 and R change state") + balances = await ctx.getBalances(trader) + + assert.equal(balances.traderBase, "1001000300480585414741") + assert.equal(balances.traderQuote, "998999599359119051790") + assert.equal(balances.DSPBase, "998999699519414585259") + assert.equal(balances.DSPQuote, "1001000400640880948210") + assert.equal(balances.maintainerBase, "0") + assert.equal(balances.maintainerQuote, "0") + + var PMMStat = await ctx.DSP.methods.getPMMState().call() + assert.equal(PMMStat.R, "1") + assert.equal(PMMStat.Q0, "999999999999999995766") + }); + + it("flash loan", async () => { + // buy + await ctx.transferQuoteToDSP(trader, decimalStr("100")) + + // buy failed + await truffleAssert.reverts(ctx.DSP.methods.flashLoan("901085803182938100000", decimalStr("101"), trader, "0x").send(ctx.sendParam(trader)), "FLASH_LOAN_FAILED") + + // buy succeed + await ctx.DSP.methods.flashLoan("98914196817061816111", "0", trader, "0x").send(ctx.sendParam(trader)) + + // trader balances + assert.equal( + await ctx.BASE.methods.balanceOf(trader).call(), + "1098914196817061816111" + ); + + // sell + await ctx.transferBaseToDSP(trader, decimalStr("1")) + + // sell failed + await truffleAssert.reverts(ctx.DSP.methods.flashLoan(decimalStr("2"), "1", trader, "0x").send(ctx.sendParam(trader)), "FLASH_LOAN_FAILED") + + // sell succeed + await ctx.DSP.methods.flashLoan("0", "999899919944970392", trader, "0x").send(ctx.sendParam(trader)) + + // trader balances + assert.equal( + await ctx.QUOTE.methods.balanceOf(trader).call(), + "900999899919944970392" + ); + }) + }); +}); diff --git a/test/utils/Contracts.ts b/test/utils/Contracts.ts index a3e17fd..e560aad 100644 --- a/test/utils/Contracts.ts +++ b/test/utils/Contracts.ts @@ -38,6 +38,8 @@ export const EXTERNAL_VALUE_NAME = "ExternalValue" export const FEE_RATE_MODEL_NAME = "FeeRateModel" export const DPP_NAME = "DPP" export const DPP_FACTORY_NAME = "DPPFactory" +export const DSP_NAME = "DSP" +export const DSP_FACTORY_NAME = "DSPFactory" export const SMART_APPROVE = "DODOApprove" export const SMART_APPROVE_PROXY = "DODOApproveProxy" export const DODO_SELL_HELPER = "DODOSellHelper" @@ -50,6 +52,8 @@ export const VDODO_NAME = "vDODOToken" export const DODO_CULATION_HELPER = "DODOCirculationHelper" export const DODO_GOVERNANCE = "Governance" export const DODO_PROXY_NAME = "DODOV2Proxy02" +export const ERC20_MINE = "ERC20Mine" +export const VDODO_MINE = "vDODOMine" interface ContractJson { abi: any; diff --git a/test/utils/DODOMineV2Context.ts b/test/utils/DODOMineV2Context.ts new file mode 100644 index 0000000..5cd1a96 --- /dev/null +++ b/test/utils/DODOMineV2Context.ts @@ -0,0 +1,114 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +import BigNumber from 'bignumber.js'; +import Web3 from 'web3'; +import { Contract } from 'web3-eth-contract'; + +import * as contracts from './Contracts'; +import { decimalStr, mweiStr, MAX_UINT256 } from './Converter'; +import { EVM, getDefaultWeb3 } from './EVM'; +import * as log from './Log'; + +BigNumber.config({ + EXPONENTIAL_AT: 1000, + DECIMAL_PLACES: 80, +}); + + +export class DODOMineV2Context { + EVM: EVM; + Web3: Web3; + + //contract + ERC20Mine: Contract; + VDODOMine: Contract; + + //account + Deployer: string; + Maintainer: string; + SpareAccounts: string[]; + + //token + REWARD_1: Contract; + REWARD_2: Contract; + ERC20: Contract; + + + async init(vdodo: string) { + this.EVM = new EVM(); + this.Web3 = getDefaultWeb3(); + + const allAccounts = await this.Web3.eth.getAccounts(); + this.Deployer = allAccounts[0]; + this.Maintainer = allAccounts[1]; + this.SpareAccounts = allAccounts.slice(2, 10); + + this.ERC20 = await contracts.newContract( + contracts.MINTABLE_ERC20_CONTRACT_NAME, + ["ERC20 Token", "ERC20", 18] + ); + + this.REWARD_1 = await contracts.newContract( + contracts.MINTABLE_ERC20_CONTRACT_NAME, + ["REWARD_1 Token", "REWARD_1", 18] + ); + + this.REWARD_2 = await contracts.newContract( + contracts.MINTABLE_ERC20_CONTRACT_NAME, + ["REWARD_2 Token", "REWARD_2", 18] + ); + + if (vdodo != null) { + this.VDODOMine = await contracts.newContract( + contracts.VDODO_MINE, + [vdodo] + ); + await this.VDODOMine.methods.initOwner(this.Deployer).send(this.sendParam(this.Deployer)); + } + + this.ERC20Mine = await contracts.newContract( + contracts.ERC20_MINE, + [this.ERC20.options.address] + ); + + await this.ERC20Mine.methods.initOwner(this.Deployer).send(this.sendParam(this.Deployer)); + + console.log(log.blueText("[Init DODOMine context]")); + } + + sendParam(sender, value = "0") { + return { + from: sender, + gas: process.env["COVERAGE"] ? 10000000000 : 7000000, + gasPrice: mweiStr("1000"), + value: decimalStr(value), + }; + } + + async mintTestToken(to: string, token: Contract, amount: string) { + await token.methods.mint(to, amount).send(this.sendParam(this.Deployer)); + } + + async increBlock(num: number) { + for (let i = 0; i < num; i++) { + await this.mintTestToken(this.Deployer, this.ERC20, decimalStr("0")); + } + } + + async approveProxy(account: string, target: string, token: Contract) { + await token.methods + .approve(target, MAX_UINT256) + .send(this.sendParam(account)); + } +} + +export async function getDODOMineContext(vdodo: string): Promise { + var context = new DODOMineV2Context(); + await context.init(vdodo); + return context; +} \ No newline at end of file diff --git a/test/utils/DSPContext.ts b/test/utils/DSPContext.ts new file mode 100644 index 0000000..25bda2e --- /dev/null +++ b/test/utils/DSPContext.ts @@ -0,0 +1,154 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +import BigNumber from 'bignumber.js'; +import Web3 from 'web3'; +import { Contract } from 'web3-eth-contract'; + +import * as contracts from './Contracts'; +import { decimalStr, MAX_UINT256 } from './Converter'; +import { EVM, getDefaultWeb3 } from './EVM'; +import * as log from './Log'; + +BigNumber.config({ + EXPONENTIAL_AT: 1000, + DECIMAL_PLACES: 80, +}); + +export interface DSPContextBalances { + traderBase: string, + traderQuote: string, + DSPBase: string, + DSPQuote: string, + maintainerBase: string, + maintainerQuote: string +} + +export interface DSPContextInitConfig { + lpFeeRate: string; + mtFeeRate: string; + k: string; + i: string; +} + +/* + price curve when k=0.1 + +──────────────────────+───────────────+ + | purchase percentage | avg slippage | + +──────────────────────+───────────────+ + | 1% | 0.1% | + | 5% | 0.5% | + | 10% | 1.1% | + | 20% | 2.5% | + | 50% | 10% | + | 70% | 23.3% | + +──────────────────────+───────────────+ +*/ +export let DefaultDSPContextInitConfig = { + lpFeeRate: decimalStr("0.002"), + mtFeeRate: decimalStr("0.001"), + k: decimalStr("0.1"), + i: decimalStr("1"), +}; + +export class DSPContext { + EVM: EVM; + Web3: Web3; + DSP: Contract; + BASE: Contract; + QUOTE: Contract; + Deployer: string; + Maintainer: string; + MtFeeRate: string; + SpareAccounts: string[]; + + mtFeeRateModel: Contract; + + + constructor() { } + + async init(config: DSPContextInitConfig) { + this.EVM = new EVM(); + this.Web3 = getDefaultWeb3(); + + this.DSP = await contracts.newContract(contracts.DSP_NAME) + var mtFeeRateModel = await contracts.newContract(contracts.FEE_RATE_MODEL_NAME) + this.mtFeeRateModel = mtFeeRateModel; + this.MtFeeRate = mtFeeRateModel.options.address + + this.BASE = await contracts.newContract( + contracts.MINTABLE_ERC20_CONTRACT_NAME, + ["TestBase", "BASE", 18] + ); + this.QUOTE = await contracts.newContract( + contracts.MINTABLE_ERC20_CONTRACT_NAME, + ["TestQuote", "QUOTE", 18] + ); + + const allAccounts = await this.Web3.eth.getAccounts(); + this.Deployer = allAccounts[0]; + this.Maintainer = allAccounts[1]; + this.SpareAccounts = allAccounts.slice(2, 10); + + await this.DSP.methods.init( + this.Maintainer, + this.BASE.options.address, + this.QUOTE.options.address, + 0, + mtFeeRateModel.options.address, + config.i, + config.k, + true + ).send(this.sendParam(this.Deployer)) + + console.log(log.blueText("[Init DSP context]")); + } + + sendParam(sender, value = "0") { + return { + from: sender, + gas: process.env["COVERAGE"] ? 10000000000 : 7000000, + gasPrice: process.env.GAS_PRICE, + value: decimalStr(value), + }; + } + + async mintTestToken(to: string, base: string, quote: string) { + await this.BASE.methods.mint(to, base).send(this.sendParam(this.Deployer)); + await this.QUOTE.methods + .mint(to, quote) + .send(this.sendParam(this.Deployer)); + } + + async transferBaseToDSP(account: string, amount: string) { + await this.BASE.methods.transfer(this.DSP.options.address, amount).send(this.sendParam(account)) + } + + async transferQuoteToDSP(account: string, amount: string) { + await this.QUOTE.methods.transfer(this.DSP.options.address, amount).send(this.sendParam(account)) + } + + async getBalances(trader: string) { + var balances: DSPContextBalances = { + traderBase: await this.BASE.methods.balanceOf(trader).call(), + traderQuote: await this.QUOTE.methods.balanceOf(trader).call(), + DSPBase: await this.BASE.methods.balanceOf(this.DSP.options.address).call(), + DSPQuote: await this.QUOTE.methods.balanceOf(this.DSP.options.address).call(), + maintainerBase: await this.BASE.methods.balanceOf(this.Maintainer).call(), + maintainerQuote: await this.QUOTE.methods.balanceOf(this.Maintainer).call() + }; + return balances; + } +} + +export async function getDSPContext( + config: DSPContextInitConfig = DefaultDSPContextInitConfig +): Promise { + var context = new DSPContext(); + await context.init(config); + return context; +} 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: { diff --git a/truffle-test.sh b/truffle-test.sh index a5ad56a..9d738d7 100644 --- a/truffle-test.sh +++ b/truffle-test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# truffle compile --all +truffle compile --all if [ "$1"x = "proxy-dpp"x ] then @@ -46,6 +46,15 @@ then truffle test ./test/vDODO/mintRedeem.test.ts fi +if [ "$1"x = "erc20-mine"x ] +then + truffle test ./test/DODOMineV2/erc20Mine.test.ts +fi + +if [ "$1"x = "vdodo-mine"x ] +then + truffle test ./test/DODOMineV2/vDODOMine.test.ts +fi # if [ "$1"x = "route-incentive"x ] # then