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/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); - }); -});