From 8f9505cec5dad1a779324384b304b7845f69022c Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Wed, 22 May 2024 15:59:31 +0200 Subject: [PATCH 01/25] add lockstake as a submodule --- .gitmodules | 3 +++ lib/lockstake | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/lockstake diff --git a/.gitmodules b/.gitmodules index c5dac77..bbea43d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "lib/dss-crop-join"] path = lib/dss-crop-join url = https://github.com/makerdao/dss-crop-join +[submodule "lib/lockstake"] + path = lib/lockstake + url = https://github.com/makerdao/lockstake diff --git a/lib/lockstake b/lib/lockstake new file mode 160000 index 0000000..735e1e8 --- /dev/null +++ b/lib/lockstake @@ -0,0 +1 @@ +Subproject commit 735e1e85ca706534a77d8e1582df0d3248cbd2b6 From c5a9cf240fac410a2e789d5f1f30060d872fe413 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Thu, 23 May 2024 16:25:12 +0200 Subject: [PATCH 02/25] update dss-interfaces to the latest commit --- lib/dss-interfaces | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dss-interfaces b/lib/dss-interfaces index 18a5d75..eb63423 160000 --- a/lib/dss-interfaces +++ b/lib/dss-interfaces @@ -1 +1 @@ -Subproject commit 18a5d75e8cc10cad0a578fdc64bbb8a027afa678 +Subproject commit eb63423fb1f777a8d2887919504b3dd0102f1aa4 From 1332caae98c528e2fcd6894eb71e9e7483db4cf5 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Thu, 23 May 2024 16:25:45 +0200 Subject: [PATCH 03/25] update tests to avoid compilation errors --- src/test/OneInchCallee.t.sol | 4 ++-- src/test/UniswapV3SplitRouteCallee.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/OneInchCallee.t.sol b/src/test/OneInchCallee.t.sol index 9ba9766..44b86b3 100644 --- a/src/test/OneInchCallee.t.sol +++ b/src/test/OneInchCallee.t.sol @@ -225,10 +225,10 @@ contract OneInchTests is DSTest { joinLink(amountLink); frobMax(amountLink, linkName); try vat.frob(linkName, aliAddr, aliAddr, aliAddr, 0, 1) { - log('not at maximum frob'); + emit log('not at maximum frob'); fail(); } catch { - log('success'); + emit log('success'); } } diff --git a/src/test/UniswapV3SplitRouteCallee.t.sol b/src/test/UniswapV3SplitRouteCallee.t.sol index b7737d9..d6f8b33 100644 --- a/src/test/UniswapV3SplitRouteCallee.t.sol +++ b/src/test/UniswapV3SplitRouteCallee.t.sol @@ -219,10 +219,10 @@ contract UniswapSplitTests is DSTest { joinLink(amountLink); frobMax(amountLink, linkName); try vat.frob(linkName, aliAddr, aliAddr, aliAddr, 0, 1) { - log('not at maximum frob'); + emit log('not at maximum frob'); fail(); } catch { - log('success'); + emit log('success'); } } From fb622173a96eb724e9a7e907e8b831ec761da1a5 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Thu, 23 May 2024 16:30:38 +0200 Subject: [PATCH 04/25] add base LockstakeClipperTest --- src/test/UniswapV2CalleeLse.t.sol | 205 ++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/test/UniswapV2CalleeLse.t.sol diff --git a/src/test/UniswapV2CalleeLse.t.sol b/src/test/UniswapV2CalleeLse.t.sol new file mode 100644 index 0000000..725a19f --- /dev/null +++ b/src/test/UniswapV2CalleeLse.t.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +import "lib/lockstake/lib/token-tests/lib/dss-test/src/DssTest.sol"; + +import { LockstakeClipper } from "lib/lockstake/src/LockstakeClipper.sol"; +import { LockstakeEngineMock } from "lib/lockstake/test/mocks/LockstakeEngineMock.sol"; +import { PipMock } from "lib/lockstake/test/mocks/PipMock.sol"; + +interface GemLike { + function balanceOf(address) external view returns (uint256); +} + +interface CalcFabLike { + function newLinearDecrease(address) external returns (address); + function newStairstepExponentialDecrease(address) external returns (address); +} + +interface CalcLike { + function file(bytes32, uint256) external; +} + +contract UniswapV2CalleeLseTest is DssTest { + using stdStorage for StdStorage; + + DssInstance dss; + address pauseProxy; + PipMock pip; + GemLike dai; + + LockstakeEngineMock engine; + LockstakeClipper clip; + + // Exchange exchange; + + address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + + address ali; + address bob; + address che; + + bytes32 constant ilk = "LSE"; + uint256 constant price = 5 ether; + + uint256 constant startTime = 604411200; // Used to avoid issues with `block.timestamp` + + function _ink(bytes32 ilk_, address urn_) internal view returns (uint256) { + (uint256 ink_,) = dss.vat.urns(ilk_, urn_); + return ink_; + } + function _art(bytes32 ilk_, address urn_) internal view returns (uint256) { + (,uint256 art_) = dss.vat.urns(ilk_, urn_); + return art_; + } + + function ray(uint256 wad) internal pure returns (uint256) { + return wad * 10 ** 9; + } + + function rad(uint256 wad) internal pure returns (uint256) { + return wad * 10 ** 27; + } + + // Copied from https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeClipper.t.sol#L211-L249 + modifier takeSetup { + address calc = CalcFabLike(dss.chainlog.getAddress("CALC_FAB")).newStairstepExponentialDecrease(address(this)); + CalcLike(calc).file("cut", RAY - ray(0.01 ether)); // 1% decrease + CalcLike(calc).file("step", 1); // Decrease every 1 second + + clip.file("buf", ray(1.25 ether)); // 25% Initial price buffer + clip.file("calc", address(calc)); // File price contract + clip.file("cusp", ray(0.3 ether)); // 70% drop before reset + clip.file("tail", 3600); // 1 hour before reset + + (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this)); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + assertEq(clip.kicks(), 0); + dss.dog.bark(ilk, address(this), address(this)); + assertEq(clip.kicks(), 1); + + (ink, art) = dss.vat.urns(ilk, address(this)); + assertEq(ink, 0); + assertEq(art, 0); + + LockstakeClipper.Sale memory sale; + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1); + assertEq(sale.pos, 0); + assertEq(sale.tab, rad(110 ether)); + assertEq(sale.lot, 40 ether); + assertEq(sale.tot, 40 ether); + assertEq(sale.usr, address(this)); + assertEq(sale.tic, block.timestamp); + assertEq(sale.top, ray(5 ether)); // $4 plus 25% + + assertEq(dss.vat.gem(ilk, ali), 0); + assertEq(dss.vat.dai(ali), rad(1000 ether)); + assertEq(dss.vat.gem(ilk, bob), 0); + assertEq(dss.vat.dai(bob), rad(1000 ether)); + + _; + } + + // Copied from https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeClipper.t.sol#L251-L317 + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + vm.warp(startTime); + + dss = MCD.loadFromChainlog(LOG); + + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + dai = GemLike(dss.chainlog.getAddress("MCD_DAI")); + + pip = new PipMock(); + pip.setPrice(price); // Spot = $2.5 + + vm.startPrank(pauseProxy); + dss.vat.init(ilk); + + dss.spotter.file(ilk, "pip", address(pip)); + dss.spotter.file(ilk, "mat", ray(2 ether)); // 200% liquidation ratio for easier test calcs + dss.spotter.poke(ilk); + + dss.vat.file(ilk, "dust", rad(20 ether)); // $20 dust + dss.vat.file(ilk, "line", rad(10000 ether)); + dss.vat.file("Line", dss.vat.Line() + rad(10000 ether)); + + dss.dog.file(ilk, "chop", 1.1 ether); // 10% chop + dss.dog.file(ilk, "hole", rad(1000 ether)); + dss.dog.file("Hole", dss.dog.Dirt() + rad(1000 ether)); + + engine = new LockstakeEngineMock(address(dss.vat), ilk); + dss.vat.rely(address(engine)); + vm.stopPrank(); + + // dust and chop filed previously so clip.chost will be set correctly + clip = new LockstakeClipper(address(dss.vat), address(dss.spotter), address(dss.dog), address(engine)); + clip.upchost(); + clip.rely(address(dss.dog)); + + vm.startPrank(pauseProxy); + dss.dog.file(ilk, "clip", address(clip)); + dss.dog.rely(address(clip)); + dss.vat.rely(address(clip)); + + dss.vat.slip(ilk, address(this), int256(1000 ether)); + vm.stopPrank(); + + assertEq(dss.vat.gem(ilk, address(this)), 1000 ether); + assertEq(dss.vat.dai(address(this)), 0); + dss.vat.frob(ilk, address(this), address(this), address(this), 40 ether, 100 ether); + assertEq(dss.vat.gem(ilk, address(this)), 960 ether); + assertEq(dss.vat.dai(address(this)), rad(100 ether)); + + pip.setPrice(4 ether); // Spot = $2 + dss.spotter.poke(ilk); // Now unsafe + + ali = address(111); + bob = address(222); + che = address(333); + + dss.vat.hope(address(clip)); + vm.prank(ali); dss.vat.hope(address(clip)); + vm.prank(bob); dss.vat.hope(address(clip)); + + vm.startPrank(pauseProxy); + dss.vat.suck(address(0), address(this), rad(1000 ether)); + dss.vat.suck(address(0), address(ali), rad(1000 ether)); + dss.vat.suck(address(0), address(bob), rad(1000 ether)); + vm.stopPrank(); + } + + // Copied from https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeClipper.t.sol#L828-L856 + function testTakeAtTab() public takeSetup { + // Bid so owe (= 22 * 5 = 110 RAD) == tab (= 110 RAD) + vm.prank(ali); clip.take({ + id: 1, + amt: 22 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + + assertEq(dss.vat.gem(ilk, ali), 22 ether); // Didn't take whole lot + assertEq(dss.vat.dai(ali), rad(890 ether)); // Paid full tab (110) + assertEq(dss.vat.gem(ilk, address(this)), 978 ether); // 960 + (40 - 22) returned to usr + + // Assert auction ends + LockstakeClipper.Sale memory sale; + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1); + assertEq(sale.pos, 0); + assertEq(sale.tab, 0); + assertEq(sale.lot, 0); + assertEq(sale.tot, 0); + assertEq(sale.usr, address(0)); + assertEq(sale.tic, 0); + assertEq(sale.top, 0); + + assertEq(dss.dog.Dirt(), 0); + (,,, uint256 dirt) = dss.dog.ilks(ilk); + assertEq(dirt, 0); + } +} From f4132899c56e168e543dc1f017d63c85b296fa76 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Thu, 23 May 2024 17:44:03 +0200 Subject: [PATCH 05/25] rename to UniswapV2LockstakeCallee --- ...{UniswapV2CalleeLse.t.sol => UniswapV2LockstakeCallee.t.sol} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/{UniswapV2CalleeLse.t.sol => UniswapV2LockstakeCallee.t.sol} (99%) diff --git a/src/test/UniswapV2CalleeLse.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol similarity index 99% rename from src/test/UniswapV2CalleeLse.t.sol rename to src/test/UniswapV2LockstakeCallee.t.sol index 725a19f..f834727 100644 --- a/src/test/UniswapV2CalleeLse.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -21,7 +21,7 @@ interface CalcLike { function file(bytes32, uint256) external; } -contract UniswapV2CalleeLseTest is DssTest { +contract UniswapV2LockstakeCalleeTest is DssTest { using stdStorage for StdStorage; DssInstance dss; From 4e69fe0ecfff2a151fa008b39ff24f430e16d1fa Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Mon, 27 May 2024 14:30:39 +0200 Subject: [PATCH 06/25] remove hardcoded version in the test.sh --- scripts/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test.sh b/scripts/test.sh index 940d462..7323717 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash if [[ $# -eq 0 ]] ; then - forge test --use solc:0.6.12 --fork-url "$ETH_RPC_URL" + forge test --fork-url "$ETH_RPC_URL" else - forge test --use solc:0.6.12 --fork-url "$ETH_RPC_URL" -vvv --match-test ${1} + forge test --fork-url "$ETH_RPC_URL" -vvv --match-test ${1} fi From 8f3efe1a9aed15c96b57b51b031933ecba6574c4 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Mon, 27 May 2024 14:56:37 +0200 Subject: [PATCH 07/25] add LockstakeEngineTest --- src/test/UniswapV2LockstakeCallee2.t.sol | 345 +++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 src/test/UniswapV2LockstakeCallee2.t.sol diff --git a/src/test/UniswapV2LockstakeCallee2.t.sol b/src/test/UniswapV2LockstakeCallee2.t.sol new file mode 100644 index 0000000..73dc241 --- /dev/null +++ b/src/test/UniswapV2LockstakeCallee2.t.sol @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +import "lib/lockstake/lib/token-tests/lib/dss-test/src/DssTest.sol"; +import "dss-interfaces/Interfaces.sol"; + +import { LockstakeDeploy } from "lib/lockstake/deploy/LockstakeDeploy.sol"; +import { LockstakeInit, LockstakeConfig, LockstakeInstance } from "lib/lockstake/deploy/LockstakeInit.sol"; +import { LockstakeMkr } from "lib/lockstake/src/LockstakeMkr.sol"; +import { LockstakeEngine } from "lib/lockstake/src/LockstakeEngine.sol"; +import { LockstakeClipper } from "lib/lockstake/src/LockstakeClipper.sol"; +import { LockstakeUrn } from "lib/lockstake/src/LockstakeUrn.sol"; +import { VoteDelegateFactoryMock, VoteDelegateMock } from "lib/lockstake/test/mocks/VoteDelegateMock.sol"; +import { GemMock } from "lib/lockstake/test/mocks/GemMock.sol"; +import { NstMock } from "lib/lockstake/test/mocks/NstMock.sol"; +import { NstJoinMock } from "lib/lockstake/test/mocks/NstJoinMock.sol"; +import { StakingRewardsMock } from "lib/lockstake/test/mocks/StakingRewardsMock.sol"; +import { MkrNgtMock } from "lib/lockstake/test/mocks/MkrNgtMock.sol"; + +interface CalcFabLike { + function newLinearDecrease(address) external returns (address); +} + +interface LineMomLike { + function ilks(bytes32) external view returns (uint256); +} + +interface MkrAuthorityLike { + function rely(address) external; +} + +contract LockstakeEngineTest is DssTest { + using stdStorage for StdStorage; + + DssInstance dss; + address pauseProxy; + DSTokenAbstract mkr; + LockstakeMkr lsmkr; + LockstakeEngine engine; + LockstakeClipper clip; + address calc; + MedianAbstract pip; + VoteDelegateFactoryMock voteDelegateFactory; + NstMock nst; + NstJoinMock nstJoin; + GemMock rTok; + StakingRewardsMock farm; + StakingRewardsMock farm2; + MkrNgtMock mkrNgt; + GemMock ngt; + bytes32 ilk = "LSE"; + address voter; + address voteDelegate; + + LockstakeConfig cfg; + + uint256 prevLine; + + address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + + event AddFarm(address farm); + event DelFarm(address farm); + event Open(address indexed owner, uint256 indexed index, address urn); + event Hope(address indexed urn, address indexed usr); + event Nope(address indexed urn, address indexed usr); + event SelectVoteDelegate(address indexed urn, address indexed voteDelegate_); + event SelectFarm(address indexed urn, address farm, uint16 ref); + event Lock(address indexed urn, uint256 wad, uint16 ref); + event LockNgt(address indexed urn, uint256 ngtWad, uint16 ref); + event Free(address indexed urn, address indexed to, uint256 wad, uint256 freed); + event FreeNgt(address indexed urn, address indexed to, uint256 ngtWad, uint256 ngtFreed); + event FreeNoFee(address indexed urn, address indexed to, uint256 wad); + event Draw(address indexed urn, address indexed to, uint256 wad); + event Wipe(address indexed urn, uint256 wad); + event GetReward(address indexed urn, address indexed farm, address indexed to, uint256 amt); + event OnKick(address indexed urn, uint256 wad); + event OnTake(address indexed urn, address indexed who, uint256 wad); + event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); + + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L87-L177 + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + + dss = MCD.loadFromChainlog(LOG); + + pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); + pip = MedianAbstract(dss.chainlog.getAddress("PIP_MKR")); + mkr = DSTokenAbstract(dss.chainlog.getAddress("MCD_GOV")); + nst = new NstMock(); + nstJoin = new NstJoinMock(address(dss.vat), address(nst)); + rTok = new GemMock(0); + ngt = new GemMock(0); + mkrNgt = new MkrNgtMock(address(mkr), address(ngt), 24_000); + vm.startPrank(pauseProxy); + MkrAuthorityLike(mkr.authority()).rely(address(mkrNgt)); + vm.stopPrank(); + + voteDelegateFactory = new VoteDelegateFactoryMock(address(mkr)); + voter = address(123); + vm.prank(voter); voteDelegate = voteDelegateFactory.create(); + + vm.prank(pauseProxy); pip.kiss(address(this)); + vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(1_500 * 10**18))); + + LockstakeInstance memory instance = LockstakeDeploy.deployLockstake( + address(this), + pauseProxy, + address(voteDelegateFactory), + address(nstJoin), + ilk, + 15 * WAD / 100, + address(mkrNgt), + bytes4(abi.encodeWithSignature("newLinearDecrease(address)")) + ); + + engine = LockstakeEngine(instance.engine); + clip = LockstakeClipper(instance.clipper); + calc = instance.clipperCalc; + lsmkr = LockstakeMkr(instance.lsmkr); + farm = new StakingRewardsMock(address(rTok), address(lsmkr)); + farm2 = new StakingRewardsMock(address(rTok), address(lsmkr)); + + address[] memory farms = new address[](2); + farms[0] = address(farm); + farms[1] = address(farm2); + + cfg = LockstakeConfig({ + ilk: ilk, + voteDelegateFactory: address(voteDelegateFactory), + nstJoin: address(nstJoin), + nst: address(nstJoin.nst()), + mkr: address(mkr), + mkrNgt: address(mkrNgt), + ngt: address(ngt), + farms: farms, + fee: 15 * WAD / 100, + maxLine: 10_000_000 * 10**45, + gap: 1_000_000 * 10**45, + ttl: 1 days, + dust: 50, + duty: 100000001 * 10**27 / 100000000, + mat: 3 * 10**27, + buf: 1.25 * 10**27, // 25% Initial price buffer + tail: 3600, // 1 hour before reset + cusp: 0.2 * 10**27, // 80% drop before reset + chip: 2 * WAD / 100, + tip: 3, + stopped: 0, + chop: 1 ether, + hole: 10_000 * 10**45, + tau: 100, + cut: 0, + step: 0, + lineMom: true, + tolerance: 0.5 * 10**27, + name: "LOCKSTAKE", + symbol: "LMKR" + }); + + prevLine = dss.vat.Line(); + + vm.startPrank(pauseProxy); + LockstakeInit.initLockstake(dss, instance, cfg); + vm.stopPrank(); + + deal(address(mkr), address(this), 100_000 * 10**18, true); + deal(address(ngt), address(this), 100_000 * 24_000 * 10**18, true); + + // Add some existing DAI assigned to nstJoin to avoid a particular error + stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(nstJoin)).depth(0).checked_write(100_000 * RAD); + } + + function _ink(bytes32 ilk_, address urn) internal view returns (uint256 ink) { + (ink,) = dss.vat.urns(ilk_, urn); + } + + function _art(bytes32 ilk_, address urn) internal view returns (uint256 art) { + (, art) = dss.vat.urns(ilk_, urn); + } + + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L962-L991 + function _urnSetUp(bool withDelegate, bool withStaking) internal returns (address urn) { + urn = engine.open(0); + if (withDelegate) { + engine.selectVoteDelegate(urn, voteDelegate); + } + if (withStaking) { + engine.selectFarm(urn, address(farm), 0); + } + mkr.approve(address(engine), 100_000 * 10**18); + engine.lock(urn, 100_000 * 10**18, 5); + engine.draw(urn, address(this), 2_000 * 10**18); + assertEq(_ink(ilk, urn), 100_000 * 10**18); + assertEq(_art(ilk, urn), 2_000 * 10**18); + + if (withDelegate) { + assertEq(engine.urnVoteDelegates(urn), voteDelegate); + assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18); + assertEq(mkr.balanceOf(address(engine)), 0); + } else { + assertEq(engine.urnVoteDelegates(urn), address(0)); + assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18); + } + if (withStaking) { + assertEq(lsmkr.balanceOf(address(urn)), 0); + assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18); + assertEq(farm.balanceOf(address(urn)), 100_000 * 10**18); + } else { + assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18); + } + } + + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L993-L1005 + function _forceLiquidation(address urn) internal returns (uint256 id) { + vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(0.05 * 10**18))); // Force liquidation + dss.spotter.poke(ilk); + assertEq(clip.kicks(), 0); + assertEq(engine.urnAuctions(urn), 0); + (,, uint256 hole,) = dss.dog.ilks(ilk); + uint256 kicked = hole < 2_000 * 10**45 ? 100_000 * 10**18 * hole / (2_000 * 10**45) : 100_000 * 10**18; + vm.expectEmit(true, true, true, true); + emit OnKick(urn, kicked); + id = dss.dog.bark(ilk, address(urn), address(this)); + assertEq(clip.kicks(), 1); + assertEq(engine.urnAuctions(urn), 1); + } + + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 + function _testOnTake(bool withDelegate, bool withStaking) internal { + address urn = _urnSetUp(withDelegate, withStaking); + uint256 mkrInitialSupply = mkr.totalSupply(); + uint256 lsmkrInitialSupply = lsmkr.totalSupply(); + address vow = address(dss.vow); + uint256 vowInitialBalance = dss.vat.dai(vow); + uint256 id = _forceLiquidation(urn); + + LockstakeClipper.Sale memory sale; + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); + assertEq(sale.pos, 0); + assertEq(sale.tab, 2_000 * 10**45); + assertEq(sale.lot, 100_000 * 10**18); + assertEq(sale.tot, 100_000 * 10**18); + assertEq(sale.usr, address(urn)); + assertEq(sale.tic, block.timestamp); + assertEq(sale.top, pip.read() * (1.25 * 10**9)); + + assertEq(_ink(ilk, urn), 0); + assertEq(_art(ilk, urn), 0); + assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18); + + if (withDelegate) { + assertEq(mkr.balanceOf(voteDelegate), 0); + } + assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18); + if (withStaking) { + assertEq(lsmkr.balanceOf(address(farm)), 0); + assertEq(farm.balanceOf(address(urn)), 0); + } + assertEq(lsmkr.balanceOf(address(urn)), 0); + assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); + + address buyer = address(888); + vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 2_000 * 10**45); + vm.prank(buyer); dss.vat.hope(address(clip)); + assertEq(mkr.balanceOf(buyer), 0); + vm.expectEmit(true, true, true, true); + emit OnTake(urn, buyer, 20_000 * 10**18); + vm.prank(buyer); clip.take(id, 20_000 * 10**18, type(uint256).max, buyer, ""); + assertEq(mkr.balanceOf(buyer), 20_000 * 10**18); + + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); + assertEq(sale.pos, 0); + assertEq(sale.tab, (2_000 - 20_000 * 0.05 * 1.25) * 10**45); + assertEq(sale.lot, 80_000 * 10**18); + assertEq(sale.tot, 100_000 * 10**18); + assertEq(sale.usr, address(urn)); + assertEq(sale.tic, block.timestamp); + assertEq(sale.top, pip.read() * (1.25 * 10**9)); + + assertEq(_ink(ilk, urn), 0); + assertEq(_art(ilk, urn), 0); + assertEq(dss.vat.gem(ilk, address(clip)), 80_000 * 10**18); + + if (withDelegate) { + assertEq(mkr.balanceOf(voteDelegate), 0); + } + assertEq(mkr.balanceOf(address(engine)), 80_000 * 10**18); + if (withStaking) { + assertEq(lsmkr.balanceOf(address(farm)), 0); + assertEq(farm.balanceOf(address(urn)), 0); + } + assertEq(lsmkr.balanceOf(address(urn)), 0); + assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); + + uint256 burn = 32_000 * 10**18 * engine.fee() / (WAD - engine.fee()); + vm.expectEmit(true, true, true, true); + emit OnTake(urn, buyer, 12_000 * 10**18); + vm.expectEmit(true, true, true, true); + emit OnRemove(urn, 32_000 * 10**18, burn, 100_000 * 10**18 - 32_000 * 10**18 - burn); + vm.prank(buyer); clip.take(id, 12_000 * 10**18, type(uint256).max, buyer, ""); + assertEq(burn, (32_000 * 10**18 + burn) * engine.fee() / WAD); + assertEq(mkr.balanceOf(buyer), 32_000 * 10**18); + assertEq(engine.urnAuctions(urn), 0); + + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); + assertEq(sale.pos, 0); + assertEq(sale.tab, 0); + assertEq(sale.lot, 0); + assertEq(sale.tot, 0); + assertEq(sale.usr, address(0)); + assertEq(sale.tic, 0); + assertEq(sale.top, 0); + + assertEq(_ink(ilk, urn), 100_000 * 10**18 - 32_000 * 10**18 - burn); + assertEq(_art(ilk, urn), 0); + assertEq(dss.vat.gem(ilk, address(clip)), 0); + + assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18 - 32_000 * 10**18 - burn); + assertEq(mkr.totalSupply(), mkrInitialSupply - burn); + if (withStaking) { + assertEq(lsmkr.balanceOf(address(farm)), 0); + assertEq(farm.balanceOf(address(urn)), 0); + } + assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18 - 32_000 * 10**18 - burn); + assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 32_000 * 10**18 - burn); + assertEq(dss.vat.dai(vow), vowInitialBalance + 2_000 * 10**45); + } + + function testOnTakeNoWithStakingNoDelegate() public { + _testOnTake(false, false); + } + + function testOnTakeNoWithStakingWithDelegate() public { + _testOnTake(true, false); + } + + function testOnTakeWithStakingNoDelegate() public { + _testOnTake(false, true); + } + + function testOnTakeWithStakingWithDelegate() public { + _testOnTake(true, true); + } +} From 4f098c36bc2ac6d00e076431b3a9f663cb6b049d Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Mon, 27 May 2024 15:01:02 +0200 Subject: [PATCH 08/25] rename test and the contract --- src/test/UniswapV2LockstakeCallee.t.sol | 448 +++++++++++++++-------- src/test/UniswapV2LockstakeCallee2.t.sol | 345 ----------------- 2 files changed, 294 insertions(+), 499 deletions(-) delete mode 100644 src/test/UniswapV2LockstakeCallee2.t.sol diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index f834727..24e1ab0 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -3,193 +3,307 @@ pragma solidity ^0.8.21; import "lib/lockstake/lib/token-tests/lib/dss-test/src/DssTest.sol"; +import "dss-interfaces/Interfaces.sol"; +import { LockstakeDeploy } from "lib/lockstake/deploy/LockstakeDeploy.sol"; +import { LockstakeInit, LockstakeConfig, LockstakeInstance } from "lib/lockstake/deploy/LockstakeInit.sol"; +import { LockstakeMkr } from "lib/lockstake/src/LockstakeMkr.sol"; +import { LockstakeEngine } from "lib/lockstake/src/LockstakeEngine.sol"; import { LockstakeClipper } from "lib/lockstake/src/LockstakeClipper.sol"; -import { LockstakeEngineMock } from "lib/lockstake/test/mocks/LockstakeEngineMock.sol"; -import { PipMock } from "lib/lockstake/test/mocks/PipMock.sol"; - -interface GemLike { - function balanceOf(address) external view returns (uint256); -} +import { LockstakeUrn } from "lib/lockstake/src/LockstakeUrn.sol"; +import { VoteDelegateFactoryMock, VoteDelegateMock } from "lib/lockstake/test/mocks/VoteDelegateMock.sol"; +import { GemMock } from "lib/lockstake/test/mocks/GemMock.sol"; +import { NstMock } from "lib/lockstake/test/mocks/NstMock.sol"; +import { NstJoinMock } from "lib/lockstake/test/mocks/NstJoinMock.sol"; +import { StakingRewardsMock } from "lib/lockstake/test/mocks/StakingRewardsMock.sol"; +import { MkrNgtMock } from "lib/lockstake/test/mocks/MkrNgtMock.sol"; interface CalcFabLike { function newLinearDecrease(address) external returns (address); - function newStairstepExponentialDecrease(address) external returns (address); } -interface CalcLike { - function file(bytes32, uint256) external; +interface LineMomLike { + function ilks(bytes32) external view returns (uint256); +} + +interface MkrAuthorityLike { + function rely(address) external; } contract UniswapV2LockstakeCalleeTest is DssTest { using stdStorage for StdStorage; - DssInstance dss; - address pauseProxy; - PipMock pip; - GemLike dai; - - LockstakeEngineMock engine; - LockstakeClipper clip; - - // Exchange exchange; - + DssInstance dss; + address pauseProxy; + DSTokenAbstract mkr; + LockstakeMkr lsmkr; + LockstakeEngine engine; + LockstakeClipper clip; + address calc; + MedianAbstract pip; + VoteDelegateFactoryMock voteDelegateFactory; + NstMock nst; + NstJoinMock nstJoin; + GemMock rTok; + StakingRewardsMock farm; + StakingRewardsMock farm2; + MkrNgtMock mkrNgt; + GemMock ngt; + bytes32 ilk = "LSE"; + address voter; + address voteDelegate; + + LockstakeConfig cfg; + + uint256 prevLine; + address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; - address ali; - address bob; - address che; - - bytes32 constant ilk = "LSE"; - uint256 constant price = 5 ether; - - uint256 constant startTime = 604411200; // Used to avoid issues with `block.timestamp` - - function _ink(bytes32 ilk_, address urn_) internal view returns (uint256) { - (uint256 ink_,) = dss.vat.urns(ilk_, urn_); - return ink_; - } - function _art(bytes32 ilk_, address urn_) internal view returns (uint256) { - (,uint256 art_) = dss.vat.urns(ilk_, urn_); - return art_; - } - - function ray(uint256 wad) internal pure returns (uint256) { - return wad * 10 ** 9; - } - - function rad(uint256 wad) internal pure returns (uint256) { - return wad * 10 ** 27; - } - - // Copied from https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeClipper.t.sol#L211-L249 - modifier takeSetup { - address calc = CalcFabLike(dss.chainlog.getAddress("CALC_FAB")).newStairstepExponentialDecrease(address(this)); - CalcLike(calc).file("cut", RAY - ray(0.01 ether)); // 1% decrease - CalcLike(calc).file("step", 1); // Decrease every 1 second - - clip.file("buf", ray(1.25 ether)); // 25% Initial price buffer - clip.file("calc", address(calc)); // File price contract - clip.file("cusp", ray(0.3 ether)); // 70% drop before reset - clip.file("tail", 3600); // 1 hour before reset - - (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this)); - assertEq(ink, 40 ether); - assertEq(art, 100 ether); - - assertEq(clip.kicks(), 0); - dss.dog.bark(ilk, address(this), address(this)); - assertEq(clip.kicks(), 1); - - (ink, art) = dss.vat.urns(ilk, address(this)); - assertEq(ink, 0); - assertEq(art, 0); - - LockstakeClipper.Sale memory sale; - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1); - assertEq(sale.pos, 0); - assertEq(sale.tab, rad(110 ether)); - assertEq(sale.lot, 40 ether); - assertEq(sale.tot, 40 ether); - assertEq(sale.usr, address(this)); - assertEq(sale.tic, block.timestamp); - assertEq(sale.top, ray(5 ether)); // $4 plus 25% - - assertEq(dss.vat.gem(ilk, ali), 0); - assertEq(dss.vat.dai(ali), rad(1000 ether)); - assertEq(dss.vat.gem(ilk, bob), 0); - assertEq(dss.vat.dai(bob), rad(1000 ether)); - - _; - } - - // Copied from https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeClipper.t.sol#L251-L317 + event AddFarm(address farm); + event DelFarm(address farm); + event Open(address indexed owner, uint256 indexed index, address urn); + event Hope(address indexed urn, address indexed usr); + event Nope(address indexed urn, address indexed usr); + event SelectVoteDelegate(address indexed urn, address indexed voteDelegate_); + event SelectFarm(address indexed urn, address farm, uint16 ref); + event Lock(address indexed urn, uint256 wad, uint16 ref); + event LockNgt(address indexed urn, uint256 ngtWad, uint16 ref); + event Free(address indexed urn, address indexed to, uint256 wad, uint256 freed); + event FreeNgt(address indexed urn, address indexed to, uint256 ngtWad, uint256 ngtFreed); + event FreeNoFee(address indexed urn, address indexed to, uint256 wad); + event Draw(address indexed urn, address indexed to, uint256 wad); + event Wipe(address indexed urn, uint256 wad); + event GetReward(address indexed urn, address indexed farm, address indexed to, uint256 amt); + event OnKick(address indexed urn, uint256 wad); + event OnTake(address indexed urn, address indexed who, uint256 wad); + event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); + + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L87-L177 function setUp() public { vm.createSelectFork(vm.envString("ETH_RPC_URL")); - vm.warp(startTime); dss = MCD.loadFromChainlog(LOG); pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); - dai = GemLike(dss.chainlog.getAddress("MCD_DAI")); - - pip = new PipMock(); - pip.setPrice(price); // Spot = $2.5 - + pip = MedianAbstract(dss.chainlog.getAddress("PIP_MKR")); + mkr = DSTokenAbstract(dss.chainlog.getAddress("MCD_GOV")); + nst = new NstMock(); + nstJoin = new NstJoinMock(address(dss.vat), address(nst)); + rTok = new GemMock(0); + ngt = new GemMock(0); + mkrNgt = new MkrNgtMock(address(mkr), address(ngt), 24_000); vm.startPrank(pauseProxy); - dss.vat.init(ilk); - - dss.spotter.file(ilk, "pip", address(pip)); - dss.spotter.file(ilk, "mat", ray(2 ether)); // 200% liquidation ratio for easier test calcs - dss.spotter.poke(ilk); - - dss.vat.file(ilk, "dust", rad(20 ether)); // $20 dust - dss.vat.file(ilk, "line", rad(10000 ether)); - dss.vat.file("Line", dss.vat.Line() + rad(10000 ether)); - - dss.dog.file(ilk, "chop", 1.1 ether); // 10% chop - dss.dog.file(ilk, "hole", rad(1000 ether)); - dss.dog.file("Hole", dss.dog.Dirt() + rad(1000 ether)); - - engine = new LockstakeEngineMock(address(dss.vat), ilk); - dss.vat.rely(address(engine)); + MkrAuthorityLike(mkr.authority()).rely(address(mkrNgt)); vm.stopPrank(); - // dust and chop filed previously so clip.chost will be set correctly - clip = new LockstakeClipper(address(dss.vat), address(dss.spotter), address(dss.dog), address(engine)); - clip.upchost(); - clip.rely(address(dss.dog)); + voteDelegateFactory = new VoteDelegateFactoryMock(address(mkr)); + voter = address(123); + vm.prank(voter); voteDelegate = voteDelegateFactory.create(); + + vm.prank(pauseProxy); pip.kiss(address(this)); + vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(1_500 * 10**18))); + + LockstakeInstance memory instance = LockstakeDeploy.deployLockstake( + address(this), + pauseProxy, + address(voteDelegateFactory), + address(nstJoin), + ilk, + 15 * WAD / 100, + address(mkrNgt), + bytes4(abi.encodeWithSignature("newLinearDecrease(address)")) + ); + + engine = LockstakeEngine(instance.engine); + clip = LockstakeClipper(instance.clipper); + calc = instance.clipperCalc; + lsmkr = LockstakeMkr(instance.lsmkr); + farm = new StakingRewardsMock(address(rTok), address(lsmkr)); + farm2 = new StakingRewardsMock(address(rTok), address(lsmkr)); + + address[] memory farms = new address[](2); + farms[0] = address(farm); + farms[1] = address(farm2); + + cfg = LockstakeConfig({ + ilk: ilk, + voteDelegateFactory: address(voteDelegateFactory), + nstJoin: address(nstJoin), + nst: address(nstJoin.nst()), + mkr: address(mkr), + mkrNgt: address(mkrNgt), + ngt: address(ngt), + farms: farms, + fee: 15 * WAD / 100, + maxLine: 10_000_000 * 10**45, + gap: 1_000_000 * 10**45, + ttl: 1 days, + dust: 50, + duty: 100000001 * 10**27 / 100000000, + mat: 3 * 10**27, + buf: 1.25 * 10**27, // 25% Initial price buffer + tail: 3600, // 1 hour before reset + cusp: 0.2 * 10**27, // 80% drop before reset + chip: 2 * WAD / 100, + tip: 3, + stopped: 0, + chop: 1 ether, + hole: 10_000 * 10**45, + tau: 100, + cut: 0, + step: 0, + lineMom: true, + tolerance: 0.5 * 10**27, + name: "LOCKSTAKE", + symbol: "LMKR" + }); - vm.startPrank(pauseProxy); - dss.dog.file(ilk, "clip", address(clip)); - dss.dog.rely(address(clip)); - dss.vat.rely(address(clip)); + prevLine = dss.vat.Line(); - dss.vat.slip(ilk, address(this), int256(1000 ether)); + vm.startPrank(pauseProxy); + LockstakeInit.initLockstake(dss, instance, cfg); vm.stopPrank(); - assertEq(dss.vat.gem(ilk, address(this)), 1000 ether); - assertEq(dss.vat.dai(address(this)), 0); - dss.vat.frob(ilk, address(this), address(this), address(this), 40 ether, 100 ether); - assertEq(dss.vat.gem(ilk, address(this)), 960 ether); - assertEq(dss.vat.dai(address(this)), rad(100 ether)); + deal(address(mkr), address(this), 100_000 * 10**18, true); + deal(address(ngt), address(this), 100_000 * 24_000 * 10**18, true); - pip.setPrice(4 ether); // Spot = $2 - dss.spotter.poke(ilk); // Now unsafe + // Add some existing DAI assigned to nstJoin to avoid a particular error + stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(nstJoin)).depth(0).checked_write(100_000 * RAD); + } - ali = address(111); - bob = address(222); - che = address(333); + function _ink(bytes32 ilk_, address urn) internal view returns (uint256 ink) { + (ink,) = dss.vat.urns(ilk_, urn); + } - dss.vat.hope(address(clip)); - vm.prank(ali); dss.vat.hope(address(clip)); - vm.prank(bob); dss.vat.hope(address(clip)); + function _art(bytes32 ilk_, address urn) internal view returns (uint256 art) { + (, art) = dss.vat.urns(ilk_, urn); + } - vm.startPrank(pauseProxy); - dss.vat.suck(address(0), address(this), rad(1000 ether)); - dss.vat.suck(address(0), address(ali), rad(1000 ether)); - dss.vat.suck(address(0), address(bob), rad(1000 ether)); - vm.stopPrank(); + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L962-L991 + function _urnSetUp(bool withDelegate, bool withStaking) internal returns (address urn) { + urn = engine.open(0); + if (withDelegate) { + engine.selectVoteDelegate(urn, voteDelegate); + } + if (withStaking) { + engine.selectFarm(urn, address(farm), 0); + } + mkr.approve(address(engine), 100_000 * 10**18); + engine.lock(urn, 100_000 * 10**18, 5); + engine.draw(urn, address(this), 2_000 * 10**18); + assertEq(_ink(ilk, urn), 100_000 * 10**18); + assertEq(_art(ilk, urn), 2_000 * 10**18); + + if (withDelegate) { + assertEq(engine.urnVoteDelegates(urn), voteDelegate); + assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18); + assertEq(mkr.balanceOf(address(engine)), 0); + } else { + assertEq(engine.urnVoteDelegates(urn), address(0)); + assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18); + } + if (withStaking) { + assertEq(lsmkr.balanceOf(address(urn)), 0); + assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18); + assertEq(farm.balanceOf(address(urn)), 100_000 * 10**18); + } else { + assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18); + } } - // Copied from https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeClipper.t.sol#L828-L856 - function testTakeAtTab() public takeSetup { - // Bid so owe (= 22 * 5 = 110 RAD) == tab (= 110 RAD) - vm.prank(ali); clip.take({ - id: 1, - amt: 22 ether, - max: ray(5 ether), - who: address(ali), - data: "" - }); + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L993-L1005 + function _forceLiquidation(address urn) internal returns (uint256 id) { + vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(0.05 * 10**18))); // Force liquidation + dss.spotter.poke(ilk); + assertEq(clip.kicks(), 0); + assertEq(engine.urnAuctions(urn), 0); + (,, uint256 hole,) = dss.dog.ilks(ilk); + uint256 kicked = hole < 2_000 * 10**45 ? 100_000 * 10**18 * hole / (2_000 * 10**45) : 100_000 * 10**18; + vm.expectEmit(true, true, true, true); + emit OnKick(urn, kicked); + id = dss.dog.bark(ilk, address(urn), address(this)); + assertEq(clip.kicks(), 1); + assertEq(engine.urnAuctions(urn), 1); + } - assertEq(dss.vat.gem(ilk, ali), 22 ether); // Didn't take whole lot - assertEq(dss.vat.dai(ali), rad(890 ether)); // Paid full tab (110) - assertEq(dss.vat.gem(ilk, address(this)), 978 ether); // 960 + (40 - 22) returned to usr + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 + function _testOnTake(bool withDelegate, bool withStaking) internal { + address urn = _urnSetUp(withDelegate, withStaking); + uint256 mkrInitialSupply = mkr.totalSupply(); + uint256 lsmkrInitialSupply = lsmkr.totalSupply(); + address vow = address(dss.vow); + uint256 vowInitialBalance = dss.vat.dai(vow); + uint256 id = _forceLiquidation(urn); - // Assert auction ends LockstakeClipper.Sale memory sale; - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1); + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); + assertEq(sale.pos, 0); + assertEq(sale.tab, 2_000 * 10**45); + assertEq(sale.lot, 100_000 * 10**18); + assertEq(sale.tot, 100_000 * 10**18); + assertEq(sale.usr, address(urn)); + assertEq(sale.tic, block.timestamp); + assertEq(sale.top, pip.read() * (1.25 * 10**9)); + + assertEq(_ink(ilk, urn), 0); + assertEq(_art(ilk, urn), 0); + assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18); + + if (withDelegate) { + assertEq(mkr.balanceOf(voteDelegate), 0); + } + assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18); + if (withStaking) { + assertEq(lsmkr.balanceOf(address(farm)), 0); + assertEq(farm.balanceOf(address(urn)), 0); + } + assertEq(lsmkr.balanceOf(address(urn)), 0); + assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); + + address buyer = address(888); + vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 2_000 * 10**45); + vm.prank(buyer); dss.vat.hope(address(clip)); + assertEq(mkr.balanceOf(buyer), 0); + vm.expectEmit(true, true, true, true); + emit OnTake(urn, buyer, 20_000 * 10**18); + vm.prank(buyer); clip.take(id, 20_000 * 10**18, type(uint256).max, buyer, ""); + assertEq(mkr.balanceOf(buyer), 20_000 * 10**18); + + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); + assertEq(sale.pos, 0); + assertEq(sale.tab, (2_000 - 20_000 * 0.05 * 1.25) * 10**45); + assertEq(sale.lot, 80_000 * 10**18); + assertEq(sale.tot, 100_000 * 10**18); + assertEq(sale.usr, address(urn)); + assertEq(sale.tic, block.timestamp); + assertEq(sale.top, pip.read() * (1.25 * 10**9)); + + assertEq(_ink(ilk, urn), 0); + assertEq(_art(ilk, urn), 0); + assertEq(dss.vat.gem(ilk, address(clip)), 80_000 * 10**18); + + if (withDelegate) { + assertEq(mkr.balanceOf(voteDelegate), 0); + } + assertEq(mkr.balanceOf(address(engine)), 80_000 * 10**18); + if (withStaking) { + assertEq(lsmkr.balanceOf(address(farm)), 0); + assertEq(farm.balanceOf(address(urn)), 0); + } + assertEq(lsmkr.balanceOf(address(urn)), 0); + assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); + + uint256 burn = 32_000 * 10**18 * engine.fee() / (WAD - engine.fee()); + vm.expectEmit(true, true, true, true); + emit OnTake(urn, buyer, 12_000 * 10**18); + vm.expectEmit(true, true, true, true); + emit OnRemove(urn, 32_000 * 10**18, burn, 100_000 * 10**18 - 32_000 * 10**18 - burn); + vm.prank(buyer); clip.take(id, 12_000 * 10**18, type(uint256).max, buyer, ""); + assertEq(burn, (32_000 * 10**18 + burn) * engine.fee() / WAD); + assertEq(mkr.balanceOf(buyer), 32_000 * 10**18); + assertEq(engine.urnAuctions(urn), 0); + + (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); assertEq(sale.pos, 0); assertEq(sale.tab, 0); assertEq(sale.lot, 0); @@ -198,8 +312,34 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(sale.tic, 0); assertEq(sale.top, 0); - assertEq(dss.dog.Dirt(), 0); - (,,, uint256 dirt) = dss.dog.ilks(ilk); - assertEq(dirt, 0); + assertEq(_ink(ilk, urn), 100_000 * 10**18 - 32_000 * 10**18 - burn); + assertEq(_art(ilk, urn), 0); + assertEq(dss.vat.gem(ilk, address(clip)), 0); + + assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18 - 32_000 * 10**18 - burn); + assertEq(mkr.totalSupply(), mkrInitialSupply - burn); + if (withStaking) { + assertEq(lsmkr.balanceOf(address(farm)), 0); + assertEq(farm.balanceOf(address(urn)), 0); + } + assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18 - 32_000 * 10**18 - burn); + assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 32_000 * 10**18 - burn); + assertEq(dss.vat.dai(vow), vowInitialBalance + 2_000 * 10**45); + } + + function testOnTakeNoWithStakingNoDelegate() public { + _testOnTake(false, false); + } + + function testOnTakeNoWithStakingWithDelegate() public { + _testOnTake(true, false); + } + + function testOnTakeWithStakingNoDelegate() public { + _testOnTake(false, true); + } + + function testOnTakeWithStakingWithDelegate() public { + _testOnTake(true, true); } } diff --git a/src/test/UniswapV2LockstakeCallee2.t.sol b/src/test/UniswapV2LockstakeCallee2.t.sol deleted file mode 100644 index 73dc241..0000000 --- a/src/test/UniswapV2LockstakeCallee2.t.sol +++ /dev/null @@ -1,345 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pragma solidity ^0.8.21; - -import "lib/lockstake/lib/token-tests/lib/dss-test/src/DssTest.sol"; -import "dss-interfaces/Interfaces.sol"; - -import { LockstakeDeploy } from "lib/lockstake/deploy/LockstakeDeploy.sol"; -import { LockstakeInit, LockstakeConfig, LockstakeInstance } from "lib/lockstake/deploy/LockstakeInit.sol"; -import { LockstakeMkr } from "lib/lockstake/src/LockstakeMkr.sol"; -import { LockstakeEngine } from "lib/lockstake/src/LockstakeEngine.sol"; -import { LockstakeClipper } from "lib/lockstake/src/LockstakeClipper.sol"; -import { LockstakeUrn } from "lib/lockstake/src/LockstakeUrn.sol"; -import { VoteDelegateFactoryMock, VoteDelegateMock } from "lib/lockstake/test/mocks/VoteDelegateMock.sol"; -import { GemMock } from "lib/lockstake/test/mocks/GemMock.sol"; -import { NstMock } from "lib/lockstake/test/mocks/NstMock.sol"; -import { NstJoinMock } from "lib/lockstake/test/mocks/NstJoinMock.sol"; -import { StakingRewardsMock } from "lib/lockstake/test/mocks/StakingRewardsMock.sol"; -import { MkrNgtMock } from "lib/lockstake/test/mocks/MkrNgtMock.sol"; - -interface CalcFabLike { - function newLinearDecrease(address) external returns (address); -} - -interface LineMomLike { - function ilks(bytes32) external view returns (uint256); -} - -interface MkrAuthorityLike { - function rely(address) external; -} - -contract LockstakeEngineTest is DssTest { - using stdStorage for StdStorage; - - DssInstance dss; - address pauseProxy; - DSTokenAbstract mkr; - LockstakeMkr lsmkr; - LockstakeEngine engine; - LockstakeClipper clip; - address calc; - MedianAbstract pip; - VoteDelegateFactoryMock voteDelegateFactory; - NstMock nst; - NstJoinMock nstJoin; - GemMock rTok; - StakingRewardsMock farm; - StakingRewardsMock farm2; - MkrNgtMock mkrNgt; - GemMock ngt; - bytes32 ilk = "LSE"; - address voter; - address voteDelegate; - - LockstakeConfig cfg; - - uint256 prevLine; - - address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; - - event AddFarm(address farm); - event DelFarm(address farm); - event Open(address indexed owner, uint256 indexed index, address urn); - event Hope(address indexed urn, address indexed usr); - event Nope(address indexed urn, address indexed usr); - event SelectVoteDelegate(address indexed urn, address indexed voteDelegate_); - event SelectFarm(address indexed urn, address farm, uint16 ref); - event Lock(address indexed urn, uint256 wad, uint16 ref); - event LockNgt(address indexed urn, uint256 ngtWad, uint16 ref); - event Free(address indexed urn, address indexed to, uint256 wad, uint256 freed); - event FreeNgt(address indexed urn, address indexed to, uint256 ngtWad, uint256 ngtFreed); - event FreeNoFee(address indexed urn, address indexed to, uint256 wad); - event Draw(address indexed urn, address indexed to, uint256 wad); - event Wipe(address indexed urn, uint256 wad); - event GetReward(address indexed urn, address indexed farm, address indexed to, uint256 amt); - event OnKick(address indexed urn, uint256 wad); - event OnTake(address indexed urn, address indexed who, uint256 wad); - event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); - - // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L87-L177 - function setUp() public { - vm.createSelectFork(vm.envString("ETH_RPC_URL")); - - dss = MCD.loadFromChainlog(LOG); - - pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); - pip = MedianAbstract(dss.chainlog.getAddress("PIP_MKR")); - mkr = DSTokenAbstract(dss.chainlog.getAddress("MCD_GOV")); - nst = new NstMock(); - nstJoin = new NstJoinMock(address(dss.vat), address(nst)); - rTok = new GemMock(0); - ngt = new GemMock(0); - mkrNgt = new MkrNgtMock(address(mkr), address(ngt), 24_000); - vm.startPrank(pauseProxy); - MkrAuthorityLike(mkr.authority()).rely(address(mkrNgt)); - vm.stopPrank(); - - voteDelegateFactory = new VoteDelegateFactoryMock(address(mkr)); - voter = address(123); - vm.prank(voter); voteDelegate = voteDelegateFactory.create(); - - vm.prank(pauseProxy); pip.kiss(address(this)); - vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(1_500 * 10**18))); - - LockstakeInstance memory instance = LockstakeDeploy.deployLockstake( - address(this), - pauseProxy, - address(voteDelegateFactory), - address(nstJoin), - ilk, - 15 * WAD / 100, - address(mkrNgt), - bytes4(abi.encodeWithSignature("newLinearDecrease(address)")) - ); - - engine = LockstakeEngine(instance.engine); - clip = LockstakeClipper(instance.clipper); - calc = instance.clipperCalc; - lsmkr = LockstakeMkr(instance.lsmkr); - farm = new StakingRewardsMock(address(rTok), address(lsmkr)); - farm2 = new StakingRewardsMock(address(rTok), address(lsmkr)); - - address[] memory farms = new address[](2); - farms[0] = address(farm); - farms[1] = address(farm2); - - cfg = LockstakeConfig({ - ilk: ilk, - voteDelegateFactory: address(voteDelegateFactory), - nstJoin: address(nstJoin), - nst: address(nstJoin.nst()), - mkr: address(mkr), - mkrNgt: address(mkrNgt), - ngt: address(ngt), - farms: farms, - fee: 15 * WAD / 100, - maxLine: 10_000_000 * 10**45, - gap: 1_000_000 * 10**45, - ttl: 1 days, - dust: 50, - duty: 100000001 * 10**27 / 100000000, - mat: 3 * 10**27, - buf: 1.25 * 10**27, // 25% Initial price buffer - tail: 3600, // 1 hour before reset - cusp: 0.2 * 10**27, // 80% drop before reset - chip: 2 * WAD / 100, - tip: 3, - stopped: 0, - chop: 1 ether, - hole: 10_000 * 10**45, - tau: 100, - cut: 0, - step: 0, - lineMom: true, - tolerance: 0.5 * 10**27, - name: "LOCKSTAKE", - symbol: "LMKR" - }); - - prevLine = dss.vat.Line(); - - vm.startPrank(pauseProxy); - LockstakeInit.initLockstake(dss, instance, cfg); - vm.stopPrank(); - - deal(address(mkr), address(this), 100_000 * 10**18, true); - deal(address(ngt), address(this), 100_000 * 24_000 * 10**18, true); - - // Add some existing DAI assigned to nstJoin to avoid a particular error - stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(nstJoin)).depth(0).checked_write(100_000 * RAD); - } - - function _ink(bytes32 ilk_, address urn) internal view returns (uint256 ink) { - (ink,) = dss.vat.urns(ilk_, urn); - } - - function _art(bytes32 ilk_, address urn) internal view returns (uint256 art) { - (, art) = dss.vat.urns(ilk_, urn); - } - - // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L962-L991 - function _urnSetUp(bool withDelegate, bool withStaking) internal returns (address urn) { - urn = engine.open(0); - if (withDelegate) { - engine.selectVoteDelegate(urn, voteDelegate); - } - if (withStaking) { - engine.selectFarm(urn, address(farm), 0); - } - mkr.approve(address(engine), 100_000 * 10**18); - engine.lock(urn, 100_000 * 10**18, 5); - engine.draw(urn, address(this), 2_000 * 10**18); - assertEq(_ink(ilk, urn), 100_000 * 10**18); - assertEq(_art(ilk, urn), 2_000 * 10**18); - - if (withDelegate) { - assertEq(engine.urnVoteDelegates(urn), voteDelegate); - assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18); - assertEq(mkr.balanceOf(address(engine)), 0); - } else { - assertEq(engine.urnVoteDelegates(urn), address(0)); - assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18); - } - if (withStaking) { - assertEq(lsmkr.balanceOf(address(urn)), 0); - assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18); - assertEq(farm.balanceOf(address(urn)), 100_000 * 10**18); - } else { - assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18); - } - } - - // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L993-L1005 - function _forceLiquidation(address urn) internal returns (uint256 id) { - vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(0.05 * 10**18))); // Force liquidation - dss.spotter.poke(ilk); - assertEq(clip.kicks(), 0); - assertEq(engine.urnAuctions(urn), 0); - (,, uint256 hole,) = dss.dog.ilks(ilk); - uint256 kicked = hole < 2_000 * 10**45 ? 100_000 * 10**18 * hole / (2_000 * 10**45) : 100_000 * 10**18; - vm.expectEmit(true, true, true, true); - emit OnKick(urn, kicked); - id = dss.dog.bark(ilk, address(urn), address(this)); - assertEq(clip.kicks(), 1); - assertEq(engine.urnAuctions(urn), 1); - } - - // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 - function _testOnTake(bool withDelegate, bool withStaking) internal { - address urn = _urnSetUp(withDelegate, withStaking); - uint256 mkrInitialSupply = mkr.totalSupply(); - uint256 lsmkrInitialSupply = lsmkr.totalSupply(); - address vow = address(dss.vow); - uint256 vowInitialBalance = dss.vat.dai(vow); - uint256 id = _forceLiquidation(urn); - - LockstakeClipper.Sale memory sale; - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); - assertEq(sale.pos, 0); - assertEq(sale.tab, 2_000 * 10**45); - assertEq(sale.lot, 100_000 * 10**18); - assertEq(sale.tot, 100_000 * 10**18); - assertEq(sale.usr, address(urn)); - assertEq(sale.tic, block.timestamp); - assertEq(sale.top, pip.read() * (1.25 * 10**9)); - - assertEq(_ink(ilk, urn), 0); - assertEq(_art(ilk, urn), 0); - assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18); - - if (withDelegate) { - assertEq(mkr.balanceOf(voteDelegate), 0); - } - assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18); - if (withStaking) { - assertEq(lsmkr.balanceOf(address(farm)), 0); - assertEq(farm.balanceOf(address(urn)), 0); - } - assertEq(lsmkr.balanceOf(address(urn)), 0); - assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); - - address buyer = address(888); - vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 2_000 * 10**45); - vm.prank(buyer); dss.vat.hope(address(clip)); - assertEq(mkr.balanceOf(buyer), 0); - vm.expectEmit(true, true, true, true); - emit OnTake(urn, buyer, 20_000 * 10**18); - vm.prank(buyer); clip.take(id, 20_000 * 10**18, type(uint256).max, buyer, ""); - assertEq(mkr.balanceOf(buyer), 20_000 * 10**18); - - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); - assertEq(sale.pos, 0); - assertEq(sale.tab, (2_000 - 20_000 * 0.05 * 1.25) * 10**45); - assertEq(sale.lot, 80_000 * 10**18); - assertEq(sale.tot, 100_000 * 10**18); - assertEq(sale.usr, address(urn)); - assertEq(sale.tic, block.timestamp); - assertEq(sale.top, pip.read() * (1.25 * 10**9)); - - assertEq(_ink(ilk, urn), 0); - assertEq(_art(ilk, urn), 0); - assertEq(dss.vat.gem(ilk, address(clip)), 80_000 * 10**18); - - if (withDelegate) { - assertEq(mkr.balanceOf(voteDelegate), 0); - } - assertEq(mkr.balanceOf(address(engine)), 80_000 * 10**18); - if (withStaking) { - assertEq(lsmkr.balanceOf(address(farm)), 0); - assertEq(farm.balanceOf(address(urn)), 0); - } - assertEq(lsmkr.balanceOf(address(urn)), 0); - assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); - - uint256 burn = 32_000 * 10**18 * engine.fee() / (WAD - engine.fee()); - vm.expectEmit(true, true, true, true); - emit OnTake(urn, buyer, 12_000 * 10**18); - vm.expectEmit(true, true, true, true); - emit OnRemove(urn, 32_000 * 10**18, burn, 100_000 * 10**18 - 32_000 * 10**18 - burn); - vm.prank(buyer); clip.take(id, 12_000 * 10**18, type(uint256).max, buyer, ""); - assertEq(burn, (32_000 * 10**18 + burn) * engine.fee() / WAD); - assertEq(mkr.balanceOf(buyer), 32_000 * 10**18); - assertEq(engine.urnAuctions(urn), 0); - - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); - assertEq(sale.pos, 0); - assertEq(sale.tab, 0); - assertEq(sale.lot, 0); - assertEq(sale.tot, 0); - assertEq(sale.usr, address(0)); - assertEq(sale.tic, 0); - assertEq(sale.top, 0); - - assertEq(_ink(ilk, urn), 100_000 * 10**18 - 32_000 * 10**18 - burn); - assertEq(_art(ilk, urn), 0); - assertEq(dss.vat.gem(ilk, address(clip)), 0); - - assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18 - 32_000 * 10**18 - burn); - assertEq(mkr.totalSupply(), mkrInitialSupply - burn); - if (withStaking) { - assertEq(lsmkr.balanceOf(address(farm)), 0); - assertEq(farm.balanceOf(address(urn)), 0); - } - assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18 - 32_000 * 10**18 - burn); - assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 32_000 * 10**18 - burn); - assertEq(dss.vat.dai(vow), vowInitialBalance + 2_000 * 10**45); - } - - function testOnTakeNoWithStakingNoDelegate() public { - _testOnTake(false, false); - } - - function testOnTakeNoWithStakingWithDelegate() public { - _testOnTake(true, false); - } - - function testOnTakeWithStakingNoDelegate() public { - _testOnTake(false, true); - } - - function testOnTakeWithStakingWithDelegate() public { - _testOnTake(true, true); - } -} From 0bf18d36f972445490cad0b04e847502fab9a943 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Mon, 27 May 2024 15:22:41 +0200 Subject: [PATCH 09/25] fix solidity version to 0.6.12 for existing callees --- src/OasisDexCallee.sol | 2 +- src/OneInchCallee.sol | 2 +- src/PSMCallee.sol | 2 +- src/UniswapV2Callee.sol | 2 +- src/UniswapV2LpTokenCallee.sol | 2 +- src/UniswapV3Callee.sol | 2 +- src/UniswapV3SplitRouteCallee.sol | 2 +- src/WstETHCurveUniv3Callee.sol | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/OasisDexCallee.sol b/src/OasisDexCallee.sol index fefee15..455507e 100644 --- a/src/OasisDexCallee.sol +++ b/src/OasisDexCallee.sol @@ -14,7 +14,7 @@ // 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 solidity 0.6.12; interface VatLike { function hope(address) external; diff --git a/src/OneInchCallee.sol b/src/OneInchCallee.sol index 0000be3..bf443f1 100644 --- a/src/OneInchCallee.sol +++ b/src/OneInchCallee.sol @@ -14,7 +14,7 @@ // 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 solidity 0.6.12; pragma experimental ABIEncoderV2; interface GemJoinLike { diff --git a/src/PSMCallee.sol b/src/PSMCallee.sol index 5990d1c..e8c24fe 100644 --- a/src/PSMCallee.sol +++ b/src/PSMCallee.sol @@ -14,7 +14,7 @@ // 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 solidity 0.6.12; pragma experimental ABIEncoderV2; interface GemJoinLike { diff --git a/src/UniswapV2Callee.sol b/src/UniswapV2Callee.sol index d7d4874..6ce2668 100644 --- a/src/UniswapV2Callee.sol +++ b/src/UniswapV2Callee.sol @@ -15,7 +15,7 @@ // 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 solidity 0.6.12; interface VatLike { function hope(address) external; diff --git a/src/UniswapV2LpTokenCallee.sol b/src/UniswapV2LpTokenCallee.sol index b00e885..e1bf59f 100644 --- a/src/UniswapV2LpTokenCallee.sol +++ b/src/UniswapV2LpTokenCallee.sol @@ -15,7 +15,7 @@ // 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 solidity 0.6.12; interface VatLike { function hope(address) external; diff --git a/src/UniswapV3Callee.sol b/src/UniswapV3Callee.sol index 0b2ccc9..6b7ecd1 100644 --- a/src/UniswapV3Callee.sol +++ b/src/UniswapV3Callee.sol @@ -14,7 +14,7 @@ // 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 solidity 0.6.12; pragma experimental ABIEncoderV2; interface GemJoinLike { diff --git a/src/UniswapV3SplitRouteCallee.sol b/src/UniswapV3SplitRouteCallee.sol index cd055c8..31fcd37 100644 --- a/src/UniswapV3SplitRouteCallee.sol +++ b/src/UniswapV3SplitRouteCallee.sol @@ -14,7 +14,7 @@ // 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 solidity 0.6.12; pragma experimental ABIEncoderV2; interface GemJoinLike { diff --git a/src/WstETHCurveUniv3Callee.sol b/src/WstETHCurveUniv3Callee.sol index d1fd289..73da461 100644 --- a/src/WstETHCurveUniv3Callee.sol +++ b/src/WstETHCurveUniv3Callee.sol @@ -14,7 +14,7 @@ // 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 solidity 0.6.12; pragma experimental ABIEncoderV2; interface GemJoinLike { From 0d6aec4eba847820ec72aff5f50d079bd1fccf12 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Mon, 27 May 2024 15:24:20 +0200 Subject: [PATCH 10/25] add initial version of UniswapV2LockstakeCallee --- src/UniswapV2LockstakeCallee.sol | 138 ++++++++++++++++++++++++ src/test/UniswapV2LockstakeCallee.t.sol | 41 ++++++- 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/UniswapV2LockstakeCallee.sol diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol new file mode 100644 index 0000000..e2d48bb --- /dev/null +++ b/src/UniswapV2LockstakeCallee.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC. +// Copyright (C) 2021 Dai Foundation +// +// 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.8.21; + +interface VatLike { + function hope(address) external; +} + +interface GemJoinLike { + function dec() external view returns (uint256); + function gem() external view returns (TokenLike); + function exit(address, uint256) external; +} + +interface daiJoinLike { + function dai() external view returns (TokenLike); + function vat() external view returns (VatLike); + function join(address, uint256) external; +} + +interface TokenLike { + function approve(address, uint256) external; + function transfer(address, uint256) external; + function balanceOf(address) external view returns (uint256); +} + +interface CharterManagerLike { + function exit(address crop, address usr, uint256 val) external; +} + +interface UniswapV2Router02Like { + function swapExactTokensForTokens(uint256, uint256, address[] calldata, address, uint256) external returns (uint[] memory); +} + +// Simple Callee Example to interact with MatchingMarket +// This Callee contract exists as a standalone contract +contract UniswapV2Callee { + UniswapV2Router02Like public uniRouter02; + daiJoinLike public daiJoin; + TokenLike public dai; + + 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; + } + + function setUp(address uniRouter02_, address daiJoin_) internal { + uniRouter02 = UniswapV2Router02Like(uniRouter02_); + daiJoin = daiJoinLike(daiJoin_); + dai = daiJoin.dai(); + + dai.approve(daiJoin_, type(uint256).max); + } + + function _fromWad(address gemJoin, uint256 wad) internal view returns (uint256 amt) { + amt = wad / 10 ** (sub(18, GemJoinLike(gemJoin).dec())); + } +} + +contract UniswapV2LockstakeCallee is UniswapV2Callee { + constructor(address uniRouter02_, address daiJoin_) { + setUp(uniRouter02_, daiJoin_); + } + + function clipperCall( + address sender, // Clipper Caller and Dai deliveryaddress + uint256 daiAmt, // Dai amount to payback[rad] + uint256 gemAmt, // Gem amount received [wad] + bytes calldata data // Extra data needed (gemJoin) + ) external { + ( + address to, // address to send remaining DAI to + address gemJoin, // gemJoin adapter address + uint256 minProfit, // minimum profit in DAI to make [wad] + address[] memory path, // Uniswap pool path + address charterManager // pass address(0) if no manager + ) = abi.decode(data, (address, address, uint256, address[], address)); + + // Convert gem amount to token precision + gemAmt = _fromWad(gemJoin, gemAmt); + + // Exit collateral to token version + if(charterManager != address(0)) { + CharterManagerLike(charterManager).exit(gemJoin, address(this), gemAmt); + } + + // Approve uniRouter02 to take gem + TokenLike gem = GemJoinLike(gemJoin).gem(); + gem.approve(address(uniRouter02), gemAmt); + + // Calculate amount of DAI to Join (as erc20 WAD value) + uint256 daiToJoin = divup(daiAmt, RAY); + + // Do operation and get dai amount bought (checking the profit is achieved) + uniRouter02.swapExactTokensForTokens( + gemAmt, + add(daiToJoin, minProfit), + path, + address(this), + block.timestamp + ); + + // Although Uniswap will accept all gems, this check is a sanity check, just in case + // Transfer any lingering gem to specified address + if (gem.balanceOf(address(this)) > 0) { + gem.transfer(to, 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/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 24e1ab0..5a05100 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -18,6 +18,8 @@ import { NstJoinMock } from "lib/lockstake/test/mocks/NstJoinMock.sol"; import { StakingRewardsMock } from "lib/lockstake/test/mocks/StakingRewardsMock.sol"; import { MkrNgtMock } from "lib/lockstake/test/mocks/MkrNgtMock.sol"; +import { UniswapV2LockstakeCallee } from "src/UniswapV2LockstakeCallee.sol"; + interface CalcFabLike { function newLinearDecrease(address) external returns (address); } @@ -30,6 +32,33 @@ interface MkrAuthorityLike { function rely(address) external; } +contract MockUniswapRouter02 is DssTest { + uint256 fixedPrice; + + constructor(uint256 price_) { + fixedPrice = price_; + } + + // Hardcoded to simulate fixed price Uniswap + /* uniRouter02.swapExactTokensForTokens(gemAmt, daiToJoin + minProfit, path, address(this), block.timestamp); */ + function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts) { + to; deadline; // silence warning + uint buyAmt = amountIn * fixedPrice; + require(amountOutMin <= buyAmt, "Minimum Fill not reached"); + + DSTokenAbstract(path[0]).transferFrom(msg.sender, address(this), amountIn); + assertEq(DSTokenAbstract(path[0]).balanceOf(address(this)), amountIn); + + DSTokenAbstract(path[path.length - 1]).transfer(msg.sender, buyAmt); + assertEq(DSTokenAbstract(path[path.length - 1]).balanceOf(msg.sender), buyAmt); + + amounts = new uint[](2); + amounts[0] = amountIn; + amounts[1] = buyAmt; + } + +} + contract UniswapV2LockstakeCalleeTest is DssTest { using stdStorage for StdStorage; @@ -59,6 +88,9 @@ contract UniswapV2LockstakeCalleeTest is DssTest { address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; + MockUniswapRouter02 uniRouter02; + UniswapV2LockstakeCallee callee; + event AddFarm(address farm); event DelFarm(address farm); event Open(address indexed owner, uint256 indexed index, address urn); @@ -78,6 +110,13 @@ contract UniswapV2LockstakeCalleeTest is DssTest { event OnTake(address indexed urn, address indexed who, uint256 wad); event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); + modifier setupCallee() { + uint256 fixedUniV2Price = 2; + uniRouter02 = new MockUniswapRouter02(fixedUniV2Price); + callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI")); + _; + } + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L87-L177 function setUp() public { vm.createSelectFork(vm.envString("ETH_RPC_URL")); @@ -227,7 +266,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { } // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 - function _testOnTake(bool withDelegate, bool withStaking) internal { + function _testOnTake(bool withDelegate, bool withStaking) internal setupCallee { address urn = _urnSetUp(withDelegate, withStaking); uint256 mkrInitialSupply = mkr.totalSupply(); uint256 lsmkrInitialSupply = lsmkr.totalSupply(); From 242ae1c1e6eeaa126d1c43552d97ecef4633373e Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Mon, 27 May 2024 15:34:34 +0200 Subject: [PATCH 11/25] update math functions --- src/UniswapV2LockstakeCallee.sol | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol index e2d48bb..8340b24 100644 --- a/src/UniswapV2LockstakeCallee.sol +++ b/src/UniswapV2LockstakeCallee.sol @@ -56,14 +56,8 @@ contract UniswapV2Callee { 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; + z = x != 0 ? ((x - 1) / y) + 1 : 0; } function setUp(address uniRouter02_, address daiJoin_) internal { @@ -75,7 +69,7 @@ contract UniswapV2Callee { } function _fromWad(address gemJoin, uint256 wad) internal view returns (uint256 amt) { - amt = wad / 10 ** (sub(18, GemJoinLike(gemJoin).dec())); + amt = wad / 10 ** (18 - GemJoinLike(gemJoin).dec()); } } @@ -116,7 +110,7 @@ contract UniswapV2LockstakeCallee is UniswapV2Callee { // Do operation and get dai amount bought (checking the profit is achieved) uniRouter02.swapExactTokensForTokens( gemAmt, - add(daiToJoin, minProfit), + daiToJoin + minProfit, path, address(this), block.timestamp From 2dad8be2f8e1ae1a0d7217fbea3c8f6f1fc0d876 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Tue, 28 May 2024 10:17:39 +0200 Subject: [PATCH 12/25] adjust callee, add working callee test --- src/UniswapV2LockstakeCallee.sol | 48 +++++++-------------- src/test/UniswapV2LockstakeCallee.t.sol | 56 +++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol index 8340b24..a7a0582 100644 --- a/src/UniswapV2LockstakeCallee.sol +++ b/src/UniswapV2LockstakeCallee.sol @@ -17,19 +17,8 @@ pragma solidity ^0.8.21; -interface VatLike { - function hope(address) external; -} - -interface GemJoinLike { - function dec() external view returns (uint256); - function gem() external view returns (TokenLike); - function exit(address, uint256) external; -} - interface daiJoinLike { function dai() external view returns (TokenLike); - function vat() external view returns (VatLike); function join(address, uint256) external; } @@ -37,10 +26,7 @@ interface TokenLike { function approve(address, uint256) external; function transfer(address, uint256) external; function balanceOf(address) external view returns (uint256); -} - -interface CharterManagerLike { - function exit(address crop, address usr, uint256 val) external; + function decimals() external view returns (uint256); } interface UniswapV2Router02Like { @@ -53,6 +39,7 @@ contract UniswapV2Callee { UniswapV2Router02Like public uniRouter02; daiJoinLike public daiJoin; TokenLike public dai; + TokenLike public mkr; uint256 public constant RAY = 10 ** 27; @@ -60,22 +47,23 @@ contract UniswapV2Callee { z = x != 0 ? ((x - 1) / y) + 1 : 0; } - function setUp(address uniRouter02_, address daiJoin_) internal { + function setUp(address uniRouter02_, address daiJoin_, address mkr_) internal { uniRouter02 = UniswapV2Router02Like(uniRouter02_); daiJoin = daiJoinLike(daiJoin_); dai = daiJoin.dai(); + mkr = TokenLike(mkr_); dai.approve(daiJoin_, type(uint256).max); } - function _fromWad(address gemJoin, uint256 wad) internal view returns (uint256 amt) { - amt = wad / 10 ** (18 - GemJoinLike(gemJoin).dec()); + function _fromWad(uint256 wad) internal view returns (uint256 amt) { + amt = wad / 10 ** (18 - mkr.decimals()); } } contract UniswapV2LockstakeCallee is UniswapV2Callee { - constructor(address uniRouter02_, address daiJoin_) { - setUp(uniRouter02_, daiJoin_); + constructor(address uniRouter02_, address daiJoin_, address mkr_) { + setUp(uniRouter02_, daiJoin_, mkr_); } function clipperCall( @@ -86,23 +74,15 @@ contract UniswapV2LockstakeCallee is UniswapV2Callee { ) external { ( address to, // address to send remaining DAI to - address gemJoin, // gemJoin adapter address uint256 minProfit, // minimum profit in DAI to make [wad] - address[] memory path, // Uniswap pool path - address charterManager // pass address(0) if no manager - ) = abi.decode(data, (address, address, uint256, address[], address)); + address[] memory path // Uniswap pool path + ) = abi.decode(data, (address, uint256, address[])); // Convert gem amount to token precision - gemAmt = _fromWad(gemJoin, gemAmt); - - // Exit collateral to token version - if(charterManager != address(0)) { - CharterManagerLike(charterManager).exit(gemJoin, address(this), gemAmt); - } + gemAmt = _fromWad(gemAmt); // Approve uniRouter02 to take gem - TokenLike gem = GemJoinLike(gemJoin).gem(); - gem.approve(address(uniRouter02), gemAmt); + mkr.approve(address(uniRouter02), gemAmt); // Calculate amount of DAI to Join (as erc20 WAD value) uint256 daiToJoin = divup(daiAmt, RAY); @@ -118,8 +98,8 @@ contract UniswapV2LockstakeCallee is UniswapV2Callee { // Although Uniswap will accept all gems, this check is a sanity check, just in case // Transfer any lingering gem to specified address - if (gem.balanceOf(address(this)) > 0) { - gem.transfer(to, gem.balanceOf(address(this))); + if (mkr.balanceOf(address(this)) > 0) { + mkr.transfer(to, mkr.balanceOf(address(this))); } // Convert DAI bought to internal vat value of the msg.sender of Clipper.take diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 5a05100..72d26f6 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -49,7 +49,7 @@ contract MockUniswapRouter02 is DssTest { DSTokenAbstract(path[0]).transferFrom(msg.sender, address(this), amountIn); assertEq(DSTokenAbstract(path[0]).balanceOf(address(this)), amountIn); - DSTokenAbstract(path[path.length - 1]).transfer(msg.sender, buyAmt); + GodMode.setBalance(path[path.length - 1], msg.sender, buyAmt); assertEq(DSTokenAbstract(path[path.length - 1]).balanceOf(msg.sender), buyAmt); amounts = new uint[](2); @@ -64,6 +64,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { DssInstance dss; address pauseProxy; + DSTokenAbstract dai; DSTokenAbstract mkr; LockstakeMkr lsmkr; LockstakeEngine engine; @@ -111,9 +112,9 @@ contract UniswapV2LockstakeCalleeTest is DssTest { event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); modifier setupCallee() { - uint256 fixedUniV2Price = 2; + uint256 fixedUniV2Price = 1; uniRouter02 = new MockUniswapRouter02(fixedUniV2Price); - callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI")); + callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(mkr)); _; } @@ -125,6 +126,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); pip = MedianAbstract(dss.chainlog.getAddress("PIP_MKR")); + dai = DSTokenAbstract(dss.chainlog.getAddress("MCD_DAI")); mkr = DSTokenAbstract(dss.chainlog.getAddress("MCD_GOV")); nst = new NstMock(); nstJoin = new NstJoinMock(address(dss.vat), address(nst)); @@ -266,7 +268,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { } // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 - function _testOnTake(bool withDelegate, bool withStaking) internal setupCallee { + function _testOnTake(bool withDelegate, bool withStaking) internal { address urn = _urnSetUp(withDelegate, withStaking); uint256 mkrInitialSupply = mkr.totalSupply(); uint256 lsmkrInitialSupply = lsmkr.totalSupply(); @@ -381,4 +383,50 @@ contract UniswapV2LockstakeCalleeTest is DssTest { function testOnTakeWithStakingWithDelegate() public { _testOnTake(true, true); } + + function _testCalleeTake(bool withDelegate, bool withStaking) internal setupCallee { + address urn = _urnSetUp(withDelegate, withStaking); + uint256 id = _forceLiquidation(urn); + + address buyer = address(888); + vm.prank(buyer); dss.vat.hope(address(clip)); + assertEq(mkr.balanceOf(buyer), 0); + assertEq(dai.balanceOf(buyer), 0); + + // partially take auction with callee + address[] memory path = new address[](2); + path[0] = address(mkr); + path[1] = address(dai); + bytes memory flashData = abi.encode( + address(buyer), // Address of the user (where profits are sent) + 0, // Minimum dai profit [wad] + path // Uniswap v2 path + ); + vm.prank(buyer); clip.take( + id, + 20_000 * 10**18, + type(uint256).max, + address(callee), + flashData + ); + + assertEq(mkr.balanceOf(buyer), 0); + assertEq(dai.balanceOf(buyer), 20_000 * 10**18 - 20_000 * pip.read() * clip.buf() / RAY); + } + + function testCalleeTakeNoWithStakingNoDelegate() public { + _testCalleeTake(false, false); + } + + function testCalleeTakeNoWithStakingWithDelegate() public { + _testCalleeTake(true, false); + } + + function testCalleeTakeWithStakingNoDelegate() public { + _testCalleeTake(false, true); + } + + function testCalleeTakeWithStakingWithDelegate() public { + _testCalleeTake(true, true); + } } From 9e28ecb51b5ca2d67b124d467cbb2cc9e72ed8ef Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Tue, 28 May 2024 10:50:03 +0200 Subject: [PATCH 13/25] add a test case to take the rest of the auction --- src/test/UniswapV2LockstakeCallee.t.sol | 56 +++++++++++++++++++------ 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 72d26f6..7a9c761 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -46,11 +46,13 @@ contract MockUniswapRouter02 is DssTest { uint buyAmt = amountIn * fixedPrice; require(amountOutMin <= buyAmt, "Minimum Fill not reached"); + uint256 initialInBalance = DSTokenAbstract(path[0]).balanceOf(address(this)); DSTokenAbstract(path[0]).transferFrom(msg.sender, address(this), amountIn); - assertEq(DSTokenAbstract(path[0]).balanceOf(address(this)), amountIn); + assertEq(DSTokenAbstract(path[0]).balanceOf(address(this)), initialInBalance + amountIn); - GodMode.setBalance(path[path.length - 1], msg.sender, buyAmt); - assertEq(DSTokenAbstract(path[path.length - 1]).balanceOf(msg.sender), buyAmt); + uint256 initialOutBalance = DSTokenAbstract(path[path.length - 1]).balanceOf(msg.sender); + GodMode.setBalance(path[path.length - 1], msg.sender, initialOutBalance + buyAmt); + assertEq(DSTokenAbstract(path[path.length - 1]).balanceOf(msg.sender), initialOutBalance + buyAmt); amounts = new uint[](2); amounts[0] = amountIn; @@ -388,21 +390,21 @@ contract UniswapV2LockstakeCalleeTest is DssTest { address urn = _urnSetUp(withDelegate, withStaking); uint256 id = _forceLiquidation(urn); - address buyer = address(888); - vm.prank(buyer); dss.vat.hope(address(clip)); - assertEq(mkr.balanceOf(buyer), 0); - assertEq(dai.balanceOf(buyer), 0); + address buyer1 = address(111); + vm.prank(buyer1); dss.vat.hope(address(clip)); + assertEq(mkr.balanceOf(buyer1), 0); + assertEq(dai.balanceOf(buyer1), 0); // partially take auction with callee address[] memory path = new address[](2); path[0] = address(mkr); path[1] = address(dai); bytes memory flashData = abi.encode( - address(buyer), // Address of the user (where profits are sent) - 0, // Minimum dai profit [wad] - path // Uniswap v2 path + address(buyer1), // Address of the user (where profits are sent) + 0, // Minimum dai profit [wad] + path // Uniswap v2 path ); - vm.prank(buyer); clip.take( + vm.prank(buyer1); clip.take( id, 20_000 * 10**18, type(uint256).max, @@ -410,8 +412,36 @@ contract UniswapV2LockstakeCalleeTest is DssTest { flashData ); - assertEq(mkr.balanceOf(buyer), 0); - assertEq(dai.balanceOf(buyer), 20_000 * 10**18 - 20_000 * pip.read() * clip.buf() / RAY); + assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); + assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); + assertEq(mkr.balanceOf(buyer1), 0, "invalid-final-buyer2-mkr-balance"); + uint256 daiBalanceAfterPartialTake = 20_000 * 10**18 - 20_000 * pip.read() * clip.buf() / RAY; + assertEq(dai.balanceOf(buyer1), daiBalanceAfterPartialTake, "invalid-final-buyer2-dai-balance"); + + // use different buyer to take the rest of the auction + address buyer2 = address(222); + vm.prank(buyer2); dss.vat.hope(address(clip)); + assertEq(mkr.balanceOf(buyer2), 0, "invalid-initial-buyer2-mkr-balance"); + assertEq(dai.balanceOf(buyer2), 0, "invalid-initial-buyer2-dai-balance"); + + // take the rest of the auction with callee + flashData = abi.encode( + address(buyer2), // Address of the user (where profits are sent) + 0, // Minimum dai profit [wad] + path // Uniswap v2 path + ); + vm.prank(buyer2); clip.take( + id, + type(uint256).max, + type(uint256).max, + address(callee), + flashData + ); + + assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); + assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); + assertEq(mkr.balanceOf(buyer2), 0, "invalid-final-buyer2-mkr-balance"); + assertEq(dai.balanceOf(buyer2), 12_000 * 10**18 - 12_000 * pip.read() * clip.buf() / RAY, "invalid-final-buyer2-dai-balance"); } function testCalleeTakeNoWithStakingNoDelegate() public { From 72154bfad715e7cd7553d91821ff11220934c6ce Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Tue, 2 Jul 2024 13:51:49 +0200 Subject: [PATCH 14/25] test profit address --- src/test/UniswapV2LockstakeCallee.t.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 7a9c761..9fb7a9b 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -387,9 +387,11 @@ contract UniswapV2LockstakeCalleeTest is DssTest { } function _testCalleeTake(bool withDelegate, bool withStaking) internal setupCallee { + // setup urn and force its liquidation address urn = _urnSetUp(withDelegate, withStaking); uint256 id = _forceLiquidation(urn); + // setup buyer address buyer1 = address(111); vm.prank(buyer1); dss.vat.hope(address(clip)); assertEq(mkr.balanceOf(buyer1), 0); @@ -411,22 +413,22 @@ contract UniswapV2LockstakeCalleeTest is DssTest { address(callee), flashData ); - assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); assertEq(mkr.balanceOf(buyer1), 0, "invalid-final-buyer2-mkr-balance"); uint256 daiBalanceAfterPartialTake = 20_000 * 10**18 - 20_000 * pip.read() * clip.buf() / RAY; assertEq(dai.balanceOf(buyer1), daiBalanceAfterPartialTake, "invalid-final-buyer2-dai-balance"); - // use different buyer to take the rest of the auction + // setup different buyer to take the rest of the auction address buyer2 = address(222); vm.prank(buyer2); dss.vat.hope(address(clip)); assertEq(mkr.balanceOf(buyer2), 0, "invalid-initial-buyer2-mkr-balance"); assertEq(dai.balanceOf(buyer2), 0, "invalid-initial-buyer2-dai-balance"); + address profitAddress = address(333); // take the rest of the auction with callee flashData = abi.encode( - address(buyer2), // Address of the user (where profits are sent) + address(profitAddress), // Address of the user (where profits are sent) 0, // Minimum dai profit [wad] path // Uniswap v2 path ); @@ -437,11 +439,10 @@ contract UniswapV2LockstakeCalleeTest is DssTest { address(callee), flashData ); - assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); assertEq(mkr.balanceOf(buyer2), 0, "invalid-final-buyer2-mkr-balance"); - assertEq(dai.balanceOf(buyer2), 12_000 * 10**18 - 12_000 * pip.read() * clip.buf() / RAY, "invalid-final-buyer2-dai-balance"); + assertEq(dai.balanceOf(profitAddress), 12_000 * 10**18 - 12_000 * pip.read() * clip.buf() / RAY, "invalid-final-profit"); } function testCalleeTakeNoWithStakingNoDelegate() public { From 602906754ae84857cc0c69a9c67690050e29f246 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Tue, 30 Jul 2024 09:27:52 +0200 Subject: [PATCH 15/25] refactor: make uniswap path configurable in tests --- src/test/UniswapV2LockstakeCallee.t.sol | 44 ++++++++++--------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 9fb7a9b..1e3fe13 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -93,29 +93,22 @@ contract UniswapV2LockstakeCalleeTest is DssTest { MockUniswapRouter02 uniRouter02; UniswapV2LockstakeCallee callee; + address[] mkrToDaiPath = new address[](2); - event AddFarm(address farm); - event DelFarm(address farm); - event Open(address indexed owner, uint256 indexed index, address urn); - event Hope(address indexed urn, address indexed usr); - event Nope(address indexed urn, address indexed usr); - event SelectVoteDelegate(address indexed urn, address indexed voteDelegate_); - event SelectFarm(address indexed urn, address farm, uint16 ref); - event Lock(address indexed urn, uint256 wad, uint16 ref); - event LockNgt(address indexed urn, uint256 ngtWad, uint16 ref); - event Free(address indexed urn, address indexed to, uint256 wad, uint256 freed); - event FreeNgt(address indexed urn, address indexed to, uint256 ngtWad, uint256 ngtFreed); - event FreeNoFee(address indexed urn, address indexed to, uint256 wad); - event Draw(address indexed urn, address indexed to, uint256 wad); - event Wipe(address indexed urn, uint256 wad); - event GetReward(address indexed urn, address indexed farm, address indexed to, uint256 amt); event OnKick(address indexed urn, uint256 wad); event OnTake(address indexed urn, address indexed who, uint256 wad); event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); modifier setupCallee() { + // setup mock of the uniswap router uint256 fixedUniV2Price = 1; uniRouter02 = new MockUniswapRouter02(fixedUniV2Price); + + // set uniswap exchange paths + mkrToDaiPath[0] = address(mkr); + mkrToDaiPath[1] = address(dai); + + // deploy callee contract callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(mkr)); _; } @@ -386,7 +379,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { _testOnTake(true, true); } - function _testCalleeTake(bool withDelegate, bool withStaking) internal setupCallee { + function _testCalleeTake(bool withDelegate, bool withStaking, address[] memory path) internal { // setup urn and force its liquidation address urn = _urnSetUp(withDelegate, withStaking); uint256 id = _forceLiquidation(urn); @@ -398,9 +391,6 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(dai.balanceOf(buyer1), 0); // partially take auction with callee - address[] memory path = new address[](2); - path[0] = address(mkr); - path[1] = address(dai); bytes memory flashData = abi.encode( address(buyer1), // Address of the user (where profits are sent) 0, // Minimum dai profit [wad] @@ -445,19 +435,19 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(dai.balanceOf(profitAddress), 12_000 * 10**18 - 12_000 * pip.read() * clip.buf() / RAY, "invalid-final-profit"); } - function testCalleeTakeNoWithStakingNoDelegate() public { - _testCalleeTake(false, false); + function testCalleeTakeNoWithStakingNoDelegate() public setupCallee { + _testCalleeTake(false, false, mkrToDaiPath); } - function testCalleeTakeNoWithStakingWithDelegate() public { - _testCalleeTake(true, false); + function testCalleeTakeNoWithStakingWithDelegate() public setupCallee { + _testCalleeTake(true, false, mkrToDaiPath); } - function testCalleeTakeWithStakingNoDelegate() public { - _testCalleeTake(false, true); + function testCalleeTakeWithStakingNoDelegate() public setupCallee { + _testCalleeTake(false, true, mkrToDaiPath); } - function testCalleeTakeWithStakingWithDelegate() public { - _testCalleeTake(true, true); + function testCalleeTakeWithStakingWithDelegate() public setupCallee { + _testCalleeTake(true, true, mkrToDaiPath); } } From d6d0a7ee6cd56759d24513989f455ba6ccd4538b Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Tue, 30 Jul 2024 10:24:02 +0200 Subject: [PATCH 16/25] determine received gem based on the path --- src/UniswapV2LockstakeCallee.sol | 23 ++++++++++++----------- src/test/UniswapV2LockstakeCallee.t.sol | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol index a7a0582..2707231 100644 --- a/src/UniswapV2LockstakeCallee.sol +++ b/src/UniswapV2LockstakeCallee.sol @@ -39,7 +39,6 @@ contract UniswapV2Callee { UniswapV2Router02Like public uniRouter02; daiJoinLike public daiJoin; TokenLike public dai; - TokenLike public mkr; uint256 public constant RAY = 10 ** 27; @@ -47,23 +46,22 @@ contract UniswapV2Callee { z = x != 0 ? ((x - 1) / y) + 1 : 0; } - function setUp(address uniRouter02_, address daiJoin_, address mkr_) internal { + function setUp(address uniRouter02_, address daiJoin_) internal { uniRouter02 = UniswapV2Router02Like(uniRouter02_); daiJoin = daiJoinLike(daiJoin_); dai = daiJoin.dai(); - mkr = TokenLike(mkr_); dai.approve(daiJoin_, type(uint256).max); } - function _fromWad(uint256 wad) internal view returns (uint256 amt) { - amt = wad / 10 ** (18 - mkr.decimals()); + function _fromWad(TokenLike gem, uint256 wad) internal view returns (uint256 amt) { + amt = wad / 10 ** (18 - gem.decimals()); } } contract UniswapV2LockstakeCallee is UniswapV2Callee { - constructor(address uniRouter02_, address daiJoin_, address mkr_) { - setUp(uniRouter02_, daiJoin_, mkr_); + constructor(address uniRouter02_, address daiJoin_) { + setUp(uniRouter02_, daiJoin_); } function clipperCall( @@ -78,11 +76,14 @@ contract UniswapV2LockstakeCallee is UniswapV2Callee { address[] memory path // Uniswap pool path ) = abi.decode(data, (address, uint256, address[])); + // Determine received token + TokenLike gem = TokenLike(path[0]); + // Convert gem amount to token precision - gemAmt = _fromWad(gemAmt); + gemAmt = _fromWad(gem, gemAmt); // Approve uniRouter02 to take gem - mkr.approve(address(uniRouter02), gemAmt); + gem.approve(address(uniRouter02), gemAmt); // Calculate amount of DAI to Join (as erc20 WAD value) uint256 daiToJoin = divup(daiAmt, RAY); @@ -98,8 +99,8 @@ contract UniswapV2LockstakeCallee is UniswapV2Callee { // Although Uniswap will accept all gems, this check is a sanity check, just in case // Transfer any lingering gem to specified address - if (mkr.balanceOf(address(this)) > 0) { - mkr.transfer(to, mkr.balanceOf(address(this))); + if (gem.balanceOf(address(this)) > 0) { + gem.transfer(to, gem.balanceOf(address(this))); } // Convert DAI bought to internal vat value of the msg.sender of Clipper.take diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 1e3fe13..6602880 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -94,6 +94,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { MockUniswapRouter02 uniRouter02; UniswapV2LockstakeCallee callee; address[] mkrToDaiPath = new address[](2); + address[] ngtToNstPath = new address[](2); event OnKick(address indexed urn, uint256 wad); event OnTake(address indexed urn, address indexed who, uint256 wad); @@ -107,9 +108,11 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // set uniswap exchange paths mkrToDaiPath[0] = address(mkr); mkrToDaiPath[1] = address(dai); + ngtToNstPath[0] = address(ngt); + ngtToNstPath[1] = address(nst); // deploy callee contract - callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(mkr)); + callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI")); _; } @@ -435,19 +438,23 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(dai.balanceOf(profitAddress), 12_000 * 10**18 - 12_000 * pip.read() * clip.buf() / RAY, "invalid-final-profit"); } - function testCalleeTakeNoWithStakingNoDelegate() public setupCallee { + function testCalleeTakeNoWithStakingNoDelegateMkr() public setupCallee { _testCalleeTake(false, false, mkrToDaiPath); } - function testCalleeTakeNoWithStakingWithDelegate() public setupCallee { + function testCalleeTakeNoWithStakingNoDelegateNgt() public setupCallee { + _testCalleeTake(false, false, ngtToNstPath); + } + + function testCalleeTakeNoWithStakingWithDelegateMkr() public setupCallee { _testCalleeTake(true, false, mkrToDaiPath); } - function testCalleeTakeWithStakingNoDelegate() public setupCallee { + function testCalleeTakeWithStakingNoDelegateMkr() public setupCallee { _testCalleeTake(false, true, mkrToDaiPath); } - function testCalleeTakeWithStakingWithDelegate() public setupCallee { + function testCalleeTakeWithStakingWithDelegateMkr() public setupCallee { _testCalleeTake(true, true, mkrToDaiPath); } } From c5a97a0bb9e114740a4fbfd5efaffbbc953dccb6 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Wed, 31 Jul 2024 09:24:17 +0200 Subject: [PATCH 17/25] Support NGT --- src/UniswapV2LockstakeCallee.sol | 51 +++++++++++++----------- src/test/UniswapV2LockstakeCallee.t.sol | 53 ++++++++++++++++--------- 2 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol index 2707231..2cf50b2 100644 --- a/src/UniswapV2LockstakeCallee.sol +++ b/src/UniswapV2LockstakeCallee.sol @@ -33,35 +33,35 @@ interface UniswapV2Router02Like { function swapExactTokensForTokens(uint256, uint256, address[] calldata, address, uint256) external returns (uint[] memory); } -// Simple Callee Example to interact with MatchingMarket -// This Callee contract exists as a standalone contract -contract UniswapV2Callee { - UniswapV2Router02Like public uniRouter02; - daiJoinLike public daiJoin; - TokenLike public dai; +interface MkrNgt { + function mkr() external view returns (address); + function ngt() external view returns (address); + function rate() external view returns (uint256); + function mkrToNgt(address usr, uint256 mkrAmt) external; +} +contract UniswapV2LockstakeCallee { + UniswapV2Router02Like public immutable uniRouter02; + daiJoinLike public immutable daiJoin; + TokenLike public immutable dai; + MkrNgt public immutable mkrNgt; + TokenLike public immutable mkr; + TokenLike public immutable ngt; uint256 public constant RAY = 10 ** 27; - function divup(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = x != 0 ? ((x - 1) / y) + 1 : 0; - } - - function setUp(address uniRouter02_, address daiJoin_) internal { + constructor(address uniRouter02_, address daiJoin_, address mkrNgt_) { uniRouter02 = UniswapV2Router02Like(uniRouter02_); daiJoin = daiJoinLike(daiJoin_); dai = daiJoin.dai(); + mkrNgt = MkrNgt(mkrNgt_); + mkr = TokenLike(mkrNgt.mkr()); + ngt = TokenLike(mkrNgt.ngt()); dai.approve(daiJoin_, type(uint256).max); } - function _fromWad(TokenLike gem, uint256 wad) internal view returns (uint256 amt) { - amt = wad / 10 ** (18 - gem.decimals()); - } -} - -contract UniswapV2LockstakeCallee is UniswapV2Callee { - constructor(address uniRouter02_, address daiJoin_) { - setUp(uniRouter02_, daiJoin_); + function divup(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x != 0 ? ((x - 1) / y) + 1 : 0; } function clipperCall( @@ -76,11 +76,14 @@ contract UniswapV2LockstakeCallee is UniswapV2Callee { address[] memory path // Uniswap pool path ) = abi.decode(data, (address, uint256, address[])); - // Determine received token - TokenLike gem = TokenLike(path[0]); - - // Convert gem amount to token precision - gemAmt = _fromWad(gem, gemAmt); + // Support NGT + TokenLike gem = mkr; + if (path[0] == address(ngt)) { + gem = ngt; + mkr.approve(address(mkrNgt), gemAmt); + mkrNgt.mkrToNgt(address(this), gemAmt); + gemAmt = gemAmt * mkrNgt.rate(); + } // Approve uniRouter02 to take gem gem.approve(address(uniRouter02), gemAmt); diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 6602880..aa20c7e 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -92,9 +92,10 @@ contract UniswapV2LockstakeCalleeTest is DssTest { address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; MockUniswapRouter02 uniRouter02; + uint256 fixedUniV2Price = 1; + address[] mkrToDaiPath = new address[](2); + address[] ngtToDaiPath = new address[](2); UniswapV2LockstakeCallee callee; - address[] mkrToDaiPath = new address[](2); - address[] ngtToNstPath = new address[](2); event OnKick(address indexed urn, uint256 wad); event OnTake(address indexed urn, address indexed who, uint256 wad); @@ -102,17 +103,16 @@ contract UniswapV2LockstakeCalleeTest is DssTest { modifier setupCallee() { // setup mock of the uniswap router - uint256 fixedUniV2Price = 1; uniRouter02 = new MockUniswapRouter02(fixedUniV2Price); // set uniswap exchange paths mkrToDaiPath[0] = address(mkr); mkrToDaiPath[1] = address(dai); - ngtToNstPath[0] = address(ngt); - ngtToNstPath[1] = address(nst); + ngtToDaiPath[0] = address(ngt); + ngtToDaiPath[1] = address(dai); // deploy callee contract - callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI")); + callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(mkrNgt)); _; } @@ -382,7 +382,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { _testOnTake(true, true); } - function _testCalleeTake(bool withDelegate, bool withStaking, address[] memory path) internal { + function _testCalleeTake(bool withDelegate, bool withStaking, address[] memory path, uint256 rate) internal { // setup urn and force its liquidation address urn = _urnSetUp(withDelegate, withStaking); uint256 id = _forceLiquidation(urn); @@ -409,8 +409,8 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); assertEq(mkr.balanceOf(buyer1), 0, "invalid-final-buyer2-mkr-balance"); - uint256 daiBalanceAfterPartialTake = 20_000 * 10**18 - 20_000 * pip.read() * clip.buf() / RAY; - assertEq(dai.balanceOf(buyer1), daiBalanceAfterPartialTake, "invalid-final-buyer2-dai-balance"); + uint256 daiBalanceAfterTake = (20_000 * 10**18) * rate - 20_000 * pip.read() * clip.buf() / RAY; + assertEq(dai.balanceOf(buyer1), daiBalanceAfterTake, "invalid-final-buyer2-dai-balance"); // setup different buyer to take the rest of the auction address buyer2 = address(222); @@ -435,26 +435,43 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); assertEq(mkr.balanceOf(buyer2), 0, "invalid-final-buyer2-mkr-balance"); - assertEq(dai.balanceOf(profitAddress), 12_000 * 10**18 - 12_000 * pip.read() * clip.buf() / RAY, "invalid-final-profit"); + daiBalanceAfterTake = (12_000 * 10**18) * rate - 12_000 * pip.read() * clip.buf() / RAY; + assertEq(dai.balanceOf(profitAddress), daiBalanceAfterTake, "invalid-final-profit"); } - function testCalleeTakeNoWithStakingNoDelegateMkr() public setupCallee { - _testCalleeTake(false, false, mkrToDaiPath); - } + // --- Callee tests using MKR --- - function testCalleeTakeNoWithStakingNoDelegateNgt() public setupCallee { - _testCalleeTake(false, false, ngtToNstPath); + function testCalleeTakeNoWithStakingNoDelegateMkr() public setupCallee { + _testCalleeTake(false, false, mkrToDaiPath, fixedUniV2Price); } function testCalleeTakeNoWithStakingWithDelegateMkr() public setupCallee { - _testCalleeTake(true, false, mkrToDaiPath); + _testCalleeTake(true, false, mkrToDaiPath, fixedUniV2Price); } function testCalleeTakeWithStakingNoDelegateMkr() public setupCallee { - _testCalleeTake(false, true, mkrToDaiPath); + _testCalleeTake(false, true, mkrToDaiPath, fixedUniV2Price); } function testCalleeTakeWithStakingWithDelegateMkr() public setupCallee { - _testCalleeTake(true, true, mkrToDaiPath); + _testCalleeTake(true, true, mkrToDaiPath, fixedUniV2Price); + } + + // --- Callee tests using NGT --- + + function testCalleeTakeNoWithStakingNoDelegateNgt() public setupCallee { + _testCalleeTake(false, false, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); + } + + function testCalleeTakeNoWithStakingWithDelegateNgt() public setupCallee { + _testCalleeTake(true, false, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); + } + + function testCalleeTakeWithStakingNoDelegateNgt() public setupCallee { + _testCalleeTake(false, true, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); + } + + function testCalleeTakeWithStakingWithDelegateNgt() public setupCallee { + _testCalleeTake(true, true, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); } } From 35cb3aa792c2258e16837b9d8f2e0549088d930d Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Wed, 31 Jul 2024 09:46:09 +0200 Subject: [PATCH 18/25] cleanup tests --- src/test/UniswapV2LockstakeCallee.t.sol | 196 +++++------------------- 1 file changed, 40 insertions(+), 156 deletions(-) diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index aa20c7e..1f373df 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -101,17 +101,17 @@ contract UniswapV2LockstakeCalleeTest is DssTest { event OnTake(address indexed urn, address indexed who, uint256 wad); event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); - modifier setupCallee() { - // setup mock of the uniswap router + modifier setUpCallee() { + // Setup mock of the uniswap router uniRouter02 = new MockUniswapRouter02(fixedUniV2Price); - // set uniswap exchange paths + // Setup uniswap exchange paths mkrToDaiPath[0] = address(mkr); mkrToDaiPath[1] = address(dai); ngtToDaiPath[0] = address(ngt); ngtToDaiPath[1] = address(dai); - // deploy callee contract + // Deploy callee contract callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(mkrNgt)); _; } @@ -265,213 +265,97 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(engine.urnAuctions(urn), 1); } - // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 - function _testOnTake(bool withDelegate, bool withStaking) internal { + // Based on the `_testOnTake` https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 + function _testCalleeTake(bool withDelegate, bool withStaking, address[] memory path, uint256 exchangeRate) internal { + // Setup urn and force its liquidation address urn = _urnSetUp(withDelegate, withStaking); - uint256 mkrInitialSupply = mkr.totalSupply(); - uint256 lsmkrInitialSupply = lsmkr.totalSupply(); - address vow = address(dss.vow); - uint256 vowInitialBalance = dss.vat.dai(vow); uint256 id = _forceLiquidation(urn); - LockstakeClipper.Sale memory sale; - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); - assertEq(sale.pos, 0); - assertEq(sale.tab, 2_000 * 10**45); - assertEq(sale.lot, 100_000 * 10**18); - assertEq(sale.tot, 100_000 * 10**18); - assertEq(sale.usr, address(urn)); - assertEq(sale.tic, block.timestamp); - assertEq(sale.top, pip.read() * (1.25 * 10**9)); - - assertEq(_ink(ilk, urn), 0); - assertEq(_art(ilk, urn), 0); - assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18); - - if (withDelegate) { - assertEq(mkr.balanceOf(voteDelegate), 0); - } - assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18); - if (withStaking) { - assertEq(lsmkr.balanceOf(address(farm)), 0); - assertEq(farm.balanceOf(address(urn)), 0); - } - assertEq(lsmkr.balanceOf(address(urn)), 0); - assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); - - address buyer = address(888); - vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 2_000 * 10**45); - vm.prank(buyer); dss.vat.hope(address(clip)); - assertEq(mkr.balanceOf(buyer), 0); - vm.expectEmit(true, true, true, true); - emit OnTake(urn, buyer, 20_000 * 10**18); - vm.prank(buyer); clip.take(id, 20_000 * 10**18, type(uint256).max, buyer, ""); - assertEq(mkr.balanceOf(buyer), 20_000 * 10**18); - - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); - assertEq(sale.pos, 0); - assertEq(sale.tab, (2_000 - 20_000 * 0.05 * 1.25) * 10**45); - assertEq(sale.lot, 80_000 * 10**18); - assertEq(sale.tot, 100_000 * 10**18); - assertEq(sale.usr, address(urn)); - assertEq(sale.tic, block.timestamp); - assertEq(sale.top, pip.read() * (1.25 * 10**9)); - - assertEq(_ink(ilk, urn), 0); - assertEq(_art(ilk, urn), 0); - assertEq(dss.vat.gem(ilk, address(clip)), 80_000 * 10**18); - - if (withDelegate) { - assertEq(mkr.balanceOf(voteDelegate), 0); - } - assertEq(mkr.balanceOf(address(engine)), 80_000 * 10**18); - if (withStaking) { - assertEq(lsmkr.balanceOf(address(farm)), 0); - assertEq(farm.balanceOf(address(urn)), 0); - } - assertEq(lsmkr.balanceOf(address(urn)), 0); - assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18); - - uint256 burn = 32_000 * 10**18 * engine.fee() / (WAD - engine.fee()); - vm.expectEmit(true, true, true, true); - emit OnTake(urn, buyer, 12_000 * 10**18); - vm.expectEmit(true, true, true, true); - emit OnRemove(urn, 32_000 * 10**18, burn, 100_000 * 10**18 - 32_000 * 10**18 - burn); - vm.prank(buyer); clip.take(id, 12_000 * 10**18, type(uint256).max, buyer, ""); - assertEq(burn, (32_000 * 10**18 + burn) * engine.fee() / WAD); - assertEq(mkr.balanceOf(buyer), 32_000 * 10**18); - assertEq(engine.urnAuctions(urn), 0); - - (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id); - assertEq(sale.pos, 0); - assertEq(sale.tab, 0); - assertEq(sale.lot, 0); - assertEq(sale.tot, 0); - assertEq(sale.usr, address(0)); - assertEq(sale.tic, 0); - assertEq(sale.top, 0); - - assertEq(_ink(ilk, urn), 100_000 * 10**18 - 32_000 * 10**18 - burn); - assertEq(_art(ilk, urn), 0); - assertEq(dss.vat.gem(ilk, address(clip)), 0); - - assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18 - 32_000 * 10**18 - burn); - assertEq(mkr.totalSupply(), mkrInitialSupply - burn); - if (withStaking) { - assertEq(lsmkr.balanceOf(address(farm)), 0); - assertEq(farm.balanceOf(address(urn)), 0); - } - assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18 - 32_000 * 10**18 - burn); - assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 32_000 * 10**18 - burn); - assertEq(dss.vat.dai(vow), vowInitialBalance + 2_000 * 10**45); - } - - function testOnTakeNoWithStakingNoDelegate() public { - _testOnTake(false, false); - } - - function testOnTakeNoWithStakingWithDelegate() public { - _testOnTake(true, false); - } - - function testOnTakeWithStakingNoDelegate() public { - _testOnTake(false, true); - } - - function testOnTakeWithStakingWithDelegate() public { - _testOnTake(true, true); - } - - function _testCalleeTake(bool withDelegate, bool withStaking, address[] memory path, uint256 rate) internal { - // setup urn and force its liquidation - address urn = _urnSetUp(withDelegate, withStaking); - uint256 id = _forceLiquidation(urn); - - // setup buyer + // Setup buyer address buyer1 = address(111); vm.prank(buyer1); dss.vat.hope(address(clip)); - assertEq(mkr.balanceOf(buyer1), 0); - assertEq(dai.balanceOf(buyer1), 0); + assertEq(mkr.balanceOf(buyer1), 0, 'unexpected-mkr-balance'); + assertEq(dai.balanceOf(buyer1), 0, 'unexpected-dai-balance'); - // partially take auction with callee + // Partially take auction with callee + uint256 expectedDaiProfit = (20_000 * 10**18) * exchangeRate - 20_000 * pip.read() * clip.buf() / RAY; bytes memory flashData = abi.encode( - address(buyer1), // Address of the user (where profits are sent) - 0, // Minimum dai profit [wad] - path // Uniswap v2 path + address(buyer1), // Address of the user (where profits are sent) + expectedDaiProfit, // Minimum dai profit [wad] + path // Uniswap v2 path ); vm.prank(buyer1); clip.take( - id, - 20_000 * 10**18, - type(uint256).max, - address(callee), - flashData + id, // Auction id + 20_000 * 10**18, // Upper limit on amount of collateral to buy [wad] + type(uint256).max, // Maximum acceptable price (DAI / collateral) [ray] + address(callee), // Receiver of collateral and external call address + flashData // Data to pass in external call; if length 0, no call is done ); assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); assertEq(mkr.balanceOf(buyer1), 0, "invalid-final-buyer2-mkr-balance"); - uint256 daiBalanceAfterTake = (20_000 * 10**18) * rate - 20_000 * pip.read() * clip.buf() / RAY; - assertEq(dai.balanceOf(buyer1), daiBalanceAfterTake, "invalid-final-buyer2-dai-balance"); + assertEq(dai.balanceOf(buyer1), expectedDaiProfit, "invalid-final-buyer2-dai-balance"); - // setup different buyer to take the rest of the auction + // Setup different buyer to take the rest of the auction address buyer2 = address(222); vm.prank(buyer2); dss.vat.hope(address(clip)); assertEq(mkr.balanceOf(buyer2), 0, "invalid-initial-buyer2-mkr-balance"); assertEq(dai.balanceOf(buyer2), 0, "invalid-initial-buyer2-dai-balance"); address profitAddress = address(333); - // take the rest of the auction with callee + // Take the rest of the auction with callee + expectedDaiProfit = (12_000 * 10**18) * exchangeRate - 12_000 * pip.read() * clip.buf() / RAY; flashData = abi.encode( address(profitAddress), // Address of the user (where profits are sent) - 0, // Minimum dai profit [wad] - path // Uniswap v2 path + expectedDaiProfit, // Minimum dai profit [wad] + path // Uniswap v2 path ); vm.prank(buyer2); clip.take( - id, - type(uint256).max, - type(uint256).max, - address(callee), - flashData + id, // Auction id + type(uint256).max, // Upper limit on amount of collateral to buy [wad] + type(uint256).max, // Maximum acceptable price (DAI / collateral) [ray] + address(callee), // Receiver of collateral and external call address + flashData // Data to pass in external call; if length 0, no call is done ); assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); assertEq(mkr.balanceOf(buyer2), 0, "invalid-final-buyer2-mkr-balance"); - daiBalanceAfterTake = (12_000 * 10**18) * rate - 12_000 * pip.read() * clip.buf() / RAY; - assertEq(dai.balanceOf(profitAddress), daiBalanceAfterTake, "invalid-final-profit"); + assertEq(dai.balanceOf(profitAddress), expectedDaiProfit, "invalid-final-profit"); } // --- Callee tests using MKR --- - function testCalleeTakeNoWithStakingNoDelegateMkr() public setupCallee { + function testCalleeTakeNoWithStakingNoDelegateMkr() public setUpCallee { _testCalleeTake(false, false, mkrToDaiPath, fixedUniV2Price); } - function testCalleeTakeNoWithStakingWithDelegateMkr() public setupCallee { + function testCalleeTakeNoWithStakingWithDelegateMkr() public setUpCallee { _testCalleeTake(true, false, mkrToDaiPath, fixedUniV2Price); } - function testCalleeTakeWithStakingNoDelegateMkr() public setupCallee { + function testCalleeTakeWithStakingNoDelegateMkr() public setUpCallee { _testCalleeTake(false, true, mkrToDaiPath, fixedUniV2Price); } - function testCalleeTakeWithStakingWithDelegateMkr() public setupCallee { + function testCalleeTakeWithStakingWithDelegateMkr() public setUpCallee { _testCalleeTake(true, true, mkrToDaiPath, fixedUniV2Price); } // --- Callee tests using NGT --- - function testCalleeTakeNoWithStakingNoDelegateNgt() public setupCallee { + function testCalleeTakeNoWithStakingNoDelegateNgt() public setUpCallee { _testCalleeTake(false, false, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); } - function testCalleeTakeNoWithStakingWithDelegateNgt() public setupCallee { + function testCalleeTakeNoWithStakingWithDelegateNgt() public setUpCallee { _testCalleeTake(true, false, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingNoDelegateNgt() public setupCallee { + function testCalleeTakeWithStakingNoDelegateNgt() public setUpCallee { _testCalleeTake(false, true, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingWithDelegateNgt() public setupCallee { + function testCalleeTakeWithStakingWithDelegateNgt() public setUpCallee { _testCalleeTake(true, true, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); } } From a6f6d599602c096e342e0da06625e253440ed73e Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Wed, 31 Jul 2024 10:14:03 +0200 Subject: [PATCH 19/25] test minimum dai profit --- src/test/UniswapV2LockstakeCallee.t.sol | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 1f373df..d745eef 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -277,9 +277,26 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(mkr.balanceOf(buyer1), 0, 'unexpected-mkr-balance'); assertEq(dai.balanceOf(buyer1), 0, 'unexpected-dai-balance'); - // Partially take auction with callee + // Partial profit uint256 expectedDaiProfit = (20_000 * 10**18) * exchangeRate - 20_000 * pip.read() * clip.buf() / RAY; + + // Expect revert if minimumDaiProfit set too high bytes memory flashData = abi.encode( + address(buyer1), // Address of the user (where profits are sent) + expectedDaiProfit + 1, // Minimum dai profit [wad] + path // Uniswap v2 path + ); + vm.expectRevert("Minimum Fill not reached"); + vm.prank(buyer1); clip.take( + id, // Auction id + 20_000 * 10**18, // Upper limit on amount of collateral to buy [wad] + type(uint256).max, // Maximum acceptable price (DAI / collateral) [ray] + address(callee), // Receiver of collateral and external call address + flashData // Data to pass in external call; if length 0, no call is done + ); + + // Partially take auction with callee + flashData = abi.encode( address(buyer1), // Address of the user (where profits are sent) expectedDaiProfit, // Minimum dai profit [wad] path // Uniswap v2 path From 84e350153da023d89636e651811676022e1f0076 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Wed, 31 Jul 2024 10:34:29 +0200 Subject: [PATCH 20/25] remove unnecessary changes --- src/test/OneInchCallee.t.sol | 4 ++-- src/test/UniswapV3SplitRouteCallee.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/OneInchCallee.t.sol b/src/test/OneInchCallee.t.sol index 44b86b3..9ba9766 100644 --- a/src/test/OneInchCallee.t.sol +++ b/src/test/OneInchCallee.t.sol @@ -225,10 +225,10 @@ contract OneInchTests is DSTest { joinLink(amountLink); frobMax(amountLink, linkName); try vat.frob(linkName, aliAddr, aliAddr, aliAddr, 0, 1) { - emit log('not at maximum frob'); + log('not at maximum frob'); fail(); } catch { - emit log('success'); + log('success'); } } diff --git a/src/test/UniswapV3SplitRouteCallee.t.sol b/src/test/UniswapV3SplitRouteCallee.t.sol index d6f8b33..b7737d9 100644 --- a/src/test/UniswapV3SplitRouteCallee.t.sol +++ b/src/test/UniswapV3SplitRouteCallee.t.sol @@ -219,10 +219,10 @@ contract UniswapSplitTests is DSTest { joinLink(amountLink); frobMax(amountLink, linkName); try vat.frob(linkName, aliAddr, aliAddr, aliAddr, 0, 1) { - emit log('not at maximum frob'); + log('not at maximum frob'); fail(); } catch { - emit log('success'); + log('success'); } } From 9b5cb11eb194c84b9b23ca46c6b28fd9bcef4c05 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Wed, 31 Jul 2024 16:16:07 +0200 Subject: [PATCH 21/25] support NST as destination token --- src/UniswapV2LockstakeCallee.sol | 51 ++++--- src/test/UniswapV2LockstakeCallee.t.sol | 178 +++++++++++++++--------- 2 files changed, 134 insertions(+), 95 deletions(-) diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol index 2cf50b2..af434f8 100644 --- a/src/UniswapV2LockstakeCallee.sol +++ b/src/UniswapV2LockstakeCallee.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC. -// Copyright (C) 2021 Dai Foundation +// Copyright (C) 2024 Dai Foundation // // 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 @@ -17,8 +17,7 @@ pragma solidity ^0.8.21; -interface daiJoinLike { - function dai() external view returns (TokenLike); +interface JoinLike { function join(address, uint256) external; } @@ -42,22 +41,18 @@ interface MkrNgt { contract UniswapV2LockstakeCallee { UniswapV2Router02Like public immutable uniRouter02; - daiJoinLike public immutable daiJoin; - TokenLike public immutable dai; + JoinLike public immutable dstJoin; MkrNgt public immutable mkrNgt; TokenLike public immutable mkr; TokenLike public immutable ngt; uint256 public constant RAY = 10 ** 27; - constructor(address uniRouter02_, address daiJoin_, address mkrNgt_) { + constructor(address uniRouter02_, address dstJoin_, address mkrNgt_) { uniRouter02 = UniswapV2Router02Like(uniRouter02_); - daiJoin = daiJoinLike(daiJoin_); - dai = daiJoin.dai(); + dstJoin = JoinLike(dstJoin_); mkrNgt = MkrNgt(mkrNgt_); mkr = TokenLike(mkrNgt.mkr()); ngt = TokenLike(mkrNgt.ngt()); - - dai.approve(daiJoin_, type(uint256).max); } function divup(uint256 x, uint256 y) internal pure returns (uint256 z) { @@ -65,15 +60,15 @@ contract UniswapV2LockstakeCallee { } function clipperCall( - address sender, // Clipper Caller and Dai deliveryaddress - uint256 daiAmt, // Dai amount to payback[rad] - uint256 gemAmt, // Gem amount received [wad] - bytes calldata data // Extra data needed (gemJoin) + address sender, // Clipper Caller and DAI/NST delivery address + uint256 dstAmt, // DAI/NST amount to payback[rad] + uint256 gemAmt, // Gem amount received [wad] + bytes calldata data // Extra data needed (gemJoin) ) external { ( - address to, // address to send remaining DAI to - uint256 minProfit, // minimum profit in DAI to make [wad] - address[] memory path // Uniswap pool path + address to, // Address to send remaining DAI/NST to + uint256 minProfit, // Minimum profit in DAI/NST to make [wad] + address[] memory path // Uniswap pool path ) = abi.decode(data, (address, uint256, address[])); // Support NGT @@ -88,29 +83,33 @@ contract UniswapV2LockstakeCallee { // Approve uniRouter02 to take gem gem.approve(address(uniRouter02), gemAmt); - // Calculate amount of DAI to Join (as erc20 WAD value) - uint256 daiToJoin = divup(daiAmt, RAY); + // Calculate amount of tokens to Join (as erc20 WAD value) + uint256 amtToJoin = divup(dstAmt, RAY); - // Do operation and get dai amount bought (checking the profit is achieved) + // Exchange tokens based on the path (checking the profit is achieved) uniRouter02.swapExactTokensForTokens( gemAmt, - daiToJoin + minProfit, + amtToJoin + minProfit, path, address(this), block.timestamp ); // Although Uniswap will accept all gems, this check is a sanity check, just in case - // Transfer any lingering gem to specified address if (gem.balanceOf(address(this)) > 0) { + // Transfer any lingering gem to specified address gem.transfer(to, gem.balanceOf(address(this))); } - // Convert DAI bought to internal vat value of the msg.sender of Clipper.take - daiJoin.join(sender, daiToJoin); + // Determine destination token + TokenLike dst = TokenLike(path[path.length - 1]); + + // Convert tokens bought to internal vat value of the msg.sender of Clipper.take + dst.approve(address(dstJoin), amtToJoin); + dstJoin.join(sender, amtToJoin); - // Transfer remaining DAI to specified address - dai.transfer(to, dai.balanceOf(address(this))); + // Transfer remaining tokens to specified address + dst.transfer(to, dst.balanceOf(address(this))); } } diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index d745eef..ffd3793 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -17,16 +17,7 @@ import { NstMock } from "lib/lockstake/test/mocks/NstMock.sol"; import { NstJoinMock } from "lib/lockstake/test/mocks/NstJoinMock.sol"; import { StakingRewardsMock } from "lib/lockstake/test/mocks/StakingRewardsMock.sol"; import { MkrNgtMock } from "lib/lockstake/test/mocks/MkrNgtMock.sol"; - -import { UniswapV2LockstakeCallee } from "src/UniswapV2LockstakeCallee.sol"; - -interface CalcFabLike { - function newLinearDecrease(address) external returns (address); -} - -interface LineMomLike { - function ilks(bytes32) external view returns (uint256); -} +import { UniswapV2LockstakeCallee, TokenLike } from "src/UniswapV2LockstakeCallee.sol"; interface MkrAuthorityLike { function rely(address) external; @@ -91,31 +82,15 @@ contract UniswapV2LockstakeCalleeTest is DssTest { address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; - MockUniswapRouter02 uniRouter02; - uint256 fixedUniV2Price = 1; - address[] mkrToDaiPath = new address[](2); - address[] ngtToDaiPath = new address[](2); + MockUniswapRouter02 uniRouter02; + uint256 uniV2Price = 1; + address[] uniV2Path = new address[](2); UniswapV2LockstakeCallee callee; event OnKick(address indexed urn, uint256 wad); event OnTake(address indexed urn, address indexed who, uint256 wad); event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); - modifier setUpCallee() { - // Setup mock of the uniswap router - uniRouter02 = new MockUniswapRouter02(fixedUniV2Price); - - // Setup uniswap exchange paths - mkrToDaiPath[0] = address(mkr); - mkrToDaiPath[1] = address(dai); - ngtToDaiPath[0] = address(ngt); - ngtToDaiPath[1] = address(dai); - - // Deploy callee contract - callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(mkrNgt)); - _; - } - // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L87-L177 function setUp() public { vm.createSelectFork(vm.envString("ETH_RPC_URL")); @@ -210,6 +185,14 @@ contract UniswapV2LockstakeCalleeTest is DssTest { stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(nstJoin)).depth(0).checked_write(100_000 * RAD); } + function setUpCallee(address join) internal { + // Setup mock of the uniswap router + uniRouter02 = new MockUniswapRouter02(uniV2Price); + + // Deploy callee contract + callee = new UniswapV2LockstakeCallee(address(uniRouter02), join, address(mkrNgt)); + } + function _ink(bytes32 ilk_, address urn) internal view returns (uint256 ink) { (ink,) = dss.vat.urns(ilk_, urn); } @@ -271,35 +254,38 @@ contract UniswapV2LockstakeCalleeTest is DssTest { address urn = _urnSetUp(withDelegate, withStaking); uint256 id = _forceLiquidation(urn); + // Determine destination token + TokenLike dst = TokenLike(path[path.length - 1]); + // Setup buyer address buyer1 = address(111); vm.prank(buyer1); dss.vat.hope(address(clip)); - assertEq(mkr.balanceOf(buyer1), 0, 'unexpected-mkr-balance'); - assertEq(dai.balanceOf(buyer1), 0, 'unexpected-dai-balance'); + assertEq(mkr.balanceOf(buyer1), 0, 'unexpected-initial-buyer1-mkr-balance'); + assertEq(dst.balanceOf(buyer1), 0, 'unexpected-initial-buyer1-dst-balance'); // Partial profit - uint256 expectedDaiProfit = (20_000 * 10**18) * exchangeRate - 20_000 * pip.read() * clip.buf() / RAY; + uint256 expectedProfit = (20_000 * 10**18) * exchangeRate - 20_000 * pip.read() * clip.buf() / RAY; // Expect revert if minimumDaiProfit set too high bytes memory flashData = abi.encode( - address(buyer1), // Address of the user (where profits are sent) - expectedDaiProfit + 1, // Minimum dai profit [wad] - path // Uniswap v2 path + address(buyer1), // Address of the user (where profits are sent) + expectedProfit + 1, // Minimum DAI/NST profit [wad] + path // Uniswap v2 path ); vm.expectRevert("Minimum Fill not reached"); vm.prank(buyer1); clip.take( - id, // Auction id - 20_000 * 10**18, // Upper limit on amount of collateral to buy [wad] - type(uint256).max, // Maximum acceptable price (DAI / collateral) [ray] - address(callee), // Receiver of collateral and external call address - flashData // Data to pass in external call; if length 0, no call is done + id, // Auction id + 20_000 * 10**18, // Upper limit on amount of collateral to buy [wad] + type(uint256).max, // Maximum acceptable price (DAI / collateral) [ray] + address(callee), // Receiver of collateral and external call address + flashData // Data to pass in external call; if length 0, no call is done ); // Partially take auction with callee flashData = abi.encode( - address(buyer1), // Address of the user (where profits are sent) - expectedDaiProfit, // Minimum dai profit [wad] - path // Uniswap v2 path + address(buyer1), // Address of the user (where profits are sent) + expectedProfit, // Minimum DAI/NST profit [wad] + path // Uniswap v2 path ); vm.prank(buyer1); clip.take( id, // Auction id @@ -309,22 +295,22 @@ contract UniswapV2LockstakeCalleeTest is DssTest { flashData // Data to pass in external call; if length 0, no call is done ); assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); - assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); + assertEq(dst.balanceOf(address(callee)), 0, "invalid-callee-dst-balance"); assertEq(mkr.balanceOf(buyer1), 0, "invalid-final-buyer2-mkr-balance"); - assertEq(dai.balanceOf(buyer1), expectedDaiProfit, "invalid-final-buyer2-dai-balance"); + assertEq(dst.balanceOf(buyer1), expectedProfit, "invalid-final-buyer2-dst-balance"); // Setup different buyer to take the rest of the auction address buyer2 = address(222); vm.prank(buyer2); dss.vat.hope(address(clip)); - assertEq(mkr.balanceOf(buyer2), 0, "invalid-initial-buyer2-mkr-balance"); - assertEq(dai.balanceOf(buyer2), 0, "invalid-initial-buyer2-dai-balance"); + assertEq(mkr.balanceOf(buyer2), 0, "unexpected-initial-buyer1-mkr-balance"); + assertEq(dst.balanceOf(buyer2), 0, "unexpected-initial-buyer1-dst-balance"); address profitAddress = address(333); // Take the rest of the auction with callee - expectedDaiProfit = (12_000 * 10**18) * exchangeRate - 12_000 * pip.read() * clip.buf() / RAY; + expectedProfit = (12_000 * 10**18) * exchangeRate - 12_000 * pip.read() * clip.buf() / RAY; flashData = abi.encode( address(profitAddress), // Address of the user (where profits are sent) - expectedDaiProfit, // Minimum dai profit [wad] + expectedProfit, // Minimum dai profit [wad] path // Uniswap v2 path ); vm.prank(buyer2); clip.take( @@ -335,44 +321,98 @@ contract UniswapV2LockstakeCalleeTest is DssTest { flashData // Data to pass in external call; if length 0, no call is done ); assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); - assertEq(dai.balanceOf(address(callee)), 0, "invalid-callee-dai-balance"); + assertEq(dst.balanceOf(address(callee)), 0, "invalid-callee-dst-balance"); assertEq(mkr.balanceOf(buyer2), 0, "invalid-final-buyer2-mkr-balance"); - assertEq(dai.balanceOf(profitAddress), expectedDaiProfit, "invalid-final-profit"); + assertEq(dst.balanceOf(profitAddress), expectedProfit, "invalid-final-profit"); + } + + // --- Callee tests using MKR/DAI path --- + + function testCalleeTakeNoWithStakingNoDelegateMkrDai() public { + setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(dai); + _testCalleeTake(false, false, uniV2Path, uniV2Price); + } + + function testCalleeTakeNoWithStakingWithDelegateMkrDai() public { + setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(dai); + _testCalleeTake(true, false, uniV2Path, uniV2Price); + } + + function testCalleeTakeWithStakingNoDelegateMkrDai() public { + setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(dai); + _testCalleeTake(false, true, uniV2Path, uniV2Price); + } + + function testCalleeTakeWithStakingWithDelegateMkrDai() public { + setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(dai); + _testCalleeTake(true, true, uniV2Path, uniV2Price); } - // --- Callee tests using MKR --- + // --- Callee tests using MKR/NST path --- - function testCalleeTakeNoWithStakingNoDelegateMkr() public setUpCallee { - _testCalleeTake(false, false, mkrToDaiPath, fixedUniV2Price); + function testCalleeTakeNoWithStakingNoDelegateMkrNst() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(nst); + _testCalleeTake(false, false, uniV2Path, uniV2Price); } - function testCalleeTakeNoWithStakingWithDelegateMkr() public setUpCallee { - _testCalleeTake(true, false, mkrToDaiPath, fixedUniV2Price); + function testCalleeTakeNoWithStakingWithDelegateMkrNst() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(nst); + _testCalleeTake(true, false, uniV2Path, uniV2Price); } - function testCalleeTakeWithStakingNoDelegateMkr() public setUpCallee { - _testCalleeTake(false, true, mkrToDaiPath, fixedUniV2Price); + function testCalleeTakeWithStakingNoDelegateMkrNst() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(nst); + _testCalleeTake(false, true, uniV2Path, uniV2Price); } - function testCalleeTakeWithStakingWithDelegateMkr() public setUpCallee { - _testCalleeTake(true, true, mkrToDaiPath, fixedUniV2Price); + function testCalleeTakeWithStakingWithDelegateMkrNst() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(mkr); + uniV2Path[1] = address(nst); + _testCalleeTake(true, true, uniV2Path, uniV2Price); } - // --- Callee tests using NGT --- + // --- Callee tests using NGT/NST path --- - function testCalleeTakeNoWithStakingNoDelegateNgt() public setUpCallee { - _testCalleeTake(false, false, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); + function testCalleeTakeNoWithStakingNoDelegateNgtDai() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(nst); + _testCalleeTake(false, false, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeNoWithStakingWithDelegateNgt() public setUpCallee { - _testCalleeTake(true, false, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); + function testCalleeTakeNoWithStakingWithDelegateNgtDai() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(nst); + _testCalleeTake(true, false, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingNoDelegateNgt() public setUpCallee { - _testCalleeTake(false, true, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); + function testCalleeTakeWithStakingNoDelegateNgtDai() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(nst); + _testCalleeTake(false, true, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingWithDelegateNgt() public setUpCallee { - _testCalleeTake(true, true, ngtToDaiPath, fixedUniV2Price * mkrNgt.rate()); + function testCalleeTakeWithStakingWithDelegateNgtDai() public { + setUpCallee(address(nstJoin)); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(nst); + _testCalleeTake(true, true, uniV2Path, uniV2Price * mkrNgt.rate()); } } From 10fb07b2815bf810be4ae13f1a1083011a434505 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Wed, 31 Jul 2024 17:09:39 +0200 Subject: [PATCH 22/25] support both NST and DAI without redeploying --- src/UniswapV2LockstakeCallee.sol | 38 +++++++++++++++++++------ src/test/UniswapV2LockstakeCallee.t.sol | 28 +++++++++--------- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol index af434f8..caa468d 100644 --- a/src/UniswapV2LockstakeCallee.sol +++ b/src/UniswapV2LockstakeCallee.sol @@ -17,10 +17,6 @@ pragma solidity ^0.8.21; -interface JoinLike { - function join(address, uint256) external; -} - interface TokenLike { function approve(address, uint256) external; function transfer(address, uint256) external; @@ -32,6 +28,16 @@ interface UniswapV2Router02Like { function swapExactTokensForTokens(uint256, uint256, address[] calldata, address, uint256) external returns (uint[] memory); } +interface DaiJoinLike { + function dai() external view returns (address); + function join(address, uint256) external; +} + +interface NstJoinLike { + function nst() external view returns (address); + function join(address, uint256) external; +} + interface MkrNgt { function mkr() external view returns (address); function ngt() external view returns (address); @@ -41,15 +47,26 @@ interface MkrNgt { contract UniswapV2LockstakeCallee { UniswapV2Router02Like public immutable uniRouter02; - JoinLike public immutable dstJoin; + DaiJoinLike public immutable daiJoin; + TokenLike public immutable dai; + NstJoinLike public immutable nstJoin; + TokenLike public immutable nst; MkrNgt public immutable mkrNgt; TokenLike public immutable mkr; TokenLike public immutable ngt; uint256 public constant RAY = 10 ** 27; - constructor(address uniRouter02_, address dstJoin_, address mkrNgt_) { + constructor(address uniRouter02_, address daiJoin_, address nstJoin_, address mkrNgt_) { uniRouter02 = UniswapV2Router02Like(uniRouter02_); - dstJoin = JoinLike(dstJoin_); + + daiJoin = DaiJoinLike(daiJoin_); + dai = TokenLike(daiJoin.dai()); + dai.approve(daiJoin_, type(uint256).max); + + nstJoin = NstJoinLike(nstJoin_); + nst = TokenLike(nstJoin.nst()); + nst.approve(nstJoin_, type(uint256).max); + mkrNgt = MkrNgt(mkrNgt_); mkr = TokenLike(mkrNgt.mkr()); ngt = TokenLike(mkrNgt.ngt()); @@ -105,8 +122,11 @@ contract UniswapV2LockstakeCallee { TokenLike dst = TokenLike(path[path.length - 1]); // Convert tokens bought to internal vat value of the msg.sender of Clipper.take - dst.approve(address(dstJoin), amtToJoin); - dstJoin.join(sender, amtToJoin); + if (address(dst) == address(dai)) { + daiJoin.join(sender, amtToJoin); + } else if (address(dst) == address(nst)) { + nstJoin.join(sender, amtToJoin); + } // Transfer remaining tokens to specified address dst.transfer(to, dst.balanceOf(address(this))); diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index ffd3793..3764ddd 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -185,12 +185,12 @@ contract UniswapV2LockstakeCalleeTest is DssTest { stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(nstJoin)).depth(0).checked_write(100_000 * RAD); } - function setUpCallee(address join) internal { + function setUpCallee() internal { // Setup mock of the uniswap router uniRouter02 = new MockUniswapRouter02(uniV2Price); // Deploy callee contract - callee = new UniswapV2LockstakeCallee(address(uniRouter02), join, address(mkrNgt)); + callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(nstJoin), address(mkrNgt)); } function _ink(bytes32 ilk_, address urn) internal view returns (uint256 ink) { @@ -329,28 +329,28 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // --- Callee tests using MKR/DAI path --- function testCalleeTakeNoWithStakingNoDelegateMkrDai() public { - setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); _testCalleeTake(false, false, uniV2Path, uniV2Price); } function testCalleeTakeNoWithStakingWithDelegateMkrDai() public { - setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); _testCalleeTake(true, false, uniV2Path, uniV2Price); } function testCalleeTakeWithStakingNoDelegateMkrDai() public { - setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); _testCalleeTake(false, true, uniV2Path, uniV2Price); } function testCalleeTakeWithStakingWithDelegateMkrDai() public { - setUpCallee(dss.chainlog.getAddress("MCD_JOIN_DAI")); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); _testCalleeTake(true, true, uniV2Path, uniV2Price); @@ -359,28 +359,28 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // --- Callee tests using MKR/NST path --- function testCalleeTakeNoWithStakingNoDelegateMkrNst() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); _testCalleeTake(false, false, uniV2Path, uniV2Price); } function testCalleeTakeNoWithStakingWithDelegateMkrNst() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); _testCalleeTake(true, false, uniV2Path, uniV2Price); } function testCalleeTakeWithStakingNoDelegateMkrNst() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); _testCalleeTake(false, true, uniV2Path, uniV2Price); } function testCalleeTakeWithStakingWithDelegateMkrNst() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); _testCalleeTake(true, true, uniV2Path, uniV2Price); @@ -389,28 +389,28 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // --- Callee tests using NGT/NST path --- function testCalleeTakeNoWithStakingNoDelegateNgtDai() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(false, false, uniV2Path, uniV2Price * mkrNgt.rate()); } function testCalleeTakeNoWithStakingWithDelegateNgtDai() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(true, false, uniV2Path, uniV2Price * mkrNgt.rate()); } function testCalleeTakeWithStakingNoDelegateNgtDai() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(false, true, uniV2Path, uniV2Price * mkrNgt.rate()); } function testCalleeTakeWithStakingWithDelegateNgtDai() public { - setUpCallee(address(nstJoin)); + setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(true, true, uniV2Path, uniV2Price * mkrNgt.rate()); From 6a0b414d48d8acc33b22df1b81b98287b3575f30 Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Mon, 5 Aug 2024 12:14:49 +0200 Subject: [PATCH 23/25] adressed review --- src/UniswapV2LockstakeCallee.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/UniswapV2LockstakeCallee.sol b/src/UniswapV2LockstakeCallee.sol index caa468d..415ccf0 100644 --- a/src/UniswapV2LockstakeCallee.sol +++ b/src/UniswapV2LockstakeCallee.sol @@ -21,11 +21,10 @@ interface TokenLike { function approve(address, uint256) external; function transfer(address, uint256) external; function balanceOf(address) external view returns (uint256); - function decimals() external view returns (uint256); } interface UniswapV2Router02Like { - function swapExactTokensForTokens(uint256, uint256, address[] calldata, address, uint256) external returns (uint[] memory); + function swapExactTokensForTokens(uint256, uint256, address[] calldata, address, uint256) external returns (uint256[] memory); } interface DaiJoinLike { @@ -42,7 +41,7 @@ interface MkrNgt { function mkr() external view returns (address); function ngt() external view returns (address); function rate() external view returns (uint256); - function mkrToNgt(address usr, uint256 mkrAmt) external; + function mkrToNgt(address, uint256) external; } contract UniswapV2LockstakeCallee { @@ -78,9 +77,9 @@ contract UniswapV2LockstakeCallee { function clipperCall( address sender, // Clipper Caller and DAI/NST delivery address - uint256 dstAmt, // DAI/NST amount to payback[rad] + uint256 dstAmt, // DAI/NST amount to payback [rad] uint256 gemAmt, // Gem amount received [wad] - bytes calldata data // Extra data needed (gemJoin) + bytes calldata data // Extra data needed ) external { ( address to, // Address to send remaining DAI/NST to From 8ddffec4790413290c8e2662f1c9361e0af13bac Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Tue, 13 Aug 2024 11:27:35 +0200 Subject: [PATCH 24/25] address review --- src/test/UniswapV2LockstakeCallee.t.sol | 47 ++++++++++++++----------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index 3764ddd..d88177f 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -58,6 +58,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { DssInstance dss; address pauseProxy; DSTokenAbstract dai; + address daiJoin; DSTokenAbstract mkr; LockstakeMkr lsmkr; LockstakeEngine engine; @@ -79,7 +80,6 @@ contract UniswapV2LockstakeCalleeTest is DssTest { LockstakeConfig cfg; uint256 prevLine; - address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; MockUniswapRouter02 uniRouter02; @@ -99,7 +99,6 @@ contract UniswapV2LockstakeCalleeTest is DssTest { pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); pip = MedianAbstract(dss.chainlog.getAddress("PIP_MKR")); - dai = DSTokenAbstract(dss.chainlog.getAddress("MCD_DAI")); mkr = DSTokenAbstract(dss.chainlog.getAddress("MCD_GOV")); nst = new NstMock(); nstJoin = new NstJoinMock(address(dss.vat), address(nst)); @@ -185,18 +184,12 @@ contract UniswapV2LockstakeCalleeTest is DssTest { stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(nstJoin)).depth(0).checked_write(100_000 * RAD); } - function setUpCallee() internal { - // Setup mock of the uniswap router - uniRouter02 = new MockUniswapRouter02(uniV2Price); - - // Deploy callee contract - callee = new UniswapV2LockstakeCallee(address(uniRouter02), dss.chainlog.getAddress("MCD_JOIN_DAI"), address(nstJoin), address(mkrNgt)); - } - + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L179-L181 function _ink(bytes32 ilk_, address urn) internal view returns (uint256 ink) { (ink,) = dss.vat.urns(ilk_, urn); } + // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L183-L185 function _art(bytes32 ilk_, address urn) internal view returns (uint256 art) { (, art) = dss.vat.urns(ilk_, urn); } @@ -248,7 +241,20 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(engine.urnAuctions(urn), 1); } - // Based on the `_testOnTake` https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 + // Callee-specific setup function + function setUpCallee() internal { + // set up additional global variables + dai = DSTokenAbstract(dss.chainlog.getAddress("MCD_DAI")); + daiJoin = dss.chainlog.getAddress("MCD_JOIN_DAI"); + + // Setup mock of the uniswap router + uniRouter02 = new MockUniswapRouter02(uniV2Price); + + // Deploy callee contract + callee = new UniswapV2LockstakeCallee(address(uniRouter02), daiJoin, address(nstJoin), address(mkrNgt)); + } + + // Test is based on the `_testOnTake` https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L1104-L1202 function _testCalleeTake(bool withDelegate, bool withStaking, address[] memory path, uint256 exchangeRate) internal { // Setup urn and force its liquidation address urn = _urnSetUp(withDelegate, withStaking); @@ -296,21 +302,22 @@ contract UniswapV2LockstakeCalleeTest is DssTest { ); assertEq(mkr.balanceOf(address(callee)), 0, "invalid-callee-mkr-balance"); assertEq(dst.balanceOf(address(callee)), 0, "invalid-callee-dst-balance"); - assertEq(mkr.balanceOf(buyer1), 0, "invalid-final-buyer2-mkr-balance"); - assertEq(dst.balanceOf(buyer1), expectedProfit, "invalid-final-buyer2-dst-balance"); + assertEq(mkr.balanceOf(buyer1), 0, "invalid-final-buyer1-mkr-balance"); + assertEq(dst.balanceOf(buyer1), expectedProfit, "invalid-final-buyer1-dst-balance"); // Setup different buyer to take the rest of the auction address buyer2 = address(222); vm.prank(buyer2); dss.vat.hope(address(clip)); - assertEq(mkr.balanceOf(buyer2), 0, "unexpected-initial-buyer1-mkr-balance"); - assertEq(dst.balanceOf(buyer2), 0, "unexpected-initial-buyer1-dst-balance"); + assertEq(mkr.balanceOf(buyer2), 0, "unexpected-initial-buyer2-mkr-balance"); + assertEq(dst.balanceOf(buyer2), 0, "unexpected-initial-buyer2-dst-balance"); address profitAddress = address(333); + assertEq(dst.balanceOf(profitAddress), 0, "unexpected-initial-profit-dst-balance"); // Take the rest of the auction with callee expectedProfit = (12_000 * 10**18) * exchangeRate - 12_000 * pip.read() * clip.buf() / RAY; flashData = abi.encode( address(profitAddress), // Address of the user (where profits are sent) - expectedProfit, // Minimum dai profit [wad] + expectedProfit, // Minimum dai profit [wad] path // Uniswap v2 path ); vm.prank(buyer2); clip.take( @@ -388,28 +395,28 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // --- Callee tests using NGT/NST path --- - function testCalleeTakeNoWithStakingNoDelegateNgtDai() public { + function testCalleeTakeNoWithStakingNoDelegateNgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(false, false, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeNoWithStakingWithDelegateNgtDai() public { + function testCalleeTakeNoWithStakingWithDelegateNgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(true, false, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingNoDelegateNgtDai() public { + function testCalleeTakeWithStakingNoDelegateNgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(false, true, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingWithDelegateNgtDai() public { + function testCalleeTakeWithStakingWithDelegateNgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); From e988c494cc148b01392a1ccfa744d01821c626fd Mon Sep 17 00:00:00 2001 From: Valia Fetisov Date: Tue, 13 Aug 2024 11:45:04 +0200 Subject: [PATCH 25/25] addressed nits + cleanup --- src/test/UniswapV2LockstakeCallee.t.sol | 87 ++++++++++++++++--------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/src/test/UniswapV2LockstakeCallee.t.sol b/src/test/UniswapV2LockstakeCallee.t.sol index d88177f..df0c672 100644 --- a/src/test/UniswapV2LockstakeCallee.t.sol +++ b/src/test/UniswapV2LockstakeCallee.t.sol @@ -33,7 +33,7 @@ contract MockUniswapRouter02 is DssTest { // Hardcoded to simulate fixed price Uniswap /* uniRouter02.swapExactTokensForTokens(gemAmt, daiToJoin + minProfit, path, address(this), block.timestamp); */ function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts) { - to; deadline; // silence warning + deadline; // silence warning uint buyAmt = amountIn * fixedPrice; require(amountOutMin <= buyAmt, "Minimum Fill not reached"); @@ -41,9 +41,9 @@ contract MockUniswapRouter02 is DssTest { DSTokenAbstract(path[0]).transferFrom(msg.sender, address(this), amountIn); assertEq(DSTokenAbstract(path[0]).balanceOf(address(this)), initialInBalance + amountIn); - uint256 initialOutBalance = DSTokenAbstract(path[path.length - 1]).balanceOf(msg.sender); - GodMode.setBalance(path[path.length - 1], msg.sender, initialOutBalance + buyAmt); - assertEq(DSTokenAbstract(path[path.length - 1]).balanceOf(msg.sender), initialOutBalance + buyAmt); + uint256 initialOutBalance = DSTokenAbstract(path[path.length - 1]).balanceOf(to); + GodMode.setBalance(path[path.length - 1], to, initialOutBalance + buyAmt); + assertEq(DSTokenAbstract(path[path.length - 1]).balanceOf(to), initialOutBalance + buyAmt); amounts = new uint[](2); amounts[0] = amountIn; @@ -57,8 +57,6 @@ contract UniswapV2LockstakeCalleeTest is DssTest { DssInstance dss; address pauseProxy; - DSTokenAbstract dai; - address daiJoin; DSTokenAbstract mkr; LockstakeMkr lsmkr; LockstakeEngine engine; @@ -76,20 +74,19 @@ contract UniswapV2LockstakeCalleeTest is DssTest { bytes32 ilk = "LSE"; address voter; address voteDelegate; + LockstakeConfig cfg; + uint256 prevLine; + address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; - LockstakeConfig cfg; - - uint256 prevLine; - address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; - + // Callee-specific variables + DSTokenAbstract dai; + address daiJoin; MockUniswapRouter02 uniRouter02; uint256 uniV2Price = 1; address[] uniV2Path = new address[](2); UniswapV2LockstakeCallee callee; event OnKick(address indexed urn, uint256 wad); - event OnTake(address indexed urn, address indexed who, uint256 wad); - event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund); // Match https://github.com/makerdao/lockstake/blob/735e1e85ca706534a77d8e1582df0d3248cbd2b6/test/LockstakeEngine.t.sol#L87-L177 function setUp() public { @@ -270,7 +267,8 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(dst.balanceOf(buyer1), 0, 'unexpected-initial-buyer1-dst-balance'); // Partial profit - uint256 expectedProfit = (20_000 * 10**18) * exchangeRate - 20_000 * pip.read() * clip.buf() / RAY; + uint256 amtToBuy = 20_000; + uint256 expectedProfit = (amtToBuy * 10**18) * exchangeRate - amtToBuy * pip.read() * clip.buf() / RAY; // Expect revert if minimumDaiProfit set too high bytes memory flashData = abi.encode( @@ -281,7 +279,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { vm.expectRevert("Minimum Fill not reached"); vm.prank(buyer1); clip.take( id, // Auction id - 20_000 * 10**18, // Upper limit on amount of collateral to buy [wad] + amtToBuy * 10**18, // Upper limit on amount of collateral to buy [wad] type(uint256).max, // Maximum acceptable price (DAI / collateral) [ray] address(callee), // Receiver of collateral and external call address flashData // Data to pass in external call; if length 0, no call is done @@ -295,7 +293,7 @@ contract UniswapV2LockstakeCalleeTest is DssTest { ); vm.prank(buyer1); clip.take( id, // Auction id - 20_000 * 10**18, // Upper limit on amount of collateral to buy [wad] + amtToBuy * 10**18, // Upper limit on amount of collateral to buy [wad] type(uint256).max, // Maximum acceptable price (DAI / collateral) [ray] address(callee), // Receiver of collateral and external call address flashData // Data to pass in external call; if length 0, no call is done @@ -314,7 +312,8 @@ contract UniswapV2LockstakeCalleeTest is DssTest { assertEq(dst.balanceOf(profitAddress), 0, "unexpected-initial-profit-dst-balance"); // Take the rest of the auction with callee - expectedProfit = (12_000 * 10**18) * exchangeRate - 12_000 * pip.read() * clip.buf() / RAY; + amtToBuy = 12_000; + expectedProfit = (amtToBuy * 10**18) * exchangeRate - amtToBuy * pip.read() * clip.buf() / RAY; flashData = abi.encode( address(profitAddress), // Address of the user (where profits are sent) expectedProfit, // Minimum dai profit [wad] @@ -335,28 +334,28 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // --- Callee tests using MKR/DAI path --- - function testCalleeTakeNoWithStakingNoDelegateMkrDai() public { + function testCalleeTake_NoDelegate_NoStaking_MkrDai() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); _testCalleeTake(false, false, uniV2Path, uniV2Price); } - function testCalleeTakeNoWithStakingWithDelegateMkrDai() public { + function testCalleeTake_WithDelegate_NoStaking_MkrDai() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); _testCalleeTake(true, false, uniV2Path, uniV2Price); } - function testCalleeTakeWithStakingNoDelegateMkrDai() public { + function testCalleeTake_NoDelegate_WithStaking_MkrDai() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); _testCalleeTake(false, true, uniV2Path, uniV2Price); } - function testCalleeTakeWithStakingWithDelegateMkrDai() public { + function testCalleeTake_WithDelegate_WithStaking_MkrDai() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(dai); @@ -365,28 +364,28 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // --- Callee tests using MKR/NST path --- - function testCalleeTakeNoWithStakingNoDelegateMkrNst() public { + function testCalleeTake_NoDelegate_NoStaking_MkrNst() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); _testCalleeTake(false, false, uniV2Path, uniV2Price); } - function testCalleeTakeNoWithStakingWithDelegateMkrNst() public { + function testCalleeTake_WithDelegate_NoStaking_MkrNst() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); _testCalleeTake(true, false, uniV2Path, uniV2Price); } - function testCalleeTakeWithStakingNoDelegateMkrNst() public { + function testCalleeTake_NoDelegate_WithStaking_MkrNst() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); _testCalleeTake(false, true, uniV2Path, uniV2Price); } - function testCalleeTakeWithStakingWithDelegateMkrNst() public { + function testCalleeTake_WithDelegate_WithStaking_MkrNst() public { setUpCallee(); uniV2Path[0] = address(mkr); uniV2Path[1] = address(nst); @@ -395,31 +394,61 @@ contract UniswapV2LockstakeCalleeTest is DssTest { // --- Callee tests using NGT/NST path --- - function testCalleeTakeNoWithStakingNoDelegateNgtNst() public { + function testCalleeTake_NoDelegate_NoStaking_NgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(false, false, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeNoWithStakingWithDelegateNgtNst() public { + function testCalleeTake_WithDelegate_NoStaking_NgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(true, false, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingNoDelegateNgtNst() public { + function testCalleeTake_NoDelegate_WithStaking_NgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(false, true, uniV2Path, uniV2Price * mkrNgt.rate()); } - function testCalleeTakeWithStakingWithDelegateNgtNst() public { + function testCalleeTake_WithDelegate_WithStaking_NgtNst() public { setUpCallee(); uniV2Path[0] = address(ngt); uniV2Path[1] = address(nst); _testCalleeTake(true, true, uniV2Path, uniV2Price * mkrNgt.rate()); } + + // --- Callee tests using NGT/DAI path --- + + function testCalleeTake_NoDelegate_NoStaking_NgtDai() public { + setUpCallee(); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(dai); + _testCalleeTake(false, false, uniV2Path, uniV2Price * mkrNgt.rate()); + } + + function testCalleeTake_WithDelegate_NoStaking_NgtDai() public { + setUpCallee(); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(dai); + _testCalleeTake(true, false, uniV2Path, uniV2Price * mkrNgt.rate()); + } + + function testCalleeTake_NoDelegate_WithStaking_NgtDai() public { + setUpCallee(); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(dai); + _testCalleeTake(false, true, uniV2Path, uniV2Price * mkrNgt.rate()); + } + + function testCalleeTake_WithDelegate_WithStaking_NgtDai() public { + setUpCallee(); + uniV2Path[0] = address(ngt); + uniV2Path[1] = address(dai); + _testCalleeTake(true, true, uniV2Path, uniV2Price * mkrNgt.rate()); + } }