From 70bb676ced138e41ed80beecd7add8a887468601 Mon Sep 17 00:00:00 2001 From: Kaze Date: Tue, 7 Oct 2025 18:21:04 +0900 Subject: [PATCH 01/43] add wrapper --- src/vendor/CowWrapper.sol | 114 +++++++ test/CowWrapper.t.sol | 477 +++++++++++++++++++++++++++++ test/EmptyWrapper.sol | 16 + test/helpers/CowWrapperHelpers.sol | 40 +++ 4 files changed, 647 insertions(+) create mode 100644 src/vendor/CowWrapper.sol create mode 100644 test/CowWrapper.t.sol create mode 100644 test/EmptyWrapper.sol create mode 100644 test/helpers/CowWrapperHelpers.sol diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol new file mode 100644 index 0000000..41613a9 --- /dev/null +++ b/src/vendor/CowWrapper.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.7.6 <0.9.0; +pragma abicoder v2; + +import {IERC20, GPv2Trade, GPv2Interaction, GPv2Authentication} from "cow/GPv2Settlement.sol"; + +import "forge-std/console.sol"; + +interface CowSettlement { + function settle( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata interactions + ) external; +} + +/** + * @dev Interface defining required methods for wrappers of the GPv2Settlement contract for CoW orders + * A wrapper should: + * * call the equivalent `settle` on the GPv2Settlement contract (0x9008D19f58AAbD9eD0D60971565AA8510560ab41) + * * verify that the caller is authorized via the GPv2Authentication contract. + * A wrapper may also execute, or otherwise put the blockchain in a state that needs to be established prior to settlement. + * Additionally, it needs to be approved by the GPv2Authentication contract + */ +abstract contract CowWrapper is CowSettlement { + event GasLeft(uint256); + error NotASolver(address unauthorized); + error WrapperHasNoSettleTarget(uint256 settleDataLength, uint256 fullCalldataLength); + + GPv2Authentication public immutable AUTHENTICATOR; + + constructor(GPv2Authentication authenticator_) { + // retrieve the authentication we are supposed to use from the settlement contract + AUTHENTICATOR = authenticator_; + } + + /** + * @dev Called to initiate a wrapped call against the settlement function. See GPv2Settlement.settle() for more information. + */ + function settle( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata interactions + ) external { + // Revert if not a valid solver + if (!AUTHENTICATOR.isSolver(msg.sender)) { + revert NotASolver(msg.sender); + } + + // Extract additional data appended after settle calldata + uint256 settleEnd = _settleCalldataLength(interactions); + + // Require additional data for next settlement address + if (msg.data.length < settleEnd + 32) { + revert WrapperHasNoSettleTarget(settleEnd, msg.data.length); + } + + // Additional data exists after the settle parameters + bytes calldata additionalData = msg.data[settleEnd:]; + + // the settle data will always be after the first 4 bytes (selector), up to the computed data end point + _wrap(msg.data[4:settleEnd], additionalData); + } + + /** + * @dev The logic for the wrapper. During this function, `_internalSettle` should be called. `wrapperData` may be consumed as required for the wrapper's particular requirements + */ + function _wrap( + bytes calldata settleData, + bytes calldata wrapperData + ) internal virtual; + + function _internalSettle( + bytes calldata settleData, + bytes calldata wrapperData + ) internal { + // the next settlement address to call will be the next word of the wrapper data + address nextSettlement; + assembly { + nextSettlement := calldataload(wrapperData.offset) + } + wrapperData = wrapperData[32:]; + // Encode the settle call + bytes memory fullCalldata; + + (bool success, bytes memory returnData) = nextSettlement.call(abi.encodePacked(CowSettlement.settle.selector, settleData, wrapperData)); + + //(bool success, bytes memory returnData) = nextSettlement.call(fullCalldata); + if (!success) { + // Bubble up the revert reason + assembly { + revert(add(returnData, 0x20), mload(returnData)) + } + } + } + + /** + * @dev Computes the length of the settle() calldata in bytes. + * This can be used to determine if there is additional data appended to msg.data. + * @return end The calldata position in bytes of the end of settle() function calldata + */ + function _settleCalldataLength( + GPv2Interaction.Data[][3] calldata interactions + ) internal pure returns (uint256 end) { + // NOTE: technically this function could fail to return the correct length, if the data encoded in the ABI is provided indexed in an unusual order + // however, doing a deeper check of the total data is very expensive and we are generally working with callers who provide data in a verifiably standardized format + GPv2Interaction.Data[] calldata lastInteractions = interactions[2]; + assembly { + end := add(lastInteractions.offset, lastInteractions.length) + } + } +} diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol new file mode 100644 index 0000000..fd87b38 --- /dev/null +++ b/test/CowWrapper.t.sol @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import {Test} from "forge-std/Test.sol"; +import {CowWrapper, CowSettlement} from "../src/vendor/CowWrapper.sol"; +import {IERC20, GPv2Trade, GPv2Interaction, GPv2Authentication} from "cow/GPv2Settlement.sol"; +import {EmptyWrapper} from "./EmptyWrapper.sol"; + +import {CowWrapperHelpers} from "./helpers/CowWrapperHelpers.sol"; + +import "forge-std/console.sol"; + +contract MockAuthentication { + mapping(address => bool) public solvers; + + function addSolver(address solver) external { + solvers[solver] = true; + } + + function isSolver(address solver) external view returns (bool) { + return solvers[solver]; + } +} + +contract MockSettlement is CowSettlement { + struct SettleCall { + IERC20[] tokens; + uint256[] clearingPrices; + GPv2Trade.Data[] trades; + bytes additionalData; + } + + SettleCall[] public settleCalls; + + function settle( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata + ) external override { + SettleCall storage call_ = settleCalls.push(); + call_.tokens = tokens; + call_.clearingPrices = clearingPrices; + + for (uint256 i = 0; i < trades.length; i++) { + call_.trades.push(trades[i]); + } + + // Extract any additional data appended after standard calldata + uint256 expectedLength = 4 + 4 * 32; + expectedLength += 32 + tokens.length * 32; + expectedLength += 32 + clearingPrices.length * 32; + expectedLength += 32; + for (uint256 i = 0; i < trades.length; i++) { + expectedLength += 9 * 32 + 32 + trades[i].signature.length; + } + expectedLength += 32; + expectedLength += 3 * 32; + expectedLength += 3 * 32; + + if (msg.data.length > expectedLength) { + call_.additionalData = msg.data[expectedLength:]; + } + } + + function getSettleCallCount() external view returns (uint256) { + return settleCalls.length; + } + + function getLastSettleCall() + external + view + returns (uint256 tokenCount, uint256 priceCount, uint256 tradeCount, bytes memory additionalData) + { + require(settleCalls.length > 0, "No settle calls"); + SettleCall storage lastCall = settleCalls[settleCalls.length - 1]; + return (lastCall.tokens.length, lastCall.clearingPrices.length, lastCall.trades.length, lastCall.additionalData); + } +} + +contract CowWrapperTest is Test, CowWrapper { + MockAuthentication public authenticator; + MockSettlement public mockSettlement; + address public solver; + + EmptyWrapper private wrapper1; + EmptyWrapper private wrapper2; + EmptyWrapper private wrapper3; + + // Track _wrap calls + struct WrapCall { + bytes settleData; + bytes wrapperData; + } + + WrapCall[] public wrapCalls; + + uint256 private skipWrappedData; + + constructor() CowWrapper(GPv2Authentication(address(0))) { + // Constructor will be called in setUp with proper authenticator + } + + function setUp() public { + // Deploy MockAuthentication and etch it to address(0) so the immutable AUTHENTICATOR works + authenticator = new MockAuthentication(); + vm.etch(address(0), address(authenticator).code); + + solver = makeAddr("solver"); + // Add solver via address(0) which now has the MockAuthentication code + MockAuthentication(address(0)).addSolver(solver); + + mockSettlement = new MockSettlement(); + + // Create three EmptyWrapper instances + wrapper1 = new EmptyWrapper(GPv2Authentication(address(0))); + wrapper2 = new EmptyWrapper(GPv2Authentication(address(0))); + wrapper3 = new EmptyWrapper(GPv2Authentication(address(0))); + + // Add all wrappers as solvers + MockAuthentication(address(0)).addSolver(address(wrapper1)); + MockAuthentication(address(0)).addSolver(address(wrapper2)); + MockAuthentication(address(0)).addSolver(address(wrapper3)); + + } + + function _wrap( + bytes calldata settleData, + bytes calldata wrapperData + ) internal override { + // Record the wrap call + WrapCall storage call_ = wrapCalls.push(); + call_.settleData = settleData; + call_.wrapperData = wrapperData[0:skipWrappedData]; + + // Call internal settle + _internalSettle(settleData, wrapperData[skipWrappedData:]); + } + + // These function needs to be exposed because the internal function expects calldata, so this is convienience to accomplish that + function exposed_settleCalldataLength( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata interactions + ) external pure returns (uint256) { + return _settleCalldataLength(interactions); + } + + function exposed_internalSettle( + bytes calldata settleData, + bytes calldata wrapperData + ) external { + _internalSettle(settleData, wrapperData); + } + + function test_debug_tradeEncoding() public view { + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = IERC20(address(0x1)); + tokens[1] = IERC20(address(0x2)); + + uint256[] memory clearingPrices = new uint256[](2); + clearingPrices[0] = 100; + clearingPrices[1] = 200; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1); + trades[0] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: address(0x123), + sellAmount: 1000, + buyAmount: 900, + validTo: uint32(block.timestamp + 1000), + appData: bytes32(0), + feeAmount: 10, + flags: 0, + executedAmount: 0, + signature: hex"1234567890" + }); + + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + bytes memory encoded = abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions); + + // Log the length difference + uint256 calculated = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); + uint256 actual = encoded.length; + + // The difference tells us what we're missing + // Actual (996) - Calculated (873) = 123 bytes + assertEq(calculated, actual, "Length mismatch"); + } + + function test_settleCalldataLength_EmptyArrays() public view { + IERC20[] memory tokens = new IERC20[](0); + uint256[] memory clearingPrices = new uint256[](0); + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); + + assertEq( + length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length + ); + } + + function test_settleCalldataLength_WithTokensAndPrices() public view { + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = IERC20(address(0x1)); + tokens[1] = IERC20(address(0x2)); + + uint256[] memory clearingPrices = new uint256[](2); + clearingPrices[0] = 100; + clearingPrices[1] = 200; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); + + assertEq( + length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length + ); + } + + function test_settleCalldataLength_WithOneTrade() public view { + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = IERC20(address(0x1)); + tokens[1] = IERC20(address(0x2)); + + uint256[] memory clearingPrices = new uint256[](2); + clearingPrices[0] = 100; + clearingPrices[1] = 200; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1); + trades[0] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: address(0x123), + sellAmount: 1000, + buyAmount: 900, + validTo: uint32(block.timestamp + 1000), + appData: bytes32(0), + feeAmount: 10, + flags: 0, + executedAmount: 0, + signature: hex"1234567890" + }); + + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); + + assertEq( + length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length + ); + } + + function test_settleCalldataLength_WithInteractions() public view { + IERC20[] memory tokens = new IERC20[](0); + uint256[] memory clearingPrices = new uint256[](0); + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); + + GPv2Interaction.Data[][3] memory interactions; + interactions[0] = new GPv2Interaction.Data[](1); + interactions[0][0] = GPv2Interaction.Data({target: address(0x456), value: 0, callData: hex"aabbccdd"}); + interactions[1] = new GPv2Interaction.Data[](0); + interactions[2] = new GPv2Interaction.Data[](0); + + uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); + + assertEq( + length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length + ); + } + + function test_settleCalldataLength_ComplexCase() public view { + IERC20[] memory tokens = new IERC20[](3); + tokens[0] = IERC20(address(0x1)); + tokens[1] = IERC20(address(0x2)); + tokens[2] = IERC20(address(0x3)); + + uint256[] memory clearingPrices = new uint256[](3); + clearingPrices[0] = 100; + clearingPrices[1] = 200; + clearingPrices[2] = 300; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](2); + trades[0] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: address(0x123), + sellAmount: 1000, + buyAmount: 900, + validTo: uint32(block.timestamp + 1000), + appData: bytes32(uint256(1)), + feeAmount: 10, + flags: 0, + executedAmount: 0, + signature: hex"aabbccddee" + }); + trades[1] = GPv2Trade.Data({ + sellTokenIndex: 1, + buyTokenIndex: 2, + receiver: address(0x456), + sellAmount: 2000, + buyAmount: 1800, + validTo: uint32(block.timestamp + 2000), + appData: bytes32(uint256(2)), + feeAmount: 20, + flags: 1, + executedAmount: 0, + signature: hex"112233445566778899aabbcc" + }); + + GPv2Interaction.Data[][3] memory interactions; + interactions[0] = new GPv2Interaction.Data[](1); + interactions[0][0] = GPv2Interaction.Data({target: address(0x789), value: 100, callData: hex"00112233"}); + interactions[1] = new GPv2Interaction.Data[](0); + interactions[2] = new GPv2Interaction.Data[](1); + interactions[2][0] = GPv2Interaction.Data({target: address(0xabc), value: 0, callData: hex"deadbeefcafe"}); + + uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); + + // Base: 452, Tokens: 96, Prices: 96, Trade1: 325, Trade2: 332, Int[0][0]: 132, Int[2][0]: 134 + assertEq( + length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length + ); + } + + function test_wrap_ReceivesCorrectParameters() public { + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = IERC20(address(0x1)); + + uint256[] memory clearingPrices = new uint256[](1); + clearingPrices[0] = 100; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + bytes memory wrapperData = hex"deadbeef"; + + address nextSettlement = address(mockSettlement); + bytes memory additionalData = abi.encodePacked(wrapperData, abi.encode(nextSettlement)); + bytes memory settleCalldata = + abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory fullCalldata = abi.encodePacked(settleCalldata, additionalData); + + skipWrappedData = wrapperData.length; + + vm.prank(solver); + (bool success,) = address(this).call(fullCalldata); + skipWrappedData = 0; + assertTrue(success); + + assertEq(wrapCalls.length, 1); + assertGt(wrapCalls[0].settleData.length, 0); + assertEq(wrapCalls[0].wrapperData, wrapperData); + } + + function test_internalSettle_CallsNextSettlement() public { + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = IERC20(address(0x1)); + + uint256[] memory clearingPrices = new uint256[](1); + clearingPrices[0] = 100; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + address nextSettlement = address(mockSettlement); + + bytes memory settleCalldata = + abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory fullCalldata = abi.encodePacked(settleCalldata, abi.encode(nextSettlement)); + + vm.prank(solver); + (bool success,) = address(this).call(fullCalldata); + assertTrue(success); + + assertEq(mockSettlement.getSettleCallCount(), 1); + (uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); + assertEq(tokenCount, 1); + assertEq(priceCount, 1); + assertEq(tradeCount, 0); + } + + function test_settle_RevertsWithWrapperHasNoSettleTarget() public { + IERC20[] memory tokens = new IERC20[](0); + uint256[] memory clearingPrices = new uint256[](0); + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + // Should revert when called without any additional wrapper data + vm.prank(solver); + vm.expectRevert(abi.encodeWithSelector(CowWrapper.WrapperHasNoSettleTarget.selector, 420, 420)); + this.settle(tokens, clearingPrices, trades, interactions); + } + + function test_settle_RevertsWithNotASolver() public { + IERC20[] memory tokens = new IERC20[](0); + uint256[] memory clearingPrices = new uint256[](0); + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + address notASolver = makeAddr("notASolver"); + + // Should revert when called by non-solver + vm.prank(notASolver); + vm.expectRevert(abi.encodeWithSelector(CowWrapper.NotASolver.selector, notASolver)); + this.settle(tokens, clearingPrices, trades, interactions); + } + + function test_integration_ThreeWrappersChained() public { + CowWrapperHelpers.SettleCall memory settlement; + + settlement.tokens = new IERC20[](2); + settlement.tokens[0] = IERC20(address(0x1)); + settlement.tokens[1] = IERC20(address(0x2)); + + settlement.clearingPrices = new uint256[](2); + settlement.clearingPrices[0] = 100; + settlement.clearingPrices[1] = 200; + + settlement.trades = new GPv2Trade.Data[](1); + settlement.trades[0] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: address(0x123), + sellAmount: 1000, + buyAmount: 900, + validTo: uint32(block.timestamp + 1000), + appData: bytes32(uint256(1)), + feeAmount: 10, + flags: 0, + executedAmount: 0, + signature: hex"aabbccddee" + }); + + settlement.interactions = + [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + // Build the chained wrapper data: + // solver -> wrapper1 -> wrapper2 -> wrapper3 -> mockSettlement + + address[] memory wrappers = new address[](3); + wrappers[0] = address(wrapper1); + wrappers[1] = address(wrapper2); + wrappers[2] = address(wrapper3); + + bytes[] memory wrapperDatas = new bytes[](3); + + (address target, bytes memory fullCalldata) = CowWrapperHelpers.encodeWrapperCall(wrappers, wrapperDatas, address(mockSettlement), settlement); + + // Call wrapper1 as the solver + vm.prank(solver); + (bool success,) = address(wrapper1).call(fullCalldata); + assertTrue(success, "Chained wrapper call should succeed"); + + // Verify that mockSettlement was called + assertEq(mockSettlement.getSettleCallCount(), 1, "MockSettlement should be called once"); + + // Verify the settlement received the correct parameters + //(uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); + //assertEq(tokenCount, 2, "Should have 2 tokens"); + //assertEq(priceCount, 2, "Should have 2 prices"); + //assertEq(tradeCount, 1, "Should have 1 trade"); + } +} diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol new file mode 100644 index 0000000..2be39f5 --- /dev/null +++ b/test/EmptyWrapper.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; +pragma abicoder v2; + +import "../src/vendor/CowWrapper.sol"; + +contract EmptyWrapper is CowWrapper { + constructor(GPv2Authentication authenticator_) CowWrapper(authenticator_) {} + + function _wrap( + bytes calldata settleData, + bytes calldata wrappedData + ) internal override { + _internalSettle(settleData, wrappedData); + } +} diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol new file mode 100644 index 0000000..885203f --- /dev/null +++ b/test/helpers/CowWrapperHelpers.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import {IERC20, GPv2Trade, GPv2Interaction, GPv2Authentication} from "cow/GPv2Settlement.sol"; + +import {CowSettlement} from "src/vendor/CowWrapper.sol"; + +library CowWrapperHelpers { + struct SettleCall { + IERC20[] tokens; + uint256[] clearingPrices; + GPv2Trade.Data[] trades; + GPv2Interaction.Data[][3] interactions; + } + + /** + * @dev This function is intended for testing purposes and is not memory efficient. + */ + function encodeWrapperCall( + address[] calldata wrappers, + bytes[] calldata wrapperDatas, + address cowSettlement, + SettleCall calldata settlement + ) external returns (address target, bytes memory wrapperData) { + for (uint256 i = 0; i < wrappers.length; i++) { + wrapperData = abi.encodePacked( + wrapperData, + wrapperDatas[i], + uint256(uint160(wrappers.length > i + 1 ? wrappers[i + 1] : cowSettlement)) + ); + } + + // build the settle calldata and append the wrapper data directly to the end of it + bytes memory settleCalldata = + abi.encodeWithSelector(CowSettlement.settle.selector, settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions); + wrapperData = abi.encodePacked(settleCalldata, wrapperData); + + return (wrappers[0], wrapperData); + } +} From 4a1683ae564a819de63ad710b884bd34cd127259 Mon Sep 17 00:00:00 2001 From: Kaze Date: Tue, 7 Oct 2025 18:42:05 +0900 Subject: [PATCH 02/43] fix test failure --- src/vendor/CowWrapper.sol | 36 +++++++++++++++++++++--------------- test/CowWrapper.t.sol | 34 ++++++++++++++-------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 41613a9..873e1ec 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -25,6 +25,7 @@ interface CowSettlement { */ abstract contract CowWrapper is CowSettlement { event GasLeft(uint256); + error NotASolver(address unauthorized); error WrapperHasNoSettleTarget(uint256 settleDataLength, uint256 fullCalldataLength); @@ -67,15 +68,9 @@ abstract contract CowWrapper is CowSettlement { /** * @dev The logic for the wrapper. During this function, `_internalSettle` should be called. `wrapperData` may be consumed as required for the wrapper's particular requirements */ - function _wrap( - bytes calldata settleData, - bytes calldata wrapperData - ) internal virtual; - - function _internalSettle( - bytes calldata settleData, - bytes calldata wrapperData - ) internal { + function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal virtual; + + function _internalSettle(bytes calldata settleData, bytes calldata wrapperData) internal { // the next settlement address to call will be the next word of the wrapper data address nextSettlement; assembly { @@ -85,7 +80,8 @@ abstract contract CowWrapper is CowSettlement { // Encode the settle call bytes memory fullCalldata; - (bool success, bytes memory returnData) = nextSettlement.call(abi.encodePacked(CowSettlement.settle.selector, settleData, wrapperData)); + (bool success, bytes memory returnData) = + nextSettlement.call(abi.encodePacked(CowSettlement.settle.selector, settleData, wrapperData)); //(bool success, bytes memory returnData) = nextSettlement.call(fullCalldata); if (!success) { @@ -101,14 +97,24 @@ abstract contract CowWrapper is CowSettlement { * This can be used to determine if there is additional data appended to msg.data. * @return end The calldata position in bytes of the end of settle() function calldata */ - function _settleCalldataLength( - GPv2Interaction.Data[][3] calldata interactions - ) internal pure returns (uint256 end) { + function _settleCalldataLength(GPv2Interaction.Data[][3] calldata interactions) + internal + pure + returns (uint256 end) + { // NOTE: technically this function could fail to return the correct length, if the data encoded in the ABI is provided indexed in an unusual order // however, doing a deeper check of the total data is very expensive and we are generally working with callers who provide data in a verifiably standardized format GPv2Interaction.Data[] calldata lastInteractions = interactions[2]; - assembly { - end := add(lastInteractions.offset, lastInteractions.length) + if (lastInteractions.length > 0) { + bytes calldata lastInteraction = lastInteractions[lastInteractions.length - 1].callData; + uint256 length = (lastInteraction.length + 31) / 32 * 32; + assembly { + end := add(lastInteraction.offset, length) + } + } else { + assembly { + end := add(lastInteractions.offset, lastInteractions.length) + } } } } diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index fd87b38..9b50f24 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -121,13 +121,9 @@ contract CowWrapperTest is Test, CowWrapper { MockAuthentication(address(0)).addSolver(address(wrapper1)); MockAuthentication(address(0)).addSolver(address(wrapper2)); MockAuthentication(address(0)).addSolver(address(wrapper3)); - } - function _wrap( - bytes calldata settleData, - bytes calldata wrapperData - ) internal override { + function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal override { // Record the wrap call WrapCall storage call_ = wrapCalls.push(); call_.settleData = settleData; @@ -139,18 +135,15 @@ contract CowWrapperTest is Test, CowWrapper { // These function needs to be exposed because the internal function expects calldata, so this is convienience to accomplish that function exposed_settleCalldataLength( - IERC20[] calldata tokens, - uint256[] calldata clearingPrices, - GPv2Trade.Data[] calldata trades, + IERC20[] calldata, + uint256[] calldata, + GPv2Trade.Data[] calldata, GPv2Interaction.Data[][3] calldata interactions ) external pure returns (uint256) { return _settleCalldataLength(interactions); } - function exposed_internalSettle( - bytes calldata settleData, - bytes calldata wrapperData - ) external { + function exposed_internalSettle(bytes calldata settleData, bytes calldata wrapperData) external { _internalSettle(settleData, wrapperData); } @@ -181,7 +174,8 @@ contract CowWrapperTest is Test, CowWrapper { GPv2Interaction.Data[][3] memory interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - bytes memory encoded = abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory encoded = + abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions); // Log the length difference uint256 calculated = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); @@ -326,7 +320,6 @@ contract CowWrapperTest is Test, CowWrapper { uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); - // Base: 452, Tokens: 96, Prices: 96, Trade1: 325, Trade2: 332, Int[0][0]: 132, Int[2][0]: 134 assertEq( length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length ); @@ -458,20 +451,21 @@ contract CowWrapperTest is Test, CowWrapper { bytes[] memory wrapperDatas = new bytes[](3); - (address target, bytes memory fullCalldata) = CowWrapperHelpers.encodeWrapperCall(wrappers, wrapperDatas, address(mockSettlement), settlement); + (address target, bytes memory fullCalldata) = + CowWrapperHelpers.encodeWrapperCall(wrappers, wrapperDatas, address(mockSettlement), settlement); // Call wrapper1 as the solver vm.prank(solver); - (bool success,) = address(wrapper1).call(fullCalldata); + (bool success,) = target.call(fullCalldata); assertTrue(success, "Chained wrapper call should succeed"); // Verify that mockSettlement was called assertEq(mockSettlement.getSettleCallCount(), 1, "MockSettlement should be called once"); // Verify the settlement received the correct parameters - //(uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); - //assertEq(tokenCount, 2, "Should have 2 tokens"); - //assertEq(priceCount, 2, "Should have 2 prices"); - //assertEq(tradeCount, 1, "Should have 1 trade"); + (uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); + assertEq(tokenCount, 2, "Should have 2 tokens"); + assertEq(priceCount, 2, "Should have 2 prices"); + assertEq(tradeCount, 1, "Should have 1 trade"); } } From a7c03fe116a1672b3de927b63db1dfffc5c19a0a Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 9 Oct 2025 14:32:14 +0900 Subject: [PATCH 03/43] change interface back to fresh call interface, clean up unneeded things --- src/vendor/CowWrapper.sol | 122 ++++++++------- test/CowWrapper.t.sol | 239 ++++------------------------- test/helpers/CowWrapperHelpers.sol | 18 ++- 3 files changed, 103 insertions(+), 276 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 873e1ec..96634d9 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -2,32 +2,63 @@ pragma solidity >=0.7.6 <0.9.0; pragma abicoder v2; -import {IERC20, GPv2Trade, GPv2Interaction, GPv2Authentication} from "cow/GPv2Settlement.sol"; - -import "forge-std/console.sol"; +/// @title Gnosis Protocol v2 Authentication Interface +/// @author Gnosis Developers +interface GPv2Authentication { + /// @dev determines whether the provided address is an authenticated solver. + /// @param prospectiveSolver the address of prospective solver. + /// @return true when prospectiveSolver is an authenticated solver, otherwise false. + function isSolver(address prospectiveSolver) external view returns (bool); +} interface CowSettlement { + struct GPv2TradeData { + uint256 sellTokenIndex; + uint256 buyTokenIndex; + address receiver; + uint256 sellAmount; + uint256 buyAmount; + uint32 validTo; + bytes32 appData; + uint256 feeAmount; + uint256 flags; + uint256 executedAmount; + bytes signature; + } + struct GPv2InteractionData { + address target; + uint256 value; + bytes callData; + } function settle( - IERC20[] calldata tokens, + address[] calldata tokens, uint256[] calldata clearingPrices, - GPv2Trade.Data[] calldata trades, - GPv2Interaction.Data[][3] calldata interactions + GPv2TradeData[] calldata trades, + GPv2InteractionData[][3] calldata interactions + ) external; +} + +interface ICowWrapper { + function wrappedSettle( + bytes calldata settleData, + bytes calldata wrapperData ) external; } /** - * @dev Interface defining required methods for wrappers of the GPv2Settlement contract for CoW orders + * @dev Base contract defining required methods for wrappers of the GPv2Settlement contract for CoW orders * A wrapper should: * * call the equivalent `settle` on the GPv2Settlement contract (0x9008D19f58AAbD9eD0D60971565AA8510560ab41) * * verify that the caller is authorized via the GPv2Authentication contract. * A wrapper may also execute, or otherwise put the blockchain in a state that needs to be established prior to settlement. * Additionally, it needs to be approved by the GPv2Authentication contract */ -abstract contract CowWrapper is CowSettlement { +abstract contract CowWrapper { event GasLeft(uint256); error NotASolver(address unauthorized); - error WrapperHasNoSettleTarget(uint256 settleDataLength, uint256 fullCalldataLength); + error WrapperHasNoSettleTarget(uint256 wrapperDataLength, uint256 requiredWrapperDataLength); + error InvalidSettleData(bytes invalidSettleData); GPv2Authentication public immutable AUTHENTICATOR; @@ -39,30 +70,22 @@ abstract contract CowWrapper is CowSettlement { /** * @dev Called to initiate a wrapped call against the settlement function. See GPv2Settlement.settle() for more information. */ - function settle( - IERC20[] calldata tokens, - uint256[] calldata clearingPrices, - GPv2Trade.Data[] calldata trades, - GPv2Interaction.Data[][3] calldata interactions + function wrappedSettle( + bytes calldata settleData, + bytes calldata wrapperData ) external { // Revert if not a valid solver if (!AUTHENTICATOR.isSolver(msg.sender)) { revert NotASolver(msg.sender); } - // Extract additional data appended after settle calldata - uint256 settleEnd = _settleCalldataLength(interactions); - // Require additional data for next settlement address - if (msg.data.length < settleEnd + 32) { - revert WrapperHasNoSettleTarget(settleEnd, msg.data.length); + if (wrapperData.length < 20) { + revert WrapperHasNoSettleTarget(wrapperData.length, 20); } - // Additional data exists after the settle parameters - bytes calldata additionalData = msg.data[settleEnd:]; - // the settle data will always be after the first 4 bytes (selector), up to the computed data end point - _wrap(msg.data[4:settleEnd], additionalData); + _wrap(settleData, wrapperData); } /** @@ -74,47 +97,30 @@ abstract contract CowWrapper is CowSettlement { // the next settlement address to call will be the next word of the wrapper data address nextSettlement; assembly { - nextSettlement := calldataload(wrapperData.offset) + nextSettlement := calldataload(sub(wrapperData.offset, 12)) } - wrapperData = wrapperData[32:]; + wrapperData = wrapperData[20:]; // Encode the settle call - bytes memory fullCalldata; - - (bool success, bytes memory returnData) = - nextSettlement.call(abi.encodePacked(CowSettlement.settle.selector, settleData, wrapperData)); - //(bool success, bytes memory returnData) = nextSettlement.call(fullCalldata); - if (!success) { - // Bubble up the revert reason - assembly { - revert(add(returnData, 0x20), mload(returnData)) + if (wrapperData.length == 0) { + // sanity: make sure we are about to call the `settle` function on the settlement contract + if (bytes4(settleData[:4]) != CowSettlement.settle.selector) { + revert InvalidSettleData(settleData); } - } - } - /** - * @dev Computes the length of the settle() calldata in bytes. - * This can be used to determine if there is additional data appended to msg.data. - * @return end The calldata position in bytes of the end of settle() function calldata - */ - function _settleCalldataLength(GPv2Interaction.Data[][3] calldata interactions) - internal - pure - returns (uint256 end) - { - // NOTE: technically this function could fail to return the correct length, if the data encoded in the ABI is provided indexed in an unusual order - // however, doing a deeper check of the total data is very expensive and we are generally working with callers who provide data in a verifiably standardized format - GPv2Interaction.Data[] calldata lastInteractions = interactions[2]; - if (lastInteractions.length > 0) { - bytes calldata lastInteraction = lastInteractions[lastInteractions.length - 1].callData; - uint256 length = (lastInteraction.length + 31) / 32 * 32; - assembly { - end := add(lastInteraction.offset, length) - } - } else { - assembly { - end := add(lastInteractions.offset, lastInteractions.length) + // we can now call the settlement contract with the settle data verbatim + (bool success, bytes memory returnData) = + nextSettlement.call(settleData); + + if (!success) { + // Bubble up the revert reason + assembly { + revert(add(returnData, 0x20), mload(returnData)) + } } } + else { + CowWrapper(nextSettlement).wrappedSettle(settleData, wrapperData); + } } } diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 9b50f24..7ed3d02 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapper, CowSettlement} from "../src/vendor/CowWrapper.sol"; -import {IERC20, GPv2Trade, GPv2Interaction, GPv2Authentication} from "cow/GPv2Settlement.sol"; +import {CowWrapper, CowSettlement, GPv2Authentication} from "../src/vendor/CowWrapper.sol"; +import {IERC20, GPv2Trade, GPv2Interaction} from "cow/GPv2Settlement.sol"; import {EmptyWrapper} from "./EmptyWrapper.sol"; import {CowWrapperHelpers} from "./helpers/CowWrapperHelpers.sol"; @@ -22,7 +22,7 @@ contract MockAuthentication { } } -contract MockSettlement is CowSettlement { +contract MockSettlement { struct SettleCall { IERC20[] tokens; uint256[] clearingPrices; @@ -37,7 +37,7 @@ contract MockSettlement is CowSettlement { uint256[] calldata clearingPrices, GPv2Trade.Data[] calldata trades, GPv2Interaction.Data[][3] calldata - ) external override { + ) external { SettleCall storage call_ = settleCalls.push(); call_.tokens = tokens; call_.clearingPrices = clearingPrices; @@ -133,198 +133,10 @@ contract CowWrapperTest is Test, CowWrapper { _internalSettle(settleData, wrapperData[skipWrappedData:]); } - // These function needs to be exposed because the internal function expects calldata, so this is convienience to accomplish that - function exposed_settleCalldataLength( - IERC20[] calldata, - uint256[] calldata, - GPv2Trade.Data[] calldata, - GPv2Interaction.Data[][3] calldata interactions - ) external pure returns (uint256) { - return _settleCalldataLength(interactions); - } - function exposed_internalSettle(bytes calldata settleData, bytes calldata wrapperData) external { _internalSettle(settleData, wrapperData); } - function test_debug_tradeEncoding() public view { - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = IERC20(address(0x1)); - tokens[1] = IERC20(address(0x2)); - - uint256[] memory clearingPrices = new uint256[](2); - clearingPrices[0] = 100; - clearingPrices[1] = 200; - - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1); - trades[0] = GPv2Trade.Data({ - sellTokenIndex: 0, - buyTokenIndex: 1, - receiver: address(0x123), - sellAmount: 1000, - buyAmount: 900, - validTo: uint32(block.timestamp + 1000), - appData: bytes32(0), - feeAmount: 10, - flags: 0, - executedAmount: 0, - signature: hex"1234567890" - }); - - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - - bytes memory encoded = - abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions); - - // Log the length difference - uint256 calculated = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); - uint256 actual = encoded.length; - - // The difference tells us what we're missing - // Actual (996) - Calculated (873) = 123 bytes - assertEq(calculated, actual, "Length mismatch"); - } - - function test_settleCalldataLength_EmptyArrays() public view { - IERC20[] memory tokens = new IERC20[](0); - uint256[] memory clearingPrices = new uint256[](0); - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - - uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); - - assertEq( - length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length - ); - } - - function test_settleCalldataLength_WithTokensAndPrices() public view { - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = IERC20(address(0x1)); - tokens[1] = IERC20(address(0x2)); - - uint256[] memory clearingPrices = new uint256[](2); - clearingPrices[0] = 100; - clearingPrices[1] = 200; - - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - - uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); - - assertEq( - length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length - ); - } - - function test_settleCalldataLength_WithOneTrade() public view { - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = IERC20(address(0x1)); - tokens[1] = IERC20(address(0x2)); - - uint256[] memory clearingPrices = new uint256[](2); - clearingPrices[0] = 100; - clearingPrices[1] = 200; - - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1); - trades[0] = GPv2Trade.Data({ - sellTokenIndex: 0, - buyTokenIndex: 1, - receiver: address(0x123), - sellAmount: 1000, - buyAmount: 900, - validTo: uint32(block.timestamp + 1000), - appData: bytes32(0), - feeAmount: 10, - flags: 0, - executedAmount: 0, - signature: hex"1234567890" - }); - - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - - uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); - - assertEq( - length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length - ); - } - - function test_settleCalldataLength_WithInteractions() public view { - IERC20[] memory tokens = new IERC20[](0); - uint256[] memory clearingPrices = new uint256[](0); - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - - GPv2Interaction.Data[][3] memory interactions; - interactions[0] = new GPv2Interaction.Data[](1); - interactions[0][0] = GPv2Interaction.Data({target: address(0x456), value: 0, callData: hex"aabbccdd"}); - interactions[1] = new GPv2Interaction.Data[](0); - interactions[2] = new GPv2Interaction.Data[](0); - - uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); - - assertEq( - length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length - ); - } - - function test_settleCalldataLength_ComplexCase() public view { - IERC20[] memory tokens = new IERC20[](3); - tokens[0] = IERC20(address(0x1)); - tokens[1] = IERC20(address(0x2)); - tokens[2] = IERC20(address(0x3)); - - uint256[] memory clearingPrices = new uint256[](3); - clearingPrices[0] = 100; - clearingPrices[1] = 200; - clearingPrices[2] = 300; - - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](2); - trades[0] = GPv2Trade.Data({ - sellTokenIndex: 0, - buyTokenIndex: 1, - receiver: address(0x123), - sellAmount: 1000, - buyAmount: 900, - validTo: uint32(block.timestamp + 1000), - appData: bytes32(uint256(1)), - feeAmount: 10, - flags: 0, - executedAmount: 0, - signature: hex"aabbccddee" - }); - trades[1] = GPv2Trade.Data({ - sellTokenIndex: 1, - buyTokenIndex: 2, - receiver: address(0x456), - sellAmount: 2000, - buyAmount: 1800, - validTo: uint32(block.timestamp + 2000), - appData: bytes32(uint256(2)), - feeAmount: 20, - flags: 1, - executedAmount: 0, - signature: hex"112233445566778899aabbcc" - }); - - GPv2Interaction.Data[][3] memory interactions; - interactions[0] = new GPv2Interaction.Data[](1); - interactions[0][0] = GPv2Interaction.Data({target: address(0x789), value: 100, callData: hex"00112233"}); - interactions[1] = new GPv2Interaction.Data[](0); - interactions[2] = new GPv2Interaction.Data[](1); - interactions[2][0] = GPv2Interaction.Data({target: address(0xabc), value: 0, callData: hex"deadbeefcafe"}); - - uint256 length = this.exposed_settleCalldataLength(tokens, clearingPrices, trades, interactions); - - assertEq( - length, abi.encodeWithSelector(this.settle.selector, tokens, clearingPrices, trades, interactions).length - ); - } - function test_wrap_ReceivesCorrectParameters() public { IERC20[] memory tokens = new IERC20[](1); tokens[0] = IERC20(address(0x1)); @@ -336,24 +148,22 @@ contract CowWrapperTest is Test, CowWrapper { GPv2Interaction.Data[][3] memory interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - bytes memory wrapperData = hex"deadbeef"; - + bytes memory customWrapperData = hex"deadbeef"; address nextSettlement = address(mockSettlement); - bytes memory additionalData = abi.encodePacked(wrapperData, abi.encode(nextSettlement)); - bytes memory settleCalldata = + + bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory fullCalldata = abi.encodePacked(settleCalldata, additionalData); + bytes memory wrapperData = abi.encodePacked(customWrapperData, nextSettlement); - skipWrappedData = wrapperData.length; + skipWrappedData = customWrapperData.length; vm.prank(solver); - (bool success,) = address(this).call(fullCalldata); + this.wrappedSettle(settleData, wrapperData); skipWrappedData = 0; - assertTrue(success); assertEq(wrapCalls.length, 1); assertGt(wrapCalls[0].settleData.length, 0); - assertEq(wrapCalls[0].wrapperData, wrapperData); + assertEq(wrapCalls[0].wrapperData, customWrapperData); } function test_internalSettle_CallsNextSettlement() public { @@ -369,13 +179,12 @@ contract CowWrapperTest is Test, CowWrapper { address nextSettlement = address(mockSettlement); - bytes memory settleCalldata = + bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory fullCalldata = abi.encodePacked(settleCalldata, abi.encode(nextSettlement)); + bytes memory wrapperData = abi.encodePacked(nextSettlement); vm.prank(solver); - (bool success,) = address(this).call(fullCalldata); - assertTrue(success); + this.wrappedSettle(settleData, wrapperData); assertEq(mockSettlement.getSettleCallCount(), 1); (uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); @@ -384,32 +193,40 @@ contract CowWrapperTest is Test, CowWrapper { assertEq(tradeCount, 0); } - function test_settle_RevertsWithWrapperHasNoSettleTarget() public { + function test_wrappedSettle_RevertsWithWrapperHasNoSettleTarget() public { IERC20[] memory tokens = new IERC20[](0); uint256[] memory clearingPrices = new uint256[](0); GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); GPv2Interaction.Data[][3] memory interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - // Should revert when called without any additional wrapper data + bytes memory settleData = + abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory wrapperData = hex""; // Empty wrapper data (less than 20 bytes) + + // Should revert when called without sufficient wrapper data vm.prank(solver); - vm.expectRevert(abi.encodeWithSelector(CowWrapper.WrapperHasNoSettleTarget.selector, 420, 420)); - this.settle(tokens, clearingPrices, trades, interactions); + vm.expectRevert(abi.encodeWithSelector(CowWrapper.WrapperHasNoSettleTarget.selector, 0, 20)); + this.wrappedSettle(settleData, wrapperData); } - function test_settle_RevertsWithNotASolver() public { + function test_wrappedSettle_RevertsWithNotASolver() public { IERC20[] memory tokens = new IERC20[](0); uint256[] memory clearingPrices = new uint256[](0); GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); GPv2Interaction.Data[][3] memory interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + bytes memory settleData = + abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory wrapperData = abi.encodePacked(address(mockSettlement)); // Packed encoding of settlement address + address notASolver = makeAddr("notASolver"); // Should revert when called by non-solver vm.prank(notASolver); vm.expectRevert(abi.encodeWithSelector(CowWrapper.NotASolver.selector, notASolver)); - this.settle(tokens, clearingPrices, trades, interactions); + this.wrappedSettle(settleData, wrapperData); } function test_integration_ThreeWrappersChained() public { diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol index 885203f..2bf43df 100644 --- a/test/helpers/CowWrapperHelpers.sol +++ b/test/helpers/CowWrapperHelpers.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8; import {IERC20, GPv2Trade, GPv2Interaction, GPv2Authentication} from "cow/GPv2Settlement.sol"; -import {CowSettlement} from "src/vendor/CowWrapper.sol"; +import {CowSettlement, ICowWrapper} from "src/vendor/CowWrapper.sol"; library CowWrapperHelpers { struct SettleCall { @@ -21,20 +21,24 @@ library CowWrapperHelpers { bytes[] calldata wrapperDatas, address cowSettlement, SettleCall calldata settlement - ) external returns (address target, bytes memory wrapperData) { + ) external returns (address target, bytes memory fullCalldata) { + // Build the wrapper data chain + bytes memory wrapperData; for (uint256 i = 0; i < wrappers.length; i++) { wrapperData = abi.encodePacked( wrapperData, wrapperDatas[i], - uint256(uint160(wrappers.length > i + 1 ? wrappers[i + 1] : cowSettlement)) + (wrappers.length > i + 1 ? wrappers[i + 1] : cowSettlement) ); } - // build the settle calldata and append the wrapper data directly to the end of it - bytes memory settleCalldata = + // Build the settle calldata + bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions); - wrapperData = abi.encodePacked(settleCalldata, wrapperData); - return (wrappers[0], wrapperData); + // Encode the wrappedSettle call + fullCalldata = abi.encodeWithSelector(ICowWrapper.wrappedSettle.selector, settleData, wrapperData); + + return (wrappers[0], fullCalldata); } } From 511f7aa7271bf45a3a72f04c2fedd4617518cd79 Mon Sep 17 00:00:00 2001 From: Kaze Date: Fri, 10 Oct 2025 01:52:01 +0900 Subject: [PATCH 04/43] add documentation --- src/vendor/CowWrapper.sol | 99 +++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 96634d9..68734ad 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -11,7 +11,11 @@ interface GPv2Authentication { function isSolver(address prospectiveSolver) external view returns (bool); } +/// @title CoW Settlement Interface +/// @notice Minimal interface for CoW Protocol's settlement contract +/// @dev Used for type-safe calls to the settlement contract's settle function interface CowSettlement { + /// @notice Trade data structure matching GPv2Settlement struct GPv2TradeData { uint256 sellTokenIndex; uint256 buyTokenIndex; @@ -25,11 +29,19 @@ interface CowSettlement { uint256 executedAmount; bytes signature; } + + /// @notice Interaction data structure for pre/intra/post-settlement hooks struct GPv2InteractionData { address target; uint256 value; bytes callData; } + + /// @notice Settles a batch of trades atomically + /// @param tokens Array of token addresses involved in the settlement + /// @param clearingPrices Array of clearing prices for each token + /// @param trades Array of trades to execute + /// @param interactions Array of three interaction arrays (pre, intra, post-settlement) function settle( address[] calldata tokens, uint256[] calldata clearingPrices, @@ -38,38 +50,62 @@ interface CowSettlement { ) external; } +/// @title CoW Wrapper Interface +/// @notice Interface for wrapper contracts that add custom logic around CoW settlements +/// @dev Wrappers can be chained together to compose multiple settlement operations interface ICowWrapper { + /// @notice Initiates a wrapped settlement call + /// @dev This is the entry point for wrapped settlements. The wrapper will execute custom logic + /// before calling the next wrapper or settlement contract in the chain. + /// @param settleData ABI-encoded call to CowSettlement.settle() + /// @param wrapperData Encoded chain of wrapper-specific data followed by addresses of next wrappers/settlement function wrappedSettle( bytes calldata settleData, bytes calldata wrapperData ) external; } -/** - * @dev Base contract defining required methods for wrappers of the GPv2Settlement contract for CoW orders - * A wrapper should: - * * call the equivalent `settle` on the GPv2Settlement contract (0x9008D19f58AAbD9eD0D60971565AA8510560ab41) - * * verify that the caller is authorized via the GPv2Authentication contract. - * A wrapper may also execute, or otherwise put the blockchain in a state that needs to be established prior to settlement. - * Additionally, it needs to be approved by the GPv2Authentication contract - */ +/// @title CoW Wrapper Base Contract +/// @notice Abstract base contract for creating wrapper contracts around CoW Protocol settlements +/// @dev A wrapper enables custom pre/post-settlement and context-setting logic and can be chained with other wrappers. +/// Wrappers must: +/// - Be approved by the GPv2Authentication contract +/// - Verify the caller is an authenticated solver +/// - Eventually call settle() on the approved GPv2Settlement contract +/// - Implement _wrap() for custom logic +/// - Implement parseWrapperData() for validation of implementation-specific wrapperData abstract contract CowWrapper { - event GasLeft(uint256); - + /// @notice Thrown when the caller is not an authenticated solver + /// @param unauthorized The address that attempted to call wrappedSettle error NotASolver(address unauthorized); + + /// @notice Thrown when wrapper data doesn't contain a settlement target address + /// @param wrapperDataLength The actual length of wrapper data provided + /// @param requiredWrapperDataLength The minimum required length (20 bytes for an address) error WrapperHasNoSettleTarget(uint256 wrapperDataLength, uint256 requiredWrapperDataLength); + + /// @notice Thrown when settle data doesn't contain the correct function selector + /// @param invalidSettleData The invalid settle data that was provided error InvalidSettleData(bytes invalidSettleData); + /// @notice The authentication contract used to verify solvers + /// @dev This is typically the GPv2AllowListAuthentication contract GPv2Authentication public immutable AUTHENTICATOR; + /// @notice Constructs a new CowWrapper + /// @param authenticator_ The GPv2Authentication contract to use for solver or upstream wrapper verification constructor(GPv2Authentication authenticator_) { // retrieve the authentication we are supposed to use from the settlement contract AUTHENTICATOR = authenticator_; } - /** - * @dev Called to initiate a wrapped call against the settlement function. See GPv2Settlement.settle() for more information. - */ + /// @notice Initiates a wrapped settlement call + /// @dev Entry point for solvers to execute wrapped settlements. Verifies the caller is a solver, + /// validates wrapper data, then delegates to _wrap() for custom logic. + /// @param settleData ABI-encoded call to CowSettlement.settle() containing trade data + /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. + /// Format: [wrapper-specific-data][next-address][remaining-wrapper-data] + /// Must be at least 20 bytes to contain the next settlement target address. function wrappedSettle( bytes calldata settleData, bytes calldata wrapperData @@ -79,47 +115,60 @@ abstract contract CowWrapper { revert NotASolver(msg.sender); } - // Require additional data for next settlement address + // Require wrapper data to contain at least the next settlement address (20 bytes) if (wrapperData.length < 20) { revert WrapperHasNoSettleTarget(wrapperData.length, 20); } - // the settle data will always be after the first 4 bytes (selector), up to the computed data end point + // Delegate to the wrapper's custom logic _wrap(settleData, wrapperData); } - /** - * @dev The logic for the wrapper. During this function, `_internalSettle` should be called. `wrapperData` may be consumed as required for the wrapper's particular requirements - */ + /// @notice Internal function containing the wrapper's custom logic + /// @dev Must be implemented by concrete wrapper contracts. Should execute custom logic + /// then eventually call _internalSettle() to continue the settlement chain. + /// @param settleData ABI-encoded call to CowSettlement.settle() + /// @param wrapperData The wrapper data, which may be parsed and consumed as needed function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal virtual; + /// @notice Continues the settlement chain by calling the next wrapper or settlement contract + /// @dev Extracts the next target address from wrapperData and either: + /// - Calls CowSettlement.settle() directly if no more wrappers remain, or + /// - Calls the next CowWrapper.wrappedSettle() to continue the chain + /// @param settleData ABI-encoded call to CowSettlement.settle() + /// @param wrapperData Remaining wrapper data starting with the next target address (20 bytes) function _internalSettle(bytes calldata settleData, bytes calldata wrapperData) internal { - // the next settlement address to call will be the next word of the wrapper data + // Extract the next settlement address from the first 20 bytes of wrapperData + // Assembly is used to efficiently read the address from calldata address nextSettlement; assembly { + // Load 32 bytes starting 12 bytes before wrapperData offset to get the address + // (addresses are 20 bytes, right-padded in 32-byte words) nextSettlement := calldataload(sub(wrapperData.offset, 12)) } + + // Skip past the address we just read wrapperData = wrapperData[20:]; - // Encode the settle call if (wrapperData.length == 0) { - // sanity: make sure we are about to call the `settle` function on the settlement contract + // No more wrapper data - we're calling the final settlement contract + // Verify the settle data has the correct function selector if (bytes4(settleData[:4]) != CowSettlement.settle.selector) { revert InvalidSettleData(settleData); } - // we can now call the settlement contract with the settle data verbatim - (bool success, bytes memory returnData) = - nextSettlement.call(settleData); + // Call the settlement contract directly with the settle data + (bool success, bytes memory returnData) = nextSettlement.call(settleData); if (!success) { - // Bubble up the revert reason + // Bubble up the revert reason from the settlement contract assembly { revert(add(returnData, 0x20), mload(returnData)) } } } else { + // More wrapper data remains - call the next wrapper in the chain CowWrapper(nextSettlement).wrappedSettle(settleData, wrapperData); } } From e82bc8a0c1e9250b219c5e92f5aea36142fe17e6 Mon Sep 17 00:00:00 2001 From: Kaze Date: Fri, 10 Oct 2025 01:54:47 +0900 Subject: [PATCH 05/43] more cleanups --- src/vendor/CowWrapper.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 68734ad..ccd8e98 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.7.6 <0.9.0; pragma abicoder v2; @@ -95,7 +95,6 @@ abstract contract CowWrapper { /// @notice Constructs a new CowWrapper /// @param authenticator_ The GPv2Authentication contract to use for solver or upstream wrapper verification constructor(GPv2Authentication authenticator_) { - // retrieve the authentication we are supposed to use from the settlement contract AUTHENTICATOR = authenticator_; } From edce0df5b0d6bfce57728878208b1103fb9dfbb1 Mon Sep 17 00:00:00 2001 From: Kaze Date: Sat, 11 Oct 2025 13:55:46 +0900 Subject: [PATCH 06/43] Update src/vendor/CowWrapper.sol Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- src/vendor/CowWrapper.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index ccd8e98..567ef46 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -3,8 +3,8 @@ pragma solidity >=0.7.6 <0.9.0; pragma abicoder v2; /// @title Gnosis Protocol v2 Authentication Interface -/// @author Gnosis Developers -interface GPv2Authentication { +/// @author CoW DAO developers +interface CowProtocolAuthentication { /// @dev determines whether the provided address is an authenticated solver. /// @param prospectiveSolver the address of prospective solver. /// @return true when prospectiveSolver is an authenticated solver, otherwise false. From 594613ea6e9fe8377419d8143176e73d55832a1e Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 13 Oct 2025 11:30:53 +0900 Subject: [PATCH 07/43] fix wrapper read next settlement doesn't need assembly --- src/vendor/CowWrapper.sol | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index ccd8e98..dd33198 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -139,12 +139,7 @@ abstract contract CowWrapper { function _internalSettle(bytes calldata settleData, bytes calldata wrapperData) internal { // Extract the next settlement address from the first 20 bytes of wrapperData // Assembly is used to efficiently read the address from calldata - address nextSettlement; - assembly { - // Load 32 bytes starting 12 bytes before wrapperData offset to get the address - // (addresses are 20 bytes, right-padded in 32-byte words) - nextSettlement := calldataload(sub(wrapperData.offset, 12)) - } + address nextSettlement = address(bytes20(wrapperData[:20])); // Skip past the address we just read wrapperData = wrapperData[20:]; From 8803d2ae6dccfa314aebe404c3ad32a0e16f1e0d Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 13 Oct 2025 11:35:32 +0900 Subject: [PATCH 08/43] switch to requires instead of if->reverts as they are supported now --- src/vendor/CowWrapper.sol | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 4434a86..1310ffe 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -110,14 +110,10 @@ abstract contract CowWrapper { bytes calldata wrapperData ) external { // Revert if not a valid solver - if (!AUTHENTICATOR.isSolver(msg.sender)) { - revert NotASolver(msg.sender); - } + require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); // Require wrapper data to contain at least the next settlement address (20 bytes) - if (wrapperData.length < 20) { - revert WrapperHasNoSettleTarget(wrapperData.length, 20); - } + require(wrapperData >= 20, WrapperHasNoSettleTarget(wrapperData.length, 20)); // Delegate to the wrapper's custom logic _wrap(settleData, wrapperData); @@ -147,9 +143,7 @@ abstract contract CowWrapper { if (wrapperData.length == 0) { // No more wrapper data - we're calling the final settlement contract // Verify the settle data has the correct function selector - if (bytes4(settleData[:4]) != CowSettlement.settle.selector) { - revert InvalidSettleData(settleData); - } + require(bytes4(settleData[:4]) == CowSettlement.settle.selector, InvalidSettleData(settleData)); // Call the settlement contract directly with the settle data (bool success, bytes memory returnData) = nextSettlement.call(settleData); From b1640623f6492c262aca3df560c053bb72f0bb30 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 13 Oct 2025 12:39:47 +0900 Subject: [PATCH 09/43] fix test failures and clean up docs --- src/vendor/CowWrapper.sol | 32 +++++++++++++++--------------- test/CowWrapper.t.sol | 22 ++++++++++---------- test/EmptyWrapper.sol | 2 +- test/helpers/CowWrapperHelpers.sol | 10 ++++------ 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 1310ffe..795a58f 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -2,21 +2,21 @@ pragma solidity >=0.7.6 <0.9.0; pragma abicoder v2; -/// @title Gnosis Protocol v2 Authentication Interface +/// @title CoW Protocol v2 Authentication Interface /// @author CoW DAO developers -interface CowProtocolAuthentication { +interface CowAuthentication { /// @dev determines whether the provided address is an authenticated solver. /// @param prospectiveSolver the address of prospective solver. /// @return true when prospectiveSolver is an authenticated solver, otherwise false. function isSolver(address prospectiveSolver) external view returns (bool); } -/// @title CoW Settlement Interface +/// @title CoW Protocol Settlement Interface /// @notice Minimal interface for CoW Protocol's settlement contract /// @dev Used for type-safe calls to the settlement contract's settle function interface CowSettlement { /// @notice Trade data structure matching GPv2Settlement - struct GPv2TradeData { + struct CowTradeData { uint256 sellTokenIndex; uint256 buyTokenIndex; address receiver; @@ -31,7 +31,7 @@ interface CowSettlement { } /// @notice Interaction data structure for pre/intra/post-settlement hooks - struct GPv2InteractionData { + struct CowInteractionData { address target; uint256 value; bytes callData; @@ -45,12 +45,12 @@ interface CowSettlement { function settle( address[] calldata tokens, uint256[] calldata clearingPrices, - GPv2TradeData[] calldata trades, - GPv2InteractionData[][3] calldata interactions + CowTradeData[] calldata trades, + CowInteractionData[][3] calldata interactions ) external; } -/// @title CoW Wrapper Interface +/// @title CoW Protocol Wrapper Interface /// @notice Interface for wrapper contracts that add custom logic around CoW settlements /// @dev Wrappers can be chained together to compose multiple settlement operations interface ICowWrapper { @@ -65,13 +65,13 @@ interface ICowWrapper { ) external; } -/// @title CoW Wrapper Base Contract +/// @title CoW Protocol Wrapper Base Contract /// @notice Abstract base contract for creating wrapper contracts around CoW Protocol settlements /// @dev A wrapper enables custom pre/post-settlement and context-setting logic and can be chained with other wrappers. /// Wrappers must: -/// - Be approved by the GPv2Authentication contract +/// - Be approved by the CowAuthentication contract /// - Verify the caller is an authenticated solver -/// - Eventually call settle() on the approved GPv2Settlement contract +/// - Eventually call settle() on the approved CowSettlement contract /// - Implement _wrap() for custom logic /// - Implement parseWrapperData() for validation of implementation-specific wrapperData abstract contract CowWrapper { @@ -89,12 +89,12 @@ abstract contract CowWrapper { error InvalidSettleData(bytes invalidSettleData); /// @notice The authentication contract used to verify solvers - /// @dev This is typically the GPv2AllowListAuthentication contract - GPv2Authentication public immutable AUTHENTICATOR; + /// @dev This is typically the CowAllowListAuthentication contract + CowAuthentication public immutable AUTHENTICATOR; /// @notice Constructs a new CowWrapper - /// @param authenticator_ The GPv2Authentication contract to use for solver or upstream wrapper verification - constructor(GPv2Authentication authenticator_) { + /// @param authenticator_ The CowAuthentication contract to use for solver or upstream wrapper verification + constructor(CowAuthentication authenticator_) { AUTHENTICATOR = authenticator_; } @@ -113,7 +113,7 @@ abstract contract CowWrapper { require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); // Require wrapper data to contain at least the next settlement address (20 bytes) - require(wrapperData >= 20, WrapperHasNoSettleTarget(wrapperData.length, 20)); + require(wrapperData.length >= 20, WrapperHasNoSettleTarget(wrapperData.length, 20)); // Delegate to the wrapper's custom logic _wrap(settleData, wrapperData); diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 7ed3d02..ab7b685 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapper, CowSettlement, GPv2Authentication} from "../src/vendor/CowWrapper.sol"; +import {CowWrapper, CowSettlement, CowAuthentication} from "../src/vendor/CowWrapper.sol"; import {IERC20, GPv2Trade, GPv2Interaction} from "cow/GPv2Settlement.sol"; import {EmptyWrapper} from "./EmptyWrapper.sol"; @@ -97,7 +97,7 @@ contract CowWrapperTest is Test, CowWrapper { uint256 private skipWrappedData; - constructor() CowWrapper(GPv2Authentication(address(0))) { + constructor() CowWrapper(CowAuthentication(address(0))) { // Constructor will be called in setUp with proper authenticator } @@ -113,9 +113,9 @@ contract CowWrapperTest is Test, CowWrapper { mockSettlement = new MockSettlement(); // Create three EmptyWrapper instances - wrapper1 = new EmptyWrapper(GPv2Authentication(address(0))); - wrapper2 = new EmptyWrapper(GPv2Authentication(address(0))); - wrapper3 = new EmptyWrapper(GPv2Authentication(address(0))); + wrapper1 = new EmptyWrapper(CowAuthentication(address(0))); + wrapper2 = new EmptyWrapper(CowAuthentication(address(0))); + wrapper3 = new EmptyWrapper(CowAuthentication(address(0))); // Add all wrappers as solvers MockAuthentication(address(0)).addSolver(address(wrapper1)); @@ -232,16 +232,16 @@ contract CowWrapperTest is Test, CowWrapper { function test_integration_ThreeWrappersChained() public { CowWrapperHelpers.SettleCall memory settlement; - settlement.tokens = new IERC20[](2); - settlement.tokens[0] = IERC20(address(0x1)); - settlement.tokens[1] = IERC20(address(0x2)); + settlement.tokens = new address[](2); + settlement.tokens[0] = address(0x1); + settlement.tokens[1] = address(0x2); settlement.clearingPrices = new uint256[](2); settlement.clearingPrices[0] = 100; settlement.clearingPrices[1] = 200; - settlement.trades = new GPv2Trade.Data[](1); - settlement.trades[0] = GPv2Trade.Data({ + settlement.trades = new CowSettlement.CowTradeData[](1); + settlement.trades[0] = CowSettlement.CowTradeData({ sellTokenIndex: 0, buyTokenIndex: 1, receiver: address(0x123), @@ -256,7 +256,7 @@ contract CowWrapperTest is Test, CowWrapper { }); settlement.interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + [new CowSettlement.CowInteractionData[](0), new CowSettlement.CowInteractionData[](0), new CowSettlement.CowInteractionData[](0)]; // Build the chained wrapper data: // solver -> wrapper1 -> wrapper2 -> wrapper3 -> mockSettlement diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index 2be39f5..d51d95a 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -5,7 +5,7 @@ pragma abicoder v2; import "../src/vendor/CowWrapper.sol"; contract EmptyWrapper is CowWrapper { - constructor(GPv2Authentication authenticator_) CowWrapper(authenticator_) {} + constructor(CowAuthentication authenticator_) CowWrapper(authenticator_) {} function _wrap( bytes calldata settleData, diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol index 2bf43df..75c7803 100644 --- a/test/helpers/CowWrapperHelpers.sol +++ b/test/helpers/CowWrapperHelpers.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8; -import {IERC20, GPv2Trade, GPv2Interaction, GPv2Authentication} from "cow/GPv2Settlement.sol"; - -import {CowSettlement, ICowWrapper} from "src/vendor/CowWrapper.sol"; +import {CowSettlement, ICowWrapper, CowAuthentication} from "src/vendor/CowWrapper.sol"; library CowWrapperHelpers { struct SettleCall { - IERC20[] tokens; + address[] tokens; uint256[] clearingPrices; - GPv2Trade.Data[] trades; - GPv2Interaction.Data[][3] interactions; + CowSettlement.CowTradeData[] trades; + CowSettlement.CowInteractionData[][3] interactions; } /** From 19b8010a1befc6d410a66a950fb2e9bcac141a0b Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 13 Oct 2025 12:44:42 +0900 Subject: [PATCH 10/43] remove wrong comment --- src/vendor/CowWrapper.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 795a58f..3dd9f8d 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -134,7 +134,6 @@ abstract contract CowWrapper { /// @param wrapperData Remaining wrapper data starting with the next target address (20 bytes) function _internalSettle(bytes calldata settleData, bytes calldata wrapperData) internal { // Extract the next settlement address from the first 20 bytes of wrapperData - // Assembly is used to efficiently read the address from calldata address nextSettlement = address(bytes20(wrapperData[:20])); // Skip past the address we just read From 1e37149493e02c6157d47b9044c884695c13ef9d Mon Sep 17 00:00:00 2001 From: Kaze Date: Fri, 17 Oct 2025 11:42:49 +0900 Subject: [PATCH 11/43] actually implement the ICowWrapper interface --- src/vendor/CowWrapper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 3dd9f8d..cc087a6 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -74,7 +74,7 @@ interface ICowWrapper { /// - Eventually call settle() on the approved CowSettlement contract /// - Implement _wrap() for custom logic /// - Implement parseWrapperData() for validation of implementation-specific wrapperData -abstract contract CowWrapper { +abstract contract CowWrapper is ICowWrapper { /// @notice Thrown when the caller is not an authenticated solver /// @param unauthorized The address that attempted to call wrappedSettle error NotASolver(address unauthorized); From e583a6e333469fa1b27d24eae35f42ba7effeedb Mon Sep 17 00:00:00 2001 From: Kaze Date: Fri, 17 Oct 2025 14:47:47 +0900 Subject: [PATCH 12/43] Update CowWrapper to use static settlement contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed constructor to accept CowSettlement instead of CowAuthentication - Removed WrapperHasNoSettleTarget error and length check - Settlement contract is now stored as immutable SETTLEMENT - Authenticator is derived from SETTLEMENT.authenticator() - Updated _internalSettle to call static SETTLEMENT when wrapperData is empty - Updated tests to pass settlement contract to constructors - Removed settlement address from wrapperData in test helpers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/vendor/CowWrapper.sol | 52 +++++------ test/CowWrapper.t.sol | 138 +++++++++++++++-------------- test/EmptyWrapper.sol | 2 +- test/helpers/CowWrapperHelpers.sol | 12 ++- 4 files changed, 105 insertions(+), 99 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index cc087a6..f6be520 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -37,6 +37,9 @@ interface CowSettlement { bytes callData; } + /// @notice Returns the authentication contract used by the settlement contract. + function authenticator() external returns (CowAuthentication); + /// @notice Settles a batch of trades atomically /// @param tokens Array of token addresses involved in the settlement /// @param clearingPrices Array of clearing prices for each token @@ -59,10 +62,7 @@ interface ICowWrapper { /// before calling the next wrapper or settlement contract in the chain. /// @param settleData ABI-encoded call to CowSettlement.settle() /// @param wrapperData Encoded chain of wrapper-specific data followed by addresses of next wrappers/settlement - function wrappedSettle( - bytes calldata settleData, - bytes calldata wrapperData - ) external; + function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external; } /// @title CoW Protocol Wrapper Base Contract @@ -79,23 +79,22 @@ abstract contract CowWrapper is ICowWrapper { /// @param unauthorized The address that attempted to call wrappedSettle error NotASolver(address unauthorized); - /// @notice Thrown when wrapper data doesn't contain a settlement target address - /// @param wrapperDataLength The actual length of wrapper data provided - /// @param requiredWrapperDataLength The minimum required length (20 bytes for an address) - error WrapperHasNoSettleTarget(uint256 wrapperDataLength, uint256 requiredWrapperDataLength); - /// @notice Thrown when settle data doesn't contain the correct function selector /// @param invalidSettleData The invalid settle data that was provided error InvalidSettleData(bytes invalidSettleData); + /// @notice The settlement contract + CowSettlement public immutable SETTLEMENT; + /// @notice The authentication contract used to verify solvers - /// @dev This is typically the CowAllowListAuthentication contract + /// @dev This is derived from `SETTLEMENT.authenticator()`. CowAuthentication public immutable AUTHENTICATOR; /// @notice Constructs a new CowWrapper - /// @param authenticator_ The CowAuthentication contract to use for solver or upstream wrapper verification - constructor(CowAuthentication authenticator_) { - AUTHENTICATOR = authenticator_; + /// @param settlement_ The CowSettlement contract to use at the end of the wrapper chain. Also used for wrapper authentication. + constructor(CowSettlement settlement_) { + SETTLEMENT = settlement_; + AUTHENTICATOR = settlement_.authenticator(); } /// @notice Initiates a wrapped settlement call @@ -105,16 +104,10 @@ abstract contract CowWrapper is ICowWrapper { /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. /// Format: [wrapper-specific-data][next-address][remaining-wrapper-data] /// Must be at least 20 bytes to contain the next settlement target address. - function wrappedSettle( - bytes calldata settleData, - bytes calldata wrapperData - ) external { + function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external { // Revert if not a valid solver require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); - // Require wrapper data to contain at least the next settlement address (20 bytes) - require(wrapperData.length >= 20, WrapperHasNoSettleTarget(wrapperData.length, 20)); - // Delegate to the wrapper's custom logic _wrap(settleData, wrapperData); } @@ -133,19 +126,13 @@ abstract contract CowWrapper is ICowWrapper { /// @param settleData ABI-encoded call to CowSettlement.settle() /// @param wrapperData Remaining wrapper data starting with the next target address (20 bytes) function _internalSettle(bytes calldata settleData, bytes calldata wrapperData) internal { - // Extract the next settlement address from the first 20 bytes of wrapperData - address nextSettlement = address(bytes20(wrapperData[:20])); - - // Skip past the address we just read - wrapperData = wrapperData[20:]; - if (wrapperData.length == 0) { // No more wrapper data - we're calling the final settlement contract // Verify the settle data has the correct function selector require(bytes4(settleData[:4]) == CowSettlement.settle.selector, InvalidSettleData(settleData)); // Call the settlement contract directly with the settle data - (bool success, bytes memory returnData) = nextSettlement.call(settleData); + (bool success, bytes memory returnData) = address(SETTLEMENT).call(settleData); if (!success) { // Bubble up the revert reason from the settlement contract @@ -153,10 +140,15 @@ abstract contract CowWrapper is ICowWrapper { revert(add(returnData, 0x20), mload(returnData)) } } - } - else { + } else { + // Extract the next wrapper address from the first 20 bytes of wrapperData + address nextWrapper = address(bytes20(wrapperData[:20])); + + // Skip past the address we just read + wrapperData = wrapperData[20:]; + // More wrapper data remains - call the next wrapper in the chain - CowWrapper(nextSettlement).wrappedSettle(settleData, wrapperData); + CowWrapper(nextWrapper).wrappedSettle(settleData, wrapperData); } } } diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index ab7b685..14bf2f6 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -31,6 +31,15 @@ contract MockSettlement { } SettleCall[] public settleCalls; + CowAuthentication private immutable _authenticator; + + constructor(CowAuthentication authenticator_) { + _authenticator = authenticator_; + } + + function authenticator() external view returns (CowAuthentication) { + return _authenticator; + } function settle( IERC20[] calldata tokens, @@ -78,15 +87,8 @@ contract MockSettlement { } } -contract CowWrapperTest is Test, CowWrapper { - MockAuthentication public authenticator; - MockSettlement public mockSettlement; - address public solver; - - EmptyWrapper private wrapper1; - EmptyWrapper private wrapper2; - EmptyWrapper private wrapper3; - +// Test wrapper that exposes internal functions +contract TestWrapper is CowWrapper { // Track _wrap calls struct WrapCall { bytes settleData; @@ -95,33 +97,9 @@ contract CowWrapperTest is Test, CowWrapper { WrapCall[] public wrapCalls; - uint256 private skipWrappedData; - - constructor() CowWrapper(CowAuthentication(address(0))) { - // Constructor will be called in setUp with proper authenticator - } - - function setUp() public { - // Deploy MockAuthentication and etch it to address(0) so the immutable AUTHENTICATOR works - authenticator = new MockAuthentication(); - vm.etch(address(0), address(authenticator).code); - - solver = makeAddr("solver"); - // Add solver via address(0) which now has the MockAuthentication code - MockAuthentication(address(0)).addSolver(solver); + uint256 public skipWrappedData; - mockSettlement = new MockSettlement(); - - // Create three EmptyWrapper instances - wrapper1 = new EmptyWrapper(CowAuthentication(address(0))); - wrapper2 = new EmptyWrapper(CowAuthentication(address(0))); - wrapper3 = new EmptyWrapper(CowAuthentication(address(0))); - - // Add all wrappers as solvers - MockAuthentication(address(0)).addSolver(address(wrapper1)); - MockAuthentication(address(0)).addSolver(address(wrapper2)); - MockAuthentication(address(0)).addSolver(address(wrapper3)); - } + constructor(CowSettlement settlement_) CowWrapper(settlement_) {} function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal override { // Record the wrap call @@ -137,6 +115,51 @@ contract CowWrapperTest is Test, CowWrapper { _internalSettle(settleData, wrapperData); } + function getWrapCallCount() external view returns (uint256) { + return wrapCalls.length; + } + + function getWrapCall(uint256 index) external view returns (bytes memory settleData, bytes memory wrapperData) { + return (wrapCalls[index].settleData, wrapCalls[index].wrapperData); + } + + function setSkipWrappedData(uint256 value) external { + skipWrappedData = value; + } +} + +contract CowWrapperTest is Test { + MockAuthentication public authenticator; + MockSettlement public mockSettlement; + address public solver; + + TestWrapper public testWrapper; + EmptyWrapper private wrapper1; + EmptyWrapper private wrapper2; + EmptyWrapper private wrapper3; + + function setUp() public { + // Deploy mock contracts + authenticator = new MockAuthentication(); + mockSettlement = new MockSettlement(CowAuthentication(address(authenticator))); + + solver = makeAddr("solver"); + // Add solver to the authenticator + authenticator.addSolver(solver); + + // Create test wrapper and three EmptyWrapper instances with the settlement contract + testWrapper = new TestWrapper(CowSettlement(address(mockSettlement))); + wrapper1 = new EmptyWrapper(CowSettlement(address(mockSettlement))); + wrapper2 = new EmptyWrapper(CowSettlement(address(mockSettlement))); + wrapper3 = new EmptyWrapper(CowSettlement(address(mockSettlement))); + + // Add all wrappers as solvers + authenticator.addSolver(address(testWrapper)); + authenticator.addSolver(address(wrapper1)); + authenticator.addSolver(address(wrapper2)); + authenticator.addSolver(address(wrapper3)); + } + function test_wrap_ReceivesCorrectParameters() public { IERC20[] memory tokens = new IERC20[](1); tokens[0] = IERC20(address(0x1)); @@ -149,21 +172,22 @@ contract CowWrapperTest is Test, CowWrapper { [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; bytes memory customWrapperData = hex"deadbeef"; - address nextSettlement = address(mockSettlement); bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory wrapperData = abi.encodePacked(customWrapperData, nextSettlement); + // wrapperData is just custom data - no settlement address needed + bytes memory wrapperData = customWrapperData; - skipWrappedData = customWrapperData.length; + testWrapper.setSkipWrappedData(customWrapperData.length); vm.prank(solver); - this.wrappedSettle(settleData, wrapperData); - skipWrappedData = 0; + testWrapper.wrappedSettle(settleData, wrapperData); + testWrapper.setSkipWrappedData(0); - assertEq(wrapCalls.length, 1); - assertGt(wrapCalls[0].settleData.length, 0); - assertEq(wrapCalls[0].wrapperData, customWrapperData); + assertEq(testWrapper.getWrapCallCount(), 1); + (bytes memory recordedSettleData, bytes memory recordedWrapperData) = testWrapper.getWrapCall(0); + assertGt(recordedSettleData.length, 0); + assertEq(recordedWrapperData, customWrapperData); } function test_internalSettle_CallsNextSettlement() public { @@ -177,14 +201,13 @@ contract CowWrapperTest is Test, CowWrapper { GPv2Interaction.Data[][3] memory interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - address nextSettlement = address(mockSettlement); - bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory wrapperData = abi.encodePacked(nextSettlement); + // Empty wrapperData means call the static SETTLEMENT contract + bytes memory wrapperData = hex""; vm.prank(solver); - this.wrappedSettle(settleData, wrapperData); + testWrapper.wrappedSettle(settleData, wrapperData); assertEq(mockSettlement.getSettleCallCount(), 1); (uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); @@ -193,23 +216,6 @@ contract CowWrapperTest is Test, CowWrapper { assertEq(tradeCount, 0); } - function test_wrappedSettle_RevertsWithWrapperHasNoSettleTarget() public { - IERC20[] memory tokens = new IERC20[](0); - uint256[] memory clearingPrices = new uint256[](0); - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - - bytes memory settleData = - abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory wrapperData = hex""; // Empty wrapper data (less than 20 bytes) - - // Should revert when called without sufficient wrapper data - vm.prank(solver); - vm.expectRevert(abi.encodeWithSelector(CowWrapper.WrapperHasNoSettleTarget.selector, 0, 20)); - this.wrappedSettle(settleData, wrapperData); - } - function test_wrappedSettle_RevertsWithNotASolver() public { IERC20[] memory tokens = new IERC20[](0); uint256[] memory clearingPrices = new uint256[](0); @@ -219,14 +225,14 @@ contract CowWrapperTest is Test, CowWrapper { bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory wrapperData = abi.encodePacked(address(mockSettlement)); // Packed encoding of settlement address + bytes memory wrapperData = hex""; address notASolver = makeAddr("notASolver"); // Should revert when called by non-solver vm.prank(notASolver); vm.expectRevert(abi.encodeWithSelector(CowWrapper.NotASolver.selector, notASolver)); - this.wrappedSettle(settleData, wrapperData); + testWrapper.wrappedSettle(settleData, wrapperData); } function test_integration_ThreeWrappersChained() public { diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index d51d95a..ef599b4 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -5,7 +5,7 @@ pragma abicoder v2; import "../src/vendor/CowWrapper.sol"; contract EmptyWrapper is CowWrapper { - constructor(CowAuthentication authenticator_) CowWrapper(authenticator_) {} + constructor(CowSettlement settlement_) CowWrapper(settlement_) {} function _wrap( bytes calldata settleData, diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol index 75c7803..3971deb 100644 --- a/test/helpers/CowWrapperHelpers.sol +++ b/test/helpers/CowWrapperHelpers.sol @@ -13,6 +13,10 @@ library CowWrapperHelpers { /** * @dev This function is intended for testing purposes and is not memory efficient. + * @param wrappers Array of wrapper addresses to chain together + * @param wrapperDatas Array of wrapper-specific data for each wrapper + * @param cowSettlement The settlement contract address (unused, kept for backwards compatibility) + * @param settlement The settlement call parameters */ function encodeWrapperCall( address[] calldata wrappers, @@ -25,9 +29,13 @@ library CowWrapperHelpers { for (uint256 i = 0; i < wrappers.length; i++) { wrapperData = abi.encodePacked( wrapperData, - wrapperDatas[i], - (wrappers.length > i + 1 ? wrappers[i + 1] : cowSettlement) + wrapperDatas[i] ); + // Include the next wrapper address if there is one + if (wrappers.length > i + 1) { + wrapperData = abi.encodePacked(wrapperData, wrappers[i + 1]); + } + // For the last wrapper, don't add anything - the static SETTLEMENT will be called automatically } // Build the settle calldata From 323e7ce425c49d5adef5748aebc4c928e3f5da8c Mon Sep 17 00:00:00 2001 From: Kaze Date: Fri, 17 Oct 2025 16:22:24 +0900 Subject: [PATCH 13/43] add required name field useful for display in informational UIs --- src/vendor/CowWrapper.sol | 3 +++ test/CowWrapper.t.sol | 3 +++ test/EmptyWrapper.sol | 2 ++ 3 files changed, 8 insertions(+) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index f6be520..cc71ef4 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -57,6 +57,9 @@ interface CowSettlement { /// @notice Interface for wrapper contracts that add custom logic around CoW settlements /// @dev Wrappers can be chained together to compose multiple settlement operations interface ICowWrapper { + /// @notice A human readable label for this wrapper. Used for display in explorer/analysis UIs + function name() external view returns (string memory); + /// @notice Initiates a wrapped settlement call /// @dev This is the entry point for wrapped settlements. The wrapper will execute custom logic /// before calling the next wrapper or settlement contract in the chain. diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 14bf2f6..b0ad240 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -89,6 +89,9 @@ contract MockSettlement { // Test wrapper that exposes internal functions contract TestWrapper is CowWrapper { + + string constant public name = "Test Wrapper"; + // Track _wrap calls struct WrapCall { bytes settleData; diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index ef599b4..b29798e 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -5,6 +5,8 @@ pragma abicoder v2; import "../src/vendor/CowWrapper.sol"; contract EmptyWrapper is CowWrapper { + string constant public name = "Empty Wrapper"; + constructor(CowSettlement settlement_) CowWrapper(settlement_) {} function _wrap( From 00ff9ba9f5a78fcf45bd51d657b7b34ff01af3b6 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 20 Oct 2025 19:36:30 +0900 Subject: [PATCH 14/43] add memory safe annotation --- src/vendor/CowWrapper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index cc71ef4..3a5d754 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -139,7 +139,7 @@ abstract contract CowWrapper is ICowWrapper { if (!success) { // Bubble up the revert reason from the settlement contract - assembly { + assembly ("memory-safe") { revert(add(returnData, 0x20), mload(returnData)) } } From cdf82fd9c4c676165f2bc735764f4731e844c279 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 20 Oct 2025 19:43:55 +0900 Subject: [PATCH 15/43] add CLAUDE.md --- CLAUDE.md | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..940a754 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,155 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This repository contains **Euler-CoW Protocol integration contracts** that enable leveraged position management (opening/closing) through CoW Protocol settlements combined with Ethereum Vault Connector (EVC) operations. The contracts act as "wrappers" that coordinate complex multi-step DeFi operations atomically. + +### Core Architecture + +**Wrapper Pattern**: The codebase uses a chaining wrapper pattern where solvers can execute wrapped settlements that perform custom logic before/during/after CoW Protocol settlements. + +- `CowWrapper.sol`: Base abstract contract providing the wrapper framework + - Validates callers are authenticated solvers + - Implements `wrappedSettle()` entry point + - Provides `_internalSettle()` for continuing the settlement chain + - Wrappers can be chained: Wrapper1 → Wrapper2 → Settlement + +- `CowWrapperHelpers.sol`: Helper utilities for wrapper data parsing and validation + +**Specialized Wrappers**: Two production wrappers implement specific EVC + CoW Protocol workflows: + +1. **`CowEvcOpenPositionWrapper.sol`**: Opens leveraged positions + - Enables collateral vault + - Enables controller (borrow vault) + - Deposits collateral + - Borrows assets + - Executes CoW settlement to swap borrowed assets → collateral + - All operations are atomic within EVC batch + +2. **`CowEvcClosePositionWrapper.sol`**: Closes leveraged positions + - Executes CoW settlement to swap collateral → repayment assets + - Repays debt to borrow vault + - Returns excess assets to user + - Disables collateral if full repayment + - All operations are atomic within EVC batch + +**Authorization Mechanisms**: Both wrappers support two authorization modes: +- **EVC Permit**: One-time permit signature for specific operation +- **Pre-Approved Hashes** (`PreApprovedHashes.sol`): Users pre-approve operation hashes on-chain (useful for EIP-7702 wallet interactions) + +### Key Dependencies + +- **Euler Vault Kit** (`lib/euler-vault-kit`): ERC4626 vault implementation with borrowing +- **Ethereum Vault Connector (EVC)** (`lib/evc`): Batch transaction coordinator with account checking +- **CoW Protocol** (`lib/cow`): DEX aggregator settlement contracts and order libraries + +## Development Commands + +### Build +```bash +forge build +``` + +### Test +```bash +# Run all tests (requires FORK_RPC_URL environment variable) +forge test + +# Run specific test file +forge test --match-path test/CowEvcOpenPositionWrapper.t.sol + +# Run specific test function +forge test --match-test test_OpenPosition + +# Run with verbose output +forge test -vvv +``` + +**Important**: Tests require mainnet fork. Set `FORK_RPC_URL` environment variable to a mainnet RPC endpoint. + +### Format +```bash +forge fmt +``` + +### Gas Snapshots +```bash +forge snapshot +``` + +## Testing Architecture + +**Base Test Contract**: `test/helpers/CowBaseTest.sol` +- Sets up mainnet fork at block 22546006 +- Configures CoW Protocol settlement and authenticator +- Deploys test solver contract +- Sets up test vaults (eSUSDS, eWETH) and tokens +- Provides helper functions for creating settlement data structures + +**Test Helpers**: +- `MilkSwap.sol`: Simple test DEX for simulating swaps in settlements +- `GPv2OrderHelper.sol`: Utilities for constructing CoW Protocol orders +- `SignerECDSA.sol`: ECDSA signature utilities for tests +- `EmptyWrapper.sol`: Minimal wrapper for testing wrapper chaining + +## Important Implementation Details + +### Wrapper Data Format +Wrapper data is passed as a calldata slice with format: +``` +[wrapper-specific-params][signature][next-wrapper-address (20 bytes)][remaining-wrapper-data] +``` + +The `parseWrapperData()` function must consume its portion and return the remainder. + +### EVC Integration +Both wrappers execute operations within EVC batches to ensure atomicity and proper account health checks. The flow is: +1. Wrapper validates authorization (permit or pre-approved hash) +2. Build EVC.BatchItem[] array with all operations +3. Call `EVC.batch()` - EVC ensures account is healthy at end + +### Settlement Execution Context +- Wrappers use `evcInternalSettle()` as internal callback from EVC batch +- This function can only be called by EVC during batch execution +- Uses transient storage (`depth`, `settleCalls`) to prevent reentrancy + +### Authentication +- Only authenticated CoW Protocol solvers can call `wrappedSettle()` +- Authentication checked via `AUTHENTICATOR.isSolver(msg.sender)` +- Wrappers themselves can be added to solver allowlist for testing + +## Foundry Configuration + +- Compiler optimization: enabled +- IR compilation: enabled (`via_ir = true`) +- Source directory: `src/` +- Test directory: `test/` +- Library dependencies managed via git submodules + +## Coding Style + +### Error Handling +**Always use `require()` with custom errors instead of `if () { revert }`**. This pattern is used consistently throughout the codebase: + +```solidity +// ✅ Preferred +require(msg.sender == address(EVC), Unauthorized(msg.sender)); +require(depth > 0 && settleCalls == 0, Unauthorized(address(0))); + +// ❌ Avoid +if (msg.sender != address(EVC)) { + revert Unauthorized(msg.sender); +} +``` + +This approach is more concise and maintains consistency with the existing codebase style. + +## Remappings + +Key import remappings: +- `cow/` → CoW Protocol contracts (`lib/cow/src/contracts`) +- `evc/` → Ethereum Vault Connector (`lib/euler-vault-kit/lib/ethereum-vault-connector/src/`) +- `euler-vault-kit/` → Euler vault implementation +- `openzeppelin/` → OpenZeppelin contracts (via EVC dependency) From e5faddf905f38122fdf83eb240bc6c458a0df036 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 20 Oct 2025 19:44:37 +0900 Subject: [PATCH 16/43] forge fmt --- src/CowEvcWrapper.sol | 2 +- test/CowWrapper.t.sol | 10 ++++++---- test/EmptyWrapper.sol | 7 ++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/CowEvcWrapper.sol b/src/CowEvcWrapper.sol index 8a1e823..6930bf5 100644 --- a/src/CowEvcWrapper.sol +++ b/src/CowEvcWrapper.sol @@ -5,7 +5,7 @@ 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"; +import {GPv2Wrapper, GPv2Interaction} from "cow/GPv2Wrapper.sol"; /// @title CowEvcWrapper /// @notice A wrapper around the EVC that allows for settlement operations diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index b0ad240..1169702 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -89,8 +89,7 @@ contract MockSettlement { // Test wrapper that exposes internal functions contract TestWrapper is CowWrapper { - - string constant public name = "Test Wrapper"; + string public constant name = "Test Wrapper"; // Track _wrap calls struct WrapCall { @@ -264,8 +263,11 @@ contract CowWrapperTest is Test { signature: hex"aabbccddee" }); - settlement.interactions = - [new CowSettlement.CowInteractionData[](0), new CowSettlement.CowInteractionData[](0), new CowSettlement.CowInteractionData[](0)]; + settlement.interactions = [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0) + ]; // Build the chained wrapper data: // solver -> wrapper1 -> wrapper2 -> wrapper3 -> mockSettlement diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index b29798e..49d7abf 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -5,14 +5,11 @@ pragma abicoder v2; import "../src/vendor/CowWrapper.sol"; contract EmptyWrapper is CowWrapper { - string constant public name = "Empty Wrapper"; + string public constant name = "Empty Wrapper"; constructor(CowSettlement settlement_) CowWrapper(settlement_) {} - function _wrap( - bytes calldata settleData, - bytes calldata wrappedData - ) internal override { + function _wrap(bytes calldata settleData, bytes calldata wrappedData) internal override { _internalSettle(settleData, wrappedData); } } From 961e71a6230b2228ff02c7fded38a01d1ef53117 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 20 Oct 2025 19:49:35 +0900 Subject: [PATCH 17/43] update CLAUDE.md to reduce unnecessary messaging about reentrancy --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 940a754..df5b6cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,7 @@ This repository contains **Euler-CoW Protocol integration contracts** that enabl - Wrappers can be chained: Wrapper1 → Wrapper2 → Settlement - `CowWrapperHelpers.sol`: Helper utilities for wrapper data parsing and validation +- The CowWrapper is designed to support reentrancy. Additionally, the CowWrapper is designed with gas efficiency in mind, so we only check if the previous contract was part of the trusted wrapper chain. Furthermore, any wrappers that will ever be approved exist will use `CowWrapper.sol` as a base, so its not possible to inject a unauthorized wrapper into the chain without it getting ultimately rejected by the time the settlement contract is reached. **Specialized Wrappers**: Two production wrappers implement specific EVC + CoW Protocol workflows: @@ -153,3 +154,4 @@ Key import remappings: - `evc/` → Ethereum Vault Connector (`lib/euler-vault-kit/lib/ethereum-vault-connector/src/`) - `euler-vault-kit/` → Euler vault implementation - `openzeppelin/` → OpenZeppelin contracts (via EVC dependency) + From 7fea7baff8e786908eb6214a55071cfdb5256f7d Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 22 Oct 2025 23:20:58 +0900 Subject: [PATCH 18/43] updates from wrapper security review --- src/vendor/CowWrapper.sol | 22 +++++++++++++--------- test/CowWrapper.t.sol | 19 +++++-------------- test/EmptyWrapper.sol | 4 ++-- test/helpers/CowWrapperHelpers.sol | 1 + 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 3a5d754..b8414ed 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -111,25 +111,29 @@ abstract contract CowWrapper is ICowWrapper { // Revert if not a valid solver require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); + // Find out how long the next wrapper data is supposed to be + uint16 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2])); + // Delegate to the wrapper's custom logic - _wrap(settleData, wrapperData); + _wrap(settleData, wrapperData[2:2+nextWrapperDataLen], wrapperData[2+nextWrapperDataLen:]); } /// @notice Internal function containing the wrapper's custom logic /// @dev Must be implemented by concrete wrapper contracts. Should execute custom logic /// then eventually call _internalSettle() to continue the settlement chain. /// @param settleData ABI-encoded call to CowSettlement.settle() - /// @param wrapperData The wrapper data, which may be parsed and consumed as needed - function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal virtual; + /// @param wrapperData The wrapper data which should be consumed by this wrapper + /// @param remainingWrapperData Additional wrapper data needed by future wrappers. This should be passed unaltered to _internalSettle + function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal virtual; /// @notice Continues the settlement chain by calling the next wrapper or settlement contract /// @dev Extracts the next target address from wrapperData and either: /// - Calls CowSettlement.settle() directly if no more wrappers remain, or /// - Calls the next CowWrapper.wrappedSettle() to continue the chain /// @param settleData ABI-encoded call to CowSettlement.settle() - /// @param wrapperData Remaining wrapper data starting with the next target address (20 bytes) - function _internalSettle(bytes calldata settleData, bytes calldata wrapperData) internal { - if (wrapperData.length == 0) { + /// @param remainingWrapperData Remaining wrapper data starting with the next target address (20 bytes) + function _internalSettle(bytes calldata settleData, bytes calldata remainingWrapperData) internal { + if (remainingWrapperData.length == 0) { // No more wrapper data - we're calling the final settlement contract // Verify the settle data has the correct function selector require(bytes4(settleData[:4]) == CowSettlement.settle.selector, InvalidSettleData(settleData)); @@ -145,13 +149,13 @@ abstract contract CowWrapper is ICowWrapper { } } else { // Extract the next wrapper address from the first 20 bytes of wrapperData - address nextWrapper = address(bytes20(wrapperData[:20])); + address nextWrapper = address(bytes20(remainingWrapperData[:20])); // Skip past the address we just read - wrapperData = wrapperData[20:]; + remainingWrapperData = remainingWrapperData[20:]; // More wrapper data remains - call the next wrapper in the chain - CowWrapper(nextWrapper).wrappedSettle(settleData, wrapperData); + CowWrapper(nextWrapper).wrappedSettle(settleData, remainingWrapperData); } } } diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 1169702..c1e7a05 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -99,18 +99,16 @@ contract TestWrapper is CowWrapper { WrapCall[] public wrapCalls; - uint256 public skipWrappedData; - constructor(CowSettlement settlement_) CowWrapper(settlement_) {} - function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal override { + function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal override { // Record the wrap call WrapCall storage call_ = wrapCalls.push(); call_.settleData = settleData; - call_.wrapperData = wrapperData[0:skipWrappedData]; + call_.wrapperData = wrapperData; // Call internal settle - _internalSettle(settleData, wrapperData[skipWrappedData:]); + _internalSettle(settleData, remainingWrapperData); } function exposed_internalSettle(bytes calldata settleData, bytes calldata wrapperData) external { @@ -124,10 +122,6 @@ contract TestWrapper is CowWrapper { function getWrapCall(uint256 index) external view returns (bytes memory settleData, bytes memory wrapperData) { return (wrapCalls[index].settleData, wrapCalls[index].wrapperData); } - - function setSkipWrappedData(uint256 value) external { - skipWrappedData = value; - } } contract CowWrapperTest is Test { @@ -178,13 +172,10 @@ contract CowWrapperTest is Test { bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); // wrapperData is just custom data - no settlement address needed - bytes memory wrapperData = customWrapperData; - - testWrapper.setSkipWrappedData(customWrapperData.length); + bytes memory wrapperData = abi.encodePacked(uint16(customWrapperData.length), customWrapperData); vm.prank(solver); testWrapper.wrappedSettle(settleData, wrapperData); - testWrapper.setSkipWrappedData(0); assertEq(testWrapper.getWrapCallCount(), 1); (bytes memory recordedSettleData, bytes memory recordedWrapperData) = testWrapper.getWrapCall(0); @@ -206,7 +197,7 @@ contract CowWrapperTest is Test { bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); // Empty wrapperData means call the static SETTLEMENT contract - bytes memory wrapperData = hex""; + bytes memory wrapperData = hex"0000"; vm.prank(solver); testWrapper.wrappedSettle(settleData, wrapperData); diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index 49d7abf..4f48e6c 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -9,7 +9,7 @@ contract EmptyWrapper is CowWrapper { constructor(CowSettlement settlement_) CowWrapper(settlement_) {} - function _wrap(bytes calldata settleData, bytes calldata wrappedData) internal override { - _internalSettle(settleData, wrappedData); + function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { + _internalSettle(settleData, remainingWrapperData); } } diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol index 3971deb..d25ab1d 100644 --- a/test/helpers/CowWrapperHelpers.sol +++ b/test/helpers/CowWrapperHelpers.sol @@ -29,6 +29,7 @@ library CowWrapperHelpers { for (uint256 i = 0; i < wrappers.length; i++) { wrapperData = abi.encodePacked( wrapperData, + uint16(wrapperDatas[i].length), wrapperDatas[i] ); // Include the next wrapper address if there is one From 61051cd7c78adb9b96f416d5755278fcb67a7e65 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 27 Oct 2025 15:06:05 +0900 Subject: [PATCH 19/43] chore: `forge fmt` --- src/CowEvcWrapper.sol | 2 +- src/vendor/interfaces/IERC20.sol | 22 +--- .../interfaces/IEthereumVaultConnector.sol | 20 ++-- src/vendor/interfaces/IGPv2Settlement.sol | 112 ++++++++++-------- test/CowEvcWrapperTest.t.sol | 5 +- test/helpers/CowBaseTest.sol | 3 +- test/helpers/SignerECDSA.sol | 1 - 7 files changed, 81 insertions(+), 84 deletions(-) diff --git a/src/CowEvcWrapper.sol b/src/CowEvcWrapper.sol index 8a1e823..6930bf5 100644 --- a/src/CowEvcWrapper.sol +++ b/src/CowEvcWrapper.sol @@ -5,7 +5,7 @@ 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"; +import {GPv2Wrapper, GPv2Interaction} from "cow/GPv2Wrapper.sol"; /// @title CowEvcWrapper /// @notice A wrapper around the EVC that allows for settlement operations diff --git a/src/vendor/interfaces/IERC20.sol b/src/vendor/interfaces/IERC20.sol index 9b77d2f..3732468 100644 --- a/src/vendor/interfaces/IERC20.sol +++ b/src/vendor/interfaces/IERC20.sol @@ -44,10 +44,7 @@ interface IERC20 { * * Emits a {Transfer} event. */ - function transfer( - address recipient, - uint256 amount - ) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); /** * @dev Returns the remaining number of tokens that `spender` will be @@ -56,10 +53,7 @@ interface IERC20 { * * This value changes when {approve} or {transferFrom} are called. */ - function allowance( - address owner, - address spender - ) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); /** * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. @@ -86,11 +80,7 @@ interface IERC20 { * * Emits a {Transfer} event. */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); /** * @dev Emitted when `value` tokens are moved from one account (`from`) to @@ -104,9 +94,5 @@ interface IERC20 { * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. */ - event Approval( - address indexed owner, - address indexed spender, - uint256 value - ); + event Approval(address indexed owner, address indexed spender, uint256 value); } diff --git a/src/vendor/interfaces/IEthereumVaultConnector.sol b/src/vendor/interfaces/IEthereumVaultConnector.sol index a77c4e9..81432ca 100644 --- a/src/vendor/interfaces/IEthereumVaultConnector.sol +++ b/src/vendor/interfaces/IEthereumVaultConnector.sol @@ -325,12 +325,10 @@ interface IEVC { /// balance of the EVC contract will be forwarded. /// @param data The encoded data which is called on the target contract. /// @return result The result of the call. - function call( - address targetContract, - address onBehalfOfAccount, - uint256 value, - bytes calldata data - ) external payable returns (bytes memory result); + function call(address targetContract, address onBehalfOfAccount, uint256 value, bytes calldata data) + external + payable + returns (bytes memory result); /// @notice For a given account, calls into one of the enabled collateral vaults from the currently enabled /// controller vault as per data encoded. @@ -345,12 +343,10 @@ interface IEVC { /// balance of the EVC contract will be forwarded. /// @param data The encoded data which is called on the target collateral. /// @return result The result of the call. - function controlCollateral( - address targetCollateral, - address onBehalfOfAccount, - uint256 value, - bytes calldata data - ) external payable returns (bytes memory result); + function controlCollateral(address targetCollateral, address onBehalfOfAccount, uint256 value, bytes calldata data) + external + payable + returns (bytes memory result); /// @notice Executes multiple calls into the target contracts while checks deferred as per batch items provided. /// @dev This function defers the account and vault status checks (it's a checks-deferrable call). If the outermost diff --git a/src/vendor/interfaces/IGPv2Settlement.sol b/src/vendor/interfaces/IGPv2Settlement.sol index c1db9e5..85d0f7c 100644 --- a/src/vendor/interfaces/IGPv2Settlement.sol +++ b/src/vendor/interfaces/IGPv2Settlement.sol @@ -4,67 +4,81 @@ pragma solidity ^0.8; pragma experimental ABIEncoderV2; interface IGPv2Settlement { + receive() external payable; + function authenticator() external view returns (address); + function domainSeparator() external view returns (bytes32); + function filledAmount(bytes memory) external view returns (uint256); + function freeFilledAmountStorage(bytes[] memory orderUids) external; + function freePreSignatureStorage(bytes[] memory orderUids) external; + function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory); + function invalidateOrder(bytes memory orderUid) external; + function preSignature(bytes memory) external view returns (uint256); + function setPreSignature(bytes memory orderUid, bool signed) external; + function settle( + address[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) external; + function simulateDelegatecall(address targetContract, bytes memory calldataPayload) + external + returns (bytes memory response); + function simulateDelegatecallInternal(address targetContract, bytes memory calldataPayload) + external + returns (bytes memory response); + function swap(IVault.BatchSwapStep[] memory swaps, address[] memory tokens, GPv2Trade.Data memory trade) external; + function vault() external view returns (address); + function vaultRelayer() external view returns (address); - - -receive () external payable; -function authenticator( ) external view returns (address ) ; -function domainSeparator( ) external view returns (bytes32 ) ; -function filledAmount( bytes memory ) external view returns (uint256 ) ; -function freeFilledAmountStorage( bytes[] memory orderUids ) external ; -function freePreSignatureStorage( bytes[] memory orderUids ) external ; -function getStorageAt( uint256 offset,uint256 length ) external view returns (bytes memory ) ; -function invalidateOrder( bytes memory orderUid ) external ; -function preSignature( bytes memory ) external view returns (uint256 ) ; -function setPreSignature( bytes memory orderUid,bool signed ) external ; -function settle( address[] memory tokens,uint256[] memory clearingPrices,GPv2Trade.Data[] memory trades,GPv2Interaction.Data[][3] memory interactions ) external ; -function simulateDelegatecall( address targetContract,bytes memory calldataPayload ) external returns (bytes memory response) ; -function simulateDelegatecallInternal( address targetContract,bytes memory calldataPayload ) external returns (bytes memory response) ; -function swap( IVault.BatchSwapStep[] memory swaps,address[] memory tokens,GPv2Trade.Data memory trade ) external ; -function vault( ) external view returns (address ) ; -function vaultRelayer( ) external view returns (address ) ; -event Interaction( address indexed target,uint256 value,bytes4 selector ) ; -event OrderInvalidated( address indexed owner,bytes orderUid ) ; -event PreSignature( address indexed owner,bytes orderUid,bool signed ) ; -event Settlement( address indexed solver ) ; -event Trade( address indexed owner,address sellToken,address buyToken,uint256 sellAmount,uint256 buyAmount,uint256 feeAmount,bytes orderUid ) ; + event Interaction(address indexed target, uint256 value, bytes4 selector); + event OrderInvalidated(address indexed owner, bytes orderUid); + event PreSignature(address indexed owner, bytes orderUid, bool signed); + event Settlement(address indexed solver); + event Trade( + address indexed owner, + address sellToken, + address buyToken, + uint256 sellAmount, + uint256 buyAmount, + uint256 feeAmount, + bytes orderUid + ); } interface GPv2Trade { -struct Data { -uint256 sellTokenIndex; -uint256 buyTokenIndex; -address receiver; -uint256 sellAmount; -uint256 buyAmount; -uint32 validTo; -bytes32 appData; -uint256 feeAmount; -uint256 flags; -uint256 executedAmount; -bytes signature; -} + struct Data { + uint256 sellTokenIndex; + uint256 buyTokenIndex; + address receiver; + uint256 sellAmount; + uint256 buyAmount; + uint32 validTo; + bytes32 appData; + uint256 feeAmount; + uint256 flags; + uint256 executedAmount; + bytes signature; + } } interface GPv2Interaction { -struct Data { -address target; -uint256 value; -bytes callData; -} + struct Data { + address target; + uint256 value; + bytes callData; + } } interface IVault { -struct BatchSwapStep { -bytes32 poolId; -uint256 assetInIndex; -uint256 assetOutIndex; -uint256 amount; -bytes userData; -} + struct BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; + } } - // THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: /* [{"type":"constructor","inputs":[{"name":"authenticator_","type":"address","internalType":"contract GPv2Authentication"},{"name":"vault_","type":"address","internalType":"contract IVault"}],"stateMutability":"nonpayable"},{"type":"receive","stateMutability":"payable"},{"type":"function","name":"authenticator","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract GPv2Authentication"}],"stateMutability":"view"},{"type":"function","name":"domainSeparator","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"filledAmount","inputs":[{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"freeFilledAmountStorage","inputs":[{"name":"orderUids","type":"bytes[]","internalType":"bytes[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"freePreSignatureStorage","inputs":[{"name":"orderUids","type":"bytes[]","internalType":"bytes[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getStorageAt","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"length","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"invalidateOrder","inputs":[{"name":"orderUid","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"preSignature","inputs":[{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"setPreSignature","inputs":[{"name":"orderUid","type":"bytes","internalType":"bytes"},{"name":"signed","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settle","inputs":[{"name":"tokens","type":"address[]","internalType":"contract IERC20[]"},{"name":"clearingPrices","type":"uint256[]","internalType":"uint256[]"},{"name":"trades","type":"tuple[]","internalType":"struct GPv2Trade.Data[]","components":[{"name":"sellTokenIndex","type":"uint256","internalType":"uint256"},{"name":"buyTokenIndex","type":"uint256","internalType":"uint256"},{"name":"receiver","type":"address","internalType":"address"},{"name":"sellAmount","type":"uint256","internalType":"uint256"},{"name":"buyAmount","type":"uint256","internalType":"uint256"},{"name":"validTo","type":"uint32","internalType":"uint32"},{"name":"appData","type":"bytes32","internalType":"bytes32"},{"name":"feeAmount","type":"uint256","internalType":"uint256"},{"name":"flags","type":"uint256","internalType":"uint256"},{"name":"executedAmount","type":"uint256","internalType":"uint256"},{"name":"signature","type":"bytes","internalType":"bytes"}]},{"name":"interactions","type":"tuple[][3]","internalType":"struct GPv2Interaction.Data[][3]","components":[{"name":"target","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"callData","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"simulateDelegatecall","inputs":[{"name":"targetContract","type":"address","internalType":"address"},{"name":"calldataPayload","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"simulateDelegatecallInternal","inputs":[{"name":"targetContract","type":"address","internalType":"address"},{"name":"calldataPayload","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"swap","inputs":[{"name":"swaps","type":"tuple[]","internalType":"struct IVault.BatchSwapStep[]","components":[{"name":"poolId","type":"bytes32","internalType":"bytes32"},{"name":"assetInIndex","type":"uint256","internalType":"uint256"},{"name":"assetOutIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"userData","type":"bytes","internalType":"bytes"}]},{"name":"tokens","type":"address[]","internalType":"contract IERC20[]"},{"name":"trade","type":"tuple","internalType":"struct GPv2Trade.Data","components":[{"name":"sellTokenIndex","type":"uint256","internalType":"uint256"},{"name":"buyTokenIndex","type":"uint256","internalType":"uint256"},{"name":"receiver","type":"address","internalType":"address"},{"name":"sellAmount","type":"uint256","internalType":"uint256"},{"name":"buyAmount","type":"uint256","internalType":"uint256"},{"name":"validTo","type":"uint32","internalType":"uint32"},{"name":"appData","type":"bytes32","internalType":"bytes32"},{"name":"feeAmount","type":"uint256","internalType":"uint256"},{"name":"flags","type":"uint256","internalType":"uint256"},{"name":"executedAmount","type":"uint256","internalType":"uint256"},{"name":"signature","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"vault","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IVault"}],"stateMutability":"view"},{"type":"function","name":"vaultRelayer","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract GPv2VaultRelayer"}],"stateMutability":"view"},{"type":"event","name":"Interaction","inputs":[{"name":"target","type":"address","indexed":true,"internalType":"address"},{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"selector","type":"bytes4","indexed":false,"internalType":"bytes4"}],"anonymous":false},{"type":"event","name":"OrderInvalidated","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"PreSignature","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"signed","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"Settlement","inputs":[{"name":"solver","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Trade","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"sellToken","type":"address","indexed":false,"internalType":"contract IERC20"},{"name":"buyToken","type":"address","indexed":false,"internalType":"contract IERC20"},{"name":"sellAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"buyAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"feeAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false}] diff --git a/test/CowEvcWrapperTest.t.sol b/test/CowEvcWrapperTest.t.sol index 68d600c..50711ac 100644 --- a/test/CowEvcWrapperTest.t.sol +++ b/test/CowEvcWrapperTest.t.sol @@ -99,7 +99,9 @@ contract CowEvcWrapperTest is CowBaseTest { uint256 susdsBalanceInMilkSwapAfter = IERC20(SUSDS).balanceOf(address(milkSwap)); assertEq( - susdsBalanceInMilkSwapAfter, susdsBalanceInMilkSwapBefore - buyAmount - 1e18, "MilkSwap should have less SUSDS" + susdsBalanceInMilkSwapAfter, + susdsBalanceInMilkSwapBefore - buyAmount - 1e18, + "MilkSwap should have less SUSDS" ); } @@ -202,7 +204,6 @@ contract CowEvcWrapperTest is CowBaseTest { wrapper.wrappedSettle.selector, tokens, clearingPrices, trades, interactions, evcActions ); solver.runBatch(targets, datas); - } // Verify the position was created diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index 4611ff1..3b6abcd 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -24,7 +24,8 @@ import {console} from "forge-std/Test.sol"; contract Solver { function runBatch(address[] memory targets, bytes[] memory datas) external { for (uint256 i = 0; i < targets.length; i++) { - targets[i].call(datas[i]); + (bool success,) = targets[i].call(datas[i]); + require(success, "Solver: call failed"); } } } diff --git a/test/helpers/SignerECDSA.sol b/test/helpers/SignerECDSA.sol index bbec68a..2bb399d 100644 --- a/test/helpers/SignerECDSA.sol +++ b/test/helpers/SignerECDSA.sol @@ -13,7 +13,6 @@ import "openzeppelin/utils/cryptography/ECDSA.sol"; // abstract contract EIP712 { - bytes32 internal constant _TYPE_HASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); From 6b12785e10c08d0eb6a7cf48a726e03784547d34 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 27 Oct 2025 15:12:38 +0900 Subject: [PATCH 20/43] foundry fixes --- .github/workflows/test.yml | 2 ++ foundry.toml | 2 +- test/CowEvcWrapperTest.t.sol | 27 ++++++++++++++------------- test/helpers/CowBaseTest.sol | 8 ++------ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a7f138..ffdaca8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,8 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + with: + version: v1.4.3 - name: Show Forge version run: | diff --git a/foundry.toml b/foundry.toml index 37f6dd8..3c4963a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,6 @@ src = "src" out = "out" libs = ["lib"] -optimize = true +optimizer = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/test/CowEvcWrapperTest.t.sol b/test/CowEvcWrapperTest.t.sol index 50711ac..337a540 100644 --- a/test/CowEvcWrapperTest.t.sol +++ b/test/CowEvcWrapperTest.t.sol @@ -75,8 +75,7 @@ contract CowEvcWrapperTest is CowBaseTest { // Get settlement, that sells WETH for SUSDS ( - bytes memory orderUid, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -125,8 +124,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -186,7 +184,9 @@ contract CowEvcWrapperTest is CowBaseTest { onBehalfOfAccount: address(0), targetContract: address(evc), value: 0, - data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + 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 @@ -236,8 +236,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -297,7 +296,9 @@ contract CowEvcWrapperTest is CowBaseTest { onBehalfOfAccount: address(0), targetContract: address(evc), value: 0, - data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + 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 @@ -349,8 +350,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -397,8 +397,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -458,7 +457,9 @@ contract CowEvcWrapperTest is CowBaseTest { onBehalfOfAccount: address(0), targetContract: address(evc), value: 0, - data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + 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 diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index 3b6abcd..b7df094 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -138,17 +138,13 @@ contract CowBaseTest is EVaultTestBase { 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)) + 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)) + target: address(SUSDS), value: 0, callData: abi.encodeCall(IERC20.transfer, (eSUSDS, buyAmount)) }); } From b1d112770172f2220c2356613aee2f73d1f13384 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 27 Oct 2025 16:19:26 +0900 Subject: [PATCH 21/43] add pull request template --- .github/pull_request_template.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..00e889b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Description +Summarize what this PR does and why in a single, clear sentence. +A reader should understand the purpose and scope of the change just from this line. + +## Context +Explain the motivation or problem this PR addresses. +Include links to any relevant documentation, tickets, or discussions. + +Provide background that helps reviewers understand *why* the change is needed. +Highlight any important design decisions, dependencies, or related components. + +## Out of Scope +Specify what is *not* covered in this PR. +This helps reviewers focus on the intended scope and prevents scope creep. + +## Testing Instructions +Provide clear, step-by-step instructions for verifying this change locally. +Assume the reviewer has no prior context about your setup. + +This section reinforces the scope of the PR by outlining what should be tested and what the expected outcomes are. From 9739ab12f57430236f242e7c4519763c1ccf6e95 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 27 Oct 2025 16:24:32 +0900 Subject: [PATCH 22/43] chore: apply formatting --- src/vendor/CowWrapper.sol | 6 +- src/vendor/interfaces/IERC20.sol | 22 +--- .../interfaces/IEthereumVaultConnector.sol | 20 ++-- src/vendor/interfaces/IGPv2Settlement.sol | 113 ++++++++++-------- test/CowEvcWrapperTest.t.sol | 32 ++--- test/CowWrapper.t.sol | 5 +- test/helpers/CowBaseTest.sol | 8 +- test/helpers/CowWrapperHelpers.sol | 15 +-- test/helpers/SignerECDSA.sol | 1 - 9 files changed, 110 insertions(+), 112 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index b8414ed..5033b34 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -115,7 +115,7 @@ abstract contract CowWrapper is ICowWrapper { uint16 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2])); // Delegate to the wrapper's custom logic - _wrap(settleData, wrapperData[2:2+nextWrapperDataLen], wrapperData[2+nextWrapperDataLen:]); + _wrap(settleData, wrapperData[2:2 + nextWrapperDataLen], wrapperData[2 + nextWrapperDataLen:]); } /// @notice Internal function containing the wrapper's custom logic @@ -124,7 +124,9 @@ abstract contract CowWrapper is ICowWrapper { /// @param settleData ABI-encoded call to CowSettlement.settle() /// @param wrapperData The wrapper data which should be consumed by this wrapper /// @param remainingWrapperData Additional wrapper data needed by future wrappers. This should be passed unaltered to _internalSettle - function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal virtual; + function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) + internal + virtual; /// @notice Continues the settlement chain by calling the next wrapper or settlement contract /// @dev Extracts the next target address from wrapperData and either: diff --git a/src/vendor/interfaces/IERC20.sol b/src/vendor/interfaces/IERC20.sol index 9b77d2f..3732468 100644 --- a/src/vendor/interfaces/IERC20.sol +++ b/src/vendor/interfaces/IERC20.sol @@ -44,10 +44,7 @@ interface IERC20 { * * Emits a {Transfer} event. */ - function transfer( - address recipient, - uint256 amount - ) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); /** * @dev Returns the remaining number of tokens that `spender` will be @@ -56,10 +53,7 @@ interface IERC20 { * * This value changes when {approve} or {transferFrom} are called. */ - function allowance( - address owner, - address spender - ) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); /** * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. @@ -86,11 +80,7 @@ interface IERC20 { * * Emits a {Transfer} event. */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); /** * @dev Emitted when `value` tokens are moved from one account (`from`) to @@ -104,9 +94,5 @@ interface IERC20 { * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. */ - event Approval( - address indexed owner, - address indexed spender, - uint256 value - ); + event Approval(address indexed owner, address indexed spender, uint256 value); } diff --git a/src/vendor/interfaces/IEthereumVaultConnector.sol b/src/vendor/interfaces/IEthereumVaultConnector.sol index a77c4e9..81432ca 100644 --- a/src/vendor/interfaces/IEthereumVaultConnector.sol +++ b/src/vendor/interfaces/IEthereumVaultConnector.sol @@ -325,12 +325,10 @@ interface IEVC { /// balance of the EVC contract will be forwarded. /// @param data The encoded data which is called on the target contract. /// @return result The result of the call. - function call( - address targetContract, - address onBehalfOfAccount, - uint256 value, - bytes calldata data - ) external payable returns (bytes memory result); + function call(address targetContract, address onBehalfOfAccount, uint256 value, bytes calldata data) + external + payable + returns (bytes memory result); /// @notice For a given account, calls into one of the enabled collateral vaults from the currently enabled /// controller vault as per data encoded. @@ -345,12 +343,10 @@ interface IEVC { /// balance of the EVC contract will be forwarded. /// @param data The encoded data which is called on the target collateral. /// @return result The result of the call. - function controlCollateral( - address targetCollateral, - address onBehalfOfAccount, - uint256 value, - bytes calldata data - ) external payable returns (bytes memory result); + function controlCollateral(address targetCollateral, address onBehalfOfAccount, uint256 value, bytes calldata data) + external + payable + returns (bytes memory result); /// @notice Executes multiple calls into the target contracts while checks deferred as per batch items provided. /// @dev This function defers the account and vault status checks (it's a checks-deferrable call). If the outermost diff --git a/src/vendor/interfaces/IGPv2Settlement.sol b/src/vendor/interfaces/IGPv2Settlement.sol index c1db9e5..dcbeb7a 100644 --- a/src/vendor/interfaces/IGPv2Settlement.sol +++ b/src/vendor/interfaces/IGPv2Settlement.sol @@ -4,67 +4,80 @@ pragma solidity ^0.8; pragma experimental ABIEncoderV2; interface IGPv2Settlement { - - - -receive () external payable; -function authenticator( ) external view returns (address ) ; -function domainSeparator( ) external view returns (bytes32 ) ; -function filledAmount( bytes memory ) external view returns (uint256 ) ; -function freeFilledAmountStorage( bytes[] memory orderUids ) external ; -function freePreSignatureStorage( bytes[] memory orderUids ) external ; -function getStorageAt( uint256 offset,uint256 length ) external view returns (bytes memory ) ; -function invalidateOrder( bytes memory orderUid ) external ; -function preSignature( bytes memory ) external view returns (uint256 ) ; -function setPreSignature( bytes memory orderUid,bool signed ) external ; -function settle( address[] memory tokens,uint256[] memory clearingPrices,GPv2Trade.Data[] memory trades,GPv2Interaction.Data[][3] memory interactions ) external ; -function simulateDelegatecall( address targetContract,bytes memory calldataPayload ) external returns (bytes memory response) ; -function simulateDelegatecallInternal( address targetContract,bytes memory calldataPayload ) external returns (bytes memory response) ; -function swap( IVault.BatchSwapStep[] memory swaps,address[] memory tokens,GPv2Trade.Data memory trade ) external ; -function vault( ) external view returns (address ) ; -function vaultRelayer( ) external view returns (address ) ; -event Interaction( address indexed target,uint256 value,bytes4 selector ) ; -event OrderInvalidated( address indexed owner,bytes orderUid ) ; -event PreSignature( address indexed owner,bytes orderUid,bool signed ) ; -event Settlement( address indexed solver ) ; -event Trade( address indexed owner,address sellToken,address buyToken,uint256 sellAmount,uint256 buyAmount,uint256 feeAmount,bytes orderUid ) ; + receive() external payable; + function authenticator() external view returns (address); + function domainSeparator() external view returns (bytes32); + function filledAmount(bytes memory) external view returns (uint256); + function freeFilledAmountStorage(bytes[] memory orderUids) external; + function freePreSignatureStorage(bytes[] memory orderUids) external; + function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory); + function invalidateOrder(bytes memory orderUid) external; + function preSignature(bytes memory) external view returns (uint256); + function setPreSignature(bytes memory orderUid, bool signed) external; + function settle( + address[] memory tokens, + uint256[] memory clearingPrices, + GPv2Trade.Data[] memory trades, + GPv2Interaction.Data[][3] memory interactions + ) external; + function simulateDelegatecall(address targetContract, bytes memory calldataPayload) + external + returns (bytes memory response); + function simulateDelegatecallInternal(address targetContract, bytes memory calldataPayload) + external + returns (bytes memory response); + function swap(IVault.BatchSwapStep[] memory swaps, address[] memory tokens, GPv2Trade.Data memory trade) external; + function vault() external view returns (address); + function vaultRelayer() external view returns (address); + event Interaction(address indexed target, uint256 value, bytes4 selector); + event OrderInvalidated(address indexed owner, bytes orderUid); + event PreSignature(address indexed owner, bytes orderUid, bool signed); + event Settlement(address indexed solver); + event Trade( + address indexed owner, + address sellToken, + address buyToken, + uint256 sellAmount, + uint256 buyAmount, + uint256 feeAmount, + bytes orderUid + ); } interface GPv2Trade { -struct Data { -uint256 sellTokenIndex; -uint256 buyTokenIndex; -address receiver; -uint256 sellAmount; -uint256 buyAmount; -uint32 validTo; -bytes32 appData; -uint256 feeAmount; -uint256 flags; -uint256 executedAmount; -bytes signature; -} + struct Data { + uint256 sellTokenIndex; + uint256 buyTokenIndex; + address receiver; + uint256 sellAmount; + uint256 buyAmount; + uint32 validTo; + bytes32 appData; + uint256 feeAmount; + uint256 flags; + uint256 executedAmount; + bytes signature; + } } interface GPv2Interaction { -struct Data { -address target; -uint256 value; -bytes callData; -} + struct Data { + address target; + uint256 value; + bytes callData; + } } interface IVault { -struct BatchSwapStep { -bytes32 poolId; -uint256 assetInIndex; -uint256 assetOutIndex; -uint256 amount; -bytes userData; -} + struct BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; + } } - // THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: /* [{"type":"constructor","inputs":[{"name":"authenticator_","type":"address","internalType":"contract GPv2Authentication"},{"name":"vault_","type":"address","internalType":"contract IVault"}],"stateMutability":"nonpayable"},{"type":"receive","stateMutability":"payable"},{"type":"function","name":"authenticator","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract GPv2Authentication"}],"stateMutability":"view"},{"type":"function","name":"domainSeparator","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"filledAmount","inputs":[{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"freeFilledAmountStorage","inputs":[{"name":"orderUids","type":"bytes[]","internalType":"bytes[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"freePreSignatureStorage","inputs":[{"name":"orderUids","type":"bytes[]","internalType":"bytes[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getStorageAt","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"length","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"invalidateOrder","inputs":[{"name":"orderUid","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"preSignature","inputs":[{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"setPreSignature","inputs":[{"name":"orderUid","type":"bytes","internalType":"bytes"},{"name":"signed","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settle","inputs":[{"name":"tokens","type":"address[]","internalType":"contract IERC20[]"},{"name":"clearingPrices","type":"uint256[]","internalType":"uint256[]"},{"name":"trades","type":"tuple[]","internalType":"struct GPv2Trade.Data[]","components":[{"name":"sellTokenIndex","type":"uint256","internalType":"uint256"},{"name":"buyTokenIndex","type":"uint256","internalType":"uint256"},{"name":"receiver","type":"address","internalType":"address"},{"name":"sellAmount","type":"uint256","internalType":"uint256"},{"name":"buyAmount","type":"uint256","internalType":"uint256"},{"name":"validTo","type":"uint32","internalType":"uint32"},{"name":"appData","type":"bytes32","internalType":"bytes32"},{"name":"feeAmount","type":"uint256","internalType":"uint256"},{"name":"flags","type":"uint256","internalType":"uint256"},{"name":"executedAmount","type":"uint256","internalType":"uint256"},{"name":"signature","type":"bytes","internalType":"bytes"}]},{"name":"interactions","type":"tuple[][3]","internalType":"struct GPv2Interaction.Data[][3]","components":[{"name":"target","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"callData","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"simulateDelegatecall","inputs":[{"name":"targetContract","type":"address","internalType":"address"},{"name":"calldataPayload","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"simulateDelegatecallInternal","inputs":[{"name":"targetContract","type":"address","internalType":"address"},{"name":"calldataPayload","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"swap","inputs":[{"name":"swaps","type":"tuple[]","internalType":"struct IVault.BatchSwapStep[]","components":[{"name":"poolId","type":"bytes32","internalType":"bytes32"},{"name":"assetInIndex","type":"uint256","internalType":"uint256"},{"name":"assetOutIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"userData","type":"bytes","internalType":"bytes"}]},{"name":"tokens","type":"address[]","internalType":"contract IERC20[]"},{"name":"trade","type":"tuple","internalType":"struct GPv2Trade.Data","components":[{"name":"sellTokenIndex","type":"uint256","internalType":"uint256"},{"name":"buyTokenIndex","type":"uint256","internalType":"uint256"},{"name":"receiver","type":"address","internalType":"address"},{"name":"sellAmount","type":"uint256","internalType":"uint256"},{"name":"buyAmount","type":"uint256","internalType":"uint256"},{"name":"validTo","type":"uint32","internalType":"uint32"},{"name":"appData","type":"bytes32","internalType":"bytes32"},{"name":"feeAmount","type":"uint256","internalType":"uint256"},{"name":"flags","type":"uint256","internalType":"uint256"},{"name":"executedAmount","type":"uint256","internalType":"uint256"},{"name":"signature","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"vault","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IVault"}],"stateMutability":"view"},{"type":"function","name":"vaultRelayer","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract GPv2VaultRelayer"}],"stateMutability":"view"},{"type":"event","name":"Interaction","inputs":[{"name":"target","type":"address","indexed":true,"internalType":"address"},{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"selector","type":"bytes4","indexed":false,"internalType":"bytes4"}],"anonymous":false},{"type":"event","name":"OrderInvalidated","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"PreSignature","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"signed","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"Settlement","inputs":[{"name":"solver","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Trade","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"sellToken","type":"address","indexed":false,"internalType":"contract IERC20"},{"name":"buyToken","type":"address","indexed":false,"internalType":"contract IERC20"},{"name":"sellAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"buyAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"feeAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false}] diff --git a/test/CowEvcWrapperTest.t.sol b/test/CowEvcWrapperTest.t.sol index 68d600c..337a540 100644 --- a/test/CowEvcWrapperTest.t.sol +++ b/test/CowEvcWrapperTest.t.sol @@ -75,8 +75,7 @@ contract CowEvcWrapperTest is CowBaseTest { // Get settlement, that sells WETH for SUSDS ( - bytes memory orderUid, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -99,7 +98,9 @@ contract CowEvcWrapperTest is CowBaseTest { uint256 susdsBalanceInMilkSwapAfter = IERC20(SUSDS).balanceOf(address(milkSwap)); assertEq( - susdsBalanceInMilkSwapAfter, susdsBalanceInMilkSwapBefore - buyAmount - 1e18, "MilkSwap should have less SUSDS" + susdsBalanceInMilkSwapAfter, + susdsBalanceInMilkSwapBefore - buyAmount - 1e18, + "MilkSwap should have less SUSDS" ); } @@ -123,8 +124,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -184,7 +184,9 @@ contract CowEvcWrapperTest is CowBaseTest { onBehalfOfAccount: address(0), targetContract: address(evc), value: 0, - data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + 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 @@ -202,7 +204,6 @@ contract CowEvcWrapperTest is CowBaseTest { wrapper.wrappedSettle.selector, tokens, clearingPrices, trades, interactions, evcActions ); solver.runBatch(targets, datas); - } // Verify the position was created @@ -235,8 +236,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -296,7 +296,9 @@ contract CowEvcWrapperTest is CowBaseTest { onBehalfOfAccount: address(0), targetContract: address(evc), value: 0, - data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + 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 @@ -348,8 +350,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -396,8 +397,7 @@ contract CowEvcWrapperTest is CowBaseTest { // 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, - , + bytes memory orderUid,, IERC20[] memory tokens, uint256[] memory clearingPrices, GPv2Trade.Data[] memory trades, @@ -457,7 +457,9 @@ contract CowEvcWrapperTest is CowBaseTest { onBehalfOfAccount: address(0), targetContract: address(evc), value: 0, - data: abi.encodeCall(IEVC.permit, (user, address(wrapper), 0, 0, block.timestamp, 0, batchData, batchSignature)) + 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 diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index c1e7a05..2435d39 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -101,7 +101,10 @@ contract TestWrapper is CowWrapper { constructor(CowSettlement settlement_) CowWrapper(settlement_) {} - function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal override { + function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) + internal + override + { // Record the wrap call WrapCall storage call_ = wrapCalls.push(); call_.settleData = settleData; diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index 4611ff1..5378e85 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -137,17 +137,13 @@ contract CowBaseTest is EVaultTestBase { 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)) + 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)) + target: address(SUSDS), value: 0, callData: abi.encodeCall(IERC20.transfer, (eSUSDS, buyAmount)) }); } diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol index d25ab1d..704233f 100644 --- a/test/helpers/CowWrapperHelpers.sol +++ b/test/helpers/CowWrapperHelpers.sol @@ -27,11 +27,7 @@ library CowWrapperHelpers { // Build the wrapper data chain bytes memory wrapperData; for (uint256 i = 0; i < wrappers.length; i++) { - wrapperData = abi.encodePacked( - wrapperData, - uint16(wrapperDatas[i].length), - wrapperDatas[i] - ); + wrapperData = abi.encodePacked(wrapperData, uint16(wrapperDatas[i].length), wrapperDatas[i]); // Include the next wrapper address if there is one if (wrappers.length > i + 1) { wrapperData = abi.encodePacked(wrapperData, wrappers[i + 1]); @@ -40,8 +36,13 @@ library CowWrapperHelpers { } // Build the settle calldata - bytes memory settleData = - abi.encodeWithSelector(CowSettlement.settle.selector, settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions); + bytes memory settleData = abi.encodeWithSelector( + CowSettlement.settle.selector, + settlement.tokens, + settlement.clearingPrices, + settlement.trades, + settlement.interactions + ); // Encode the wrappedSettle call fullCalldata = abi.encodeWithSelector(ICowWrapper.wrappedSettle.selector, settleData, wrapperData); diff --git a/test/helpers/SignerECDSA.sol b/test/helpers/SignerECDSA.sol index bbec68a..2bb399d 100644 --- a/test/helpers/SignerECDSA.sol +++ b/test/helpers/SignerECDSA.sol @@ -13,7 +13,6 @@ import "openzeppelin/utils/cryptography/ECDSA.sol"; // abstract contract EIP712 { - bytes32 internal constant _TYPE_HASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); From 85291917de07eb60a76903d50bf7f0b539dae13a Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 02:13:47 +0900 Subject: [PATCH 23/43] fixes from feedback * add comment adding a bit of clariifcation on the length * minior refactor to reduce duplication on the tests --- src/vendor/CowWrapper.sol | 5 ++- test/CowWrapper.t.sol | 88 ++++++++++++--------------------------- 2 files changed, 30 insertions(+), 63 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 5033b34..927fdb1 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -105,13 +105,14 @@ abstract contract CowWrapper is ICowWrapper { /// validates wrapper data, then delegates to _wrap() for custom logic. /// @param settleData ABI-encoded call to CowSettlement.settle() containing trade data /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. - /// Format: [wrapper-specific-data][next-address][remaining-wrapper-data] - /// Must be at least 20 bytes to contain the next settlement target address. + /// Format: [len][wrapper-specific-data][next-address]([len][wrapper-specific-data][next-address]...) function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external { // Revert if not a valid solver require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); // Find out how long the next wrapper data is supposed to be + // We use 2 bytes to decode the length of the wrapper data because it allows for up to 64KB of data for each wrapper. + // This should be plenty of length for all identified use-cases of wrappers in the forseeable future. uint16 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2])); // Delegate to the wrapper's custom logic diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 2435d39..1c66b8d 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -27,7 +27,7 @@ contract MockSettlement { IERC20[] tokens; uint256[] clearingPrices; GPv2Trade.Data[] trades; - bytes additionalData; + bytes origData; } SettleCall[] public settleCalls; @@ -45,7 +45,7 @@ contract MockSettlement { IERC20[] calldata tokens, uint256[] calldata clearingPrices, GPv2Trade.Data[] calldata trades, - GPv2Interaction.Data[][3] calldata + GPv2Interaction.Data[][3] calldata interactions ) external { SettleCall storage call_ = settleCalls.push(); call_.tokens = tokens; @@ -55,21 +55,7 @@ contract MockSettlement { call_.trades.push(trades[i]); } - // Extract any additional data appended after standard calldata - uint256 expectedLength = 4 + 4 * 32; - expectedLength += 32 + tokens.length * 32; - expectedLength += 32 + clearingPrices.length * 32; - expectedLength += 32; - for (uint256 i = 0; i < trades.length; i++) { - expectedLength += 9 * 32 + 32 + trades[i].signature.length; - } - expectedLength += 32; - expectedLength += 3 * 32; - expectedLength += 3 * 32; - - if (msg.data.length > expectedLength) { - call_.additionalData = msg.data[expectedLength:]; - } + call_.origData = msg.data; } function getSettleCallCount() external view returns (uint256) { @@ -79,11 +65,11 @@ contract MockSettlement { function getLastSettleCall() external view - returns (uint256 tokenCount, uint256 priceCount, uint256 tradeCount, bytes memory additionalData) + returns (uint256 tokenCount, uint256 priceCount, uint256 tradeCount, bytes memory origData) { require(settleCalls.length > 0, "No settle calls"); SettleCall storage lastCall = settleCalls[settleCalls.length - 1]; - return (lastCall.tokens.length, lastCall.clearingPrices.length, lastCall.trades.length, lastCall.additionalData); + return (lastCall.tokens.length, lastCall.clearingPrices.length, lastCall.trades.length, lastCall.origData); } } @@ -159,21 +145,25 @@ contract CowWrapperTest is Test { authenticator.addSolver(address(wrapper3)); } - function test_wrap_ReceivesCorrectParameters() public { - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = IERC20(address(0x1)); - - uint256[] memory clearingPrices = new uint256[](1); - clearingPrices[0] = 100; + function _emptyInteractions() private pure returns (GPv2Interaction.Data[][3] memory) { + return [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + } - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + function _createSimpleSettleData(uint256 tokenCount) private pure returns (bytes memory) { + IERC20[] memory tokens = new IERC20[](tokenCount); + uint256[] memory clearingPrices = new uint256[](tokenCount); + for (uint256 i = 0; i < tokenCount; i++) { + tokens[i] = IERC20(address(uint160(i + 1))); + clearingPrices[i] = 100 * (i + 1); + } + return abi.encodeWithSelector( + CowSettlement.settle.selector, tokens, clearingPrices, new GPv2Trade.Data[](0), _emptyInteractions() + ); + } + function test_wrap_ReceivesCorrectParameters() public { bytes memory customWrapperData = hex"deadbeef"; - - bytes memory settleData = - abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory settleData = _createSimpleSettleData(1); // wrapperData is just custom data - no settlement address needed bytes memory wrapperData = abi.encodePacked(uint16(customWrapperData.length), customWrapperData); @@ -187,18 +177,7 @@ contract CowWrapperTest is Test { } function test_internalSettle_CallsNextSettlement() public { - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = IERC20(address(0x1)); - - uint256[] memory clearingPrices = new uint256[](1); - clearingPrices[0] = 100; - - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - - bytes memory settleData = - abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory settleData = abi.encodePacked(_createSimpleSettleData(1), hex"123456"); // Empty wrapperData means call the static SETTLEMENT contract bytes memory wrapperData = hex"0000"; @@ -206,38 +185,28 @@ contract CowWrapperTest is Test { testWrapper.wrappedSettle(settleData, wrapperData); assertEq(mockSettlement.getSettleCallCount(), 1); - (uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); + (uint256 tokenCount, uint256 priceCount, uint256 tradeCount, bytes memory origData) = mockSettlement.getLastSettleCall(); assertEq(tokenCount, 1); assertEq(priceCount, 1); assertEq(tradeCount, 0); + assertEq(origData, settleData); } function test_wrappedSettle_RevertsWithNotASolver() public { - IERC20[] memory tokens = new IERC20[](0); - uint256[] memory clearingPrices = new uint256[](0); - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = - [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - - bytes memory settleData = - abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory wrapperData = hex""; - + bytes memory settleData = _createSimpleSettleData(0); address notASolver = makeAddr("notASolver"); // Should revert when called by non-solver vm.prank(notASolver); vm.expectRevert(abi.encodeWithSelector(CowWrapper.NotASolver.selector, notASolver)); - testWrapper.wrappedSettle(settleData, wrapperData); + testWrapper.wrappedSettle(settleData, hex""); } function test_integration_ThreeWrappersChained() public { CowWrapperHelpers.SettleCall memory settlement; - settlement.tokens = new address[](2); settlement.tokens[0] = address(0x1); settlement.tokens[1] = address(0x2); - settlement.clearingPrices = new uint256[](2); settlement.clearingPrices[0] = 100; settlement.clearingPrices[1] = 200; @@ -265,16 +234,13 @@ contract CowWrapperTest is Test { // Build the chained wrapper data: // solver -> wrapper1 -> wrapper2 -> wrapper3 -> mockSettlement - address[] memory wrappers = new address[](3); wrappers[0] = address(wrapper1); wrappers[1] = address(wrapper2); wrappers[2] = address(wrapper3); - bytes[] memory wrapperDatas = new bytes[](3); - (address target, bytes memory fullCalldata) = - CowWrapperHelpers.encodeWrapperCall(wrappers, wrapperDatas, address(mockSettlement), settlement); + CowWrapperHelpers.encodeWrapperCall(wrappers, new bytes[](3), address(mockSettlement), settlement); // Call wrapper1 as the solver vm.prank(solver); From 41ddd46b0685b7cbc530ba9d465df0d6af74aacd Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 02:46:57 +0900 Subject: [PATCH 24/43] clarify the thing that is being tested and use expectCall more --- test/CowWrapper.t.sol | 116 +++++++++++++----------------------------- 1 file changed, 35 insertions(+), 81 deletions(-) diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 1c66b8d..77c09fe 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -23,14 +23,6 @@ contract MockAuthentication { } contract MockSettlement { - struct SettleCall { - IERC20[] tokens; - uint256[] clearingPrices; - GPv2Trade.Data[] trades; - bytes origData; - } - - SettleCall[] public settleCalls; CowAuthentication private immutable _authenticator; constructor(CowAuthentication authenticator_) { @@ -42,35 +34,11 @@ contract MockSettlement { } function settle( - IERC20[] calldata tokens, - uint256[] calldata clearingPrices, - GPv2Trade.Data[] calldata trades, - GPv2Interaction.Data[][3] calldata interactions - ) external { - SettleCall storage call_ = settleCalls.push(); - call_.tokens = tokens; - call_.clearingPrices = clearingPrices; - - for (uint256 i = 0; i < trades.length; i++) { - call_.trades.push(trades[i]); - } - - call_.origData = msg.data; - } - - function getSettleCallCount() external view returns (uint256) { - return settleCalls.length; - } - - function getLastSettleCall() - external - view - returns (uint256 tokenCount, uint256 priceCount, uint256 tradeCount, bytes memory origData) - { - require(settleCalls.length > 0, "No settle calls"); - SettleCall storage lastCall = settleCalls[settleCalls.length - 1]; - return (lastCall.tokens.length, lastCall.clearingPrices.length, lastCall.trades.length, lastCall.origData); - } + IERC20[] calldata, + uint256[] calldata, + GPv2Trade.Data[] calldata, + GPv2Interaction.Data[][3] calldata + ) external {} } // Test wrapper that exposes internal functions @@ -103,14 +71,6 @@ contract TestWrapper is CowWrapper { function exposed_internalSettle(bytes calldata settleData, bytes calldata wrapperData) external { _internalSettle(settleData, wrapperData); } - - function getWrapCallCount() external view returns (uint256) { - return wrapCalls.length; - } - - function getWrapCall(uint256 index) external view returns (bytes memory settleData, bytes memory wrapperData) { - return (wrapCalls[index].settleData, wrapCalls[index].wrapperData); - } } contract CowWrapperTest is Test { @@ -161,36 +121,24 @@ contract CowWrapperTest is Test { ); } - function test_wrap_ReceivesCorrectParameters() public { - bytes memory customWrapperData = hex"deadbeef"; - bytes memory settleData = _createSimpleSettleData(1); - // wrapperData is just custom data - no settlement address needed - bytes memory wrapperData = abi.encodePacked(uint16(customWrapperData.length), customWrapperData); + function test_internalSettle_CallsWrapperAndThenNextSettlement() public { + bytes memory settleData = abi.encodePacked(_createSimpleSettleData(1), hex"123456"); + bytes memory secondCallWrapperData = hex"0003098765"; + bytes memory wrapperData = abi.encodePacked(hex"00021234", address(testWrapper), secondCallWrapperData); - vm.prank(solver); - testWrapper.wrappedSettle(settleData, wrapperData); + // the wrapper gets called exactly twice (once below and again inside the wrapper data calling self) + vm.expectCall(address(testWrapper), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 2); - assertEq(testWrapper.getWrapCallCount(), 1); - (bytes memory recordedSettleData, bytes memory recordedWrapperData) = testWrapper.getWrapCall(0); - assertGt(recordedSettleData.length, 0); - assertEq(recordedWrapperData, customWrapperData); - } + // verify the internal wrapper call data + vm.expectCall(address(testWrapper), abi.encodeWithSelector(testWrapper.wrappedSettle.selector, settleData, secondCallWrapperData)); - function test_internalSettle_CallsNextSettlement() public { - bytes memory settleData = abi.encodePacked(_createSimpleSettleData(1), hex"123456"); - // Empty wrapperData means call the static SETTLEMENT contract - bytes memory wrapperData = hex"0000"; + // the settlement contract gets called once after wrappers (including the surplus data at the end) + vm.expectCall(address(mockSettlement), 0, settleData, 1); vm.prank(solver); testWrapper.wrappedSettle(settleData, wrapperData); - - assertEq(mockSettlement.getSettleCallCount(), 1); - (uint256 tokenCount, uint256 priceCount, uint256 tradeCount, bytes memory origData) = mockSettlement.getLastSettleCall(); - assertEq(tokenCount, 1); - assertEq(priceCount, 1); - assertEq(tradeCount, 0); - assertEq(origData, settleData); } + function test_wrappedSettle_RevertsWithNotASolver() public { bytes memory settleData = _createSimpleSettleData(0); @@ -203,6 +151,7 @@ contract CowWrapperTest is Test { } function test_integration_ThreeWrappersChained() public { + // Set up a more sophisticated settlement call to make sure it all gets through as expected. CowWrapperHelpers.SettleCall memory settlement; settlement.tokens = new address[](2); settlement.tokens[0] = address(0x1); @@ -233,27 +182,32 @@ contract CowWrapperTest is Test { ]; // Build the chained wrapper data: - // solver -> wrapper1 -> wrapper2 -> wrapper3 -> mockSettlement - address[] memory wrappers = new address[](3); + // solver -> wrapper1 -> wrapper2 -> wrapper1 -> wrapper3 -> mockSettlement + address[] memory wrappers = new address[](4); wrappers[0] = address(wrapper1); wrappers[1] = address(wrapper2); - wrappers[2] = address(wrapper3); + wrappers[2] = address(wrapper1); + wrappers[3] = address(wrapper3); + + bytes[] memory datas = new bytes[](4); + + datas[2] = hex"828348"; (address target, bytes memory fullCalldata) = - CowWrapperHelpers.encodeWrapperCall(wrappers, new bytes[](3), address(mockSettlement), settlement); + CowWrapperHelpers.encodeWrapperCall(wrappers, datas, address(mockSettlement), settlement); + + + // all the wrappers gets called, with wrapper 1 called twice + vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 2); + vm.expectCall(address(wrapper2), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 1); + vm.expectCall(address(wrapper3), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 1); + + // the settlement gets called with the full data + vm.expectCall(address(mockSettlement), new bytes(0)); // Call wrapper1 as the solver vm.prank(solver); (bool success,) = target.call(fullCalldata); assertTrue(success, "Chained wrapper call should succeed"); - - // Verify that mockSettlement was called - assertEq(mockSettlement.getSettleCallCount(), 1, "MockSettlement should be called once"); - - // Verify the settlement received the correct parameters - (uint256 tokenCount, uint256 priceCount, uint256 tradeCount,) = mockSettlement.getLastSettleCall(); - assertEq(tokenCount, 2, "Should have 2 tokens"); - assertEq(priceCount, 2, "Should have 2 prices"); - assertEq(tradeCount, 1, "Should have 1 trade"); } } From 63cc658bf422297ad5462519e7de91a0ee7f7af5 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 02:47:26 +0900 Subject: [PATCH 25/43] fix fmt --- test/CowWrapper.t.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 77c09fe..cf43a66 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -130,7 +130,10 @@ contract CowWrapperTest is Test { vm.expectCall(address(testWrapper), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 2); // verify the internal wrapper call data - vm.expectCall(address(testWrapper), abi.encodeWithSelector(testWrapper.wrappedSettle.selector, settleData, secondCallWrapperData)); + vm.expectCall( + address(testWrapper), + abi.encodeWithSelector(testWrapper.wrappedSettle.selector, settleData, secondCallWrapperData) + ); // the settlement contract gets called once after wrappers (including the surplus data at the end) vm.expectCall(address(mockSettlement), 0, settleData, 1); @@ -138,7 +141,6 @@ contract CowWrapperTest is Test { vm.prank(solver); testWrapper.wrappedSettle(settleData, wrapperData); } - function test_wrappedSettle_RevertsWithNotASolver() public { bytes memory settleData = _createSimpleSettleData(0); @@ -196,7 +198,6 @@ contract CowWrapperTest is Test { (address target, bytes memory fullCalldata) = CowWrapperHelpers.encodeWrapperCall(wrappers, datas, address(mockSettlement), settlement); - // all the wrappers gets called, with wrapper 1 called twice vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 2); vm.expectCall(address(wrapper2), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 1); From a464c98fcfef230f9c50b78bec8b43e9bfe4c908 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 05:22:11 +0900 Subject: [PATCH 26/43] remove more dead code --- test/CowWrapper.t.sol | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index cf43a66..63ea442 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -45,26 +45,9 @@ contract MockSettlement { contract TestWrapper is CowWrapper { string public constant name = "Test Wrapper"; - // Track _wrap calls - struct WrapCall { - bytes settleData; - bytes wrapperData; - } - - WrapCall[] public wrapCalls; - constructor(CowSettlement settlement_) CowWrapper(settlement_) {} - function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) - internal - override - { - // Record the wrap call - WrapCall storage call_ = wrapCalls.push(); - call_.settleData = settleData; - call_.wrapperData = wrapperData; - - // Call internal settle + function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { _internalSettle(settleData, remainingWrapperData); } From ca2c6b433b9752a9630743d942a82c7abbd8ff35 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 05:26:18 +0900 Subject: [PATCH 27/43] Update src/vendor/CowWrapper.sol Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- src/vendor/CowWrapper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 927fdb1..e824a60 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -2,7 +2,7 @@ pragma solidity >=0.7.6 <0.9.0; pragma abicoder v2; -/// @title CoW Protocol v2 Authentication Interface +/// @title CoW Protocol Authentication Interface /// @author CoW DAO developers interface CowAuthentication { /// @dev determines whether the provided address is an authenticated solver. From 549d740ed553741c00a6e9d74a6b46c6c137d014 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 05:27:46 +0900 Subject: [PATCH 28/43] Update src/vendor/CowWrapper.sol Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- src/vendor/CowWrapper.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index e824a60..117f557 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -4,7 +4,7 @@ pragma abicoder v2; /// @title CoW Protocol Authentication Interface /// @author CoW DAO developers -interface CowAuthentication { +interface ICowAuthentication { /// @dev determines whether the provided address is an authenticated solver. /// @param prospectiveSolver the address of prospective solver. /// @return true when prospectiveSolver is an authenticated solver, otherwise false. @@ -14,7 +14,7 @@ interface CowAuthentication { /// @title CoW Protocol Settlement Interface /// @notice Minimal interface for CoW Protocol's settlement contract /// @dev Used for type-safe calls to the settlement contract's settle function -interface CowSettlement { +interface ICowSettlement { /// @notice Trade data structure matching GPv2Settlement struct CowTradeData { uint256 sellTokenIndex; From 3a1654f7e199e008309e91bb39d9f6490c6808a8 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 05:28:22 +0900 Subject: [PATCH 29/43] Update src/vendor/CowWrapper.sol Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- src/vendor/CowWrapper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 117f557..3cb29b6 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -16,7 +16,7 @@ interface ICowAuthentication { /// @dev Used for type-safe calls to the settlement contract's settle function interface ICowSettlement { /// @notice Trade data structure matching GPv2Settlement - struct CowTradeData { + struct Trade { uint256 sellTokenIndex; uint256 buyTokenIndex; address receiver; From 972f958d981550fa6c4882b0d5cac43a445e44f4 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 06:01:06 +0900 Subject: [PATCH 30/43] more warnings fixes and make it compile with previous suggestion accepts --- src/vendor/CowWrapper.sol | 32 ++++++++++----------- test/CowWrapper.t.sol | 46 ++++++++++++++---------------- test/EmptyWrapper.sol | 6 ++-- test/helpers/CowWrapperHelpers.sol | 13 ++++----- 4 files changed, 47 insertions(+), 50 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 3cb29b6..cac2afe 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -31,14 +31,14 @@ interface ICowSettlement { } /// @notice Interaction data structure for pre/intra/post-settlement hooks - struct CowInteractionData { + struct Interaction { address target; uint256 value; bytes callData; } /// @notice Returns the authentication contract used by the settlement contract. - function authenticator() external returns (CowAuthentication); + function authenticator() external returns (ICowAuthentication); /// @notice Settles a batch of trades atomically /// @param tokens Array of token addresses involved in the settlement @@ -48,8 +48,8 @@ interface ICowSettlement { function settle( address[] calldata tokens, uint256[] calldata clearingPrices, - CowTradeData[] calldata trades, - CowInteractionData[][3] calldata interactions + Trade[] calldata trades, + Interaction[][3] calldata interactions ) external; } @@ -63,7 +63,7 @@ interface ICowWrapper { /// @notice Initiates a wrapped settlement call /// @dev This is the entry point for wrapped settlements. The wrapper will execute custom logic /// before calling the next wrapper or settlement contract in the chain. - /// @param settleData ABI-encoded call to CowSettlement.settle() + /// @param settleData ABI-encoded call to ICowSettlement.settle() /// @param wrapperData Encoded chain of wrapper-specific data followed by addresses of next wrappers/settlement function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external; } @@ -72,9 +72,9 @@ interface ICowWrapper { /// @notice Abstract base contract for creating wrapper contracts around CoW Protocol settlements /// @dev A wrapper enables custom pre/post-settlement and context-setting logic and can be chained with other wrappers. /// Wrappers must: -/// - Be approved by the CowAuthentication contract +/// - Be approved by the ICowAuthentication contract /// - Verify the caller is an authenticated solver -/// - Eventually call settle() on the approved CowSettlement contract +/// - Eventually call settle() on the approved ICowSettlement contract /// - Implement _wrap() for custom logic /// - Implement parseWrapperData() for validation of implementation-specific wrapperData abstract contract CowWrapper is ICowWrapper { @@ -87,15 +87,15 @@ abstract contract CowWrapper is ICowWrapper { error InvalidSettleData(bytes invalidSettleData); /// @notice The settlement contract - CowSettlement public immutable SETTLEMENT; + ICowSettlement public immutable SETTLEMENT; /// @notice The authentication contract used to verify solvers /// @dev This is derived from `SETTLEMENT.authenticator()`. - CowAuthentication public immutable AUTHENTICATOR; + ICowAuthentication public immutable AUTHENTICATOR; /// @notice Constructs a new CowWrapper - /// @param settlement_ The CowSettlement contract to use at the end of the wrapper chain. Also used for wrapper authentication. - constructor(CowSettlement settlement_) { + /// @param settlement_ The ICowSettlement contract to use at the end of the wrapper chain. Also used for wrapper authentication. + constructor(ICowSettlement settlement_) { SETTLEMENT = settlement_; AUTHENTICATOR = settlement_.authenticator(); } @@ -103,7 +103,7 @@ abstract contract CowWrapper is ICowWrapper { /// @notice Initiates a wrapped settlement call /// @dev Entry point for solvers to execute wrapped settlements. Verifies the caller is a solver, /// validates wrapper data, then delegates to _wrap() for custom logic. - /// @param settleData ABI-encoded call to CowSettlement.settle() containing trade data + /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. /// Format: [len][wrapper-specific-data][next-address]([len][wrapper-specific-data][next-address]...) function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external { @@ -122,7 +122,7 @@ abstract contract CowWrapper is ICowWrapper { /// @notice Internal function containing the wrapper's custom logic /// @dev Must be implemented by concrete wrapper contracts. Should execute custom logic /// then eventually call _internalSettle() to continue the settlement chain. - /// @param settleData ABI-encoded call to CowSettlement.settle() + /// @param settleData ABI-encoded call to ICowSettlement.settle() /// @param wrapperData The wrapper data which should be consumed by this wrapper /// @param remainingWrapperData Additional wrapper data needed by future wrappers. This should be passed unaltered to _internalSettle function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) @@ -131,15 +131,15 @@ abstract contract CowWrapper is ICowWrapper { /// @notice Continues the settlement chain by calling the next wrapper or settlement contract /// @dev Extracts the next target address from wrapperData and either: - /// - Calls CowSettlement.settle() directly if no more wrappers remain, or + /// - Calls ICowSettlement.settle() directly if no more wrappers remain, or /// - Calls the next CowWrapper.wrappedSettle() to continue the chain - /// @param settleData ABI-encoded call to CowSettlement.settle() + /// @param settleData ABI-encoded call to ICowSettlement.settle() /// @param remainingWrapperData Remaining wrapper data starting with the next target address (20 bytes) function _internalSettle(bytes calldata settleData, bytes calldata remainingWrapperData) internal { if (remainingWrapperData.length == 0) { // No more wrapper data - we're calling the final settlement contract // Verify the settle data has the correct function selector - require(bytes4(settleData[:4]) == CowSettlement.settle.selector, InvalidSettleData(settleData)); + require(bytes4(settleData[:4]) == ICowSettlement.settle.selector, InvalidSettleData(settleData)); // Call the settlement contract directly with the settle data (bool success, bytes memory returnData) = address(SETTLEMENT).call(settleData); diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 63ea442..b2b277a 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapper, CowSettlement, CowAuthentication} from "../src/vendor/CowWrapper.sol"; +import {CowWrapper, ICowSettlement, ICowAuthentication} from "../src/vendor/CowWrapper.sol"; import {IERC20, GPv2Trade, GPv2Interaction} from "cow/GPv2Settlement.sol"; import {EmptyWrapper} from "./EmptyWrapper.sol"; import {CowWrapperHelpers} from "./helpers/CowWrapperHelpers.sol"; -import "forge-std/console.sol"; - contract MockAuthentication { mapping(address => bool) public solvers; @@ -23,14 +21,14 @@ contract MockAuthentication { } contract MockSettlement { - CowAuthentication private immutable _authenticator; + ICowAuthentication private immutable AUTHENTICATOR; - constructor(CowAuthentication authenticator_) { - _authenticator = authenticator_; + constructor(ICowAuthentication authenticator_) { + AUTHENTICATOR = authenticator_; } - function authenticator() external view returns (CowAuthentication) { - return _authenticator; + function authenticator() external view returns (ICowAuthentication) { + return AUTHENTICATOR; } function settle( @@ -43,15 +41,15 @@ contract MockSettlement { // Test wrapper that exposes internal functions contract TestWrapper is CowWrapper { - string public constant name = "Test Wrapper"; + string public override name = "Test Wrapper"; - constructor(CowSettlement settlement_) CowWrapper(settlement_) {} + constructor(ICowSettlement settlement_) CowWrapper(settlement_) {} function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { _internalSettle(settleData, remainingWrapperData); } - function exposed_internalSettle(bytes calldata settleData, bytes calldata wrapperData) external { + function exposedInternalSettle(bytes calldata settleData, bytes calldata wrapperData) external { _internalSettle(settleData, wrapperData); } } @@ -69,17 +67,17 @@ contract CowWrapperTest is Test { function setUp() public { // Deploy mock contracts authenticator = new MockAuthentication(); - mockSettlement = new MockSettlement(CowAuthentication(address(authenticator))); + mockSettlement = new MockSettlement(ICowAuthentication(address(authenticator))); solver = makeAddr("solver"); // Add solver to the authenticator authenticator.addSolver(solver); // Create test wrapper and three EmptyWrapper instances with the settlement contract - testWrapper = new TestWrapper(CowSettlement(address(mockSettlement))); - wrapper1 = new EmptyWrapper(CowSettlement(address(mockSettlement))); - wrapper2 = new EmptyWrapper(CowSettlement(address(mockSettlement))); - wrapper3 = new EmptyWrapper(CowSettlement(address(mockSettlement))); + testWrapper = new TestWrapper(ICowSettlement(address(mockSettlement))); + wrapper1 = new EmptyWrapper(ICowSettlement(address(mockSettlement))); + wrapper2 = new EmptyWrapper(ICowSettlement(address(mockSettlement))); + wrapper3 = new EmptyWrapper(ICowSettlement(address(mockSettlement))); // Add all wrappers as solvers authenticator.addSolver(address(testWrapper)); @@ -92,15 +90,15 @@ contract CowWrapperTest is Test { return [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; } - function _createSimpleSettleData(uint256 tokenCount) private pure returns (bytes memory) { + function _createSimpleSettleData(uint256 tokenCount) private returns (bytes memory) { IERC20[] memory tokens = new IERC20[](tokenCount); uint256[] memory clearingPrices = new uint256[](tokenCount); for (uint256 i = 0; i < tokenCount; i++) { - tokens[i] = IERC20(address(uint160(i + 1))); + tokens[i] = IERC20(makeAddr(string(abi.encodePacked("Settle Token #", vm.toString(i + 1))))); clearingPrices[i] = 100 * (i + 1); } return abi.encodeWithSelector( - CowSettlement.settle.selector, tokens, clearingPrices, new GPv2Trade.Data[](0), _emptyInteractions() + ICowSettlement.settle.selector, tokens, clearingPrices, new GPv2Trade.Data[](0), _emptyInteractions() ); } @@ -145,8 +143,8 @@ contract CowWrapperTest is Test { settlement.clearingPrices[0] = 100; settlement.clearingPrices[1] = 200; - settlement.trades = new CowSettlement.CowTradeData[](1); - settlement.trades[0] = CowSettlement.CowTradeData({ + settlement.trades = new ICowSettlement.Trade[](1); + settlement.trades[0] = ICowSettlement.Trade({ sellTokenIndex: 0, buyTokenIndex: 1, receiver: address(0x123), @@ -161,9 +159,9 @@ contract CowWrapperTest is Test { }); settlement.interactions = [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ]; // Build the chained wrapper data: diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index 4f48e6c..824ce1b 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8; pragma abicoder v2; -import "../src/vendor/CowWrapper.sol"; +import {ICowSettlement, CowWrapper} from "../src/vendor/CowWrapper.sol"; contract EmptyWrapper is CowWrapper { - string public constant name = "Empty Wrapper"; + string public override name = "Empty Wrapper"; - constructor(CowSettlement settlement_) CowWrapper(settlement_) {} + constructor(ICowSettlement settlement_) CowWrapper(settlement_) {} function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { _internalSettle(settleData, remainingWrapperData); diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol index 704233f..3635a95 100644 --- a/test/helpers/CowWrapperHelpers.sol +++ b/test/helpers/CowWrapperHelpers.sol @@ -1,29 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8; -import {CowSettlement, ICowWrapper, CowAuthentication} from "src/vendor/CowWrapper.sol"; +import {ICowSettlement, ICowAuthentication, ICowWrapper} from "src/vendor/CowWrapper.sol"; library CowWrapperHelpers { struct SettleCall { address[] tokens; uint256[] clearingPrices; - CowSettlement.CowTradeData[] trades; - CowSettlement.CowInteractionData[][3] interactions; + ICowSettlement.Trade[] trades; + ICowSettlement.Interaction[][3] interactions; } /** * @dev This function is intended for testing purposes and is not memory efficient. * @param wrappers Array of wrapper addresses to chain together * @param wrapperDatas Array of wrapper-specific data for each wrapper - * @param cowSettlement The settlement contract address (unused, kept for backwards compatibility) * @param settlement The settlement call parameters */ function encodeWrapperCall( address[] calldata wrappers, bytes[] calldata wrapperDatas, - address cowSettlement, + address, SettleCall calldata settlement - ) external returns (address target, bytes memory fullCalldata) { + ) external pure returns (address target, bytes memory fullCalldata) { // Build the wrapper data chain bytes memory wrapperData; for (uint256 i = 0; i < wrappers.length; i++) { @@ -37,7 +36,7 @@ library CowWrapperHelpers { // Build the settle calldata bytes memory settleData = abi.encodeWithSelector( - CowSettlement.settle.selector, + ICowSettlement.settle.selector, settlement.tokens, settlement.clearingPrices, settlement.trades, From b4c9b97b39e2df118a62dca8d822d5c66f0cf32e Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 06:10:17 +0900 Subject: [PATCH 31/43] update claude md to quiet the ai --- CLAUDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index df5b6cc..e3ba40a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,3 +155,6 @@ Key import remappings: - `euler-vault-kit/` → Euler vault implementation - `openzeppelin/` → OpenZeppelin contracts (via EVC dependency) + +## When Giving PR feedback +* do not re-suggest or address feedback after it has already been given, either by you or other contributors who have commented. From fe6446a9c05aa72215c4ef4a48e6493c49edb45b Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 06:49:53 +0900 Subject: [PATCH 32/43] try to quiet claude * add test * even more explicit instructions to not give inline comments --- CLAUDE.md | 1 + test/CowWrapper.t.sol | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index e3ba40a..89f2351 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,3 +158,4 @@ Key import remappings: ## When Giving PR feedback * do not re-suggest or address feedback after it has already been given, either by you or other contributors who have commented. +* be careful not to use too many inline comments. If there are already inline comments on the same line that you want to comment on, or if the inline comment is about something that has already been suggested, don't comment. diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index b2b277a..13789e9 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -133,6 +133,14 @@ contract CowWrapperTest is Test { testWrapper.wrappedSettle(settleData, hex""); } + function test_wrappedSettle_RevertsOnInvalidSettleSelector() public { + bytes memory settleData = abi.encodePacked(bytes4(0xdeadbeef), hex"1234"); + bytes memory wrapperData = hex"0000"; // Empty wrapper data, goes straight to settlement + vm.prank(solver); + vm.expectRevert(abi.encodeWithSelector(CowWrapper.InvalidSettleData.selector, settleData)); + testWrapper.wrappedSettle(settleData, wrapperData); + } + function test_integration_ThreeWrappersChained() public { // Set up a more sophisticated settlement call to make sure it all gets through as expected. CowWrapperHelpers.SettleCall memory settlement; From 13e09aac5d2870e81f6f1e559e37eb2424fca2b5 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 07:18:12 +0900 Subject: [PATCH 33/43] move the cow wrapper file out of vendor, and update the docs a bit more --- src/{vendor => }/CowWrapper.sol | 13 ++++++++++++- test/CowWrapper.t.sol | 2 +- test/EmptyWrapper.sol | 2 +- test/helpers/CowWrapperHelpers.sol | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) rename src/{vendor => }/CowWrapper.sol (90%) diff --git a/src/vendor/CowWrapper.sol b/src/CowWrapper.sol similarity index 90% rename from src/vendor/CowWrapper.sol rename to src/CowWrapper.sol index cac2afe..14b079b 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -2,6 +2,17 @@ pragma solidity >=0.7.6 <0.9.0; pragma abicoder v2; +/** + * @title CoW Wrapper all-in-one integration file + * @author CoW Protocol Developers + * @notice This file is completely self-contained (ie no dependencies) and can be portably copied to whatever projects it is needed. + * It contains: + * * CowWrapper -- an abstract base contract which should be inherited by all wrappers + * * ICowWrapper -- the required interface for all wrappers + * * ICowSettlement -- A minimized interface and base structures for CoW Protocol settlement contract. From https://github.com/cowprotocol/contracts/blob/main/src/contracts/GPv2Settlement.sol + * * ICowAuthentication -- The authentication interface used by ICowSettlement. From https://github.com/cowprotocol/contracts/blob/main/src/contracts/interfaces/GPv2Authentication.sol + */ + /// @title CoW Protocol Authentication Interface /// @author CoW DAO developers interface ICowAuthentication { @@ -105,7 +116,7 @@ abstract contract CowWrapper is ICowWrapper { /// validates wrapper data, then delegates to _wrap() for custom logic. /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. - /// Format: [len][wrapper-specific-data][next-address]([len][wrapper-specific-data][next-address]...) + /// Format: [2-byte len][wrapper-specific-data][next-address]([2-byte len][wrapper-specific-data][next-address]...) function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external { // Revert if not a valid solver require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 13789e9..3413c74 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapper, ICowSettlement, ICowAuthentication} from "../src/vendor/CowWrapper.sol"; +import {CowWrapper, ICowSettlement, ICowAuthentication} from "../src/CowWrapper.sol"; import {IERC20, GPv2Trade, GPv2Interaction} from "cow/GPv2Settlement.sol"; import {EmptyWrapper} from "./EmptyWrapper.sol"; diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index 824ce1b..43fabce 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8; pragma abicoder v2; -import {ICowSettlement, CowWrapper} from "../src/vendor/CowWrapper.sol"; +import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; contract EmptyWrapper is CowWrapper { string public override name = "Empty Wrapper"; diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol index 3635a95..a830af9 100644 --- a/test/helpers/CowWrapperHelpers.sol +++ b/test/helpers/CowWrapperHelpers.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8; -import {ICowSettlement, ICowAuthentication, ICowWrapper} from "src/vendor/CowWrapper.sol"; +import {ICowSettlement, ICowAuthentication, ICowWrapper} from "src/CowWrapper.sol"; library CowWrapperHelpers { struct SettleCall { From 7c6be098a4639dff2d4f34a52f05a4d64dfc3237 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 07:23:02 +0900 Subject: [PATCH 34/43] update documentation on the wrapper interface --- src/CowWrapper.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index 14b079b..b43c91a 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -74,8 +74,9 @@ interface ICowWrapper { /// @notice Initiates a wrapped settlement call /// @dev This is the entry point for wrapped settlements. The wrapper will execute custom logic /// before calling the next wrapper or settlement contract in the chain. - /// @param settleData ABI-encoded call to ICowSettlement.settle() - /// @param wrapperData Encoded chain of wrapper-specific data followed by addresses of next wrappers/settlement + /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data + /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. + /// Format: [2-byte len][wrapper-specific-data][next-address]([2-byte len][wrapper-specific-data][next-address]...) function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external; } From 0313ff2436017211a0b94f4551094516933cf651 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 07:27:33 +0900 Subject: [PATCH 35/43] remove parseWrapperData reference in comment --- src/CowWrapper.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index b43c91a..cf32b33 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -88,7 +88,6 @@ interface ICowWrapper { /// - Verify the caller is an authenticated solver /// - Eventually call settle() on the approved ICowSettlement contract /// - Implement _wrap() for custom logic -/// - Implement parseWrapperData() for validation of implementation-specific wrapperData abstract contract CowWrapper is ICowWrapper { /// @notice Thrown when the caller is not an authenticated solver /// @param unauthorized The address that attempted to call wrappedSettle From d358b09aad9e261b9fc2f350710d96f45dd28128 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 07:31:26 +0900 Subject: [PATCH 36/43] add remaining wrapper data start variable --- src/CowWrapper.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index cf32b33..69481a1 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -127,7 +127,8 @@ abstract contract CowWrapper is ICowWrapper { uint16 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2])); // Delegate to the wrapper's custom logic - _wrap(settleData, wrapperData[2:2 + nextWrapperDataLen], wrapperData[2 + nextWrapperDataLen:]); + uint256 remainingWrapperDataStart = 2 + nextWrapperDataLen; + _wrap(settleData, wrapperData[2:remainingWrapperDataStart], wrapperData[remainingWrapperDataStart:]); } /// @notice Internal function containing the wrapper's custom logic From 694e7b083d055a481462a4f680f5360704a13704 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 07:34:06 +0900 Subject: [PATCH 37/43] Update src/CowWrapper.sol Co-authored-by: Anxo Rodriguez --- src/CowWrapper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index 69481a1..5c81015 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -136,7 +136,7 @@ abstract contract CowWrapper is ICowWrapper { /// then eventually call _internalSettle() to continue the settlement chain. /// @param settleData ABI-encoded call to ICowSettlement.settle() /// @param wrapperData The wrapper data which should be consumed by this wrapper - /// @param remainingWrapperData Additional wrapper data needed by future wrappers. This should be passed unaltered to _internalSettle + /// @param remainingWrapperData Additional wrapper data. It is the reminder bytes resulting from consuming the current's wrapper data from the original `wrapperData` in the `wrappedSettle` call. This should be passed unaltered to `_internalSettle` that will call the settlement function if this remainder is empty, or delegate the settlement to the next wrapper function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal virtual; From bccb8db1e32a776108a8ab51ce842eeab086dae4 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 07:37:03 +0900 Subject: [PATCH 38/43] rename `_internalSettle` to `_next` --- src/CowWrapper.sol | 8 ++++---- test/CowWrapper.t.sol | 8 ++------ test/EmptyWrapper.sol | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index 5c81015..0a91bde 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -133,21 +133,21 @@ abstract contract CowWrapper is ICowWrapper { /// @notice Internal function containing the wrapper's custom logic /// @dev Must be implemented by concrete wrapper contracts. Should execute custom logic - /// then eventually call _internalSettle() to continue the settlement chain. + /// then eventually call _next() to continue the wrapped settlement chain. /// @param settleData ABI-encoded call to ICowSettlement.settle() /// @param wrapperData The wrapper data which should be consumed by this wrapper - /// @param remainingWrapperData Additional wrapper data. It is the reminder bytes resulting from consuming the current's wrapper data from the original `wrapperData` in the `wrappedSettle` call. This should be passed unaltered to `_internalSettle` that will call the settlement function if this remainder is empty, or delegate the settlement to the next wrapper + /// @param remainingWrapperData Additional wrapper data. It is the reminder bytes resulting from consuming the current's wrapper data from the original `wrapperData` in the `wrappedSettle` call. This should be passed unaltered to `_next` that will call the settlement function if this remainder is empty, or delegate the settlement to the next wrapper function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal virtual; - /// @notice Continues the settlement chain by calling the next wrapper or settlement contract + /// @notice Continues the wrapped settlement chain by calling the next wrapper or settlement contract /// @dev Extracts the next target address from wrapperData and either: /// - Calls ICowSettlement.settle() directly if no more wrappers remain, or /// - Calls the next CowWrapper.wrappedSettle() to continue the chain /// @param settleData ABI-encoded call to ICowSettlement.settle() /// @param remainingWrapperData Remaining wrapper data starting with the next target address (20 bytes) - function _internalSettle(bytes calldata settleData, bytes calldata remainingWrapperData) internal { + function _next(bytes calldata settleData, bytes calldata remainingWrapperData) internal { if (remainingWrapperData.length == 0) { // No more wrapper data - we're calling the final settlement contract // Verify the settle data has the correct function selector diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 3413c74..6571442 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -46,11 +46,7 @@ contract TestWrapper is CowWrapper { constructor(ICowSettlement settlement_) CowWrapper(settlement_) {} function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { - _internalSettle(settleData, remainingWrapperData); - } - - function exposedInternalSettle(bytes calldata settleData, bytes calldata wrapperData) external { - _internalSettle(settleData, wrapperData); + _next(settleData, remainingWrapperData); } } @@ -102,7 +98,7 @@ contract CowWrapperTest is Test { ); } - function test_internalSettle_CallsWrapperAndThenNextSettlement() public { + function test_next_CallsWrapperAndThenNextSettlement() public { bytes memory settleData = abi.encodePacked(_createSimpleSettleData(1), hex"123456"); bytes memory secondCallWrapperData = hex"0003098765"; bytes memory wrapperData = abi.encodePacked(hex"00021234", address(testWrapper), secondCallWrapperData); diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index 43fabce..99a2aca 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -10,6 +10,6 @@ contract EmptyWrapper is CowWrapper { constructor(ICowSettlement settlement_) CowWrapper(settlement_) {} function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { - _internalSettle(settleData, remainingWrapperData); + _next(settleData, remainingWrapperData); } } From c4f4aeab35cd9b755c42ef4a5d36184cd52c53cb Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 09:22:38 +0900 Subject: [PATCH 39/43] refactor tests --- test/{ => unit}/CowWrapper.t.sol | 110 +++++++++------------------- test/unit/mocks/MockCowProtocol.sol | 68 +++++++++++++++++ 2 files changed, 101 insertions(+), 77 deletions(-) rename test/{ => unit}/CowWrapper.t.sol (58%) create mode 100644 test/unit/mocks/MockCowProtocol.sol diff --git a/test/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol similarity index 58% rename from test/CowWrapper.t.sol rename to test/unit/CowWrapper.t.sol index 6571442..c5d104c 100644 --- a/test/CowWrapper.t.sol +++ b/test/unit/CowWrapper.t.sol @@ -2,121 +2,77 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapper, ICowSettlement, ICowAuthentication} from "../src/CowWrapper.sol"; -import {IERC20, GPv2Trade, GPv2Interaction} from "cow/GPv2Settlement.sol"; -import {EmptyWrapper} from "./EmptyWrapper.sol"; +import {CowWrapper, ICowSettlement, ICowAuthentication} from "../../src/CowWrapper.sol"; +import {EmptyWrapper} from "../EmptyWrapper.sol"; -import {CowWrapperHelpers} from "./helpers/CowWrapperHelpers.sol"; +import {MockWrapper, MockCowSettlement, MockCowAuthentication} from "./mocks/MockCowProtocol.sol"; -contract MockAuthentication { - mapping(address => bool) public solvers; - - function addSolver(address solver) external { - solvers[solver] = true; - } - - function isSolver(address solver) external view returns (bool) { - return solvers[solver]; - } -} - -contract MockSettlement { - ICowAuthentication private immutable AUTHENTICATOR; - - constructor(ICowAuthentication authenticator_) { - AUTHENTICATOR = authenticator_; - } - - function authenticator() external view returns (ICowAuthentication) { - return AUTHENTICATOR; - } - - function settle( - IERC20[] calldata, - uint256[] calldata, - GPv2Trade.Data[] calldata, - GPv2Interaction.Data[][3] calldata - ) external {} -} - -// Test wrapper that exposes internal functions -contract TestWrapper is CowWrapper { - string public override name = "Test Wrapper"; - - constructor(ICowSettlement settlement_) CowWrapper(settlement_) {} - - function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { - _next(settleData, remainingWrapperData); - } -} +import {CowWrapperHelpers} from "../helpers/CowWrapperHelpers.sol"; contract CowWrapperTest is Test { - MockAuthentication public authenticator; - MockSettlement public mockSettlement; + MockCowAuthentication public authenticator; + MockCowSettlement public mockSettlement; address public solver; - TestWrapper public testWrapper; - EmptyWrapper private wrapper1; - EmptyWrapper private wrapper2; - EmptyWrapper private wrapper3; + MockWrapper private wrapper1; + MockWrapper private wrapper2; + MockWrapper private wrapper3; function setUp() public { // Deploy mock contracts - authenticator = new MockAuthentication(); - mockSettlement = new MockSettlement(ICowAuthentication(address(authenticator))); + authenticator = new MockCowAuthentication(); + mockSettlement = new MockCowSettlement(address(authenticator)); solver = makeAddr("solver"); // Add solver to the authenticator - authenticator.addSolver(solver); + authenticator.setSolver(solver, true); // Create test wrapper and three EmptyWrapper instances with the settlement contract - testWrapper = new TestWrapper(ICowSettlement(address(mockSettlement))); - wrapper1 = new EmptyWrapper(ICowSettlement(address(mockSettlement))); - wrapper2 = new EmptyWrapper(ICowSettlement(address(mockSettlement))); - wrapper3 = new EmptyWrapper(ICowSettlement(address(mockSettlement))); + wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); + wrapper2 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); + wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); // Add all wrappers as solvers - authenticator.addSolver(address(testWrapper)); - authenticator.addSolver(address(wrapper1)); - authenticator.addSolver(address(wrapper2)); - authenticator.addSolver(address(wrapper3)); + authenticator.setSolver(address(wrapper1), true); + authenticator.setSolver(address(wrapper2), true); + authenticator.setSolver(address(wrapper3), true); } - function _emptyInteractions() private pure returns (GPv2Interaction.Data[][3] memory) { - return [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + function _emptyInteractions() private pure returns (ICowSettlement.Interaction[][3] memory) { + return [new ICowSettlement.Interaction[](0), new ICowSettlement.Interaction[](0), new ICowSettlement.Interaction[](0)]; } function _createSimpleSettleData(uint256 tokenCount) private returns (bytes memory) { - IERC20[] memory tokens = new IERC20[](tokenCount); + address[] memory tokens = new address[](tokenCount); uint256[] memory clearingPrices = new uint256[](tokenCount); for (uint256 i = 0; i < tokenCount; i++) { - tokens[i] = IERC20(makeAddr(string(abi.encodePacked("Settle Token #", vm.toString(i + 1))))); + tokens[i] = makeAddr(string(abi.encodePacked("Settle Token #", vm.toString(i + 1)))); clearingPrices[i] = 100 * (i + 1); } return abi.encodeWithSelector( - ICowSettlement.settle.selector, tokens, clearingPrices, new GPv2Trade.Data[](0), _emptyInteractions() + ICowSettlement.settle.selector, tokens, clearingPrices, new ICowSettlement.Trade[](0), _emptyInteractions() ); } function test_next_CallsWrapperAndThenNextSettlement() public { bytes memory settleData = abi.encodePacked(_createSimpleSettleData(1), hex"123456"); bytes memory secondCallWrapperData = hex"0003098765"; - bytes memory wrapperData = abi.encodePacked(hex"00021234", address(testWrapper), secondCallWrapperData); + bytes memory wrapperData = abi.encodePacked(hex"00021234", address(wrapper1), secondCallWrapperData); // the wrapper gets called exactly twice (once below and again inside the wrapper data calling self) - vm.expectCall(address(testWrapper), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 2); + vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 2); // verify the internal wrapper call data vm.expectCall( - address(testWrapper), - abi.encodeWithSelector(testWrapper.wrappedSettle.selector, settleData, secondCallWrapperData) + address(wrapper1), + abi.encodeWithSelector(wrapper1.wrappedSettle.selector, settleData, secondCallWrapperData) ); // the settlement contract gets called once after wrappers (including the surplus data at the end) vm.expectCall(address(mockSettlement), 0, settleData, 1); vm.prank(solver); - testWrapper.wrappedSettle(settleData, wrapperData); + wrapper1.wrappedSettle(settleData, wrapperData); } function test_wrappedSettle_RevertsWithNotASolver() public { @@ -126,7 +82,7 @@ contract CowWrapperTest is Test { // Should revert when called by non-solver vm.prank(notASolver); vm.expectRevert(abi.encodeWithSelector(CowWrapper.NotASolver.selector, notASolver)); - testWrapper.wrappedSettle(settleData, hex""); + wrapper1.wrappedSettle(settleData, hex""); } function test_wrappedSettle_RevertsOnInvalidSettleSelector() public { @@ -134,7 +90,7 @@ contract CowWrapperTest is Test { bytes memory wrapperData = hex"0000"; // Empty wrapper data, goes straight to settlement vm.prank(solver); vm.expectRevert(abi.encodeWithSelector(CowWrapper.InvalidSettleData.selector, settleData)); - testWrapper.wrappedSettle(settleData, wrapperData); + wrapper1.wrappedSettle(settleData, wrapperData); } function test_integration_ThreeWrappersChained() public { @@ -184,9 +140,9 @@ contract CowWrapperTest is Test { CowWrapperHelpers.encodeWrapperCall(wrappers, datas, address(mockSettlement), settlement); // all the wrappers gets called, with wrapper 1 called twice - vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 2); - vm.expectCall(address(wrapper2), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 1); - vm.expectCall(address(wrapper3), 0, abi.encodeWithSelector(testWrapper.wrappedSettle.selector), 1); + vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 2); + vm.expectCall(address(wrapper2), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 1); + vm.expectCall(address(wrapper3), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 1); // the settlement gets called with the full data vm.expectCall(address(mockSettlement), new bytes(0)); diff --git a/test/unit/mocks/MockCowProtocol.sol b/test/unit/mocks/MockCowProtocol.sol new file mode 100644 index 0000000..fa37bb0 --- /dev/null +++ b/test/unit/mocks/MockCowProtocol.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8; + +import {ICowSettlement, ICowAuthentication, CowWrapper} from "../../../src/CowWrapper.sol"; + +/// @title MockICowAuthentication +/// @notice Mock implementation of CoW Protocol authenticator for unit testing +contract MockCowAuthentication is ICowAuthentication { + mapping(address => bool) public solvers; + + function setSolver(address solver, bool authorized) external { + solvers[solver] = authorized; + } + + function isSolver(address prospectiveSolver) external view override returns (bool) { + return solvers[prospectiveSolver]; + } +} + +/// @title MockICowSettlement +/// @notice Mock implementation of CoW Protocol settlement contract for unit testing +contract MockCowSettlement is ICowSettlement { + ICowAuthentication public immutable AUTH; + bool public shouldSucceed = true; + + constructor(address _auth) { + AUTH = ICowAuthentication(_auth); + } + + function authenticator() external view override returns (ICowAuthentication) { + return AUTH; + } + + function vaultRelayer() external pure returns (address) { + return address(0x7777); + } + + function domainSeparator() external pure returns (bytes32) { + return keccak256("MockDomainSeparator"); + } + + function setPreSignature(bytes calldata, bool) external pure {} + + function settle(address[] calldata, uint256[] calldata, Trade[] calldata, Interaction[][3] calldata) + external + view + override + { + require(shouldSucceed, "MockICowSettlement: settle failed"); + } + + function setSuccessfulSettle(bool success) external { + shouldSucceed = success; + } +} + +contract MockWrapper is CowWrapper { + string public constant name = "Mock Wrapper"; + uint256 public consumeBytes; + + constructor(ICowSettlement settlement_, uint256 consumeBytes_) CowWrapper(settlement_) { + consumeBytes = consumeBytes_; + } + + function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { + _next(settleData, remainingWrapperData); + } +} From 9bac22fd7eadc1f0841d6b860f2b27aec3a15957 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 23:43:39 +0900 Subject: [PATCH 40/43] forge fmt --- test/unit/CowWrapper.t.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/unit/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol index c5d104c..e15f0a5 100644 --- a/test/unit/CowWrapper.t.sol +++ b/test/unit/CowWrapper.t.sol @@ -39,7 +39,12 @@ contract CowWrapperTest is Test { } function _emptyInteractions() private pure returns (ICowSettlement.Interaction[][3] memory) { - return [new ICowSettlement.Interaction[](0), new ICowSettlement.Interaction[](0), new ICowSettlement.Interaction[](0)]; + return + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ]; } function _createSimpleSettleData(uint256 tokenCount) private returns (bytes memory) { From 5e3e4ee9e6f826ff5cd3d6e0405cb1a267d410e2 Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 10:24:54 +0900 Subject: [PATCH 41/43] pull in upstream requirements these changes are ultimately requierd for use in the wrappers later, and they are generally good to have for any wrappers in the future. --- src/CowWrapper.sol | 15 ++++++++++++++- test/unit/mocks/MockCowProtocol.sol | 14 +++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index 0a91bde..42e6621 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -49,7 +49,16 @@ interface ICowSettlement { } /// @notice Returns the authentication contract used by the settlement contract. - function authenticator() external returns (ICowAuthentication); + function authenticator() external view returns (ICowAuthentication); + + /// @notice Returns the address of the vaultRelayer, the target for approvals for funds entering the settlement contract. + function vaultRelayer() external view returns (address); + + /// @notice Returns the domain separator for EIP-712 signing + function domainSeparator() external view returns (bytes32); + + /// @notice Allows for approval of orders by submitting an authorized hash on-chain prior to order execution. + function setPreSignature(bytes calldata orderUid, bool signed) external; /// @notice Settles a batch of trades atomically /// @param tokens Array of token addresses involved in the settlement @@ -71,6 +80,10 @@ interface ICowWrapper { /// @notice A human readable label for this wrapper. Used for display in explorer/analysis UIs function name() external view returns (string memory); + /// @notice The settlement contract used by this wrapper + /// @return The CowSettlement contract address + function SETTLEMENT() external view returns (ICowSettlement); + /// @notice Initiates a wrapped settlement call /// @dev This is the entry point for wrapped settlements. The wrapper will execute custom logic /// before calling the next wrapper or settlement contract in the chain. diff --git a/test/unit/mocks/MockCowProtocol.sol b/test/unit/mocks/MockCowProtocol.sol index fa37bb0..aaffbd9 100644 --- a/test/unit/mocks/MockCowProtocol.sol +++ b/test/unit/mocks/MockCowProtocol.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8; import {ICowSettlement, ICowAuthentication, CowWrapper} from "../../../src/CowWrapper.sol"; -/// @title MockICowAuthentication +/// @title MockCowAuthentication /// @notice Mock implementation of CoW Protocol authenticator for unit testing contract MockCowAuthentication is ICowAuthentication { mapping(address => bool) public solvers; @@ -17,7 +17,7 @@ contract MockCowAuthentication is ICowAuthentication { } } -/// @title MockICowSettlement +/// @title MockCowSettlement /// @notice Mock implementation of CoW Protocol settlement contract for unit testing contract MockCowSettlement is ICowSettlement { ICowAuthentication public immutable AUTH; @@ -31,22 +31,22 @@ contract MockCowSettlement is ICowSettlement { return AUTH; } - function vaultRelayer() external pure returns (address) { + function vaultRelayer() external pure override returns (address) { return address(0x7777); } - function domainSeparator() external pure returns (bytes32) { + function domainSeparator() external pure override returns (bytes32) { return keccak256("MockDomainSeparator"); } - function setPreSignature(bytes calldata, bool) external pure {} + function setPreSignature(bytes calldata, bool) external pure override {} function settle(address[] calldata, uint256[] calldata, Trade[] calldata, Interaction[][3] calldata) external view override { - require(shouldSucceed, "MockICowSettlement: settle failed"); + require(shouldSucceed, "MockCowSettlement: settle failed"); } function setSuccessfulSettle(bool success) external { @@ -55,7 +55,7 @@ contract MockCowSettlement is ICowSettlement { } contract MockWrapper is CowWrapper { - string public constant name = "Mock Wrapper"; + string public override name = "Mock Wrapper"; uint256 public consumeBytes; constructor(ICowSettlement settlement_, uint256 consumeBytes_) CowWrapper(settlement_) { From d828cdbf2f94da42de49fd18325c2e71fd52aa54 Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 16:14:31 +0900 Subject: [PATCH 42/43] fix comments from federico * add minor edge case checks (found a bug in max length!) * better documentation * clean up usages of `abi.encodeWithSelector` as appropriate * fix licenses --- src/CowWrapper.sol | 19 ++- test/EmptyWrapper.sol | 3 +- test/unit/CowWrapper.t.sol | 189 +++++++++++++++++++++++----- test/unit/mocks/MockCowProtocol.sol | 2 +- 4 files changed, 166 insertions(+), 47 deletions(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index 42e6621..0fb5f41 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.7.6 <0.9.0; -pragma abicoder v2; +pragma solidity >=0.8.0 <0.9.0; /** - * @title CoW Wrapper all-in-one integration file - * @author CoW Protocol Developers - * @notice This file is completely self-contained (ie no dependencies) and can be portably copied to whatever projects it is needed. + * CoW Wrapper all-in-one integration file + * CoW Protocol Developers + * This file is completely self-contained (ie no dependencies) and can be portably copied to whatever projects it is needed. * It contains: * * CowWrapper -- an abstract base contract which should be inherited by all wrappers * * ICowWrapper -- the required interface for all wrappers @@ -41,7 +40,7 @@ interface ICowSettlement { bytes signature; } - /// @notice Interaction data structure for pre/intra/post-settlement hooks + /// @notice Interaction data structure for pre/intra/post-settlement actions which are supplied by the solver to complete the user request struct Interaction { address target; uint256 value; @@ -89,7 +88,7 @@ interface ICowWrapper { /// before calling the next wrapper or settlement contract in the chain. /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. - /// Format: [2-byte len][wrapper-specific-data][next-address]([2-byte len][wrapper-specific-data][next-address]...) + /// Format: [2-byte len][user-supplied wrapper specific data][next address]([2-byte len][user supplied wrapper specific data]...) function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external; } @@ -127,9 +126,9 @@ abstract contract CowWrapper is ICowWrapper { /// @notice Initiates a wrapped settlement call /// @dev Entry point for solvers to execute wrapped settlements. Verifies the caller is a solver, /// validates wrapper data, then delegates to _wrap() for custom logic. - /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data + /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data. NOTE: wrappers may read this data, but it should not be trusted for anything of great importance (ex. destination of funds) because a malicious solver can modify this data later in the chain. /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. - /// Format: [2-byte len][wrapper-specific-data][next-address]([2-byte len][wrapper-specific-data][next-address]...) + /// Format: [2-byte len][user-supplied wrapper specific data][next address]([2-byte len][user supplied wrapper specific data]...) function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external { // Revert if not a valid solver require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); @@ -137,7 +136,7 @@ abstract contract CowWrapper is ICowWrapper { // Find out how long the next wrapper data is supposed to be // We use 2 bytes to decode the length of the wrapper data because it allows for up to 64KB of data for each wrapper. // This should be plenty of length for all identified use-cases of wrappers in the forseeable future. - uint16 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2])); + uint256 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2])); // Delegate to the wrapper's custom logic uint256 remainingWrapperDataStart = 2 + nextWrapperDataLen; diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index 99a2aca..325535c 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -1,6 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8; -pragma abicoder v2; import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; diff --git a/test/unit/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol index e15f0a5..e950b21 100644 --- a/test/unit/CowWrapper.t.sol +++ b/test/unit/CowWrapper.t.sol @@ -1,8 +1,8 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapper, ICowSettlement, ICowAuthentication} from "../../src/CowWrapper.sol"; +import {CowWrapper, ICowWrapper, ICowSettlement, ICowAuthentication} from "../../src/CowWrapper.sol"; import {EmptyWrapper} from "../EmptyWrapper.sol"; import {MockWrapper, MockCowSettlement, MockCowAuthentication} from "./mocks/MockCowProtocol.sol"; @@ -28,9 +28,11 @@ contract CowWrapperTest is Test { authenticator.setSolver(solver, true); // Create test wrapper and three EmptyWrapper instances with the settlement contract - wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); - wrapper2 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); - wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); + // (use type(uint16).max because it will force consuming all the wrapper data, which is + // most useful for these tests) + wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), type(uint16).max); + wrapper2 = new MockWrapper(ICowSettlement(address(mockSettlement)), type(uint16).max); + wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), type(uint16).max); // Add all wrappers as solvers authenticator.setSolver(address(wrapper1), true); @@ -51,27 +53,39 @@ contract CowWrapperTest is Test { address[] memory tokens = new address[](tokenCount); uint256[] memory clearingPrices = new uint256[](tokenCount); for (uint256 i = 0; i < tokenCount; i++) { - tokens[i] = makeAddr(string(abi.encodePacked("Settle Token #", vm.toString(i + 1)))); + tokens[i] = makeAddr(string.concat("Settle Token #", vm.toString(i + 1))); clearingPrices[i] = 100 * (i + 1); } - return abi.encodeWithSelector( - ICowSettlement.settle.selector, tokens, clearingPrices, new ICowSettlement.Trade[](0), _emptyInteractions() + return abi.encodeCall( + ICowSettlement.settle, (tokens, clearingPrices, new ICowSettlement.Trade[](0), _emptyInteractions()) + ); + } + + function test_verifyInitialState() public { + assertEq( + address(wrapper1.SETTLEMENT()), + address(mockSettlement), + "Settlement contract should be initialized correctly" + ); + assertEq( + address(wrapper1.AUTHENTICATOR()), + address(authenticator), + "Authenticator contract should be initialized from the settlement contract" ); } function test_next_CallsWrapperAndThenNextSettlement() public { bytes memory settleData = abi.encodePacked(_createSimpleSettleData(1), hex"123456"); - bytes memory secondCallWrapperData = hex"0003098765"; - bytes memory wrapperData = abi.encodePacked(hex"00021234", address(wrapper1), secondCallWrapperData); + // here we encode [2-byte len] followed by the actual wrapper data (which is 3 bytes, 6 chars hex) + bytes memory secondCallWrapperData = abi.encodePacked(uint16(3), hex"098765"); + // here we encode [2-byte len] followed by the actual wrapper data (which is 2 bytes, 4 chars hex), and build the chain + bytes memory wrapperData = abi.encodePacked(uint16(2), hex"1234", address(wrapper1), secondCallWrapperData); - // the wrapper gets called exactly twice (once below and again inside the wrapper data calling self) - vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 2); + // verify the outside wrapper call data + vm.expectCall(address(wrapper1), abi.encodeCall(ICowWrapper.wrappedSettle, (settleData, wrapperData))); // verify the internal wrapper call data - vm.expectCall( - address(wrapper1), - abi.encodeWithSelector(wrapper1.wrappedSettle.selector, settleData, secondCallWrapperData) - ); + vm.expectCall(address(wrapper1), abi.encodeCall(ICowWrapper.wrappedSettle, (settleData, secondCallWrapperData))); // the settlement contract gets called once after wrappers (including the surplus data at the end) vm.expectCall(address(mockSettlement), 0, settleData, 1); @@ -108,20 +122,22 @@ contract CowWrapperTest is Test { settlement.clearingPrices[0] = 100; settlement.clearingPrices[1] = 200; - settlement.trades = new ICowSettlement.Trade[](1); - settlement.trades[0] = ICowSettlement.Trade({ - sellTokenIndex: 0, - buyTokenIndex: 1, - receiver: address(0x123), - sellAmount: 1000, - buyAmount: 900, - validTo: uint32(block.timestamp + 1000), - appData: bytes32(uint256(1)), - feeAmount: 10, - flags: 0, - executedAmount: 0, - signature: hex"aabbccddee" - }); + settlement.trades = new ICowSettlement.Trade[](10); + for (uint256 i = 0; i < 10; i++) { + settlement.trades[i] = ICowSettlement.Trade({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: address(0x123), + sellAmount: 1000 * i, + buyAmount: 900 * i, + validTo: uint32(block.timestamp + 1000), + appData: bytes32(uint256(1)), + feeAmount: 10, + flags: 0, + executedAmount: 0, + signature: hex"aabbccddee" + }); + } settlement.interactions = [ new ICowSettlement.Interaction[](0), @@ -144,10 +160,13 @@ contract CowWrapperTest is Test { (address target, bytes memory fullCalldata) = CowWrapperHelpers.encodeWrapperCall(wrappers, datas, address(mockSettlement), settlement); - // all the wrappers gets called, with wrapper 1 called twice - vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 2); - vm.expectCall(address(wrapper2), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 1); - vm.expectCall(address(wrapper3), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 1); + // all the wrappers get called, with wrapper 1 called twice + + // we only want to verify that wrappedSettle was called. (not the specific data passed to wrappedSettle) + bytes memory wrappedSettleSelector = abi.encodePacked(ICowWrapper.wrappedSettle.selector); + vm.expectCall(address(wrapper1), 0, wrappedSettleSelector, 2); + vm.expectCall(address(wrapper2), 0, wrappedSettleSelector, 1); + vm.expectCall(address(wrapper3), 0, wrappedSettleSelector, 1); // the settlement gets called with the full data vm.expectCall(address(mockSettlement), new bytes(0)); @@ -157,4 +176,106 @@ contract CowWrapperTest is Test { (bool success,) = target.call(fullCalldata); assertTrue(success, "Chained wrapper call should succeed"); } + + function test_wrappedSettle_RevertsOnZeroLengthWrapperData() public { + bytes memory settleData = _createSimpleSettleData(0); + bytes memory wrapperData = hex""; // Completely empty wrapper data + + vm.prank(solver); + vm.expectRevert(); // Should revert with out-of-bounds array access + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsOnOneByteWrapperData() public { + bytes memory settleData = _createSimpleSettleData(0); + bytes memory wrapperData = hex"01"; // Only 1 byte - not enough to read the 2-byte length + + vm.prank(solver); + vm.expectRevert(); // Should revert with out-of-bounds array access + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_SucceedsWithZeroLengthIndicator() public { + bytes memory settleData = _createSimpleSettleData(0); + bytes memory wrapperData = hex"0000"; // 2 bytes indicating 0-length wrapper data + + // Should call settlement directly with no wrapper-specific data + vm.expectCall(address(mockSettlement), 0, settleData, 1); + + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_SucceedsWithMaximumLengthWrapperData() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Create maximum length wrapper data (65535 bytes) + // Format: [2-byte length = 0xFFFF][65535 bytes of data] + bytes memory maxData = new bytes(65535); + for (uint256 i = 0; i < 65535; i++) { + maxData[i] = bytes1(uint8(i % 256)); + } + + bytes memory wrapperData = abi.encodePacked(uint16(65535), maxData); + + // Should successfully parse the maximum length data and call settlement + vm.expectCall(address(mockSettlement), 0, settleData, 1); + + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsWhenDataShorterThanIndicated() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Wrapper data claims to be 100 bytes but only provides 50 + bytes memory shortData = new bytes(50); + bytes memory wrapperData = abi.encodePacked(uint16(100), shortData); + + vm.prank(solver); + vm.expectRevert(); // Should revert with out-of-bounds array access + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_SucceedsWithMaxLengthAndNextWrapper() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Create maximum length wrapper data followed by next wrapper address + bytes memory maxData = new bytes(65535); + for (uint256 i = 0; i < 65535; i++) { + maxData[i] = bytes1(uint8(i % 256)); + } + + // Format: [2-byte length = 0xFFFF][65535 bytes of data][20-byte next wrapper address][remaining data] + bytes memory nextWrapperData = hex"00030000FF"; // 3 bytes of data for next wrapper + bytes memory wrapperData = abi.encodePacked(type(uint16).max, maxData, address(wrapper2), nextWrapperData); + + // Should call wrapper2 with the remaining data + vm.expectCall(address(wrapper2), 0, abi.encodePacked(ICowWrapper.wrappedSettle.selector), 1); + + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsWithInsufficientLengthData() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Format: [1-byte length = 1 (insufficient)] + bytes memory wrapperData = hex"01"; + + vm.expectRevert(new bytes(0)); + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsWithInsufficientCallData() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Format: [2-byte length = 0xa][9 bytes of data (insufficient)] + bytes memory wrapperData = hex"000A123412341234123412"; + + vm.expectRevert(new bytes(0)); + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } } diff --git a/test/unit/mocks/MockCowProtocol.sol b/test/unit/mocks/MockCowProtocol.sol index aaffbd9..5d774f2 100644 --- a/test/unit/mocks/MockCowProtocol.sol +++ b/test/unit/mocks/MockCowProtocol.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8; import {ICowSettlement, ICowAuthentication, CowWrapper} from "../../../src/CowWrapper.sol"; From daf68fc805595bc07eaf072306d2e514802b18ed Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 16:30:09 +0900 Subject: [PATCH 43/43] impl comment suggestion also switch to @inheritdoc --- src/CowWrapper.sol | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index 0fb5f41..5828272 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -87,8 +87,22 @@ interface ICowWrapper { /// @dev This is the entry point for wrapped settlements. The wrapper will execute custom logic /// before calling the next wrapper or settlement contract in the chain. /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data - /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. - /// Format: [2-byte len][user-supplied wrapper specific data][next address]([2-byte len][user supplied wrapper specific data]...) + /// @dev SECURITY: `settleData` is NOT guaranteed to remain unchanged through the wrapper chain. + /// Intermediate wrappers could modify it before passing it along. Do not rely on + /// `settleData` validation for security-critical checks. + /// @param wrapperData Encoded wrapper chain with the following format: + /// Structure: [uint16 len1][bytes data1][address wrapper2][uint16 len2][bytes data2][address wrapper3]... + /// + /// Each wrapper in the chain consists of: + /// - 2 bytes: uint16 length of wrapper-specific data + /// - `length` bytes: wrapper-specific data for this wrapper + /// - 20 bytes: address of next wrapper (omitted for the final wrapper) + /// + /// The final wrapper in the chain omits the next wrapper address and calls SETTLEMENT directly. + /// + /// Example: [0x0005][0xAABBCCDDEE][0x1234...ABCD][0x0003][0x112233] + /// ↑len ↑data ↑next wrapper ↑len ↑data (final, no next address) + /// function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external; } @@ -123,12 +137,7 @@ abstract contract CowWrapper is ICowWrapper { AUTHENTICATOR = settlement_.authenticator(); } - /// @notice Initiates a wrapped settlement call - /// @dev Entry point for solvers to execute wrapped settlements. Verifies the caller is a solver, - /// validates wrapper data, then delegates to _wrap() for custom logic. - /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data. NOTE: wrappers may read this data, but it should not be trusted for anything of great importance (ex. destination of funds) because a malicious solver can modify this data later in the chain. - /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. - /// Format: [2-byte len][user-supplied wrapper specific data][next address]([2-byte len][user supplied wrapper specific data]...) + /// @inheritdoc ICowWrapper function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external { // Revert if not a valid solver require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender));