diff --git a/contracts/DODOEthProxy.sol b/contracts/DODOEthProxy.sol index cbabd87..a619be1 100644 --- a/contracts/DODOEthProxy.sol +++ b/contracts/DODOEthProxy.sol @@ -10,45 +10,67 @@ pragma experimental ABIEncoderV2; import {ReentrancyGuard} from "./lib/ReentrancyGuard.sol"; import {SafeERC20} from "./lib/SafeERC20.sol"; +import {SafeMath} from "./lib/SafeMath.sol"; import {IDODO} from "./intf/IDODO.sol"; import {IERC20} from "./intf/IERC20.sol"; import {IWETH} from "./intf/IWETH.sol"; + interface IDODOZoo { function getDODO(address baseToken, address quoteToken) external view returns (address); } + /** * @title DODO Eth Proxy * @author DODO Breeder * - * @notice Handle ETH-WETH converting for users. Use it only when WETH is base token + * @notice Handle ETH-WETH converting for users. */ contract DODOEthProxy is ReentrancyGuard { using SafeERC20 for IERC20; + using SafeMath for uint256; address public _DODO_ZOO_; address payable public _WETH_; // ============ Events ============ - event ProxySellEth( + event ProxySellEthToToken( address indexed seller, address indexed quoteToken, uint256 payEth, - uint256 receiveQuote + uint256 receiveToken ); - event ProxyBuyEth( + event ProxyBuyEthWithToken( address indexed buyer, address indexed quoteToken, uint256 receiveEth, - uint256 payQuote + uint256 payToken ); - event ProxyDepositEth(address indexed lp, address indexed DODO, uint256 ethAmount); + event ProxySellTokenToEth( + address indexed seller, + address indexed baseToken, + uint256 payToken, + uint256 receiveEth + ); - event ProxyWithdrawEth(address indexed lp, address indexed DODO, uint256 ethAmount); + event ProxyBuyTokenWithEth( + address indexed buyer, + address indexed baseToken, + uint256 receiveToken, + uint256 payEth + ); + + event ProxyDepositEthAsBase(address indexed lp, address indexed DODO, uint256 ethAmount); + + event ProxyWithdrawEthAsBase(address indexed lp, address indexed DODO, uint256 ethAmount); + + event ProxyDepositEthAsQuote(address indexed lp, address indexed DODO, uint256 ethAmount); + + event ProxyWithdrawEthAsQuote(address indexed lp, address indexed DODO, uint256 ethAmount); // ============ Functions ============ @@ -65,7 +87,7 @@ contract DODOEthProxy is ReentrancyGuard { require(msg.sender == _WETH_, "WE_SAVED_YOUR_ETH_:)"); } - function sellEthTo( + function sellEthToToken( address quoteTokenAddress, uint256 ethAmount, uint256 minReceiveTokenAmount @@ -77,11 +99,11 @@ contract DODOEthProxy is ReentrancyGuard { IWETH(_WETH_).approve(DODO, ethAmount); receiveTokenAmount = IDODO(DODO).sellBaseToken(ethAmount, minReceiveTokenAmount, ""); _transferOut(quoteTokenAddress, msg.sender, receiveTokenAmount); - emit ProxySellEth(msg.sender, quoteTokenAddress, ethAmount, receiveTokenAmount); + emit ProxySellEthToToken(msg.sender, quoteTokenAddress, ethAmount, receiveTokenAmount); return receiveTokenAmount; } - function buyEthWith( + function buyEthWithToken( address quoteTokenAddress, uint256 ethAmount, uint256 maxPayTokenAmount @@ -94,11 +116,48 @@ contract DODOEthProxy is ReentrancyGuard { IDODO(DODO).buyBaseToken(ethAmount, maxPayTokenAmount, ""); IWETH(_WETH_).withdraw(ethAmount); msg.sender.transfer(ethAmount); - emit ProxyBuyEth(msg.sender, quoteTokenAddress, ethAmount, payTokenAmount); + emit ProxyBuyEthWithToken(msg.sender, quoteTokenAddress, ethAmount, payTokenAmount); return payTokenAmount; } - function depositEth(uint256 ethAmount, address quoteTokenAddress) + function sellTokenToEth( + address baseTokenAddress, + uint256 tokenAmount, + uint256 minReceiveEthAmount + ) external preventReentrant returns (uint256 receiveEthAmount) { + address DODO = IDODOZoo(_DODO_ZOO_).getDODO(baseTokenAddress, _WETH_); + require(DODO != address(0), "DODO_NOT_EXIST"); + IERC20(baseTokenAddress).approve(DODO, tokenAmount); + _transferIn(baseTokenAddress, msg.sender, tokenAmount); + receiveEthAmount = IDODO(DODO).sellBaseToken(tokenAmount, minReceiveEthAmount, ""); + IWETH(_WETH_).withdraw(receiveEthAmount); + msg.sender.transfer(receiveEthAmount); + emit ProxySellTokenToEth(msg.sender, baseTokenAddress, tokenAmount, receiveEthAmount); + return receiveEthAmount; + } + + function buyTokenWithEth( + address baseTokenAddress, + uint256 tokenAmount, + uint256 maxPayEthAmount + ) external payable preventReentrant returns (uint256 payEthAmount) { + require(msg.value == maxPayEthAmount, "ETH_AMOUNT_NOT_MATCH"); + address DODO = IDODOZoo(_DODO_ZOO_).getDODO(baseTokenAddress, _WETH_); + require(DODO != address(0), "DODO_NOT_EXIST"); + payEthAmount = IDODO(DODO).queryBuyBaseToken(tokenAmount); + IWETH(_WETH_).deposit{value: payEthAmount}(); + IWETH(_WETH_).approve(DODO, payEthAmount); + IDODO(DODO).buyBaseToken(tokenAmount, maxPayEthAmount, ""); + _transferOut(baseTokenAddress, msg.sender, tokenAmount); + uint256 refund = maxPayEthAmount.sub(payEthAmount); + if (refund > 0) { + msg.sender.transfer(refund); + } + emit ProxyBuyTokenWithEth(msg.sender, baseTokenAddress, tokenAmount, payEthAmount); + return payEthAmount; + } + + function depositEthAsBase(uint256 ethAmount, address quoteTokenAddress) external payable preventReentrant @@ -109,10 +168,10 @@ contract DODOEthProxy is ReentrancyGuard { IWETH(_WETH_).deposit{value: ethAmount}(); IWETH(_WETH_).approve(DODO, ethAmount); IDODO(DODO).depositBaseTo(msg.sender, ethAmount); - emit ProxyDepositEth(msg.sender, DODO, ethAmount); + emit ProxyDepositEthAsBase(msg.sender, DODO, ethAmount); } - function withdrawEth(uint256 ethAmount, address quoteTokenAddress) + function withdrawEthAsBase(uint256 ethAmount, address quoteTokenAddress) external preventReentrant returns (uint256 withdrawAmount) @@ -135,11 +194,11 @@ contract DODOEthProxy is ReentrancyGuard { uint256 wethAmount = IERC20(_WETH_).balanceOf(address(this)); IWETH(_WETH_).withdraw(wethAmount); msg.sender.transfer(wethAmount); - emit ProxyWithdrawEth(msg.sender, DODO, wethAmount); + emit ProxyWithdrawEthAsBase(msg.sender, DODO, wethAmount); return wethAmount; } - function withdrawAllEth(address quoteTokenAddress) + function withdrawAllEthAsBase(address quoteTokenAddress) external preventReentrant returns (uint256 withdrawAmount) @@ -158,7 +217,71 @@ contract DODOEthProxy is ReentrancyGuard { uint256 wethAmount = IERC20(_WETH_).balanceOf(address(this)); IWETH(_WETH_).withdraw(wethAmount); msg.sender.transfer(wethAmount); - emit ProxyWithdrawEth(msg.sender, DODO, wethAmount); + emit ProxyWithdrawEthAsBase(msg.sender, DODO, wethAmount); + return wethAmount; + } + + function depositEthAsQuote(uint256 ethAmount, address baseTokenAddress) + external + payable + preventReentrant + { + require(msg.value == ethAmount, "ETH_AMOUNT_NOT_MATCH"); + address DODO = IDODOZoo(_DODO_ZOO_).getDODO(baseTokenAddress, _WETH_); + require(DODO != address(0), "DODO_NOT_EXIST"); + IWETH(_WETH_).deposit{value: ethAmount}(); + IWETH(_WETH_).approve(DODO, ethAmount); + IDODO(DODO).depositQuoteTo(msg.sender, ethAmount); + emit ProxyDepositEthAsQuote(msg.sender, DODO, ethAmount); + } + + function withdrawEthAsQuote(uint256 ethAmount, address baseTokenAddress) + external + preventReentrant + returns (uint256 withdrawAmount) + { + address DODO = IDODOZoo(_DODO_ZOO_).getDODO(baseTokenAddress, _WETH_); + require(DODO != address(0), "DODO_NOT_EXIST"); + address ethLpToken = IDODO(DODO)._QUOTE_CAPITAL_TOKEN_(); + + // transfer all pool shares to proxy + uint256 lpBalance = IERC20(ethLpToken).balanceOf(msg.sender); + IERC20(ethLpToken).transferFrom(msg.sender, address(this), lpBalance); + IDODO(DODO).withdrawQuote(ethAmount); + + // transfer remain shares back to msg.sender + lpBalance = IERC20(ethLpToken).balanceOf(address(this)); + IERC20(ethLpToken).transfer(msg.sender, lpBalance); + + // because of withdraw penalty, withdrawAmount may not equal to ethAmount + // query weth amount first and than transfer ETH to msg.sender + uint256 wethAmount = IERC20(_WETH_).balanceOf(address(this)); + IWETH(_WETH_).withdraw(wethAmount); + msg.sender.transfer(wethAmount); + emit ProxyWithdrawEthAsQuote(msg.sender, DODO, wethAmount); + return wethAmount; + } + + function withdrawAllEthAsQuote(address baseTokenAddress) + external + preventReentrant + returns (uint256 withdrawAmount) + { + address DODO = IDODOZoo(_DODO_ZOO_).getDODO(baseTokenAddress, _WETH_); + require(DODO != address(0), "DODO_NOT_EXIST"); + address ethLpToken = IDODO(DODO)._QUOTE_CAPITAL_TOKEN_(); + + // transfer all pool shares to proxy + uint256 lpBalance = IERC20(ethLpToken).balanceOf(msg.sender); + IERC20(ethLpToken).transferFrom(msg.sender, address(this), lpBalance); + IDODO(DODO).withdrawAllQuote(); + + // because of withdraw penalty, withdrawAmount may not equal to ethAmount + // query weth amount first and than transfer ETH to msg.sender + uint256 wethAmount = IERC20(_WETH_).balanceOf(address(this)); + IWETH(_WETH_).withdraw(wethAmount); + msg.sender.transfer(wethAmount); + emit ProxyWithdrawEthAsQuote(msg.sender, DODO, wethAmount); return wethAmount; } diff --git a/test/DODOEthProxy.test.ts b/test/DODOEthProxy.test.ts deleted file mode 100644 index 2bd6302..0000000 --- a/test/DODOEthProxy.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - - 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"; -import { logGas } from './utils/Log'; - -let lp: string -let trader: string -let DODOEthProxy: Contract - -async function init(ctx: DODOContext): Promise { - // switch ctx to eth proxy mode - let WETH = await contracts.newContract(contracts.WETH_CONTRACT_NAME) - await ctx.DODOZoo.methods.breedDODO( - 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" - logGas(await DODOEthProxy.methods.buyEthWith(ctx.QUOTE.options.address, decimalStr(buyAmount), decimalStr("200")).send(ctx.sendParam(trader)), "buy with eth directly") - 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" - logGas(await DODOEthProxy.methods.sellEthTo(ctx.QUOTE.options.address, decimalStr(sellAmount), decimalStr("50")).send(ctx.sendParam(trader, sellAmount)), "sell to eth directly") - assert.equal(await ctx.DODO.methods._BASE_BALANCE_().call(), decimalStr("11")) - assert.equal(await ctx.QUOTE.methods.balanceOf(trader).call(), "1098617454226610630663") - }) - }) - - describe("withdraw eth directly", () => { - it("withdraw", async () => { - let baseLpTokenAddress = await ctx.DODO.methods._BASE_CAPITAL_TOKEN_().call() - let baseLpToken = contracts.getContractWithAddress(contracts.TEST_ERC20_CONTRACT_NAME, baseLpTokenAddress) - await baseLpToken.methods.approve(DODOEthProxy.options.address, MAX_UINT256).send(ctx.sendParam(lp)) - await DODOEthProxy.methods.withdrawEth(decimalStr("5"), ctx.QUOTE.options.address).send(ctx.sendParam(lp)) - - assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp).call(), decimalStr("5")) - // console.log(await ctx.Web3.eth.getBalance(lp)) eth balance confirmed - }) - - it("withdraw all", async () => { - let baseLpTokenAddress = await ctx.DODO.methods._BASE_CAPITAL_TOKEN_().call() - let baseLpToken = contracts.getContractWithAddress(contracts.TEST_ERC20_CONTRACT_NAME, baseLpTokenAddress) - await baseLpToken.methods.approve(DODOEthProxy.options.address, MAX_UINT256).send(ctx.sendParam(lp)) - await DODOEthProxy.methods.withdrawAllEth(ctx.QUOTE.options.address).send(ctx.sendParam(lp)) - - assert.equal(await ctx.DODO.methods.getLpBaseBalance(lp).call(), "0") - // console.log(await ctx.Web3.eth.getBalance(lp)) eth balance confirmed - }) - }) - - 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/ - ) - }) - }) -}) \ No newline at end of file diff --git a/test/DODOEthProxyAsBase.test.ts b/test/DODOEthProxyAsBase.test.ts new file mode 100644 index 0000000..010ed7b --- /dev/null +++ b/test/DODOEthProxyAsBase.test.ts @@ -0,0 +1,232 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ +import * as assert from 'assert'; +import { BigNumber } from 'bignumber.js'; +import { TransactionReceipt } from 'web3-core'; +import { Contract } from 'web3-eth-contract'; + +import { + DefaultDODOContextInitConfig, + DODOContext, + getDODOContext, +} from './utils/Context'; +import * as contracts from './utils/Contracts'; +import { decimalStr, MAX_UINT256 } from './utils/Converter'; +import { logGas } from './utils/Log'; + +let lp: string; +let trader: string; +let DODOEthProxy: Contract; + +async function init(ctx: DODOContext): Promise { + // switch ctx to eth proxy mode + const WETH = await contracts.newContract(contracts.WETH_CONTRACT_NAME); + await ctx.DODOZoo.methods + .breedDODO( + 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 = contracts.getContractWithAddress( + contracts.DODO_CONTRACT_NAME, + await ctx.DODOZoo.methods + .getDODO(WETH.options.address, ctx.QUOTE.options.address) + .call() + ); + + ctx.BASE = WETH; + + 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(); + const depositAmount = "10"; + await DODOEthProxy.methods + .depositEthAsBase(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 () => { + const buyAmount = "1"; + logGas( + await DODOEthProxy.methods + .buyEthWithToken( + ctx.QUOTE.options.address, + decimalStr(buyAmount), + decimalStr("200") + ) + .send(ctx.sendParam(trader)), + "buy ETH with token directly" + ); + assert.strictEqual( + await ctx.DODO.methods._BASE_BALANCE_().call(), + decimalStr("8.999") + ); + assert.strictEqual( + await ctx.QUOTE.methods.balanceOf(trader).call(), + "898581839502056240973" + ); + }); + it("sell", async () => { + const sellAmount = "1"; + logGas( + await DODOEthProxy.methods + .sellEthToToken( + ctx.QUOTE.options.address, + decimalStr(sellAmount), + decimalStr("50") + ) + .send(ctx.sendParam(trader, sellAmount)), + "sell ETH to token directly" + ); + assert.strictEqual( + await ctx.DODO.methods._BASE_BALANCE_().call(), + decimalStr("11") + ); + assert.strictEqual( + await ctx.QUOTE.methods.balanceOf(trader).call(), + "1098617454226610630663" + ); + }); + }); + + describe("withdraw eth directly", () => { + it("withdraw", async () => { + const withdrawAmount = decimalStr("5"); + const baseLpTokenAddress = await ctx.DODO.methods + ._BASE_CAPITAL_TOKEN_() + .call(); + const baseLpToken = contracts.getContractWithAddress( + contracts.TEST_ERC20_CONTRACT_NAME, + baseLpTokenAddress + ); + await baseLpToken.methods + .approve(DODOEthProxy.options.address, MAX_UINT256) + .send(ctx.sendParam(lp)); + const lpEthBalanceBefore = await ctx.Web3.eth.getBalance(lp); + const txReceipt: TransactionReceipt = await DODOEthProxy.methods + .withdrawEthAsBase(withdrawAmount, ctx.QUOTE.options.address) + .send(ctx.sendParam(lp)); + + assert.strictEqual( + await ctx.DODO.methods.getLpBaseBalance(lp).call(), + withdrawAmount + ); + const tx = await ctx.Web3.eth.getTransaction(txReceipt.transactionHash); + const ethSpentOnGas = new BigNumber(tx.gasPrice).times(txReceipt.gasUsed); + const lpEthBalanceAfter = await ctx.Web3.eth.getBalance(lp); + assert.ok( + new BigNumber(lpEthBalanceBefore) + .plus(withdrawAmount) + .minus(ethSpentOnGas) + .eq(lpEthBalanceAfter) + ); + }); + + it("withdraw all", async () => { + const withdrawAmount = decimalStr("10"); + const baseLpTokenAddress = await ctx.DODO.methods + ._BASE_CAPITAL_TOKEN_() + .call(); + const baseLpToken = contracts.getContractWithAddress( + contracts.TEST_ERC20_CONTRACT_NAME, + baseLpTokenAddress + ); + await baseLpToken.methods + .approve(DODOEthProxy.options.address, MAX_UINT256) + .send(ctx.sendParam(lp)); + const lpEthBalanceBefore = await ctx.Web3.eth.getBalance(lp); + const txReceipt: TransactionReceipt = await DODOEthProxy.methods + .withdrawAllEthAsBase(ctx.QUOTE.options.address) + .send(ctx.sendParam(lp)); + + assert.strictEqual( + await ctx.DODO.methods.getLpBaseBalance(lp).call(), + "0" + ); + const tx = await ctx.Web3.eth.getTransaction(txReceipt.transactionHash); + const ethSpentOnGas = new BigNumber(tx.gasPrice).times(txReceipt.gasUsed); + const lpEthBalanceAfter = await ctx.Web3.eth.getBalance(lp); + assert.ok( + new BigNumber(lpEthBalanceBefore) + .plus(withdrawAmount) + .minus(ethSpentOnGas) + .eq(lpEthBalanceAfter) + ); + }); + }); + + describe("revert cases", () => { + it("value not match", async () => { + await assert.rejects( + DODOEthProxy.methods + .sellEthToToken( + ctx.QUOTE.options.address, + decimalStr("1"), + decimalStr("50") + ) + .send(ctx.sendParam(trader, "2")), + /ETH_AMOUNT_NOT_MATCH/ + ); + await assert.rejects( + DODOEthProxy.methods + .depositEthAsBase(decimalStr("1"), ctx.QUOTE.options.address) + .send(ctx.sendParam(lp, "2")), + /ETH_AMOUNT_NOT_MATCH/ + ); + }); + }); +}); diff --git a/test/DODOEthProxyAsQuote.test.ts b/test/DODOEthProxyAsQuote.test.ts new file mode 100644 index 0000000..985a8b4 --- /dev/null +++ b/test/DODOEthProxyAsQuote.test.ts @@ -0,0 +1,244 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ +import * as assert from 'assert'; +import BigNumber from 'bignumber.js'; +import { TransactionReceipt } from 'web3-core'; +import { Contract } from 'web3-eth-contract'; + +import { + DefaultDODOContextInitConfig, + DODOContext, + getDODOContext, +} from './utils/Context'; +import * as contracts from './utils/Contracts'; +import { decimalStr, MAX_UINT256 } from './utils/Converter'; +import { logGas } from './utils/Log'; + +let lp: string; +let trader: string; +let DODOEthProxy: Contract; + +async function init(ctx: DODOContext): Promise { + // switch ctx to eth proxy mode + const WETH = await contracts.newContract(contracts.WETH_CONTRACT_NAME); + await ctx.DODOZoo.methods + .breedDODO( + ctx.Maintainer, + ctx.BASE.options.address, + WETH.options.address, + ctx.ORACLE.options.address, + DefaultDODOContextInitConfig.lpFeeRate, + DefaultDODOContextInitConfig.mtFeeRate, + DefaultDODOContextInitConfig.k, + DefaultDODOContextInitConfig.gasPriceLimit + ) + .send(ctx.sendParam(ctx.Deployer)); + + ctx.DODO = contracts.getContractWithAddress( + contracts.DODO_CONTRACT_NAME, + await ctx.DODOZoo.methods + .getDODO(ctx.BASE.options.address, WETH.options.address) + .call() + ); + + ctx.QUOTE = WETH; + + 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("0.01")); + await ctx.approveDODO(lp); + await ctx.approveDODO(trader); + + await ctx.BASE.methods + .mint(lp, decimalStr("1000")) + .send(ctx.sendParam(ctx.Deployer)); + await ctx.BASE.methods + .mint(trader, decimalStr("1000")) + .send(ctx.sendParam(ctx.Deployer)); + await ctx.BASE.methods + .approve(DODOEthProxy.options.address, MAX_UINT256) + .send(ctx.sendParam(trader)); + + await ctx.DODO.methods + .depositBase(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.BASE.methods + .approve(DODOEthProxy.options.address, MAX_UINT256) + .send(ctx.sendParam(trader)); + }); + + beforeEach(async () => { + snapshotId = await ctx.EVM.snapshot(); + let depositAmount = "10"; + await DODOEthProxy.methods + .depositEthAsQuote(decimalStr(depositAmount), ctx.BASE.options.address) + .send(ctx.sendParam(lp, depositAmount)); + }); + + afterEach(async () => { + await ctx.EVM.reset(snapshotId); + }); + + describe("buy&sell eth directly", () => { + it("buy", async () => { + const maxPayEthAmount = "2.1"; + const ethInPoolBefore = decimalStr("10"); + const traderEthBalanceBefore = await ctx.Web3.eth.getBalance(trader); + const txReceipt: TransactionReceipt = await DODOEthProxy.methods + .buyTokenWithEth( + ctx.BASE.options.address, + decimalStr("200"), + decimalStr(maxPayEthAmount) + ) + .send(ctx.sendParam(trader, maxPayEthAmount)); + logGas(txReceipt, "buy token with ETH directly"); + const ethInPoolAfter = "12056338203652739553"; + assert.strictEqual( + await ctx.DODO.methods._QUOTE_BALANCE_().call(), + ethInPoolAfter + ); + assert.strictEqual( + await ctx.BASE.methods.balanceOf(trader).call(), + decimalStr("1200") + ); + const tx = await ctx.Web3.eth.getTransaction(txReceipt.transactionHash); + const ethSpentOnGas = new BigNumber(tx.gasPrice).times(txReceipt.gasUsed); + const traderEthBalanceAfter = await ctx.Web3.eth.getBalance(trader); + + const totalEthBefore = new BigNumber(traderEthBalanceBefore).plus( + ethInPoolBefore + ); + const totalEthAfter = new BigNumber(traderEthBalanceAfter) + .plus(ethSpentOnGas) + .plus(ethInPoolAfter); + assert.ok(totalEthBefore.eq(totalEthAfter)); + }); + it("sell", async () => { + const minReceiveEthAmount = "0.45"; + logGas( + await DODOEthProxy.methods + .sellTokenToEth( + ctx.BASE.options.address, + decimalStr("50"), + decimalStr(minReceiveEthAmount) + ) + .send(ctx.sendParam(trader)), + "sell token to ETH directly" + ); + assert.strictEqual( + await ctx.DODO.methods._QUOTE_BALANCE_().call(), + "9503598324131652490" + ); + assert.strictEqual( + await ctx.BASE.methods.balanceOf(trader).call(), + decimalStr("950") + ); + }); + }); + + describe("withdraw eth directly", () => { + it("withdraw", async () => { + const withdrawAmount = decimalStr("5"); + const quoteLpTokenAddress = await ctx.DODO.methods + ._QUOTE_CAPITAL_TOKEN_() + .call(); + const quoteLpToken = contracts.getContractWithAddress( + contracts.TEST_ERC20_CONTRACT_NAME, + quoteLpTokenAddress + ); + await quoteLpToken.methods + .approve(DODOEthProxy.options.address, MAX_UINT256) + .send(ctx.sendParam(lp)); + const lpEthBalanceBefore = await ctx.Web3.eth.getBalance(lp); + const txReceipt: TransactionReceipt = await DODOEthProxy.methods + .withdrawEthAsQuote(withdrawAmount, ctx.BASE.options.address) + .send(ctx.sendParam(lp)); + + assert.strictEqual( + await ctx.DODO.methods.getLpQuoteBalance(lp).call(), + withdrawAmount + ); + const tx = await ctx.Web3.eth.getTransaction(txReceipt.transactionHash); + const ethSpentOnGas = new BigNumber(tx.gasPrice).times(txReceipt.gasUsed); + const lpEthBalanceAfter = await ctx.Web3.eth.getBalance(lp); + assert.ok( + new BigNumber(lpEthBalanceBefore) + .plus(withdrawAmount) + .minus(ethSpentOnGas) + .eq(lpEthBalanceAfter) + ); + }); + + it("withdraw all", async () => { + const withdrawAmount = decimalStr("10"); + const quoteLpTokenAddress = await ctx.DODO.methods + ._QUOTE_CAPITAL_TOKEN_() + .call(); + const quoteLpToken = contracts.getContractWithAddress( + contracts.TEST_ERC20_CONTRACT_NAME, + quoteLpTokenAddress + ); + await quoteLpToken.methods + .approve(DODOEthProxy.options.address, MAX_UINT256) + .send(ctx.sendParam(lp)); + const lpEthBalanceBefore = await ctx.Web3.eth.getBalance(lp); + const txReceipt: TransactionReceipt = await DODOEthProxy.methods + .withdrawAllEthAsQuote(ctx.BASE.options.address) + .send(ctx.sendParam(lp)); + + assert.strictEqual( + await ctx.DODO.methods.getLpQuoteBalance(lp).call(), + "0" + ); + const tx = await ctx.Web3.eth.getTransaction(txReceipt.transactionHash); + const ethSpentOnGas = new BigNumber(tx.gasPrice).times(txReceipt.gasUsed); + const lpEthBalanceAfter = await ctx.Web3.eth.getBalance(lp); + assert.ok( + new BigNumber(lpEthBalanceBefore) + .plus(withdrawAmount) + .minus(ethSpentOnGas) + .eq(lpEthBalanceAfter) + ); + }); + }); + + describe("revert cases", () => { + it("value not match", async () => { + await assert.rejects( + DODOEthProxy.methods + .buyTokenWithEth( + ctx.BASE.options.address, + decimalStr("50"), + decimalStr("1") + ) + .send(ctx.sendParam(trader, "2")), + /ETH_AMOUNT_NOT_MATCH/ + ); + await assert.rejects( + DODOEthProxy.methods + .depositEthAsQuote(decimalStr("1"), ctx.BASE.options.address) + .send(ctx.sendParam(lp, "2")), + /ETH_AMOUNT_NOT_MATCH/ + ); + }); + }); +});