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