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