diff --git a/script/base/Base.sol b/script/base/Base.sol index 5db3cdc..e69502d 100644 --- a/script/base/Base.sol +++ b/script/base/Base.sol @@ -108,7 +108,7 @@ abstract contract Base is Script, DeployTestInfra { ), PartialOrderLib.hash(order), PartialOrderLib.WITNESS_TYPE, - address(config.reactor) + address(config.reactorPartial) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); diff --git a/src/LiquidityHub.sol b/src/LiquidityHub.sol index 36f4912..7a3cf60 100644 --- a/src/LiquidityHub.sol +++ b/src/LiquidityHub.sol @@ -44,7 +44,7 @@ contract LiquidityHub is IReactorCallback, IValidationCallback { } /** - * Entry point + * Entry point, always DutchOrder */ function execute(SignedOrder[] calldata orders, Call[] calldata calls) external onlyAllowed { reactor.executeBatchWithCallback(orders, abi.encode(calls)); @@ -83,6 +83,11 @@ contract LiquidityHub is IReactorCallback, IValidationCallback { function _withdrawSlippage(ResolvedOrder[] memory orders) private { for (uint256 i = 0; i < orders.length; i++) { ResolvedOrder memory order = orders[i]; + + IERC20 inToken = IERC20(address(order.input.token)); + uint256 inBalance = inToken.balanceOf(address(this)); + if (inBalance > 0) inToken.safeTransfer(feeRecipient, inBalance); + for (uint256 j = 0; j < order.outputs.length; j++) { address token = order.outputs[j].token; if (token != address(0)) { diff --git a/test/E2E.t.sol b/test/E2E.t.sol new file mode 100644 index 0000000..5a0cdda --- /dev/null +++ b/test/E2E.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.x; + +import "forge-std/Test.sol"; + +import {BaseTest, ERC20Mock, IERC20, IWETH, SignedOrder, Consts} from "test/base/BaseTest.sol"; + +import {PartialOrderReactor, RePermit, Call, BaseReactor} from "src/PartialOrderReactor.sol"; + +contract E2ETest is BaseTest { + address public taker; + uint256 public takerPK; + address public maker; + uint256 public makerPK; + ERC20Mock public weth; + ERC20Mock public usdc; + uint256 public wethTakerStartBalance = 10 ether; + uint256 public usdcMakerStartBalance = 25_000 * 10 ** 6; + + function setUp() public override { + super.setUp(); + (taker, takerPK) = makeAddrAndKey("taker"); + (maker, makerPK) = makeAddrAndKey("maker"); + + weth = new ERC20Mock(); + usdc = new ERC20Mock(); + vm.label(address(weth), "weth"); + vm.label(address(usdc), "usdc"); + + weth.mint(taker, wethTakerStartBalance); + usdc.mint(maker, usdcMakerStartBalance); + + hoax(taker); + weth.approve(Consts.PERMIT2_ADDRESS, type(uint256).max); + + hoax(maker); + usdc.approve(address(config.repermit), type(uint256).max); + } + + function test_e2e_ExactMirrorMatch() public { + uint256 wethTakerAmount = 1 ether; // taker input, selling 1 eth + uint256 usdcTakerAmount = 2500 * 10 ** 6; // taker output, buying 2500 usdc + // $2500 + + uint256 usdcMakerAmount = 2510 * 10 ** 6; // maker input, selling 2510 usdc + uint256 wethMakerAmount = 1 ether; // maker output, buying 1 eth + // $2510 + + uint256 usdcAmountGas = 1 * 10 ** 6; // 1 usdc gas fee, from maker's output + + SignedOrder memory takerOrder = createAndSignOrder( + taker, takerPK, address(weth), address(usdc), wethTakerAmount, usdcTakerAmount, usdcAmountGas + ); + + SignedOrder memory makerOrder = createAndSignPartialOrder( + maker, makerPK, address(usdc), address(weth), usdcMakerAmount, usdcMakerAmount, wethMakerAmount + ); + + SignedOrder[] memory orders = new SignedOrder[](1); + orders[0] = takerOrder; + Call[] memory calls = new Call[](2); + calls[0] = Call({ + target: address(weth), + callData: abi.encodeWithSelector(IERC20.approve.selector, address(config.reactorPartial), wethMakerAmount) + }); + calls[1] = Call({ + target: address(config.reactorPartial), + callData: abi.encodeWithSelector(BaseReactor.execute.selector, makerOrder) + }); + hoax(config.treasury.owner()); + config.executor.execute(orders, calls); + + assertEq(weth.balanceOf(taker), wethTakerStartBalance - wethTakerAmount, "weth taker balance"); + assertEq(usdc.balanceOf(taker), usdcTakerAmount, "usdc taker balance"); + assertEq(weth.balanceOf(maker), wethTakerAmount, "weth maker balance"); + assertEq(usdc.balanceOf(maker), usdcMakerStartBalance - usdcMakerAmount, "usdc maker balance"); + assertEq(usdc.balanceOf(address(config.treasury)), usdcAmountGas, "gas fee"); + assertEq(weth.balanceOf(address(config.executor)), 0, "no weth leftovers"); + assertEq(usdc.balanceOf(address(config.executor)), 0, "no usdc leftovers"); + assertEq(usdc.balanceOf(config.executor.feeRecipient()), 9 * 10 ** 6, "usdc positive slippage"); + assertEq(weth.balanceOf(config.executor.feeRecipient()), 0, "weth positive slippage"); + } + + function test_e2e_PartialInputMatch() public { + uint256 wethTakerAmount = 1 ether; // taker input, selling 1 eth + uint256 usdcTakerAmount = 2500 * 10 ** 6; // taker output, buying 2500 usdc + // $2500 + + uint256 usdcMakerAmount = 2510 * 10 ** 6; // maker input, selling 2510 usdc + uint256 wethMakerAmount = 1 ether; // maker output, buying 1 eth + // $2510 + + uint256 usdcAmountGas = 1 * 10 ** 6; // 1 usdc gas fee, from maker's output + + SignedOrder memory takerOrder = createAndSignOrder( + taker, takerPK, address(weth), address(usdc), wethTakerAmount, usdcTakerAmount, usdcAmountGas + ); + + // $3000 + SignedOrder memory makerOrder = createAndSignPartialOrder( + maker, makerPK, address(usdc), address(weth), 3000 * 10 ** 6, usdcMakerAmount, wethMakerAmount + ); + + SignedOrder[] memory orders = new SignedOrder[](1); + orders[0] = takerOrder; + Call[] memory calls = new Call[](2); + calls[0] = Call({ + target: address(weth), + callData: abi.encodeWithSelector(IERC20.approve.selector, address(config.reactorPartial), wethMakerAmount) + }); + calls[1] = Call({ + target: address(config.reactorPartial), + callData: abi.encodeWithSelector(BaseReactor.execute.selector, makerOrder) + }); + hoax(config.treasury.owner()); + config.executor.execute(orders, calls); + + assertEq(weth.balanceOf(taker), wethTakerStartBalance - wethTakerAmount, "weth taker balance"); + assertEq(usdc.balanceOf(taker), usdcTakerAmount, "usdc taker balance"); + assertEq(weth.balanceOf(maker), 0.836666666666666666 ether, "maker bought 0.8366 eth"); + assertEq(usdcMakerStartBalance - usdc.balanceOf(maker), 2510 * 10 ** 6, "maker paid $2510"); + assertEq(usdc.balanceOf(address(config.treasury)), usdcAmountGas, "gas fee"); + assertEq(weth.balanceOf(address(config.executor)), 0, "no weth leftovers"); + assertEq(usdc.balanceOf(address(config.executor)), 0, "no usdc leftovers"); + assertEq(usdc.balanceOf(config.executor.feeRecipient()), 9 * 10 ** 6, "usdc positive slippage"); + assertEq(weth.balanceOf(config.executor.feeRecipient()), 0.163333333333333334 ether, "weth positive slippage"); + } +} diff --git a/test/LiquidityHub.execute.t.sol b/test/LiquidityHub.execute.t.sol index 8651057..758cc0b 100644 --- a/test/LiquidityHub.execute.t.sol +++ b/test/LiquidityHub.execute.t.sol @@ -175,6 +175,7 @@ contract LiquidityHubExecuteTest is BaseTest { function test_GasToTreasury() public { ERC20Mock inToken = new ERC20Mock(); + ERC20Mock outToken = new ERC20Mock(); uint256 inAmount = 1 ether; uint256 outAmount = 0.5 ether; uint256 outAmountGas = 0.25 ether; @@ -184,13 +185,22 @@ contract LiquidityHubExecuteTest is BaseTest { SignedOrder[] memory orders = new SignedOrder[](1); orders[0] = createAndSignOrder( - swapper, swapperPK, address(inToken), address(inToken), inAmount, outAmount, outAmountGas + swapper, swapperPK, address(inToken), address(outToken), inAmount, outAmount, outAmountGas ); inToken.mint(swapper, inAmount); + Call[] memory calls = new Call[](2); + calls[0] = Call({ + target: address(outToken), + callData: abi.encodeWithSelector(ERC20Mock.mint.selector, address(config.executor), outAmount + outAmountGas) + }); + calls[1] = Call({ + target: address(inToken), + callData: abi.encodeWithSelector(ERC20Mock.burn.selector, address(config.executor), inAmount) + }); hoax(config.treasury.owner()); - config.executor.execute(orders, new Call[](0)); + config.executor.execute(orders, calls); assertEq(inToken.balanceOf(swapper), outAmount); assertEq(inToken.balanceOf(address(config.executor)), 0); diff --git a/test/PartialOrderReactor.t.sol b/test/PartialOrderReactor.t.sol index 5820c9d..b3dde7d 100644 --- a/test/PartialOrderReactor.t.sol +++ b/test/PartialOrderReactor.t.sol @@ -16,7 +16,6 @@ contract PartialOrderReactorTest is BaseTest { function setUp() public override { super.setUp(); - vm.etch(address(config.reactor), address(config.reactorPartial).code); (swapper, swapperPK) = makeAddrAndKey("swapper"); @@ -148,7 +147,7 @@ contract PartialOrderReactorTest is BaseTest { ) internal view returns (SignedOrder[] memory orders) { orders = new SignedOrder[](1); orders[0] = createAndSignPartialOrder( - swapper, swapperPK, address(inToken), address(outToken), inAmount, inAmountRequest, outAmount, outAmountGas + swapper, swapperPK, address(inToken), address(outToken), inAmount, inAmountRequest, outAmount ); } diff --git a/test/base/BaseTest.sol b/test/base/BaseTest.sol index 6124bcd..7f0ee77 100644 --- a/test/base/BaseTest.sol +++ b/test/base/BaseTest.sol @@ -18,7 +18,14 @@ import { import {Base, Config} from "script/base/Base.sol"; import { - LiquidityHub, Consts, IMulticall, IReactor, IERC20, SignedOrder, IValidationCallback + LiquidityHub, + Consts, + IMulticall, + IReactor, + IERC20, + SignedOrder, + IValidationCallback, + Call } from "src/LiquidityHub.sol"; import {Treasury, IWETH} from "src/Treasury.sol"; import {PartialOrderLib} from "src/PartialOrderReactor.sol"; @@ -36,8 +43,8 @@ abstract contract BaseTest is Base, PermitSignature { } function createAndSignOrder( - address swapper, - uint256 privateKey, + address signer, + uint256 signerPK, address inToken, address outToken, uint256 inAmount, @@ -47,7 +54,7 @@ abstract contract BaseTest is Base, PermitSignature { ExclusiveDutchOrder memory order; { order.info.reactor = config.reactor; - order.info.swapper = swapper; + order.info.swapper = signer; order.info.nonce = block.timestamp; order.info.deadline = block.timestamp + 10 minutes; order.decayStartTime = order.info.deadline; @@ -61,42 +68,52 @@ abstract contract BaseTest is Base, PermitSignature { order.input.endAmount = inAmount; order.outputs = new DutchOutput[](2); - order.outputs[0] = DutchOutput(outToken, outAmount, outAmount, swapper); + order.outputs[0] = DutchOutput(outToken, outAmount, outAmount, signer); order.outputs[1] = DutchOutput(outToken, outAmountGas, outAmountGas, address(config.treasury)); } - result.sig = signOrder(privateKey, PERMIT2_ADDRESS, order); + result.sig = signOrder(signerPK, PERMIT2_ADDRESS, order); result.order = abi.encode(order); } function createAndSignPartialOrder( - address swapper, - uint256 privateKey, + address signer, + uint256 signerPK, address inToken, address outToken, - uint256 orderAmount, - uint256 partialOrderAmount, - uint256 outAmount, - uint256 outAmountGas + uint256 inMaxAmount, + uint256 inPartialAmount, + uint256 outAmount ) internal view returns (SignedOrder memory result) { PartialOrderLib.PartialOrder memory order; { - order.info.reactor = config.reactor; - order.info.swapper = swapper; + order.info.reactor = config.reactorPartial; + order.info.swapper = signer; order.info.nonce = block.timestamp; order.info.deadline = block.timestamp + 10 minutes; order.exclusiveFiller = address(config.executor); + // order.info.additionalValidationContract = IValidationCallback(config.executor); // this will work, but redundant and wastes gas order.input.token = inToken; - order.input.amount = orderAmount; + order.input.amount = inMaxAmount; - order.outputs = new PartialOrderLib.PartialOutput[](2); - order.outputs[0] = PartialOrderLib.PartialOutput(outToken, outAmount, swapper); - order.outputs[1] = PartialOrderLib.PartialOutput(outToken, outAmountGas, address(config.treasury)); + order.outputs = new PartialOrderLib.PartialOutput[](1); + order.outputs[0] = PartialOrderLib.PartialOutput(outToken, outAmount, signer); } - result.sig = signRePermit(privateKey, order); - result.order = abi.encode(order, partialOrderAmount); + result.sig = signRePermit(signerPK, order); + result.order = abi.encode(order, inPartialAmount); + } + + function mockSwapCalls(ERC20Mock inToken, ERC20Mock outToken, uint256 inAmount, uint256 outAmount) + internal + returns (Call[] memory calls) + { + calls = new Call[](2); + calls[0] = + Call(address(inToken), abi.encodeWithSelector(inToken.burn.selector, address(config.executor), inAmount)); + calls[1] = + Call(address(outToken), abi.encodeWithSelector(outToken.mint.selector, address(config.executor), outAmount)); } }