diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f8b25ba..7fcf67db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,10 @@ jobs: name: Foundry project runs-on: ubuntu-latest + env: + # should support archive requests + FORK_URL: 'https://eth.llamarpc.com' + FORK_BLOCK_NUMBER: 20691292 steps: - uses: actions/checkout@v4 with: @@ -66,5 +70,9 @@ jobs: - name: Run Forge tests if: matrix.profile != 'solc-0.7.6' run: | + if [ "${{ matrix.profile }}" == "default" ]; then + echo "Setting FORK_URL for default profile" + export FORK_URL="${{ secrets.MAINNET_ARCHIVE_RPC }}" + fi FOUNDRY_PROFILE=ci forge test -vvv id: test diff --git a/foundry.toml b/foundry.toml index ef318bbb..a0113718 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,7 +14,7 @@ deny_warnings = true fs_permissions = [ { access = "read", path = "./balancer"}, { access = "read", path = "./networks.json"}, - { access = "read", path = "./out"} + { access = "read", path = "./out"}, ] [fmt] diff --git a/package.json b/package.json index 0043addc..cd7d1fe4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "ethers": "^5.4.0" }, "devDependencies": { - "@0x/contract-artifacts-v2": "npm:@0x/contract-artifacts@^2.2.2", "@gnosis.pm/safe-contracts": "^1.3.0", "@nomicfoundation/hardhat-verify": "^2.0.1", "@nomiclabs/hardhat-ethers": "^2.2.3", diff --git a/test/e2e/0xTrade.test.ts b/test/e2e/0xTrade.test.ts deleted file mode 100644 index 75bcc30a..00000000 --- a/test/e2e/0xTrade.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import { expect } from "chai"; -import Debug from "debug"; -import { BigNumber, Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - Order, - OrderKind, - Prices, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; -import { SimpleOrder as ZeroExSimpleOrder } from "./zero-ex"; -import * as ZeroExV2 from "./zero-ex/v2"; - -const debug = Debug("test:e2e:0xTrade"); - -describe("E2E: Can settle a 0x trade", () => { - let deployer: Wallet; - let solver: Wallet; - let trader: Wallet; - let marketMaker: Wallet; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let owl: Contract; - let gno: Contract; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - settlement, - vaultRelayer, - wallets: [solver, trader, marketMaker], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - owl = await waffle.deployContract(deployer, ERC20, ["OWL", 18]); - gno = await waffle.deployContract(deployer, ERC20, ["GNO", 18]); - }); - - function generateSettlementSolution(): { - gpv2Order: Order; - zeroExOrder: ZeroExSimpleOrder; - zeroExTakerAmount: BigNumber; - clearingPrices: Prices; - gpv2OwlSurplus: BigNumber; - zeroExOwlSurplus: BigNumber; - } { - const gpv2Order = { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: gno.address, - sellToken: owl.address, - buyAmount: ethers.utils.parseEther("1.0"), - sellAmount: ethers.utils.parseEther("130.0"), - feeAmount: ethers.utils.parseEther("10.0"), - validTo: 0xffffffff, - appData: 1, - }; - - const zeroExGnoPrice = 110; - const zeroExOrder = { - takerAddress: settlement.address, - makerAssetAddress: gno.address, - makerAssetAmount: ethers.utils.parseEther("1000.0"), - takerAssetAddress: owl.address, - takerAssetAmount: ethers.utils.parseEther("1000.0").mul(zeroExGnoPrice), - }; - const zeroExTakerAmount = gpv2Order.buyAmount.mul(zeroExGnoPrice); - - const gpv2GnoPrice = 120; - const clearingPrices = { - [owl.address]: 1, - [gno.address]: gpv2GnoPrice, - }; - - const gpv2OwlSurplus = gpv2Order.sellAmount.sub( - gpv2Order.buyAmount.mul(gpv2GnoPrice), - ); - const zeroExOwlSurplus = gpv2Order.buyAmount.mul( - gpv2GnoPrice - zeroExGnoPrice, - ); - - return { - gpv2Order, - zeroExOrder, - zeroExTakerAmount, - clearingPrices, - gpv2OwlSurplus, - zeroExOwlSurplus, - }; - } - - describe("0x Protocol v2", () => { - it("should settle an EOA trade with a 0x trade", async () => { - // Settles a market order buying 1 GNO for 120 OWL and get matched with a - // market maker using 0x orders. - - const { - gpv2Order, - zeroExOrder, - zeroExTakerAmount, - clearingPrices, - gpv2OwlSurplus, - zeroExOwlSurplus, - } = generateSettlementSolution(); - - await owl.mint(trader.address, ethers.utils.parseEther("140")); - await owl - .connect(trader) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - - const zeroEx = await ZeroExV2.deployExchange(deployer); - - await gno.mint(marketMaker.address, ethers.utils.parseEther("1000.0")); - await gno - .connect(marketMaker) - .approve(zeroEx.erc20Proxy.address, ethers.constants.MaxUint256); - - const zeroExSignedOrder = await ZeroExV2.signSimpleOrder( - marketMaker, - zeroEx.domainSeparator, - zeroExOrder, - ); - expect( - await zeroEx.exchange.isValidSignature( - zeroExSignedOrder.hash, - marketMaker.address, - zeroExSignedOrder.signature, - ), - ).to.be.true; - - const encoder = new SettlementEncoder(domainSeparator); - await encoder.signEncodeTrade(gpv2Order, trader, SigningScheme.EIP712); - encoder.encodeInteraction({ - target: owl.address, - callData: owl.interface.encodeFunctionData("approve", [ - zeroEx.erc20Proxy.address, - zeroExTakerAmount, - ]), - }); - encoder.encodeInteraction({ - target: zeroEx.exchange.address, - callData: zeroEx.exchange.interface.encodeFunctionData("fillOrder", [ - zeroExSignedOrder.order, - zeroExTakerAmount, - zeroExSignedOrder.signature, - ]), - }); - - const tx = await settlement - .connect(solver) - .settle(...encoder.encodedSettlement(clearingPrices)); - - const { gasUsed } = await tx.wait(); - debug(`gas used: ${gasUsed}`); - - expect(await gno.balanceOf(trader.address)).to.deep.equal( - ethers.utils.parseEther("1.0"), - ); - expect(await gno.balanceOf(marketMaker.address)).to.deep.equal( - ethers.utils.parseEther("999.0"), - ); - - // NOTE: The user keeps the surplus from their trade. - expect(await owl.balanceOf(trader.address)).to.deep.equal(gpv2OwlSurplus); - // NOTE: The exchange keeps the surplus from the 0x order. - expect(await owl.balanceOf(settlement.address)).to.deep.equal( - zeroExOwlSurplus.add(gpv2Order.feeAmount), - ); - }); - }); -}); diff --git a/test/e2e/BurnFees.t.sol b/test/e2e/BurnFees.t.sol new file mode 100644 index 00000000..e3ac5c1a --- /dev/null +++ b/test/e2e/BurnFees.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {GPv2Interaction} from "src/contracts/libraries/GPv2Interaction.sol"; +import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; +import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; + +import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +contract BurnFeesTest is Helper(false) { + IERC20Mintable owl; + IERC20Mintable dai; + + function setUp() public override { + super.setUp(); + + owl = deployMintableErc20("owl", "owl"); + dai = deployMintableErc20("dai", "dai"); + } + + function test_uses_post_interaction_to_burn_settlement_fees() external { + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + + // mint some owl to trader1 + owl.mint(trader1.addr, 1001 ether); + vm.prank(trader1.addr); + // approve owl for trading on settlement contract + owl.approve(vaultRelayer, type(uint256).max); + // place order to sell 1000 owl for min 1000 dai + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellToken: owl, + buyToken: dai, + sellAmount: 1000 ether, + buyAmount: 1000 ether, + feeAmount: 1 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some dai to trader2 + dai.mint(trader2.addr, 1000 ether); + vm.prank(trader2.addr); + // approve dai for trading on settlement contract + dai.approve(vaultRelayer, type(uint256).max); + // place order to BUY 1000 owl with max 1000 dai + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellToken: dai, + buyToken: owl, + sellAmount: 1000 ether, + buyAmount: 1000 ether, + feeAmount: 0, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // add post interaction to burn owl fees + encoder.addInteraction( + GPv2Interaction.Data({target: address(owl), value: 0, callData: abi.encodeCall(owl.burn, (1 ether))}), + SettlementEncoder.InteractionStage.POST + ); + + // set the token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = owl; + tokens[1] = dai; + uint256[] memory prices = new uint256[](2); + prices[0] = 1; + prices[1] = 1; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + vm.prank(solver); + vm.expectEmit(); + emit IERC20.Transfer(address(settlement), address(0), 1 ether); + settle(encodedSettlement); + + assertEq(dai.balanceOf(address(settlement)), 0, "dai balance of settlement contract not 0"); + } +} diff --git a/test/e2e/BuyEth.t.sol b/test/e2e/BuyEth.t.sol new file mode 100644 index 00000000..44f5b541 --- /dev/null +++ b/test/e2e/BuyEth.t.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; +import {GPv2Transfer} from "src/contracts/libraries/GPv2Transfer.sol"; + +import { + GPv2Interaction, GPv2Order, GPv2Signing, SettlementEncoder +} from "test/libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "test/libraries/encoders/TokenRegistry.sol"; + +import {Helper} from "./Helper.sol"; + +interface IUSDT { + function getOwner() external view returns (address); + function issue(uint256) external; + // approve and transfer doesn't return the bool for USDT + function approve(address, uint256) external; + function transfer(address, uint256) external; +} + +IUSDT constant USDT = IUSDT(0xdAC17F958D2ee523a2206206994597C13D831ec7); +IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +contract BuyEthTest is Helper(true) { + // Settle a trivial batch between two overlapping trades: + // + // /----(1. SELL 1 WETH for USDT if p(WETH) >= 1100)----\ + // | v + // [USDT] [(W)ETH] + // ^ | + // \-----(2. BUY 1 ETH for USDT if p(WETH) <= 1200)-----/ + function test_should_unwrap_weth_for_eth_buy_orders() external { + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + + // give some weth to trader1 + deal(address(WETH), trader1.addr, 1.001 ether); + // approve weth for trading on the vault + vm.prank(trader1.addr); + WETH.approve(vaultRelayer, type(uint256).max); + // place the weth to usdt swap order + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + sellToken: WETH, + buyToken: IERC20(address(USDT)), + receiver: trader1.addr, + sellAmount: 1 ether, + buyAmount: 1100e6, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0.001 ether, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // give some usdt to trader2 + _mintUsdt(trader2.addr, 1201.2e6); + // approve usdt for trading on the vault + vm.prank(trader2.addr); + USDT.approve(vaultRelayer, type(uint256).max); + // place the usdt to eth swap order + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + sellToken: IERC20(address(USDT)), + buyToken: IERC20(GPv2Transfer.BUY_ETH_ADDRESS), + receiver: trader2.addr, + sellAmount: 1200e6, + buyAmount: 1 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 1.2e6, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // encode the weth withdraw interaction + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(WETH), + value: 0, + callData: abi.encodeWithSignature("withdraw(uint256)", 1 ether) + }), + SettlementEncoder.InteractionStage.INTRA + ); + + // set the token prices + IERC20[] memory tokens = new IERC20[](3); + tokens[0] = WETH; + tokens[1] = IERC20(GPv2Transfer.BUY_ETH_ADDRESS); + tokens[2] = IERC20(address(USDT)); + uint256[] memory prices = new uint256[](3); + prices[0] = 1150e6; + prices[1] = 1150e6; + prices[2] = 1 ether; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + + uint256 trader2InitialBalance = trader2.addr.balance; + vm.prank(solver); + settle(encodedSettlement); + assertEq( + WETH.balanceOf(address(settlement)), + 0.001 ether, + "settlement contract's weth balance from trade fee not as expected" + ); // the fee + assertEq(WETH.balanceOf(trader1.addr), 0, "trader1 weth balance is not 0"); + assertEq( + trader2.addr.balance, trader2InitialBalance + 1 ether, "trader2 eth balance did not increase as expected" + ); + } + + function _mintUsdt(address receiver, uint256 amt) internal { + address owner = USDT.getOwner(); + vm.startPrank(owner); + USDT.issue(amt); + USDT.transfer(receiver, amt); + vm.stopPrank(); + } +} diff --git a/test/e2e/ContractOrdersWithGnosisSafe.t.sol b/test/e2e/ContractOrdersWithGnosisSafe.t.sol new file mode 100644 index 00000000..17a09184 --- /dev/null +++ b/test/e2e/ContractOrdersWithGnosisSafe.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; +import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; + +import {Eip712} from "../libraries/Eip712.sol"; + +import {Sign} from "../libraries/Sign.sol"; +import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +interface ISafeProxyFactory { + function createProxy(address singleton, bytes calldata data) external returns (ISafe); +} + +interface ISafe { + enum Operation { + Call, + DelegateCall + } + + function setup( + address[] calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address payable paymentReceiver + ) external; + + function execTransaction( + address to, + uint256 value, + bytes calldata data, + Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes memory signatures + ) external payable returns (bool success); + + function getTransactionHash( + address to, + uint256 value, + bytes calldata data, + Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address refundReceiver, + uint256 _nonce + ) external view returns (bytes32); + + function nonce() external view returns (uint256); + + function getMessageHash(bytes calldata) external view returns (bytes32); + + function isValidSignature(bytes32, bytes calldata) external view returns (bytes4); +} + +ISafeProxyFactory constant SAFE_PROXY_FACTORY = ISafeProxyFactory(0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2); +address constant SAFE_SINGLETON = 0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552; +address constant SAFE_COMPATIBILITY_FALLBACK_HANDLER = 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4; +bytes4 constant EIP1271_MAGICVALUE = bytes4(keccak256("isValidSignature(bytes32,bytes)")); + +contract ContractOrdersWithGnosisSafeTest is Helper(true) { + IERC20Mintable dai; + IERC20Mintable wETH; + + ISafe safe; + + Vm.Wallet signer1; + Vm.Wallet signer2; + Vm.Wallet signer3; + Vm.Wallet signer4; + Vm.Wallet signer5; + + function setUp() public override { + super.setUp(); + + dai = deployMintableErc20("dai", "dai"); + wETH = deployMintableErc20("wETH", "wETH"); + + signer1 = vm.createWallet("signer1"); + signer2 = vm.createWallet("signer2"); + signer3 = vm.createWallet("signer3"); + signer4 = vm.createWallet("signer4"); + signer5 = vm.createWallet("signer5"); + + address[] memory signers = new address[](5); + signers[0] = signer1.addr; + signers[1] = signer2.addr; + signers[2] = signer3.addr; + signers[3] = signer4.addr; + signers[4] = signer5.addr; + + bytes memory data = abi.encodeCall( + ISafe.setup, + (signers, 2, address(0), hex"", SAFE_COMPATIBILITY_FALLBACK_HANDLER, address(0), 0, payable(address(0))) + ); + safe = SAFE_PROXY_FACTORY.createProxy(SAFE_SINGLETON, data); + } + + function test_should_settle_matching_orders() external { + // EOA trader: sell 1 wETH for 900 dai + // Safe: buy 1 wETH for 1100 dai + // Settlement price at 1000 dai for 1 wETH. + + // mint some tokens to trader + wETH.mint(trader.addr, 1.001 ether); + // approve the tokens for trading on settlement contract + vm.prank(trader.addr); + wETH.approve(vaultRelayer, type(uint256).max); + + // place order to sell 1 wETH for min 900 dai + encoder.signEncodeTrade( + vm, + trader, + GPv2Order.Data({ + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellToken: wETH, + buyToken: dai, + sellAmount: 1 ether, + buyAmount: 900 ether, + feeAmount: 0.001 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some dai to the safe + dai.mint(address(safe), 1110 ether); + // approve dai for trading on settlement contract + _execSafeTransaction( + safe, address(dai), 0, abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), signer1, signer2 + ); + assertEq(dai.allowance(address(safe), vaultRelayer), type(uint256).max, "allowance not as expected"); + + // place order to buy 1 wETH with max 1100 dai + GPv2Order.Data memory order = GPv2Order.Data({ + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellToken: dai, + buyToken: wETH, + sellAmount: 1100 ether, + buyAmount: 1 ether, + feeAmount: 10 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }); + + bytes32 orderHash = Eip712.typedDataHash(Eip712.toEip712SignedStruct(order), domainSeparator); + bytes32 safeMessageHash = safe.getMessageHash(abi.encode(orderHash)); + bytes memory signatures = _safeSignature(signer3, signer4, safeMessageHash); + + assertEq(safe.isValidSignature(orderHash, signatures), EIP1271_MAGICVALUE, "invalid signature for the order"); + + encoder.encodeTrade( + order, + Sign.Signature({scheme: GPv2Signing.Scheme.Eip1271, data: abi.encodePacked(address(safe), signatures)}), + 0 + ); + + // set token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = dai; + tokens[1] = wETH; + uint256[] memory prices = new uint256[](2); + prices[0] = 1 ether; + prices[1] = 1000 ether; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + + vm.prank(solver); + settle(encodedSettlement); + + assertEq(wETH.balanceOf(trader.addr), 0, "trader weth balance not as expected"); + assertEq(dai.balanceOf(trader.addr), 1000 ether, "trader dai balance not as expected"); + + assertEq(wETH.balanceOf(address(safe)), 1 ether, "safe weth balance not as expected"); + assertEq(dai.balanceOf(address(safe)), 100 ether, "safe dai balance not as expected"); + + assertEq(wETH.balanceOf(address(settlement)), 0.001 ether, "settlement weth fee not as expected"); + assertEq(dai.balanceOf(address(settlement)), 10 ether, "settlement dai fee not as expected"); + } + + function _execSafeTransaction( + ISafe safe_, + address to, + uint256 value, + bytes memory data, + Vm.Wallet memory signer1_, + Vm.Wallet memory signer2_ + ) internal { + uint256 nonce = safe_.nonce(); + bytes32 hash = + safe_.getTransactionHash(to, value, data, ISafe.Operation.Call, 0, 0, 0, address(0), address(0), nonce); + bytes memory signatures = _safeSignature(signer1_, signer2_, hash); + safe_.execTransaction( + to, value, data, ISafe.Operation.Call, 0, 0, 0, address(0), payable(address(0)), signatures + ); + } + + function _sign(Vm.Wallet memory wallet, bytes32 hash) internal returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet, hash); + return abi.encodePacked(r, s, v); + } + + function _safeSignature(Vm.Wallet memory signer1_, Vm.Wallet memory signer2_, bytes32 hash) + internal + returns (bytes memory) + { + bytes memory signature1 = _sign(signer1_, hash); + bytes memory signature2 = _sign(signer2_, hash); + bytes memory signatures = signer1_.addr < signer2_.addr + ? abi.encodePacked(signature1, signature2) + : abi.encodePacked(signature2, signature1); + return signatures; + } +} diff --git a/test/e2e/Deployment.t.sol b/test/e2e/Deployment.t.sol new file mode 100644 index 00000000..456549e5 --- /dev/null +++ b/test/e2e/Deployment.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {GPv2AllowListAuthentication} from "src/contracts/GPv2AllowListAuthentication.sol"; + +import {Helper} from "./Helper.sol"; + +interface IEIP173Proxy { + function owner() external view returns (address); +} + +// ref: https://github.com/wighawag/hardhat-deploy/blob/e0ffcf9e7dc92b246e832c4d175f9dbd8b6df14d/solc_0.8/proxy/EIP173Proxy.sol +bytes32 constant EIP173_IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + +contract DeploymentTest is Helper(false) { + event Metadata(string, bytes); + + function test__same_built_and_deployed_bytecode_metadata__authenticator() external { + _assertBuiltAndDeployedMetadataCoincide(address(allowListImpl), "GPv2AllowListAuthentication"); + } + + function test__same_built_and_deployed_bytecode_metadata__settlement() external { + _assertBuiltAndDeployedMetadataCoincide(address(settlement), "GPv2Settlement"); + } + + function test__same_built_and_deployed_bytecode_metadata__vault_relayer() external { + _assertBuiltAndDeployedMetadataCoincide(address(vaultRelayer), "GPv2VaultRelayer"); + } + + function test__determininstic_addresses__authenticator__proxy() external view { + assertEq( + _computeCreate2Addr( + abi.encodePacked( + vm.getCode("EIP173Proxy"), + abi.encode( + _implementationAddress(address(allowList)), + owner, + abi.encodeCall(GPv2AllowListAuthentication.initializeManager, (owner)) + ) + ) + ), + address(authenticator), + "authenticator address not as expected" + ); + } + + function test__determininstic_addresses__authenticator__implementation() external view { + assertEq( + _computeCreate2Addr(vm.getCode("GPv2AllowListAuthentication")), + _implementationAddress(address(allowList)), + "authenticator impl address not as expected" + ); + } + + function test__determininstic_addresses__settlement() external view { + assertEq( + _computeCreate2Addr( + abi.encodePacked(vm.getCode("GPv2Settlement"), abi.encode(address(authenticator), address(vault))) + ), + address(settlement), + "settlement address not as expected" + ); + } + + function test__authorization__authenticator_has_dedicated_owner() external view { + assertEq(IEIP173Proxy(address(allowList)).owner(), owner, "owner not as expected"); + } + + function test__authorization__authenticator_has_dedicated_manager() external view { + assertEq(allowList.manager(), owner, "manager not as expected"); + } + + function _assertBuiltAndDeployedMetadataCoincide(address addr, string memory artifactName) internal { + bytes memory deployedCode = vm.getDeployedCode(artifactName); + assertEq( + keccak256(_getMetadata(string(abi.encodePacked("deployed ", artifactName)), addr.code)), + keccak256(_getMetadata(artifactName, deployedCode)), + "metadata doesnt match" + ); + } + + function _getMetadata(string memory hint, bytes memory bytecode) internal returns (bytes memory metadata) { + assembly ("memory-safe") { + // the last two bytes encode the size of the cbor encoded metadata + let bytecodeSize := mload(bytecode) + let bytecodeStart := add(bytecode, 0x20) + let cborSizeOffset := add(bytecodeStart, sub(bytecodeSize, 0x20)) + let cborSize := and(mload(cborSizeOffset), 0xffff) + + // copy the metadata out + metadata := mload(0x40) + let metadataSize := add(cborSize, 0x02) + mstore(metadata, metadataSize) + let metadataOffset := add(bytecodeStart, sub(bytecodeSize, metadataSize)) + mcopy(add(metadata, 0x20), metadataOffset, metadataSize) + + // update free memory ptr + mstore(0x40, add(metadata, add(metadataSize, 0x20))) + } + emit Metadata(hint, metadata); + } + + function _computeCreate2Addr(bytes memory initCode) internal view returns (address) { + return vm.computeCreate2Address(SALT, hashInitCode(initCode), deployer); + } + + function _implementationAddress(address proxy) internal view returns (address) { + return address(uint160(uint256(vm.load(proxy, EIP173_IMPLEMENTATION_SLOT)))); + } +} diff --git a/test/e2e/ERC20Mintable.sol b/test/e2e/ERC20Mintable.sol new file mode 100644 index 00000000..30b53884 --- /dev/null +++ b/test/e2e/ERC20Mintable.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// solhint-disable-next-line compiler-version +pragma solidity ^0.7; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mintable is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(uint256 amount) external { + _burn(msg.sender, amount); + } +} diff --git a/test/e2e/Helper.sol b/test/e2e/Helper.sol new file mode 100644 index 00000000..c56e20cb --- /dev/null +++ b/test/e2e/Helper.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Test, Vm, stdJson} from "forge-std/Test.sol"; +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {GPv2AllowListAuthentication} from "src/contracts/GPv2AllowListAuthentication.sol"; +import { + GPv2Authentication, + GPv2Interaction, + GPv2Settlement, + GPv2Trade, + IERC20, + IVault +} from "src/contracts/GPv2Settlement.sol"; + +import {WETH9} from "./WETH9.sol"; +import {SettlementEncoder} from "test/libraries/encoders/SettlementEncoder.sol"; +import {SwapEncoder} from "test/libraries/encoders/SwapEncoder.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; +address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; + +uint256 constant FORK_BLOCK_NUMBER = 20913563; + +interface IAuthorizer { + function grantRole(bytes32, address) external; + function canPerform(bytes32 actionId, address account, address where) external view returns (bool); +} + +interface IERC20Mintable is IERC20 { + function mint(address, uint256) external; + function burn(uint256) external; +} + +interface IBalancerVault is IVault { + function getAuthorizer() external view returns (address); +} + +// solhint-disable func-name-mixedcase +// solhint-disable max-states-count +abstract contract Helper is Test { + using stdJson for string; + using SettlementEncoder for SettlementEncoder.State; + + address internal deployer; + address internal owner; + GPv2Settlement internal settlement; + bytes32 internal domainSeparator; + GPv2Authentication internal authenticator; + IVault internal vault; + GPv2AllowListAuthentication internal allowList; + GPv2AllowListAuthentication internal allowListImpl; + address vaultRelayer; + address balancerVaultAuthorizer; + + SettlementEncoder.State internal encoder; + SwapEncoder.State internal swapEncoder; + + address internal solver; + Vm.Wallet internal trader; + + bool immutable isForked; + uint256 forkId; + + WETH9 weth; + + bytes32 constant SALT = "Mattresses in Berlin!"; + + constructor(bool _isForked) { + isForked = _isForked; + } + + function setUp() public virtual { + if (isForked) { + string memory forkUrl; + + try vm.envString("FORK_URL") returns (string memory url) { + forkUrl = url; + } catch { + forkUrl = "https://eth.merkle.io"; + } + + forkId = vm.createSelectFork(forkUrl, FORK_BLOCK_NUMBER); + weth = WETH9(payable(WETH)); + } else { + weth = new WETH9(); + } + + // Configure addresses + deployer = makeAddr("E2E.Helper: deployer"); + owner = makeAddr("E2E.Helper: owner"); + solver = makeAddr("E2E.Helper: solver"); + vm.startPrank(deployer); + + // Deploy the allowlist manager + allowListImpl = new GPv2AllowListAuthentication{salt: SALT}(); + allowList = GPv2AllowListAuthentication( + deployProxy(address(allowListImpl), owner, abi.encodeCall(allowListImpl.initializeManager, (owner)), SALT) + ); + authenticator = allowList; + + (balancerVaultAuthorizer, vault) = _deployBalancerVault(); + + // Deploy the settlement contract + settlement = new GPv2Settlement{salt: SALT}(authenticator, vault); + vaultRelayer = address(settlement.vaultRelayer()); + + // Reset the prank + vm.stopPrank(); + + _grantBalancerRolesToRelayer(balancerVaultAuthorizer, address(vault), vaultRelayer); + + // By default, allow `solver` to settle + vm.prank(owner); + allowList.addSolver(solver); + + // Configure default encoders + encoder = SettlementEncoder.makeSettlementEncoder(); + swapEncoder = SwapEncoder.makeSwapEncoder(); + + // Set the domain separator + domainSeparator = settlement.domainSeparator(); + + // Create wallets + trader = vm.createWallet("E2E.Helper: trader"); + } + + function settle(SettlementEncoder.EncodedSettlement memory _settlement) internal { + settlement.settle(_settlement.tokens, _settlement.clearingPrices, _settlement.trades, _settlement.interactions); + } + + function swap(SwapEncoder.EncodedSwap memory _swap) internal { + settlement.swap(_swap.swaps, _swap.tokens, _swap.trade); + } + + function emptySettlement() internal pure returns (SettlementEncoder.EncodedSettlement memory) { + return SettlementEncoder.EncodedSettlement({ + tokens: new IERC20[](0), + clearingPrices: new uint256[](0), + trades: new GPv2Trade.Data[](0), + interactions: [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)] + }); + } + + function _deployBalancerVault() internal returns (address, IBalancerVault) { + if (isForked) { + IBalancerVault balancerVault = IBalancerVault(BALANCER_VAULT); + address authorizer = balancerVault.getAuthorizer(); + return (authorizer, balancerVault); + } else { + bytes memory authorizerInitCode = abi.encodePacked(_getBalancerBytecode("Authorizer"), abi.encode(owner)); + address authorizer = _create(authorizerInitCode, 0); + + bytes memory vaultInitCode = + abi.encodePacked(_getBalancerBytecode("Vault"), abi.encode(authorizer, address(weth), 0, 0)); + address deployedVault = _create(vaultInitCode, 0); + + return (authorizer, IBalancerVault(deployedVault)); + } + } + + function _grantBalancerRolesToRelayer(address authorizer, address deployedVault, address relayer) internal { + _grantBalancerActionRole( + authorizer, deployedVault, relayer, "manageUserBalance((uint8,address,uint256,address,address)[])" + ); + _grantBalancerActionRole( + authorizer, + deployedVault, + relayer, + "batchSwap(uint8,(bytes32,uint256,uint256,uint256,bytes)[],address[],(address,bool,address,bool),int256[],uint256)" + ); + } + + function _grantBalancerActionRole(address authorizer, address balVault, address to, string memory action) + internal + { + bytes32 actionId = _getActionId(action, balVault); + vm.mockCall( + address(authorizer), abi.encodeCall(IAuthorizer.canPerform, (actionId, to, balVault)), abi.encode(true) + ); + } + + function _getActionId(string memory fnDef, address vaultAddr) internal pure returns (bytes32) { + bytes32 hash = keccak256(bytes(fnDef)); + bytes4 selector = bytes4(hash); + return keccak256(abi.encodePacked(uint256(uint160(vaultAddr)), selector)); + } + + function _getBalancerBytecode(string memory artifactName) internal view returns (bytes memory) { + string memory data = vm.readFile(string(abi.encodePacked("balancer/", artifactName, ".json"))); + return vm.parseJsonBytes(data, ".bytecode"); + } + + function _create(bytes memory initCode, uint256 value) internal returns (address deployed) { + assembly ("memory-safe") { + deployed := create(value, add(initCode, 0x20), mload(initCode)) + } + require(deployed != address(0), "deployment failed"); + } + + function _create2(bytes memory initCode, uint256 value, bytes32 salt) internal returns (address deployed) { + assembly ("memory-safe") { + deployed := create2(value, add(initCode, 0x20), mload(initCode), salt) + } + require(deployed != address(0), "deployment failed"); + } + + function deployMintableErc20(string memory name, string memory symbol) internal returns (IERC20Mintable token) { + // need to use like this because OZ requires ^0.7 and tests are on ^0.8 + bytes memory initCode = abi.encodePacked(vm.getCode("ERC20Mintable"), abi.encode(name, symbol)); + token = IERC20Mintable(_create(initCode, 0)); + } + + function deployProxy(address implAddress, address ownerAddress, bytes memory data, bytes32 salt) + internal + returns (address proxy) + { + proxy = + _create2(abi.encodePacked(vm.getCode("EIP173Proxy"), abi.encode(implAddress, ownerAddress, data)), 0, salt); + } +} diff --git a/test/e2e/InternalBalances.t.sol b/test/e2e/InternalBalances.t.sol new file mode 100644 index 00000000..77461e26 --- /dev/null +++ b/test/e2e/InternalBalances.t.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; +import {IVault} from "src/contracts/interfaces/IVault.sol"; + +import {GPv2Order, GPv2Signing, SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +interface IBalancerVault is IVault { + function setRelayerApproval(address, address, bool) external; + function getInternalBalance(address user, IERC20[] memory tokens) external view returns (uint256[] memory); + function hasApprovedRelayer(address, address) external view returns (bool); +} + +contract InternalBalancesTest is Helper(false) { + IERC20Mintable token1; + IERC20Mintable token2; + + function setUp() public override { + super.setUp(); + + token1 = deployMintableErc20("TK1", "TK1"); + token2 = deployMintableErc20("TK2", "TK2"); + + vm.startPrank(address(settlement)); + token1.approve(address(vault), type(uint256).max); + token2.approve(address(vault), type(uint256).max); + vm.stopPrank(); + } + + function test_should_settle_orders_buying_and_selling_with_internal_balances() external { + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + Vm.Wallet memory trader3 = vm.createWallet("trader3"); + Vm.Wallet memory trader4 = vm.createWallet("trader4"); + + // mint some tokens to trader1 + _mintTokens(token1, trader1.addr, 1.001 ether); + + // approve tokens to the balancer vault and approve the settlement contract to + // be able to spend the balancer internal/external balances + vm.startPrank(trader1.addr); + token1.approve(address(vault), type(uint256).max); + IBalancerVault(address(vault)).setRelayerApproval(trader1.addr, vaultRelayer, true); + vm.stopPrank(); + + // place order for selling 1 token1 for 500 token2 + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + sellToken: token1, + buyToken: token2, + receiver: trader1.addr, + sellAmount: 1 ether, + buyAmount: 500 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0.001 ether, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_EXTERNAL, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some tokens to trader2 + _mintTokens(token2, trader2.addr, 300.3 ether); + + // approve tokens to the balancer vault and deposit some tokens to balancer internal + // balance + vm.startPrank(trader2.addr); + token2.approve(address(vault), type(uint256).max); + IVault.UserBalanceOp[] memory ops = new IVault.UserBalanceOp[](1); + ops[0] = IVault.UserBalanceOp({ + kind: IVault.UserBalanceOpKind.DEPOSIT_INTERNAL, + asset: token2, + amount: 300.3 ether, + sender: trader2.addr, + recipient: payable(trader2.addr) + }); + vault.manageUserBalance(ops); + IBalancerVault(address(vault)).setRelayerApproval(trader2.addr, vaultRelayer, true); + vm.stopPrank(); + + // place order for buying 0.5 token1 with max 300 token2 + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + sellToken: token2, + buyToken: token1, + receiver: trader2.addr, + sellAmount: 300 ether, + buyAmount: 0.5 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0.3 ether, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_INTERNAL, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some tokens to trader3 + _mintTokens(token1, trader3.addr, 2.002 ether); + + // approve the tokens to cow vault relayer + vm.prank(trader3.addr); + token1.approve(vaultRelayer, type(uint256).max); + + // place order for selling 2 token1 for min 1000 token2 + encoder.signEncodeTrade( + vm, + trader3, + GPv2Order.Data({ + sellToken: token1, + buyToken: token2, + receiver: trader3.addr, + sellAmount: 2 ether, + buyAmount: 1000 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0.002 ether, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_INTERNAL + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some tokens to trader4 + _mintTokens(token2, trader4.addr, 1501.5 ether); + + // approve tokens to the balancer vault and deposit some tokens to balancer internal + // balance + vm.startPrank(trader4.addr); + token2.approve(address(vault), type(uint256).max); + ops = new IVault.UserBalanceOp[](1); + ops[0] = IVault.UserBalanceOp({ + kind: IVault.UserBalanceOpKind.DEPOSIT_INTERNAL, + asset: token2, + amount: 1501.5 ether, + sender: trader4.addr, + recipient: payable(trader4.addr) + }); + IBalancerVault(address(vault)).manageUserBalance(ops); + IBalancerVault(address(vault)).setRelayerApproval(trader4.addr, vaultRelayer, true); + vm.stopPrank(); + + // place order to buy 2.5 token1 with max 1500 token2 + encoder.signEncodeTrade( + vm, + trader4, + GPv2Order.Data({ + sellToken: token2, + buyToken: token1, + receiver: trader4.addr, + sellAmount: 1500 ether, + buyAmount: 2.5 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 1.5 ether, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_INTERNAL, + buyTokenBalance: GPv2Order.BALANCE_INTERNAL + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // set token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = token1; + tokens[1] = token2; + uint256[] memory prices = new uint256[](2); + prices[0] = 550; + prices[1] = 1; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + // settle the orders + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + vm.prank(solver); + settle(encodedSettlement); + + assertEq(token2.balanceOf(trader1.addr), 550 ether, "trader1 amountOut not as expected"); + assertEq(token1.balanceOf(trader2.addr), 0.5 ether, "trader2 amountOut not as expected"); + assertEq(_getInternalBalance(address(token2), trader3.addr), 1100 ether, "trader3 amountOut not as expected"); + assertEq(_getInternalBalance(address(token1), trader4.addr), 2.5 ether, "trader4 amountOut not as expected"); + + assertEq(token1.balanceOf(address(settlement)), 0.003 ether, "token1 settlement fee amount not as expected"); + assertEq(token2.balanceOf(address(settlement)), 1.8 ether, "token2 settlement fee amount not as expected"); + } + + function _mintTokens(IERC20Mintable token, address to, uint256 amt) internal { + token.mint(to, amt); + } + + function _getInternalBalance(address token, address who) internal view returns (uint256) { + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = IERC20(token); + uint256[] memory bals = IBalancerVault(address(vault)).getInternalBalance(who, tokens); + return bals[0]; + } +} diff --git a/test/e2e/NonStandardErc20.t.sol b/test/e2e/NonStandardErc20.t.sol new file mode 100644 index 00000000..1445d1d9 --- /dev/null +++ b/test/e2e/NonStandardErc20.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {Helper} from "./Helper.sol"; +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {GPv2Order, GPv2Signing, SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; + +abstract contract NonStandardERC20 { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + } + + function approve(address spender, uint256 amount) external { + allowance[msg.sender][spender] = amount; + } + + function transfer_(address to, uint256 amount) internal { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + } + + function transferFrom_(address from, address to, uint256 amount) internal { + allowance[from][msg.sender] -= amount; + balanceOf[from] -= amount; + balanceOf[to] += amount; + } +} + +contract ERC20NoReturn is NonStandardERC20 { + function transfer(address to, uint256 amount) external { + transfer_(to, amount); + } + + function transferFrom(address from, address to, uint256 amount) external { + transferFrom_(from, to, amount); + } +} + +contract ERC20ReturningUint is NonStandardERC20 { + // Largest 256-bit prime :) + uint256 private constant OK = 115792089237316195423570985008687907853269984665640564039457584007913129639747; + + function transfer(address to, uint256 amount) external returns (uint256) { + transfer_(to, amount); + return OK; + } + + function transferFrom(address from, address to, uint256 amount) external returns (uint256) { + transferFrom_(from, to, amount); + return OK; + } +} + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +contract NonStandardErc20Test is Helper(false) { + ERC20NoReturn noReturnToken; + ERC20ReturningUint uintReturningToken; + + function setUp() public override { + super.setUp(); + + noReturnToken = new ERC20NoReturn(); + uintReturningToken = new ERC20ReturningUint(); + } + + function test_should_allow_trading_non_standard_erc20_tokens() external { + uint256 amount = 1 ether; + uint256 feeAmount = 0.01 ether; + + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + + // mint some noReturnToken tokens to trader1 + noReturnToken.mint(trader1.addr, amount + feeAmount); + vm.prank(trader1.addr); + noReturnToken.approve(vaultRelayer, type(uint256).max); + // place order to swap noReturnToken for uintReturningToken + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + sellToken: IERC20(address(noReturnToken)), + buyToken: IERC20(address(uintReturningToken)), + receiver: trader1.addr, + sellAmount: amount, + buyAmount: amount, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: feeAmount, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some uintReturningToken tokens to trader2 + uintReturningToken.mint(trader2.addr, amount + feeAmount); + vm.prank(trader2.addr); + uintReturningToken.approve(vaultRelayer, type(uint256).max); + // place order to swap uintReturningToken for noReturnToken + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + sellToken: IERC20(address(uintReturningToken)), + buyToken: IERC20(address(noReturnToken)), + receiver: trader2.addr, + sellAmount: amount, + buyAmount: amount, + validTo: 0xffffffff, + appData: bytes32(uint256(2)), + feeAmount: feeAmount, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // set token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = IERC20(address(noReturnToken)); + tokens[1] = IERC20(address(uintReturningToken)); + uint256[] memory prices = new uint256[](2); + prices[0] = 1; + prices[1] = 1; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + // settle the orders + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + vm.prank(solver); + settle(encodedSettlement); + + assertEq(noReturnToken.balanceOf(address(settlement)), feeAmount, "order1 fee not charged as expected"); + assertEq(noReturnToken.balanceOf(trader2.addr), amount, "order1 swap output not as expected"); + + assertEq(uintReturningToken.balanceOf(address(settlement)), feeAmount, "order2 fee not charged as expected"); + assertEq(uintReturningToken.balanceOf(trader1.addr), amount, "order2 swap output not as expected"); + } +} diff --git a/test/e2e/OffchainAllowances.t.sol b/test/e2e/OffchainAllowances.t.sol new file mode 100644 index 00000000..38d304bf --- /dev/null +++ b/test/e2e/OffchainAllowances.t.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; +import {IVault} from "src/contracts/interfaces/IVault.sol"; +import {GPv2Interaction} from "src/contracts/libraries/GPv2Interaction.sol"; +import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; +import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; + +import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +interface IERC2612 { + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + function nonces(address owner) external view returns (uint256); + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + +interface IBalancerVault { + function getDomainSeparator() external view returns (bytes32); + function setRelayerApproval(address, address, bool) external; +} + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +contract OffchainAllowancesTest is Helper(false) { + IERC20Mintable eur1; + IERC20Mintable eur2; + + Vm.Wallet trader1; + Vm.Wallet trader2; + + function setUp() public override { + super.setUp(); + + eur1 = IERC20Mintable(_create(abi.encodePacked(vm.getCode("ERC20PresetPermit"), abi.encode("eur1")), 0)); + eur2 = IERC20Mintable(_create(abi.encodePacked(vm.getCode("ERC20PresetPermit"), abi.encode("eur1")), 0)); + + trader1 = vm.createWallet("trader1"); + trader2 = vm.createWallet("trader2"); + } + + function test_eip_2612_permits_trader_allowance_with_settlement() external { + // mint and approve tokens to and from trader1 + eur1.mint(trader1.addr, 1 ether); + vm.prank(trader1.addr); + eur1.approve(vaultRelayer, type(uint256).max); + + // place order to sell 1 eur1 for min 1 eur2 from trader1 + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + sellToken: eur1, + buyToken: eur2, + receiver: trader1.addr, + sellAmount: 1 ether, + buyAmount: 1 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some tokens to trader2 + eur2.mint(trader2.addr, 1 ether); + uint256 nonce = IERC2612(address(eur2)).nonces(trader2.addr); + (uint8 v, bytes32 r, bytes32 s) = _permit(eur2, trader2, vaultRelayer, 1 ether, nonce, 0xffffffff); + // interaction for setting the approval with permit + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(eur2), + value: 0, + callData: abi.encodeCall(IERC2612.permit, (trader2.addr, vaultRelayer, 1 ether, 0xffffffff, v, r, s)) + }), + SettlementEncoder.InteractionStage.PRE + ); + + // buy 1 eur1 with max 1 eur2 + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + sellToken: eur2, + buyToken: eur1, + receiver: trader2.addr, + sellAmount: 1 ether, + buyAmount: 1 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // set prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = eur1; + tokens[1] = eur2; + uint256[] memory prices = new uint256[](2); + prices[0] = 1; + prices[1] = 1; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + vm.prank(solver); + settle(encodedSettlement); + + assertEq(eur2.balanceOf(trader2.addr), 0, "permit didnt work"); + } + + function test_allows_setting_vault_relayer_approval_with_interactions() external { + // mint and approve tokens to and from trader1 + eur1.mint(trader1.addr, 1 ether); + vm.prank(trader1.addr); + eur1.approve(vaultRelayer, type(uint256).max); + + // place order to sell 1 eur1 for min 1 eur2 from trader1 + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + sellToken: eur1, + buyToken: eur2, + receiver: trader1.addr, + sellAmount: 1 ether, + buyAmount: 1 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some tokens to trader2 + eur2.mint(trader2.addr, 1 ether); + // deposit tokens into balancer internal balance + vm.startPrank(trader2.addr); + eur2.approve(address(vault), type(uint256).max); + IVault.UserBalanceOp[] memory ops = new IVault.UserBalanceOp[](1); + ops[0] = IVault.UserBalanceOp({ + kind: IVault.UserBalanceOpKind.DEPOSIT_INTERNAL, + asset: eur2, + amount: 1 ether, + sender: trader2.addr, + recipient: payable(trader2.addr) + }); + vault.manageUserBalance(ops); + vm.stopPrank(); + + _grantBalancerActionRole( + balancerVaultAuthorizer, address(vault), address(settlement), "setRelayerApproval(address,address,bool)" + ); + bytes memory approval = abi.encodeCall(IBalancerVault.setRelayerApproval, (trader2.addr, vaultRelayer, true)); + (uint8 v, bytes32 r, bytes32 s) = + _balancerSetRelayerApprovalSignature(trader2, approval, address(settlement), 0, 0xffffffff); + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(vault), + value: 0, + callData: abi.encodePacked(approval, abi.encode(0xffffffff, v, r, s)) + }), + SettlementEncoder.InteractionStage.PRE + ); + + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + sellToken: eur2, + buyToken: eur1, + receiver: trader2.addr, + sellAmount: 1 ether, + buyAmount: 1 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: 0, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_INTERNAL, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // set prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = eur1; + tokens[1] = eur2; + uint256[] memory prices = new uint256[](2); + prices[0] = 1; + prices[1] = 1; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + + vm.prank(solver); + settle(encodedSettlement); + + assertEq(eur2.balanceOf(trader2.addr), 0, "balancer signed approval didnt work"); + } + + function _permit( + IERC20Mintable token, + Vm.Wallet memory owner, + address spender, + uint256 value, + uint256 nonce, + uint256 deadline + ) internal returns (uint8 v, bytes32 r, bytes32 s) { + bytes32 ds = IERC2612(address(token)).DOMAIN_SEPARATOR(); + bytes32 digest = keccak256( + abi.encodePacked( + hex"1901", + ds, + keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner.addr, + spender, + value, + nonce, + deadline + ) + ) + ) + ); + (v, r, s) = vm.sign(owner, digest); + } + + function _balancerSetRelayerApprovalSignature( + Vm.Wallet memory owner, + bytes memory cd, + address sender, + uint256 nonce, + uint256 deadline + ) internal returns (uint8 v, bytes32 r, bytes32 s) { + bytes32 ds = IBalancerVault(address(vault)).getDomainSeparator(); + bytes memory ecd = abi.encode( + keccak256("SetRelayerApproval(bytes calldata,address sender,uint256 nonce,uint256 deadline)"), + keccak256(cd), + sender, + nonce, + deadline + ); + bytes32 digest = keccak256(abi.encodePacked(hex"1901", ds, keccak256(ecd))); + (v, r, s) = vm.sign(owner, digest); + } +} diff --git a/test/e2e/SmartOrder.t.sol b/test/e2e/SmartOrder.t.sol new file mode 100644 index 00000000..c3dc2dbb --- /dev/null +++ b/test/e2e/SmartOrder.t.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {GPv2Settlement} from "src/contracts/GPv2Settlement.sol"; +import {EIP1271Verifier, GPv2EIP1271} from "src/contracts/interfaces/GPv2EIP1271.sol"; +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; +import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; +import {GPv2SafeERC20} from "src/contracts/libraries/GPv2SafeERC20.sol"; +import {SafeMath} from "src/contracts/libraries/SafeMath.sol"; +import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; + +import {Sign} from "../libraries/Sign.sol"; +import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +/// @title Proof of Concept Smart Order +/// @author Gnosis Developers +contract SmartSellOrder is EIP1271Verifier { + using GPv2Order for GPv2Order.Data; + using GPv2SafeERC20 for IERC20; + using SafeMath for uint256; + + bytes32 public constant APPDATA = keccak256("SmartSellOrder"); + + address public immutable owner; + bytes32 public immutable domainSeparator; + IERC20 public immutable sellToken; + IERC20 public immutable buyToken; + uint256 public immutable totalSellAmount; + uint256 public immutable totalFeeAmount; + uint32 public immutable validTo; + + constructor( + GPv2Settlement settlement, + IERC20 sellToken_, + IERC20 buyToken_, + uint32 validTo_, + uint256 totalSellAmount_, + uint256 totalFeeAmount_ + ) { + owner = msg.sender; + domainSeparator = settlement.domainSeparator(); + sellToken = sellToken_; + buyToken = buyToken_; + validTo = validTo_; + totalSellAmount = totalSellAmount_; + totalFeeAmount = totalFeeAmount_; + + sellToken_.approve(address(settlement.vaultRelayer()), type(uint256).max); + } + + modifier onlyOwner() { + require(msg.sender == owner, "not owner"); + _; + } + + function withdraw(uint256 amount) external onlyOwner { + sellToken.safeTransfer(owner, amount); + } + + function close() external onlyOwner { + uint256 balance = sellToken.balanceOf(address(this)); + if (balance != 0) { + sellToken.safeTransfer(owner, balance); + } + } + + function isValidSignature(bytes32 hash, bytes memory signature) + external + view + override + returns (bytes4 magicValue) + { + uint256 sellAmount = abi.decode(signature, (uint256)); + GPv2Order.Data memory order = orderForSellAmount(sellAmount); + + if (order.hash(domainSeparator) == hash) { + magicValue = GPv2EIP1271.MAGICVALUE; + } + } + + function orderForSellAmount(uint256 sellAmount) public view returns (GPv2Order.Data memory order) { + order.sellToken = sellToken; + order.buyToken = buyToken; + order.receiver = owner; + order.sellAmount = sellAmount; + order.buyAmount = buyAmountForSellAmount(sellAmount); + order.validTo = validTo; + order.appData = APPDATA; + order.feeAmount = totalFeeAmount.mul(sellAmount).div(totalSellAmount); + order.kind = GPv2Order.KIND_SELL; + // NOTE: We counter-intuitively set `partiallyFillable` to `false`, even + // if the smart order as a whole acts like a partially fillable order. + // This is done since, once a settlement commits to a specific sell + // amount, then it is expected to use it completely and not partially. + order.partiallyFillable = false; + order.sellTokenBalance = GPv2Order.BALANCE_ERC20; + order.buyTokenBalance = GPv2Order.BALANCE_ERC20; + } + + function buyAmountForSellAmount(uint256 sellAmount) private view returns (uint256 buyAmount) { + uint256 feeAdjustedBalance = + sellToken.balanceOf(address(this)).mul(totalSellAmount).div(totalSellAmount.add(totalFeeAmount)); + uint256 soldAmount = totalSellAmount > feeAdjustedBalance ? totalSellAmount - feeAdjustedBalance : 0; + + // NOTE: This is currently a silly price strategy where the xrate + // increases linearly from 1:1 to 1:2 as the smart order gets filled. + // This can be extended to more complex "price curves". + buyAmount = sellAmount.mul(totalSellAmount.add(sellAmount).add(soldAmount)).div(totalSellAmount); + } +} + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +contract SmartOrderTest is Helper(false) { + IERC20Mintable token1; + IERC20Mintable token2; + + function setUp() public override { + super.setUp(); + + token1 = deployMintableErc20("TK1", "TK1"); + token2 = deployMintableErc20("TK2", "TK2"); + } + + function test_permits_trader_allowance_with_settlement() external { + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + + // mint some tokens + token1.mint(trader1.addr, 1.01 ether); + vm.prank(trader1.addr); + // approve tokens to vault relayer + token1.approve(vaultRelayer, type(uint256).max); + // place order to buy 0.5 token2 with 1 token1 max + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellToken: token1, + buyToken: token2, + sellAmount: 1 ether, + buyAmount: 0.5 ether, + feeAmount: 0.01 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + vm.prank(trader2.addr); + SmartSellOrder smartOrder = new SmartSellOrder(settlement, token2, token1, 0xffffffff, 1 ether, 0.1 ether); + token2.mint(trader2.addr, 1.1 ether); + vm.prank(trader2.addr); + token2.transfer(address(smartOrder), 1.1 ether); + + uint256 smartOrderSellAmount = 0.5 ether; + GPv2Order.Data memory smartOrderTrade = smartOrder.orderForSellAmount(smartOrderSellAmount); + GPv2Order.Data memory expectedOrder = GPv2Order.Data({ + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellToken: token2, + buyToken: token1, + receiver: trader2.addr, + sellAmount: smartOrderSellAmount, + buyAmount: 0.75 ether, + feeAmount: 0.05 ether, + validTo: 0xffffffff, + appData: smartOrder.APPDATA(), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + assertEq( + keccak256(abi.encode(smartOrderTrade)), keccak256(abi.encode(expectedOrder)), "smart order not as expected" + ); + + encoder.encodeTrade( + smartOrderTrade, + Sign.Signature({ + scheme: GPv2Signing.Scheme.Eip1271, + data: abi.encodePacked(address(smartOrder), abi.encode(smartOrderSellAmount)) + }), + 0 + ); + + // set token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = token1; + tokens[1] = token2; + uint256[] memory prices = new uint256[](2); + prices[0] = 10; + prices[1] = 15; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + + vm.prank(solver); + settle(encodedSettlement); + + assertEq(token1.balanceOf(trader2.addr), 0.75 ether); + } +} diff --git a/test/e2e/UniswapTrade.t.sol b/test/e2e/UniswapTrade.t.sol new file mode 100644 index 00000000..ab71c6f2 --- /dev/null +++ b/test/e2e/UniswapTrade.t.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {GPv2Interaction} from "src/contracts/libraries/GPv2Interaction.sol"; +import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; +import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; + +import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +interface IUniswapV2Factory { + function createPair(address, address) external returns (address); +} + +interface IUniswapV2Pair { + function token0() external view returns (address); + function mint(address) external; + function swap(uint256, uint256, address, bytes calldata) external; + function getReserves() external view returns (uint256, uint256); +} + +contract UniswapTradeTest is Helper(true) { + IERC20Mintable dai; + IERC20Mintable wETH; + + IUniswapV2Factory factory; + IUniswapV2Pair uniswapPair; + + bool isWethToken0; + + function setUp() public override { + super.setUp(); + + dai = deployMintableErc20("dai", "dai"); + wETH = deployMintableErc20("wETH", "wETH"); + + factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); + uniswapPair = IUniswapV2Pair(factory.createPair(address(wETH), address(dai))); + + isWethToken0 = uniswapPair.token0() == address(wETH); + } + + // Settles the following batch: + // + // /----(1. SELL 1 wETH for dai if p(wETH) >= 500)-----\ + // | | + // | v + // [dai]<---(Uniswap Pair 1000 wETH / 600.000 dai)--->[wETH] + // ^ | + // | | + // \----(2. BUY 0.5 wETH for dai if p(wETH) <= 600)----/ + function test_should_two_overlapping_orders_and_trade_surplus_with_uniswap() external { + uint256 wethReserve = 1000 ether; + uint256 daiReserve = 600000 ether; + wETH.mint(address(uniswapPair), wethReserve); + dai.mint(address(uniswapPair), daiReserve); + uniswapPair.mint(address(this)); + + // The current batch has a sell order selling 1 wETH and a buy order buying + // 0.5 wETH. This means there is exactly a surplus 0.5 wETH that needs to be + // sold to Uniswap. Uniswap is governed by a balancing equation which can be + // used to compute the exact buy amount for selling the 0.5 wETH and we can + // use to build our the settlement with a smart contract interaction. + // ``` + // (reservewETH + inwETH * 0.997) * (reservedai - outdai) = reservewETH * reservedai + // outdai = (reservedai * inwETH * 0.997) / (reservewETH + inwETH * 0.997) + // = (reservedai * inwETH * 997) / (reservewETH * 1000 + inwETH * 997) + // ``` + uint256 uniswapWethInAmount = 0.5 ether; + uint256 uniswapDaiOutAmount = + daiReserve * uniswapWethInAmount * 997 / ((wethReserve * 1000) + (uniswapWethInAmount * 997)); + + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + + // mint some weth + wETH.mint(trader1.addr, 1.001 ether); + vm.prank(trader1.addr); + wETH.approve(vaultRelayer, type(uint256).max); + + // place order to sell 1 wETH for min 500 dai + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellToken: wETH, + buyToken: dai, + sellAmount: 1 ether, + buyAmount: 500 ether, + feeAmount: 0.001 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some dai + dai.mint(trader2.addr, 300.3 ether); + vm.prank(trader2.addr); + dai.approve(vaultRelayer, type(uint256).max); + + // place order to buy 0.5 wETH for max 300 dai + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellToken: dai, + buyToken: wETH, + sellAmount: 300 ether, + buyAmount: 0.5 ether, + feeAmount: 0.3 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // interaction to swap the remainder on uniswap + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(wETH), + value: 0, + callData: abi.encodeCall(IERC20.transfer, (address(uniswapPair), uniswapWethInAmount)) + }), + SettlementEncoder.InteractionStage.INTRA + ); + (uint256 amount0Out, uint256 amount1Out) = + isWethToken0 ? (uint256(0), uniswapDaiOutAmount) : (uniswapDaiOutAmount, uint256(0)); + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(uniswapPair), + value: 0, + callData: abi.encodeCall(IUniswapV2Pair.swap, (amount0Out, amount1Out, address(settlement), hex"")) + }), + SettlementEncoder.InteractionStage.INTRA + ); + + // set token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = wETH; + tokens[1] = dai; + uint256[] memory prices = new uint256[](2); + prices[0] = uniswapDaiOutAmount; + prices[1] = uniswapWethInAmount; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + + vm.prank(solver); + settle(encodedSettlement); + + assertEq(wETH.balanceOf(address(settlement)), 0.001 ether, "weth fees not as expected"); + assertEq(dai.balanceOf(address(settlement)), 0.3 ether, "dai fees not as expected"); + + assertEq(wETH.balanceOf(trader1.addr), 0, "not all weth sold"); + assertEq(dai.balanceOf(trader1.addr), uniswapDaiOutAmount * 2, "dai received not as expected"); + + assertEq(wETH.balanceOf(trader2.addr), 0.5 ether, "weth bought not correct amount"); + assertEq(dai.balanceOf(trader2.addr), 300.3 ether - (uniswapDaiOutAmount + 0.3 ether)); + + uint256 finalWethReserve; + uint256 finalDaiReserve; + + { + (uint256 token0Reserve, uint256 token1Reserve) = uniswapPair.getReserves(); + (finalWethReserve, finalDaiReserve) = + isWethToken0 ? (token0Reserve, token1Reserve) : (token1Reserve, token0Reserve); + } + assertEq(finalWethReserve, wethReserve + uniswapWethInAmount, "weth reserve not as expected"); + assertEq(finalDaiReserve, daiReserve - uniswapDaiOutAmount, "dai reserve not as expected"); + } +} diff --git a/test/e2e/UpgradeAuthenticator.t.sol b/test/e2e/UpgradeAuthenticator.t.sol new file mode 100644 index 00000000..b1319696 --- /dev/null +++ b/test/e2e/UpgradeAuthenticator.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {GPv2AllowListAuthentication} from "src/contracts/GPv2AllowListAuthentication.sol"; + +import {Helper} from "./Helper.sol"; + +interface IEIP173Proxy { + function upgradeTo(address) external; + function transferOwnership(address) external; + function owner() external view returns (address); +} + +contract GPv2AllowListAuthenticationV2 is GPv2AllowListAuthentication { + function newMethod() external pure returns (uint256) { + return 1337; + } +} + +contract UpgradeAuthenticatorTest is Helper(false) { + GPv2AllowListAuthenticationV2 v2Impl; + + function setUp() public override { + super.setUp(); + v2Impl = new GPv2AllowListAuthenticationV2(); + } + + function test_should_upgrade_authenticator() external { + vm.expectRevert(); + GPv2AllowListAuthenticationV2(address(authenticator)).newMethod(); + + vm.prank(owner); + IEIP173Proxy(address(authenticator)).upgradeTo(address(v2Impl)); + + assertEq( + GPv2AllowListAuthenticationV2(address(authenticator)).newMethod(), 1337, "proxy didnt update as expected" + ); + } + + function test_should_preserve_storage() external { + address newSolver = makeAddr("newSolver"); + address newManager = makeAddr("newManager"); + + vm.startPrank(owner); + GPv2AllowListAuthentication(address(authenticator)).addSolver(newSolver); + GPv2AllowListAuthentication(address(authenticator)).setManager(newManager); + + IEIP173Proxy(address(authenticator)).upgradeTo(address(v2Impl)); + vm.stopPrank(); + + assertEq(authenticator.isSolver(newSolver), true, "solver not retained in storage after proxy upgrade"); + assertEq( + GPv2AllowListAuthentication(address(authenticator)).manager(), + newManager, + "manager not retained in storage after proxy upgrade" + ); + } + + function test_should_allow_proxy_owner_to_change_manager() external { + // transfer ownership to a new address and then assert the behavior + // to have a proxy owner that is different address than manager + address newOwner = makeAddr("newOwner"); + vm.prank(owner); + IEIP173Proxy(address(authenticator)).transferOwnership(newOwner); + + address newManager = makeAddr("newManager"); + vm.prank(newOwner); + GPv2AllowListAuthentication(address(authenticator)).setManager(newManager); + + assertEq( + GPv2AllowListAuthentication(address(authenticator)).manager(), + newManager, + "proxy owner couldnt update manager" + ); + } + + function test_should_be_able_to_transfer_proxy_ownership() external { + address newOwner = makeAddr("newOwner"); + vm.prank(owner); + IEIP173Proxy(address(authenticator)).transferOwnership(newOwner); + + assertEq(IEIP173Proxy(address(authenticator)).owner(), newOwner, "ownership didnt transfer as expected"); + } + + function test_should_revert_when_upgrading_with_the_authentication_manager() external { + address newManager = makeAddr("newManager"); + vm.prank(owner); + GPv2AllowListAuthentication(address(authenticator)).setManager(newManager); + + vm.prank(newManager); + vm.expectRevert("NOT_AUTHORIZED"); + IEIP173Proxy(address(authenticator)).upgradeTo(address(v2Impl)); + } + + function test_should_revert_when_not_upgrading_with_the_proxy_owner() external { + address nobody = makeAddr("nobody"); + vm.prank(nobody); + vm.expectRevert("NOT_AUTHORIZED"); + IEIP173Proxy(address(authenticator)).upgradeTo(address(v2Impl)); + } +} diff --git a/test/e2e/WETH9.sol b/test/e2e/WETH9.sol new file mode 100644 index 00000000..71f4586a --- /dev/null +++ b/test/e2e/WETH9.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/test/e2e/WineOilMarket.t.sol b/test/e2e/WineOilMarket.t.sol new file mode 100644 index 00000000..7a73423f --- /dev/null +++ b/test/e2e/WineOilMarket.t.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {GPv2Order, GPv2Signing, SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +contract WineOilTest is Helper(false) { + IERC20Mintable eur; + IERC20Mintable oil; + IERC20Mintable wine; + + uint256 constant STARTING_BALANCE = 1000 ether; + + function setUp() public override { + super.setUp(); + + eur = deployMintableErc20("eur", "eur"); + oil = deployMintableErc20("oil", "oil"); + wine = deployMintableErc20("wine", "wine"); + } + + // Settlement for the RetrETH wine and olive oil market: + // + // /---(6. BUY 10 🍷 with 💶 if p(🍷) <= 13)--> [🍷] + // | | + // | | + // [💶] (1. SELL 12 🍷 for 🫒 if p(🍷) >= p(🫒)) + // |^ | + // || | + // |\--(4. SELL 15 🫒 for 💶 if p(🫒) >= 12)--\ v + // \---(5. BUY 4 🫒 with 💶 if p(🫒) <= 13)---> [🫒] + function test_should_settle_red_wine_and_olive_oil_market() external { + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + Vm.Wallet memory trader3 = vm.createWallet("trader3"); + Vm.Wallet memory trader4 = vm.createWallet("trader4"); + uint256 feeAmount = 1 ether; + + // sell 12 wine for min 12 oil + _createOrder( + trader1, + _orderData({ + sellToken: wine, + buyToken: oil, + sellAmount: 12 ether, + buyAmount: 12 ether, + feeAmount: feeAmount, + orderKind: GPv2Order.KIND_SELL, + partiallyFillable: false + }), + 0 + ); + // sell 15 oil for min 180 eur + _createOrder( + trader2, + _orderData({ + sellToken: oil, + buyToken: eur, + sellAmount: 15 ether, + buyAmount: 180 ether, + feeAmount: feeAmount, + orderKind: GPv2Order.KIND_SELL, + partiallyFillable: false + }), + 0 + ); + // buy 4 oil with max 52 eur + uint256 order3ExecutedAmount = uint256(27 ether) / 13; + _createOrder( + trader3, + _orderData({ + sellToken: eur, + buyToken: oil, + sellAmount: 52 ether, + buyAmount: 4 ether, + feeAmount: feeAmount, + orderKind: GPv2Order.KIND_BUY, + partiallyFillable: true + }), + order3ExecutedAmount + ); + // buy 20 wine with max 280 eur + uint256 order4ExecutedAmount = 12 ether; + _createOrder( + trader4, + _orderData({ + sellToken: eur, + buyToken: wine, + sellAmount: 280 ether, + buyAmount: 20 ether, + feeAmount: feeAmount, + orderKind: GPv2Order.KIND_BUY, + partiallyFillable: true + }), + order4ExecutedAmount + ); + + uint256 oilPrice = 13 ether; + uint256 winePrice = 14 ether; + { + // set token prices + IERC20[] memory tokens = new IERC20[](3); + tokens[0] = eur; + tokens[1] = oil; + tokens[2] = wine; + uint256[] memory prices = new uint256[](3); + prices[0] = 1 ether; + prices[1] = oilPrice; + prices[2] = winePrice; + + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + } + + // settle the orders + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + vm.prank(solver); + settle(encodedSettlement); + + assertEq( + wine.balanceOf(trader1.addr), + STARTING_BALANCE - 12 ether - feeAmount, + "trader1 sold token amounts not as expected" + ); + uint256 trader1AmountOut = ceilDiv(uint256(12 ether * 14 ether), 13 ether); + assertEq(oil.balanceOf(trader1.addr), trader1AmountOut, "trader1 amountOut not as expected"); + + assertEq( + oil.balanceOf(trader2.addr), + STARTING_BALANCE - 15 ether - feeAmount, + "trader2 sold token amounts not as expected" + ); + assertEq(eur.balanceOf(trader2.addr), 15 ether * 13, "trader2 amountOut not as expected"); + + // order: buy 4 oil with max 52 eur, partial execution + uint256 order3SellAmount = order3ExecutedAmount * oilPrice / 1 ether; + uint256 order3FeeAmount = feeAmount * order3ExecutedAmount / 4 ether; + assertEq( + eur.balanceOf(trader3.addr), + STARTING_BALANCE - order3SellAmount - order3FeeAmount, + "trader3 sold token amount not as expected" + ); + assertEq(oil.balanceOf(trader3.addr), order3ExecutedAmount, "trader3 amountOut not as expected"); + + // order: buy 20 wine with max 280 eur, partial execution + uint256 order4SellAmount = order4ExecutedAmount * winePrice / 1 ether; + uint256 order4FeeAmount = feeAmount * order4ExecutedAmount / 20 ether; + assertEq( + eur.balanceOf(trader4.addr), + STARTING_BALANCE - order4SellAmount - order4FeeAmount, + "trader4 sold token amount not as expected" + ); + assertEq(wine.balanceOf(trader4.addr), order4ExecutedAmount, "trader4 amountOut not as expected"); + } + + function _createOrder(Vm.Wallet memory wallet, GPv2Order.Data memory order, uint256 executedAmount) internal { + IERC20Mintable(address(order.sellToken)).mint(wallet.addr, STARTING_BALANCE); + vm.prank(wallet.addr); + order.sellToken.approve(vaultRelayer, type(uint256).max); + + encoder.signEncodeTrade(vm, wallet, order, domainSeparator, GPv2Signing.Scheme.Eip712, executedAmount); + } + + function _orderData( + IERC20 sellToken, + IERC20 buyToken, + uint256 sellAmount, + uint256 buyAmount, + uint256 feeAmount, + bytes32 orderKind, + bool partiallyFillable + ) internal pure returns (GPv2Order.Data memory order) { + order = GPv2Order.Data({ + sellToken: sellToken, + buyToken: buyToken, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, + sellAmount: sellAmount, + buyAmount: buyAmount, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + feeAmount: feeAmount, + kind: orderKind, + partiallyFillable: partiallyFillable, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + } + + function ceilDiv(uint256 num, uint256 den) internal pure returns (uint256) { + return num % den == 0 ? num / den : (num / den) + 1; + } +} diff --git a/test/e2e/burnFees.test.ts b/test/e2e/burnFees.test.ts deleted file mode 100644 index af76b2d0..00000000 --- a/test/e2e/burnFees.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - InteractionStage, - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Burn fees", () => { - let deployer: Wallet; - let solver: Wallet; - let traders: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let owl: Contract; - let dai: Contract; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - settlement, - vaultRelayer, - wallets: [solver, ...traders], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - owl = await waffle.deployContract(deployer, ERC20, ["OWL", "Owl token"]); - dai = await waffle.deployContract(deployer, ERC20, ["DAI", "Dai token"]); - }); - - it("uses post-interation to burn settlement fees", async () => { - // Settle a trivial 1:1 trade between DAI and OWL. - - const ONE_USD = ethers.utils.parseEther("1.0"); - - const encoder = new SettlementEncoder(domainSeparator); - - await owl.mint(traders[0].address, ONE_USD.mul(1001)); - await owl - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: owl.address, - buyToken: dai.address, - sellAmount: ONE_USD.mul(1000), - buyAmount: ONE_USD.mul(1000), - feeAmount: ONE_USD, - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - await dai.mint(traders[1].address, ONE_USD.mul(1000)); - await dai - .connect(traders[1]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: owl.address, - sellToken: dai.address, - buyAmount: ONE_USD.mul(1000), - sellAmount: ONE_USD.mul(1000), - feeAmount: ethers.constants.Zero, - validTo: 0xffffffff, - appData: 2, - }, - traders[1], - SigningScheme.EIP712, - ); - - encoder.encodeInteraction( - { - target: owl.address, - callData: owl.interface.encodeFunctionData("burn", [ONE_USD]), - }, - InteractionStage.POST, - ); - - const tx = settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [owl.address]: 1, - [dai.address]: 1, - }), - ); - - await expect(tx) - .to.emit(owl, "Transfer") - .withArgs(settlement.address, ethers.constants.AddressZero, ONE_USD); - expect(await dai.balanceOf(settlement.address)).to.deep.equal( - ethers.constants.Zero, - ); - }); -}); diff --git a/test/e2e/buyEth.test.ts b/test/e2e/buyEth.test.ts deleted file mode 100644 index bfcb6eda..00000000 --- a/test/e2e/buyEth.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - BUY_ETH_ADDRESS, - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Buy Ether", () => { - let deployer: Wallet; - let solver: Wallet; - let traders: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let weth: Contract; - let usdt: Contract; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - weth, - settlement, - vaultRelayer, - wallets: [solver, ...traders], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - usdt = await waffle.deployContract(deployer, ERC20, ["USDT", 6]); - }); - - it("should unwrap WETH for orders buying Ether", async () => { - // Settle a trivial batch between two overlapping trades: - // - // /----(1. SELL 1 WETH for USDT if p(WETH) >= 1100)----\ - // | v - // [USDT] [(W)ETH] - // ^ | - // \-----(2. BUY 1 ETH for USDT if p(WETH) <= 1200)-----/ - - const encoder = new SettlementEncoder(domainSeparator); - - await weth - .connect(traders[0]) - .deposit({ value: ethers.utils.parseEther("1.001") }); - await weth - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: weth.address, - buyToken: usdt.address, - sellAmount: ethers.utils.parseEther("1.0"), - buyAmount: ethers.utils.parseUnits("1100.0", 6), - feeAmount: ethers.utils.parseEther("0.001"), - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - await usdt.mint(traders[1].address, ethers.utils.parseUnits("1201.2", 6)); - await usdt - .connect(traders[1]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: BUY_ETH_ADDRESS, - sellToken: usdt.address, - buyAmount: ethers.utils.parseEther("1.0"), - sellAmount: ethers.utils.parseUnits("1200.0", 6), - feeAmount: ethers.utils.parseUnits("1.2", 6), - validTo: 0xffffffff, - appData: 2, - }, - traders[1], - SigningScheme.EIP712, - ); - - encoder.encodeInteraction({ - target: weth.address, - callData: weth.interface.encodeFunctionData("withdraw", [ - ethers.utils.parseEther("1.0"), - ]), - }); - - const trader1InitialBalance = await traders[1].getBalance(); - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [weth.address]: ethers.utils.parseUnits("1150.0", 6), - [BUY_ETH_ADDRESS]: ethers.utils.parseUnits("1150.0", 6), - [usdt.address]: ethers.utils.parseEther("1.0"), - }), - ); - - expect(await weth.balanceOf(settlement.address)).to.deep.equal( - ethers.utils.parseEther("0.001"), - ); - expect(await weth.balanceOf(traders[0].address)).to.deep.equal( - ethers.constants.Zero, - ); - expect(await traders[1].getBalance()).to.deep.equal( - trader1InitialBalance.add(ethers.utils.parseEther("1.0")), - ); - }); -}); diff --git a/test/e2e/contractOrdersWithGnosisSafe.test.ts b/test/e2e/contractOrdersWithGnosisSafe.test.ts deleted file mode 100644 index 9d5a6093..00000000 --- a/test/e2e/contractOrdersWithGnosisSafe.test.ts +++ /dev/null @@ -1,302 +0,0 @@ -import GnosisSafe from "@gnosis.pm/safe-contracts/build/artifacts/contracts/GnosisSafe.sol/GnosisSafe.json"; -import CompatibilityFallbackHandler from "@gnosis.pm/safe-contracts/build/artifacts/contracts/handler/CompatibilityFallbackHandler.sol/CompatibilityFallbackHandler.json"; -import GnosisSafeProxyFactory from "@gnosis.pm/safe-contracts/build/artifacts/contracts/proxies/GnosisSafeProxyFactory.sol/GnosisSafeProxyFactory.json"; -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import { expect } from "chai"; -import { BytesLike, Signer, Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - EIP1271_MAGICVALUE, - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, - hashOrder, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -interface SafeTransaction { - to: string; - data: BytesLike; -} - -class GnosisSafeManager { - constructor( - readonly deployer: Signer, - readonly masterCopy: Contract, - readonly signingFallback: Contract, - readonly proxyFactory: Contract, - ) {} - - static async init(deployer: Signer): Promise { - const masterCopy = await waffle.deployContract(deployer, GnosisSafe); - const proxyFactory = await waffle.deployContract( - deployer, - GnosisSafeProxyFactory, - ); - const signingFallback = await waffle.deployContract( - deployer, - CompatibilityFallbackHandler, - ); - return new GnosisSafeManager( - deployer, - masterCopy, - signingFallback, - proxyFactory, - ); - } - - async newSafe( - owners: string[], - threshold: number, - fallback = ethers.constants.AddressZero, - ): Promise { - const proxyAddress = await this.proxyFactory.callStatic.createProxy( - this.masterCopy.address, - "0x", - ); - await this.proxyFactory.createProxy(this.masterCopy.address, "0x"); - const safe = await ethers.getContractAt(GnosisSafe.abi, proxyAddress); - await safe.setup( - owners, - threshold, - ethers.constants.AddressZero, - "0x", - fallback, - ethers.constants.AddressZero, - 0, - ethers.constants.AddressZero, - ); - return safe; - } -} - -async function gnosisSafeSign( - message: BytesLike, - signers: Signer[], -): Promise { - // https://docs.gnosis.io/safe/docs/contracts_signatures/ - const signerAddresses = await Promise.all( - signers.map(async (signer) => (await signer.getAddress()).toLowerCase()), - ); - const sortedSigners = signers - .map((_, index) => index) - .sort((lhs, rhs) => - signerAddresses[lhs] < signerAddresses[rhs] - ? -1 - : signerAddresses[lhs] > signerAddresses[rhs] - ? 1 - : 0, - ) - .map((index) => signers[index]); - - async function encodeEcdsaSignature( - message: BytesLike, - signer: Signer, - ): Promise { - const sig = await signer.signMessage(ethers.utils.arrayify(message)); - const { r, s, v } = ethers.utils.splitSignature(sig); - return ethers.utils.hexConcat([r, s, [v + 4]]); - } - return ethers.utils.hexConcat( - await Promise.all( - sortedSigners.map( - async (signer) => await encodeEcdsaSignature(message, signer), - ), - ), - ); -} - -async function execSafeTransaction( - safe: Contract, - transaction: SafeTransaction, - signers: Signer[], -): Promise { - // most parameters are not needed for this test - const transactionParameters = [ - transaction.to, - 0, - transaction.data, - 0, - 0, - 0, - 0, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ]; - const nonce = await safe.nonce(); - const message = await safe.getTransactionHash( - ...transactionParameters, - nonce, - ); - const sigs = await gnosisSafeSign(message, signers); - await safe.execTransaction(...transactionParameters, sigs); -} - -async function fallbackSign( - safeAsFallback: Contract, - message: BytesLike, - signers: Signer[], -): Promise { - const safeMessage = await safeAsFallback.getMessageHash( - ethers.utils.defaultAbiCoder.encode(["bytes32"], [message]), - ); - return gnosisSafeSign(safeMessage, signers); -} - -describe("E2E: Order From A Gnosis Safe", () => { - let deployer: Wallet; - let solver: Wallet; - let trader: Wallet; - let safeOwners: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let safe: Contract; - let domainSeparator: TypedDataDomain; - let gnosisSafeManager: GnosisSafeManager; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - settlement, - vaultRelayer, - wallets: [solver, trader, ...safeOwners], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - gnosisSafeManager = await GnosisSafeManager.init(deployer); - safe = await gnosisSafeManager.newSafe( - safeOwners.map((wallet) => wallet.address), - 2, - gnosisSafeManager.signingFallback.address, - ); - }); - - it("should settle matching orders", async () => { - // EOA trader: sell 1 WETH for 900 DAI - // Safe: buy 1 WETH for 1100 DAI - // Settlement price at 1000 DAI for 1 WETH. - - const erc20 = (symbol: string) => - waffle.deployContract(deployer, ERC20, [symbol, 18]); - - const dai = await erc20("DAI"); - const weth = await erc20("WETH"); - - const UNLIMITED_VALID_TO = 0xffffffff; - const encoder = new SettlementEncoder(domainSeparator); - - const TRADER_FEE = ethers.utils.parseEther("0.001"); - const TRADER_SOLD_AMOUNT = ethers.utils.parseEther("1.0"); - const TRADER_BOUGHT_AMOUNT = ethers.utils.parseEther("900.0"); - - await weth.mint(trader.address, TRADER_SOLD_AMOUNT.add(TRADER_FEE)); - await weth - .connect(trader) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - - encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: weth.address, - buyToken: dai.address, - sellAmount: TRADER_SOLD_AMOUNT, - buyAmount: TRADER_BOUGHT_AMOUNT, - appData: 0, - validTo: UNLIMITED_VALID_TO, - feeAmount: TRADER_FEE, - }, - trader, - SigningScheme.ETHSIGN, - ); - - const SAFE_FEE = ethers.utils.parseEther("10.0"); - const SAFE_SOLD_AMOUNT = ethers.utils.parseEther("1100.0"); - const SAFE_BOUGHT_AMOUNT = ethers.utils.parseEther("1.0"); - - await dai.mint(safe.address, SAFE_SOLD_AMOUNT.add(SAFE_FEE)); - const approveTransaction = { - to: dai.address, - data: dai.interface.encodeFunctionData("approve", [ - vaultRelayer.address, - ethers.constants.MaxUint256, - ]), - }; - await execSafeTransaction(safe, approveTransaction, safeOwners); - expect( - await dai.allowance(safe.address, vaultRelayer.address), - ).to.deep.equal(ethers.constants.MaxUint256); - - const order = { - kind: OrderKind.BUY, - partiallyFillable: false, - sellToken: dai.address, - buyToken: weth.address, - sellAmount: SAFE_SOLD_AMOUNT, - buyAmount: SAFE_BOUGHT_AMOUNT, - appData: 0, - validTo: UNLIMITED_VALID_TO, - feeAmount: SAFE_FEE, - }; - const gpv2Message = hashOrder(domainSeparator, order); - const safeAsFallback = gnosisSafeManager.signingFallback.attach( - safe.address, - ); - // Note: threshold is 2, any two owners should suffice. - const signature = await fallbackSign(safeAsFallback, gpv2Message, [ - safeOwners[4], - safeOwners[2], - ]); - // Note: the fallback handler provides two versions of `isValidSignature` - // for compatibility reasons. The following is the signature of the most - // recent EIP1271-compatible verification function. - const isValidSignature = "isValidSignature(bytes32,bytes)"; - expect( - await safeAsFallback.callStatic[isValidSignature](gpv2Message, signature), - ).to.equal(EIP1271_MAGICVALUE); - - encoder.encodeTrade(order, { - scheme: SigningScheme.EIP1271, - data: { - verifier: safe.address, - signature, - }, - }); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [dai.address]: ethers.utils.parseEther("1.0"), - [weth.address]: ethers.utils.parseEther("1000.0"), - }), - ); - - expect(await weth.balanceOf(trader.address)).to.deep.equal( - ethers.constants.Zero, - ); - expect(await dai.balanceOf(trader.address)).to.deep.equal( - ethers.utils.parseEther("1000.0"), - ); - - expect(await weth.balanceOf(safe.address)).to.deep.equal( - ethers.utils.parseEther("1.0"), - ); - expect(await dai.balanceOf(safe.address)).to.deep.equal( - SAFE_SOLD_AMOUNT.sub(ethers.utils.parseEther("1000.0")), - ); - - expect(await weth.balanceOf(settlement.address)).to.deep.equal(TRADER_FEE); - expect(await dai.balanceOf(settlement.address)).to.deep.equal(SAFE_FEE); - }); -}); diff --git a/test/e2e/deployment.test.ts b/test/e2e/deployment.test.ts deleted file mode 100644 index 394c2c53..00000000 --- a/test/e2e/deployment.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { artifacts, ethers } from "hardhat"; -import Proxy from "hardhat-deploy/extendedArtifacts/EIP173Proxy.json"; - -import { - ContractName, - DeploymentArguments, - deterministicDeploymentAddress, - implementationAddress, - proxyInterface, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -async function contractAddress( - contractName: C, - ...deploymentArguments: DeploymentArguments -): Promise { - const artifact = await artifacts.readArtifact(contractName); - return deterministicDeploymentAddress(artifact, deploymentArguments); -} - -async function builtAndDeployedMetadataCoincide( - contractAddress: string, - contractName: string, -): Promise { - const contractArtifacts = await artifacts.readArtifact(contractName); - - const code = await ethers.provider.send("eth_getCode", [ - contractAddress, - "latest", - ]); - - // NOTE: The last 53 bytes in a deployed contract's bytecode contains the - // contract metadata. Compare the deployed contract's metadata with the - // compiled contract's metadata. - // - const metadata = (bytecode: string) => bytecode.slice(-106); - - return metadata(code) === metadata(contractArtifacts.deployedBytecode); -} - -describe("E2E: Deployment", () => { - let owner: Wallet; - let manager: Wallet; - let user: Wallet; - - let authenticator: Contract; - let vault: Contract; - let settlement: Contract; - let vaultRelayer: Contract; - - beforeEach(async () => { - ({ - owner, - manager, - wallets: [user], - authenticator, - vault, - settlement, - vaultRelayer, - } = await deployTestContracts()); - - authenticator.connect(user); - settlement.connect(user); - vaultRelayer.connect(user); - }); - - describe("same built and deployed bytecode metadata", () => { - it("authenticator", async () => { - expect( - await builtAndDeployedMetadataCoincide( - await implementationAddress(ethers.provider, authenticator.address), - "GPv2AllowListAuthentication", - ), - ).to.be.true; - }); - - it("settlement", async () => { - expect( - await builtAndDeployedMetadataCoincide( - settlement.address, - "GPv2Settlement", - ), - ).to.be.true; - }); - - it("vaultRelayer", async () => { - expect( - await builtAndDeployedMetadataCoincide( - vaultRelayer.address, - "GPv2VaultRelayer", - ), - ).to.be.true; - }); - }); - - describe("deterministic addresses", () => { - describe("authenticator", () => { - it("proxy", async () => { - expect( - deterministicDeploymentAddress(Proxy, [ - await implementationAddress(ethers.provider, authenticator.address), - owner.address, - authenticator.interface.encodeFunctionData("initializeManager", [ - manager.address, - ]), - ]), - ).to.equal(authenticator.address); - }); - - it("implementation", async () => { - expect(await contractAddress("GPv2AllowListAuthentication")).to.equal( - await implementationAddress(ethers.provider, authenticator.address), - ); - }); - }); - - it("settlement", async () => { - expect( - await contractAddress( - "GPv2Settlement", - authenticator.address, - vault.address, - ), - ).to.equal(settlement.address); - }); - }); - - describe("authorization", () => { - it("authenticator has dedicated owner", async () => { - const proxy = proxyInterface(authenticator); - expect(await proxy.owner()).to.equal(owner.address); - }); - - it("authenticator has dedicated manager", async () => { - expect(await authenticator.manager()).to.equal(manager.address); - }); - }); -}); diff --git a/test/e2e/internalBalances.test.ts b/test/e2e/internalBalances.test.ts deleted file mode 100644 index 8cfdedb6..00000000 --- a/test/e2e/internalBalances.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - OrderBalance, - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, - grantRequiredRoles, -} from "../../src/ts"; -import { UserBalanceOpKind } from "../balancer"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Should allow trading with Vault internal balances", () => { - let deployer: Wallet; - let solver: Wallet; - let traders: Wallet[]; - - let vault: Contract; - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let tokens: [Contract, Contract]; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - vault, - settlement, - vaultRelayer, - wallets: [solver, ...traders], - } = deployment); - - const { vaultAuthorizer, authenticator, manager } = deployment; - await grantRequiredRoles( - vaultAuthorizer.connect(manager), - vault.address, - vaultRelayer.address, - ); - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - tokens = [ - await waffle.deployContract(deployer, ERC20, ["T0", 18]), - await waffle.deployContract(deployer, ERC20, ["T1", 18]), - ]; - - await settlement.connect(solver).settle( - ...SettlementEncoder.encodedSetup( - ...tokens.map((token) => ({ - target: token.address, - callData: token.interface.encodeFunctionData("approve", [ - vault.address, - ethers.constants.MaxUint256, - ]), - })), - ), - ); - }); - - it("should settle orders buying and selling with internal balances", async () => { - const encoder = new SettlementEncoder(domainSeparator); - - await tokens[0].mint(traders[0].address, ethers.utils.parseEther("1.001")); - await tokens[0] - .connect(traders[0]) - .approve(vault.address, ethers.constants.MaxUint256); - await vault - .connect(traders[0]) - .setRelayerApproval(traders[0].address, vaultRelayer.address, true); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: tokens[0].address, - buyToken: tokens[1].address, - sellAmount: ethers.utils.parseEther("1.0"), - buyAmount: ethers.utils.parseEther("500.0"), - feeAmount: ethers.utils.parseEther("0.001"), - validTo: 0xffffffff, - appData: 1, - sellTokenBalance: OrderBalance.EXTERNAL, - }, - traders[0], - SigningScheme.EIP712, - ); - - await tokens[1].mint(traders[1].address, ethers.utils.parseEther("300.3")); - await tokens[1] - .connect(traders[1]) - .approve(vault.address, ethers.constants.MaxUint256); - await vault.connect(traders[1]).manageUserBalance([ - { - kind: UserBalanceOpKind.DEPOSIT_INTERNAL, - asset: tokens[1].address, - amount: ethers.utils.parseEther("300.3"), - sender: traders[1].address, - recipient: traders[1].address, - }, - ]); - await vault - .connect(traders[1]) - .setRelayerApproval(traders[1].address, vaultRelayer.address, true); - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: tokens[0].address, - sellToken: tokens[1].address, - buyAmount: ethers.utils.parseEther("0.5"), - sellAmount: ethers.utils.parseEther("300.0"), - feeAmount: ethers.utils.parseEther("0.3"), - validTo: 0xffffffff, - appData: 2, - sellTokenBalance: OrderBalance.INTERNAL, - }, - traders[1], - SigningScheme.EIP712, - ); - - await tokens[0].mint(traders[2].address, ethers.utils.parseEther("2.002")); - await tokens[0] - .connect(traders[2]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: tokens[0].address, - buyToken: tokens[1].address, - sellAmount: ethers.utils.parseEther("2.0"), - buyAmount: ethers.utils.parseEther("1000.0"), - feeAmount: ethers.utils.parseEther("0.002"), - validTo: 0xffffffff, - appData: 2, - buyTokenBalance: OrderBalance.INTERNAL, - }, - traders[2], - SigningScheme.EIP712, - ); - - await tokens[1].mint(traders[3].address, ethers.utils.parseEther("1501.5")); - await tokens[1] - .connect(traders[3]) - .approve(vault.address, ethers.constants.MaxUint256); - await vault.connect(traders[3]).manageUserBalance([ - { - kind: UserBalanceOpKind.DEPOSIT_INTERNAL, - asset: tokens[1].address, - amount: ethers.utils.parseEther("1501.5"), - sender: traders[3].address, - recipient: traders[3].address, - }, - ]); - await vault - .connect(traders[3]) - .setRelayerApproval(traders[3].address, vaultRelayer.address, true); - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: tokens[0].address, - sellToken: tokens[1].address, - buyAmount: ethers.utils.parseEther("2.5"), - sellAmount: ethers.utils.parseEther("1500.0"), - feeAmount: ethers.utils.parseEther("1.5"), - validTo: 0xffffffff, - appData: 2, - sellTokenBalance: OrderBalance.INTERNAL, - buyTokenBalance: OrderBalance.INTERNAL, - }, - traders[3], - SigningScheme.EIP712, - ); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [tokens[0].address]: 550, - [tokens[1].address]: 1, - }), - ); - - expect(await tokens[1].balanceOf(traders[0].address)).to.equal( - ethers.utils.parseEther("550.0"), - ); - expect(await tokens[0].balanceOf(traders[1].address)).to.equal( - ethers.utils.parseEther("0.5"), - ); - expect( - await vault.getInternalBalance(traders[2].address, [tokens[1].address]), - ).to.deep.equal([ethers.utils.parseEther("1100")]); - expect( - await vault.getInternalBalance(traders[3].address, [tokens[0].address]), - ).to.deep.equal([ethers.utils.parseEther("2.5")]); - - expect(await tokens[0].balanceOf(settlement.address)).to.equal( - ethers.utils.parseEther("0.003"), - ); - expect(await tokens[1].balanceOf(settlement.address)).to.equal( - ethers.utils.parseEther("1.8"), - ); - }); -}); diff --git a/test/e2e/nonStandardErc20.test.ts b/test/e2e/nonStandardErc20.test.ts deleted file mode 100644 index 3b638c19..00000000 --- a/test/e2e/nonStandardErc20.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { ethers } from "hardhat"; - -import { - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Non-Standard ERC20 Tokens", () => { - let solver: Wallet; - let traders: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let tokens: [Contract, Contract]; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - settlement, - vaultRelayer, - wallets: [solver, ...traders], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - const ERC20NoReturn = await ethers.getContractFactory("ERC20NoReturn"); - const ERC20ReturningUint = await ethers.getContractFactory( - "ERC20ReturningUint", - ); - tokens = [await ERC20NoReturn.deploy(), await ERC20ReturningUint.deploy()]; - }); - - it("should allow trading non-standard ERC20 tokens", async () => { - // Just trade 1:1 - - const encoder = new SettlementEncoder(domainSeparator); - const amount = ethers.utils.parseEther("1.0"); - const feeAmount = ethers.utils.parseEther("0.01"); - - await tokens[0].mint(traders[0].address, amount.add(feeAmount)); - await tokens[0] - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: tokens[0].address, - buyToken: tokens[1].address, - sellAmount: amount, - buyAmount: amount, - feeAmount, - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - await tokens[1].mint(traders[1].address, amount.add(feeAmount)); - await tokens[1] - .connect(traders[1]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - sellToken: tokens[1].address, - buyToken: tokens[0].address, - buyAmount: amount, - sellAmount: amount, - feeAmount, - validTo: 0xffffffff, - appData: 2, - }, - traders[1], - SigningScheme.EIP712, - ); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [tokens[0].address]: 1, - [tokens[1].address]: 1, - }), - ); - - expect(await tokens[0].balanceOf(settlement.address)).to.equal(feeAmount); - expect(await tokens[0].balanceOf(traders[1].address)).to.equal(amount); - - expect(await tokens[1].balanceOf(settlement.address)).to.equal(feeAmount); - expect(await tokens[1].balanceOf(traders[0].address)).to.equal(amount); - }); -}); diff --git a/test/e2e/offchainAllowances.test.ts b/test/e2e/offchainAllowances.test.ts deleted file mode 100644 index 56629d41..00000000 --- a/test/e2e/offchainAllowances.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { ethers } from "hardhat"; - -import { - InteractionStage, - OrderBalance, - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, - grantRequiredRoles, -} from "../../src/ts"; -import { UserBalanceOpKind } from "../balancer"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Off-chain Allowances", () => { - let manager: Wallet; - let solver: Wallet; - let traders: Wallet[]; - - let vault: Contract; - let vaultAuthorizer: Contract; - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let eurs: [Contract, Contract]; - const ONE_EUR = ethers.utils.parseEther("1.0"); - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - vault, - vaultAuthorizer, - settlement, - vaultRelayer, - manager, - wallets: [solver, ...traders], - } = deployment); - - const { authenticator } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - const ERC20 = await ethers.getContractFactory("ERC20PresetPermit"); - eurs = [await ERC20.deploy("EUR1"), await ERC20.deploy("EUR2")]; - }); - - describe("EIP-2612 Permit", () => { - it("permits trader allowance with settlement", async () => { - // Settle a trivial trade where all € stable coins trade 1:1. - - const encoder = new SettlementEncoder(domainSeparator); - - await eurs[0].mint(traders[0].address, ONE_EUR); - await eurs[0] - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: eurs[0].address, - buyToken: eurs[1].address, - sellAmount: ONE_EUR, - buyAmount: ONE_EUR, - feeAmount: ethers.constants.Zero, - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - await eurs[1].mint(traders[1].address, ONE_EUR); - - const permit = { - owner: traders[1].address, - spender: vaultRelayer.address, - value: ONE_EUR, - nonce: await eurs[1].nonces(traders[1].address), - deadline: 0xffffffff, - }; - const { r, s, v } = ethers.utils.splitSignature( - await traders[1]._signTypedData( - { - name: await eurs[1].name(), - version: "1", - chainId: domainSeparator.chainId, - verifyingContract: eurs[1].address, - }, - { - Permit: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - { name: "value", type: "uint256" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" }, - ], - }, - permit, - ), - ); - encoder.encodeInteraction( - { - target: eurs[1].address, - callData: eurs[1].interface.encodeFunctionData("permit", [ - permit.owner, - permit.spender, - permit.value, - permit.deadline, - v, - r, - s, - ]), - }, - InteractionStage.PRE, - ); - - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: eurs[0].address, - sellToken: eurs[1].address, - buyAmount: ONE_EUR, - sellAmount: ONE_EUR, - feeAmount: ethers.constants.Zero, - validTo: 0xffffffff, - appData: 2, - }, - traders[1], - SigningScheme.EIP712, - ); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [eurs[0].address]: 1, - [eurs[1].address]: 1, - }), - ); - - expect(await eurs[1].balanceOf(traders[1].address)).to.deep.equal( - ethers.constants.Zero, - ); - }); - }); - - describe("Vault Allowance", () => { - it("allows setting Vault relayer approval with interactions", async () => { - // Settle a trivial trade where all € stable coins trade 1:1. - - const encoder = new SettlementEncoder(domainSeparator); - - await eurs[0].mint(traders[0].address, ONE_EUR); - await eurs[0] - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: eurs[0].address, - buyToken: eurs[1].address, - sellAmount: ONE_EUR, - buyAmount: ONE_EUR, - feeAmount: ethers.constants.Zero, - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - await eurs[1].mint(traders[1].address, ONE_EUR); - await eurs[1] - .connect(traders[1]) - .approve(vault.address, ethers.constants.MaxUint256); - await vault.connect(traders[1]).manageUserBalance([ - { - kind: UserBalanceOpKind.DEPOSIT_INTERNAL, - asset: eurs[1].address, - amount: ONE_EUR, - sender: traders[1].address, - recipient: traders[1].address, - }, - ]); - - // The settlement contract needs to be authorized as a relayer to change - // relayer allowances for users by signature. - await vaultAuthorizer - .connect(manager) - .grantRole( - ethers.utils.solidityKeccak256( - ["uint256", "bytes4"], - [vault.address, vault.interface.getSighash("setRelayerApproval")], - ), - settlement.address, - ); - await grantRequiredRoles( - vaultAuthorizer.connect(manager), - vault.address, - vaultRelayer.address, - ); - - const deadline = 0xffffffff; - const { chainId } = await ethers.provider.getNetwork(); - const approval = vault.interface.encodeFunctionData( - "setRelayerApproval", - [traders[1].address, vaultRelayer.address, true], - ); - const { v, r, s } = ethers.utils.splitSignature( - await traders[1]._signTypedData( - { - name: "Balancer V2 Vault", - version: "1", - chainId, - verifyingContract: vault.address, - }, - { - SetRelayerApproval: [ - { name: "calldata", type: "bytes" }, - { name: "sender", type: "address" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" }, - ], - }, - { - calldata: approval, - sender: settlement.address, - nonce: 0, - deadline, - }, - ), - ); - encoder.encodeInteraction( - { - target: vault.address, - callData: ethers.utils.hexConcat([ - approval, - ethers.utils.defaultAbiCoder.encode( - ["uint256", "uint8", "bytes32", "bytes32"], - [deadline, v, r, s], - ), - ]), - }, - InteractionStage.PRE, - ); - - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: eurs[0].address, - sellToken: eurs[1].address, - buyAmount: ONE_EUR, - sellAmount: ONE_EUR, - feeAmount: ethers.constants.Zero, - validTo: 0xffffffff, - appData: 2, - sellTokenBalance: OrderBalance.INTERNAL, - }, - traders[1], - SigningScheme.EIP712, - ); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [eurs[0].address]: 1, - [eurs[1].address]: 1, - }), - ); - - expect(await eurs[1].balanceOf(traders[1].address)).to.deep.equal( - ethers.constants.Zero, - ); - }); - }); -}); diff --git a/test/e2e/smartOrder.test.ts b/test/e2e/smartOrder.test.ts deleted file mode 100644 index 921cfd3f..00000000 --- a/test/e2e/smartOrder.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import { expect } from "chai"; -import { Contract, ContractFactory, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - OrderBalance, - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; -import { decodeOrder } from "../encoding"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Dumb Smart Order", () => { - let deployer: Wallet; - let solver: Wallet; - let traders: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let tokens: [Contract, Contract]; - - let SmartSellOrder: ContractFactory; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - settlement, - vaultRelayer, - wallets: [solver, ...traders], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - tokens = [ - await waffle.deployContract(deployer, ERC20, ["T0", 18]), - await waffle.deployContract(deployer, ERC20, ["T1", 18]), - ]; - - SmartSellOrder = await ethers.getContractFactory("SmartSellOrder"); - }); - - it("permits trader allowance with settlement", async () => { - // Settle half of the smart order. - const encoder = new SettlementEncoder(domainSeparator); - - await tokens[0].mint(traders[0].address, ethers.utils.parseEther("1.01")); - await tokens[0] - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - sellToken: tokens[0].address, - buyToken: tokens[1].address, - sellAmount: ethers.utils.parseEther("1.0"), - buyAmount: ethers.utils.parseEther("0.5"), - feeAmount: ethers.utils.parseEther("0.01"), - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - const smartOrder = await SmartSellOrder.connect(traders[1]).deploy( - settlement.address, - tokens[1].address, - tokens[0].address, - 0xffffffff, - ethers.utils.parseEther("1.0"), - ethers.utils.parseEther("0.1"), - ); - await tokens[1].mint(traders[1].address, ethers.utils.parseEther("1.1")); - await tokens[1] - .connect(traders[1]) - .transfer(smartOrder.address, ethers.utils.parseEther("1.1")); - - const smartOrderSellAmount = ethers.utils.parseEther("0.5"); - const smartOrderTrade = decodeOrder( - await smartOrder.orderForSellAmount(smartOrderSellAmount), - ); - expect(smartOrderTrade).to.deep.equal({ - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: tokens[1].address, - buyToken: tokens[0].address, - receiver: traders[1].address, - sellAmount: smartOrderSellAmount, - buyAmount: ethers.utils.parseEther("0.75"), - feeAmount: ethers.utils.parseEther("0.05"), - validTo: 0xffffffff, - appData: await smartOrder.APPDATA(), - sellTokenBalance: OrderBalance.ERC20, - buyTokenBalance: OrderBalance.ERC20, - }); - - await encoder.encodeTrade(smartOrderTrade, { - scheme: SigningScheme.EIP1271, - data: { - verifier: smartOrder.address, - signature: ethers.utils.defaultAbiCoder.encode( - ["uint256"], - [smartOrderSellAmount], - ), - }, - }); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [tokens[0].address]: 10, - [tokens[1].address]: 15, - }), - ); - - expect(await tokens[0].balanceOf(traders[1].address)).to.deep.equal( - ethers.utils.parseEther("0.75"), - ); - }); -}); diff --git a/test/e2e/uniswapTrade.test.ts b/test/e2e/uniswapTrade.test.ts deleted file mode 100644 index e7080af8..00000000 --- a/test/e2e/uniswapTrade.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import UniswapV2Factory from "@uniswap/v2-core/build/UniswapV2Factory.json"; -import UniswapV2Pair from "@uniswap/v2-core/build/UniswapV2Pair.json"; -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Should Trade Surplus With Uniswap", () => { - let deployer: Wallet; - let solver: Wallet; - let pooler: Wallet; - let traders: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let weth: Contract; - let usdt: Contract; - let uniswapPair: Contract; - let isWethToken0: boolean; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - settlement, - vaultRelayer, - wallets: [solver, pooler, ...traders], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - weth = await waffle.deployContract(deployer, ERC20, ["WETH", 18]); - usdt = await waffle.deployContract(deployer, ERC20, ["USDT", 6]); - - const uniswapFactory = await waffle.deployContract( - deployer, - UniswapV2Factory, - [deployer.address], - ); - await uniswapFactory.createPair(weth.address, usdt.address); - uniswapPair = new Contract( - await uniswapFactory.getPair(weth.address, usdt.address), - UniswapV2Pair.abi, - deployer, - ); - - // NOTE: Which token ends up as token 0 or token 1 depends on the addresses - // of the WETH and USDT token which can change depending on which order the - // tests are run. Because of this, check the Uniswap pair to see which token - // ended up on which index. - isWethToken0 = (await uniswapPair.token0()) === weth.address; - }); - - it("should settle two overlapping orders and trade surplus with Uniswap", async () => { - // Settles the following batch: - // - // /----(1. SELL 1 WETH for USDT if p(WETH) >= 500)-----\ - // | | - // | v - // [USDT]<---(Uniswap Pair 1000 WETH / 600.000 USDT)--->[WETH] - // ^ | - // | | - // \----(2. BUY 0.5 WETH for USDT if p(WETH) <= 600)----/ - - const uniswapWethReserve = ethers.utils.parseEther("1000.0"); - const uniswapUsdtReserve = ethers.utils.parseUnits("600000.0", 6); - await weth.mint(uniswapPair.address, uniswapWethReserve); - await usdt.mint(uniswapPair.address, uniswapUsdtReserve); - await uniswapPair.mint(pooler.address); - - // The current batch has a sell order selling 1 WETH and a buy order buying - // 0.5 WETH. This means there is exactly a surplus 0.5 WETH that needs to be - // sold to Uniswap. Uniswap is governed by a balancing equation which can be - // used to compute the exact buy amount for selling the 0.5 WETH and we can - // use to build our the settlement with a smart contract interaction. - // ``` - // (reserveWETH + inWETH * 0.997) * (reserveUSDT - outUSDT) = reserveWETH * reserveUSDT - // outUSDT = (reserveUSDT * inWETH * 0.997) / (reserveWETH + inWETH * 0.997) - // = (reserveUSDT * inWETH * 997) / (reserveWETH * 1000 + inWETH * 997) - // ``` - const uniswapWethInAmount = ethers.utils.parseEther("0.5"); - const uniswapUsdtOutAmount = uniswapUsdtReserve - .mul(uniswapWethInAmount) - .mul(997) - .div(uniswapWethReserve.mul(1000).add(uniswapWethInAmount.mul(997))); - - const encoder = new SettlementEncoder(domainSeparator); - - await weth.mint(traders[0].address, ethers.utils.parseEther("1.001")); - await weth - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: weth.address, - buyToken: usdt.address, - sellAmount: ethers.utils.parseEther("1.0"), - buyAmount: ethers.utils.parseUnits("500.0", 6), - feeAmount: ethers.utils.parseEther("0.001"), - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - await usdt.mint(traders[1].address, ethers.utils.parseUnits("300.3", 6)); - await usdt - .connect(traders[1]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: weth.address, - sellToken: usdt.address, - buyAmount: ethers.utils.parseEther("0.5"), - sellAmount: ethers.utils.parseUnits("300.0", 6), - feeAmount: ethers.utils.parseUnits("0.3", 6), - validTo: 0xffffffff, - appData: 2, - }, - traders[1], - SigningScheme.EIP712, - ); - - encoder.encodeInteraction({ - target: weth.address, - callData: weth.interface.encodeFunctionData("transfer", [ - uniswapPair.address, - uniswapWethInAmount, - ]), - }); - - const [amount0Out, amount1Out] = isWethToken0 - ? [0, uniswapUsdtOutAmount] - : [uniswapUsdtOutAmount, 0]; - encoder.encodeInteraction({ - target: uniswapPair.address, - callData: uniswapPair.interface.encodeFunctionData("swap", [ - amount0Out, - amount1Out, - settlement.address, - "0x", - ]), - }); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [weth.address]: uniswapUsdtOutAmount, - [usdt.address]: uniswapWethInAmount, - }), - ); - - expect(await weth.balanceOf(settlement.address)).to.deep.equal( - ethers.utils.parseEther("0.001"), - ); - expect(await usdt.balanceOf(settlement.address)).to.deep.equal( - ethers.utils.parseUnits("0.3", 6), - ); - - expect(await weth.balanceOf(traders[0].address)).to.deep.equal( - ethers.constants.Zero, - ); - expect(await usdt.balanceOf(traders[0].address)).to.deep.equal( - uniswapUsdtOutAmount.mul(2), - ); - - expect(await weth.balanceOf(traders[1].address)).to.deep.equal( - ethers.utils.parseEther("0.5"), - ); - expect(await usdt.balanceOf(traders[1].address)).to.deep.equal( - ethers.utils - .parseUnits("300.3", 6) - .sub(uniswapUsdtOutAmount.add(ethers.utils.parseUnits("0.3", 6))), - ); - - const [token0Reserve, token1Reserve] = await uniswapPair.getReserves(); - const [finalWethReserve, finalUsdtReserve] = isWethToken0 - ? [token0Reserve, token1Reserve] - : [token1Reserve, token0Reserve]; - expect([finalWethReserve, finalUsdtReserve]).to.deep.equal([ - uniswapWethReserve.add(uniswapWethInAmount), - uniswapUsdtReserve.sub(uniswapUsdtOutAmount), - ]); - }); -}); diff --git a/test/e2e/upgradeAuthenticator.test.ts b/test/e2e/upgradeAuthenticator.test.ts deleted file mode 100644 index abfa0bd2..00000000 --- a/test/e2e/upgradeAuthenticator.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { deployments, ethers } from "hardhat"; - -import { proxyInterface } from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -async function rejectError( - promise: Promise, -): Promise { - try { - await promise; - return undefined; - } catch (err) { - if (err instanceof Error) { - return err; - } else { - throw new Error("Invalid rejection output"); - } - } -} - -async function upgrade( - proxyOwner: Wallet, - contractName: string, - newContractName: string, -) { - // Note that deterministic deployment and gasLimit are not needed/used here as deployment args. - await deployments.deploy(contractName, { - contract: newContractName, - // From differs from initial deployment here since the proxy owner is the Authenticator manager. - from: proxyOwner.address, - proxy: true, - }); -} - -describe("E2E: Upgrade Authenticator", () => { - let authenticator: Contract; - let deployer: Wallet; - let owner: Wallet; - let manager: Wallet; - let nobody: Wallet; - let newOwner: Wallet; - let newManager: Wallet; - let solver: Wallet; - - beforeEach(async () => { - ({ - authenticator, - deployer, - owner, - manager, - wallets: [nobody, newOwner, newManager, solver], - } = await deployTestContracts()); - }); - - it("should upgrade authenticator", async () => { - const GPv2AllowListAuthenticationV2 = await ethers.getContractFactory( - "GPv2AllowListAuthenticationV2", - deployer, - ); - // Note that, before the upgrade this is actually the old instance - const authenticatorV2 = GPv2AllowListAuthenticationV2.attach( - authenticator.address, - ); - // This method doesn't exist before upgrade - await expect(authenticatorV2.newMethod()).to.be.reverted; - - await upgrade( - owner, - "GPv2AllowListAuthentication", - "GPv2AllowListAuthenticationV2", - ); - // This method should exist on after upgrade - expect(await authenticatorV2.newMethod()).to.equal(1337); - }); - - it("should preserve storage", async () => { - await authenticator.connect(manager).addSolver(solver.address); - await authenticator.connect(manager).setManager(newManager.address); - - // Upgrade after storage is set with **proxy owner**; - await upgrade( - owner, - "GPv2AllowListAuthentication", - "GPv2AllowListAuthenticationV2", - ); - - const GPv2AllowListAuthenticationV2 = await ethers.getContractFactory( - "GPv2AllowListAuthenticationV2", - deployer, - ); - const authenticatorV2 = GPv2AllowListAuthenticationV2.attach( - authenticator.address, - ); - - // Both, the listed solvers and updated manager are still set - expect(await authenticatorV2.isSolver(solver.address)).to.equal(true); - expect(await authenticatorV2.manager()).to.equal(newManager.address); - }); - - it("should allow the proxy owner to change the manager", async () => { - await authenticator.connect(owner).setManager(newManager.address); - expect(await authenticator.manager()).to.equal(newManager.address); - }); - - it("should be able to transfer proxy ownership", async () => { - const proxy = proxyInterface(authenticator); - await proxy.connect(owner).transferOwnership(newOwner.address); - expect(await proxy.owner()).to.equal(newOwner.address); - - await upgrade( - newOwner, - "GPv2AllowListAuthentication", - "GPv2AllowListAuthenticationV2", - ); - }); - - it("should revert when not upgrading with the authentication manager", async () => { - await authenticator.connect(owner).setManager(newManager.address); - expect( - await rejectError( - upgrade( - newManager, - "GPv2AllowListAuthentication", - "GPv2AllowListAuthenticationV2", - ), - ), - ).to.not.be.undefined; - }); - - it("should revert when not upgrading with the proxy owner", async () => { - expect( - await rejectError( - upgrade( - nobody, - "GPv2AllowListAuthentication", - "GPv2AllowListAuthenticationV2", - ), - ), - ).to.not.be.undefined; - }); -}); diff --git a/test/e2e/wineOilMarket.test.ts b/test/e2e/wineOilMarket.test.ts deleted file mode 100644 index 3531903a..00000000 --- a/test/e2e/wineOilMarket.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import { expect } from "chai"; -import { BigNumber, BigNumberish, Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - Order, - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -function ceilDiv(p: BigNumberish, q: BigNumberish): BigNumber { - return BigNumber.from(p).add(q).sub(1).div(q); -} - -describe("E2E: RetrETH Red Wine and Olive Oil Market", () => { - let deployer: Wallet; - let solver: Wallet; - let traders: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - settlement, - vaultRelayer, - wallets: [solver, ...traders], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - }); - - it("should settle red wine and olive oil market", async () => { - // Settlement for the RetrETH wine and olive oil market: - // - // /---(6. BUY 10 🍷 with 💶 if p(🍷) <= 13)--> [🍷] - // | | - // | | - // [💶] (1. SELL 12 🍷 for 🫒 if p(🍷) >= p(🫒)) - // |^ | - // || | - // |\--(4. SELL 15 🫒 for 💶 if p(🫒) >= 12)--\ v - // \---(5. BUY 4 🫒 with 💶 if p(🫒) <= 13)---> [🫒] - - const STARTING_BALANCE = ethers.utils.parseEther("1000.0"); - const erc20 = (symbol: string) => - waffle.deployContract(deployer, ERC20, [symbol, 18]); - - const eur = await erc20("💶"); - const oil = await erc20("🫒"); - const wine = await erc20("🍷"); - - const orderDefaults = { - validTo: 0xffffffff, - feeAmount: ethers.utils.parseEther("1.0"), - }; - const encoder = new SettlementEncoder(domainSeparator); - - const addOrder = async ( - trader: Wallet, - order: Order, - executedAmount?: BigNumber, - ) => { - const sellToken = await ethers.getContractAt( - ERC20.abi, - order.sellToken, - deployer, - ); - await sellToken.mint(trader.address, STARTING_BALANCE); - await sellToken - .connect(trader) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - - await encoder.signEncodeTrade(order, trader, SigningScheme.EIP712, { - executedAmount, - }); - }; - - await addOrder(traders[0], { - ...orderDefaults, - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: wine.address, - buyToken: oil.address, - sellAmount: ethers.utils.parseEther("12.0"), - buyAmount: ethers.utils.parseEther("12.0"), - appData: 1, - }); - - await addOrder(traders[1], { - ...orderDefaults, - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: oil.address, - buyToken: eur.address, - sellAmount: ethers.utils.parseEther("15.0"), - buyAmount: ethers.utils.parseEther("180.0"), - appData: 4, - }); - - await addOrder( - traders[2], - { - ...orderDefaults, - kind: OrderKind.BUY, - partiallyFillable: true, - buyToken: oil.address, - sellToken: eur.address, - buyAmount: ethers.utils.parseEther("4.0"), - sellAmount: ethers.utils.parseEther("52.0"), - appData: 5, - }, - ethers.utils.parseEther("27.0").div(13), - ); - - await addOrder( - traders[3], - { - ...orderDefaults, - kind: OrderKind.BUY, - partiallyFillable: true, - buyToken: wine.address, - sellToken: eur.address, - buyAmount: ethers.utils.parseEther("20.0"), - sellAmount: ethers.utils.parseEther("280.0"), - appData: 6, - }, - ethers.utils.parseEther("12.0"), - ); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [eur.address]: ethers.utils.parseEther("1.0"), - [oil.address]: ethers.utils.parseEther("13.0"), - [wine.address]: ethers.utils.parseEther("14.0"), - }), - ); - - expect(await wine.balanceOf(traders[0].address)).to.deep.equal( - STARTING_BALANCE.sub(ethers.utils.parseEther("12.0")).sub( - orderDefaults.feeAmount, - ), - ); - expect(await oil.balanceOf(traders[0].address)).to.deep.equal( - ceilDiv(ethers.utils.parseEther("12.0").mul(14), 13), - ); - - expect(await oil.balanceOf(traders[1].address)).to.deep.equal( - STARTING_BALANCE.sub(ethers.utils.parseEther("15.0")).sub( - orderDefaults.feeAmount, - ), - ); - expect(await eur.balanceOf(traders[1].address)).to.deep.equal( - ethers.utils.parseEther("15.0").mul(13), - ); - - expect(await eur.balanceOf(traders[2].address)).to.deep.equal( - STARTING_BALANCE.sub(ethers.utils.parseEther("27.0")) - .sub( - orderDefaults.feeAmount - .mul(ethers.utils.parseEther("27.0").div(13)) - .div(ethers.utils.parseEther("4.0")), - ) - // NOTE: Account for rounding error from computing sell amount that is - // an order of magnitude larger than executed buy amount from the - // settlement. - .add(1), - ); - expect(await oil.balanceOf(traders[2].address)).to.deep.equal( - ethers.utils.parseEther("27.0").div(13), - ); - - expect(await eur.balanceOf(traders[3].address)).to.deep.equal( - STARTING_BALANCE.sub(ethers.utils.parseEther("12.0").mul(14)).sub( - orderDefaults.feeAmount - .mul(ethers.utils.parseEther("12.0")) - .div(ethers.utils.parseEther("20.0")), - ), - ); - expect(await wine.balanceOf(traders[3].address)).to.deep.equal( - ethers.utils.parseEther("12.0"), - ); - }); -}); diff --git a/test/e2e/zero-ex/index.ts b/test/e2e/zero-ex/index.ts deleted file mode 100644 index 5cae98f5..00000000 --- a/test/e2e/zero-ex/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BigNumberish, BytesLike } from "ethers"; - -export interface SimpleOrder { - takerAddress: string; - makerAssetAmount: BigNumberish; - takerAssetAmount: BigNumberish; - makerAssetAddress: BytesLike; - takerAssetAddress: BytesLike; -} diff --git a/test/e2e/zero-ex/v2/index.ts b/test/e2e/zero-ex/v2/index.ts deleted file mode 100644 index e25924d3..00000000 --- a/test/e2e/zero-ex/v2/index.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { ERC20Proxy, Exchange, ZRXToken } from "@0x/contract-artifacts-v2"; -import { BigNumberish, BytesLike, Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { SimpleOrder } from ".."; -import { TypedDataDomain } from "../../../../src/ts"; - -// NOTE: Order type from: -// -export interface Order { - makerAddress: string; - takerAddress: string; - feeRecipientAddress: string; - senderAddress: string; - makerAssetAmount: BigNumberish; - takerAssetAmount: BigNumberish; - makerFee: BigNumberish; - takerFee: BigNumberish; - expirationTimeSeconds: BigNumberish; - salt: BigNumberish; - makerAssetData: BytesLike; - takerAssetData: BytesLike; -} - -const ORDER_TYPE_DESCRIPTOR = { - Order: [ - { name: "makerAddress", type: "address" }, - { name: "takerAddress", type: "address" }, - { name: "feeRecipientAddress", type: "address" }, - { name: "senderAddress", type: "address" }, - { name: "makerAssetAmount", type: "uint256" }, - { name: "takerAssetAmount", type: "uint256" }, - { name: "makerFee", type: "uint256" }, - { name: "takerFee", type: "uint256" }, - { name: "expirationTimeSeconds", type: "uint256" }, - { name: "salt", type: "uint256" }, - { name: "makerAssetData", type: "bytes" }, - { name: "takerAssetData", type: "bytes" }, - ], -}; - -export interface SignedOrder { - order: Order; - hash: BytesLike; - signature: BytesLike; -} - -function encodeErc20AssetData(tokenAddress: BytesLike): string { - // NOTE: ERC20 proxy asset data defined in: - // - - const { id, hexDataSlice } = ethers.utils; - const PROXY_ID = hexDataSlice(id("ERC20Token(address)"), 0, 4); - - return ethers.utils.hexConcat([ - PROXY_ID, - ethers.utils.defaultAbiCoder.encode(["address"], [tokenAddress]), - ]); -} - -export async function signSimpleOrder( - maker: Wallet, - domain: TypedDataDomain, - simpleOrder: SimpleOrder, -): Promise { - const order = { - ...simpleOrder, - makerAddress: maker.address, - makerAssetData: encodeErc20AssetData(simpleOrder.makerAssetAddress), - takerAssetData: encodeErc20AssetData(simpleOrder.takerAssetAddress), - - // NOTE: Unused. - expirationTimeSeconds: 0xffffffff, - salt: ethers.constants.Zero, - - // NOTE: Setting taker and sender address to `address(0)` means that the - // order can be executed (sender) against any counterparty (taker). For the - // purposes of GPv2, these need to be either `address(0)` or the settlement - // contract. - takerAddress: ethers.constants.AddressZero, - senderAddress: ethers.constants.AddressZero, - - // NOTE: Include no additional fees. I am not sure how this is used by - // market makers, but in theory this can be used to assign an additional - // fee, on top of the 0x protocol fee, to the GPv2 settlement contract. - feeRecipientAddress: ethers.constants.AddressZero, - makerFee: 0, - takerFee: 0, - }; - - const hash = ethers.utils._TypedDataEncoder.hash( - domain, - ORDER_TYPE_DESCRIPTOR, - order, - ); - - // NOTE: Use EIP-712 signing scheme for the order. The signature is just the - // ECDSA signature post-fixed with the signature scheme ID (0x02): - // - - const EIP712_SIGNATURE_ID = 0x02; - const { v, r, s } = ethers.utils.splitSignature( - await maker._signTypedData(domain, ORDER_TYPE_DESCRIPTOR, order), - ); - const signature = ethers.utils.solidityPack( - ["uint8", "bytes32", "bytes32", "uint8"], - [v, r, s, EIP712_SIGNATURE_ID], - ); - - return { order, hash, signature }; -} - -export interface Deployment { - zrxToken: Contract; - exchange: Contract; - erc20Proxy: Contract; - domainSeparator: TypedDataDomain; -} - -export async function deployExchange(deployer: Wallet): Promise { - const zrxToken = await waffle.deployContract( - deployer, - ZRXToken.compilerOutput, - ); - - const zrxAssetData = encodeErc20AssetData(zrxToken.address); - const exchange = await waffle.deployContract( - deployer, - Exchange.compilerOutput, - [zrxAssetData], - ); - - const erc20Proxy = await waffle.deployContract( - deployer, - ERC20Proxy.compilerOutput, - ); - - await erc20Proxy.addAuthorizedAddress(exchange.address); - await exchange.registerAssetProxy(erc20Proxy.address); - - return { - zrxToken, - exchange, - erc20Proxy, - // NOTE: Domain separator parameters taken from: - // - domainSeparator: { - name: "0x Protocol", - version: "2", - verifyingContract: exchange.address, - }, - }; -} diff --git a/test/src/EIP173Proxy.sol b/test/src/EIP173Proxy.sol new file mode 100644 index 00000000..b4a71ee4 --- /dev/null +++ b/test/src/EIP173Proxy.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// solhint-disable-next-line compiler-version +pragma solidity ^0.7; + +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; + +interface IERC165 { + function supportsInterface(bytes4 id) external view returns (bool); +} + +// ref: https://github.com/wighawag/hardhat-deploy/blob/e0ffcf9e7dc92b246e832c4d175f9dbd8b6df14d/solc_0.8/proxy/EIP173Proxy.sol +contract EIP173Proxy is Proxy { + bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 constant OWNER_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + modifier onlyOwner() { + require(msg.sender == _owner(), "NOT_AUTHORIZED"); + _; + } + + constructor(address implAddress, address ownerAddress, bytes memory data) { + _setOwner(ownerAddress); + _setImplementation(implAddress, data); + } + + function owner() external view returns (address) { + return _owner(); + } + + function supportsInterface(bytes4 id) external view returns (bool) { + if (id == 0x01ffc9a7 || id == 0x7f5828d0) { + return true; + } + if (id == 0xFFFFFFFF) { + return false; + } + + IERC165 implementation; + assembly { + implementation := sload(IMPLEMENTATION_SLOT) + } + + // Technically this is not standard compliant as ERC-165 require 30,000 gas which that call cannot ensure + // because it is itself inside `supportsInterface` that might only get 30,000 gas. + // In practise this is unlikely to be an issue. + try implementation.supportsInterface(id) returns (bool support) { + return support; + } catch { + return false; + } + } + + function transferOwnership(address newOwner) external onlyOwner { + _setOwner(newOwner); + } + + function upgradeTo(address newImplementation) external onlyOwner { + _setImplementation(newImplementation, ""); + } + + function upgradeToAndCall(address newImplementation, bytes calldata data) external payable onlyOwner { + _setImplementation(newImplementation, data); + } + + function _owner() internal view returns (address adminAddress) { + assembly { + adminAddress := sload(OWNER_SLOT) + } + } + + function _setOwner(address newOwner) internal { + address previousOwner = _owner(); + assembly { + sstore(OWNER_SLOT, newOwner) + } + emit OwnershipTransferred(previousOwner, newOwner); + } + + function _implementation() internal view override returns (address) { + address impl; + assembly { + impl := sload(IMPLEMENTATION_SLOT) + } + return impl; + } + + function _setImplementation(address impl, bytes memory data) internal { + assembly { + sstore(IMPLEMENTATION_SLOT, impl) + } + + if (data.length > 0) { + // solhint-disable-next-line avoid-low-level-calls + (bool success,) = impl.delegatecall(data); + if (!success) { + assembly { + // This assembly ensure the revert contains the exact string data + let returnDataSize := returndatasize() + returndatacopy(0, 0, returnDataSize) + revert(0, returnDataSize) + } + } + } + } +} diff --git a/test/src/ERC20Mintable.sol b/test/src/ERC20Mintable.sol new file mode 120000 index 00000000..c31a1693 --- /dev/null +++ b/test/src/ERC20Mintable.sol @@ -0,0 +1 @@ +../e2e/ERC20Mintable.sol \ No newline at end of file diff --git a/test/src/GPv2AllowListAuthenticationV2.sol b/test/src/GPv2AllowListAuthenticationV2.sol deleted file mode 100644 index 40dc6ca2..00000000 --- a/test/src/GPv2AllowListAuthenticationV2.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// solhint-disable-next-line compiler-version -pragma solidity >=0.7.6 <0.9.0; - -import "src/contracts/GPv2AllowListAuthentication.sol"; - -contract GPv2AllowListAuthenticationV2 is GPv2AllowListAuthentication { - function newMethod() external pure returns (uint256) { - return 1337; - } -} diff --git a/test/src/NonStandardERC20.sol b/test/src/NonStandardERC20.sol deleted file mode 100644 index c91f8843..00000000 --- a/test/src/NonStandardERC20.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// solhint-disable-next-line compiler-version -pragma solidity >=0.7.6 <0.9.0; - -import "src/contracts/libraries/SafeMath.sol"; - -abstract contract NonStandardERC20 { - using SafeMath for uint256; - - mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; - - function mint(address to, uint256 amount) external { - balanceOf[to] = balanceOf[to].add(amount); - } - - function approve(address spender, uint256 amount) external { - allowance[msg.sender][spender] = amount; - } - - function transfer_(address to, uint256 amount) internal { - balanceOf[msg.sender] = balanceOf[msg.sender].sub(amount); - balanceOf[to] = balanceOf[to].add(amount); - } - - function transferFrom_(address from, address to, uint256 amount) internal { - allowance[from][msg.sender] = allowance[from][msg.sender].sub(amount); - balanceOf[from] = balanceOf[from].sub(amount); - balanceOf[to] = balanceOf[to].add(amount); - } -} - -contract ERC20NoReturn is NonStandardERC20 { - function transfer(address to, uint256 amount) external { - transfer_(to, amount); - } - - function transferFrom(address from, address to, uint256 amount) external { - transferFrom_(from, to, amount); - } -} - -contract ERC20ReturningUint is NonStandardERC20 { - // Largest 256-bit prime :) - uint256 private constant OK = 115792089237316195423570985008687907853269984665640564039457584007913129639747; - - function transfer(address to, uint256 amount) external returns (uint256) { - transfer_(to, amount); - return OK; - } - - function transferFrom(address from, address to, uint256 amount) external returns (uint256) { - transferFrom_(from, to, amount); - return OK; - } -} diff --git a/test/src/SmartSellOrder.sol b/test/src/SmartSellOrder.sol deleted file mode 100644 index 23336f53..00000000 --- a/test/src/SmartSellOrder.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// solhint-disable-next-line compiler-version -pragma solidity >=0.7.6 <0.9.0; -pragma abicoder v2; - -import "src/contracts/GPv2Settlement.sol"; -import "src/contracts/interfaces/GPv2EIP1271.sol"; -import "src/contracts/interfaces/IERC20.sol"; -import "src/contracts/libraries/GPv2Order.sol"; -import "src/contracts/libraries/GPv2SafeERC20.sol"; -import "src/contracts/libraries/SafeMath.sol"; - -/// @title Proof of Concept Smart Order -/// @author Gnosis Developers -contract SmartSellOrder is EIP1271Verifier { - using GPv2Order for GPv2Order.Data; - using GPv2SafeERC20 for IERC20; - using SafeMath for uint256; - - bytes32 public constant APPDATA = keccak256("SmartSellOrder"); - - address public immutable owner; - bytes32 public immutable domainSeparator; - IERC20 public immutable sellToken; - IERC20 public immutable buyToken; - uint256 public immutable totalSellAmount; - uint256 public immutable totalFeeAmount; - uint32 public immutable validTo; - - constructor( - GPv2Settlement settlement, - IERC20 sellToken_, - IERC20 buyToken_, - uint32 validTo_, - uint256 totalSellAmount_, - uint256 totalFeeAmount_ - ) { - owner = msg.sender; - domainSeparator = settlement.domainSeparator(); - sellToken = sellToken_; - buyToken = buyToken_; - validTo = validTo_; - totalSellAmount = totalSellAmount_; - totalFeeAmount = totalFeeAmount_; - - sellToken_.approve(address(settlement.vaultRelayer()), type(uint256).max); - } - - modifier onlyOwner() { - require(msg.sender == owner, "not owner"); - _; - } - - function withdraw(uint256 amount) external onlyOwner { - sellToken.safeTransfer(owner, amount); - } - - function close() external onlyOwner { - uint256 balance = sellToken.balanceOf(address(this)); - if (balance != 0) { - sellToken.safeTransfer(owner, balance); - } - } - - function isValidSignature(bytes32 hash, bytes memory signature) - external - view - override - returns (bytes4 magicValue) - { - uint256 sellAmount = abi.decode(signature, (uint256)); - GPv2Order.Data memory order = orderForSellAmount(sellAmount); - - if (order.hash(domainSeparator) == hash) { - magicValue = GPv2EIP1271.MAGICVALUE; - } - } - - function orderForSellAmount(uint256 sellAmount) public view returns (GPv2Order.Data memory order) { - order.sellToken = sellToken; - order.buyToken = buyToken; - order.receiver = owner; - order.sellAmount = sellAmount; - order.buyAmount = buyAmountForSellAmount(sellAmount); - order.validTo = validTo; - order.appData = APPDATA; - order.feeAmount = totalFeeAmount.mul(sellAmount).div(totalSellAmount); - order.kind = GPv2Order.KIND_SELL; - // NOTE: We counter-intuitively set `partiallyFillable` to `false`, even - // if the smart order as a whole acts like a partially fillable order. - // This is done since, once a settlement commits to a specific sell - // amount, then it is expected to use it completely and not partially. - order.partiallyFillable = false; - order.sellTokenBalance = GPv2Order.BALANCE_ERC20; - order.buyTokenBalance = GPv2Order.BALANCE_ERC20; - } - - function buyAmountForSellAmount(uint256 sellAmount) private view returns (uint256 buyAmount) { - uint256 feeAdjustedBalance = - sellToken.balanceOf(address(this)).mul(totalSellAmount).div(totalSellAmount.add(totalFeeAmount)); - uint256 soldAmount = totalSellAmount > feeAdjustedBalance ? totalSellAmount - feeAdjustedBalance : 0; - - // NOTE: This is currently a silly price strategy where the xrate - // increases linearly from 1:1 to 1:2 as the smart order gets filled. - // This can be extended to more complex "price curves". - buyAmount = sellAmount.mul(totalSellAmount.add(sellAmount).add(soldAmount)).div(totalSellAmount); - } -} diff --git a/yarn.lock b/yarn.lock index 43e1761b..affcaeda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@0x/contract-artifacts-v2@npm:@0x/contract-artifacts@^2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@0x/contract-artifacts/-/contract-artifacts-2.2.2.tgz#e6d771afb58d0b59c19c5364af5a42a3dfd17219" - integrity sha512-sbFnSXE6PlmYsbPXpKtEOR3YdVlSn63HhbPgQB3J5jm27wwQtnZ2Lf21I7BdiRBsHwwbf75C/s2pjNqafaRrgQ== - "@babel/code-frame@^7.0.0": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"