Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate GPv2Settlement.swap variant tests to Foundry #194

Merged
merged 13 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 2 additions & 175 deletions test/GPv2Settlement.test.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
import IERC20 from "@openzeppelin/contracts/build/contracts/IERC20.json";
import { expect } from "chai";
import { MockContract } from "ethereum-waffle";
import { Contract } from "ethers";
import { artifacts, ethers, waffle } from "hardhat";

import {
OrderBalance,
OrderKind,
PRE_SIGNED,
SigningScheme,
SwapEncoder,
SwapExecution,
TypedDataDomain,
computeOrderUid,
domain,
packOrderUidParams,
} from "../src/ts";

function fillBytes(count: number, byte: number): string {
return ethers.utils.hexlify([...Array(count)].map(() => byte));
}
import { PRE_SIGNED, packOrderUidParams } from "../src/ts";

describe("GPv2Settlement", () => {
const [deployer, owner, solver, ...traders] = waffle.provider.getWallets();
const [deployer, owner, ...traders] = waffle.provider.getWallets();

let authenticator: Contract;
let vault: MockContract;
let settlement: Contract;
let testDomain: TypedDataDomain;

beforeEach(async () => {
const GPv2AllowListAuthentication = await ethers.getContractFactory(
Expand All @@ -48,162 +31,6 @@ describe("GPv2Settlement", () => {
authenticator.address,
vault.address,
);

const { chainId } = await ethers.provider.getNetwork();
testDomain = domain(chainId, settlement.address);
});

describe("swap", () => {
let alwaysSuccessfulTokens: [Contract, Contract];

before(async () => {
alwaysSuccessfulTokens = [
await waffle.deployMockContract(deployer, IERC20.abi),
await waffle.deployMockContract(deployer, IERC20.abi),
];
for (const token of alwaysSuccessfulTokens) {
await token.mock.transfer.returns(true);
await token.mock.transferFrom.returns(true);
}
});

describe("Swap Variants", () => {
const sellAmount = ethers.utils.parseEther("4.2");
const buyAmount = ethers.utils.parseEther("13.37");

for (const kind of [OrderKind.SELL, OrderKind.BUY]) {
const order = {
kind,
sellToken: fillBytes(20, 1),
buyToken: fillBytes(20, 2),
sellAmount,
buyAmount,
validTo: 0x01020304,
appData: 0,
feeAmount: ethers.utils.parseEther("1.0"),
sellTokenBalance: OrderBalance.INTERNAL,
partiallyFillable: true,
};
const orderUid = () =>
computeOrderUid(testDomain, order, traders[0].address);
const encodeSwap = (swapExecution?: Partial<SwapExecution>) =>
SwapEncoder.encodeSwap(
testDomain,
[],
order,
traders[0],
SigningScheme.ETHSIGN,
swapExecution,
);

it(`executes ${kind} order against swap`, async () => {
const [swaps, tokens, trade] = await encodeSwap();

await vault.mock.batchSwap.returns([sellAmount, buyAmount.mul(-1)]);
await vault.mock.manageUserBalance.returns();

await authenticator.connect(owner).addSolver(solver.address);
await expect(settlement.connect(solver).swap(swaps, tokens, trade)).to
.not.be.reverted;
});

it(`updates the filled amount to be the full ${kind} amount`, async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const filledAmount = (order as any)[`${kind}Amount`];

await vault.mock.batchSwap.returns([sellAmount, buyAmount.mul(-1)]);
await vault.mock.manageUserBalance.returns();

await authenticator.connect(owner).addSolver(solver.address);
await settlement.connect(solver).swap(...(await encodeSwap()));

expect(await settlement.filledAmount(orderUid())).to.equal(
filledAmount,
);
});

it(`reverts for cancelled ${kind} orders`, async () => {
await vault.mock.batchSwap.returns([0, 0]);
await vault.mock.manageUserBalance.returns();

await settlement.connect(traders[0]).invalidateOrder(orderUid());
await authenticator.connect(owner).addSolver(solver.address);
await expect(
settlement.connect(solver).swap(...(await encodeSwap())),
).to.be.revertedWith("order filled");
});

it(`reverts for partially filled ${kind} orders`, async () => {
await vault.mock.batchSwap.returns([0, 0]);
await vault.mock.manageUserBalance.returns();

await settlement.setFilledAmount(orderUid(), 1);
await authenticator.connect(owner).addSolver(solver.address);
await expect(
settlement.connect(solver).swap(...(await encodeSwap())),
).to.be.revertedWith("order filled");
});

it(`reverts when not exactly trading ${kind} amount`, async () => {
await vault.mock.batchSwap.returns([
sellAmount.sub(1),
buyAmount.add(1).mul(-1),
]);
await vault.mock.manageUserBalance.returns();

await authenticator.connect(owner).addSolver(solver.address);
await expect(
settlement.connect(solver).swap(...(await encodeSwap())),
).to.be.revertedWith(`${kind} amount not respected`);
});

it(`reverts when specified limit amount does not satisfy ${kind} price`, async () => {
const [swaps, tokens, trade] = await encodeSwap({
// Specify a swap limit amount that is slightly worse than the
// order's limit price.
limitAmount:
kind == OrderKind.SELL
? order.buyAmount.sub(1) // receive slightly less buy token
: order.sellAmount.add(1), // pay slightly more sell token
});

await vault.mock.batchSwap.returns([sellAmount, buyAmount.mul(-1)]);
await vault.mock.manageUserBalance.returns();

await authenticator.connect(owner).addSolver(solver.address);
await expect(
settlement.connect(solver).swap(swaps, tokens, trade),
).to.be.revertedWith(
kind == OrderKind.SELL ? "limit too low" : "limit too high",
);
});

it(`emits a ${kind} trade event`, async () => {
const [executedSellAmount, executedBuyAmount] =
kind == OrderKind.SELL
? [order.sellAmount, order.buyAmount.mul(2)]
: [order.sellAmount.div(2), order.buyAmount];
await vault.mock.batchSwap.returns([
executedSellAmount,
executedBuyAmount.mul(-1),
]);
await vault.mock.manageUserBalance.returns();

await authenticator.connect(owner).addSolver(solver.address);
await expect(settlement.connect(solver).swap(...(await encodeSwap())))
.to.emit(settlement, "Trade")
.withArgs(
traders[0].address,
order.sellToken,
order.buyToken,
executedSellAmount,
executedBuyAmount,
order.feeAmount,
orderUid(),
);
});
}
});
});

describe("Order Refunds", () => {
Expand Down
177 changes: 177 additions & 0 deletions test/GPv2Settlement/Swap/Variants.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity ^0.8;

import {GPv2Order, GPv2Settlement, GPv2Signing, IERC20, IVault} from "src/contracts/GPv2Settlement.sol";

import {Helper} from "../Helper.sol";

import {Order} from "test/libraries/Order.sol";
import {SwapEncoder} from "test/libraries/encoders/SwapEncoder.sol";

abstract contract Variant is Helper {
using SwapEncoder for SwapEncoder.State;

IERC20 private sellToken = IERC20(makeAddr("GPv2Settlement.Swap.Variants sell token"));
IERC20 private buyToken = IERC20(makeAddr("GPv2Settlement.Swap.Variants buy token"));

uint256 constant sellAmount = 4.2 ether;
uint256 constant buyAmount = 13.37 ether;

bytes32 immutable kind;

constructor(bytes32 _kind) {
kind = _kind;
}

function defaultOrder() private view returns (GPv2Order.Data memory) {
return GPv2Order.Data({
sellToken: sellToken,
buyToken: buyToken,
sellAmount: sellAmount,
buyAmount: buyAmount,
receiver: address(0),
validTo: 0x01020304,
appData: keccak256("GPv2Settlement.Swap.Variants default app data"),
feeAmount: 1 ether,
sellTokenBalance: GPv2Order.BALANCE_INTERNAL,
buyTokenBalance: GPv2Order.BALANCE_ERC20,
partiallyFillable: true,
kind: kind
});
}

function defaultOrderUid() private view returns (bytes memory) {
return Order.computeOrderUid(defaultOrder(), domainSeparator, trader.addr);
}

function encodedDefaultSwap() private returns (SwapEncoder.EncodedSwap memory) {
return encodedDefaultSwap(0);
}

function encodedDefaultSwap(uint256 executedAmount) private returns (SwapEncoder.EncodedSwap memory) {
GPv2Order.Data memory order = defaultOrder();

SwapEncoder.State storage swapEncoder = SwapEncoder.makeSwapEncoder();

swapEncoder.signEncodeTrade({
vm: vm,
owner: trader,
order: order,
domainSeparator: domainSeparator,
signingScheme: GPv2Signing.Scheme.Eip712,
executedAmount: executedAmount
});
return swapEncoder.encode();
}

function mockBalancerVaultCallsReturn(int256 mockSellAmount, int256 mockBuyAmount) private {
int256[] memory output = new int256[](2);
output[0] = mockSellAmount;
output[1] = mockBuyAmount;
vm.mockCall(address(vault), abi.encodePacked(IVault.batchSwap.selector), abi.encode(output));
vm.mockCall(address(vault), abi.encodePacked(IVault.manageUserBalance.selector), hex"");
}

function test_executes_order_against_swap() public {
SwapEncoder.EncodedSwap memory encodedSwap = encodedDefaultSwap();

mockBalancerVaultCallsReturn(int256(sellAmount), -int256(buyAmount));

vm.prank(solver);
swap(encodedSwap);
}

function test_updates_the_filled_amount_to_be_the_full_sell_or_buy_amount() public {
SwapEncoder.EncodedSwap memory encodedSwap = encodedDefaultSwap();

mockBalancerVaultCallsReturn(int256(sellAmount), -int256(buyAmount));

vm.prank(solver);
swap(encodedSwap);

uint256 expectedFilledAmount = (kind == GPv2Order.KIND_SELL) ? sellAmount : buyAmount;
assertEq(settlement.filledAmount(defaultOrderUid()), expectedFilledAmount);
}

function test_reverts_for_cancelled_orders() public {
SwapEncoder.EncodedSwap memory encodedSwap = encodedDefaultSwap();

mockBalancerVaultCallsReturn(0, 0);

vm.prank(trader.addr);
settlement.invalidateOrder(defaultOrderUid());

vm.prank(solver);
vm.expectRevert("GPv2: order filled");
swap(encodedSwap);
}

function test_reverts_for_partially_filled_orders() public {
SwapEncoder.EncodedSwap memory encodedSwap = encodedDefaultSwap();

mockBalancerVaultCallsReturn(0, 0);

vm.prank(trader.addr);
settlement.setFilledAmount(defaultOrderUid(), 1);

vm.prank(solver);
vm.expectRevert("GPv2: order filled");
swap(encodedSwap);
}

function test_reverts_when_not_exactly_trading_expected_amount() public {
SwapEncoder.EncodedSwap memory encodedSwap = encodedDefaultSwap();

mockBalancerVaultCallsReturn(int256(sellAmount) - 1, -(int256(buyAmount) + 1));

string memory kindString = (kind == GPv2Order.KIND_SELL) ? "sell" : "buy";
vm.prank(solver);
vm.expectRevert(bytes(string.concat("GPv2: ", kindString, " amount not respected")));
swap(encodedSwap);
}

function test_reverts_when_specified_limit_amount_does_not_satisfy_expected_price() public {
uint256 limitAmount = kind == GPv2Order.KIND_SELL
? buyAmount - 1 // receive slightly less buy token
: sellAmount + 1; // pay slightly more sell token;
SwapEncoder.EncodedSwap memory encodedSwap = encodedDefaultSwap(limitAmount);

mockBalancerVaultCallsReturn(int256(sellAmount), -int256(buyAmount));

vm.prank(solver);
vm.expectRevert(bytes((kind == GPv2Order.KIND_SELL) ? "GPv2: limit too low" : "GPv2: limit too high"));
swap(encodedSwap);
}

function test_emits_a_trade_event() public {
SwapEncoder.EncodedSwap memory encodedSwap = encodedDefaultSwap();

uint256 executedSellAmount = sellAmount;
uint256 executedBuyAmount = buyAmount;
if (kind == GPv2Order.KIND_SELL) {
executedBuyAmount = executedBuyAmount * 2;
} else {
executedSellAmount = executedSellAmount / 2;
}
mockBalancerVaultCallsReturn(int256(executedSellAmount), -int256(executedBuyAmount));

vm.prank(solver);
vm.expectEmit(address(settlement));
emit GPv2Settlement.Trade({
owner: trader.addr,
sellToken: sellToken,
buyToken: buyToken,
sellAmount: executedSellAmount,
buyAmount: executedBuyAmount,
feeAmount: encodedSwap.trade.feeAmount,
orderUid: defaultOrderUid()
});
swap(encodedSwap);
}
}

// solhint-disable-next-line no-empty-blocks
contract SellVariant is Variant(GPv2Order.KIND_SELL) {}

// solhint-disable-next-line no-empty-blocks
contract BuyVariant is Variant(GPv2Order.KIND_BUY) {}
Loading