first commit

This commit is contained in:
mingda
2020-06-26 00:31:25 +08:00
commit a1579ddb66
58 changed files with 20970 additions and 0 deletions

3
.babelrc Normal file
View File

@@ -0,0 +1,3 @@
{
"presets": ["es2015", "stage-2", "stage-3"]
}

4
.env Normal file
View File

@@ -0,0 +1,4 @@
RPC_NODE_URI=http://127.0.0.1:8545
RESET_SNAPSHOT_ID=0x2
NETWORK_ID=5777
GAS_PRICE=1

17
.eslintrc.yaml Normal file
View File

@@ -0,0 +1,17 @@
extends: airbnb-base
parser: babel-eslint
env:
node: true
es6: true
globals:
artifacts: true
rules:
no-use-before-define: 0
class-methods-use-this: 0
no-underscore-dangle: 0
max-len:
- error
- 100

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.sol linguist-language=Solidity

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
.coverage*
.DS_Store
.env.local
.idea
build/
dist/
docs/
node_modules/
coverage/
lint/
# VIM
*.swo
*.swp

15
.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"explicitTypes": "always"
}
}
]
}

16
.solcover.js Normal file
View File

@@ -0,0 +1,16 @@
module.exports = {
client: require("ganache-cli"),
port: 6545,
testrpcOptions:
"--port 6545 -l 0x1fffffffffffff -i 1002 -g 1 --allowUnlimitedContractSize",
skipFiles: [
"lib/SafeMath.sol",
"lib/DecimalMath.sol",
"lib/Types.sol",
"lib/ReentrancyGuard.sol",
"lib/Ownable.sol",
"impl/DODOLpToken.sol",
"intf",
"helper",
],
};

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# DODO10x better liquidity than uniswap
✍️[Introducing DODO](https://medium.com/@dodo.in.the.zoo/introducing-dodo-10x-better-liquidity-than-uniswap-852ce2137c57)
- Current AMM failed in providing comparable liquidity in mainstream trading pairs because AMM cannot really work as human market makers.
- DODO is based on a brand new market maker algorithm with an essential idea of risk neutrality to keep liquidity providers portfolio stable.
- Compared with AMMs, DODO will perform 10x better in liquidity.

128
contracts/DODOEthProxy.sol Normal file
View File

@@ -0,0 +1,128 @@
/*
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 {SafeERC20} from "./lib/SafeERC20.sol";
import {IDODO} from "./intf/IDODO.sol";
import {IDODOZoo} from "./intf/IDODOZoo.sol";
import {IERC20} from "./intf/IERC20.sol";
import {IWETH} from "./intf/IWETH.sol";
/**
* @title DODO Eth Proxy
* @author DODO Breeder
*
* @notice Handle ETH-WETH converting for users
*/
contract DODOEthProxy is ReentrancyGuard {
using SafeERC20 for IERC20;
address public _DODO_ZOO_;
address payable public _WETH_;
// ============ Events ============
event ProxySellEth(
address indexed seller,
address indexed quoteToken,
uint256 payEth,
uint256 receiveQuote
);
event ProxyBuyEth(
address indexed buyer,
address indexed quoteToken,
uint256 receiveEth,
uint256 payQuote
);
event ProxyDepositEth(address indexed lp, address indexed quoteToken, uint256 ethAmount);
// ============ Functions ============
constructor(address dodoZoo, address payable weth) public {
_DODO_ZOO_ = dodoZoo;
_WETH_ = weth;
}
fallback() external payable {
require(msg.sender == _WETH_, "WE_SAVED_YOUR_ETH_:)");
}
receive() external payable {
require(msg.sender == _WETH_, "WE_SAVED_YOUR_ETH_:)");
}
function sellEthTo(
address quoteTokenAddress,
uint256 ethAmount,
uint256 minReceiveTokenAmount
) external payable preventReentrant returns (uint256 receiveTokenAmount) {
require(msg.value == ethAmount, "ETH_AMOUNT_NOT_MATCH");
address DODO = IDODOZoo(_DODO_ZOO_).getDODO(_WETH_, quoteTokenAddress);
receiveTokenAmount = IDODO(DODO).querySellBaseToken(ethAmount);
require(receiveTokenAmount >= minReceiveTokenAmount, "RECEIVE_NOT_ENOUGH");
IWETH(_WETH_).deposit{value: ethAmount}();
IWETH(_WETH_).approve(DODO, ethAmount);
IDODO(DODO).sellBaseToken(ethAmount, minReceiveTokenAmount);
_transferOut(quoteTokenAddress, msg.sender, receiveTokenAmount);
emit ProxySellEth(msg.sender, quoteTokenAddress, ethAmount, receiveTokenAmount);
return receiveTokenAmount;
}
function buyEthWith(
address quoteTokenAddress,
uint256 ethAmount,
uint256 maxPayTokenAmount
) external preventReentrant returns (uint256 payTokenAmount) {
address DODO = IDODOZoo(_DODO_ZOO_).getDODO(_WETH_, quoteTokenAddress);
payTokenAmount = IDODO(DODO).queryBuyBaseToken(ethAmount);
require(payTokenAmount <= maxPayTokenAmount, "PAY_TOO_MUCH");
_transferIn(quoteTokenAddress, msg.sender, payTokenAmount);
IERC20(quoteTokenAddress).approve(DODO, payTokenAmount);
IDODO(DODO).buyBaseToken(ethAmount, maxPayTokenAmount);
IWETH(_WETH_).withdraw(ethAmount);
msg.sender.transfer(ethAmount);
emit ProxyBuyEth(msg.sender, quoteTokenAddress, ethAmount, payTokenAmount);
return payTokenAmount;
}
function depositEth(uint256 ethAmount, address quoteTokenAddress)
external
payable
preventReentrant
{
require(msg.value == ethAmount, "ETH_AMOUNT_NOT_MATCH");
address DODO = IDODOZoo(_DODO_ZOO_).getDODO(_WETH_, quoteTokenAddress);
IWETH(_WETH_).deposit{value: ethAmount}();
IWETH(_WETH_).approve(DODO, ethAmount);
IDODO(DODO).depositBaseTo(msg.sender, ethAmount);
emit ProxyDepositEth(msg.sender, quoteTokenAddress, ethAmount);
}
// ============ Helper Functions ============
function _transferIn(
address tokenAddress,
address from,
uint256 amount
) internal {
IERC20(tokenAddress).safeTransferFrom(from, address(this), amount);
}
function _transferOut(
address tokenAddress,
address to,
uint256 amount
) internal {
IERC20(tokenAddress).safeTransfer(to, amount);
}
}

78
contracts/DODOZoo.sol Normal file
View File

@@ -0,0 +1,78 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {Ownable} from "./lib/Ownable.sol";
import {IDODO} from "./intf/IDODO.sol";
import {DODO} from "./DODO.sol";
/**
* @title DODOZoo
* @author DODO Breeder
*
* @notice Register of All DODO
*/
contract DODOZoo is Ownable {
mapping(address => mapping(address => address)) internal _DODO_REGISTER_;
// ============ Events ============
event DODOBirth(address newBorn);
// ============ Breed DODO Function ============
function breedDODO(
address supervisor,
address maintainer,
address baseToken,
address quoteToken,
address oracle,
uint256 lpFeeRate,
uint256 mtFeeRate,
uint256 k,
uint256 gasPriceLimit
) public onlyOwner returns (address) {
require(!isDODORegistered(baseToken, quoteToken), "DODO_IS_REGISTERED");
require(baseToken != quoteToken, "BASE_IS_SAME_WITH_QUOTE");
address newBornDODO = address(new DODO());
IDODO(newBornDODO).init(
supervisor,
maintainer,
baseToken,
quoteToken,
oracle,
lpFeeRate,
mtFeeRate,
k,
gasPriceLimit
);
IDODO(newBornDODO).transferOwnership(_OWNER_);
_DODO_REGISTER_[baseToken][quoteToken] = newBornDODO;
emit DODOBirth(newBornDODO);
return newBornDODO;
}
// ============ View Functions ============
function isDODORegistered(address baseToken, address quoteToken) public view returns (bool) {
if (
_DODO_REGISTER_[baseToken][quoteToken] == address(0) &&
_DODO_REGISTER_[quoteToken][baseToken] == address(0)
) {
return false;
} else {
return true;
}
}
function getDODO(address baseToken, address quoteToken) external view returns (address) {
return _DODO_REGISTER_[baseToken][quoteToken];
}
}

61
contracts/dodo.sol Normal file
View File

@@ -0,0 +1,61 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {Types} from "./lib/Types.sol";
import {Storage} from "./impl/Storage.sol";
import {Trader} from "./impl/Trader.sol";
import {LiquidityProvider} from "./impl/LiquidityProvider.sol";
import {Admin} from "./impl/Admin.sol";
import {DODOLpToken} from "./impl/DODOLpToken.sol";
/**
* @title DODO
* @author DODO Breeder
*
* @notice Entrance for users
*/
contract DODO is Admin, Trader, LiquidityProvider {
function init(
address supervisor,
address maintainer,
address baseToken,
address quoteToken,
address oracle,
uint256 lpFeeRate,
uint256 mtFeeRate,
uint256 k,
uint256 gasPriceLimit
) external onlyOwner preventReentrant {
require(!_INITIALIZED_, "DODO_ALREADY_INITIALIZED");
_INITIALIZED_ = true;
_SUPERVISOR_ = supervisor;
_MAINTAINER_ = maintainer;
_BASE_TOKEN_ = baseToken;
_QUOTE_TOKEN_ = quoteToken;
_ORACLE_ = oracle;
_DEPOSIT_BASE_ALLOWED_ = true;
_DEPOSIT_QUOTE_ALLOWED_ = true;
_TRADE_ALLOWED_ = true;
_GAS_PRICE_LIMIT_ = gasPriceLimit;
_LP_FEE_RATE_ = lpFeeRate;
_MT_FEE_RATE_ = mtFeeRate;
_K_ = k;
_R_STATUS_ = Types.RStatus.ONE;
_BASE_CAPITAL_TOKEN_ = address(new DODOLpToken());
_QUOTE_CAPITAL_TOKEN_ = address(new DODOLpToken());
_checkDODOParameters();
}
}

View File

@@ -0,0 +1,34 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
contract Migrations {
address public owner;
uint256 public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) {
_;
}
}
constructor() public {
owner = msg.sender;
}
function setCompleted(uint256 completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address newAddress) public restricted {
Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(last_completed_migration);
}
}

View File

@@ -0,0 +1,25 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {Ownable} from "../lib/Ownable.sol";
// Oracle only for test
contract NaiveOracle is Ownable {
uint256 public tokenPrice;
function setPrice(uint256 newPrice) external onlyOwner {
tokenPrice = newPrice;
}
function getPrice() external view returns (uint256) {
return tokenPrice;
}
}

View File

@@ -0,0 +1,65 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
import {SafeMath} from "../lib/SafeMath.sol";
contract TestERC20 {
using SafeMath for uint256;
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) internal allowed;
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "TO_ADDRESS_IS_EMPTY");
require(amount <= balances[msg.sender], "BALANCE_NOT_ENOUGH");
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
emit Transfer(msg.sender, to, amount);
return true;
}
function balanceOf(address owner) public view returns (uint256 balance) {
return balances[owner];
}
function transferFrom(
address from,
address to,
uint256 amount
) public returns (bool) {
require(to != address(0), "TO_ADDRESS_IS_EMPTY");
require(amount <= balances[from], "BALANCE_NOT_ENOUGH");
require(amount <= allowed[from][msg.sender], "ALLOWANCE_NOT_ENOUGH");
balances[from] = balances[from].sub(amount);
balances[to] = balances[to].add(amount);
allowed[from][msg.sender] = allowed[from][msg.sender].sub(amount);
emit Transfer(from, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowed[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function allowance(address owner, address spender) public view returns (uint256) {
return allowed[owner][spender];
}
function mint(address account, uint256 amount) external {
balances[account] = balances[account].add(amount);
}
}

View File

@@ -0,0 +1,77 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
contract WETH9 {
string public name = "Wrapped Ether";
string public symbol = "WETH";
uint8 public decimals = 18;
event Approval(address indexed src, address indexed guy, uint256 wad);
event Transfer(address indexed src, address indexed dst, uint256 wad);
event Deposit(address indexed dst, uint256 wad);
event Withdrawal(address indexed src, uint256 wad);
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
fallback() external payable {
deposit();
}
receive() external payable {
deposit();
}
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 wad) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
msg.sender.transfer(wad);
emit Withdrawal(msg.sender, wad);
}
function totalSupply() public view returns (uint256) {
return address(this).balance;
}
function approve(address guy, uint256 wad) public returns (bool) {
allowance[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}
function transfer(address dst, uint256 wad) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}
function transferFrom(
address src,
address dst,
uint256 wad
) public returns (bool) {
require(balanceOf[src] >= wad);
if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) {
require(allowance[src][msg.sender] >= wad);
allowance[src][msg.sender] -= wad;
}
balanceOf[src] -= wad;
balanceOf[dst] += wad;
Transfer(src, dst, wad);
return true;
}
}

83
contracts/impl/Admin.sol Normal file
View File

@@ -0,0 +1,83 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {Storage} from "./Storage.sol";
/**
* @title Admin
* @author DODO Breeder
*
* @notice Functions for admin operations
*/
contract Admin is Storage {
// ============ Events ============
event UpdateGasPriceLimit(uint256 newGasPriceLimit);
// ============ Params Setting Functions ============
function setOracle(address newOracle) external onlyOwner {
_ORACLE_ = newOracle;
}
function setSupervisor(address newSupervisor) external onlyOwner {
_SUPERVISOR_ = newSupervisor;
}
function setMaintainer(address newMaintainer) external onlyOwner {
_MAINTAINER_ = newMaintainer;
}
function setLiquidityProviderFeeRate(uint256 newLiquidityPorviderFeeRate) external onlyOwner {
_LP_FEE_RATE_ = newLiquidityPorviderFeeRate;
_checkDODOParameters();
}
function setMaintainerFeeRate(uint256 newMaintainerFeeRate) external onlyOwner {
_MT_FEE_RATE_ = newMaintainerFeeRate;
_checkDODOParameters();
}
function setK(uint256 newK) external onlyOwner {
_K_ = newK;
_checkDODOParameters();
}
function setGasPriceLimit(uint256 newGasPriceLimit) external onlySupervisorOrOwner {
_GAS_PRICE_LIMIT_ = newGasPriceLimit;
emit UpdateGasPriceLimit(newGasPriceLimit);
}
// ============ System Control Functions ============
function disableTrading() external onlySupervisorOrOwner {
_TRADE_ALLOWED_ = false;
}
function enableTrading() external onlyOwner notClosed {
_TRADE_ALLOWED_ = true;
}
function disableQuoteDeposit() external onlySupervisorOrOwner {
_DEPOSIT_QUOTE_ALLOWED_ = false;
}
function enableQuoteDeposit() external onlyOwner notClosed {
_DEPOSIT_QUOTE_ALLOWED_ = true;
}
function disableBaseDeposit() external onlySupervisorOrOwner {
_DEPOSIT_BASE_ALLOWED_ = false;
}
function enableBaseDeposit() external onlyOwner notClosed {
_DEPOSIT_BASE_ALLOWED_ = true;
}
}

View File

@@ -0,0 +1,118 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {SafeMath} from "../lib/SafeMath.sol";
import {Ownable} from "../lib/Ownable.sol";
/**
* @title DODOLpToken
* @author DODO Breeder
*
* @notice Tokenize liquidity pool assets. An ordinary ERC20 contract with mint and burn functions
*/
contract DODOLpToken is Ownable {
using SafeMath for uint256;
uint256 public totalSupply;
mapping(address => uint256) internal balances;
mapping(address => mapping(address => uint256)) internal allowed;
// ============ 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);
// ============ Functions ============
/**
* @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(to != address(0), "TO_ADDRESS_IS_EMPTY");
require(amount <= balances[msg.sender], "BALANCE_NOT_ENOUGH");
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[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 balances[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(to != address(0), "TO_ADDRESS_IS_EMPTY");
require(amount <= balances[from], "BALANCE_NOT_ENOUGH");
require(amount <= allowed[from][msg.sender], "ALLOWANCE_NOT_ENOUGH");
balances[from] = balances[from].sub(amount);
balances[to] = balances[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) {
allowed[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
/**
* @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) external onlyOwner {
balances[user] = balances[user].add(value);
totalSupply = totalSupply.add(value);
emit Mint(address(0), value);
emit Transfer(address(0), user, value);
}
function burn(address user, uint256 value) external onlyOwner {
balances[user] = balances[user].sub(value);
totalSupply = totalSupply.sub(value);
emit Burn(user, value);
}
}

View File

@@ -0,0 +1,309 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {SafeMath} from "../lib/SafeMath.sol";
import {DecimalMath} from "../lib/DecimalMath.sol";
import {DODOMath} from "../lib/DODOMath.sol";
import {Types} from "../lib/Types.sol";
import {IDODOLpToken} from "../intf/IDODOLpToken.sol";
import {Storage} from "./Storage.sol";
import {Settlement} from "./Settlement.sol";
import {Pricing} from "./Pricing.sol";
/**
* @title LiquidityProvider
* @author DODO Breeder
*
* @notice Functions for liquidity provider operations
*/
contract LiquidityProvider is Storage, Pricing, Settlement {
using SafeMath for uint256;
// ============ Events ============
event DepositBaseToken(address indexed payer, address indexed receiver, uint256 amount);
event DepositQuoteToken(address indexed payer, address indexed receiver, uint256 amount);
event WithdrawBaseToken(address indexed payer, address indexed receiver, uint256 amount);
event WithdrawQuoteToken(address indexed payer, address indexed receiver, uint256 amount);
event ChargeBasePenalty(address indexed payer, uint256 amount);
event ChargeQuotePenalty(address indexed payer, uint256 amount);
// ============ Modifiers ============
modifier depositQuoteAllowed() {
require(_DEPOSIT_QUOTE_ALLOWED_, "DEPOSIT_QUOTE_NOT_ALLOWED");
_;
}
modifier depositBaseAllowed() {
require(_DEPOSIT_BASE_ALLOWED_, "DEPOSIT_BASE_NOT_ALLOWED");
_;
}
// ============ Routine Functions ============
function withdrawBase(uint256 amount) external returns (uint256) {
return withdrawBaseTo(msg.sender, amount);
}
function depositBase(uint256 amount) external {
depositBaseTo(msg.sender, amount);
}
function withdrawQuote(uint256 amount) external returns (uint256) {
return withdrawQuoteTo(msg.sender, amount);
}
function depositQuote(uint256 amount) external {
depositQuoteTo(msg.sender, amount);
}
function withdrawAllBase() external returns (uint256) {
return withdrawAllBaseTo(msg.sender);
}
function withdrawAllQuote() external returns (uint256) {
return withdrawAllQuoteTo(msg.sender);
}
// ============ Deposit Functions ============
function depositQuoteTo(address to, uint256 amount)
public
preventReentrant
depositQuoteAllowed
{
(, uint256 quoteTarget) = _getExpectedTarget();
uint256 capital = amount;
uint256 totalQuoteCapital = getTotalQuoteCapital();
if (totalQuoteCapital == 0) {
capital = amount.add(quoteTarget); // give remaining quote token to lp as a gift
}
if (quoteTarget > 0 && totalQuoteCapital > 0) {
capital = amount.mul(totalQuoteCapital).div(quoteTarget);
}
// settlement
_quoteTokenTransferIn(msg.sender, amount);
_mintQuoteTokenCapital(to, capital);
_TARGET_QUOTE_TOKEN_AMOUNT_ = _TARGET_QUOTE_TOKEN_AMOUNT_.add(amount);
emit DepositQuoteToken(msg.sender, to, amount);
}
function depositBaseTo(address to, uint256 amount) public preventReentrant depositBaseAllowed {
(uint256 baseTarget, ) = _getExpectedTarget();
uint256 capital = amount;
uint256 totalBaseCapital = getTotalBaseCapital();
if (totalBaseCapital == 0) {
capital = amount.add(baseTarget); // give remaining base token to lp as a gift
}
if (baseTarget > 0 && totalBaseCapital > 0) {
capital = amount.mul(totalBaseCapital).div(baseTarget);
}
// settlement
_baseTokenTransferIn(msg.sender, amount);
_mintBaseTokenCapital(to, capital);
_TARGET_BASE_TOKEN_AMOUNT_ = _TARGET_BASE_TOKEN_AMOUNT_.add(amount);
emit DepositBaseToken(msg.sender, to, amount);
}
// ============ Withdraw Functions ============
function withdrawQuoteTo(address to, uint256 amount) public preventReentrant returns (uint256) {
uint256 Q = _QUOTE_BALANCE_;
require(amount <= Q, "DODO_QUOTE_TOKEN_BALANCE_NOT_ENOUGH");
// calculate capital
(, uint256 quoteTarget) = _getExpectedTarget();
uint256 requireQuoteCapital = amount.mul(getTotalQuoteCapital()).divCeil(quoteTarget);
require(
requireQuoteCapital <= getQuoteCapitalBalanceOf(msg.sender),
"LP_QUOTE_CAPITAL_BALANCE_NOT_ENOUGH"
);
// handle penalty, penalty may exceed amount
uint256 penalty = getWithdrawQuotePenalty(amount);
require(penalty <= amount, "COULD_NOT_AFFORD_LIQUIDITY_PENALTY");
// settlement
_TARGET_QUOTE_TOKEN_AMOUNT_ = _TARGET_QUOTE_TOKEN_AMOUNT_.sub(amount);
_burnQuoteTokenCapital(msg.sender, requireQuoteCapital);
_quoteTokenTransferOut(to, amount.sub(penalty));
_donateQuoteToken(penalty);
emit WithdrawQuoteToken(msg.sender, to, amount.sub(penalty));
emit ChargeQuotePenalty(msg.sender, penalty);
return amount.sub(penalty);
}
function withdrawBaseTo(address to, uint256 amount) public preventReentrant returns (uint256) {
uint256 B = _BASE_BALANCE_;
require(amount <= B, "DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH");
// calculate capital
(uint256 baseTarget, ) = _getExpectedTarget();
uint256 requireBaseCapital = amount.mul(getTotalBaseCapital()).divCeil(baseTarget);
require(
requireBaseCapital <= getBaseCapitalBalanceOf(msg.sender),
"LP_BASE_CAPITAL_BALANCE_NOT_ENOUGH"
);
// handle penalty, penalty may exceed amount
uint256 penalty = getWithdrawBasePenalty(amount);
require(penalty <= amount, "COULD_NOT_AFFORD_LIQUIDITY_PENALTY");
// settlement
_TARGET_BASE_TOKEN_AMOUNT_ = _TARGET_BASE_TOKEN_AMOUNT_.sub(amount);
_burnBaseTokenCapital(msg.sender, requireBaseCapital);
_baseTokenTransferOut(to, amount.sub(penalty));
_donateBaseToken(penalty);
emit WithdrawBaseToken(msg.sender, to, amount.sub(penalty));
emit ChargeBasePenalty(msg.sender, penalty);
return amount.sub(penalty);
}
// ============ Withdraw all Functions ============
function withdrawAllQuoteTo(address to) public preventReentrant returns (uint256) {
uint256 Q = _QUOTE_BALANCE_;
uint256 withdrawAmount = getLpQuoteBalance(msg.sender);
require(withdrawAmount <= Q, "DODO_QUOTE_TOKEN_BALANCE_NOT_ENOUGH");
// handle penalty, penalty may exceed amount
uint256 penalty = getWithdrawQuotePenalty(withdrawAmount);
require(penalty <= withdrawAmount, "COULD_NOT_AFFORD_LIQUIDITY_PENALTY");
// settlement
_TARGET_QUOTE_TOKEN_AMOUNT_ = _TARGET_QUOTE_TOKEN_AMOUNT_.sub(withdrawAmount);
_burnQuoteTokenCapital(msg.sender, getQuoteCapitalBalanceOf(msg.sender));
_quoteTokenTransferOut(to, withdrawAmount.sub(penalty));
_donateQuoteToken(penalty);
emit WithdrawQuoteToken(msg.sender, to, withdrawAmount);
emit ChargeQuotePenalty(msg.sender, penalty);
return withdrawAmount.sub(penalty);
}
function withdrawAllBaseTo(address to) public preventReentrant returns (uint256) {
uint256 B = _BASE_BALANCE_;
uint256 withdrawAmount = getLpBaseBalance(msg.sender);
require(withdrawAmount <= B, "DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH");
// handle penalty, penalty may exceed amount
uint256 penalty = getWithdrawBasePenalty(withdrawAmount);
require(penalty <= withdrawAmount, "COULD_NOT_AFFORD_LIQUIDITY_PENALTY");
// settlement
_TARGET_BASE_TOKEN_AMOUNT_ = _TARGET_BASE_TOKEN_AMOUNT_.sub(withdrawAmount);
_burnBaseTokenCapital(msg.sender, getBaseCapitalBalanceOf(msg.sender));
_baseTokenTransferOut(to, withdrawAmount.sub(penalty));
_donateBaseToken(penalty);
emit WithdrawBaseToken(msg.sender, to, withdrawAmount);
emit ChargeBasePenalty(msg.sender, penalty);
return withdrawAmount.sub(penalty);
}
// ============ Helper Functions ============
function _mintBaseTokenCapital(address user, uint256 amount) internal {
IDODOLpToken(_BASE_CAPITAL_TOKEN_).mint(user, amount);
}
function _mintQuoteTokenCapital(address user, uint256 amount) internal {
IDODOLpToken(_QUOTE_CAPITAL_TOKEN_).mint(user, amount);
}
function _burnBaseTokenCapital(address user, uint256 amount) internal {
IDODOLpToken(_BASE_CAPITAL_TOKEN_).burn(user, amount);
}
function _burnQuoteTokenCapital(address user, uint256 amount) internal {
IDODOLpToken(_QUOTE_CAPITAL_TOKEN_).burn(user, amount);
}
// ============ Getter Functions ============
function getLpBaseBalance(address lp) public view returns (uint256 lpBalance) {
uint256 totalBaseCapital = getTotalBaseCapital();
(uint256 baseTarget, ) = _getExpectedTarget();
if (totalBaseCapital == 0) {
return 0;
}
lpBalance = getBaseCapitalBalanceOf(lp).mul(baseTarget).div(totalBaseCapital);
return lpBalance;
}
function getLpQuoteBalance(address lp) public view returns (uint256 lpBalance) {
uint256 totalQuoteCapital = getTotalQuoteCapital();
(, uint256 quoteTarget) = _getExpectedTarget();
if (totalQuoteCapital == 0) {
return 0;
}
lpBalance = getQuoteCapitalBalanceOf(lp).mul(quoteTarget).div(totalQuoteCapital);
return lpBalance;
}
function getWithdrawQuotePenalty(uint256 amount) public view returns (uint256 penalty) {
if (_R_STATUS_ == Types.RStatus.BELOW_ONE) {
require(amount < _QUOTE_BALANCE_, "DODO_QUOTE_BALANCE_NOT_ENOUGH");
uint256 spareBase = _BASE_BALANCE_.sub(_TARGET_BASE_TOKEN_AMOUNT_);
uint256 price = getOraclePrice();
uint256 fairAmount = DecimalMath.mul(spareBase, price);
uint256 targetQuote = DODOMath._SolveQuadraticFunctionForTarget(
_QUOTE_BALANCE_,
_K_,
fairAmount
);
uint256 targetQuoteWithWithdraw = DODOMath._SolveQuadraticFunctionForTarget(
_QUOTE_BALANCE_.sub(amount),
_K_,
fairAmount
);
return targetQuote.sub(targetQuoteWithWithdraw.add(amount));
} else {
return 0;
}
}
function getWithdrawBasePenalty(uint256 amount) public view returns (uint256 penalty) {
if (_R_STATUS_ == Types.RStatus.ABOVE_ONE) {
require(amount < _BASE_BALANCE_, "DODO_BASE_BALANCE_NOT_ENOUGH");
uint256 spareQuote = _QUOTE_BALANCE_.sub(_TARGET_QUOTE_TOKEN_AMOUNT_);
uint256 price = getOraclePrice();
uint256 fairAmount = DecimalMath.divFloor(spareQuote, price);
uint256 targetBase = DODOMath._SolveQuadraticFunctionForTarget(
_BASE_BALANCE_,
_K_,
fairAmount
);
uint256 targetBaseWithWithdraw = DODOMath._SolveQuadraticFunctionForTarget(
_BASE_BALANCE_.sub(amount),
_K_,
fairAmount
);
return targetBase.sub(targetBaseWithWithdraw.add(amount));
} else {
return 0;
}
}
}

186
contracts/impl/Pricing.sol Normal file
View File

@@ -0,0 +1,186 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {SafeMath} from "../lib/SafeMath.sol";
import {DecimalMath} from "../lib/DecimalMath.sol";
import {DODOMath} from "../lib/DODOMath.sol";
import {Types} from "../lib/Types.sol";
import {Storage} from "./Storage.sol";
/**
* @title Pricing
* @author DODO Breeder
*
* @notice DODO Pricing model
*/
contract Pricing is Storage {
using SafeMath for uint256;
// ============ R = 1 cases ============
function _ROneSellBaseToken(uint256 amount, uint256 targetQuoteTokenAmount)
internal
view
returns (uint256 receiveQuoteToken)
{
uint256 i = getOraclePrice();
uint256 Q2 = DODOMath._SolveQuadraticFunctionForTrade(
targetQuoteTokenAmount,
targetQuoteTokenAmount,
DecimalMath.mul(i, amount),
false,
_K_
);
// in theory Q2 <= targetQuoteTokenAmount
// however when amount is close to 0, precision problems may cause Q2 > targetQuoteTokenAmount
return targetQuoteTokenAmount.sub(Q2);
}
function _ROneBuyBaseToken(uint256 amount, uint256 targetBaseTokenAmount)
internal
view
returns (uint256 payQuoteToken)
{
require(amount < targetBaseTokenAmount, "DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH");
uint256 B2 = targetBaseTokenAmount.sub(amount);
payQuoteToken = _RAboveIntegrate(targetBaseTokenAmount, targetBaseTokenAmount, B2);
return payQuoteToken;
}
// ============ R < 1 cases ============
function _RBelowSellBaseToken(
uint256 amount,
uint256 quoteBalance,
uint256 targetQuoteAmount
) internal view returns (uint256 receieQuoteToken) {
uint256 i = getOraclePrice();
uint256 Q2 = DODOMath._SolveQuadraticFunctionForTrade(
targetQuoteAmount,
quoteBalance,
DecimalMath.mul(i, amount),
false,
_K_
);
return quoteBalance.sub(Q2);
}
function _RBelowBuyBaseToken(
uint256 amount,
uint256 quoteBalance,
uint256 targetQuoteAmount
) internal view returns (uint256 payQuoteToken) {
// Here we don't require amount less than some value
// Because it is limited at upper function
// See Trader.queryBuyBaseToken
uint256 i = getOraclePrice();
uint256 Q2 = DODOMath._SolveQuadraticFunctionForTrade(
targetQuoteAmount,
quoteBalance,
DecimalMath.mul(i, amount),
true,
_K_
);
return Q2.sub(quoteBalance);
}
function _RBelowBackToOne()
internal
view
returns (uint256 payQuoteToken, uint256 receiveBaseToken)
{
// important: carefully design the system to make sure spareBase always greater than or equal to 0
uint256 spareBase = _BASE_BALANCE_.sub(_TARGET_BASE_TOKEN_AMOUNT_);
uint256 price = getOraclePrice();
uint256 fairAmount = DecimalMath.mul(spareBase, price);
uint256 newTargetQuote = DODOMath._SolveQuadraticFunctionForTarget(
_QUOTE_BALANCE_,
_K_,
fairAmount
);
return (newTargetQuote.sub(_QUOTE_BALANCE_), spareBase);
}
// ============ R > 1 cases ============
function _RAboveBuyBaseToken(
uint256 amount,
uint256 baseBalance,
uint256 targetBaseAmount
) internal view returns (uint256 payQuoteToken) {
require(amount < baseBalance, "DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH");
uint256 B2 = baseBalance.sub(amount);
return _RAboveIntegrate(targetBaseAmount, baseBalance, B2);
}
function _RAboveSellBaseToken(
uint256 amount,
uint256 baseBalance,
uint256 targetBaseAmount
) internal view returns (uint256 receiveQuoteToken) {
// here we don't require B1 <= targetBaseAmount
// Because it is limited at upper function
// See Trader.querySellBaseToken
uint256 B1 = baseBalance.add(amount);
return _RAboveIntegrate(targetBaseAmount, B1, baseBalance);
}
function _RAboveBackToOne()
internal
view
returns (uint256 payBaseToken, uint256 receiveQuoteToken)
{
// important: carefully design the system to make sure spareBase always greater than or equal to 0
uint256 spareQuote = _QUOTE_BALANCE_.sub(_TARGET_QUOTE_TOKEN_AMOUNT_);
uint256 price = getOraclePrice();
uint256 fairAmount = DecimalMath.divFloor(spareQuote, price);
uint256 newTargetBase = DODOMath._SolveQuadraticFunctionForTarget(
_BASE_BALANCE_,
_K_,
fairAmount
);
return (newTargetBase.sub(_BASE_BALANCE_), spareQuote);
}
// ============ Helper functions ============
function _getExpectedTarget() internal view returns (uint256 baseTarget, uint256 quoteTarget) {
uint256 Q = _QUOTE_BALANCE_;
uint256 B = _BASE_BALANCE_;
if (_R_STATUS_ == Types.RStatus.ONE) {
return (_TARGET_BASE_TOKEN_AMOUNT_, _TARGET_QUOTE_TOKEN_AMOUNT_);
} else if (_R_STATUS_ == Types.RStatus.BELOW_ONE) {
(uint256 payQuoteToken, uint256 receiveBaseToken) = _RBelowBackToOne();
return (B.sub(receiveBaseToken), Q.add(payQuoteToken));
} else if (_R_STATUS_ == Types.RStatus.ABOVE_ONE) {
(uint256 payBaseToken, uint256 receiveQuoteToken) = _RAboveBackToOne();
return (B.add(payBaseToken), Q.sub(receiveQuoteToken));
}
}
function _RAboveIntegrate(
uint256 B0,
uint256 B1,
uint256 B2
) internal view returns (uint256) {
uint256 i = getOraclePrice();
return DODOMath._GeneralIntegrate(B0, B1, B2, i, _K_);
}
// function _RBelowIntegrate(
// uint256 Q0,
// uint256 Q1,
// uint256 Q2
// ) internal view returns (uint256) {
// uint256 i = getOraclePrice();
// i = DecimalMath.divFloor(DecimalMath.ONE, i); // 1/i
// return DODOMath._GeneralIntegrate(Q0, Q1, Q2, i, _K_);
// }
}

View File

@@ -0,0 +1,143 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {SafeMath} from "../lib/SafeMath.sol";
import {SafeERC20} from "../lib/SafeERC20.sol";
import {DecimalMath} from "../lib/DecimalMath.sol";
import {Types} from "../lib/Types.sol";
import {IERC20} from "../intf/IERC20.sol";
import {Storage} from "./Storage.sol";
/**
* @title Settlement
* @author DODO Breeder
*
* @notice Functions for assets settlement
*/
contract Settlement is Storage {
using SafeMath for uint256;
using SafeERC20 for IERC20;
// ============ Events ============
event DonateBaseToken(uint256 amount);
event DonateQuoteToken(uint256 amount);
event Claim(address indexed user, uint256 baseTokenAmount, uint256 quoteTokenAmount);
// ============ Assets IN/OUT Functions ============
function _baseTokenTransferIn(address from, uint256 amount) internal {
IERC20(_BASE_TOKEN_).safeTransferFrom(from, address(this), amount);
_BASE_BALANCE_ = _BASE_BALANCE_.add(amount);
}
function _quoteTokenTransferIn(address from, uint256 amount) internal {
IERC20(_QUOTE_TOKEN_).safeTransferFrom(from, address(this), amount);
_QUOTE_BALANCE_ = _QUOTE_BALANCE_.add(amount);
}
function _baseTokenTransferOut(address to, uint256 amount) internal {
IERC20(_BASE_TOKEN_).safeTransfer(to, amount);
_BASE_BALANCE_ = _BASE_BALANCE_.sub(amount);
}
function _quoteTokenTransferOut(address to, uint256 amount) internal {
IERC20(_QUOTE_TOKEN_).safeTransfer(to, amount);
_QUOTE_BALANCE_ = _QUOTE_BALANCE_.sub(amount);
}
// ============ Donate to Liquidity Pool Functions ============
function _donateBaseToken(uint256 amount) internal {
_TARGET_BASE_TOKEN_AMOUNT_ = _TARGET_BASE_TOKEN_AMOUNT_.add(amount);
emit DonateBaseToken(amount);
}
function _donateQuoteToken(uint256 amount) internal {
_TARGET_QUOTE_TOKEN_AMOUNT_ = _TARGET_QUOTE_TOKEN_AMOUNT_.add(amount);
emit DonateQuoteToken(amount);
}
function donateBaseToken(uint256 amount) external {
_baseTokenTransferIn(msg.sender, amount);
_donateBaseToken(amount);
}
function donateQuoteToken(uint256 amount) external {
_quoteTokenTransferIn(msg.sender, amount);
_donateQuoteToken(amount);
}
// ============ Final Settlement Functions ============
// last step to shut down dodo
function finalSettlement() external onlyOwner notClosed {
_CLOSED_ = true;
_DEPOSIT_QUOTE_ALLOWED_ = false;
_DEPOSIT_BASE_ALLOWED_ = false;
_TRADE_ALLOWED_ = false;
uint256 totalBaseCapital = getTotalBaseCapital();
uint256 totalQuoteCapital = getTotalQuoteCapital();
if (_QUOTE_BALANCE_ > _TARGET_QUOTE_TOKEN_AMOUNT_) {
uint256 spareQuote = _QUOTE_BALANCE_.sub(_TARGET_QUOTE_TOKEN_AMOUNT_);
_BASE_CAPITAL_RECEIVE_QUOTE_ = DecimalMath.divFloor(spareQuote, totalBaseCapital);
} else {
_TARGET_QUOTE_TOKEN_AMOUNT_ = _QUOTE_BALANCE_;
}
if (_BASE_BALANCE_ > _TARGET_BASE_TOKEN_AMOUNT_) {
uint256 spareBase = _BASE_BALANCE_.sub(_TARGET_BASE_TOKEN_AMOUNT_);
_QUOTE_CAPITAL_RECEIVE_BASE_ = DecimalMath.divFloor(spareBase, totalQuoteCapital);
} else {
_TARGET_BASE_TOKEN_AMOUNT_ = _BASE_BALANCE_;
}
_R_STATUS_ = Types.RStatus.ONE;
}
// claim remaining assets after final settlement
function claim() external preventReentrant {
require(_CLOSED_, "DODO_IS_NOT_CLOSED");
require(!_CLAIMED_[msg.sender], "ALREADY_CLAIMED");
_CLAIMED_[msg.sender] = true;
uint256 quoteAmount = DecimalMath.mul(
getBaseCapitalBalanceOf(msg.sender),
_BASE_CAPITAL_RECEIVE_QUOTE_
);
uint256 baseAmount = DecimalMath.mul(
getQuoteCapitalBalanceOf(msg.sender),
_QUOTE_CAPITAL_RECEIVE_BASE_
);
_baseTokenTransferOut(msg.sender, baseAmount);
_quoteTokenTransferOut(msg.sender, quoteAmount);
emit Claim(msg.sender, baseAmount, quoteAmount);
return;
}
// in case someone transfer to contract directly
function retrieve(address token, uint256 amount) external onlyOwner {
if (token == _BASE_TOKEN_) {
require(
IERC20(_BASE_TOKEN_).balanceOf(address(this)) >= _BASE_BALANCE_.add(amount),
"DODO_BASE_BALANCE_NOT_ENOUGH"
);
}
if (token == _QUOTE_TOKEN_) {
require(
IERC20(_QUOTE_TOKEN_).balanceOf(address(this)) >= _QUOTE_BALANCE_.add(amount),
"DODO_QUOTE_BALANCE_NOT_ENOUGH"
);
}
IERC20(token).safeTransfer(msg.sender, amount);
}
}

106
contracts/impl/Storage.sol Normal file
View File

@@ -0,0 +1,106 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {Ownable} from "../lib/Ownable.sol";
import {SafeMath} from "../lib/SafeMath.sol";
import {DecimalMath} from "../lib/DecimalMath.sol";
import {ReentrancyGuard} from "../lib/ReentrancyGuard.sol";
import {IOracle} from "../intf/IOracle.sol";
import {IDODOLpToken} from "../intf/IDODOLpToken.sol";
import {Types} from "../lib/Types.sol";
/**
* @title Storage
* @author DODO Breeder
*
* @notice Local Variables
*/
contract Storage is Ownable, ReentrancyGuard {
using SafeMath for uint256;
// ============ Variables for Control ============
bool internal _INITIALIZED_;
bool public _CLOSED_;
bool public _DEPOSIT_QUOTE_ALLOWED_;
bool public _DEPOSIT_BASE_ALLOWED_;
bool public _TRADE_ALLOWED_;
uint256 public _GAS_PRICE_LIMIT_;
// ============ Core Address ============
address public _SUPERVISOR_; // could freeze system in emergency
address public _MAINTAINER_; // collect maintainer fee to buy food for DODO
address public _BASE_TOKEN_;
address public _QUOTE_TOKEN_;
address public _ORACLE_;
// ============ Variables for PMM Algorithm ============
uint256 public _LP_FEE_RATE_;
uint256 public _MT_FEE_RATE_;
uint256 public _K_;
Types.RStatus public _R_STATUS_;
uint256 public _TARGET_BASE_TOKEN_AMOUNT_;
uint256 public _TARGET_QUOTE_TOKEN_AMOUNT_;
uint256 public _BASE_BALANCE_;
uint256 public _QUOTE_BALANCE_;
address public _BASE_CAPITAL_TOKEN_;
address public _QUOTE_CAPITAL_TOKEN_;
// ============ Variables for Final Settlement ============
uint256 public _BASE_CAPITAL_RECEIVE_QUOTE_;
uint256 public _QUOTE_CAPITAL_RECEIVE_BASE_;
mapping(address => bool) public _CLAIMED_;
// ============ Modifiers ============
modifier onlySupervisorOrOwner() {
require(msg.sender == _SUPERVISOR_ || msg.sender == _OWNER_, "NOT_SUPERVISOR_OR_OWNER");
_;
}
modifier notClosed() {
require(!_CLOSED_, "DODO_IS_CLOSED");
_;
}
// ============ Helper Functions ============
function _checkDODOParameters() internal view returns (uint256) {
require(_K_ < DecimalMath.ONE, "K_MUST_BE_LESS_THAN_ONE");
require(_K_ > 0, "K_MUST_BE_GREATER_THAN_ZERO");
require(_LP_FEE_RATE_.add(_MT_FEE_RATE_) < DecimalMath.ONE, "FEE_MUST_BE_LESS_THAN_ONE");
}
function getOraclePrice() public view returns (uint256) {
return IOracle(_ORACLE_).getPrice();
}
function getBaseCapitalBalanceOf(address lp) public view returns (uint256) {
return IDODOLpToken(_BASE_CAPITAL_TOKEN_).balanceOf(lp);
}
function getTotalBaseCapital() public view returns (uint256) {
return IDODOLpToken(_BASE_CAPITAL_TOKEN_).totalSupply();
}
function getQuoteCapitalBalanceOf(address lp) public view returns (uint256) {
return IDODOLpToken(_QUOTE_CAPITAL_TOKEN_).balanceOf(lp);
}
function getTotalQuoteCapital() public view returns (uint256) {
return IDODOLpToken(_QUOTE_CAPITAL_TOKEN_).totalSupply();
}
}

243
contracts/impl/Trader.sol Normal file
View File

@@ -0,0 +1,243 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {SafeMath} from "../lib/SafeMath.sol";
import {DecimalMath} from "../lib/DecimalMath.sol";
import {Types} from "../lib/Types.sol";
import {Storage} from "./Storage.sol";
import {Pricing} from "./Pricing.sol";
import {Settlement} from "./Settlement.sol";
/**
* @title Trader
* @author DODO Breeder
*
* @notice Functions for trader operations
*/
contract Trader is Storage, Pricing, Settlement {
using SafeMath for uint256;
// ============ Events ============
event SellBaseToken(address indexed seller, uint256 payBase, uint256 receiveQuote);
event BuyBaseToken(address indexed buyer, uint256 receiveBase, uint256 payQuote);
event MaintainerFee(bool isBaseToken, uint256 amount);
// ============ Modifiers ============
modifier tradeAllowed() {
require(_TRADE_ALLOWED_, "TRADE_NOT_ALLOWED");
_;
}
modifier gasPriceLimit() {
require(tx.gasprice <= _GAS_PRICE_LIMIT_, "GAS_PRICE_EXCEED");
_;
}
// ============ Trade Functions ============
function sellBaseToken(uint256 amount, uint256 minReceiveQuote)
external
tradeAllowed
gasPriceLimit
preventReentrant
returns (uint256)
{
// query price
(
uint256 receiveQuote,
uint256 lpFeeQuote,
uint256 mtFeeQuote,
Types.RStatus newRStatus,
uint256 newQuoteTarget,
uint256 newBaseTarget
) = _querySellBaseToken(amount);
require(receiveQuote >= minReceiveQuote, "SELL_BASE_RECEIVE_NOT_ENOUGH");
// settle assets
_baseTokenTransferIn(msg.sender, amount);
_quoteTokenTransferOut(msg.sender, receiveQuote);
_quoteTokenTransferOut(_MAINTAINER_, mtFeeQuote);
// update TARGET
_TARGET_QUOTE_TOKEN_AMOUNT_ = newQuoteTarget;
_TARGET_BASE_TOKEN_AMOUNT_ = newBaseTarget;
_R_STATUS_ = newRStatus;
_donateQuoteToken(lpFeeQuote);
emit SellBaseToken(msg.sender, amount, receiveQuote);
emit MaintainerFee(false, mtFeeQuote);
return receiveQuote;
}
function buyBaseToken(uint256 amount, uint256 maxPayQuote)
external
tradeAllowed
gasPriceLimit
preventReentrant
returns (uint256)
{
// query price
(
uint256 payQuote,
uint256 lpFeeBase,
uint256 mtFeeBase,
Types.RStatus newRStatus,
uint256 newQuoteTarget,
uint256 newBaseTarget
) = _queryBuyBaseToken(amount);
require(payQuote <= maxPayQuote, "BUY_BASE_COST_TOO_MUCH");
// settle assets
_quoteTokenTransferIn(msg.sender, payQuote);
_baseTokenTransferOut(msg.sender, amount);
_baseTokenTransferOut(_MAINTAINER_, mtFeeBase);
// update TARGET
_TARGET_QUOTE_TOKEN_AMOUNT_ = newQuoteTarget;
_TARGET_BASE_TOKEN_AMOUNT_ = newBaseTarget;
_R_STATUS_ = newRStatus;
_donateBaseToken(lpFeeBase);
emit BuyBaseToken(msg.sender, amount, payQuote);
emit MaintainerFee(true, mtFeeBase);
return payQuote;
}
// ============ Query Functions ============
function querySellBaseToken(uint256 amount) external view returns (uint256 receiveQuote) {
(receiveQuote, , , , , ) = _querySellBaseToken(amount);
return receiveQuote;
}
function queryBuyBaseToken(uint256 amount) external view returns (uint256 payQuote) {
(payQuote, , , , , ) = _queryBuyBaseToken(amount);
return payQuote;
}
function _querySellBaseToken(uint256 amount)
internal
view
returns (
uint256 receiveQuote,
uint256 lpFeeQuote,
uint256 mtFeeQuote,
Types.RStatus newRStatus,
uint256 newQuoteTarget,
uint256 newBaseTarget
)
{
(newBaseTarget, newQuoteTarget) = _getExpectedTarget();
uint256 sellBaseAmount = amount;
if (_R_STATUS_ == Types.RStatus.ONE) {
// case 1: R=1
// R falls below one
receiveQuote = _ROneSellBaseToken(sellBaseAmount, newQuoteTarget);
newRStatus = Types.RStatus.BELOW_ONE;
} else if (_R_STATUS_ == Types.RStatus.ABOVE_ONE) {
uint256 backToOnePayBase = newBaseTarget.sub(_BASE_BALANCE_);
uint256 backToOneReceiveQuote = _QUOTE_BALANCE_.sub(newQuoteTarget);
// case 2: R>1
// complex case, R status depends on trading amount
if (sellBaseAmount < backToOnePayBase) {
// case 2.1: R status do not change
receiveQuote = _RAboveSellBaseToken(sellBaseAmount, _BASE_BALANCE_, newBaseTarget);
newRStatus = Types.RStatus.ABOVE_ONE;
if (receiveQuote > backToOneReceiveQuote) {
// [Important corner case!] may enter this branch when some precision problem happens. And consequently contribute to negative spare quote amount
// to make sure spare quote>=0, mannually set receiveQuote=backToOneReceiveQuote
receiveQuote = backToOneReceiveQuote;
}
} else if (sellBaseAmount == backToOnePayBase) {
// case 2.2: R status changes to ONE
receiveQuote = backToOneReceiveQuote;
newRStatus = Types.RStatus.ONE;
} else {
// case 2.3: R status changes to BELOW_ONE
receiveQuote = backToOneReceiveQuote.add(
_ROneSellBaseToken(sellBaseAmount.sub(backToOnePayBase), newQuoteTarget)
);
newRStatus = Types.RStatus.BELOW_ONE;
}
} else {
// _R_STATUS_ == Types.RStatus.BELOW_ONE
// case 3: R<1
receiveQuote = _RBelowSellBaseToken(sellBaseAmount, _QUOTE_BALANCE_, newQuoteTarget);
newRStatus = Types.RStatus.BELOW_ONE;
}
// count fees
lpFeeQuote = DecimalMath.mul(receiveQuote, _LP_FEE_RATE_);
mtFeeQuote = DecimalMath.mul(receiveQuote, _MT_FEE_RATE_);
receiveQuote = receiveQuote.sub(lpFeeQuote).sub(mtFeeQuote);
return (receiveQuote, lpFeeQuote, mtFeeQuote, newRStatus, newQuoteTarget, newBaseTarget);
}
function _queryBuyBaseToken(uint256 amount)
internal
view
returns (
uint256 payQuote,
uint256 lpFeeBase,
uint256 mtFeeBase,
Types.RStatus newRStatus,
uint256 newQuoteTarget,
uint256 newBaseTarget
)
{
(newBaseTarget, newQuoteTarget) = _getExpectedTarget();
// charge fee from user receive amount
lpFeeBase = DecimalMath.mul(amount, _LP_FEE_RATE_);
mtFeeBase = DecimalMath.mul(amount, _MT_FEE_RATE_);
uint256 buyBaseAmount = amount.add(lpFeeBase).add(mtFeeBase);
if (_R_STATUS_ == Types.RStatus.ONE) {
// case 1: R=1
payQuote = _ROneBuyBaseToken(buyBaseAmount, newBaseTarget);
newRStatus = Types.RStatus.ABOVE_ONE;
} else if (_R_STATUS_ == Types.RStatus.ABOVE_ONE) {
// case 2: R>1
payQuote = _RAboveBuyBaseToken(buyBaseAmount, _BASE_BALANCE_, newBaseTarget);
newRStatus = Types.RStatus.ABOVE_ONE;
} else if (_R_STATUS_ == Types.RStatus.BELOW_ONE) {
uint256 backToOnePayQuote = newQuoteTarget.sub(_QUOTE_BALANCE_);
uint256 backToOneReceiveBase = _BASE_BALANCE_.sub(newBaseTarget);
// case 3: R<1
// complex case, R status may change
if (buyBaseAmount < backToOneReceiveBase) {
// case 3.1: R status do not change
payQuote = _RBelowBuyBaseToken(buyBaseAmount, _QUOTE_BALANCE_, newQuoteTarget);
newRStatus = Types.RStatus.BELOW_ONE;
} else if (buyBaseAmount == backToOneReceiveBase) {
// case 3.2: R status changes to ONE
payQuote = backToOnePayQuote;
newRStatus = Types.RStatus.ONE;
} else {
// case 3.3: R status changes to ABOVE_ONE
payQuote = backToOnePayQuote.add(
_ROneBuyBaseToken(buyBaseAmount.sub(backToOneReceiveBase), newBaseTarget)
);
newRStatus = Types.RStatus.ABOVE_ONE;
}
}
return (payQuote, lpFeeBase, mtFeeBase, newRStatus, newQuoteTarget, newBaseTarget);
}
}

46
contracts/intf/IDODO.sol Normal file
View File

@@ -0,0 +1,46 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
interface IDODO {
function init(
address supervisor,
address maintainer,
address baseToken,
address quoteToken,
address oracle,
uint256 lpFeeRate,
uint256 mtFeeRate,
uint256 k,
uint256 gasPriceLimit
) external;
function transferOwnership(address newOwner) external;
function sellBaseToken(uint256 amount, uint256 minReceiveQuote) external returns (uint256);
function buyBaseToken(uint256 amount, uint256 maxPayQuote) external returns (uint256);
function querySellBaseToken(uint256 amount) external view returns (uint256 receiveQuote);
function queryBuyBaseToken(uint256 amount) external view returns (uint256 payQuote);
function depositBaseTo(address to, uint256 amount) external;
function withdrawBase(uint256 amount) external returns (uint256);
function withdrawAllBase() external returns (uint256);
function depositQuoteTo(address to, uint256 amount) external;
function withdrawQuote(uint256 amount) external returns (uint256);
function withdrawAllQuote() external returns (uint256);
}

View File

@@ -0,0 +1,20 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity ^0.6.9;
pragma experimental ABIEncoderV2;
interface IDODOLpToken {
function mint(address user, uint256 value) external;
function burn(address user, uint256 value) external;
function balanceOf(address owner) external view returns (uint256);
function totalSupply() external view returns (uint256);
}

View File

@@ -0,0 +1,14 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
interface IDODOZoo {
function getDODO(address baseToken, address quoteToken) external view returns (address);
}

84
contracts/intf/IERC20.sol Normal file
View File

@@ -0,0 +1,84 @@
// This is a file copied from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}

View File

@@ -0,0 +1,14 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
interface IOracle {
function getPrice() external view returns (uint256);
}

32
contracts/intf/IWETH.sol Normal file
View File

@@ -0,0 +1,32 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
interface IWETH {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address src,
address dst,
uint256 wad
) external returns (bool);
function deposit() external payable;
function withdraw(uint256 wad) external;
}

117
contracts/lib/DODOMath.sol Normal file
View File

@@ -0,0 +1,117 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {SafeMath} from "./SafeMath.sol";
import {DecimalMath} from "./DecimalMath.sol";
/**
* @title DODOMath
* @author DODO Breeder
*
* @notice Functions for complex calculating. Including ONE Integration and TWO Quadratic solutions
*/
library DODOMath {
using SafeMath for uint256;
/*
Integrate dodo curve fron V1 to V2
require V0>=V1>=V2>0
res = (1-k)i(V1-V2)+ikV0*V0(1/V2-1/V1)
let V1-V2=delta
res = i*delta*(1-k+k(V0^2/V1/V2))
*/
function _GeneralIntegrate(
uint256 V0,
uint256 V1,
uint256 V2,
uint256 i,
uint256 k
) internal pure returns (uint256) {
uint256 fairAmount = DecimalMath.mul(i, V1.sub(V2)); // i*delta
uint256 V0V1 = DecimalMath.divCeil(V0, V1); // V0/V1
uint256 V0V2 = DecimalMath.divCeil(V0, V2); // V0/V2
uint256 penalty = DecimalMath.mul(DecimalMath.mul(k, V0V1), V0V2); // k(V0^2/V1/V2)
return DecimalMath.mul(fairAmount, DecimalMath.ONE.sub(k).add(penalty));
}
/*
The same with integration expression above, we have:
i*deltaB = (Q2-Q1)*(1-k+kQ0^2/Q1/Q2)
Given Q1 and deltaB, solve Q2
This is a quadratic function and the standard version is
aQ2^2 + bQ2 + c = 0, where
a=1-k
-b=(1-k)Q1-kQ0^2/Q1+i*deltaB
c=-kQ0^2
and Q2=(-b+sqrt(b^2+4(1-k)kQ0^2))/2(1-k)
note: another root is negative, abondan
if deltaBSig=true, then Q2>Q1
if deltaBSig=false, then Q2<Q1
*/
function _SolveQuadraticFunctionForTrade(
uint256 Q0,
uint256 Q1,
uint256 ideltaB,
bool deltaBSig,
uint256 k
) internal pure returns (uint256) {
// calculate -b value and sig
// -b = (1-k)Q1-kQ0^2/Q1+i*deltaB
uint256 kQ02Q1 = DecimalMath.mul(k, Q0).mul(Q0).div(Q1); // kQ0^2/Q1
uint256 b = DecimalMath.mul(DecimalMath.ONE.sub(k), Q1); // (1-k)Q1
bool minusbSig = true;
if (deltaBSig) {
b = b.add(ideltaB); // (1-k)Q1+i*deltaB
} else {
kQ02Q1 = kQ02Q1.add(ideltaB); // -i*(-deltaB)-kQ0^2/Q1
}
if (b >= kQ02Q1) {
b = b.sub(kQ02Q1);
minusbSig = true;
} else {
b = kQ02Q1.sub(b);
minusbSig = false;
}
// calculate sqrt
uint256 squareRoot = DecimalMath.mul(
DecimalMath.ONE.sub(k).mul(4),
DecimalMath.mul(k, Q0).mul(Q0)
); // 4(1-k)kQ0^2
squareRoot = b.mul(b).add(squareRoot).sqrt(); // sqrt(b*b-4(1-k)kQ0*Q0)
// final res
uint256 denominator = DecimalMath.ONE.sub(k).mul(2); // 2(1-k)
if (minusbSig) {
return DecimalMath.divFloor(b.add(squareRoot), denominator);
} else {
return DecimalMath.divFloor(squareRoot.sub(b), denominator);
}
}
/*
Start from the integration function
i*deltaB = (Q2-Q1)*(1-k+kQ0^2/Q1/Q2)
Assume Q2=Q0, Given Q1 and deltaB, solve Q0
let fairAmount = i*deltaB
*/
function _SolveQuadraticFunctionForTarget(
uint256 V1,
uint256 k,
uint256 fairAmount
) internal pure returns (uint256 V0) {
// V0 = V1+V1*(sqrt-1)/2k
uint256 sqrt = DecimalMath.divFloor(DecimalMath.mul(k, fairAmount), V1).mul(4);
sqrt = sqrt.add(DecimalMath.ONE).mul(DecimalMath.ONE).sqrt();
uint256 premium = DecimalMath.divFloor(sqrt.sub(DecimalMath.ONE), k.mul(2));
// V0 is greater than or equal to V1 according to the solution
return DecimalMath.mul(V1, DecimalMath.ONE.add(premium));
}
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {SafeMath} from "./SafeMath.sol";
/**
* @title DecimalMath
* @author DODO Breeder
*
* @notice Functions for fixed point number with 18 decimals
*/
library DecimalMath {
using SafeMath for uint256;
uint256 constant ONE = 10**18;
function mul(uint256 target, uint256 d) internal pure returns (uint256) {
return target.mul(d) / ONE;
}
function divFloor(uint256 target, uint256 d) internal pure returns (uint256) {
return target.mul(ONE).div(d);
}
function divCeil(uint256 target, uint256 d) internal pure returns (uint256) {
return target.mul(ONE).divCeil(d);
}
}

43
contracts/lib/Ownable.sol Normal file
View File

@@ -0,0 +1,43 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
/**
* @title Ownable
* @author DODO Breeder
*
* @notice Ownership related functions
*/
contract Ownable {
address public _OWNER_;
// ============ Events ============
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
// ============ Modifiers ============
modifier onlyOwner() {
require(msg.sender == _OWNER_, "NOT_OWNER");
_;
}
// ============ Functions ============
constructor() internal {
_OWNER_ = msg.sender;
emit OwnershipTransferred(address(0), _OWNER_);
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "INVALID_OWNER");
emit OwnershipTransferred(_OWNER_, newOwner);
_OWNER_ = newOwner;
}
}

View File

@@ -0,0 +1,32 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {Types} from "./Types.sol";
/**
* @title ReentrancyGuard
* @author DODO Breeder
*
* @notice Protect functions from Reentrancy Attack
*/
contract ReentrancyGuard {
Types.EnterStatus private _ENTER_STATUS_;
constructor() internal {
_ENTER_STATUS_ = Types.EnterStatus.NOT_ENTERED;
}
modifier preventReentrant() {
require(_ENTER_STATUS_ != Types.EnterStatus.ENTERED, "ReentrancyGuard: reentrant call");
_ENTER_STATUS_ = Types.EnterStatus.ENTERED;
_;
_ENTER_STATUS_ = Types.EnterStatus.NOT_ENTERED;
}
}

View File

@@ -0,0 +1,74 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
This is a simplified version of OpenZepplin's SafeERC20 library
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
import {IERC20} from "../intf/IERC20.sol";
import {SafeMath} from "./SafeMath.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for ERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using SafeMath for uint256;
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(
IERC20 token,
address from,
address to,
uint256 value
) internal {
_callOptionalReturn(
token,
abi.encodeWithSelector(token.transferFrom.selector, from, to, value)
);
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves.
// A Solidity high level call has three parts:
// 1. The target address is checked to verify it contains contract code
// 2. The call itself is made, and success asserted
// 3. The return value is decoded, which in turn checks the size of the returned data.
// solhint-disable-next-line max-line-length
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = address(token).call(data);
require(success, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
// Return data is optional
// solhint-disable-next-line max-line-length
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}

View File

@@ -0,0 +1,63 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
/**
* @title SafeMath
* @author DODO Breeder
*
* @notice Math operations with safety checks that revert on error
*/
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "MUL_ERROR");
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "DIVIDING_ERROR");
return a / b;
}
function divCeil(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 quotient = div(a, b);
uint256 remainder = a - quotient * b;
if (remainder > 0) {
return quotient + 1;
} else {
return quotient;
}
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SUB_ERROR");
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "ADD_ERROR");
return c;
}
function sqrt(uint256 x) internal pure returns (uint256 y) {
uint256 z = (x + 1) / 2;
y = x;
while (z < y) {
y = z;
z = (x / z + z) / 2;
}
}
}

14
contracts/lib/Types.sol Normal file
View File

@@ -0,0 +1,14 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;
library Types {
enum RStatus {ONE, ABOVE_ONE, BELOW_ONE}
enum EnterStatus {ENTERED, NOT_ENTERED}
}

1
coverage.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};

27
migrations/2_deploy.js Normal file
View File

@@ -0,0 +1,27 @@
const DecimalMath = artifacts.require("DecimalMath");
const SafeERC20 = artifacts.require("SafeERC20");
const DODOMath = artifacts.require("DODOMath");
const DODO = artifacts.require("DODO");
const DODOZoo = artifacts.require("DODOZoo");
module.exports = async (deployer, network) => {
const deployDODO = async () => {
await deployer.deploy(DecimalMath);
await deployer.deploy(SafeERC20);
await deployer.deploy(DODOMath);
await deployer.link(SafeERC20, DODO);
await deployer.link(DecimalMath, DODO);
await deployer.link(DODOMath, DODO);
await deployer.deploy(DODO);
await deployer.deploy(DODOZoo);
};
if (network == "production") {
} else if (network == "kovan") {
} else {
// for development & test
await deployDODO();
}
};

10691
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "dodo",
"version": "1.0.0",
"description": "a kind of bird",
"main": "index.js",
"author": "dodo breeder",
"license": "Apache-2.0",
"keywords": [
"dodo",
"ethereum",
"lmm"
],
"scripts": {
"prettier": "prettier --write **/*.sol",
"migrate": "truffle migrate",
"compile": "truffle compile",
"coverage": "NETWORK_ID=1002 RPC_NODE_URI=http://127.0.0.1:6545 COVERAGE=true truffle run coverage",
"test": "truffle compile && truffle test",
"test_only": "truffle test",
"deploy": "truffle migrate --network=$NETWORK --reset",
"deploy_kovan": "NETWORK=kovan npm run deploy",
"deploy_mainnet": "NETWORK=mainnet npm run deploy",
"deploy_test": "NETWORK=development npm run deploy",
"node": "ganache-cli --port 8545 -l 0x1fffffffffffff -i 5777 -g 1 --allowUnlimitedContractSize"
},
"dependencies": {
"@types/chai": "^4.2.11",
"@types/es6-promisify": "^6.0.0",
"@types/mocha": "^7.0.2",
"assert": "^2.0.0",
"babel-cli": "^6.26.0",
"babel-eslint": "^10.1.0",
"bignumber.js": "^9.0.0",
"chai": "^4.2.0",
"chai-bignumber": "^3.0.0",
"debug": "^4.1.1",
"dotenv-flow": "^3.1.0",
"es6-promisify": "^6.1.1",
"ethereumjs-util": "^7.0.2",
"lodash": "^4.17.15",
"mocha": "^7.2.0",
"truffle-hdwallet-provider": "^1.0.17",
"ts-node": "^8.10.2",
"typescript": "^3.9.5",
"web3": "^1.2.8",
"web3-core-helpers": "^1.2.8",
"web3-eth-contract": "^1.2.8"
},
"devDependencies": {
"ganache-cli": "^6.9.1",
"prettier": "^2.0.5",
"prettier-plugin-solidity": "^1.0.0-alpha.52",
"solidity-coverage": "^0.7.7"
}
}

340
test/Admin.test.ts Normal file
View File

@@ -0,0 +1,340 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext } from './utils/Context';
import { decimalStr } from './utils/Converter';
// import BigNumber from "bignumber.js";
import * as assert from "assert"
let lp1: string
let lp2: string
let trader: string
let tempAccount: string
async function init(ctx: DODOContext): Promise<void> {
await ctx.setOraclePrice(decimalStr("100"))
tempAccount = ctx.spareAccounts[5]
lp1 = ctx.spareAccounts[0]
lp2 = ctx.spareAccounts[1]
trader = ctx.spareAccounts[2]
await ctx.mintTestToken(lp1, decimalStr("100"), decimalStr("10000"))
await ctx.mintTestToken(lp2, decimalStr("100"), decimalStr("10000"))
await ctx.mintTestToken(trader, decimalStr("100"), decimalStr("10000"))
await ctx.approveDODO(lp1)
await ctx.approveDODO(lp2)
await ctx.approveDODO(trader)
}
describe("Admin", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
ctx = await getDODOContext()
await init(ctx);
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
describe("Settings", () => {
it("set oracle", async () => {
await ctx.DODO.methods.setOracle(tempAccount).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._ORACLE_().call(), tempAccount)
})
it("set suprevisor", async () => {
await ctx.DODO.methods.setSupervisor(tempAccount).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._SUPERVISOR_().call(), tempAccount)
})
it("set maintainer", async () => {
await ctx.DODO.methods.setMaintainer(tempAccount).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._MAINTAINER_().call(), tempAccount)
})
it("set liquidity provider fee rate", async () => {
await ctx.DODO.methods.setLiquidityProviderFeeRate(decimalStr("0.01")).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._LP_FEE_RATE_().call(), decimalStr("0.01"))
})
it("set maintainer fee rate", async () => {
await ctx.DODO.methods.setMaintainerFeeRate(decimalStr("0.01")).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._MT_FEE_RATE_().call(), decimalStr("0.01"))
})
it("set k", async () => {
await ctx.DODO.methods.setK(decimalStr("0.2")).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._K_().call(), decimalStr("0.2"))
})
it("set gas price limit", async () => {
await ctx.DODO.methods.setGasPriceLimit(decimalStr("100")).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._GAS_PRICE_LIMIT_().call(), decimalStr("100"))
})
})
describe("Controls", () => {
it("control flow", async () => {
await ctx.DODO.methods.disableBaseDeposit().send(ctx.sendParam(ctx.Supervisor))
await assert.rejects(
ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1)),
/DEPOSIT_BASE_NOT_ALLOWED/
)
await ctx.DODO.methods.enableBaseDeposit().send(ctx.sendParam(ctx.Deployer))
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), decimalStr("10"))
await ctx.DODO.methods.disableQuoteDeposit().send(ctx.sendParam(ctx.Supervisor))
await assert.rejects(
ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1)),
/DEPOSIT_QUOTE_NOT_ALLOWED/
)
await ctx.DODO.methods.enableQuoteDeposit().send(ctx.sendParam(ctx.Deployer))
await ctx.DODO.methods.depositQuote(decimalStr("10")).send(ctx.sendParam(lp1))
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), decimalStr("10"))
await ctx.DODO.methods.disableTrading().send(ctx.sendParam(ctx.Supervisor))
await assert.rejects(
ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("200")).send(ctx.sendParam(trader)),
/TRADE_NOT_ALLOWED/
)
await ctx.DODO.methods.enableTrading().send(ctx.sendParam(ctx.Deployer))
await ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("200")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("101"))
})
it("control flow premission", async () => {
await assert.rejects(
ctx.DODO.methods.setGasPriceLimit("1").send(ctx.sendParam(trader)),
/NOT_SUPERVISOR_OR_OWNER/
)
await assert.rejects(
ctx.DODO.methods.disableTrading().send(ctx.sendParam(trader)),
/NOT_SUPERVISOR_OR_OWNER/
)
await assert.rejects(
ctx.DODO.methods.disableQuoteDeposit().send(ctx.sendParam(trader)),
/NOT_SUPERVISOR_OR_OWNER/
)
await assert.rejects(
ctx.DODO.methods.disableBaseDeposit().send(ctx.sendParam(trader)),
/NOT_SUPERVISOR_OR_OWNER/
)
await assert.rejects(
ctx.DODO.methods.setOracle(trader).send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.setSupervisor(trader).send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.setMaintainer(trader).send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.setLiquidityProviderFeeRate(decimalStr("0.1")).send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.setMaintainerFeeRate(decimalStr("0.1")).send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.setK(decimalStr("0.1")).send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.enableTrading().send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.enableQuoteDeposit().send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.enableBaseDeposit().send(ctx.sendParam(trader)),
/NOT_OWNER/
)
})
})
describe("Final settlement", () => {
it("final settlement when R is ONE", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.finalSettlement().send(ctx.sendParam(ctx.Deployer))
await ctx.DODO.methods.claim().send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp1))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("100"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), decimalStr("10000"))
})
it("final settlement when R is ABOVE ONE", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
await ctx.DODO.methods.finalSettlement().send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "0")
await ctx.DODO.methods.claim().send(ctx.sendParam(lp1))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("90"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), "9551951805416248746110")
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp1))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("94.995"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), "10551951805416248746110")
})
it("final settlement when R is BELOW ONE", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.sellBaseToken(decimalStr("5"), decimalStr("100")).send(ctx.sendParam(trader))
await ctx.DODO.methods.finalSettlement().send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "0")
await ctx.DODO.methods.claim().send(ctx.sendParam(lp1))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("95"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), decimalStr("9000"))
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp1))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("105"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), "9540265973590798352834")
})
it("final settlement revert cases", async () => {
await assert.rejects(
ctx.DODO.methods.claim().send(ctx.sendParam(lp1)),
/DODO_IS_NOT_CLOSED/
)
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("500")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
await ctx.DODO.methods.finalSettlement().send(ctx.sendParam(ctx.Deployer))
await assert.rejects(
ctx.DODO.methods.finalSettlement().send(ctx.sendParam(ctx.Deployer)),
/ DODO_IS_CLOSED/
)
await ctx.DODO.methods.claim().send(ctx.sendParam(lp2))
await assert.rejects(
ctx.DODO.methods.claim().send(ctx.sendParam(lp2)),
/ALREADY_CLAIMED/
)
await assert.rejects(
ctx.DODO.methods.enableQuoteDeposit().send(ctx.sendParam(ctx.Deployer)),
/DODO_IS_CLOSED/
)
await assert.rejects(
ctx.DODO.methods.enableBaseDeposit().send(ctx.sendParam(ctx.Deployer)),
/DODO_IS_CLOSED/
)
await assert.rejects(
ctx.DODO.methods.enableTrading().send(ctx.sendParam(ctx.Deployer)),
/DODO_IS_CLOSED/
)
})
})
describe("donate", () => {
it("donate quote & base token", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositBase(decimalStr("20")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("2000")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.donateBaseToken(decimalStr("2")).send(ctx.sendParam(trader))
await ctx.DODO.methods.donateQuoteToken(decimalStr("500")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "10666666666666666666")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), "1166666666666666666666")
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp2).call(), "21333333333333333333")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp2).call(), "2333333333333333333333")
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp2))
await ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp2))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), "100666666666666666666")
assert.equal(await ctx.BASE.methods.balanceOf(lp2).call(), "101333333333333333334")
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), "10166666666666666666666")
assert.equal(await ctx.QUOTE.methods.balanceOf(lp2).call(), "10333333333333333333334")
})
})
describe("retrieve", () => {
it("retrieve base token", async () => {
await ctx.BASE.methods.transfer(ctx.DODO.options.address, decimalStr("1")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.retrieve(ctx.BASE.options.address, decimalStr("1")).send(ctx.sendParam(trader)),
/NOT_OWNER/
)
await assert.rejects(
ctx.DODO.methods.retrieve(ctx.BASE.options.address, decimalStr("2")).send(ctx.sendParam(ctx.Deployer)),
/DODO_BASE_BALANCE_NOT_ENOUGH/
)
await ctx.DODO.methods.retrieve(ctx.BASE.options.address, decimalStr("1")).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Deployer).call(), decimalStr("1"))
})
it("retrieve quote token", async () => {
await ctx.QUOTE.methods.transfer(ctx.DODO.options.address, decimalStr("1")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.retrieve(ctx.QUOTE.options.address, decimalStr("2")).send(ctx.sendParam(ctx.Deployer)),
/DODO_QUOTE_BALANCE_NOT_ENOUGH/
)
await ctx.DODO.methods.retrieve(ctx.QUOTE.options.address, decimalStr("1")).send(ctx.sendParam(ctx.Deployer))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Deployer).call(), decimalStr("1"))
})
})
describe("revert cases", () => {
it("k revert cases", async () => {
await assert.rejects(
ctx.DODO.methods.setK(decimalStr("1")).send(ctx.sendParam(ctx.Deployer)),
/K_MUST_BE_LESS_THAN_ONE/
)
await assert.rejects(
ctx.DODO.methods.setK(decimalStr("0")).send(ctx.sendParam(ctx.Deployer)),
/K_MUST_BE_GREATER_THAN_ZERO/
)
})
it("fee revert cases", async () => {
await assert.rejects(
ctx.DODO.methods.setLiquidityProviderFeeRate(decimalStr("0.999")).send(ctx.sendParam(ctx.Deployer)),
/FEE_MUST_BE_LESS_THAN_ONE/
)
await assert.rejects(
ctx.DODO.methods.setMaintainerFeeRate(decimalStr("0.998")).send(ctx.sendParam(ctx.Deployer)),
/FEE_MUST_BE_LESS_THAN_ONE/
)
})
})
})

157
test/Attacks.test.ts Normal file
View File

@@ -0,0 +1,157 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext } from './utils/Context';
import { decimalStr, gweiStr } from './utils/Converter';
import BigNumber from "bignumber.js";
import * as assert from "assert"
let lp1: string
let lp2: string
let trader: string
let hacker: string
async function init(ctx: DODOContext): Promise<void> {
await ctx.setOraclePrice(decimalStr("100"))
lp1 = ctx.spareAccounts[0]
lp2 = ctx.spareAccounts[1]
trader = ctx.spareAccounts[2]
hacker = ctx.spareAccounts[3]
await ctx.mintTestToken(lp1, decimalStr("100"), decimalStr("10000"))
await ctx.mintTestToken(lp2, decimalStr("100"), decimalStr("10000"))
await ctx.mintTestToken(trader, decimalStr("100"), decimalStr("10000"))
await ctx.mintTestToken(hacker, decimalStr("10000"), decimalStr("1000000"))
await ctx.approveDODO(lp1)
await ctx.approveDODO(lp2)
await ctx.approveDODO(trader)
await ctx.approveDODO(hacker)
}
describe("Attacks", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
ctx = await getDODOContext()
await init(ctx);
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
describe("Price offset attack", () => {
/*
attack describe:
1. hacker deposit a great number of base token
2. hacker buy base token
3. hacker withdraw a great number of base token
4. hacker sell or buy base token to finish the arbitrage loop
expected:
1. hacker won't earn any quote token or sell base token with price better than what dodo provides
2. quote token lp and base token lp have no loss
Same in quote direction
*/
it("attack on base token", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
let hackerInitBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(hacker).call())
let hackerInitQuoteBalance = new BigNumber(await ctx.QUOTE.methods.balanceOf(hacker).call())
// attack step 1
await ctx.DODO.methods.depositBase(decimalStr("5000")).send(ctx.sendParam(hacker))
// attack step 2
await ctx.DODO.methods.buyBaseToken(decimalStr("9.5"), decimalStr("2000")).send(ctx.sendParam(hacker))
// attack step 3
await ctx.DODO.methods.withdrawBase(decimalStr("5000")).send(ctx.sendParam(hacker))
// attack step 4
let hackerTempBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(hacker).call())
if (hackerTempBaseBalance.isGreaterThan(hackerInitBaseBalance)) {
await ctx.DODO.methods.sellBaseToken(hackerTempBaseBalance.minus(hackerInitBaseBalance).toString(), "0").send(ctx.sendParam(hacker))
} else {
await ctx.DODO.methods.buyBaseToken(hackerInitBaseBalance.minus(hackerTempBaseBalance).toString(), decimalStr("5000")).send(ctx.sendParam(hacker))
}
// expected hacker no profit
let hackerBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(hacker).call())
let hackerQuoteBalance = new BigNumber(await ctx.QUOTE.methods.balanceOf(hacker).call())
assert.ok(hackerBaseBalance.isLessThanOrEqualTo(hackerInitBaseBalance))
assert.ok(hackerQuoteBalance.isLessThanOrEqualTo(hackerInitQuoteBalance))
// expected lp no loss
let lpBaseBalance = new BigNumber(await ctx.DODO.methods.getLpBaseBalance(lp1).call())
let lpQuoteBalance = new BigNumber(await ctx.DODO.methods.getLpQuoteBalance(lp1).call())
assert.ok(lpBaseBalance.isGreaterThanOrEqualTo(decimalStr("10")))
assert.ok(lpQuoteBalance.isGreaterThanOrEqualTo(decimalStr("1000")))
})
it("attack on quote token", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
let hackerInitBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(hacker).call())
let hackerInitQuoteBalance = new BigNumber(await ctx.QUOTE.methods.balanceOf(hacker).call())
// attack step 1
await ctx.DODO.methods.depositQuote(decimalStr("100000")).send(ctx.sendParam(hacker))
// attack step 2
await ctx.DODO.methods.sellBaseToken(decimalStr("9"), decimalStr("500")).send(ctx.sendParam(hacker))
// attack step 3
await ctx.DODO.methods.withdrawQuote(decimalStr("100000")).send(ctx.sendParam(hacker))
// attack step 4
let hackerTempBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(hacker).call())
if (hackerTempBaseBalance.isGreaterThan(hackerInitBaseBalance)) {
await ctx.DODO.methods.sellBaseToken(hackerTempBaseBalance.minus(hackerInitBaseBalance).toString(), "0").send(ctx.sendParam(hacker))
} else {
await ctx.DODO.methods.buyBaseToken(hackerInitBaseBalance.minus(hackerTempBaseBalance).toString(), decimalStr("5000")).send(ctx.sendParam(hacker))
}
// expected hacker no profit
let hackerBaseBalance = new BigNumber(await ctx.BASE.methods.balanceOf(hacker).call())
let hackerQuoteBalance = new BigNumber(await ctx.QUOTE.methods.balanceOf(hacker).call())
assert.ok(hackerBaseBalance.isLessThanOrEqualTo(hackerInitBaseBalance))
assert.ok(hackerQuoteBalance.isLessThanOrEqualTo(hackerInitQuoteBalance))
// expected lp no loss
let lpBaseBalance = new BigNumber(await ctx.DODO.methods.getLpBaseBalance(lp1).call())
let lpQuoteBalance = new BigNumber(await ctx.DODO.methods.getLpQuoteBalance(lp1).call())
assert.ok(lpBaseBalance.isGreaterThanOrEqualTo(decimalStr("10")))
assert.ok(lpQuoteBalance.isGreaterThanOrEqualTo(decimalStr("1000")))
})
})
describe("Front run attack", () => {
/*
attack describe:
hacker tries to front run oracle updating by sending tx with higher gas price
expected:
revert tx
*/
it("front run", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await assert.rejects(
ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("200")).send({ from: trader, gas: 300000, gasPrice: gweiStr("200") }), /GAS_PRICE_EXCEED/
)
await assert.rejects(
ctx.DODO.methods.sellBaseToken(decimalStr("1"), decimalStr("200")).send({ from: trader, gas: 300000, gasPrice: gweiStr("200") }), /GAS_PRICE_EXCEED/
)
})
})
})

104
test/DODOEthProxy.test.ts Normal file
View File

@@ -0,0 +1,104 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext, DefaultDODOContextInitConfig } from './utils/Context';
import * as contracts from "./utils/Contracts";
import * as assert from "assert"
import { decimalStr, MAX_UINT256 } from './utils/Converter';
import { Contract } from "web3-eth-contract";
let lp: string
let trader: string
let DODOEthProxy: Contract
async function init(ctx: DODOContext): Promise<void> {
// switch ctx to eth proxy mode
let WETH = await contracts.newContract(contracts.WETH_CONTRACT_NAME)
await ctx.DODOZoo.methods.breedDODO(
ctx.Supervisor,
ctx.Maintainer,
WETH.options.address,
ctx.QUOTE.options.address,
ctx.ORACLE.options.address,
DefaultDODOContextInitConfig.lpFeeRate,
DefaultDODOContextInitConfig.mtFeeRate,
DefaultDODOContextInitConfig.k,
DefaultDODOContextInitConfig.gasPriceLimit
).send(ctx.sendParam(ctx.Deployer))
ctx.DODO = await contracts.getContractWithAddress(contracts.DODO_CONTRACT_NAME, await ctx.DODOZoo.methods.getDODO(WETH.options.address, ctx.QUOTE.options.address).call())
ctx.BASE = WETH
ctx.BaseCapital = await contracts.getContractWithAddress(contracts.DODO_LP_TOKEN_CONTRACT_NAME, await ctx.DODO.methods._BASE_CAPITAL_TOKEN_().call())
DODOEthProxy = await contracts.newContract(contracts.DODO_ETH_PROXY_CONTRACT_NAME, [ctx.DODOZoo.options.address, WETH.options.address])
// env
lp = ctx.spareAccounts[0]
trader = ctx.spareAccounts[1]
await ctx.setOraclePrice(decimalStr("100"))
await ctx.approveDODO(lp)
await ctx.approveDODO(trader)
await ctx.QUOTE.methods.mint(lp, decimalStr("1000")).send(ctx.sendParam(ctx.Deployer))
await ctx.QUOTE.methods.mint(trader, decimalStr("1000")).send(ctx.sendParam(ctx.Deployer))
await ctx.QUOTE.methods.approve(DODOEthProxy.options.address, MAX_UINT256).send(ctx.sendParam(trader))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp))
}
describe("DODO ETH PROXY", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
ctx = await getDODOContext()
await init(ctx)
await ctx.QUOTE.methods.approve(DODOEthProxy.options.address, MAX_UINT256).send(ctx.sendParam(trader))
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
let depositAmount = "10"
await DODOEthProxy.methods.depositEth(decimalStr(depositAmount), ctx.QUOTE.options.address).send(ctx.sendParam(lp, depositAmount))
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
describe("buy&sell eth directly", () => {
it("buy", async () => {
let buyAmount = "1"
await DODOEthProxy.methods.buyEthWith(ctx.QUOTE.options.address, decimalStr(buyAmount), decimalStr("200")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("8.999"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "898581839502056240973")
ctx.Web3
})
it("sell", async () => {
let sellAmount = "1"
await DODOEthProxy.methods.sellEthTo(ctx.QUOTE.options.address, decimalStr(sellAmount), decimalStr("50")).send(ctx.sendParam(trader, sellAmount))
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("11"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1098617454226610630664")
})
})
describe("revert cases", () => {
it("value not match", async () => {
await assert.rejects(
DODOEthProxy.methods.sellEthTo(ctx.QUOTE.options.address, decimalStr("1"), decimalStr("50")).send(ctx.sendParam(trader, "2")),
/ETH_AMOUNT_NOT_MATCH/
)
await assert.rejects(
DODOEthProxy.methods.depositEth(decimalStr("1"), ctx.QUOTE.options.address).send(ctx.sendParam(lp, "2")),
/ETH_AMOUNT_NOT_MATCH/
)
})
})
})

68
test/DODOZoo.test.ts Normal file
View File

@@ -0,0 +1,68 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext } from './utils/Context';
import * as assert from "assert"
import { newContract, TEST_ERC20_CONTRACT_NAME, getContractWithAddress, DODO_CONTRACT_NAME } from './utils/Contracts';
async function init(ctx: DODOContext): Promise<void> { }
describe("DODO ZOO", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
ctx = await getDODOContext()
await init(ctx);
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
describe("Breed new dodo", () => {
it("could not deploy the same dodo", async () => {
await assert.rejects(
ctx.DODOZoo.methods.breedDODO(ctx.Supervisor, ctx.Maintainer, ctx.BASE.options.address, ctx.QUOTE.options.address, ctx.ORACLE.options.address, "0", "0", "1", "0").send(ctx.sendParam(ctx.Deployer)),
/DODO_IS_REGISTERED/
)
await assert.rejects(
ctx.DODOZoo.methods.breedDODO(ctx.Supervisor, ctx.Maintainer, ctx.QUOTE.options.address, ctx.BASE.options.address, ctx.ORACLE.options.address, "0", "0", "1", "0").send(ctx.sendParam(ctx.Deployer)),
/DODO_IS_REGISTERED/
)
})
it("breed new dodo", async () => {
let newBase = await newContract(TEST_ERC20_CONTRACT_NAME)
let newQuote = await newContract(TEST_ERC20_CONTRACT_NAME)
await assert.rejects(
ctx.DODOZoo.methods.breedDODO(ctx.Supervisor, ctx.Maintainer, newBase.options.address, newQuote.options.address, ctx.ORACLE.options.address, "0", "0", "1", "0").send(ctx.sendParam(ctx.Maintainer)),
/NOT_OWNER/
)
await ctx.DODOZoo.methods.breedDODO(ctx.Supervisor, ctx.Maintainer, newBase.options.address, newQuote.options.address, ctx.ORACLE.options.address, "0", "0", "1", "0").send(ctx.sendParam(ctx.Deployer))
let newDODO = getContractWithAddress(DODO_CONTRACT_NAME, await ctx.DODOZoo.methods.getDODO(newBase.options.address, newQuote.options.address).call())
assert.equal(await newDODO.methods._BASE_TOKEN_().call(), newBase.options.address)
assert.equal(await newDODO.methods._QUOTE_TOKEN_().call(), newQuote.options.address)
// could not init twice
await assert.rejects(
newDODO.methods.init(ctx.Supervisor, ctx.Maintainer, ctx.QUOTE.options.address, ctx.BASE.options.address, ctx.ORACLE.options.address, "0", "0", "1", "0").send(ctx.sendParam(ctx.Deployer)),
/DODO_ALREADY_INITIALIZED/
)
})
})
})

View File

@@ -0,0 +1,446 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext } from './utils/Context';
import { decimalStr } from './utils/Converter';
import { logGas } from './utils/Log';
import * as assert from "assert"
let lp1: string
let lp2: string
let trader: string
async function init(ctx: DODOContext): Promise<void> {
await ctx.setOraclePrice(decimalStr("100"))
lp1 = ctx.spareAccounts[0]
lp2 = ctx.spareAccounts[1]
trader = ctx.spareAccounts[2]
await ctx.mintTestToken(lp1, decimalStr("100"), decimalStr("10000"))
await ctx.mintTestToken(lp2, decimalStr("100"), decimalStr("10000"))
await ctx.mintTestToken(trader, decimalStr("100"), decimalStr("10000"))
await ctx.approveDODO(lp1)
await ctx.approveDODO(lp2)
await ctx.approveDODO(trader)
}
describe("LiquidityProvider", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
ctx = await getDODOContext()
await init(ctx);
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
describe("R equals to ONE", () => {
it("multi lp deposit & withdraw", async () => {
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("0"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("0"))
logGas(await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1)), "deposit base")
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("90"))
logGas(await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1)), "deposit quote")
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), decimalStr("9000"))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("10"))
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("10"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("1000"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), decimalStr("1000"))
await ctx.DODO.methods.depositBase(decimalStr("3")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.depositQuote(decimalStr("70")).send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("10"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("1000"))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp2).call(), decimalStr("3"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp2).call(), decimalStr("70"))
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("13"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), decimalStr("1070"))
await ctx.DODO.methods.withdrawBase(decimalStr("5")).send(ctx.sendParam(lp1))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("5"))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("95"))
await ctx.DODO.methods.withdrawQuote(decimalStr("100")).send(ctx.sendParam(lp1))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("900"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), decimalStr("9100"))
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp1))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "0")
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("100"))
await ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp1))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), "0")
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), decimalStr("10000"))
})
})
describe("R is ABOVE ONE", () => {
it("deposit", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "10010841132009222923")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("1000"))
await ctx.DODO.methods.depositBase(decimalStr("5")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.depositQuote(decimalStr("100")).send(ctx.sendParam(lp2))
// lp1 & lp2 would both have profit because the curve becomes flatter
// but the withdraw penalty is greater than this free profit
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "10163234422929069690")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("1000"))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp2).call(), "5076114129127759275")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp2).call(), decimalStr("100"))
assert.equal(await ctx.DODO.methods.getWithdrawBasePenalty(decimalStr("5")).call(), "228507420047606043")
assert.equal(await ctx.DODO.methods.getWithdrawQuotePenalty(decimalStr("100")).call(), "0")
})
it("withdraw", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods.getWithdrawBasePenalty(decimalStr("4")).call(), "1065045389392391670")
assert.equal(await ctx.DODO.methods.getWithdrawQuotePenalty(decimalStr("100")).call(), "0")
await ctx.DODO.methods.withdrawBase(decimalStr("4")).send(ctx.sendParam(lp1))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), "92934954610607608330")
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), "2060045389392391670")
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "7075045389392391670")
await ctx.DODO.methods.withdrawQuote(decimalStr("100")).send(ctx.sendParam(lp1))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), decimalStr("9100"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "1451951805416248746119")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), decimalStr("900"))
})
})
describe("R is BELOW ONE", () => {
it("deposit", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.sellBaseToken(decimalStr("5"), decimalStr("200")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("10"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), "1000978629616255274293")
await ctx.DODO.methods.depositQuote(decimalStr("500")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.depositBase(decimalStr("5")).send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("10"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), "1012529270910521748792")
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp2).call(), decimalStr("5"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp2).call(), "505769674273013520099")
assert.equal(await ctx.DODO.methods.getWithdrawBasePenalty(decimalStr("5")).call(), "0")
assert.equal(await ctx.DODO.methods.getWithdrawQuotePenalty(decimalStr("500")).call(), "17320315567279994599")
})
it("withdraw", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.sellBaseToken(decimalStr("5"), decimalStr("200")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods.getWithdrawBasePenalty(decimalStr("4")).call(), "0")
assert.equal(await ctx.DODO.methods.getWithdrawQuotePenalty(decimalStr("100")).call(), "7389428846238898052")
await ctx.DODO.methods.withdrawQuote(decimalStr("100")).send(ctx.sendParam(lp1))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), "9092610571153761101948")
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "447655402437037250886")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "908310739520405634819")
await ctx.DODO.methods.withdrawBase(decimalStr("4")).send(ctx.sendParam(lp1))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("94"))
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("11"))
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), decimalStr("6"))
})
})
describe("Oracle changes", () => {
it("base side lp don't has pnl when R is BELOW ONE", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.sellBaseToken(decimalStr("5"), decimalStr("200")).send(ctx.sendParam(trader))
await ctx.setOraclePrice(decimalStr("80"));
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("10"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), "914362409397559034505")
await ctx.setOraclePrice(decimalStr("120"))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), decimalStr("10"))
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), "1085284653936129403614")
})
it("quote side lp don't has pnl when R is ABOVE ONE", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("600")).send(ctx.sendParam(trader))
await ctx.setOraclePrice(decimalStr("80"));
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "11138732839027528597")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("1000"))
await ctx.setOraclePrice(decimalStr("120"))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "9234731968726215538")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), decimalStr("1000"))
})
})
describe("Transfer lp token", () => {
it("transfer", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.BaseCapital.methods.transfer(lp2, decimalStr("5")).send(ctx.sendParam(lp1))
await ctx.QuoteCapital.methods.transfer(lp2, decimalStr("5")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp2))
await ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp2))
assert.equal(await ctx.BASE.methods.balanceOf(lp2).call(), decimalStr("105"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp2).call(), decimalStr("10005"))
})
})
describe("Deposit & transfer to other account", () => {
it("base token", async () => {
await ctx.DODO.methods.depositBaseTo(lp2, decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawBaseTo(trader, decimalStr("5")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.withdrawAllBaseTo(ctx.Supervisor).send(ctx.sendParam(lp2))
assert.equal(await ctx.BASE.methods.balanceOf(lp1).call(), decimalStr("90"))
assert.equal(await ctx.BASE.methods.balanceOf(lp2).call(), decimalStr("100"))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("105"))
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Supervisor).call(), decimalStr("5"))
})
it("quote token", async () => {
await ctx.DODO.methods.depositQuoteTo(lp2, decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.withdrawQuoteTo(trader, decimalStr("500")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.withdrawAllQuoteTo(ctx.Supervisor).send(ctx.sendParam(lp2))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp1).call(), decimalStr("9000"))
assert.equal(await ctx.QUOTE.methods.balanceOf(lp2).call(), decimalStr("10000"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), decimalStr("10500"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Supervisor).call(), decimalStr("500"))
})
})
describe("Corner cases", () => {
it("single side deposit", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
await ctx.DODO.methods.sellBaseToken("5015841132009222923", decimalStr("0")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "0")
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "10010841132009222923")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "1103903610832497492")
await ctx.DODO.methods.depositQuote("1").send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getQuoteCapitalBalanceOf(lp2).call(), "1103903610832497493")
})
it("single side deposit & lp deposit when R isn't equal to ONE", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
await ctx.DODO.methods.depositQuote("1").send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getQuoteCapitalBalanceOf(lp2).call(), "1")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp2).call(), "1")
})
it("single side deposit (base) & oracle change introduces loss", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
await ctx.setOraclePrice(decimalStr("120"))
await ctx.DODO.methods.sellBaseToken(decimalStr("4"), decimalStr("0")).send(ctx.sendParam(trader))
await ctx.DODO.methods.sellBaseToken(decimalStr("1"), decimalStr("0")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "2")
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "9234731968726215513")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "1105993618321025490")
await ctx.DODO.methods.depositQuote("1").send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getQuoteCapitalBalanceOf(lp2).call(), "7221653398290522326")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp2).call(), "7221653398290522382")
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "9234731968726215513")
})
it("single side deposit (base) & oracle change introduces profit", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
await ctx.setOraclePrice(decimalStr("80"))
await ctx.DODO.methods.sellBaseToken(decimalStr("4"), decimalStr("0")).send(ctx.sendParam(trader))
await ctx.DODO.methods.sellBaseToken(decimalStr("4"), decimalStr("0")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "2")
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "11138732839027528584")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "1105408308382702868")
await ctx.DODO.methods.depositQuote("1").send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getQuoteCapitalBalanceOf(lp2).call(), "21553269260529319697")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp2).call(), "21553269260529319725")
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "11138732839027528584")
})
it("single side deposit (quote) & oracle change introduces loss", async () => {
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.sellBaseToken(decimalStr("5"), decimalStr("0")).send(ctx.sendParam(trader))
await ctx.setOraclePrice(decimalStr("80"))
await ctx.DODO.methods.buyBaseToken(decimalStr("4"), decimalStr("600")).send(ctx.sendParam(trader))
await ctx.DODO.methods.buyBaseToken(decimalStr("0.99"), decimalStr("500")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "1")
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "9980000000000000")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "914362409397559031579")
await ctx.DODO.methods.depositBase("1").send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getBaseCapitalBalanceOf(lp2).call(), "10247647352975730")
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp2).call(), "10247647352975730")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp1).call(), "914362409397559031579")
})
it("deposit and withdraw immediately", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "10010841132009222923")
await ctx.DODO.methods.depositBase(decimalStr("5")).send(ctx.sendParam(lp2))
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "10163234422929069690")
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp2).call(), "5076114129127759275")
await ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp2))
assert.equal(await ctx.BASE.methods.balanceOf(lp2).call(), "99841132414635941818")
assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp1).call(), "10182702153814588570")
})
})
describe("Revert cases", () => {
it("withdraw base amount exceeds DODO balance", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("5"), decimalStr("1000")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.withdrawBase(decimalStr("6")).send(ctx.sendParam(lp1)),
/DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH/
)
await assert.rejects(
ctx.DODO.methods.withdrawAllBase().send(ctx.sendParam(lp1)),
/DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH/
)
})
it("withdraw quote amount exceeds DODO balance", async () => {
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.sellBaseToken(decimalStr("5"), decimalStr("0")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.withdrawQuote(decimalStr("600")).send(ctx.sendParam(lp1)),
/DODO_QUOTE_TOKEN_BALANCE_NOT_ENOUGH/
)
await assert.rejects(
ctx.DODO.methods.withdrawAllQuote().send(ctx.sendParam(lp1)),
/DODO_QUOTE_TOKEN_BALANCE_NOT_ENOUGH/
)
})
it("withdraw base could not afford penalty", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.buyBaseToken(decimalStr("9"), decimalStr("10000")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.withdrawBase(decimalStr("0.5")).send(ctx.sendParam(lp1)),
/COULD_NOT_AFFORD_LIQUIDITY_PENALTY/
)
await assert.rejects(
ctx.DODO.methods.getWithdrawBasePenalty(decimalStr("10")).call(),
/DODO_BASE_BALANCE_NOT_ENOUGH/
)
})
it("withdraw quote could not afford penalty", async () => {
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.sellBaseToken(decimalStr("10"), decimalStr("0")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.withdrawQuote(decimalStr("200")).send(ctx.sendParam(lp1)),
/COULD_NOT_AFFORD_LIQUIDITY_PENALTY/
)
await assert.rejects(
ctx.DODO.methods.getWithdrawQuotePenalty(decimalStr("1000")).call(),
/DODO_QUOTE_BALANCE_NOT_ENOUGH/
)
})
it("withdraw all base could not afford penalty", async () => {
await ctx.DODO.methods.depositBase(decimalStr("9.5")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositBase(decimalStr("0.5")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.buyBaseToken(decimalStr("9"), decimalStr("10000")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.withdrawBase(decimalStr("0.5")).send(ctx.sendParam(lp2)),
/COULD_NOT_AFFORD_LIQUIDITY_PENALTY/
)
})
it("withdraw all quote could not afford penalty", async () => {
await ctx.DODO.methods.depositQuote(decimalStr("800")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("200")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.sellBaseToken(decimalStr("10"), decimalStr("0")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.withdrawQuote(decimalStr("200")).send(ctx.sendParam(lp2)),
/COULD_NOT_AFFORD_LIQUIDITY_PENALTY/
)
})
it("withdraw amount exceeds lp balance", async () => {
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp2))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp1))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp2))
await assert.rejects(
ctx.DODO.methods.withdrawBase(decimalStr("11")).send(ctx.sendParam(lp1)),
/LP_BASE_CAPITAL_BALANCE_NOT_ENOUGH/
)
await assert.rejects(
ctx.DODO.methods.withdrawQuote(decimalStr("1100")).send(ctx.sendParam(lp1)),
/LP_QUOTE_CAPITAL_BALANCE_NOT_ENOUGH/
)
})
})
})

View File

@@ -0,0 +1,94 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext } from './utils/Context';
import { decimalStr, gweiStr } from './utils/Converter';
import * as assert from "assert"
let lp: string
let trader: string
async function init(ctx: DODOContext): Promise<void> {
await ctx.setOraclePrice(decimalStr("10"))
lp = ctx.spareAccounts[0]
trader = ctx.spareAccounts[1]
await ctx.approveDODO(lp)
await ctx.approveDODO(trader)
await ctx.mintTestToken(lp, decimalStr("10000"), decimalStr("10000000"))
await ctx.mintTestToken(trader, decimalStr("0"), decimalStr("10000000"))
await ctx.DODO.methods.depositBase(decimalStr("10000")).send(ctx.sendParam(lp))
}
describe("Trader", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
let dodoContextInitConfig = {
lpFeeRate: decimalStr("0"),
mtFeeRate: decimalStr("0"),
k: decimalStr("0.99"), // nearly one
gasPriceLimit: gweiStr("100"),
}
ctx = await getDODOContext(dodoContextInitConfig)
await init(ctx);
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
// price change quickly
describe("Trade long tail coin", () => {
it("price discover", async () => {
// 10% depth
// avg price = 11.137
await ctx.DODO.methods.buyBaseToken(decimalStr("1000"), decimalStr("100000")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("1000"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "9988900000000000000000000")
// 20% depth
// avg price = 12.475
await ctx.DODO.methods.buyBaseToken(decimalStr("1000"), decimalStr("100000")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("2000"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "9975050000000000000020000")
// 50% depth
// avg price = 19.9
await ctx.DODO.methods.buyBaseToken(decimalStr("3000"), decimalStr("300000")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("5000"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "9900500000000000000260000")
// 80% depth
// avg price = 49.6
await ctx.DODO.methods.buyBaseToken(decimalStr("3000"), decimalStr("300000")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("8000"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "9603200000000000001130000")
})
it("user has no pnl if buy and sell immediately", async () => {
// lp buy
await ctx.DODO.methods.buyBaseToken(decimalStr("1000"), decimalStr("100000")).send(ctx.sendParam(lp))
// trader buy and sell
await ctx.DODO.methods.buyBaseToken(decimalStr("1000"), decimalStr("100000")).send(ctx.sendParam(trader))
await ctx.DODO.methods.sellBaseToken(decimalStr("1000"), decimalStr("0")).send(ctx.sendParam(trader))
// no profit or loss (may have precision problems)
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), "0")
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "9999999999999999999970000")
})
})
})

103
test/StableCoinMode.test.ts Normal file
View File

@@ -0,0 +1,103 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext } from './utils/Context';
import { decimalStr, gweiStr } from './utils/Converter';
import * as assert from "assert"
let lp: string
let trader: string
async function init(ctx: DODOContext): Promise<void> {
await ctx.setOraclePrice(decimalStr("1"))
lp = ctx.spareAccounts[0]
trader = ctx.spareAccounts[1]
await ctx.approveDODO(lp)
await ctx.approveDODO(trader)
await ctx.mintTestToken(lp, decimalStr("10000"), decimalStr("10000"))
await ctx.mintTestToken(trader, decimalStr("10000"), decimalStr("10000"))
await ctx.DODO.methods.depositBase(decimalStr("10000")).send(ctx.sendParam(lp))
await ctx.DODO.methods.depositQuote(decimalStr("10000")).send(ctx.sendParam(lp))
}
describe("Trader", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
let dodoContextInitConfig = {
lpFeeRate: decimalStr("0.0001"),
mtFeeRate: decimalStr("0"),
k: gweiStr("1"), // nearly zero
gasPriceLimit: gweiStr("100"),
}
ctx = await getDODOContext(dodoContextInitConfig)
await init(ctx);
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
describe("Trade stable coin", () => {
it("trade with tiny slippage", async () => {
// 10% depth avg price 1.000100000111135
await ctx.DODO.methods.buyBaseToken(decimalStr("1000"), decimalStr("1001")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("11000"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "8999899999888865431655")
// 99.9% depth avg price 1.00010109
await ctx.DODO.methods.buyBaseToken(decimalStr("8990"), decimalStr("10000")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("19990"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "8990031967821738650")
// sell to 99.9% depth avg price 0.9999
await ctx.DODO.methods.sellBaseToken(decimalStr("19980"), decimalStr("19970")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("10"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "19986992950440794519885")
})
it("huge sell trading amount", async () => {
// trader could sell any number of base token
// but the price will drop quickly
await ctx.mintTestToken(trader, decimalStr("10000"), decimalStr("0"))
await ctx.DODO.methods.sellBaseToken(decimalStr("20000"), decimalStr("0")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("0"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "19998999990001000029998")
})
it("huge buy trading amount", async () => {
// could not buy all base balance
await assert.rejects(
ctx.DODO.methods.buyBaseToken(decimalStr("10000"), decimalStr("10010")).send(ctx.sendParam(trader)),
/DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH/
)
// when buy amount close to base balance, price will increase quickly
await ctx.mintTestToken(trader, decimalStr("0"), decimalStr("10000"))
await ctx.DODO.methods.buyBaseToken(decimalStr("9999"), decimalStr("20000")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("19999"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "9000000119999999900000")
})
it("tiny withdraw penalty", async () => {
await ctx.DODO.methods.buyBaseToken(decimalStr("9990"), decimalStr("10000")).send(ctx.sendParam(trader))
// penalty only 0.2% even if withdraw make pool utilization rate raise to 99.5%
assert.equal(await ctx.DODO.methods.getWithdrawBasePenalty(decimalStr("5")).call(), "9981962500000000")
})
})
})

281
test/Trader.test.ts Normal file
View File

@@ -0,0 +1,281 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { DODOContext, getDODOContext } from './utils/Context';
import { decimalStr } from './utils/Converter';
import { logGas } from './utils/Log';
import * as assert from "assert"
let lp: string
let trader: string
async function init(ctx: DODOContext): Promise<void> {
await ctx.setOraclePrice(decimalStr("100"))
lp = ctx.spareAccounts[0]
trader = ctx.spareAccounts[1]
await ctx.approveDODO(lp)
await ctx.approveDODO(trader)
await ctx.mintTestToken(lp, decimalStr("10"), decimalStr("1000"))
await ctx.mintTestToken(trader, decimalStr("10"), decimalStr("1000"))
await ctx.DODO.methods.depositBase(decimalStr("10")).send(ctx.sendParam(lp))
await ctx.DODO.methods.depositQuote(decimalStr("1000")).send(ctx.sendParam(lp))
}
describe("Trader", () => {
let snapshotId: string
let ctx: DODOContext
before(async () => {
ctx = await getDODOContext()
await init(ctx);
})
beforeEach(async () => {
snapshotId = await ctx.EVM.snapshot();
});
afterEach(async () => {
await ctx.EVM.reset(snapshotId)
});
describe("R goes above ONE", () => {
it("buy when R equals ONE", async () => {
logGas(await ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("110")).send(ctx.sendParam(trader)), "buy base token")
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("11"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "898581839502056240973")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0.001"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0"))
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("8.999"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "1101418160497943759027")
})
it("buy when R is ABOVE ONE", async () => {
await ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("110")).send(ctx.sendParam(trader))
await ctx.DODO.methods.buyBaseToken(decimalStr("4"), decimalStr("500")).send(ctx.sendParam(trader))
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("15"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "448068135932873382076")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0.005"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0"))
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("4.995"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "1551931864067126617924")
})
it("sell when R is ABOVE ONE", async () => {
await ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("110")).send(ctx.sendParam(trader))
await ctx.DODO.methods.sellBaseToken(decimalStr("0.5"), decimalStr("40")).send(ctx.sendParam(trader))
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("10.5"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "949280846351657143136")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0.001"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "50851561534203512")
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("9.499"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "1050668302086808653352")
})
it("sell when R is ABOVE ONE and RStatus back to ONE", async () => {
await ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("110")).send(ctx.sendParam(trader))
await ctx.DODO.methods.sellBaseToken("1003002430889317763", decimalStr("90")).send(ctx.sendParam(trader))
// R status
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "0")
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), "9996997569110682237")
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "999695745518506168723")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0.001"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "101418160497943759")
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), "10002002430889317763")
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "1000202836320995887518")
// target status
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "10002002430889317763")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "1000202836320995887518")
})
it("sell when R is ABOVE ONE and RStatus becomes BELOW ONE", async () => {
await ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("110")).send(ctx.sendParam(trader))
logGas(await ctx.DODO.methods.sellBaseToken(decimalStr("2"), decimalStr("90")).send(ctx.sendParam(trader)), "sell base token gas cost worst case")
// R status
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "2")
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("9"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1098020621600061709145")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0.001"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "200038898794388634")
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("10.999"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "901779339501143902221")
// target status
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "10002002430889317763")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "1000400077797588777268")
})
})
describe("R goes below ONE", () => {
it("sell when R equals ONE", async () => {
logGas(await ctx.DODO.methods.sellBaseToken(decimalStr("1"), decimalStr("90")).send(ctx.sendParam(trader)), "sell base token")
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("9"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1098617454226610630664")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), "0")
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "98914196817061816")
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("11"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "901283631576572307520")
})
it.only("sell when R is BELOW ONE", async () => {
await ctx.DODO.methods.sellBaseToken(decimalStr("3"), decimalStr("90")).send(ctx.sendParam(trader))
console.log(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("8"))
console.log(await ctx.QUOTE.methods.balanceOf(trader).call(), "1197235140964438116338")
console.log(await ctx.DODO.methods._QUOTE_BALANCE_().call())
await ctx.DODO.methods.sellBaseToken(decimalStr("3"), decimalStr("90")).send(ctx.sendParam(trader))
// trader balances
console.log(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("8"))
console.log(await ctx.QUOTE.methods.balanceOf(trader).call(), "1197235140964438116338")
// maintainer balances
console.log(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), "0")
console.log(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "197828626844973035")
// dodo balances
console.log(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("12"))
console.log(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "802567030408716910627")
})
it("buy when R is BELOW ONE", async () => {
await ctx.DODO.methods.sellBaseToken(decimalStr("1"), decimalStr("90")).send(ctx.sendParam(trader))
await ctx.DODO.methods.buyBaseToken(decimalStr("0.5"), decimalStr("60")).send(ctx.sendParam(trader))
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("9.5"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1049294316148665165351")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0.0005"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "98914196817061816")
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("10.4995"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "950606769654517772833")
})
it("buy when R is BELOW ONE and RStatus back to ONE", async () => {
await ctx.DODO.methods.sellBaseToken(decimalStr("1"), decimalStr("90")).send(ctx.sendParam(trader))
await ctx.DODO.methods.buyBaseToken("997008973080757728", decimalStr("110")).send(ctx.sendParam(trader))
// R status
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "0")
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), "9997008973080757728")
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "999703024198699420514")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), "997008973080757")
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "98914196817061816")
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), "10001994017946161515")
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "1000198061604483517670")
// target status
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "10001994017946161515")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "1000198061604483517670")
})
it("buy when R is BELOW ONE and RStatus becomes ABOVE ONE", async () => {
await ctx.DODO.methods.sellBaseToken(decimalStr("1"), decimalStr("90")).send(ctx.sendParam(trader))
logGas(await ctx.DODO.methods.buyBaseToken(decimalStr("2"), decimalStr("220")).send(ctx.sendParam(trader)), "buy base token gas cost worst case")
// R status
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "1")
// trader balances
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("11"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "897977789597854412810")
// maintainer balances
assert.equal(await ctx.BASE.methods.balanceOf(ctx.Maintainer).call(), decimalStr("0.002"))
assert.equal(await ctx.QUOTE.methods.balanceOf(ctx.Maintainer).call(), "98914196817061816")
// dodo balances
assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("8.998"))
assert.equal(await ctx.DODO.methods._QUOTE_BALANCE_().call(), "1101923296205328525374")
// target status
assert.equal(await ctx.DODO.methods._TARGET_BASE_TOKEN_AMOUNT_().call(), "10004000000000000000")
assert.equal(await ctx.DODO.methods._TARGET_QUOTE_TOKEN_AMOUNT_().call(), "1000198061604483517670")
})
})
describe("Corner cases", () => {
it("buy or sell 0", async () => {
await ctx.DODO.methods.sellBaseToken(decimalStr("0"), decimalStr("0")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("10"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), decimalStr("1000"))
await ctx.DODO.methods.buyBaseToken(decimalStr("0"), decimalStr("0")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), decimalStr("10"))
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), decimalStr("1000"))
})
it("buy or sell a tiny amount", async () => {
// no precision problem
await ctx.DODO.methods.sellBaseToken("1", decimalStr("0")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), "9999999999999999999")
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1000000000000000000100")
// have precision problem, charge 0
await ctx.DODO.methods.buyBaseToken("1", decimalStr("1")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), "10000000000000000000")
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1000000000000000000100")
assert.equal(await ctx.DODO.methods._R_STATUS_().call(), "0")
// no precision problem if trading amount is extremely small
await ctx.DODO.methods.buyBaseToken("10", decimalStr("1")).send(ctx.sendParam(trader))
assert.equal(await ctx.BASE.methods.balanceOf(trader).call(), "10000000000000000010")
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "999999999999999999100")
})
it("sell a huge amount of base token", async () => {
await ctx.mintTestToken(trader, decimalStr("10000"), "0")
await ctx.DODO.methods.sellBaseToken(decimalStr("10000"), "0").send(ctx.sendParam(trader))
// nearly drain out quote pool
// because the fee donated is greater than remaining quote pool
// quote lp earn a considerable profit
assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1996900220185135480814")
assert.equal(await ctx.DODO.methods.getLpQuoteBalance(lp).call(), "4574057156329524018663")
})
})
describe("Revert cases", () => {
it("price limit", async () => {
await assert.rejects(
ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("100")).send(ctx.sendParam(trader)),
/BUY_BASE_COST_TOO_MUCH/
)
await assert.rejects(
ctx.DODO.methods.sellBaseToken(decimalStr("1"), decimalStr("100")).send(ctx.sendParam(trader)),
/SELL_BASE_RECEIVE_NOT_ENOUGH/
)
})
it("base balance limit", async () => {
await assert.rejects(
ctx.DODO.methods.buyBaseToken(decimalStr("11"), decimalStr("10000")).send(ctx.sendParam(trader)),
/DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH/
)
await ctx.DODO.methods.buyBaseToken(decimalStr("1"), decimalStr("200")).send(ctx.sendParam(trader))
await assert.rejects(
ctx.DODO.methods.buyBaseToken(decimalStr("11"), decimalStr("10000")).send(ctx.sendParam(trader)),
/DODO_BASE_TOKEN_BALANCE_NOT_ENOUGH/
)
})
})
})

127
test/utils/Context.ts Normal file
View File

@@ -0,0 +1,127 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { EVM, getDefaultWeb3 } from "./EVM";
import Web3 from "web3";
import { Contract } from "web3-eth-contract";
import BigNumber from "bignumber.js";
import * as contracts from "./Contracts";
import { decimalStr, gweiStr, MAX_UINT256 } from "./Converter";
import * as log from "./Log";
BigNumber.config({
EXPONENTIAL_AT: 1000,
DECIMAL_PLACES: 80,
});
export interface DODOContextInitConfig {
lpFeeRate: string,
mtFeeRate: string,
k: string,
gasPriceLimit: 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 DefaultDODOContextInitConfig = {
lpFeeRate: decimalStr("0.002"),
mtFeeRate: decimalStr("0.001"),
k: decimalStr("0.1"),
gasPriceLimit: gweiStr("100"),
}
export class DODOContext {
EVM: EVM
Web3: Web3
DODO: Contract
DODOZoo: Contract
BASE: Contract
BaseCapital: Contract
QUOTE: Contract
QuoteCapital: Contract
ORACLE: Contract
Deployer: string
Supervisor: string
Maintainer: string
spareAccounts: string[]
constructor() { }
async init(config: DODOContextInitConfig) {
this.EVM = new EVM
this.Web3 = getDefaultWeb3()
this.DODOZoo = await contracts.newContract(contracts.DODO_ZOO_CONTRACT_NAME)
this.BASE = await contracts.newContract(contracts.TEST_ERC20_CONTRACT_NAME)
this.QUOTE = await contracts.newContract(contracts.TEST_ERC20_CONTRACT_NAME)
this.ORACLE = await contracts.newContract(contracts.NAIVE_ORACLE_CONTRACT_NAME)
const allAccounts = await this.Web3.eth.getAccounts();
this.Deployer = allAccounts[0]
this.Supervisor = allAccounts[1]
this.Maintainer = allAccounts[2]
this.spareAccounts = allAccounts.slice(3, 10)
await this.DODOZoo.methods.breedDODO(
this.Supervisor,
this.Maintainer,
this.BASE.options.address,
this.QUOTE.options.address,
this.ORACLE.options.address,
config.lpFeeRate,
config.mtFeeRate,
config.k,
config.gasPriceLimit
).send(this.sendParam(this.Deployer))
this.DODO = contracts.getContractWithAddress(contracts.DODO_CONTRACT_NAME, await this.DODOZoo.methods.getDODO(this.BASE.options.address, this.QUOTE.options.address).call())
this.BaseCapital = contracts.getContractWithAddress(contracts.DODO_LP_TOKEN_CONTRACT_NAME, await this.DODO.methods._BASE_CAPITAL_TOKEN_().call())
this.QuoteCapital = contracts.getContractWithAddress(contracts.DODO_LP_TOKEN_CONTRACT_NAME, await this.DODO.methods._QUOTE_CAPITAL_TOKEN_().call())
console.log(log.blueText("[Init dodo context]"))
}
sendParam(sender, value = "0") {
return {
from: sender,
gas: process.env["COVERAGE"] ? 10000000000 : 7000000,
gasPrice: process.env.GAS_PRICE,
value: decimalStr(value)
}
}
async setOraclePrice(price: string) {
await this.ORACLE.methods.setPrice(price).send(this.sendParam(this.Deployer))
}
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 approveDODO(account: string) {
await this.BASE.methods.approve(this.DODO.options.address, MAX_UINT256).send(this.sendParam(account))
await this.QUOTE.methods.approve(this.DODO.options.address, MAX_UINT256).send(this.sendParam(account))
}
}
export async function getDODOContext(config: DODOContextInitConfig = DefaultDODOContextInitConfig): Promise<DODOContext> {
var context = new DODOContext()
await context.init(config)
return context
}

112
test/utils/Contracts.ts Normal file
View File

@@ -0,0 +1,112 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
var jsonPath: string = "../../build/contracts/"
if (process.env["COVERAGE"]) {
console.log("[Coverage mode]")
jsonPath = "../../.coverage_artifacts/contracts/"
}
const DODO = require(`${jsonPath}DODO.json`)
const DODOZoo = require(`${jsonPath}DODOZoo.json`)
const DODOEthProxy = require(`${jsonPath}DODOEthProxy.json`)
const WETH = require(`${jsonPath}WETH9.json`)
const TestERC20 = require(`${jsonPath}TestERC20.json`)
const NaiveOracle = require(`${jsonPath}NaiveOracle.json`)
const DODOLpToken = require(`${jsonPath}DODOLpToken.json`)
import { getDefaultWeb3 } from './EVM';
import { Contract } from 'web3-eth-contract';
export const DODO_CONTRACT_NAME = "DODO"
export const TEST_ERC20_CONTRACT_NAME = "TestERC20"
export const NAIVE_ORACLE_CONTRACT_NAME = "NaiveOracle"
export const DODO_LP_TOKEN_CONTRACT_NAME = "DODOLpToken"
export const DODO_ZOO_CONTRACT_NAME = "DOOZoo"
export const DODO_ETH_PROXY_CONTRACT_NAME = "DODOEthProxy"
export const WETH_CONTRACT_NAME = "WETH"
interface ContractJson {
abi: any;
networks: { [network: number]: any };
byteCode: string;
}
function _getContractJSON(contractName: string): ContractJson {
switch (contractName) {
case DODO_CONTRACT_NAME:
return {
abi: DODO.abi,
networks: DODO.networks,
byteCode: DODO.bytecode
};
case TEST_ERC20_CONTRACT_NAME:
return {
abi: TestERC20.abi,
networks: TestERC20.networks,
byteCode: TestERC20.bytecode
};
case NAIVE_ORACLE_CONTRACT_NAME:
return {
abi: NaiveOracle.abi,
networks: NaiveOracle.networks,
byteCode: NaiveOracle.bytecode
};
case DODO_LP_TOKEN_CONTRACT_NAME:
return {
abi: DODOLpToken.abi,
networks: DODOLpToken.networks,
byteCode: DODOLpToken.bytecode
};
case DODO_ZOO_CONTRACT_NAME:
return {
abi: DODOZoo.abi,
networks: DODOZoo.networks,
byteCode: DODOZoo.bytecode
};
case DODO_ETH_PROXY_CONTRACT_NAME:
return {
abi: DODOEthProxy.abi,
networks: DODOEthProxy.networks,
byteCode: DODOEthProxy.bytecode
};
case WETH_CONTRACT_NAME:
return {
abi: WETH.abi,
networks: WETH.networks,
byteCode: WETH.bytecode
};
default:
throw "CONTRACT_NAME_NOT_FOUND";
}
}
export function getContractWithAddress(contractName: string, address: string) {
var Json = _getContractJSON(contractName)
var web3 = getDefaultWeb3()
return new web3.eth.Contract(Json.abi, address)
}
export function getDepolyedContract(contractName: string): Contract {
var Json = _getContractJSON(contractName)
var networkId = process.env.NETWORK_ID
var deployedAddress = _getContractJSON(contractName).networks[networkId].address
var web3 = getDefaultWeb3()
return new web3.eth.Contract(Json.abi, deployedAddress)
}
export async function newContract(contractName: string, args: any[] = []): Promise<Contract> {
var web3 = getDefaultWeb3()
var Json = _getContractJSON(contractName)
var contract = new web3.eth.Contract(Json.abi)
var adminAccount = (await web3.eth.getAccounts())[0]
let parameter = {
from: adminAccount,
gas: process.env["COVERAGE"] ? 10000000000 : 7000000,
gasPrice: web3.utils.toHex(web3.utils.toWei('1', 'wei'))
}
return await contract.deploy({ data: Json.byteCode, arguments: args }).send(parameter)
}

11
test/utils/Converter.ts Normal file
View File

@@ -0,0 +1,11 @@
import BigNumber from "bignumber.js";
export const MAX_UINT256 = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
export function decimalStr(value: string): string {
return new BigNumber(value).multipliedBy(10 ** 18).toFixed(0, BigNumber.ROUND_DOWN)
}
export function gweiStr(gwei: string): string {
return new BigNumber(gwei).multipliedBy(10 ** 9).toFixed(0, BigNumber.ROUND_DOWN)
}

83
test/utils/EVM.ts Normal file
View File

@@ -0,0 +1,83 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
// require('dotenv-flow').config();
import { JsonRpcPayload, JsonRpcResponse } from 'web3-core-helpers';
import Web3 from 'web3';
export function getDefaultWeb3() {
return new Web3(process.env.RPC_NODE_URI)
}
export class EVM {
private provider = new Web3.providers.HttpProvider(process.env.RPC_NODE_URI);
public async reset(id: string): Promise<string> {
if (!id) {
throw new Error('id must be set');
}
await this.callJsonrpcMethod('evm_revert', [id]);
return this.snapshot();
}
public async snapshot(): Promise<string> {
return this.callJsonrpcMethod('evm_snapshot');
}
public async evmRevert(id: string): Promise<string> {
return this.callJsonrpcMethod('evm_revert', [id]);
}
public async stopMining(): Promise<string> {
return this.callJsonrpcMethod('miner_stop');
}
public async startMining(): Promise<string> {
return this.callJsonrpcMethod('miner_start');
}
public async mineBlock(): Promise<string> {
return this.callJsonrpcMethod('evm_mine');
}
public async increaseTime(duration: number): Promise<string> {
return this.callJsonrpcMethod('evm_increaseTime', [duration]);
}
public async callJsonrpcMethod(method: string, params?: (any[])): Promise<string> {
const args: JsonRpcPayload = {
method,
params,
jsonrpc: '2.0',
id: new Date().getTime(),
};
const response = await this.send(args);
return response.result;
}
private async send(args: JsonRpcPayload): Promise<any> {
return new Promise((resolve, reject) => {
const callback: any = (error: Error, val: JsonRpcResponse): void => {
if (error) {
reject(error);
} else {
resolve(val);
}
};
this.provider.send(
args,
callback,
);
});
}
}

29
test/utils/Log.ts Normal file
View File

@@ -0,0 +1,29 @@
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
import { TransactionReceipt } from "web3-core"
export const blueText = x => `\x1b[36m${x}\x1b[0m`;
export const yellowText = x => `\x1b[33m${x}\x1b[0m`;
export const greenText = x => `\x1b[32m${x}\x1b[0m`;
export const redText = x => `\x1b[31m${x}\x1b[0m`;
export const numberWithCommas = x => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
export function logGas(receipt: TransactionReceipt, desc: string) {
const gasUsed = receipt.gasUsed;
let colorFn;
if (gasUsed < 80000) {
colorFn = greenText;
} else if (gasUsed < 200000) {
colorFn = yellowText;
} else {
colorFn = redText;
}
console.log(("Gas used:").padEnd(60, '.'), blueText(desc) + " ", colorFn(numberWithCommas(gasUsed).padStart(5)));
}

View File

@@ -0,0 +1,11 @@
function calculateSlippage(buyPercentage: number) {
const k = 0.1
console.log(buyPercentage, ":", ((1 / (1 - buyPercentage)) * k - k) * 100, "%")
}
// calculateSlippage(0.01)
// calculateSlippage(0.05)
// calculateSlippage(0.1)
// calculateSlippage(0.2)
// calculateSlippage(0.5)
// calculateSlippage(0.7)

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["es2015", "es2016", "es2017", "dom"],
"strict": false,
"sourceMap": true,
"declaration": true,
"downlevelIteration": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"outDir": "dist",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"typeRoots": ["node_modules/@types"],
"types": ["node", "mocha", "chai"]
},
"include": ["src", "test"],
"exclude": ["scripts/**/*", "build/**/*", "migrations/**/*"],
"compileOnSave": true
}

13
tslint.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": ["tslint-config-airbnb", "tslint-no-focused-test"],
"rules": {
"import-name": false,
"no-floating-promises": true,
"no-focused-test": true,
"variable-name": [
true,
"allow-pascal-case",
"ban-keywords"
]
}
}

5667
yarn.lock Normal file

File diff suppressed because it is too large Load Diff