Skip to content

Commit

Permalink
e2e with inToken positive slippage
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielZlotin committed Jan 31, 2024
1 parent cba82be commit b174ec6
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 26 deletions.
2 changes: 1 addition & 1 deletion script/base/Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion src/LiquidityHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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)) {
Expand Down
128 changes: 128 additions & 0 deletions test/E2E.t.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
14 changes: 12 additions & 2 deletions test/LiquidityHub.execute.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
3 changes: 1 addition & 2 deletions test/PartialOrderReactor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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
);
}

Expand Down
57 changes: 37 additions & 20 deletions test/base/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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));
}
}

0 comments on commit b174ec6

Please sign in to comment.