Merge pull request #12 from DODOEX/feature/mineV2AndDSP

Feature/mine v2 and dsp
This commit is contained in:
owen05
2021-04-12 09:34:51 +08:00
committed by GitHub
26 changed files with 2693 additions and 8 deletions

View File

@@ -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";
}
}

View File

@@ -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 Happenreserve >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]);
}
}

View File

@@ -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());
}
}

View File

@@ -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<quoteReserve
require(
uint256(_BASE_RESERVE_).sub(baseBalance) <= receiveBaseAmount,
"FLASH_LOAN_FAILED"
);
_transferBaseOut(_MAINTAINER_, mtFee);
if (_RState_ != uint32(newRState)) {
require(newQuoteTarget <= uint112(-1), "OVERFLOW");
_QUOTE_TARGET_ = uint112(newQuoteTarget);
_RState_ = uint32(newRState);
emit RChange(newRState);
}
emit DODOSwap(
address(_QUOTE_TOKEN_),
address(_BASE_TOKEN_),
quoteInput,
receiveBaseAmount,
msg.sender,
assetTo
);
}
// sell base case
// base input + quote output
if (quoteBalance < _QUOTE_RESERVE_) {
uint256 baseInput = baseBalance.sub(uint256(_BASE_RESERVE_));
(
uint256 receiveQuoteAmount,
uint256 mtFee,
PMMPricing.RState newRState,
uint256 newBaseTarget
) = querySellBase(tx.origin, baseInput); // revert if baseBalance<baseReserve
require(
uint256(_QUOTE_RESERVE_).sub(quoteBalance) <= receiveQuoteAmount,
"FLASH_LOAN_FAILED"
);
_transferQuoteOut(_MAINTAINER_, mtFee);
if (_RState_ != uint32(newRState)) {
require(newBaseTarget <= uint112(-1), "OVERFLOW");
_BASE_TARGET_ = uint112(newBaseTarget);
_RState_ = uint32(newRState);
emit RChange(newRState);
}
emit DODOSwap(
address(_BASE_TOKEN_),
address(_QUOTE_TOKEN_),
baseInput,
receiveQuoteAmount,
msg.sender,
assetTo
);
}
_sync();
emit DODOFlashLoan(msg.sender, assetTo, baseAmount, quoteAmount);
}
// ============ Query Functions ============
function querySellBase(address trader, uint256 payBaseAmount)
public
view
returns (
uint256 receiveQuoteAmount,
uint256 mtFee,
PMMPricing.RState newRState,
uint256 newBaseTarget
)
{
PMMPricing.PMMState memory state = getPMMState();
(receiveQuoteAmount, newRState) = PMMPricing.sellBaseToken(state, payBaseAmount);
uint256 lpFeeRate = _LP_FEE_RATE_;
uint256 mtFeeRate = _MT_FEE_RATE_MODEL_.getFeeRate(trader);
mtFee = DecimalMath.mulFloor(receiveQuoteAmount, mtFeeRate);
receiveQuoteAmount = receiveQuoteAmount
.sub(DecimalMath.mulFloor(receiveQuoteAmount, lpFeeRate))
.sub(mtFee);
newBaseTarget = state.B0;
}
function querySellQuote(address trader, uint256 payQuoteAmount)
public
view
returns (
uint256 receiveBaseAmount,
uint256 mtFee,
PMMPricing.RState newRState,
uint256 newQuoteTarget
)
{
PMMPricing.PMMState memory state = getPMMState();
(receiveBaseAmount, newRState) = PMMPricing.sellQuoteToken(state, payQuoteAmount);
uint256 lpFeeRate = _LP_FEE_RATE_;
uint256 mtFeeRate = _MT_FEE_RATE_MODEL_.getFeeRate(trader);
mtFee = DecimalMath.mulFloor(receiveBaseAmount, mtFeeRate);
receiveBaseAmount = receiveBaseAmount
.sub(DecimalMath.mulFloor(receiveBaseAmount, lpFeeRate))
.sub(mtFee);
newQuoteTarget = state.Q0;
}
}

View File

@@ -0,0 +1,236 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {IERC20} from "../../intf/IERC20.sol";
import {SafeMath} from "../../lib/SafeMath.sol";
import {DecimalMath} from "../../lib/DecimalMath.sol";
import {SafeERC20} from "../../lib/SafeERC20.sol";
import {DSPStorage} from "./DSPStorage.sol";
contract DSPVault is DSPStorage {
using SafeMath for uint256;
using SafeERC20 for IERC20;
// ============ Events ============
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
event Mint(address indexed user, uint256 value);
event Burn(address indexed user, uint256 value);
// ============ View Functions ============
function getVaultReserve() external view returns (uint256 baseReserve, uint256 quoteReserve) {
baseReserve = _BASE_RESERVE_;
quoteReserve = _QUOTE_RESERVE_;
}
function getUserFeeRate(address user)
external
view
returns (uint256 lpFeeRate, uint256 mtFeeRate)
{
lpFeeRate = _LP_FEE_RATE_;
mtFeeRate = _MT_FEE_RATE_MODEL_.getFeeRate(user);
}
// ============ Asset In ============
function getBaseInput() public view returns (uint256 input) {
return _BASE_TOKEN_.balanceOf(address(this)).sub(uint256(_BASE_RESERVE_));
}
function getQuoteInput() public view returns (uint256 input) {
return _QUOTE_TOKEN_.balanceOf(address(this)).sub(uint256(_QUOTE_RESERVE_));
}
// ============ TWAP UPDATE ===========
function _twapUpdate() internal {
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - _BLOCK_TIMESTAMP_LAST_;
if (timeElapsed > 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);
}
}

View File

@@ -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);
}

View File

@@ -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<rewardTokenInfos.length, "DODOMineV2: REWARD_ID_NOT_FOUND");
RewardTokenInfo storage rt = rewardTokenInfos[i];
uint256 accRewardPerShare = rt.accRewardPerShare;
if (rt.lastRewardBlock != block.number) {
accRewardPerShare = _getAccRewardPerShare(i);
}
return
DecimalMath.mulFloor(
balanceOf(user),
accRewardPerShare.sub(rt.userRewardPerSharePaid[user])
).add(rt.userRewards[user]);
}
function getPendingRewardByToken(address user, address rewardToken) external view returns (uint256) {
return getPendingReward(user, getIdByRewardToken(rewardToken));
}
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
function balanceOf(address user) public view returns (uint256) {
return _balances[user];
}
function getRewardTokenById(uint256 i) external view returns (address) {
require(i<rewardTokenInfos.length, "DODOMineV2: REWARD_ID_NOT_FOUND");
RewardTokenInfo memory rt = rewardTokenInfos[i];
return rt.rewardToken;
}
function getIdByRewardToken(address rewardToken) public view returns(uint256) {
uint256 len = rewardTokenInfos.length;
for (uint256 i = 0; i < len; i++) {
if (rewardToken == rewardTokenInfos[i].rewardToken) {
return i;
}
}
require(false, "DODOMineV2: TOKEN_NOT_FOUND");
}
// ============ Claim ============
function claimReward(uint256 i) public {
require(i<rewardTokenInfos.length, "DODOMineV2: REWARD_ID_NOT_FOUND");
_updateReward(msg.sender, i);
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 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())
);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;

View File

@@ -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];
}
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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);

View File

@@ -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<void> {
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");
})
})
});

View File

@@ -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<void> {
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<void> {
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"
)
})
})
});

188
test/DSP/funding.test.ts Normal file
View File

@@ -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<void> {
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"
)
})
})
});

192
test/DSP/trader.test.ts Normal file
View File

@@ -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<void> {
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"
);
})
});
});

View File

@@ -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;

View File

@@ -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<DODOMineV2Context> {
var context = new DODOMineV2Context();
await context.init(vdodo);
return context;
}

154
test/utils/DSPContext.ts Normal file
View File

@@ -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<DSPContext> {
var context = new DSPContext();
await context.init(config);
return context;
}

View File

@@ -54,7 +54,8 @@ module.exports = {
DVM: false,
CP: false,
CPFactory: false,
MultiCall: false
MultiCall: false,
LockedVault: false
},
networks: {

View File

@@ -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