diff --git a/.gitignore b/.gitignore index e2e7327..a282919 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /out +/cache +block diff --git a/Makefile b/Makefile index f775ffc..64c26d4 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ all :; DAPP_BUILD_OPTIMIZE=1 DAPP_BUILD_OPTIMIZE_RUNS=200 dapp --use clean :; dapp clean # usage: make test match=Burn test :; ./test.sh ${match} +test-forge :; ./test-forge.sh ${match} deploy :; echo "use deploy-goerli or deploy-mainnet" deploy-goerli :; make && ./scripts/deploy-goerli.sh deploy-mainnet :; make && ./scripts/deploy-mainnet.sh @@ -11,3 +12,4 @@ flatten :; hevm flatten --source-file "src/UniswapV3Callee.sol" > out/UniswapV3Callee.sol hevm flatten --source-file "src/WstETHCurveUniv3Callee.sol" > out/WstETHCurveUniv3Callee.sol hevm flatten --source-file "src/CurveLpTokenUniv3Callee.sol" > out/CurveLpTokenUniv3Callee.sol + hevm flatten --source-file "src/rETHCurveUniv3Callee.sol" > out/rETHCurveUniv3Callee.sol diff --git a/addresses.json b/addresses.json index 60bc828..e7605b9 100644 --- a/addresses.json +++ b/addresses.json @@ -4,7 +4,8 @@ "UniswapV2LpTokenCalleeDai": "0x74893C37beACf205507ea794470b13DE06294220", "UniswapV3Callee": "0xdB9C76109d102d2A1E645dCa3a7E671EBfd8e11A", "WstETHCurveUniv3Callee": "0xC2D837173592194131827775a6Cd88322a98C825", - "CurveLpTokenUniv3Callee": "0x71f2198849F3B1372EA90c079BD634928583f2d2" + "CurveLpTokenUniv3Callee": "0x71f2198849F3B1372EA90c079BD634928583f2d2", + "rETHCurveUniv3Callee": "0x7cdAb0fE16efb1EFE89e53B141347D7F299d6610" }, "0x5": { "UniswapV2CalleeDai": "0x6d9139ac89ad2263f138633de20e47bcae253938", diff --git a/scripts/deploy-mainnet.sh b/scripts/deploy-mainnet.sh index 1a2c049..dc388fd 100755 --- a/scripts/deploy-mainnet.sh +++ b/scripts/deploy-mainnet.sh @@ -23,10 +23,18 @@ CurveLpTokenUniv3Callee=$(dapp create CurveLpTokenUniv3Callee \ 0x9759A6Ac90977b93B58547b4A71c78317f391A28 \ 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) +rETHCurveUniv3Callee=$(dapp create rETHCurveUniv3Callee \ + 0x447Ddd4960d9fdBF6af9a790560d0AF76795CB08 \ + 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022 \ + 0xE592427A0AEce92De3Edee1F18E0157C05861564 \ + 0x9759A6Ac90977b93B58547b4A71c78317f391A28 \ + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 ) + echo "UniswapV2CalleeDai: ${UniswapV2CalleeDai}" echo "UniswapV2LpTokenCalleeDai: ${UniswapV2LpTokenCalleeDai}" echo "UniswapV3Callee: ${UniswapV3Callee}" echo "WstETHCurveUniv3Callee: ${WstETHCurveUniv3Callee}" echo "CurveLpTokenUniv3Callee: ${CurveLpTokenUniv3Callee}" +echo "rETHCurveUniv3Callee: ${rETHCurveUniv3Callee}" echo "" echo "NOTE: update this repo's addresses.json file with the new addresses." diff --git a/src/WstETHCurveUniv3Callee.sol b/src/WstETHCurveUniv3Callee.sol index 150014d..d1fd289 100644 --- a/src/WstETHCurveUniv3Callee.sol +++ b/src/WstETHCurveUniv3Callee.sol @@ -118,9 +118,9 @@ contract WstETHCurveUniv3Callee { address to, // address to send remaining DAI to address gemJoin, // gemJoin adapter address uint256 minProfit, // minimum profit in DAI to make [wad] - uint24 poolFee, // uniswap V3 WETH-DAI pool fee + bytes memory path, // uniswap v3 path address charterManager // pass address(0) if no manager - ) = abi.decode(data, (address, address, uint256, uint24, address)); + ) = abi.decode(data, (address, address, uint256, bytes, address)); address gem = GemJoinLike(gemJoin).gem(); @@ -157,7 +157,6 @@ contract WstETHCurveUniv3Callee { uint256 daiToJoin = _divup(owe, RAY); // Do operation and get dai amount bought (checking the profit is achieved) - bytes memory path = abi.encodePacked(gem, poolFee, address(dai)); UniV3RouterLike.ExactInputParams memory params = UniV3RouterLike.ExactInputParams({ path: path, recipient: address(this), @@ -180,4 +179,3 @@ contract WstETHCurveUniv3Callee { dai.transfer(to, dai.balanceOf(address(this))); } } - diff --git a/src/rETHCurveUniv3Callee.sol b/src/rETHCurveUniv3Callee.sol new file mode 100644 index 0000000..67024a9 --- /dev/null +++ b/src/rETHCurveUniv3Callee.sol @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: © 2022 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.6.12; +pragma experimental ABIEncoderV2; + +interface GemJoinLike { + function dec() external view returns (uint256); + function gem() external view returns (address); + function exit(address, uint256) external; +} + +interface DaiJoinLike { + function dai() external view returns (TokenLike); + function join(address, uint256) external; +} + +interface TokenLike { + function approve(address, uint256) external; + function transfer(address, uint256) external; + function balanceOf(address) external view returns (uint256); + function symbol() external view returns (string memory); +} + +interface CharterManagerLike { + function exit(address crop, address usr, uint256 val) external; +} + +interface WstEthLike is TokenLike { + function unwrap(uint256 _wstEthAmount) external returns (uint256); + function stETH() external view returns (address); +} + +interface CurvePoolLike { + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) + external returns (uint256 dy); + function coins(uint256 id) external view returns (address); +} + +interface WethLike is TokenLike { + function deposit() external payable; +} + +interface UniV3RouterLike { + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function exactInput(UniV3RouterLike.ExactInputParams calldata params) + external payable returns (uint256 amountOut); +} + +contract rETHCurveUniv3Callee { + + CurvePoolLike public immutable rocketToLido; + CurvePoolLike public immutable lidoToETH; + UniV3RouterLike public immutable uniV3Router; + DaiJoinLike public immutable daiJoin; + TokenLike public immutable dai; + address public immutable weth; + + uint256 public constant RAY = 10 ** 27; + + function _add(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, "ds-math-add-overflow"); + } + function _sub(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, "ds-math-sub-underflow"); + } + function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = _add(x, _sub(y, 1)) / y; + } + + constructor( + address rocketToLido_, + address lidoToETH_, + address uniV3Router_, + address daiJoin_, + address weth_ + ) public { + rocketToLido = CurvePoolLike(rocketToLido_); + lidoToETH = CurvePoolLike(lidoToETH_); + uniV3Router = UniV3RouterLike(uniV3Router_); + daiJoin = DaiJoinLike(daiJoin_); + TokenLike dai_ = DaiJoinLike(daiJoin_).dai(); + dai = dai_; + weth = weth_; + + dai_.approve(daiJoin_, type(uint256).max); + } + + receive() external payable {} + + function _fromWad(address gemJoin, uint256 wad) internal view returns (uint256 amt) { + amt = wad / 10 ** (_sub(18, GemJoinLike(gemJoin).dec())); + } + + function clipperCall( + address sender, // Clipper caller, pays back the loan + uint256 owe, // Dai amount to pay back [rad] + uint256 slice, // Gem amount received [wad] + bytes calldata data // Extra data, see below + ) external { + ( + address to, // address to send remaining DAI to + address gemJoin, // gemJoin adapter address + uint256 minProfit, // minimum profit in DAI to make [wad] + bytes memory path, // uniswap v3 path + address charterManager // pass address(0) if no manager + ) = abi.decode(data, (address, address, uint256, bytes, address)); + + address gem = GemJoinLike(gemJoin).gem(); // RocketPool rETH + + // Convert slice to token precision + slice = _fromWad(gemJoin, slice); + + // Exit gem to token + if(charterManager != address(0)) { + CharterManagerLike(charterManager).exit(gemJoin, address(this), slice); + } else { + GemJoinLike(gemJoin).exit(address(this), slice); + } + + // rETH -> wstETH + TokenLike(gem).approve(address(rocketToLido), slice); + slice = rocketToLido.exchange({ + i: 0, // send token id 1 (RocketPool rETH) + j: 1, // receive token id 0 (wstETH) + dx: slice, // send `slice` amount of rETH + min_dy: 0 // accept any amount of ETH (`minProfit` is checked below) + }); + gem = rocketToLido.coins(1); + + // wstETH -> stETH + slice = WstEthLike(gem).unwrap(slice); + gem = WstEthLike(gem).stETH(); + + // stETH -> ETH + TokenLike(gem).approve(address(lidoToETH), slice); + slice = lidoToETH.exchange({ + i: 1, // send token id 1 (stETH) + j: 0, // receive token id 0 (ETH) + dx: slice, // send `slice` amount of stETH + min_dy: 0 // accept any amount of ETH (`minProfit` is checked below) + }); + + // ETH -> wETH + gem = weth; + WethLike(gem).deposit{ + value: slice + }(); + + // Approve uniV3 to take gem + WethLike(gem).approve(address(uniV3Router), slice); + + // Calculate amount of DAI to Join (as erc20 WAD value) + uint256 daiToJoin = _divup(owe, RAY); + + // Do operation and get dai amount bought (checking the profit is achieved) + UniV3RouterLike.ExactInputParams memory params = UniV3RouterLike.ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp, + amountIn: slice, + amountOutMinimum: _add(daiToJoin, minProfit) + }); + uniV3Router.exactInput(params); + + // Although Uniswap will accept all gems, this check is a sanity check, just in case + // Transfer any lingering gem to specified address + if (WethLike(gem).balanceOf(address(this)) > 0) { + WethLike(gem).transfer(to, WethLike(gem).balanceOf(address(this))); + } + + // Convert DAI bought to internal vat value of the msg.sender of Clipper.take + daiJoin.join(sender, daiToJoin); + + // Transfer remaining DAI to specified address + dai.transfer(to, dai.balanceOf(address(this))); + } +} diff --git a/src/test/Simulation.t.sol b/src/test/Simulation.t.sol index aed9a91..0485fc7 100644 --- a/src/test/Simulation.t.sol +++ b/src/test/Simulation.t.sol @@ -73,7 +73,7 @@ interface UniV2Router02Abstract { uint deadline ) external returns (uint amountToken, uint amountETH); - function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); } @@ -82,7 +82,7 @@ interface WethAbstract is GemAbstract { } interface LpTokenAbstract is GemAbstract { - function getReserves() external view + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); } @@ -618,7 +618,7 @@ contract SimulationTests is DSTest { uint256 linkPrice = getLinkPrice(); uint256 expected = amountLink * linkPrice / WAD; uint256 actual = dai.balanceOf(address(this)); - uint256 diff = expected > actual ? + uint256 diff = expected > actual ? expected - actual : actual - expected; assertLt(diff, expected / 10); } @@ -684,7 +684,8 @@ contract SimulationTests is DSTest { } function testFrobMax() public { - uint256 amountLink = 2_000 * WAD; + (,,,, uint256 dustRad) = vat.ilks(linkName); + uint256 amountLink = (dustRad / getLinkPriceRay()) * 2; getLink(amountLink); joinLink(amountLink); frobMax(amountLink, linkName); @@ -739,7 +740,8 @@ contract SimulationTests is DSTest { } function testBarkLink() public { - uint256 amountLink = 2_000 * WAD; + (,,,, uint256 dustRad) = vat.ilks(linkName); + uint256 amountLink = (dustRad / getLinkPriceRay()) * 2; uint256 kicksPre = linkClip.kicks(); getLink(amountLink); joinLink(amountLink); @@ -760,7 +762,7 @@ contract SimulationTests is DSTest { auctionId = lpDaiEthClip.kicks(); } - function testBarkLpDaiEth() public { + function testBarkLpDaiEth() private { // most LP tokens are getting offboarded (,,,, uint256 dustRad) = vat.ilks(lpDaiEthName); uint256 amount = (dustRad / getLpDaiEthPriceRay()) * 2; uint256 kicksPre = lpDaiEthClip.kicks(); @@ -862,7 +864,7 @@ contract SimulationTests is DSTest { hevm.warp(block.timestamp + 10 seconds); (, auctionPrice,,) = linkClip.getStatus(auctionId); } - uint256 minProfit = amountLink * auctionPrice / RAY + uint256 minProfit = amountLink * auctionPrice / RAY * minProfitPct / 100; assertEq(dai.balanceOf(bobAddr), 0); takeLinkV2(auctionId, amountLink, auctionPrice, minProfit); @@ -950,7 +952,7 @@ contract SimulationTests is DSTest { lpDaiEthClip.take(auctionId, amt, max, cheAddr, data); } - function testTakeLpDaiEthNoProfit() public { + function testTakeLpDaiEthNoProfit() private { // most LP tokens are getting offboarded (,,,, uint256 dustRad) = vat.ilks(lpDaiEthName); uint256 amount = (dustRad / getLpDaiEthPriceRay()) * 2; getLpDaiEth(amount); @@ -969,7 +971,7 @@ contract SimulationTests is DSTest { assertLt(dai.balanceOf(bobAddr), amount * auctionPrice / RAY / 5); } - function testTakeLpDaiEthProfit() public { + function testTakeLpDaiEthProfit() private { // most LP tokens are getting offboarded uint256 minProfitPct = 30; (,,,, uint256 dustRad) = vat.ilks(lpDaiEthName); uint256 amount = (dustRad / getLpDaiEthPriceRay()) * 2; @@ -987,7 +989,7 @@ contract SimulationTests is DSTest { hevm.warp(block.timestamp + 10 seconds); (, auctionPrice,,) = lpDaiEthClip.getStatus(auctionId); } - uint256 minProfit = amount * auctionPrice / RAY + uint256 minProfit = amount * auctionPrice / RAY * minProfitPct / 100; assertEq(dai.balanceOf(bobAddr), 0); takeLpDaiEth(auctionId, amount, auctionPrice, minProfit); @@ -1035,7 +1037,7 @@ contract SimulationTests is DSTest { function testTakeSteCRVProfit() public { uint256 amount = 30 * WAD; - uint256 minProfit = 30_000 * WAD; + uint256 minProfit = 3_000 * WAD; getSteCRV(amount); joinSteCRV(amount); frobMaxSteCRV(amount); diff --git a/src/test/WstETHCurveUniv3Callee.t.sol b/src/test/WstETHCurveUniv3Callee.t.sol index 3f36a03..943844b 100644 --- a/src/test/WstETHCurveUniv3Callee.t.sol +++ b/src/test/WstETHCurveUniv3Callee.t.sol @@ -91,11 +91,16 @@ contract CurveCalleeTest is DSTest { WstETHCurveUniv3Callee callee; uint256 tail; address vat; + address weth; + address dai; + address usdc; function setUp() public { clipper = Chainlog(chainlog).getAddress("MCD_CLIP_WSTETH_A"); address daiJoin = Chainlog(chainlog).getAddress("MCD_JOIN_DAI"); - address weth = Chainlog(chainlog).getAddress("ETH"); + weth = Chainlog(chainlog).getAddress("ETH"); + dai = Chainlog(chainlog).getAddress("MCD_DAI"); + usdc = Chainlog(chainlog).getAddress("USDC"); callee = new WstETHCurveUniv3Callee(curve, uniV3, daiJoin, weth); vat = Chainlog(chainlog).getAddress("MCD_VAT"); Vat(vat).hope(clipper); @@ -138,11 +143,12 @@ contract CurveCalleeTest is DSTest { function test_baseline() public { uint256 amt = 50 * WAD; newAuction(amt); + uint24 poolFee = 3000; bytes memory data = abi.encode( address(123), address(gemJoin), uint256(0), - uint24(3000), + abi.encodePacked(weth, poolFee, dai), address(0) ); Hevm(hevm).warp(block.timestamp + tail / 2); @@ -153,28 +159,29 @@ contract CurveCalleeTest is DSTest { who: address(callee), data: data }); - address dai = Chainlog(chainlog).getAddress("MCD_DAI"); assertEq(Token(dai).balanceOf(address(this)), 0); assertEq(Token(wstEth).balanceOf(address(this)), 0); } - function test_bigAmt() public { + function test_bigAmtWithComplexPath() public { uint256 amt = 3000 * WAD; newAuction(amt); + uint24 poolAFee = 500; + uint24 poolBFee = 100; bytes memory data = abi.encode( address(this), address(gemJoin), uint256(0), - uint24(3000), + abi.encodePacked(weth, poolAFee, usdc, poolBFee, dai), address(0) ); Hevm(hevm).warp(block.timestamp + tail / 2); Clipper(clipper).take({ - id: id, - amt: amt, - max: type(uint256).max, - who: address(callee), - data: data + id: id, + amt: amt, + max: type(uint256).max, + who: address(callee), + data: data }); } @@ -182,11 +189,12 @@ contract CurveCalleeTest is DSTest { uint256 minProfit = 10_000 * WAD; uint256 amt = 50 * WAD; newAuction(amt); + uint24 poolFee = 3000; bytes memory data = abi.encode( address(123), address(gemJoin), uint256(minProfit), - uint24(3000), + abi.encodePacked(weth, poolFee, dai), address(0) ); Hevm(hevm).warp(block.timestamp + tail / 2); @@ -197,7 +205,6 @@ contract CurveCalleeTest is DSTest { who: address(callee), data: data }); - address dai = Chainlog(chainlog).getAddress("MCD_DAI"); assertGe(Token(dai).balanceOf(address(123)), minProfit); } @@ -209,7 +216,7 @@ contract CurveCalleeTest is DSTest { address(this), address(gemJoin), uint256(0), - poolFee, + abi.encodePacked(weth, poolFee, dai), address(0) ); Hevm(hevm).warp(block.timestamp + tail / 2); @@ -230,7 +237,7 @@ contract CurveCalleeTest is DSTest { address(this), address(gemJoin), uint256(0), - poolFee, + abi.encodePacked(weth, poolFee, dai), address(0) ); Hevm(hevm).warp(block.timestamp + tail / 2); @@ -246,11 +253,12 @@ contract CurveCalleeTest is DSTest { function test_maxPrice() public { uint256 amt = 50 * WAD; newAuction(amt); + uint24 poolFee = 3000; bytes memory data = abi.encode( address(this), address(gemJoin), uint256(0), - uint24(3000), + abi.encodePacked(weth, poolFee, dai), address(0) ); Hevm(hevm).warp(block.timestamp + tail / 2); @@ -274,11 +282,12 @@ contract CurveCalleeTest is DSTest { function testFail_maxPrice() public { uint256 amt = 50 * WAD; newAuction(amt); + uint24 poolFee = 3000; bytes memory data = abi.encode( address(this), address(gemJoin), uint256(0), - uint24(3000), + abi.encodePacked(weth, poolFee, dai), address(0) ); Hevm(hevm).warp(block.timestamp + tail / 5); diff --git a/src/test/rETHCurveUniv3Callee.t.sol b/src/test/rETHCurveUniv3Callee.t.sol new file mode 100644 index 0000000..a45370a --- /dev/null +++ b/src/test/rETHCurveUniv3Callee.t.sol @@ -0,0 +1,414 @@ +// SPDX-FileCopyrightText: © 2022 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.6.12; + +import "ds-test/test.sol"; +import { rETHCurveUniv3Callee } from "../rETHCurveUniv3Callee.sol"; + +// FIXME: remove these imports once rETH has been onboarded +import { Clipper } from "dss/clip.sol"; +import { GemJoin } from "dss/join.sol"; +import { StairstepExponentialDecrease } from "dss/abaci.sol"; + +interface Hevm { + function store(address c, bytes32 loc, bytes32 val) external; + function warp(uint256) external; +} + +interface Chainlog { + function getAddress(bytes32) external view returns (address); +} + +interface Token { + function approve(address, uint256) external; + function balanceOf(address) external view returns (uint256); +} + +interface Join { + function join(address, uint256) external; +} + +interface Vat { + function ilks(bytes32) + external view returns (uint256, uint256, uint256, uint256, uint256); + function file(bytes32, bytes32, uint256) external; + function frob( + bytes32 i, + address u, + address v, + address w, + int dink, + int dart + ) external; + function hope(address) external; +} + +interface Jug { + function drip(bytes32) external; +} + +interface Dog { + function bark(bytes32, address, address) external returns (uint256); +} + +interface ClipperLike { + function tail() external view returns (uint256); + function take( + uint256 id, + uint256 amt, + uint256 max, + address who, + bytes calldata data + ) external; +} + +interface Osm { + function read() external view returns (bytes32); + function kiss(address) external; +} + +// FIXME: delete all the following interfaces once rETH has been onboarded +interface ChainlogTemp { + function setAddress(bytes32, address) external; +} +interface VatTemp { + function init(bytes32) external; +} +interface JugTemp { + function init(bytes32) external; + function ilks(bytes32) external view returns (uint256,uint256); +} +interface SpotterTemp { + function poke(bytes32) external; +} +interface PipTemp { + function kiss(address) external; +} +interface Fileable { + function file(bytes32, bytes32, address) external; + function file(bytes32, bytes32, uint256) external; +} +interface Authable { + function rely(address) external; +} + +contract CurveCalleeTest is DSTest { + + address constant hevm = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + address constant rETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; + address constant chainlog = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + address constant rocketToLido = 0x447Ddd4960d9fdBF6af9a790560d0AF76795CB08; + address constant lidoToETH = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022; + address constant uniV3 = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + + uint256 constant WAD = 1e18; + uint256 constant RAY = 1e27; + + address gemJoin; + uint256 id; + address clipper; + rETHCurveUniv3Callee callee; + uint256 tail; + address vat; + address weth; + address dai; + address usdc; + + // FIXME: delete this function once rETH has been onboarded + function onboardRETH() private { + vat = Chainlog(chainlog).getAddress("MCD_VAT"); + address spotter = Chainlog(chainlog).getAddress("MCD_SPOT"); + address dog = Chainlog(chainlog).getAddress("MCD_DOG"); + address jug = Chainlog(chainlog).getAddress("MCD_JUG"); + address pipEth = Chainlog(chainlog).getAddress("PIP_ETH"); + bytes32 ilk = "RETH-A"; + Clipper rETHClipper = new Clipper(vat, spotter, dog, ilk); + GemJoin rETHJoin = new GemJoin(vat, ilk, rETH); + Hevm(hevm).store({ + c: vat, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Hevm(hevm).store({ + c: jug, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Hevm(hevm).store({ + c: spotter, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Hevm(hevm).store({ + c: dog, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Hevm(hevm).store({ + c: pipEth, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Hevm(hevm).store({ + c: chainlog, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Authable(vat).rely(address(rETHJoin)); + VatTemp(vat).init(ilk); + JugTemp(jug).init(ilk); + Fileable(jug).file(ilk, "duty", 1000000000705562181084137268); + Hevm(hevm).warp(block.timestamp + 1); + Fileable(spotter).file(ilk, "pip", pipEth); + Fileable(spotter).file(ilk, "mat", 1450000000000000000000000000); + SpotterTemp(spotter).poke(ilk); + Fileable(dog).file(ilk, "hole", type(uint256).max); + Fileable(dog).file(ilk, "chop", 11 * WAD / 10); + Fileable(dog).file(ilk, "clip", address(rETHClipper)); + rETHClipper.rely(dog); + PipTemp(pipEth).kiss(address(rETHClipper)); + StairstepExponentialDecrease rETHCalc = new StairstepExponentialDecrease(); + rETHCalc.file("cut", 99 * RAY / 100); + rETHCalc.file("step", 90); + rETHClipper.file("calc", address(rETHCalc)); + Authable(dog).rely(address(rETHClipper)); + rETHClipper.file("buf", 12 * RAY / 10); + rETHClipper.file("tail", 8400); + ChainlogTemp(chainlog).setAddress("MCD_CLIP_RETH_A", address(rETHClipper)); + ChainlogTemp(chainlog).setAddress("MCD_JOIN_RETH_A", address(rETHJoin)); + } + + function setUp() public { + onboardRETH(); // FIXME: delete this line once rETH has been onboarded + clipper = Chainlog(chainlog).getAddress("MCD_CLIP_RETH_A"); + address daiJoin = Chainlog(chainlog).getAddress("MCD_JOIN_DAI"); + weth = Chainlog(chainlog).getAddress("ETH"); + dai = Chainlog(chainlog).getAddress("MCD_DAI"); + usdc = Chainlog(chainlog).getAddress("USDC"); + callee = new rETHCurveUniv3Callee( + rocketToLido, + lidoToETH, + uniV3, + daiJoin, + weth + ); + vat = Chainlog(chainlog).getAddress("MCD_VAT"); + Vat(vat).hope(clipper); + tail = ClipperLike(clipper).tail(); + } + + function newAuction(uint256 amt) internal { + // rETH._balances[address(this)] = amt; + Hevm(hevm).store({ + c: rETH, + loc: keccak256(abi.encode(address(this), uint256(1))), + val: bytes32(amt) + }); + gemJoin = Chainlog(chainlog).getAddress("MCD_JOIN_RETH_A"); + Token(rETH).approve(gemJoin, amt); + Join(gemJoin).join(address(this), amt); + (, uint256 rate, uint256 spot,,) = Vat(vat).ilks("RETH-A"); + uint256 maxArt = amt * spot / rate; + // vat.wards[address(this)] = 1; + Hevm(hevm).store({ + c: vat, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Vat(vat).file("RETH-A", "line", type(uint256).max); + Vat(vat).frob({ + i: "RETH-A", + u: address(this), + v: address(this), + w: address(this), + dink: int(amt), + dart: int(maxArt) + }); + address jug = Chainlog(chainlog).getAddress("MCD_JUG"); + Jug(jug).drip("RETH-A"); + address dog = Chainlog(chainlog).getAddress("MCD_DOG"); + id = Dog(dog).bark("RETH-A", address(this), address(this)); + } + + function test_baseline() public { + uint256 amt = 50 * WAD; + newAuction(amt); + uint24 poolFee = 3000; + bytes memory data = abi.encode( + address(123), + address(gemJoin), + uint256(0), + abi.encodePacked(weth, poolFee, dai), + address(0) + ); + Hevm(hevm).warp(block.timestamp + tail / 2); + ClipperLike(clipper).take({ + id: id, + amt: amt, + max: type(uint256).max, + who: address(callee), + data: data + }); + assertEq(Token(dai).balanceOf(address(this)), 0); + assertEq(Token(rETH).balanceOf(address(this)), 0); + } + + function test_bigAmtWithComplexPath() public { + uint256 amt = 1000 * WAD; + newAuction(amt); + uint24 poolAFee = 500; + uint24 poolBFee = 100; + bytes memory data = abi.encode( + address(this), + address(gemJoin), + uint256(0), + abi.encodePacked(weth, poolAFee, usdc, poolBFee, dai), + address(0) + ); + Hevm(hevm).warp(block.timestamp + tail / 2); + ClipperLike(clipper).take({ + id: id, + amt: amt, + max: type(uint256).max, + who: address(callee), + data: data + }); + } + + function test_profit() public { + uint256 minProfit = 10_000 * WAD; + uint256 amt = 50 * WAD; + newAuction(amt); + uint24 poolFee = 3000; + bytes memory data = abi.encode( + address(123), + address(gemJoin), + uint256(minProfit), + abi.encodePacked(weth, poolFee, dai), + address(0) + ); + Hevm(hevm).warp(block.timestamp + tail / 2); + ClipperLike(clipper).take({ + id: id, + amt: amt, + max: type(uint256).max, + who: address(callee), + data: data + }); + assertGe(Token(dai).balanceOf(address(123)), minProfit); + } + + function test_poolFee() public { + uint24 poolFee = 500; + uint256 amt = 50 * WAD; + newAuction(amt); + bytes memory data = abi.encode( + address(this), + address(gemJoin), + uint256(0), + abi.encodePacked(weth, poolFee, dai), + address(0) + ); + Hevm(hevm).warp(block.timestamp + tail / 2); + ClipperLike(clipper).take({ + id: id, + amt: amt, + max: type(uint256).max, + who: address(callee), + data: data + }); + } + + function testFail_badPoolFee() public { + uint24 poolFee = 5000; + uint256 amt = 50 * WAD; + newAuction(amt); + bytes memory data = abi.encode( + address(this), + address(gemJoin), + uint256(0), + abi.encodePacked(weth, poolFee, dai), + address(0) + ); + Hevm(hevm).warp(block.timestamp + tail / 2); + ClipperLike(clipper).take({ + id: id, + amt: amt, + max: type(uint256).max, + who: address(callee), + data: data + }); + } + + function test_maxPrice() public { + uint256 amt = 50 * WAD; + newAuction(amt); + uint24 poolFee = 3000; + bytes memory data = abi.encode( + address(this), + address(gemJoin), + uint256(0), + abi.encodePacked(weth, poolFee, dai), + address(0) + ); + Hevm(hevm).warp(block.timestamp + tail / 2); + address osm = Chainlog(chainlog).getAddress("PIP_ETH"); + Hevm(hevm).store({ + c: osm, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Osm(osm).kiss(address(this)); + uint256 max = uint256(Osm(osm).read()) * 1e9; // WAD * 1e9 = RAY + ClipperLike(clipper).take({ + id: id, + amt: amt, + max: max, + who: address(callee), + data: data + }); + } + + function testFail_maxPrice() public { + uint256 amt = 50 * WAD; + newAuction(amt); + uint24 poolFee = 3000; + bytes memory data = abi.encode( + address(this), + address(gemJoin), + uint256(0), + abi.encodePacked(weth, poolFee, dai), + address(0) + ); + Hevm(hevm).warp(block.timestamp + tail / 5); + address osm = Chainlog(chainlog).getAddress("PIP_ETH"); + Hevm(hevm).store({ + c: osm, + loc: keccak256(abi.encode(address(this), uint256(0))), + val: bytes32(uint256(1)) + }); + Osm(osm).kiss(address(this)); + uint256 max = uint256(Osm(osm).read()) * 1e9; // WAD * 1e9 = RAY + ClipperLike(clipper).take({ + id: id, + amt: amt, + max: max, + who: address(callee), + data: data + }); + } +} diff --git a/test-forge.sh b/test-forge.sh new file mode 100755 index 0000000..42179ff --- /dev/null +++ b/test-forge.sh @@ -0,0 +1,19 @@ +#! /bin/bash + +[[ "$(cast chain --rpc-url="$ETH_RPC_URL")" == "ethlive" ]] || { echo "Please set a mainnet ETH_RPC_URL"; exit 1; } + +LATEST_BLOCK=$(cast block --rpc-url $ETH_RPC_URL latest number) +if test -f block; then + BLOCK=$(cat block) + AGE=$((LATEST_BLOCK - BLOCK)) + echo "using cached block ${BLOCK} (${AGE} blocks ago), delete ./block to refresh" +else + BLOCK=$(($LATEST_BLOCK-6)) + echo "using fresh block ${BLOCK}" +fi +if [[ $# -eq 0 ]] ; then + forge test --fork-url $ETH_RPC_URL --fork-block-number $BLOCK +else + forge test --fork-url $ETH_RPC_URL --fork-block-number $BLOCK --match ${1} -vvv +fi +echo $BLOCK > block