first commit
This commit is contained in:
4
.env
Normal file
4
.env
Normal 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
17
.eslintrc.yaml
Normal 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
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sol linguist-language=Solidity
|
||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal 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
15
.prettierrc
Normal 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
16
.solcover.js
Normal 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
201
LICENSE
Normal 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
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# DODO:10x 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
128
contracts/DODOEthProxy.sol
Normal 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
78
contracts/DODOZoo.sol
Normal 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
61
contracts/dodo.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
34
contracts/helper/Migrations.sol
Normal file
34
contracts/helper/Migrations.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
25
contracts/helper/NaiveOracle.sol
Normal file
25
contracts/helper/NaiveOracle.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
65
contracts/helper/TestERC20.sol
Normal file
65
contracts/helper/TestERC20.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
77
contracts/helper/TestWETH.sol
Normal file
77
contracts/helper/TestWETH.sol
Normal 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
83
contracts/impl/Admin.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
118
contracts/impl/DODOLpToken.sol
Normal file
118
contracts/impl/DODOLpToken.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
309
contracts/impl/LiquidityProvider.sol
Normal file
309
contracts/impl/LiquidityProvider.sol
Normal 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
186
contracts/impl/Pricing.sol
Normal 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_);
|
||||
// }
|
||||
}
|
||||
143
contracts/impl/Settlement.sol
Normal file
143
contracts/impl/Settlement.sol
Normal 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
106
contracts/impl/Storage.sol
Normal 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
243
contracts/impl/Trader.sol
Normal 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
46
contracts/intf/IDODO.sol
Normal 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);
|
||||
}
|
||||
20
contracts/intf/IDODOLpToken.sol
Normal file
20
contracts/intf/IDODOLpToken.sol
Normal 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);
|
||||
}
|
||||
14
contracts/intf/IDODOZoo.sol
Normal file
14
contracts/intf/IDODOZoo.sol
Normal 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
84
contracts/intf/IERC20.sol
Normal 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);
|
||||
}
|
||||
14
contracts/intf/IOracle.sol
Normal file
14
contracts/intf/IOracle.sol
Normal 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
32
contracts/intf/IWETH.sol
Normal 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
117
contracts/lib/DODOMath.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
35
contracts/lib/DecimalMath.sol
Normal file
35
contracts/lib/DecimalMath.sol
Normal 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
43
contracts/lib/Ownable.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
32
contracts/lib/ReentrancyGuard.sol
Normal file
32
contracts/lib/ReentrancyGuard.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
74
contracts/lib/SafeERC20.sol
Normal file
74
contracts/lib/SafeERC20.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
63
contracts/lib/SafeMath.sol
Normal file
63
contracts/lib/SafeMath.sol
Normal 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
14
contracts/lib/Types.sol
Normal 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
1
coverage.json
Normal file
File diff suppressed because one or more lines are too long
5
migrations/1_initial_migration.js
Normal file
5
migrations/1_initial_migration.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const Migrations = artifacts.require("Migrations");
|
||||
|
||||
module.exports = function(deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
||||
27
migrations/2_deploy.js
Normal file
27
migrations/2_deploy.js
Normal 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
10691
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
package.json
Normal file
55
package.json
Normal 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
340
test/Admin.test.ts
Normal 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
157
test/Attacks.test.ts
Normal 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
104
test/DODOEthProxy.test.ts
Normal 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
68
test/DODOZoo.test.ts
Normal 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/
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
446
test/LiquidityProvider.test.ts
Normal file
446
test/LiquidityProvider.test.ts
Normal 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/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
94
test/LongTailTokenlMode.test.ts
Normal file
94
test/LongTailTokenlMode.test.ts
Normal 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
103
test/StableCoinMode.test.ts
Normal 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
281
test/Trader.test.ts
Normal 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
127
test/utils/Context.ts
Normal 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
112
test/utils/Contracts.ts
Normal 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
11
test/utils/Converter.ts
Normal 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
83
test/utils/EVM.ts
Normal 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
29
test/utils/Log.ts
Normal 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)));
|
||||
}
|
||||
11
test/utils/SlippageFormula.ts
Normal file
11
test/utils/SlippageFormula.ts
Normal 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
21
tsconfig.json
Normal 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
13
tslint.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user