From a78f94a0cfea1378894adb063d94ee0f64488c99 Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 9 Oct 2025 15:43:50 +0900 Subject: [PATCH 01/12] feat: CowWrapper helpers A new contract which can be used to help solvers and other integrators work with wrappers from offchain --- src/vendor/CowWrapper.sol | 12 +++++ src/vendor/CowWrapperHelpers.sol | 72 ++++++++++++++++++++++++++++++ test/CowWrapper.t.sol | 30 +++++++++---- test/EmptyWrapper.sol | 5 +++ test/helpers/CowWrapperHelpers.sol | 44 ------------------ 5 files changed, 111 insertions(+), 52 deletions(-) create mode 100644 src/vendor/CowWrapperHelpers.sol delete mode 100644 test/helpers/CowWrapperHelpers.sol diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 96634d9..650a6a6 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -43,6 +43,12 @@ interface ICowWrapper { bytes calldata settleData, bytes calldata wrapperData ) external; + + /** + * @dev Parses the wrapper data for this wrapper. + * @return remainingWrapperData Any wrapper data that was not read in order to parse the wrapper data. + */ + function parseWrapperData(bytes calldata wrapperData) external view returns (bytes calldata remainingWrapperData); } /** @@ -88,6 +94,12 @@ abstract contract CowWrapper { _wrap(settleData, wrapperData); } + /** + * @dev Parses the wrapper data for this wrapper. + * @return remainingWrapperData Any wrapper data that was not read in order to parse the wrapper data. + */ + function parseWrapperData(bytes calldata wrapperData) external virtual view returns (bytes calldata remainingWrapperData); + /** * @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 */ diff --git a/src/vendor/CowWrapperHelpers.sol b/src/vendor/CowWrapperHelpers.sol new file mode 100644 index 0000000..30681d0 --- /dev/null +++ b/src/vendor/CowWrapperHelpers.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.7.6 <0.9.0; +pragma abicoder v2; + +import "forge-std/console.sol"; + +import {GPv2Authentication, ICowWrapper} from "./CowWrapper.sol"; + +/** + * A helper contract which provides `view` functions for working with wrappers. + * @dev This contract is not designed to be gas-efficient, and is intended for off-chain use only. + */ +contract CowWrapperHelpers { + error InvalidInputLengths(uint256 wrappersLength, uint256 individualWrapperDatasLength); + error NotAWrapper(uint256 wrapperIndex, address unauthorized, address authenticatorContract); + error WrapperDataNotFullyConsumed(uint256 wrapperIndex, bytes remainingWrapperData); + error WrapperDataMalformed(uint256 wrapperIndex, bytes wrapperError); + error SettlementContractShouldNotBeSolver(address settlementContract, address authenticatorContract); + + GPv2Authentication public immutable WRAPPER_AUTHENTICATOR; + GPv2Authentication public immutable SOLVER_AUTHENTICATOR; + + constructor(GPv2Authentication wrapperAuthenticator_, GPv2Authentication solverAuthenticator_) { + // retrieve the authentication we are supposed to use from the settlement contract + WRAPPER_AUTHENTICATOR = wrapperAuthenticator_; + SOLVER_AUTHENTICATOR = solverAuthenticator_; + } + + function verifyAndBuildWrapperData(address[] calldata wrapperAddresses, bytes[] calldata individualWrapperDatas, address settlementContract) external view returns (bytes memory wrapperData) { + // Basic Sanity: Input arrays should have correct length + if (wrapperAddresses.length != individualWrapperDatas.length) { + revert InvalidInputLengths(wrapperAddresses.length, individualWrapperDatas.length); + } + + for (uint256 i = 0;i < wrapperAddresses.length;i++) { + // Wrapper must be authorized + if (!WRAPPER_AUTHENTICATOR.isSolver(wrapperAddresses[i])) { + revert NotAWrapper(i, wrapperAddresses[i], address(WRAPPER_AUTHENTICATOR)); + } + + + // The wrapper data must be parsable and fully consumed + try ICowWrapper(wrapperAddresses[i]).parseWrapperData(individualWrapperDatas[i]) returns (bytes memory remainingWrapperData) { + if (remainingWrapperData.length > 0) { + revert WrapperDataNotFullyConsumed(i, remainingWrapperData); + } + } catch (bytes memory err) { + revert WrapperDataMalformed(i, err); + } + } + + // The Settlement Contract should not be a solver + if (SOLVER_AUTHENTICATOR.isSolver(settlementContract)) { + revert SettlementContractShouldNotBeSolver(settlementContract, address(SOLVER_AUTHENTICATOR)); + } + + uint256 totalIndividualWrapperDatasLength = 0; + for (uint256 i = 0;i < individualWrapperDatas.length;i++) { + totalIndividualWrapperDatasLength += individualWrapperDatas[i].length; + } + + wrapperData = abi.encodePacked(individualWrapperDatas[0]); + + for (uint256 i = 0;i < individualWrapperDatas.length;i++) { + wrapperData = abi.encodePacked(wrapperData, wrapperAddresses[i], individualWrapperDatas[i]); + } + + wrapperData = abi.encodePacked(wrapperData, settlementContract); + + return wrapperData; + } +} diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 7ed3d02..dff6d51 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -6,7 +6,7 @@ import {CowWrapper, CowSettlement, GPv2Authentication} from "../src/vendor/CowWr import {IERC20, GPv2Trade, GPv2Interaction} from "cow/GPv2Settlement.sol"; import {EmptyWrapper} from "./EmptyWrapper.sol"; -import {CowWrapperHelpers} from "./helpers/CowWrapperHelpers.sol"; +import {CowWrapperHelpers} from "../src/vendor/CowWrapperHelpers.sol"; import "forge-std/console.sol"; @@ -27,6 +27,7 @@ contract MockSettlement { IERC20[] tokens; uint256[] clearingPrices; GPv2Trade.Data[] trades; + GPv2Interaction.Data[][3] interactions; bytes additionalData; } @@ -36,11 +37,16 @@ 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; call_.clearingPrices = clearingPrices; + for (uint256 i = 0;i < 3;i++) { + for (uint256 j = 0;j < interactions[i].length;i++) { + call_.interactions[i].push(interactions[i][j]); + } + } for (uint256 i = 0; i < trades.length; i++) { call_.trades.push(trades[i]); @@ -81,6 +87,7 @@ contract MockSettlement { contract CowWrapperTest is Test, CowWrapper { MockAuthentication public authenticator; MockSettlement public mockSettlement; + CowWrapperHelpers helpers; address public solver; EmptyWrapper private wrapper1; @@ -111,6 +118,7 @@ contract CowWrapperTest is Test, CowWrapper { MockAuthentication(address(0)).addSolver(solver); mockSettlement = new MockSettlement(); + helpers = new CowWrapperHelpers(GPv2Authentication(address(0)), GPv2Authentication(address(0))); // Create three EmptyWrapper instances wrapper1 = new EmptyWrapper(GPv2Authentication(address(0))); @@ -137,6 +145,11 @@ contract CowWrapperTest is Test, CowWrapper { _internalSettle(settleData, wrapperData); } + function parseWrapperData(bytes calldata wrapperData) external pure override returns (bytes calldata remainingWrapperData) { + // CowWrapperTest doesn't consume any wrapper data + return wrapperData; + } + function test_wrap_ReceivesCorrectParameters() public { IERC20[] memory tokens = new IERC20[](1); tokens[0] = IERC20(address(0x1)); @@ -230,7 +243,7 @@ contract CowWrapperTest is Test, CowWrapper { } function test_integration_ThreeWrappersChained() public { - CowWrapperHelpers.SettleCall memory settlement; + MockSettlement.SettleCall memory settlement; settlement.tokens = new IERC20[](2); settlement.tokens[0] = IERC20(address(0x1)); @@ -255,7 +268,7 @@ contract CowWrapperTest is Test, CowWrapper { signature: hex"aabbccddee" }); - settlement.interactions = + settlement.interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; // Build the chained wrapper data: @@ -268,13 +281,14 @@ contract CowWrapperTest is Test, CowWrapper { bytes[] memory wrapperDatas = new bytes[](3); - (address target, bytes memory fullCalldata) = - CowWrapperHelpers.encodeWrapperCall(wrappers, wrapperDatas, address(mockSettlement), settlement); + bytes memory settleData = abi.encodeCall(MockSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions)); + + bytes memory wrapperData = + helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, address(mockSettlement)); // Call wrapper1 as the solver vm.prank(solver); - (bool success,) = target.call(fullCalldata); - assertTrue(success, "Chained wrapper call should succeed"); + wrapper1.wrappedSettle(settleData, wrapperData); // Verify that mockSettlement was called assertEq(mockSettlement.getSettleCallCount(), 1, "MockSettlement should be called once"); diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index 2be39f5..d413e77 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -13,4 +13,9 @@ contract EmptyWrapper is CowWrapper { ) internal override { _internalSettle(settleData, wrappedData); } + + function parseWrapperData(bytes calldata wrapperData) external pure override returns (bytes calldata remainingWrapperData) { + // EmptyWrapper doesn't consume any wrapper data + return wrapperData; + } } diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol deleted file mode 100644 index 2bf43df..0000000 --- a/test/helpers/CowWrapperHelpers.sol +++ /dev/null @@ -1,44 +0,0 @@ -// 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"; - -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 fullCalldata) { - // Build the wrapper data chain - bytes memory wrapperData; - for (uint256 i = 0; i < wrappers.length; i++) { - wrapperData = abi.encodePacked( - wrapperData, - wrapperDatas[i], - (wrappers.length > i + 1 ? wrappers[i + 1] : cowSettlement) - ); - } - - // Build the settle calldata - 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); - - return (wrappers[0], fullCalldata); - } -} From 878ec3353a0d5275a610bb705854da6c95e0fca5 Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 9 Oct 2025 17:14:36 +0900 Subject: [PATCH 02/12] clean up, add lots of documentation to wrappers, verify test coverage --- src/vendor/CowWrapper.sol | 120 ++++++++++----- src/vendor/CowWrapperHelpers.sol | 57 +++++-- test/CowWrapper.t.sol | 59 ++++--- test/CowWrapperHelpers.t.sol | 254 +++++++++++++++++++++++++++++++ test/EmptyWrapper.sol | 2 +- 5 files changed, 423 insertions(+), 69 deletions(-) create mode 100644 test/CowWrapperHelpers.t.sol diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 650a6a6..4891013 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; @@ -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,44 +50,68 @@ 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 Parses the wrapper data for this wrapper. - * @return remainingWrapperData Any wrapper data that was not read in order to parse the wrapper data. - */ + /// @notice Parses and validates wrapper-specific data + /// @dev Used by CowWrapperHelpers to validate wrapper data before execution. + /// Implementations should consume their portion of wrapperData and return the rest. + /// @param wrapperData The wrapper-specific data to parse + /// @return remainingWrapperData Any wrapper data that was not consumed by this wrapper function parseWrapperData(bytes calldata wrapperData) external view returns (bytes calldata remainingWrapperData); } -/** - * @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 @@ -85,53 +121,67 @@ 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 Parses the wrapper data for this wrapper. - * @return remainingWrapperData Any wrapper data that was not read in order to parse the wrapper data. - */ + /// @notice Parses and validates wrapper-specific data + /// @dev Must be implemented by concrete wrapper contracts. Used for pre-execution validation. + /// The implementation should consume its wrapper-specific data and return the remainder. + /// @param wrapperData The full wrapper data to parse + /// @return remainingWrapperData The portion of wrapper data not consumed by this wrapper function parseWrapperData(bytes calldata wrapperData) external virtual view returns (bytes calldata remainingWrapperData); - /** - * @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); } } diff --git a/src/vendor/CowWrapperHelpers.sol b/src/vendor/CowWrapperHelpers.sol index 30681d0..1cf84f1 100644 --- a/src/vendor/CowWrapperHelpers.sol +++ b/src/vendor/CowWrapperHelpers.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; @@ -6,26 +6,63 @@ import "forge-std/console.sol"; import {GPv2Authentication, ICowWrapper} from "./CowWrapper.sol"; -/** - * A helper contract which provides `view` functions for working with wrappers. - * @dev This contract is not designed to be gas-efficient, and is intended for off-chain use only. - */ +/// @title CoW Wrapper Helpers +/// @notice Helper contract providing validation and encoding utilities for CoW Protocol wrapper chains +/// @dev This contract is not designed to be gas-efficient and is intended for off-chain use only. contract CowWrapperHelpers { + /// @notice Thrown when wrapper and wrapper data array lengths don't match + /// @param wrappersLength The length of the wrappers array + /// @param individualWrapperDatasLength The length of the wrapper data array error InvalidInputLengths(uint256 wrappersLength, uint256 individualWrapperDatasLength); + + /// @notice Thrown when a provided address is not an authenticated wrapper + /// @param wrapperIndex The index of the invalid wrapper in the array + /// @param unauthorized The address that is not authenticated as a wrapper + /// @param authenticatorContract The authentication contract that rejected the wrapper error NotAWrapper(uint256 wrapperIndex, address unauthorized, address authenticatorContract); + + /// @notice Thrown when a wrapper's parseWrapperData doesn't fully consume its data + /// @param wrapperIndex The index of the wrapper that didn't consume all its data + /// @param remainingWrapperData The data that was not consumed by the wrapper error WrapperDataNotFullyConsumed(uint256 wrapperIndex, bytes remainingWrapperData); + + /// @notice Thrown when a wrapper's parseWrapperData reverts, which is assumed to be due to malformed data + /// @param wrapperIndex The index of the wrapper with malformed data + /// @param wrapperError The error returned by the wrapper's parseWrapperData error WrapperDataMalformed(uint256 wrapperIndex, bytes wrapperError); + + /// @notice Thrown when the settlement contract is authenticated as a solver + /// @dev The settlement contract should not be a solver to prevent direct settlement calls bypassing wrappers + /// @param settlementContract The settlement contract address + /// @param authenticatorContract The authentication contract that authenticated the settlement as a solver error SettlementContractShouldNotBeSolver(address settlementContract, address authenticatorContract); + /// @notice The authentication contract used to verify wrapper contracts GPv2Authentication public immutable WRAPPER_AUTHENTICATOR; + + /// @notice The authentication contract used to verify solvers GPv2Authentication public immutable SOLVER_AUTHENTICATOR; + /// @notice Constructs a new CowWrapperHelpers contract + /// @param wrapperAuthenticator_ The GPv2Authentication contract used to verify wrapper contracts + /// @param solverAuthenticator_ The GPv2Authentication contract used to verify solvers constructor(GPv2Authentication wrapperAuthenticator_, GPv2Authentication solverAuthenticator_) { - // retrieve the authentication we are supposed to use from the settlement contract WRAPPER_AUTHENTICATOR = wrapperAuthenticator_; SOLVER_AUTHENTICATOR = solverAuthenticator_; } + /// @notice Validates a wrapper chain configuration and builds the properly formatted wrapper data + /// @dev Performs comprehensive validation of the wrapper chain before encoding: + /// 1. Verifies array lengths match + /// 2. Verifies each wrapper is authenticated via WRAPPER_AUTHENTICATOR + /// 3. Verifies each wrapper's data is valid and fully consumed by calling parseWrapperData + /// 4. Verifies the settlement contract is not authenticated as a solver + /// The returned wrapper data format is: [data0][addr1][data1][addr2][data2]...[settlement] + /// where data0 is for the first wrapper, addr1 is the second wrapper address, etc. + /// @param wrapperAddresses Array of wrapper contract addresses in execution order + /// @param individualWrapperDatas Array of wrapper-specific data corresponding to each wrapper + /// @param settlementContract The final settlement contract address to call after all wrappers + /// @return wrapperData The encoded wrapper data ready to be passed to the first wrapper's wrappedSettle function verifyAndBuildWrapperData(address[] calldata wrapperAddresses, bytes[] calldata individualWrapperDatas, address settlementContract) external view returns (bytes memory wrapperData) { // Basic Sanity: Input arrays should have correct length if (wrapperAddresses.length != individualWrapperDatas.length) { @@ -59,10 +96,12 @@ contract CowWrapperHelpers { totalIndividualWrapperDatasLength += individualWrapperDatas[i].length; } - wrapperData = abi.encodePacked(individualWrapperDatas[0]); + if (wrapperAddresses.length > 0) { + wrapperData = abi.encodePacked(individualWrapperDatas[0]); - for (uint256 i = 0;i < individualWrapperDatas.length;i++) { - wrapperData = abi.encodePacked(wrapperData, wrapperAddresses[i], individualWrapperDatas[i]); + for (uint256 i = 1;i < individualWrapperDatas.length;i++) { + wrapperData = abi.encodePacked(wrapperData, wrapperAddresses[i], individualWrapperDatas[i]); + } } wrapperData = abi.encodePacked(wrapperData, settlementContract); diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index dff6d51..2428d7c 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -1,4 +1,4 @@ -// 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"; @@ -129,6 +129,9 @@ contract CowWrapperTest is Test, CowWrapper { MockAuthentication(address(0)).addSolver(address(wrapper1)); MockAuthentication(address(0)).addSolver(address(wrapper2)); MockAuthentication(address(0)).addSolver(address(wrapper3)); + + // Add test contract as solver for test_wrap_ReceivesCorrectParameters + MockAuthentication(address(0)).addSolver(address(this)); } function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal override { @@ -145,31 +148,37 @@ contract CowWrapperTest is Test, CowWrapper { _internalSettle(settleData, wrapperData); } - function parseWrapperData(bytes calldata wrapperData) external pure override returns (bytes calldata remainingWrapperData) { - // CowWrapperTest doesn't consume any wrapper data - return wrapperData; + function parseWrapperData(bytes calldata wrapperData) external view override returns (bytes calldata remainingWrapperData) { + // CowWrapperTest consumes skipWrappedData bytes + return wrapperData[skipWrappedData:]; } function test_wrap_ReceivesCorrectParameters() public { - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = IERC20(address(0x1)); + MockSettlement.SettleCall memory settlement; - uint256[] memory clearingPrices = new uint256[](1); - clearingPrices[0] = 100; + settlement.tokens = new IERC20[](1); + settlement.tokens[0] = IERC20(address(0x1)); - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = + settlement.clearingPrices = new uint256[](1); + settlement.clearingPrices[0] = 100; + + settlement.trades = new GPv2Trade.Data[](0); + settlement.interactions = [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); + address[] memory wrappers = new address[](1); + wrappers[0] = address(this); + + bytes[] memory wrapperDatas = new bytes[](1); + wrapperDatas[0] = customWrapperData; skipWrappedData = customWrapperData.length; + bytes memory settleData = abi.encodeCall(MockSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions)); + bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, address(mockSettlement)); + vm.prank(solver); this.wrappedSettle(settleData, wrapperData); skipWrappedData = 0; @@ -180,21 +189,23 @@ contract CowWrapperTest is Test, CowWrapper { } function test_internalSettle_CallsNextSettlement() public { - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = IERC20(address(0x1)); + MockSettlement.SettleCall memory settlement; - uint256[] memory clearingPrices = new uint256[](1); - clearingPrices[0] = 100; + settlement.tokens = new IERC20[](1); + settlement.tokens[0] = IERC20(address(0x1)); - GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](0); - GPv2Interaction.Data[][3] memory interactions = + settlement.clearingPrices = new uint256[](1); + settlement.clearingPrices[0] = 100; + + settlement.trades = new GPv2Trade.Data[](0); + settlement.interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - address nextSettlement = address(mockSettlement); + address[] memory wrappers = new address[](0); + bytes[] memory wrapperDatas = new bytes[](0); - bytes memory settleData = - abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); - bytes memory wrapperData = abi.encodePacked(nextSettlement); + bytes memory settleData = abi.encodeCall(MockSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions)); + bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, address(mockSettlement)); vm.prank(solver); this.wrappedSettle(settleData, wrapperData); diff --git a/test/CowWrapperHelpers.t.sol b/test/CowWrapperHelpers.t.sol new file mode 100644 index 0000000..71c8a4f --- /dev/null +++ b/test/CowWrapperHelpers.t.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {Test} from "forge-std/Test.sol"; +import {CowWrapperHelpers} from "../src/vendor/CowWrapperHelpers.sol"; +import {CowWrapper, GPv2Authentication, ICowWrapper} from "../src/vendor/CowWrapper.sol"; + +contract MockAuthenticator { + 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 MockWrapper is CowWrapper { + uint256 public consumeBytes; + + constructor(GPv2Authentication authenticator_, uint256 consumeBytes_) CowWrapper(authenticator_) { + consumeBytes = consumeBytes_; + } + + function _wrap(bytes calldata, bytes calldata) internal override { + // Not used in these tests + } + + function parseWrapperData(bytes calldata wrapperData) external view override returns (bytes calldata remainingWrapperData) { + return wrapperData[consumeBytes:]; + } +} + +contract BrokenWrapper is CowWrapper { + constructor(GPv2Authentication authenticator_) CowWrapper(authenticator_) {} + + function _wrap(bytes calldata, bytes calldata) internal override { + // Not used in these tests + } + + function parseWrapperData(bytes calldata) external pure override returns (bytes calldata) { + revert("Intentionally broken"); + } +} + +contract CowWrapperHelpersTest is Test { + CowWrapperHelpers helpers; + MockAuthenticator wrapperAuth; + MockAuthenticator solverAuth; + address settlement; + + MockWrapper wrapper1; + MockWrapper wrapper2; + MockWrapper wrapper3; + BrokenWrapper brokenWrapper; + + function setUp() public { + wrapperAuth = new MockAuthenticator(); + solverAuth = new MockAuthenticator(); + helpers = new CowWrapperHelpers(GPv2Authentication(address(wrapperAuth)), GPv2Authentication(address(solverAuth))); + + settlement = makeAddr("settlement"); + + // Create mock wrappers + wrapper1 = new MockWrapper(GPv2Authentication(address(wrapperAuth)), 4); + wrapper2 = new MockWrapper(GPv2Authentication(address(wrapperAuth)), 8); + wrapper3 = new MockWrapper(GPv2Authentication(address(wrapperAuth)), 0); + brokenWrapper = new BrokenWrapper(GPv2Authentication(address(wrapperAuth))); + + // Add wrappers as solvers + wrapperAuth.addSolver(address(wrapper1)); + wrapperAuth.addSolver(address(wrapper2)); + wrapperAuth.addSolver(address(wrapper3)); + wrapperAuth.addSolver(address(brokenWrapper)); + } + + function test_verifyAndBuildWrapperData_EmptyArrays() public view { + address[] memory wrappers = new address[](0); + bytes[] memory wrapperDatas = new bytes[](0); + + bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + + // Should only contain the settlement address + assertEq(result, abi.encodePacked(settlement)); + } + + function test_verifyAndBuildWrapperData_SingleWrapper() public view { + address[] memory wrappers = new address[](1); + wrappers[0] = address(wrapper1); + + bytes[] memory wrapperDatas = new bytes[](1); + wrapperDatas[0] = hex"deadbeef"; + + bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + + // Should contain: wrapperData[0] + settlement + bytes memory expected = abi.encodePacked(hex"deadbeef", settlement); + assertEq(result, expected); + } + + function test_verifyAndBuildWrapperData_MultipleWrappers() public view { + address[] memory wrappers = new address[](3); + wrappers[0] = address(wrapper1); + wrappers[1] = address(wrapper2); + wrappers[2] = address(wrapper3); + + bytes[] memory wrapperDatas = new bytes[](3); + wrapperDatas[0] = hex"deadbeef"; + wrapperDatas[1] = hex"cafebabe12345678"; + wrapperDatas[2] = hex""; + + bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + + // Should contain: wrapperData[0] + wrapper[1] + wrapperData[1] + wrapper[2] + wrapperData[2] + settlement + bytes memory expected = abi.encodePacked( + hex"deadbeef", + address(wrapper2), + hex"cafebabe12345678", + address(wrapper3), + hex"", + settlement + ); + assertEq(result, expected); + } + + function test_verifyAndBuildWrapperData_RevertsOnInvalidInputLengths() public { + address[] memory wrappers = new address[](2); + wrappers[0] = address(wrapper1); + wrappers[1] = address(wrapper2); + + bytes[] memory wrapperDatas = new bytes[](1); + wrapperDatas[0] = hex"deadbeef"; + + vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.InvalidInputLengths.selector, 2, 1)); + helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + } + + function test_verifyAndBuildWrapperData_RevertsOnNotAWrapper() public { + address notAWrapper = makeAddr("notAWrapper"); + + address[] memory wrappers = new address[](1); + wrappers[0] = notAWrapper; + + bytes[] memory wrapperDatas = new bytes[](1); + wrapperDatas[0] = hex""; + + vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 0, notAWrapper, address(wrapperAuth))); + helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + } + + function test_verifyAndBuildWrapperData_RevertsOnNotAWrapper_SecondWrapper() public { + address notAWrapper = makeAddr("notAWrapper"); + + address[] memory wrappers = new address[](2); + wrappers[0] = address(wrapper1); + wrappers[1] = notAWrapper; + + bytes[] memory wrapperDatas = new bytes[](2); + wrapperDatas[0] = hex"deadbeef"; + wrapperDatas[1] = hex""; + + vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 1, notAWrapper, address(wrapperAuth))); + helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + } + + function test_verifyAndBuildWrapperData_RevertsOnWrapperDataNotFullyConsumed() public { + address[] memory wrappers = new address[](1); + wrappers[0] = address(wrapper1); // Consumes 4 bytes + + bytes[] memory wrapperDatas = new bytes[](1); + wrapperDatas[0] = hex"deadbeefcafe"; // 6 bytes, but wrapper only consumes 4 + + vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.WrapperDataNotFullyConsumed.selector, 0, hex"cafe")); + helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + } + + function test_verifyAndBuildWrapperData_RevertsOnWrapperDataMalformed() public { + address[] memory wrappers = new address[](1); + wrappers[0] = address(brokenWrapper); + + bytes[] memory wrapperDatas = new bytes[](1); + wrapperDatas[0] = hex"deadbeef"; + + vm.expectRevert(); + helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + } + + function test_verifyAndBuildWrapperData_RevertsOnSettlementContractShouldNotBeSolver() public { + // Add settlement as a solver (which should not be allowed) + solverAuth.addSolver(settlement); + + address[] memory wrappers = new address[](1); + wrappers[0] = address(wrapper1); + + bytes[] memory wrapperDatas = new bytes[](1); + wrapperDatas[0] = hex"deadbeef"; + + vm.expectRevert( + abi.encodeWithSelector( + CowWrapperHelpers.SettlementContractShouldNotBeSolver.selector, + settlement, + address(solverAuth) + ) + ); + helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + } + + function test_verifyAndBuildWrapperData_EmptyWrapperData() public view { + address[] memory wrappers = new address[](2); + wrappers[0] = address(wrapper3); // Consumes 0 bytes + wrappers[1] = address(wrapper3); // Consumes 0 bytes + + bytes[] memory wrapperDatas = new bytes[](2); + wrapperDatas[0] = hex""; + wrapperDatas[1] = hex""; + + bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + + // Should contain: "" + wrapper[1] + "" + settlement + bytes memory expected = abi.encodePacked(hex"", address(wrapper3), hex"", settlement); + assertEq(result, expected); + } + + function test_verifyAndBuildWrapperData_MixedWrapperDataSizes() public view { + address[] memory wrappers = new address[](3); + wrappers[0] = address(wrapper3); // Consumes 0 bytes + wrappers[1] = address(wrapper1); // Consumes 4 bytes + wrappers[2] = address(wrapper2); // Consumes 8 bytes + + bytes[] memory wrapperDatas = new bytes[](3); + wrapperDatas[0] = hex""; + wrapperDatas[1] = hex"deadbeef"; + wrapperDatas[2] = hex"cafebabe12345678"; + + bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + + bytes memory expected = abi.encodePacked( + hex"", + address(wrapper1), + hex"deadbeef", + address(wrapper2), + hex"cafebabe12345678", + settlement + ); + assertEq(result, expected); + } + + function test_immutableAuthenticators() public view { + assertEq(address(helpers.WRAPPER_AUTHENTICATOR()), address(wrapperAuth)); + assertEq(address(helpers.SOLVER_AUTHENTICATOR()), address(solverAuth)); + } +} diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index d413e77..b032900 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8; pragma abicoder v2; From 45c16923167c56e55a05ad02ac1644befbebdf05 Mon Sep 17 00:00:00 2001 From: Kaze Date: Fri, 10 Oct 2025 01:13:19 +0900 Subject: [PATCH 03/12] switch to more natural call object pattern --- src/vendor/CowWrapperHelpers.sol | 48 ++++----- test/CowWrapper.t.sol | 38 ++++--- test/CowWrapperHelpers.t.sol | 177 +++++++++++++++---------------- 3 files changed, 128 insertions(+), 135 deletions(-) diff --git a/src/vendor/CowWrapperHelpers.sol b/src/vendor/CowWrapperHelpers.sol index 1cf84f1..a340d1d 100644 --- a/src/vendor/CowWrapperHelpers.sol +++ b/src/vendor/CowWrapperHelpers.sol @@ -10,11 +10,6 @@ import {GPv2Authentication, ICowWrapper} from "./CowWrapper.sol"; /// @notice Helper contract providing validation and encoding utilities for CoW Protocol wrapper chains /// @dev This contract is not designed to be gas-efficient and is intended for off-chain use only. contract CowWrapperHelpers { - /// @notice Thrown when wrapper and wrapper data array lengths don't match - /// @param wrappersLength The length of the wrappers array - /// @param individualWrapperDatasLength The length of the wrapper data array - error InvalidInputLengths(uint256 wrappersLength, uint256 individualWrapperDatasLength); - /// @notice Thrown when a provided address is not an authenticated wrapper /// @param wrapperIndex The index of the invalid wrapper in the array /// @param unauthorized The address that is not authenticated as a wrapper @@ -37,6 +32,11 @@ contract CowWrapperHelpers { /// @param authenticatorContract The authentication contract that authenticated the settlement as a solver error SettlementContractShouldNotBeSolver(address settlementContract, address authenticatorContract); + struct WrapperCall { + address target; + bytes data; + } + /// @notice The authentication contract used to verify wrapper contracts GPv2Authentication public immutable WRAPPER_AUTHENTICATOR; @@ -53,31 +53,24 @@ contract CowWrapperHelpers { /// @notice Validates a wrapper chain configuration and builds the properly formatted wrapper data /// @dev Performs comprehensive validation of the wrapper chain before encoding: - /// 1. Verifies array lengths match - /// 2. Verifies each wrapper is authenticated via WRAPPER_AUTHENTICATOR - /// 3. Verifies each wrapper's data is valid and fully consumed by calling parseWrapperData - /// 4. Verifies the settlement contract is not authenticated as a solver + /// 1. Verifies each wrapper is authenticated via WRAPPER_AUTHENTICATOR + /// 2. Verifies each wrapper's data is valid and fully consumed by calling parseWrapperData + /// 3. Verifies the settlement contract is not authenticated as a solver /// The returned wrapper data format is: [data0][addr1][data1][addr2][data2]...[settlement] /// where data0 is for the first wrapper, addr1 is the second wrapper address, etc. - /// @param wrapperAddresses Array of wrapper contract addresses in execution order - /// @param individualWrapperDatas Array of wrapper-specific data corresponding to each wrapper + /// @param wrapperCalls Array of calls in execution order /// @param settlementContract The final settlement contract address to call after all wrappers /// @return wrapperData The encoded wrapper data ready to be passed to the first wrapper's wrappedSettle - function verifyAndBuildWrapperData(address[] calldata wrapperAddresses, bytes[] calldata individualWrapperDatas, address settlementContract) external view returns (bytes memory wrapperData) { - // Basic Sanity: Input arrays should have correct length - if (wrapperAddresses.length != individualWrapperDatas.length) { - revert InvalidInputLengths(wrapperAddresses.length, individualWrapperDatas.length); - } - - for (uint256 i = 0;i < wrapperAddresses.length;i++) { + function verifyAndBuildWrapperData(WrapperCall[] memory wrapperCalls, address settlementContract) external view returns (bytes memory wrapperData) { + for (uint256 i = 0;i < wrapperCalls.length;i++) { // Wrapper must be authorized - if (!WRAPPER_AUTHENTICATOR.isSolver(wrapperAddresses[i])) { - revert NotAWrapper(i, wrapperAddresses[i], address(WRAPPER_AUTHENTICATOR)); + if (!WRAPPER_AUTHENTICATOR.isSolver(wrapperCalls[i].target)) { + revert NotAWrapper(i, wrapperCalls[i].target, address(WRAPPER_AUTHENTICATOR)); } // The wrapper data must be parsable and fully consumed - try ICowWrapper(wrapperAddresses[i]).parseWrapperData(individualWrapperDatas[i]) returns (bytes memory remainingWrapperData) { + try ICowWrapper(wrapperCalls[i].target).parseWrapperData(wrapperCalls[i].data) returns (bytes memory remainingWrapperData) { if (remainingWrapperData.length > 0) { revert WrapperDataNotFullyConsumed(i, remainingWrapperData); } @@ -91,16 +84,11 @@ contract CowWrapperHelpers { revert SettlementContractShouldNotBeSolver(settlementContract, address(SOLVER_AUTHENTICATOR)); } - uint256 totalIndividualWrapperDatasLength = 0; - for (uint256 i = 0;i < individualWrapperDatas.length;i++) { - totalIndividualWrapperDatasLength += individualWrapperDatas[i].length; - } - - if (wrapperAddresses.length > 0) { - wrapperData = abi.encodePacked(individualWrapperDatas[0]); + if (wrapperCalls.length > 0) { + wrapperData = abi.encodePacked(wrapperCalls[0].data); - for (uint256 i = 1;i < individualWrapperDatas.length;i++) { - wrapperData = abi.encodePacked(wrapperData, wrapperAddresses[i], individualWrapperDatas[i]); + for (uint256 i = 1;i < wrapperCalls.length;i++) { + wrapperData = abi.encodePacked(wrapperData, wrapperCalls[i].target, wrapperCalls[i].data); } } diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 2428d7c..60df73c 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -168,16 +168,16 @@ contract CowWrapperTest is Test, CowWrapper { bytes memory customWrapperData = hex"deadbeef"; - address[] memory wrappers = new address[](1); - wrappers[0] = address(this); - - bytes[] memory wrapperDatas = new bytes[](1); - wrapperDatas[0] = customWrapperData; + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(this), + data: customWrapperData + }); skipWrappedData = customWrapperData.length; bytes memory settleData = abi.encodeCall(MockSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions)); - bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, address(mockSettlement)); + bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrapperCalls, address(mockSettlement)); vm.prank(solver); this.wrappedSettle(settleData, wrapperData); @@ -201,11 +201,10 @@ contract CowWrapperTest is Test, CowWrapper { settlement.interactions = [new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; - address[] memory wrappers = new address[](0); - bytes[] memory wrapperDatas = new bytes[](0); + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](0); bytes memory settleData = abi.encodeCall(MockSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions)); - bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, address(mockSettlement)); + bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrapperCalls, address(mockSettlement)); vm.prank(solver); this.wrappedSettle(settleData, wrapperData); @@ -285,17 +284,24 @@ contract CowWrapperTest is Test, CowWrapper { // 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); + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](3); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper1), + data: hex"" + }); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper2), + data: hex"" + }); + wrapperCalls[2] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper3), + data: hex"" + }); bytes memory settleData = abi.encodeCall(MockSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions)); bytes memory wrapperData = - helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, address(mockSettlement)); + helpers.verifyAndBuildWrapperData(wrapperCalls, address(mockSettlement)); // Call wrapper1 as the solver vm.prank(solver); diff --git a/test/CowWrapperHelpers.t.sol b/test/CowWrapperHelpers.t.sol index 71c8a4f..5abacff 100644 --- a/test/CowWrapperHelpers.t.sol +++ b/test/CowWrapperHelpers.t.sol @@ -77,43 +77,46 @@ contract CowWrapperHelpersTest is Test { } function test_verifyAndBuildWrapperData_EmptyArrays() public view { - address[] memory wrappers = new address[](0); - bytes[] memory wrapperDatas = new bytes[](0); + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](0); - bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); // Should only contain the settlement address assertEq(result, abi.encodePacked(settlement)); } function test_verifyAndBuildWrapperData_SingleWrapper() public view { - address[] memory wrappers = new address[](1); - wrappers[0] = address(wrapper1); + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper1), + data: hex"deadbeef" + }); - bytes[] memory wrapperDatas = new bytes[](1); - wrapperDatas[0] = hex"deadbeef"; + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); - bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); - - // Should contain: wrapperData[0] + settlement + // Should contain: data[0] + settlement bytes memory expected = abi.encodePacked(hex"deadbeef", settlement); assertEq(result, expected); } function test_verifyAndBuildWrapperData_MultipleWrappers() public view { - address[] memory wrappers = new address[](3); - wrappers[0] = address(wrapper1); - wrappers[1] = address(wrapper2); - wrappers[2] = address(wrapper3); - - bytes[] memory wrapperDatas = new bytes[](3); - wrapperDatas[0] = hex"deadbeef"; - wrapperDatas[1] = hex"cafebabe12345678"; - wrapperDatas[2] = hex""; - - bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); - - // Should contain: wrapperData[0] + wrapper[1] + wrapperData[1] + wrapper[2] + wrapperData[2] + settlement + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](3); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper1), + data: hex"deadbeef" + }); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper2), + data: hex"cafebabe12345678" + }); + wrapperCalls[2] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper3), + data: hex"" + }); + + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); + + // Should contain: data[0] + target[1] + data[1] + target[2] + data[2] + settlement bytes memory expected = abi.encodePacked( hex"deadbeef", address(wrapper2), @@ -125,77 +128,67 @@ contract CowWrapperHelpersTest is Test { assertEq(result, expected); } - function test_verifyAndBuildWrapperData_RevertsOnInvalidInputLengths() public { - address[] memory wrappers = new address[](2); - wrappers[0] = address(wrapper1); - wrappers[1] = address(wrapper2); - - bytes[] memory wrapperDatas = new bytes[](1); - wrapperDatas[0] = hex"deadbeef"; - - vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.InvalidInputLengths.selector, 2, 1)); - helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); - } - function test_verifyAndBuildWrapperData_RevertsOnNotAWrapper() public { address notAWrapper = makeAddr("notAWrapper"); - address[] memory wrappers = new address[](1); - wrappers[0] = notAWrapper; - - bytes[] memory wrapperDatas = new bytes[](1); - wrapperDatas[0] = hex""; + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: notAWrapper, + data: hex"" + }); vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 0, notAWrapper, address(wrapperAuth))); - helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); } function test_verifyAndBuildWrapperData_RevertsOnNotAWrapper_SecondWrapper() public { address notAWrapper = makeAddr("notAWrapper"); - address[] memory wrappers = new address[](2); - wrappers[0] = address(wrapper1); - wrappers[1] = notAWrapper; - - bytes[] memory wrapperDatas = new bytes[](2); - wrapperDatas[0] = hex"deadbeef"; - wrapperDatas[1] = hex""; + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](2); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper1), + data: hex"deadbeef" + }); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ + target: notAWrapper, + data: hex"" + }); vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 1, notAWrapper, address(wrapperAuth))); - helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); } function test_verifyAndBuildWrapperData_RevertsOnWrapperDataNotFullyConsumed() public { - address[] memory wrappers = new address[](1); - wrappers[0] = address(wrapper1); // Consumes 4 bytes - - bytes[] memory wrapperDatas = new bytes[](1); - wrapperDatas[0] = hex"deadbeefcafe"; // 6 bytes, but wrapper only consumes 4 + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper1), // Consumes 4 bytes + data: hex"deadbeefcafe" // 6 bytes, but wrapper only consumes 4 + }); vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.WrapperDataNotFullyConsumed.selector, 0, hex"cafe")); - helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); } function test_verifyAndBuildWrapperData_RevertsOnWrapperDataMalformed() public { - address[] memory wrappers = new address[](1); - wrappers[0] = address(brokenWrapper); - - bytes[] memory wrapperDatas = new bytes[](1); - wrapperDatas[0] = hex"deadbeef"; + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(brokenWrapper), + data: hex"deadbeef" + }); vm.expectRevert(); - helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); } function test_verifyAndBuildWrapperData_RevertsOnSettlementContractShouldNotBeSolver() public { // Add settlement as a solver (which should not be allowed) solverAuth.addSolver(settlement); - address[] memory wrappers = new address[](1); - wrappers[0] = address(wrapper1); - - bytes[] memory wrapperDatas = new bytes[](1); - wrapperDatas[0] = hex"deadbeef"; + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper1), + data: hex"deadbeef" + }); vm.expectRevert( abi.encodeWithSelector( @@ -204,37 +197,43 @@ contract CowWrapperHelpersTest is Test { address(solverAuth) ) ); - helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); } function test_verifyAndBuildWrapperData_EmptyWrapperData() public view { - address[] memory wrappers = new address[](2); - wrappers[0] = address(wrapper3); // Consumes 0 bytes - wrappers[1] = address(wrapper3); // Consumes 0 bytes - - bytes[] memory wrapperDatas = new bytes[](2); - wrapperDatas[0] = hex""; - wrapperDatas[1] = hex""; - - bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); - - // Should contain: "" + wrapper[1] + "" + settlement + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](2); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper3), // Consumes 0 bytes + data: hex"" + }); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper3), // Consumes 0 bytes + data: hex"" + }); + + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); + + // Should contain: data[0] + target[1] + data[1] + settlement bytes memory expected = abi.encodePacked(hex"", address(wrapper3), hex"", settlement); assertEq(result, expected); } function test_verifyAndBuildWrapperData_MixedWrapperDataSizes() public view { - address[] memory wrappers = new address[](3); - wrappers[0] = address(wrapper3); // Consumes 0 bytes - wrappers[1] = address(wrapper1); // Consumes 4 bytes - wrappers[2] = address(wrapper2); // Consumes 8 bytes - - bytes[] memory wrapperDatas = new bytes[](3); - wrapperDatas[0] = hex""; - wrapperDatas[1] = hex"deadbeef"; - wrapperDatas[2] = hex"cafebabe12345678"; - - bytes memory result = helpers.verifyAndBuildWrapperData(wrappers, wrapperDatas, settlement); + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](3); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper3), // Consumes 0 bytes + data: hex"" + }); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper1), // Consumes 4 bytes + data: hex"deadbeef" + }); + wrapperCalls[2] = CowWrapperHelpers.WrapperCall({ + target: address(wrapper2), // Consumes 8 bytes + data: hex"cafebabe12345678" + }); + + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls, settlement); bytes memory expected = abi.encodePacked( hex"", From bfecf38ddfdecf8af1e7e1241daaa103883511d2 Mon Sep 17 00:00:00 2001 From: Kaze Date: Fri, 10 Oct 2025 01:20:44 +0900 Subject: [PATCH 04/12] add docs --- src/vendor/CowWrapperHelpers.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vendor/CowWrapperHelpers.sol b/src/vendor/CowWrapperHelpers.sol index a340d1d..6b87a06 100644 --- a/src/vendor/CowWrapperHelpers.sol +++ b/src/vendor/CowWrapperHelpers.sol @@ -32,8 +32,13 @@ contract CowWrapperHelpers { /// @param authenticatorContract The authentication contract that authenticated the settlement as a solver error SettlementContractShouldNotBeSolver(address settlementContract, address authenticatorContract); + /// @notice A definition for a single call to a wrapper + /// @dev This corresponds to the `wrappers` item structure on the CoW Orderbook API struct WrapperCall { + /// @notice The smart contract that will be receiving the call address target; + + /// @notice Any additional data which will be required to execute the wrapper call bytes data; } From 5a290037461f487c047940271d08eb191ccff8c2 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 13 Oct 2025 12:22:59 +0900 Subject: [PATCH 05/12] renames, cleanups, make it build --- src/vendor/CowWrapper.sol | 16 ++++++++-------- src/vendor/CowWrapperHelpers.sol | 14 +++++++------- test/CowWrapper.t.sol | 12 ++++++------ test/CowWrapperHelpers.t.sol | 16 ++++++++-------- test/EmptyWrapper.sol | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 0781e82..8e13c30 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -2,16 +2,16 @@ pragma solidity >=0.7.6 <0.9.0; pragma abicoder v2; -/// @title Gnosis Protocol v2 Authentication Interface +/// @title CoW Protocol 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 { @@ -50,7 +50,7 @@ interface CowSettlement { ) 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 { @@ -72,7 +72,7 @@ interface ICowWrapper { function parseWrapperData(bytes calldata wrapperData) external view returns (bytes calldata remainingWrapperData); } -/// @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: @@ -97,11 +97,11 @@ abstract contract CowWrapper { /// @notice The authentication contract used to verify solvers /// @dev This is typically the GPv2AllowListAuthentication contract - GPv2Authentication public immutable AUTHENTICATOR; + 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_) { + constructor(CowAuthentication authenticator_) { AUTHENTICATOR = authenticator_; } @@ -120,7 +120,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/src/vendor/CowWrapperHelpers.sol b/src/vendor/CowWrapperHelpers.sol index 6b87a06..019d187 100644 --- a/src/vendor/CowWrapperHelpers.sol +++ b/src/vendor/CowWrapperHelpers.sol @@ -4,9 +4,9 @@ pragma abicoder v2; import "forge-std/console.sol"; -import {GPv2Authentication, ICowWrapper} from "./CowWrapper.sol"; +import {CowAuthentication, ICowWrapper} from "./CowWrapper.sol"; -/// @title CoW Wrapper Helpers +/// @title CoW Protocol Wrapper Helpers /// @notice Helper contract providing validation and encoding utilities for CoW Protocol wrapper chains /// @dev This contract is not designed to be gas-efficient and is intended for off-chain use only. contract CowWrapperHelpers { @@ -43,15 +43,15 @@ contract CowWrapperHelpers { } /// @notice The authentication contract used to verify wrapper contracts - GPv2Authentication public immutable WRAPPER_AUTHENTICATOR; + CowAuthentication public immutable WRAPPER_AUTHENTICATOR; /// @notice The authentication contract used to verify solvers - GPv2Authentication public immutable SOLVER_AUTHENTICATOR; + CowAuthentication public immutable SOLVER_AUTHENTICATOR; /// @notice Constructs a new CowWrapperHelpers contract - /// @param wrapperAuthenticator_ The GPv2Authentication contract used to verify wrapper contracts - /// @param solverAuthenticator_ The GPv2Authentication contract used to verify solvers - constructor(GPv2Authentication wrapperAuthenticator_, GPv2Authentication solverAuthenticator_) { + /// @param wrapperAuthenticator_ The CowAuthentication contract used to verify wrapper contracts + /// @param solverAuthenticator_ The CowAuthentication contract used to verify solvers + constructor(CowAuthentication wrapperAuthenticator_, CowAuthentication solverAuthenticator_) { WRAPPER_AUTHENTICATOR = wrapperAuthenticator_; SOLVER_AUTHENTICATOR = solverAuthenticator_; } diff --git a/test/CowWrapper.t.sol b/test/CowWrapper.t.sol index 60df73c..7dac3e4 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"; @@ -104,7 +104,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 } @@ -118,12 +118,12 @@ contract CowWrapperTest is Test, CowWrapper { MockAuthentication(address(0)).addSolver(solver); mockSettlement = new MockSettlement(); - helpers = new CowWrapperHelpers(GPv2Authentication(address(0)), GPv2Authentication(address(0))); + helpers = new CowWrapperHelpers(CowAuthentication(address(0)), CowAuthentication(address(0))); // 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)); diff --git a/test/CowWrapperHelpers.t.sol b/test/CowWrapperHelpers.t.sol index 5abacff..13ab37f 100644 --- a/test/CowWrapperHelpers.t.sol +++ b/test/CowWrapperHelpers.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; import {CowWrapperHelpers} from "../src/vendor/CowWrapperHelpers.sol"; -import {CowWrapper, GPv2Authentication, ICowWrapper} from "../src/vendor/CowWrapper.sol"; +import {CowWrapper, CowAuthentication, ICowWrapper} from "../src/vendor/CowWrapper.sol"; contract MockAuthenticator { mapping(address => bool) public solvers; @@ -20,7 +20,7 @@ contract MockAuthenticator { contract MockWrapper is CowWrapper { uint256 public consumeBytes; - constructor(GPv2Authentication authenticator_, uint256 consumeBytes_) CowWrapper(authenticator_) { + constructor(CowAuthentication authenticator_, uint256 consumeBytes_) CowWrapper(authenticator_) { consumeBytes = consumeBytes_; } @@ -34,7 +34,7 @@ contract MockWrapper is CowWrapper { } contract BrokenWrapper is CowWrapper { - constructor(GPv2Authentication authenticator_) CowWrapper(authenticator_) {} + constructor(CowAuthentication authenticator_) CowWrapper(authenticator_) {} function _wrap(bytes calldata, bytes calldata) internal override { // Not used in these tests @@ -59,15 +59,15 @@ contract CowWrapperHelpersTest is Test { function setUp() public { wrapperAuth = new MockAuthenticator(); solverAuth = new MockAuthenticator(); - helpers = new CowWrapperHelpers(GPv2Authentication(address(wrapperAuth)), GPv2Authentication(address(solverAuth))); + helpers = new CowWrapperHelpers(CowAuthentication(address(wrapperAuth)), CowAuthentication(address(solverAuth))); settlement = makeAddr("settlement"); // Create mock wrappers - wrapper1 = new MockWrapper(GPv2Authentication(address(wrapperAuth)), 4); - wrapper2 = new MockWrapper(GPv2Authentication(address(wrapperAuth)), 8); - wrapper3 = new MockWrapper(GPv2Authentication(address(wrapperAuth)), 0); - brokenWrapper = new BrokenWrapper(GPv2Authentication(address(wrapperAuth))); + wrapper1 = new MockWrapper(CowAuthentication(address(wrapperAuth)), 4); + wrapper2 = new MockWrapper(CowAuthentication(address(wrapperAuth)), 8); + wrapper3 = new MockWrapper(CowAuthentication(address(wrapperAuth)), 0); + brokenWrapper = new BrokenWrapper(CowAuthentication(address(wrapperAuth))); // Add wrappers as solvers wrapperAuth.addSolver(address(wrapper1)); diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index b032900..5f0ea1e 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, From 4c9156769fc1fa3ec24ea2987f47dcb7fed87d18 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 20 Oct 2025 19:36:30 +0900 Subject: [PATCH 06/12] 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 3972fe7..e1a09fa 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -157,7 +157,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 5877000aa8e2648f60e1b00d1d7f641e10c2d1bc Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 27 Oct 2025 16:27:40 +0900 Subject: [PATCH 07/12] chore: apply formatting --- src/vendor/CowWrapper.sol | 12 +- src/vendor/CowWrapperHelpers.sol | 21 ++-- 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 | 33 ++--- test/CowWrapperHelpers.t.sol | 62 ++++------ test/EmptyWrapper.sol | 7 +- test/helpers/CowBaseTest.sol | 8 +- test/helpers/SignerECDSA.sol | 1 - 11 files changed, 162 insertions(+), 169 deletions(-) diff --git a/src/vendor/CowWrapper.sol b/src/vendor/CowWrapper.sol index 9de58cd..89185b4 100644 --- a/src/vendor/CowWrapper.sol +++ b/src/vendor/CowWrapper.sol @@ -126,7 +126,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 Parses and validates wrapper-specific data @@ -134,7 +134,11 @@ abstract contract CowWrapper is ICowWrapper { /// The implementation should consume its wrapper-specific data and return the remainder. /// @param wrapperData The full wrapper data to parse /// @return remainingWrapperData The portion of wrapper data not consumed by this wrapper - function parseWrapperData(bytes calldata wrapperData) external virtual view returns (bytes calldata remainingWrapperData); + function parseWrapperData(bytes calldata wrapperData) + external + view + virtual + returns (bytes calldata remainingWrapperData); /// @notice Internal function containing the wrapper's custom logic /// @dev Must be implemented by concrete wrapper contracts. Should execute custom logic @@ -142,7 +146,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/CowWrapperHelpers.sol b/src/vendor/CowWrapperHelpers.sol index 153db31..a547956 100644 --- a/src/vendor/CowWrapperHelpers.sol +++ b/src/vendor/CowWrapperHelpers.sol @@ -71,13 +71,17 @@ contract CowWrapperHelpers { /// Note: No settlement address is appended as wrappers now use a static SETTLEMENT. /// @param wrapperCalls Array of calls in execution order /// @return wrapperData The encoded wrapper data ready to be passed to the first wrapper's wrappedSettle - function verifyAndBuildWrapperData(WrapperCall[] memory wrapperCalls) external view returns (bytes memory wrapperData) { + function verifyAndBuildWrapperData(WrapperCall[] memory wrapperCalls) + external + view + returns (bytes memory wrapperData) + { if (wrapperCalls.length == 0) { return wrapperData; } // First pass: verify all wrappers are authenticated - for (uint256 i = 0;i < wrapperCalls.length;i++) { + for (uint256 i = 0; i < wrapperCalls.length; i++) { if (!WRAPPER_AUTHENTICATOR.isSolver(wrapperCalls[i].target)) { revert NotAWrapper(i, wrapperCalls[i].target, address(WRAPPER_AUTHENTICATOR)); } @@ -86,8 +90,7 @@ contract CowWrapperHelpers { // Get the expected settlement from the first wrapper address expectedSettlement = address(ICowWrapper(wrapperCalls[0].target).SETTLEMENT()); - for (uint256 i = 0;i < wrapperCalls.length;i++) { - + for (uint256 i = 0; i < wrapperCalls.length; i++) { // All wrappers must use the same settlement contract address wrapperSettlement = address(ICowWrapper(wrapperCalls[i].target).SETTLEMENT()); if (wrapperSettlement != expectedSettlement) { @@ -95,7 +98,9 @@ contract CowWrapperHelpers { } // The wrapper data must be parsable and fully consumed - try ICowWrapper(wrapperCalls[i].target).parseWrapperData(wrapperCalls[i].data) returns (bytes memory remainingWrapperData) { + try ICowWrapper(wrapperCalls[i].target).parseWrapperData(wrapperCalls[i].data) returns ( + bytes memory remainingWrapperData + ) { if (remainingWrapperData.length > 0) { revert WrapperDataNotFullyConsumed(i, remainingWrapperData); } @@ -112,8 +117,10 @@ contract CowWrapperHelpers { // Build wrapper data without settlement address at the end wrapperData = abi.encodePacked(uint16(wrapperCalls[0].data.length), wrapperCalls[0].data); - for (uint256 i = 1;i < wrapperCalls.length;i++) { - wrapperData = abi.encodePacked(wrapperData, wrapperCalls[i].target, uint16(wrapperCalls[i].data.length), wrapperCalls[i].data); + for (uint256 i = 1; i < wrapperCalls.length; i++) { + wrapperData = abi.encodePacked( + wrapperData, wrapperCalls[i].target, uint16(wrapperCalls[i].data.length), wrapperCalls[i].data + ); } return wrapperData; 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 feb40d2..9753097 100644 --- a/test/CowWrapper.t.sol +++ b/test/CowWrapper.t.sol @@ -99,7 +99,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; @@ -121,7 +124,12 @@ contract TestWrapper is CowWrapper { return (wrapCalls[index].settleData, wrapCalls[index].wrapperData); } - function parseWrapperData(bytes calldata wrapperData) external view override returns (bytes calldata remainingWrapperData) { + function parseWrapperData(bytes calldata wrapperData) + external + view + override + returns (bytes calldata remainingWrapperData) + { return wrapperData; } } @@ -141,7 +149,8 @@ contract CowWrapperTest is Test { // Deploy mock contracts authenticator = new MockAuthentication(); mockSettlement = new MockSettlement(CowAuthentication(address(authenticator))); - helpers = new CowWrapperHelpers(CowAuthentication(address(authenticator)), CowAuthentication(address(authenticator))); + helpers = + new CowWrapperHelpers(CowAuthentication(address(authenticator)), CowAuthentication(address(authenticator))); solver = makeAddr("solver"); // Add solver to the authenticator @@ -263,21 +272,13 @@ contract CowWrapperTest is Test { // solver -> wrapper1 -> wrapper2 -> wrapper3 -> static SETTLEMENT CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](3); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper1), - data: hex"" - }); - wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper2), - data: hex"" - }); - wrapperCalls[2] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper3), - data: hex"" - }); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex""}); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({target: address(wrapper2), data: hex""}); + wrapperCalls[2] = CowWrapperHelpers.WrapperCall({target: address(wrapper3), data: hex""}); bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrapperCalls); - bytes memory settleData = abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); + bytes memory settleData = + abi.encodeWithSelector(CowSettlement.settle.selector, tokens, clearingPrices, trades, interactions); // Call wrapper1 as the solver vm.prank(solver); diff --git a/test/CowWrapperHelpers.t.sol b/test/CowWrapperHelpers.t.sol index d4c625f..989f47b 100644 --- a/test/CowWrapperHelpers.t.sol +++ b/test/CowWrapperHelpers.t.sol @@ -30,7 +30,7 @@ contract MockSettlement { } contract MockWrapper is CowWrapper { - string constant public name = "Mock Wrapper"; + string public constant name = "Mock Wrapper"; uint256 public consumeBytes; constructor(CowSettlement settlement_, uint256 consumeBytes_) CowWrapper(settlement_) { @@ -41,7 +41,12 @@ contract MockWrapper is CowWrapper { // Not used in these tests } - function parseWrapperData(bytes calldata wrapperData) external view override returns (bytes calldata remainingWrapperData) { + function parseWrapperData(bytes calldata wrapperData) + external + view + override + returns (bytes calldata remainingWrapperData) + { return wrapperData[consumeBytes:]; } } @@ -101,10 +106,7 @@ contract CowWrapperHelpersTest is Test { function test_verifyAndBuildWrapperData_SingleWrapper() public view { CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper1), - data: hex"deadbeef" - }); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls); @@ -115,18 +117,9 @@ contract CowWrapperHelpersTest is Test { function test_verifyAndBuildWrapperData_MultipleWrappers() public view { CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](3); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper1), - data: hex"deadbeef" - }); - wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper2), - data: hex"cafebabe12345678" - }); - wrapperCalls[2] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper3), - data: hex"" - }); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({target: address(wrapper2), data: hex"cafebabe12345678"}); + wrapperCalls[2] = CowWrapperHelpers.WrapperCall({target: address(wrapper3), data: hex""}); bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls); @@ -148,12 +141,11 @@ contract CowWrapperHelpersTest is Test { address notAWrapper = makeAddr("notAWrapper"); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ - target: notAWrapper, - data: hex"" - }); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: notAWrapper, data: hex""}); - vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 0, notAWrapper, address(wrapperAuth))); + vm.expectRevert( + abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 0, notAWrapper, address(wrapperAuth)) + ); helpers.verifyAndBuildWrapperData(wrapperCalls); } @@ -161,16 +153,12 @@ contract CowWrapperHelpersTest is Test { address notAWrapper = makeAddr("notAWrapper"); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](2); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper1), - data: hex"deadbeef" - }); - wrapperCalls[1] = CowWrapperHelpers.WrapperCall({ - target: notAWrapper, - data: hex"" - }); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({target: notAWrapper, data: hex""}); - vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 1, notAWrapper, address(wrapperAuth))); + vm.expectRevert( + abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 1, notAWrapper, address(wrapperAuth)) + ); helpers.verifyAndBuildWrapperData(wrapperCalls); } @@ -187,10 +175,7 @@ contract CowWrapperHelpersTest is Test { function test_verifyAndBuildWrapperData_RevertsOnWrapperDataMalformed() public { CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ - target: address(brokenWrapper), - data: hex"deadbeef" - }); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(brokenWrapper), data: hex"deadbeef"}); vm.expectRevert(); helpers.verifyAndBuildWrapperData(wrapperCalls); @@ -201,10 +186,7 @@ contract CowWrapperHelpersTest is Test { solverAuth.addSolver(address(mockSettlement)); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({ - target: address(wrapper1), - data: hex"deadbeef" - }); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); vm.expectRevert( abi.encodeWithSelector( diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol index e44e89e..bf993fc 100644 --- a/test/EmptyWrapper.sol +++ b/test/EmptyWrapper.sol @@ -13,7 +13,12 @@ contract EmptyWrapper is CowWrapper { _internalSettle(settleData, remainingWrapperData); } - function parseWrapperData(bytes calldata wrapperData) external pure override returns (bytes calldata remainingWrapperData) { + function parseWrapperData(bytes calldata wrapperData) + external + pure + override + returns (bytes calldata remainingWrapperData) + { // EmptyWrapper doesn't consume any wrapper data return wrapperData; } 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/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 fc6501a4a6a3064f1a24e77312458debbe97695d Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 27 Oct 2025 22:29:32 +0900 Subject: [PATCH 08/12] fix claude suggestions * add check to see if wrapper data is too long * used the newer error format * add missing test case --- src/vendor/CowWrapperHelpers.sol | 32 +++++++++------ test/CowWrapperHelpers.t.sol | 70 ++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/src/vendor/CowWrapperHelpers.sol b/src/vendor/CowWrapperHelpers.sol index a547956..eb32b9c 100644 --- a/src/vendor/CowWrapperHelpers.sol +++ b/src/vendor/CowWrapperHelpers.sol @@ -24,6 +24,11 @@ contract CowWrapperHelpers { /// @param wrapperError The error returned by the wrapper's parseWrapperData error WrapperDataMalformed(uint256 wrapperIndex, bytes wrapperError); + /// @notice Thrown when the data for the wrapper is too long. Its limited to 65535 bytes. + /// @param wrapperIndex The index of the wrapper with data that is too long + /// @param exceedingLength The observed length of the data + error WrapperDataTooLong(uint256 wrapperIndex, uint256 exceedingLength); + /// @notice Thrown when the settlement contract is authenticated as a solver /// @dev The settlement contract should not be a solver to prevent direct settlement calls bypassing wrappers /// @param settlementContract The settlement contract address @@ -82,9 +87,10 @@ contract CowWrapperHelpers { // First pass: verify all wrappers are authenticated for (uint256 i = 0; i < wrapperCalls.length; i++) { - if (!WRAPPER_AUTHENTICATOR.isSolver(wrapperCalls[i].target)) { - revert NotAWrapper(i, wrapperCalls[i].target, address(WRAPPER_AUTHENTICATOR)); - } + require( + WRAPPER_AUTHENTICATOR.isSolver(wrapperCalls[i].target), + NotAWrapper(i, wrapperCalls[i].target, address(WRAPPER_AUTHENTICATOR)) + ); } // Get the expected settlement from the first wrapper @@ -93,9 +99,10 @@ contract CowWrapperHelpers { for (uint256 i = 0; i < wrapperCalls.length; i++) { // All wrappers must use the same settlement contract address wrapperSettlement = address(ICowWrapper(wrapperCalls[i].target).SETTLEMENT()); - if (wrapperSettlement != expectedSettlement) { - revert SettlementMismatch(i, expectedSettlement, wrapperSettlement); - } + + require( + wrapperSettlement == expectedSettlement, SettlementMismatch(i, expectedSettlement, wrapperSettlement) + ); // The wrapper data must be parsable and fully consumed try ICowWrapper(wrapperCalls[i].target).parseWrapperData(wrapperCalls[i].data) returns ( @@ -114,13 +121,14 @@ contract CowWrapperHelpers { revert SettlementContractShouldNotBeSolver(expectedSettlement, address(SOLVER_AUTHENTICATOR)); } - // Build wrapper data without settlement address at the end - wrapperData = abi.encodePacked(uint16(wrapperCalls[0].data.length), wrapperCalls[0].data); + // Build wrapper data + for (uint256 i = 0; i < wrapperCalls.length; i++) { + if (i > 0) { + wrapperData = abi.encodePacked(wrapperData, wrapperCalls[i].target); + } - for (uint256 i = 1; i < wrapperCalls.length; i++) { - wrapperData = abi.encodePacked( - wrapperData, wrapperCalls[i].target, uint16(wrapperCalls[i].data.length), wrapperCalls[i].data - ); + require(wrapperCalls[i].data.length < 65536, WrapperDataTooLong(i, wrapperCalls[i].data.length)); + wrapperData = abi.encodePacked(wrapperData, uint16(wrapperCalls[i].data.length), wrapperCalls[i].data); } return wrapperData; diff --git a/test/CowWrapperHelpers.t.sol b/test/CowWrapperHelpers.t.sol index 989f47b..c4e6dd1 100644 --- a/test/CowWrapperHelpers.t.sol +++ b/test/CowWrapperHelpers.t.sol @@ -246,8 +246,78 @@ contract CowWrapperHelpersTest is Test { assertEq(result, expected); } + function test_verifyAndBuildWrapperData_RevertsOnSettlementMismatch() public { + MockSettlement differentSettlement = new MockSettlement(CowAuthentication(address(wrapperAuth))); + MockWrapper differentWrapper = new MockWrapper(CowSettlement(address(differentSettlement)), 4); + wrapperAuth.addSolver(address(differentWrapper)); + + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](2); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({target: address(differentWrapper), data: hex"cafebabe"}); + + vm.expectRevert( + abi.encodeWithSelector( + CowWrapperHelpers.SettlementMismatch.selector, 1, address(mockSettlement), address(differentSettlement) + ) + ); + helpers.verifyAndBuildWrapperData(wrapperCalls); + } + function test_immutableAuthenticators() public view { assertEq(address(helpers.WRAPPER_AUTHENTICATOR()), address(wrapperAuth)); assertEq(address(helpers.SOLVER_AUTHENTICATOR()), address(solverAuth)); } + + function test_verifyAndBuildWrapperData_RevertsOnWrapperDataTooLong_FirstWrapper() public { + // Create data that's exactly 65536 bytes (exceeds uint16 max of 65535) + bytes memory tooLongData = new bytes(65536); + + // Create a wrapper that consumes all bytes passed to it + MockWrapper largeWrapper = new MockWrapper(CowSettlement(address(mockSettlement)), 65536); + wrapperAuth.addSolver(address(largeWrapper)); + + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(largeWrapper), data: tooLongData}); + + vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.WrapperDataTooLong.selector, 0, 65536)); + helpers.verifyAndBuildWrapperData(wrapperCalls); + } + + function test_verifyAndBuildWrapperData_RevertsOnWrapperDataTooLong_SecondWrapper() public { + // Create data that's exactly 65536 bytes for the second wrapper + bytes memory tooLongData = new bytes(65536); + + // Create a wrapper that consumes all bytes passed to it + MockWrapper largeWrapper = new MockWrapper(CowSettlement(address(mockSettlement)), 65536); + wrapperAuth.addSolver(address(largeWrapper)); + + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](2); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({target: address(largeWrapper), data: tooLongData}); + + vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.WrapperDataTooLong.selector, 1, 65536)); + helpers.verifyAndBuildWrapperData(wrapperCalls); + } + + function test_verifyAndBuildWrapperData_SucceedsWithMaxLengthData() public { + // Create data that's exactly 65535 bytes (max valid uint16) + bytes memory maxLengthData = new bytes(65535); + + // Create a wrapper that consumes all bytes + MockWrapper largeWrapper = new MockWrapper(CowSettlement(address(mockSettlement)), 65535); + wrapperAuth.addSolver(address(largeWrapper)); + + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(largeWrapper), data: maxLengthData}); + + // Should not revert - 65535 is the max valid length + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls); + + // Verify the length prefix is correct (first 2 bytes) + bytes2 lengthPrefix; + assembly { + lengthPrefix := mload(add(result, 32)) + } + assertEq(uint16(lengthPrefix), 65535); + } } From 6be9b27e66b01b18642e866afdd2d2f029492c1f Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 08:43:00 +0900 Subject: [PATCH 09/12] clean up the tests --- test/CowWrapperHelpers.t.sol | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/test/CowWrapperHelpers.t.sol b/test/CowWrapperHelpers.t.sol index 246705b..f643bdb 100644 --- a/test/CowWrapperHelpers.t.sol +++ b/test/CowWrapperHelpers.t.sol @@ -51,19 +51,6 @@ contract MockWrapper is CowWrapper { } } -contract BrokenWrapper is CowWrapper { - string public constant name = "Broken Wrapper"; - constructor(ICowSettlement settlement_) CowWrapper(settlement_) {} - - function _wrap(bytes calldata, bytes calldata, bytes calldata) internal override { - // Not used in these tests - } - - function parseWrapperData(bytes calldata) external pure override returns (bytes calldata) { - revert("Intentionally broken"); - } -} - contract CowWrapperHelpersTest is Test { CowWrapperHelpers helpers; MockAuthenticator wrapperAuth; @@ -73,7 +60,6 @@ contract CowWrapperHelpersTest is Test { MockWrapper wrapper1; MockWrapper wrapper2; MockWrapper wrapper3; - BrokenWrapper brokenWrapper; function setUp() public { wrapperAuth = new MockAuthenticator(); @@ -86,13 +72,11 @@ contract CowWrapperHelpersTest is Test { wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), 4); wrapper2 = new MockWrapper(ICowSettlement(address(mockSettlement)), 8); wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), 0); - brokenWrapper = new BrokenWrapper(ICowSettlement(address(mockSettlement))); // Add wrappers as solvers wrapperAuth.addSolver(address(wrapper1)); wrapperAuth.addSolver(address(wrapper2)); wrapperAuth.addSolver(address(wrapper3)); - wrapperAuth.addSolver(address(brokenWrapper)); } function test_verifyAndBuildWrapperData_EmptyArrays() public view { @@ -175,9 +159,12 @@ contract CowWrapperHelpersTest is Test { function test_verifyAndBuildWrapperData_RevertsOnWrapperDataMalformed() public { CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); - wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(brokenWrapper), data: hex"deadbeef"}); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); + + bytes memory errorData = hex"feab"; + vm.mockCallRevert(address(wrapper1), abi.encodeWithSelector(wrapper1.parseWrapperData.selector), errorData); - vm.expectRevert(); + vm.expectRevert(abi.encodeWithSelector(CowWrapperHelpers.WrapperDataMalformed.selector, 0, errorData)); helpers.verifyAndBuildWrapperData(wrapperCalls); } @@ -211,7 +198,7 @@ contract CowWrapperHelpersTest is Test { bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls); - // Should contain: data[0] + target[1] + data[1] (no settlement) + // Should contain: data[0] + target[1] + data[1] bytes memory expected = abi.encodePacked(uint16(0), hex"", address(wrapper3), uint16(0), hex""); assertEq(result, expected); } From 97af3e3db17ebdbc9bd962163640321820c92300 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 09:02:27 +0900 Subject: [PATCH 10/12] switch to be more like the rest of the unit testing files --- test/{ => unit}/CowWrapper.t.sol | 86 ++++++------------------- test/{ => unit}/CowWrapperHelpers.t.sol | 80 ++++++----------------- test/unit/mocks/MockCowProtocol.sol | 78 ++++++++++++++++++++++ 3 files changed, 114 insertions(+), 130 deletions(-) rename test/{ => unit}/CowWrapper.t.sol (71%) rename test/{ => unit}/CowWrapperHelpers.t.sol (84%) create mode 100644 test/unit/mocks/MockCowProtocol.sol diff --git a/test/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol similarity index 71% rename from test/CowWrapper.t.sol rename to test/unit/CowWrapper.t.sol index 7545430..ea928d7 100644 --- a/test/CowWrapper.t.sol +++ b/test/unit/CowWrapper.t.sol @@ -2,93 +2,43 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapper, ICowSettlement, ICowAuthentication} from "../src/CowWrapper.sol"; -import {EmptyWrapper} from "./EmptyWrapper.sol"; +import {CowWrapper, ICowSettlement, ICowAuthentication} from "../../src/CowWrapper.sol"; +import {EmptyWrapper} from "../EmptyWrapper.sol"; -import {CowWrapperHelpers} from "../src/CowWrapperHelpers.sol"; +import {CowWrapperHelpers} from "../../src/CowWrapperHelpers.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( - address[] calldata, - uint256[] calldata, - ICowSettlement.Trade[] calldata, - ICowSettlement.Interaction[][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); - } - - function parseWrapperData(bytes calldata wrapperData) - external - pure - override - returns (bytes calldata remainingWrapperData) - { - // always pretend to consume all the wrapper data - return wrapperData[0:0]; - } -} +import {MockCowSettlement, MockCowAuthentication, MockWrapper} from "./mocks/MockCowProtocol.sol"; contract CowWrapperTest is Test { - MockAuthentication public authenticator; - MockSettlement public mockSettlement; + MockCowAuthentication public authenticator; + MockCowSettlement public mockSettlement; CowWrapperHelpers public helpers; address public solver; - TestWrapper private wrapper1; - TestWrapper private wrapper2; - TestWrapper 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)); helpers = new CowWrapperHelpers(ICowAuthentication(address(authenticator)), ICowAuthentication(address(authenticator))); solver = makeAddr("solver"); // Add solver to the authenticator - authenticator.addSolver(solver); + authenticator.setSolver(solver, true); // Create test wrappers - wrapper1 = new TestWrapper(ICowSettlement(address(mockSettlement))); - wrapper2 = new TestWrapper(ICowSettlement(address(mockSettlement))); - wrapper3 = new TestWrapper(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(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 (ICowSettlement.Interaction[][3] memory) { diff --git a/test/CowWrapperHelpers.t.sol b/test/unit/CowWrapperHelpers.t.sol similarity index 84% rename from test/CowWrapperHelpers.t.sol rename to test/unit/CowWrapperHelpers.t.sol index f643bdb..2d74601 100644 --- a/test/CowWrapperHelpers.t.sol +++ b/test/unit/CowWrapperHelpers.t.sol @@ -2,71 +2,27 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; -import {CowWrapperHelpers} from "../src/CowWrapperHelpers.sol"; -import {CowWrapper, ICowAuthentication, ICowSettlement} from "../src/CowWrapper.sol"; +import {CowWrapperHelpers} from "../../src/CowWrapperHelpers.sol"; +import {CowWrapper, ICowAuthentication, ICowSettlement} from "../../src/CowWrapper.sol"; -contract MockAuthenticator { - 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; - } -} - -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, bytes calldata, bytes calldata) internal override { - // Not used in these tests - } - - function parseWrapperData(bytes calldata wrapperData) - external - view - override - returns (bytes calldata remainingWrapperData) - { - return wrapperData[consumeBytes:]; - } -} +import {MockCowSettlement, MockCowAuthentication, MockWrapper} from "./mocks/MockCowProtocol.sol"; contract CowWrapperHelpersTest is Test { CowWrapperHelpers helpers; - MockAuthenticator wrapperAuth; - MockAuthenticator solverAuth; - MockSettlement mockSettlement; + MockCowAuthentication wrapperAuth; + MockCowAuthentication solverAuth; + MockCowSettlement mockSettlement; MockWrapper wrapper1; MockWrapper wrapper2; MockWrapper wrapper3; function setUp() public { - wrapperAuth = new MockAuthenticator(); - solverAuth = new MockAuthenticator(); + wrapperAuth = new MockCowAuthentication(); + solverAuth = new MockCowAuthentication(); helpers = new CowWrapperHelpers(ICowAuthentication(address(wrapperAuth)), ICowAuthentication(address(solverAuth))); - mockSettlement = new MockSettlement(ICowAuthentication(address(wrapperAuth))); + mockSettlement = new MockCowSettlement(address(wrapperAuth)); // Create mock wrappers wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), 4); @@ -74,9 +30,9 @@ contract CowWrapperHelpersTest is Test { wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), 0); // Add wrappers as solvers - wrapperAuth.addSolver(address(wrapper1)); - wrapperAuth.addSolver(address(wrapper2)); - wrapperAuth.addSolver(address(wrapper3)); + wrapperAuth.setSolver(address(wrapper1), true); + wrapperAuth.setSolver(address(wrapper2), true); + wrapperAuth.setSolver(address(wrapper3), true); } function test_verifyAndBuildWrapperData_EmptyArrays() public view { @@ -170,7 +126,7 @@ contract CowWrapperHelpersTest is Test { function test_verifyAndBuildWrapperData_RevertsOnSettlementContractShouldNotBeSolver() public { // Add settlement as a solver (which should not be allowed) - solverAuth.addSolver(address(mockSettlement)); + solverAuth.setSolver(address(mockSettlement), true); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); @@ -234,9 +190,9 @@ contract CowWrapperHelpersTest is Test { } function test_verifyAndBuildWrapperData_RevertsOnSettlementMismatch() public { - MockSettlement differentSettlement = new MockSettlement(ICowAuthentication(address(wrapperAuth))); + MockCowSettlement differentSettlement = new MockCowSettlement(address(wrapperAuth)); MockWrapper differentWrapper = new MockWrapper(ICowSettlement(address(differentSettlement)), 4); - wrapperAuth.addSolver(address(differentWrapper)); + wrapperAuth.setSolver(address(differentWrapper), true); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](2); wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); @@ -261,7 +217,7 @@ contract CowWrapperHelpersTest is Test { // Create a wrapper that consumes all bytes passed to it MockWrapper largeWrapper = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); - wrapperAuth.addSolver(address(largeWrapper)); + wrapperAuth.setSolver(address(largeWrapper), true); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(largeWrapper), data: tooLongData}); @@ -276,7 +232,7 @@ contract CowWrapperHelpersTest is Test { // Create a wrapper that consumes all bytes passed to it MockWrapper largeWrapper = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); - wrapperAuth.addSolver(address(largeWrapper)); + wrapperAuth.setSolver(address(largeWrapper), true); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](2); wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); @@ -292,7 +248,7 @@ contract CowWrapperHelpersTest is Test { // Create a wrapper that consumes all bytes MockWrapper largeWrapper = new MockWrapper(ICowSettlement(address(mockSettlement)), 65535); - wrapperAuth.addSolver(address(largeWrapper)); + wrapperAuth.setSolver(address(largeWrapper), true); CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(largeWrapper), data: maxLengthData}); diff --git a/test/unit/mocks/MockCowProtocol.sol b/test/unit/mocks/MockCowProtocol.sol new file mode 100644 index 0000000..50de37e --- /dev/null +++ b/test/unit/mocks/MockCowProtocol.sol @@ -0,0 +1,78 @@ +// 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); + } + + function parseWrapperData(bytes calldata wrapperData) + external + view + override + returns (bytes calldata remainingWrapperData) + { + // consume up to `consumeBytes` bytes + return wrapperData[(consumeBytes < wrapperData.length ? consumeBytes : wrapperData.length):]; + } +} From a9918c6a84177bdff48379dd9f2965bf7686bfe5 Mon Sep 17 00:00:00 2001 From: Kaze Date: Wed, 5 Nov 2025 23:07:34 +0900 Subject: [PATCH 11/12] format --- test/unit/CowWrapper.t.sol | 15 +++++++++++---- test/unit/CowWrapperHelpers.t.sol | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/test/unit/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol index ea928d7..c9ce951 100644 --- a/test/unit/CowWrapper.t.sol +++ b/test/unit/CowWrapper.t.sol @@ -23,8 +23,9 @@ contract CowWrapperTest is Test { // Deploy mock contracts authenticator = new MockCowAuthentication(); mockSettlement = new MockCowSettlement(address(authenticator)); - helpers = - new CowWrapperHelpers(ICowAuthentication(address(authenticator)), ICowAuthentication(address(authenticator))); + helpers = new CowWrapperHelpers( + ICowAuthentication(address(authenticator)), ICowAuthentication(address(authenticator)) + ); solver = makeAddr("solver"); // Add solver to the authenticator @@ -42,7 +43,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) { @@ -120,7 +126,8 @@ contract CowWrapperTest is Test { signature: hex"aabbccddee" }); - bytes memory settleData = abi.encodeCall(ICowSettlement.settle, (tokens, clearingPrices, trades, _emptyInteractions())); + bytes memory settleData = + abi.encodeCall(ICowSettlement.settle, (tokens, clearingPrices, trades, _emptyInteractions())); // Build the chained wrapper data: // solver -> wrapper1 -> wrapper2 -> wrapper1 -> wrapper3 -> mockSettlement diff --git a/test/unit/CowWrapperHelpers.t.sol b/test/unit/CowWrapperHelpers.t.sol index 2d74601..2d02f8d 100644 --- a/test/unit/CowWrapperHelpers.t.sol +++ b/test/unit/CowWrapperHelpers.t.sol @@ -20,7 +20,8 @@ contract CowWrapperHelpersTest is Test { function setUp() public { wrapperAuth = new MockCowAuthentication(); solverAuth = new MockCowAuthentication(); - helpers = new CowWrapperHelpers(ICowAuthentication(address(wrapperAuth)), ICowAuthentication(address(solverAuth))); + helpers = + new CowWrapperHelpers(ICowAuthentication(address(wrapperAuth)), ICowAuthentication(address(solverAuth))); mockSettlement = new MockCowSettlement(address(wrapperAuth)); From 07b897a32f38abee34eb4352d2df09c1058fc6c8 Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 10:30:01 +0900 Subject: [PATCH 12/12] changes from upstream --- test/unit/CowWrapper.t.sol | 1 - test/unit/CowWrapperHelpers.t.sol | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/unit/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol index c9ce951..eaa32f4 100644 --- a/test/unit/CowWrapper.t.sol +++ b/test/unit/CowWrapper.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; import {CowWrapper, ICowSettlement, ICowAuthentication} from "../../src/CowWrapper.sol"; -import {EmptyWrapper} from "../EmptyWrapper.sol"; import {CowWrapperHelpers} from "../../src/CowWrapperHelpers.sol"; diff --git a/test/unit/CowWrapperHelpers.t.sol b/test/unit/CowWrapperHelpers.t.sol index 2d02f8d..9cd2817 100644 --- a/test/unit/CowWrapperHelpers.t.sol +++ b/test/unit/CowWrapperHelpers.t.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8; import {Test} from "forge-std/Test.sol"; import {CowWrapperHelpers} from "../../src/CowWrapperHelpers.sol"; -import {CowWrapper, ICowAuthentication, ICowSettlement} from "../../src/CowWrapper.sol"; - +import {ICowAuthentication, ICowSettlement} from "../../src/CowWrapper.sol"; import {MockCowSettlement, MockCowAuthentication, MockWrapper} from "./mocks/MockCowProtocol.sol"; contract CowWrapperHelpersTest is Test {