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/ZeroExTrade.t.sol b/test/e2e/ZeroExTrade.t.sol new file mode 100644 index 00000000..f99afe6f --- /dev/null +++ b/test/e2e/ZeroExTrade.t.sol @@ -0,0 +1,134 @@ +// 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 {Eip712} from "../libraries/Eip712.sol"; +import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; +import {IExchange, ZeroExV2, ZeroExV2Order, ZeroExV2SimpleOrder} from "./ZeroExV2.sol"; + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +contract ZeroExTradeTest is Helper(false) { + IERC20Mintable OWL; + IERC20Mintable GNO; + + Vm.Wallet marketMaker; + + address exchange; + address erc20Proxy; + address zrx; + + function setUp() public override { + super.setUp(); + + OWL = deployMintableErc20("OWL", "OWL"); + GNO = deployMintableErc20("GNO", "GNO"); + + marketMaker = vm.createWallet("marketMaker"); + + (zrx, erc20Proxy, exchange) = ZeroExV2.deployExchange(deployer); + } + + function test_should_settle_an_eoa_trade_with_a_0x_trade() external { + // mint some tokens to trader + OWL.mint(trader.addr, 140 ether); + vm.prank(trader.addr); + OWL.approve(vaultRelayer, type(uint256).max); + + // place order to buy 1 OWL with max 130 GNO + GPv2Order.Data memory makerOrder = GPv2Order.Data({ + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + buyToken: GNO, + sellToken: OWL, + buyAmount: 1 ether, + sellAmount: 130 ether, + feeAmount: 10 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }); + encoder.signEncodeTrade(vm, trader, makerOrder, domainSeparator, GPv2Signing.Scheme.Eip712, 0); + + // mint some tokens to market maker + GNO.mint(marketMaker.addr, 1000 ether); + vm.prank(marketMaker.addr); + GNO.approve(erc20Proxy, type(uint256).max); + + // sign zero ex order + uint256 zeroExGnoPrice = 110; + (ZeroExV2Order memory order, bytes32 hash, uint8 v, bytes32 r, bytes32 s) = ZeroExV2.signSimpleOrder( + marketMaker, + exchange, + ZeroExV2SimpleOrder({ + takerAddress: address(settlement), + makerAssetAddress: address(GNO), + makerAssetAmount: 1000 ether, + takerAssetAddress: address(OWL), + takerAssetAmount: 1000 ether * zeroExGnoPrice + }) + ); + assertTrue( + IExchange(exchange).isValidSignature(hash, marketMaker.addr, ZeroExV2.encodeSignature(v, r, s)), + "zero ex v2 order signature is invalid" + ); + + uint256 zeroExTakerAmount = makerOrder.buyAmount * zeroExGnoPrice; + + uint256 gpv2GnoPrice = 120; + // add interactions for filling the zero ex order in settlement + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(OWL), + value: 0, + callData: abi.encodeCall(IERC20.approve, (erc20Proxy, zeroExTakerAmount)) + }), + SettlementEncoder.InteractionStage.INTRA + ); + encoder.addInteraction( + GPv2Interaction.Data({ + target: exchange, + value: 0, + callData: abi.encodeCall(IExchange.fillOrder, (order, zeroExTakerAmount, ZeroExV2.encodeSignature(v, r, s))) + }), + SettlementEncoder.InteractionStage.INTRA + ); + + // set token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = OWL; + tokens[1] = GNO; + uint256[] memory prices = new uint256[](2); + prices[0] = 1; + prices[1] = gpv2GnoPrice; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + uint256 gpv2OwlSurplus = makerOrder.sellAmount - (makerOrder.buyAmount * gpv2GnoPrice); + uint256 zeroExOwlSurplus = makerOrder.buyAmount * (gpv2GnoPrice - zeroExGnoPrice); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + vm.prank(solver); + settle(encodedSettlement); + + assertEq(OWL.balanceOf(trader.addr), gpv2OwlSurplus, "trader owl surplus not as expected"); + assertEq( + OWL.balanceOf(address(settlement)), + zeroExOwlSurplus + makerOrder.feeAmount, + "settlement surplus and fee not as expected" + ); + } + + function _generateSettlementSolution() internal {} +} diff --git a/test/e2e/ZeroExV2.sol b/test/e2e/ZeroExV2.sol new file mode 100644 index 00000000..8e0c42a9 --- /dev/null +++ b/test/e2e/ZeroExV2.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +interface IERC20Proxy { + function addAuthorizedAddress(address) external; +} + +interface IExchange { + function registerAssetProxy(address) external; + function isValidSignature(bytes32, address, bytes calldata) external view returns (bool); + function fillOrder(ZeroExV2Order calldata, uint256, bytes calldata) external; + function EIP712_DOMAIN_HASH() external view returns (bytes32); +} + +struct ZeroExV2Order { + address makerAddress; + address takerAddress; + address feeRecipientAddress; + address sender; + uint256 makerAssetAmount; + uint256 takerAssetAmount; + uint256 makerFee; + uint256 takerFee; + uint256 expirationTimeSeconds; + uint256 salt; + bytes makerAssetData; + bytes takerAssetData; +} + +string constant ZERO_EX_V2_ORDER_TYPE_STRING = + "Order(address makerAddress,address takerAddress,address feeRecipientAddress,address senderAddress,uint256 makerAssetAmount,uint256 takerAssetAmount,uint256 makerFee,uint256 takerFee,uint256 expirationTimeSeconds,uint256 salt,bytes makerAssetData,bytes takerAssetData)"; +bytes32 constant ZERO_EX_V2_ORDER_TYPE_HASH = keccak256(bytes(ZERO_EX_V2_ORDER_TYPE_STRING)); + +struct ZeroExV2SimpleOrder { + address takerAddress; + uint256 makerAssetAmount; + uint256 takerAssetAmount; + address makerAssetAddress; + address takerAssetAddress; +} + +library ZeroExV2 { + event log_bytes32(bytes32); + + function signSimpleOrder(Vm.Wallet memory wallet, address exchange, ZeroExV2SimpleOrder memory simpleOrder) + internal + returns (ZeroExV2Order memory order, bytes32 hash, uint8 v, bytes32 r, bytes32 s) + { + order = ZeroExV2Order({ + makerAddress: wallet.addr, + takerAddress: address(0), + feeRecipientAddress: address(0), + sender: address(0), + makerAssetAmount: simpleOrder.makerAssetAmount, + takerAssetAmount: simpleOrder.takerAssetAmount, + makerFee: 0, + takerFee: 0, + expirationTimeSeconds: 0xffffffff, + salt: 0, + makerAssetData: _encodeErc20AssetData(simpleOrder.makerAssetAddress), + takerAssetData: _encodeErc20AssetData(simpleOrder.takerAssetAddress) + }); + bytes32 domainSeparator = IExchange(exchange).EIP712_DOMAIN_HASH(); + + hash = keccak256(abi.encodePacked(hex"1901", domainSeparator, _hashStruct(order))); + (v, r, s) = _vm().sign(wallet, hash); + } + + function deployExchange(address deployer) + internal + returns (address zrxToken, address erc20Proxy, address exchange) + { + Vm vm = _vm(); + vm.startPrank(deployer); + zrxToken = _create(_getCode("ZRXToken"), 0); + + erc20Proxy = _create(_getCode("ERC20Proxy"), 0); + + bytes memory zrxAssetData = _encodeErc20AssetData(zrxToken); + exchange = _create(abi.encodePacked(_getCode("Exchange"), abi.encode(zrxAssetData)), 0); + + IERC20Proxy(erc20Proxy).addAuthorizedAddress(exchange); + IExchange(exchange).registerAssetProxy(erc20Proxy); + + vm.stopPrank(); + } + + function encodeSignature(uint8 v, bytes32 r, bytes32 s) internal pure returns (bytes memory) { + return abi.encodePacked(v, r, s, uint8(0x02)); + } + + 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), "contract creation failed"); + } + + function _vm() internal pure returns (Vm) { + return Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + } + + function _encodeErc20AssetData(address token) internal pure returns (bytes memory) { + return abi.encodePacked(bytes4(keccak256(bytes("ERC20Token(address)"))), abi.encode(token)); + } + + function _hashStruct(ZeroExV2Order memory order) internal pure returns (bytes32) { + // stack too deep hack: + bytes32 schemaHash = ZERO_EX_V2_ORDER_TYPE_HASH; + bytes32 makerAssetDataHash = keccak256(order.makerAssetData); + bytes32 takerAssetDataHash = keccak256(order.takerAssetData); + + // Assembly for more efficiently computing: + // keccak256(abi.encodePacked( + // EIP712_ORDER_SCHEMA_HASH, + // bytes32(order.makerAddress), + // bytes32(order.takerAddress), + // bytes32(order.feeRecipientAddress), + // bytes32(order.senderAddress), + // order.makerAssetAmount, + // order.takerAssetAmount, + // order.makerFee, + // order.takerFee, + // order.expirationTimeSeconds, + // order.salt, + // keccak256(order.makerAssetData), + // keccak256(order.takerAssetData) + // )); + + bytes32 result; + assembly { + // Calculate memory addresses that will be swapped out before hashing + let pos1 := sub(order, 32) + let pos2 := add(order, 320) + let pos3 := add(order, 352) + + // Backup + let temp1 := mload(pos1) + let temp2 := mload(pos2) + let temp3 := mload(pos3) + + // Hash in place + mstore(pos1, schemaHash) + mstore(pos2, makerAssetDataHash) + mstore(pos3, takerAssetDataHash) + result := keccak256(pos1, 416) + + // Restore + mstore(pos1, temp1) + mstore(pos2, temp2) + mstore(pos3, temp3) + } + return result; + } + + function _getCode(string memory artifactName) internal view returns (bytes memory) { + Vm vm = _vm(); + string memory data = vm.readFile( + string(abi.encodePacked("node_modules/@0x/contract-artifacts-v2/lib/artifacts/", artifactName, ".json")) + ); + return vm.parseJsonBytes(data, ".compilerOutput.evm.bytecode.object"); + } +} 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, - }, - }; -}