first commit

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

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

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

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

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

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

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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