Skip to content

Commit

Permalink
chore: migrate GPv2Settlement.swap variant tests to Foundry (#194)
Browse files Browse the repository at this point in the history
## Description

See title. This is the last PR that involves migrating
`GPv2Transfer.swap` tests.

## Test Plan

CI.

## Related Issues

#119

---------

Co-authored-by: mfw78 <[email protected]>
  • Loading branch information
fedgiac and mfw78 authored Aug 7, 2024
1 parent 5e76761 commit 37e020e
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 175 deletions.
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) {}

0 comments on commit 37e020e

Please sign in to comment.