diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4481ec6..6a7f138 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ on: env: FOUNDRY_PROFILE: ci + FORK_RPC_URL: ${{ secrets.RPC_URL_1 }} jobs: check: @@ -31,7 +32,7 @@ jobs: - name: Run Forge build run: | - forge build --sizes + forge build id: build - name: Run Forge tests diff --git a/.gitmodules b/.gitmodules index 837af89..c7908b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,7 @@ [submodule "lib/cow"] path = lib/cow url = https://github.com/cowprotocol/contracts + branch = feat/wrapper [submodule "lib/euler-vault-kit"] path = lib/euler-vault-kit url = https://github.com/euler-xyz/euler-vault-kit diff --git a/foundry.lock b/foundry.lock index 775a9f7..ee1cfbe 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,14 +1,17 @@ { "lib/cow": { - "rev": "39d7f4d68e37d14adeaf3c0caca30ea5c1a2fad9" + "branch": { + "name": "feat/wrapper", + "rev": "1e8127f476f8fef7758cf25033a0010d325dba8d" + } }, "lib/euler-vault-kit": { "rev": "5b98b42048ba11ae82fb62dfec06d1010c8e41e6" }, + "lib/evc": { + "rev": "34bb788288a0eb0fbba06bc370cb8ca3dd42614e" + }, "lib/forge-std": { - "tag": { - "name": "v1.10.0", - "rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824" - } + "rev": "0768d9c08c085c79bb31d88683a78770764fec49" } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 25b918f..37f6dd8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,6 @@ src = "src" out = "out" libs = ["lib"] +optimize = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index f01d69c..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/CowEvcWrapper.sol b/src/CowEvcWrapper.sol new file mode 100644 index 0000000..8a1e823 --- /dev/null +++ b/src/CowEvcWrapper.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import {IEVC} from "evc/EthereumVaultConnector.sol"; +import {IGPv2Authentication} from "./vendor/interfaces/IGPv2Authentication.sol"; + +import {GPv2Signing, IERC20, GPv2Trade} from "cow/mixins/GPv2Signing.sol"; +import {GPv2Wrapper,GPv2Interaction} from "cow/GPv2Wrapper.sol"; + +/// @title CowEvcWrapper +/// @notice A wrapper around the EVC that allows for settlement operations +contract CowEvcWrapper is GPv2Wrapper, GPv2Signing { + IEVC public immutable EVC; + + /// @notice 0 = not executing, 1 = wrappedSettle() called and not yet internal settle, 2 = evcInternalSettle() called + uint256 public transient settleState; + + error Unauthorized(address msgSender); + error NoReentrancy(); + error MultiplePossibleReceivers( + address resolvedVault, address resolvedSender, address secondVault, address secondSender + ); + + error NotEVCSettlement(); + + constructor(address _evc, address payable _settlement) GPv2Wrapper(_settlement) { + EVC = IEVC(_evc); + } + + struct ResolvedValues { + address vault; + address sender; + uint256 minAmount; + } + + /// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations around settlement + /// @param tokens Tokens involved in settlement + /// @param clearingPrices Clearing prices for settlement + /// @param trades Trade data for settlement + /// @param interactions Interaction data for settlement + /// @param wrapperData Additional data for the wrapper (unused in this implementation) + function _wrap( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata interactions, + bytes calldata wrapperData + ) internal override { + // prevent reentrancy: there is no reason why we would want to allow it here + if (settleState != 0) { + revert NoReentrancy(); + } + settleState = 1; + + // Decode wrapperData into pre and post settlement actions + (IEVC.BatchItem[] memory preSettlementItems, IEVC.BatchItem[] memory postSettlementItems) = + abi.decode(wrapperData, (IEVC.BatchItem[], IEVC.BatchItem[])); + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](preSettlementItems.length + postSettlementItems.length + 1); + + // Copy pre-settlement items + for (uint256 i = 0; i < preSettlementItems.length; i++) { + items[i] = preSettlementItems[i]; + } + + // Add settlement call to wrapper - use _internalSettle from GPv2Wrapper + items[preSettlementItems.length] = IEVC.BatchItem({ + onBehalfOfAccount: msg.sender, + targetContract: address(this), + value: 0, + data: abi.encodeCall(this.evcInternalSettle, (tokens, clearingPrices, trades, interactions)) + }); + + // Copy post-settlement items + for (uint256 i = 0; i < postSettlementItems.length; i++) { + items[preSettlementItems.length + 1 + i] = postSettlementItems[i]; + } + + // Execute all items in a single batch + EVC.batch(items); + settleState = 0; + } + + /// @notice Internal settlement function called by EVC + /// @param tokens Tokens involved in settlement + /// @param clearingPrices Clearing prices for settlement + /// @param trades Trade data for settlement + /// @param interactions Interaction data for settlement + function evcInternalSettle( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata interactions + ) external payable { + if (msg.sender != address(EVC)) { + revert Unauthorized(msg.sender); + } + + if (settleState != 1) { + // origSender will be address(0) here which indiates that internal settle was called when it shouldn't be (outside of wrappedSettle call) + revert Unauthorized(address(0)); + } + + settleState = 2; + + // Use GPv2Wrapper's _internalSettle to call the settlement contract + _internalSettle(tokens, clearingPrices, trades, interactions); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 4831910..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/CowEvcWrapperTest.t.sol b/test/CowEvcWrapperTest.t.sol new file mode 100644 index 0000000..68d600c --- /dev/null +++ b/test/CowEvcWrapperTest.t.sol @@ -0,0 +1,475 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8; + +import {GPv2Signing} from "cow/mixins/GPv2Signing.sol"; +import {GPv2Order} from "cow/libraries/GPv2Order.sol"; + +import {IEVC} from "evc/EthereumVaultConnector.sol"; +import {IEVault, IERC4626, IBorrowing} from "euler-vault-kit/src/EVault/IEVault.sol"; + +import {EVaultTestBase} from "euler-vault-kit/test/unit/evault/EVaultTestBase.t.sol"; + +import {CowEvcWrapper, GPv2Trade, GPv2Interaction} from "../src/CowEvcWrapper.sol"; +import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; + +import {IERC20} from "cow/libraries/GPv2Trade.sol"; +import {console} from "forge-std/Test.sol"; + +import {CowBaseTest} from "./helpers/CowBaseTest.sol"; + +import {SignerECDSA} from "./helpers/SignerECDSA.sol"; + +contract CowEvcWrapperTest is CowBaseTest { + // Euler vaults + + SignerECDSA internal signerECDSA; + bytes internal emptySettleActions; + + function setUp() public override { + super.setUp(); + signerECDSA = new SignerECDSA(evc); + emptySettleActions = abi.encode(new IEVC.BatchItem[](0), new IEVC.BatchItem[](0)); + } + + function test_batchWithSettle_Empty() external { + vm.skip(bytes(FORK_RPC_URL).length == 0); + + ( + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) = getEmptySettlement(); + + vm.prank(address(solver)); + wrapper.wrappedSettle(tokens, clearingPrices, trades, interactions, emptySettleActions); + } + + function test_batchWithSettle_NonSolver() external { + vm.skip(bytes(FORK_RPC_URL).length == 0); + address nonSolver = makeAddr("nonSolver"); + vm.startPrank(nonSolver); + + ( + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) = getEmptySettlement(); + + vm.expectRevert("GPv2Wrapper: not a solver"); + wrapper.wrappedSettle(tokens, clearingPrices, trades, interactions, emptySettleActions); + } + + function test_batchWithSettle_WithCoWOrder() external { + vm.skip(bytes(FORK_RPC_URL).length == 0); + uint256 susdsBalanceInMilkSwapBefore = IERC20(SUSDS).balanceOf(address(milkSwap)); + + // Setup user with WETH + deal(WETH, user, 1e18); + vm.startPrank(user); + + // Create order parameters + uint256 sellAmount = 1e18; // 1 WETH + uint256 buyAmount = 999e18; // 1000 SUSDS + + // Get settlement, that sells WETH for SUSDS + ( + bytes memory orderUid, + , + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) = getSwapSettlement(user, user, sellAmount, buyAmount); + + // User, pre-approve the order + console.logBytes(orderUid); + cowSettlement.setPreSignature(orderUid, true); + + // Execute the settlement through the wrapper + vm.stopPrank(); + vm.startPrank(address(solver)); + + wrapper.wrappedSettle(tokens, clearingPrices, trades, interactions, emptySettleActions); + + // Verify the swap was executed + assertEq(IERC20(eSUSDS).balanceOf(user), buyAmount, "User should receive SUSDS"); + assertEq(IERC20(WETH).balanceOf(address(milkSwap)), sellAmount, "MilkSwap should receive WETH"); + + uint256 susdsBalanceInMilkSwapAfter = IERC20(SUSDS).balanceOf(address(milkSwap)); + assertEq( + susdsBalanceInMilkSwapAfter, susdsBalanceInMilkSwapBefore - buyAmount - 1e18, "MilkSwap should have less SUSDS" + ); + } + + function test_leverage_WithCoWOrder() external { + vm.skip(bytes(FORK_RPC_URL).length == 0); + + // sUSDS is not currently a collateral for WETH borrow, fix it + vm.startPrank(IEVault(eWETH).governorAdmin()); + IEVault(eWETH).setLTV(eSUSDS, 0.9e4, 0.9e4, 0); + + uint256 SUSDS_MARGIN = 2000e18; + // Setup user with SUSDS + deal(SUSDS, user, SUSDS_MARGIN); + + vm.startPrank(user); + + // Create order parameters + uint256 sellAmount = 1e18; // 1 WETH + uint256 buyAmount = 999e18; // 999 eSUSDS (1000 SUSDS actually deposited) + + // Get settlement, that sells WETH for SUSDS + // NOTE the receiver is the SUSDS vault, because we'll skim the output for the user in post-settlement + ( + bytes memory orderUid, + , + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) = getSwapSettlement(user, user, sellAmount, buyAmount); + + // User, pre-approve the order + console.logBytes(orderUid); + cowSettlement.setPreSignature(orderUid, true); + + signerECDSA.setPrivateKey(privateKey); + + // User approves SUSDS vault for deposit + IERC20(SUSDS).approve(eSUSDS, type(uint256).max); + + // Construct a batch with deposit of margin collateral and a borrow + // TODO user approved CoW vault relayer on WETH, therefore the borrow to user's wallet + // provides WETH to swap. It should be possible to do it without approval by setting borrow recipient + // to some trusted contract. EVC wrapper? The next batch item could be approving the relayer. + // How would an order be signed then? + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](4); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.enableCollateral, (user, eSUSDS)) + }); + items[1] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.enableController, (user, eWETH)) + }); + items[2] = IEVC.BatchItem({ + onBehalfOfAccount: user, + targetContract: eSUSDS, + value: 0, + data: abi.encodeCall(IERC4626.deposit, (SUSDS_MARGIN, user)) + }); + items[3] = IEVC.BatchItem({ + onBehalfOfAccount: user, + targetContract: eWETH, + value: 0, + data: abi.encodeCall(IBorrowing.borrow, (sellAmount, user)) + }); + + // User signs the batch + bytes memory batchData = abi.encodeCall(IEVC.batch, items); + bytes memory batchSignature = + signerECDSA.signPermit(user, address(wrapper), 0, 0, block.timestamp, 0, batchData); + + vm.stopPrank(); + + // pre-settlement will include nested batch signed and executed through `EVC.permit` + IEVC.BatchItem[] memory preSettlementItems = new IEVC.BatchItem[](1); + preSettlementItems[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + }); + + // post-settlement will check slippage and skim the free cash on the destination vault for the user + IEVC.BatchItem[] memory postSettlementItems = new IEVC.BatchItem[](0); + + // Execute the settlement through the wrapper + vm.stopPrank(); + + { + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + bytes memory evcActions = abi.encode(preSettlementItems, postSettlementItems); + targets[0] = address(wrapper); + datas[0] = abi.encodeWithSelector( + wrapper.wrappedSettle.selector, tokens, clearingPrices, trades, interactions, evcActions + ); + solver.runBatch(targets, datas); + + } + + // Verify the position was created + assertApproxEqAbs( + IEVault(eSUSDS).convertToAssets(IERC20(eSUSDS).balanceOf(user)), + buyAmount + SUSDS_MARGIN, + 1 ether, // rounding in favor of the vault during deposits + "User should receive eSUSDS" + ); + assertEq(IEVault(eWETH).debtOf(user), sellAmount, "User should receive eWETH debt"); + } + + function test_leverage_MaliciousSolverDoesntRedepositFull() external { + vm.skip(bytes(FORK_RPC_URL).length == 0); + + // sUSDS is not currently a collateral for WETH borrow, fix it + vm.startPrank(IEVault(eWETH).governorAdmin()); + IEVault(eWETH).setLTV(eSUSDS, 0.9e4, 0.9e4, 0); + + uint256 SUSDS_MARGIN = 2000e18; + // Setup user with SUSDS + deal(SUSDS, user, SUSDS_MARGIN); + + vm.startPrank(user); + + // Create order parameters + uint256 sellAmount = 1e18; // 1 WETH + uint256 buyAmount = 999e18; // 999 eSUSDS + + // Get settlement, that sells WETH for buying SUSDS + // NOTE the receiver is the SUSDS vault, because we'll skim the output for the user in post-settlement + ( + bytes memory orderUid, + , + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) = getSwapSettlement(user, user, sellAmount, buyAmount); + + // User, pre-approve the order + console.logBytes(orderUid); + cowSettlement.setPreSignature(orderUid, true); + + signerECDSA.setPrivateKey(privateKey); + + // User approves SUSDS vault for deposit + IERC20(SUSDS).approve(eSUSDS, type(uint256).max); + + // Construct a batch with deposit of margin collateral and a borrow + // TODO user approved CoW vault relayer on WETH, therefore the borrow to user's wallet + // provides WETH to swap. It should be possible to do it without approval by setting borrow recipient + // to some trusted contract. EVC wrapper? The next batch item could be approving the relayer. + // How would an order be signed then? + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](4); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.enableCollateral, (user, eSUSDS)) + }); + items[1] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.enableController, (user, eWETH)) + }); + items[2] = IEVC.BatchItem({ + onBehalfOfAccount: user, + targetContract: eSUSDS, + value: 0, + data: abi.encodeCall(IERC4626.deposit, (SUSDS_MARGIN, user)) + }); + items[3] = IEVC.BatchItem({ + onBehalfOfAccount: user, + targetContract: eWETH, + value: 0, + data: abi.encodeCall(IBorrowing.borrow, (sellAmount, user)) + }); + + // User signs the batch + bytes memory batchData = abi.encodeCall(IEVC.batch, items); + bytes memory batchSignature = + signerECDSA.signPermit(user, address(wrapper), 0, 0, block.timestamp, 0, batchData); + + vm.stopPrank(); + + // pre-settlement will include nested batch signed and executed through `EVC.permit` + IEVC.BatchItem[] memory preSettlementItems = new IEVC.BatchItem[](1); + preSettlementItems[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + }); + + // post-settlement, first lets assume we don't call the swap verifier + IEVC.BatchItem[] memory postSettlementItems = new IEVC.BatchItem[](0); + + // Execute the settlement through the wrapper + vm.stopPrank(); + //vm.startPrank(solver); + + { + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + bytes memory evcActions = abi.encode(preSettlementItems, postSettlementItems); + targets[0] = address(wrapper); + datas[0] = abi.encodeWithSelector( + wrapper.wrappedSettle.selector, tokens, clearingPrices, trades, interactions, evcActions + ); + + solver.runBatch(targets, datas); + } + + // Verify the position was created + assertApproxEqAbs( + IEVault(eSUSDS).convertToAssets(IERC20(eSUSDS).balanceOf(user)), + buyAmount + SUSDS_MARGIN, + 1 ether, // rounding in favor of the vault during deposits + "User should receive eSUSDS" + ); + assertEq(IEVault(eWETH).debtOf(user), sellAmount, "User should receive eWETH debt"); + } + + function test_leverage_MaliciousNonSolverCallsInternalSettleDirectly() external { + vm.skip(bytes(FORK_RPC_URL).length == 0); + + // sUSDS is not currently a collateral for WETH borrow, fix it + vm.startPrank(IEVault(eWETH).governorAdmin()); + IEVault(eWETH).setLTV(eSUSDS, 0.9e4, 0.9e4, 0); + + uint256 SUSDS_MARGIN = 2000e18; + // Setup user with SUSDS + deal(SUSDS, user, SUSDS_MARGIN); + + vm.startPrank(user); + + // Create order parameters + uint256 sellAmount = 1e18; // 1 WETH + uint256 buyAmount = 999e18; // 999 eSUSDS + + // Get settlement, that sells WETH for SUSDS + // NOTE the receiver is the SUSDS vault, because we'll skim the output for the user in post-settlement + ( + bytes memory orderUid, + , + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) = getSwapSettlement(user, eSUSDS, sellAmount, buyAmount); + cowSettlement.setPreSignature(orderUid, true); + + vm.stopPrank(); + + // User approves SUSDS vault for deposit + IERC20(SUSDS).approve(eSUSDS, type(uint256).max); + + // This contract will be the "malicious" solver. It should not be able to complete the settle flow + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(this), + targetContract: address(wrapper), + value: 0, + data: abi.encodeCall(CowEvcWrapper.evcInternalSettle, (tokens, clearingPrices, trades, interactions)) + }); + + vm.expectRevert(abi.encodeWithSelector(CowEvcWrapper.Unauthorized.selector, address(0))); + evc.batch(items); + } + + function test_leverage_MaliciousNonSolverTriesToDoIt() external { + vm.skip(bytes(FORK_RPC_URL).length == 0); + + // sUSDS is not currently a collateral for WETH borrow, fix it + vm.startPrank(IEVault(eWETH).governorAdmin()); + IEVault(eWETH).setLTV(eSUSDS, 0.9e4, 0.9e4, 0); + + uint256 SUSDS_MARGIN = 2000e18; + // Setup user with SUSDS + deal(SUSDS, user, SUSDS_MARGIN); + + vm.startPrank(user); + + // Create order parameters + uint256 sellAmount = 1e18; // 1 WETH + uint256 buyAmount = 1000e18; // 1000 SUSDS + + // Get settlement, that sells WETH for SUSDS + // NOTE the receiver is the SUSDS vault, because we'll skim the output for the user in post-settlement + ( + bytes memory orderUid, + , + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) = getSwapSettlement(user, eSUSDS, sellAmount, buyAmount); + + // User, pre-approve the order + console.logBytes(orderUid); + cowSettlement.setPreSignature(orderUid, true); + + signerECDSA.setPrivateKey(privateKey); + + // User approves SUSDS vault for deposit + IERC20(SUSDS).approve(eSUSDS, type(uint256).max); + + // Construct a batch with deposit of margin collateral and a borrow + // TODO user approved CoW vault relayer on WETH, therefore the borrow to user's wallet + // provides WETH to swap. It should be possible to do it without approval by setting borrow recipient + // to some trusted contract. EVC wrapper? The next batch item could be approving the relayer. + // How would an order be signed then? + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](4); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.enableCollateral, (user, eSUSDS)) + }); + items[1] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.enableController, (user, eWETH)) + }); + items[2] = IEVC.BatchItem({ + onBehalfOfAccount: user, + targetContract: eSUSDS, + value: 0, + data: abi.encodeCall(IERC4626.deposit, (SUSDS_MARGIN, user)) + }); + items[3] = IEVC.BatchItem({ + onBehalfOfAccount: user, + targetContract: eWETH, + value: 0, + data: abi.encodeCall(IBorrowing.borrow, (sellAmount, user)) + }); + + // User signs the batch + bytes memory batchData = abi.encodeCall(IEVC.batch, items); + bytes memory batchSignature = + signerECDSA.signPermit(user, address(wrapper), 0, 0, block.timestamp, 0, batchData); + + vm.stopPrank(); + + // pre-settlement will include nested batch signed and executed through `EVC.permit` + IEVC.BatchItem[] memory preSettlementItems = new IEVC.BatchItem[](1); + preSettlementItems[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + }); + + // post-settlement does not need to do anything because the settlement contract will automatically verify the amount of remaining funds + IEVC.BatchItem[] memory postSettlementItems = new IEVC.BatchItem[](0); + + // Execute the settlement through the wrapper + vm.stopPrank(); + + // This contract will be the "malicious" solver. It should not be able to complete the settle flow + bytes memory evcActions = abi.encode(preSettlementItems, postSettlementItems); + + vm.expectRevert("GPv2Wrapper: not a solver"); + wrapper.wrappedSettle(tokens, clearingPrices, trades, interactions, evcActions); + } +} diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol new file mode 100644 index 0000000..4611ff1 --- /dev/null +++ b/test/helpers/CowBaseTest.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8; + +import {GPv2Signing} from "cow/mixins/GPv2Signing.sol"; +import {GPv2Order} from "cow/libraries/GPv2Order.sol"; +import {IERC20} from "cow/libraries/GPv2Trade.sol"; + +import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; +import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; +import {EVaultTestBase} from "lib/euler-vault-kit/test/unit/evault/EVaultTestBase.t.sol"; +import {IVault} from "euler-vault-kit/src/EVault/IEVault.sol"; + +import {CowEvcWrapper} from "../../src/CowEvcWrapper.sol"; +import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; +import {GPv2Settlement} from "cow/GPv2Settlement.sol"; +import {CowEvcWrapper, GPv2Trade, GPv2Interaction} from "../../src/CowEvcWrapper.sol"; + +import {MilkSwap} from "./MilkSwap.sol"; +import {GPv2OrderHelper} from "./GPv2OrderHelper.sol"; + +import {console} from "forge-std/Test.sol"; + +// intermediate contrct that acts as solver and creates a "batched" transaction +contract Solver { + function runBatch(address[] memory targets, bytes[] memory datas) external { + for (uint256 i = 0; i < targets.length; i++) { + targets[i].call(datas[i]); + } + } +} + +contract CowBaseTest is EVaultTestBase { + uint256 mainnetFork; + uint256 BLOCK_NUMBER = 22546006; + string FORK_RPC_URL = vm.envOr("FORK_RPC_URL", string("")); + + address constant allowListManager = 0xA03be496e67Ec29bC62F01a428683D7F9c204930; + + address constant SUSDS = 0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD; + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // Vaults + address internal eSUSDS = 0x1e548CfcE5FCF17247E024eF06d32A01841fF404; + address internal eWETH = 0xD8b27CF359b7D15710a5BE299AF6e7Bf904984C2; + + address payable constant realEVC = payable(0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383); + address internal swapVerifier = 0xae26485ACDDeFd486Fe9ad7C2b34169d360737c7; + + GPv2Settlement constant cowSettlement = GPv2Settlement(payable(0x9008D19f58AAbD9eD0D60971565AA8510560ab41)); + + CowEvcWrapper public wrapper; + MilkSwap public milkSwap; + address user; + uint256 privateKey = 123; + + GPv2OrderHelper helper; + + Solver internal solver; + + function setUp() public virtual override { + super.setUp(); + helper = new GPv2OrderHelper(); + solver = new Solver(); + + if (bytes(FORK_RPC_URL).length == 0) { + revert("Must supply FORK_RPC_URL"); + } + + mainnetFork = vm.createSelectFork(FORK_RPC_URL); + vm.rollFork(BLOCK_NUMBER); + + evc = EthereumVaultConnector(realEVC); + + user = vm.addr(privateKey); + wrapper = new CowEvcWrapper(address(evc), payable(cowSettlement)); + + // Add wrapper and our fake solver as solver + GPv2AllowListAuthentication allowList = GPv2AllowListAuthentication(address(cowSettlement.authenticator())); + address manager = allowList.manager(); + // vm.deal(address(manager), 1e18); + vm.startPrank(manager); + allowList.addSolver(address(wrapper)); + allowList.addSolver(address(solver)); + vm.stopPrank(); + + // Setup some liquidity for MilkSwap + milkSwap = new MilkSwap(SUSDS); + deal(SUSDS, address(milkSwap), 10000e18); // Add SUSDS to MilkSwap + milkSwap.setPrice(WETH, 1000); // 1 ETH = 1,000 SUSDS + + // Set the approval for MilSwap in the settlement + vm.prank(address(cowSettlement)); + IERC20(WETH).approve(address(milkSwap), type(uint256).max); + + // User has approved WETH for COW Protocol + address vaultRelayer = address(cowSettlement.vaultRelayer()); + vm.prank(user); + IERC20(WETH).approve(vaultRelayer, type(uint256).max); + + // Setup labels + vm.label(allowListManager, "allowListManager"); + vm.label(user, "user"); + vm.label(SUSDS, "SUSDS"); + vm.label(WETH, "WETH"); + vm.label(eSUSDS, "eSUSDS"); + vm.label(eWETH, "eWETH"); + vm.label(address(cowSettlement), "cowSettlement"); + vm.label(address(wrapper), "wrapper"); + vm.label(address(milkSwap), "milkSwap"); + } + + function getEmptySettlement() + public + pure + returns ( + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) + { + return ( + new IERC20[](0), + new uint256[](0), + new GPv2Trade.Data[](0), + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)] + ); + } + + function getOrderUid(address owner, GPv2Order.Data memory orderData) public view returns (bytes memory orderUid) { + // Generate order digest using EIP-712 + bytes32 orderDigest = GPv2Order.hash(orderData, cowSettlement.domainSeparator()); + + // Create order UID by concatenating orderDigest, owner, and validTo + return abi.encodePacked(orderDigest, address(owner), uint32(orderData.validTo)); + } + + function getSwapInteraction(uint256 sellAmount) public view returns (GPv2Interaction.Data memory) { + return GPv2Interaction.Data({ + target: address(milkSwap), + value: 0, + callData: abi.encodeCall(MilkSwap.swap, (WETH, SUSDS, sellAmount)) + }); + } + + function getDepositInteraction(uint256 buyAmount) public view returns (GPv2Interaction.Data memory) { + return GPv2Interaction.Data({ + target: address(SUSDS), + value: 0, + callData: abi.encodeCall(IERC20.transfer, (eSUSDS, buyAmount)) + }); + } + + function getSkimInteraction() public view returns (GPv2Interaction.Data memory) { + return GPv2Interaction.Data({ + target: address(eSUSDS), + value: 0, + callData: abi.encodeCall(IVault.skim, (type(uint256).max, address(cowSettlement))) + }); + } + + function getTradeData(uint256 sellAmount, uint256 buyAmount, uint32 validTo, address owner, address receiver) + public + pure + returns (GPv2Trade.Data memory) + { + // Set flags for (pre-sign, FoK sell order) + // See + // https://github.com/cowprotocol/contracts/blob/08f8627d8427c8842ae5d29ed8b44519f7674879/src/contracts/libraries/GPv2Trade.sol#L89-L94 + uint256 flags = 3 << 5; // 1100000 + + return GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: receiver, + sellAmount: sellAmount, + buyAmount: buyAmount, + validTo: validTo, + appData: bytes32(0), + feeAmount: 0, + flags: flags, + executedAmount: 0, + signature: abi.encodePacked(owner) + }); + } + + function getTokensAndPrices() public view returns (IERC20[] memory tokens, uint256[] memory clearingPrices) { + tokens = new IERC20[](2); + tokens[0] = IERC20(WETH); + tokens[1] = IERC20(eSUSDS); + + clearingPrices = new uint256[](2); + clearingPrices[0] = 999; // WETH price (if it was against SUSD then 1000) + clearingPrices[1] = 1; // eSUSDS price + } + + function getSwapSettlement(address owner, address receiver, uint256 sellAmount, uint256 buyAmount) + public + view + returns ( + bytes memory orderUid, + GPv2Order.Data memory orderData, + IERC20[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) + { + uint32 validTo = uint32(block.timestamp + 1 hours); + + // Create order data + orderData = GPv2Order.Data({ + sellToken: IERC20(WETH), + buyToken: IERC20(eSUSDS), + receiver: receiver, + sellAmount: sellAmount, + buyAmount: buyAmount, + validTo: validTo, + appData: bytes32(0), + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + + // Get order UID for the order + orderUid = getOrderUid(owner, orderData); + + // Get trade data + trades = new GPv2Trade.Data[](1); + trades[0] = getTradeData(sellAmount, buyAmount, validTo, owner, orderData.receiver); + + // Get tokens and prices + (tokens, clearingPrices) = getTokensAndPrices(); + + // Setup interactions + interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](3), new GPv2Interaction.Data[](0)]; + interactions[1][0] = getSwapInteraction(sellAmount); + interactions[1][1] = getDepositInteraction(buyAmount + 1 ether); + interactions[1][2] = getSkimInteraction(); + return (orderUid, orderData, tokens, clearingPrices, trades, interactions); + } +} diff --git a/test/helpers/MilkSwap.sol b/test/helpers/MilkSwap.sol new file mode 100644 index 0000000..aeea525 --- /dev/null +++ b/test/helpers/MilkSwap.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8; + +import {IERC20} from "../../src/vendor/interfaces/IERC20.sol"; + +contract MilkSwap { + mapping(address => uint256) public prices; // Price expressed in atoms of the quote per unit of the base token + address public quoteToken; + + constructor(address _quoteToken) { + quoteToken = _quoteToken; + } + + function setPrice(address token, uint256 price) external { + prices[token] = price; + } + + function getAmountOut(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut) { + uint256 price = prices[tokenIn]; + require(price > 0, "tokenIn is not supported"); + + return (amountIn * price); + } + + function swap(address tokenIn, address tokenOut, uint256 amountIn) external { + require(tokenOut == quoteToken, "tokenOut must be the quote token"); + + uint256 amountOut = this.getAmountOut(tokenIn, amountIn); + + IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); + IERC20(tokenOut).transfer(msg.sender, amountOut); + } +}