diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol index 42e6621..273b6f6 100644 --- a/src/CowWrapper.sol +++ b/src/CowWrapper.sol @@ -91,6 +91,13 @@ interface ICowWrapper { /// @param wrapperData Encoded data for this wrapper and the chain of next wrappers/settlement. /// Format: [2-byte len][wrapper-specific-data][next-address]([2-byte len][wrapper-specific-data][next-address]...) function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external; + + /// @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); } /// @title CoW Protocol Wrapper Base Contract @@ -144,6 +151,17 @@ abstract contract CowWrapper is ICowWrapper { _wrap(settleData, wrapperData[2:remainingWrapperDataStart], wrapperData[remainingWrapperDataStart:]); } + /// @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 + 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 /// then eventually call _next() to continue the wrapped settlement chain. diff --git a/src/CowWrapperHelpers.sol b/src/CowWrapperHelpers.sol new file mode 100644 index 0000000..2c9c3e9 --- /dev/null +++ b/src/CowWrapperHelpers.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.7.6 <0.9.0; +pragma abicoder v2; + +import {ICowAuthentication, ICowWrapper} from "./CowWrapper.sol"; + +/// @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 { + /// @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 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 + /// @param authenticatorContract The authentication contract that authenticated the settlement as a solver + error SettlementContractShouldNotBeSolver(address settlementContract, address authenticatorContract); + + /// @notice Thrown when wrappers in the chain use different settlement contracts + /// @param wrapperIndex The index of the wrapper with a mismatched settlement + /// @param expectedSettlement The settlement contract used by the first wrapper + /// @param actualSettlement The settlement contract used by this wrapper + error SettlementMismatch(uint256 wrapperIndex, address expectedSettlement, address actualSettlement); + + /// @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; + } + + /// @notice The authentication contract used to verify wrapper contracts + ICowAuthentication public immutable WRAPPER_AUTHENTICATOR; + + /// @notice The authentication contract used to verify solvers + ICowAuthentication public immutable SOLVER_AUTHENTICATOR; + + /// @notice Constructs a new CowWrapperHelpers contract + /// @param wrapperAuthenticator_ The ICowAuthentication contract used to verify wrapper contracts + /// @param solverAuthenticator_ The ICowAuthentication contract used to verify solvers + constructor(ICowAuthentication wrapperAuthenticator_, ICowAuthentication solverAuthenticator_) { + 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 each wrapper is authenticated via WRAPPER_AUTHENTICATOR + /// 2. Verifies each wrapper's data is valid and fully consumed by calling parseWrapperData + /// 3. Verifies all wrappers use the same settlement contract (from first wrapper's SETTLEMENT) + /// 4. Verifies the settlement contract is not authenticated as a solver + /// The returned wrapper data format is: [data0][addr1][data1][addr2][data2]... + /// where data0 is for the first wrapper, addr1 is the second wrapper address, etc. + /// 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) + { + if (wrapperCalls.length == 0) { + return wrapperData; + } + + // First pass: verify all wrappers are authenticated + for (uint256 i = 0; i < wrapperCalls.length; i++) { + require( + WRAPPER_AUTHENTICATOR.isSolver(wrapperCalls[i].target), + NotAWrapper(i, wrapperCalls[i].target, address(WRAPPER_AUTHENTICATOR)) + ); + } + + // Get the expected settlement from the first wrapper + address expectedSettlement = address(ICowWrapper(wrapperCalls[0].target).SETTLEMENT()); + + for (uint256 i = 0; i < wrapperCalls.length; i++) { + // All wrappers must use the same settlement contract + address wrapperSettlement = address(ICowWrapper(wrapperCalls[i].target).SETTLEMENT()); + + 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 ( + 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(expectedSettlement)) { + revert SettlementContractShouldNotBeSolver(expectedSettlement, address(SOLVER_AUTHENTICATOR)); + } + + // Build wrapper data + for (uint256 i = 0; i < wrapperCalls.length; i++) { + if (i > 0) { + wrapperData = abi.encodePacked(wrapperData, wrapperCalls[i].target); + } + + 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/EmptyWrapper.sol b/test/EmptyWrapper.sol index 99a2aca..41cd4cd 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; @@ -12,4 +12,14 @@ contract EmptyWrapper is CowWrapper { 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) + { + // 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 a830af9..0000000 --- a/test/helpers/CowWrapperHelpers.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8; - -import {ICowSettlement, ICowAuthentication, ICowWrapper} from "src/CowWrapper.sol"; - -library CowWrapperHelpers { - struct SettleCall { - address[] tokens; - uint256[] clearingPrices; - ICowSettlement.Trade[] trades; - ICowSettlement.Interaction[][3] interactions; - } - - /** - * @dev This function is intended for testing purposes and is not memory efficient. - * @param wrappers Array of wrapper addresses to chain together - * @param wrapperDatas Array of wrapper-specific data for each wrapper - * @param settlement The settlement call parameters - */ - function encodeWrapperCall( - address[] calldata wrappers, - bytes[] calldata wrapperDatas, - address, - SettleCall calldata settlement - ) external pure 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, uint16(wrapperDatas[i].length), wrapperDatas[i]); - // Include the next wrapper address if there is one - if (wrappers.length > i + 1) { - wrapperData = abi.encodePacked(wrapperData, wrappers[i + 1]); - } - // For the last wrapper, don't add anything - the static SETTLEMENT will be called automatically - } - - // Build the settle calldata - bytes memory settleData = abi.encodeWithSelector( - ICowSettlement.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); - } -} diff --git a/test/unit/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol index e15f0a5..eaa32f4 100644 --- a/test/unit/CowWrapper.t.sol +++ b/test/unit/CowWrapper.t.sol @@ -3,15 +3,15 @@ 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 {MockWrapper, MockCowSettlement, MockCowAuthentication} from "./mocks/MockCowProtocol.sol"; +import {CowWrapperHelpers} from "../../src/CowWrapperHelpers.sol"; -import {CowWrapperHelpers} from "../helpers/CowWrapperHelpers.sol"; +import {MockCowSettlement, MockCowAuthentication, MockWrapper} from "./mocks/MockCowProtocol.sol"; contract CowWrapperTest is Test { MockCowAuthentication public authenticator; MockCowSettlement public mockSettlement; + CowWrapperHelpers public helpers; address public solver; MockWrapper private wrapper1; @@ -22,12 +22,15 @@ contract CowWrapperTest is Test { // Deploy mock contracts 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.setSolver(solver, true); - // Create test wrapper and three EmptyWrapper instances with the settlement contract + // Create test wrappers wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); wrapper2 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), 65536); @@ -99,17 +102,16 @@ contract CowWrapperTest is Test { } function test_integration_ThreeWrappersChained() public { - // Set up a more sophisticated settlement call to make sure it all gets through as expected. - CowWrapperHelpers.SettleCall memory settlement; - settlement.tokens = new address[](2); - settlement.tokens[0] = address(0x1); - settlement.tokens[1] = address(0x2); - settlement.clearingPrices = new uint256[](2); - settlement.clearingPrices[0] = 100; - settlement.clearingPrices[1] = 200; - - settlement.trades = new ICowSettlement.Trade[](1); - settlement.trades[0] = ICowSettlement.Trade({ + address[] memory tokens = new address[](2); + tokens[0] = address(0x1); + tokens[1] = address(0x2); + + uint256[] memory clearingPrices = new uint256[](2); + clearingPrices[0] = 100; + clearingPrices[1] = 200; + + ICowSettlement.Trade[] memory trades = new ICowSettlement.Trade[](1); + trades[0] = ICowSettlement.Trade({ sellTokenIndex: 0, buyTokenIndex: 1, receiver: address(0x123), @@ -123,38 +125,29 @@ contract CowWrapperTest is Test { signature: hex"aabbccddee" }); - settlement.interactions = [ - new ICowSettlement.Interaction[](0), - new ICowSettlement.Interaction[](0), - new ICowSettlement.Interaction[](0) - ]; + bytes memory settleData = + abi.encodeCall(ICowSettlement.settle, (tokens, clearingPrices, trades, _emptyInteractions())); // Build the chained wrapper data: // solver -> wrapper1 -> wrapper2 -> wrapper1 -> wrapper3 -> mockSettlement - address[] memory wrappers = new address[](4); - wrappers[0] = address(wrapper1); - wrappers[1] = address(wrapper2); - wrappers[2] = address(wrapper1); - wrappers[3] = address(wrapper3); + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](4); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex""}); + wrapperCalls[1] = CowWrapperHelpers.WrapperCall({target: address(wrapper2), data: hex""}); + wrapperCalls[2] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"828348"}); + wrapperCalls[3] = CowWrapperHelpers.WrapperCall({target: address(wrapper3), data: hex""}); - bytes[] memory datas = new bytes[](4); - - datas[2] = hex"828348"; - - (address target, bytes memory fullCalldata) = - CowWrapperHelpers.encodeWrapperCall(wrappers, datas, address(mockSettlement), settlement); + bytes memory wrapperData = helpers.verifyAndBuildWrapperData(wrapperCalls); // all the wrappers gets called, with wrapper 1 called twice vm.expectCall(address(wrapper1), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 2); - vm.expectCall(address(wrapper2), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 1); - vm.expectCall(address(wrapper3), 0, abi.encodeWithSelector(wrapper1.wrappedSettle.selector), 1); + vm.expectCall(address(wrapper2), 0, abi.encodeWithSelector(wrapper2.wrappedSettle.selector), 1); + vm.expectCall(address(wrapper3), 0, abi.encodeWithSelector(wrapper3.wrappedSettle.selector), 1); // the settlement gets called with the full data vm.expectCall(address(mockSettlement), new bytes(0)); // Call wrapper1 as the solver vm.prank(solver); - (bool success,) = target.call(fullCalldata); - assertTrue(success, "Chained wrapper call should succeed"); + wrapper1.wrappedSettle(settleData, wrapperData); } } diff --git a/test/unit/CowWrapperHelpers.t.sol b/test/unit/CowWrapperHelpers.t.sol new file mode 100644 index 0000000..9cd2817 --- /dev/null +++ b/test/unit/CowWrapperHelpers.t.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {Test} from "forge-std/Test.sol"; +import {CowWrapperHelpers} from "../../src/CowWrapperHelpers.sol"; +import {ICowAuthentication, ICowSettlement} from "../../src/CowWrapper.sol"; +import {MockCowSettlement, MockCowAuthentication, MockWrapper} from "./mocks/MockCowProtocol.sol"; + +contract CowWrapperHelpersTest is Test { + CowWrapperHelpers helpers; + MockCowAuthentication wrapperAuth; + MockCowAuthentication solverAuth; + MockCowSettlement mockSettlement; + + MockWrapper wrapper1; + MockWrapper wrapper2; + MockWrapper wrapper3; + + function setUp() public { + wrapperAuth = new MockCowAuthentication(); + solverAuth = new MockCowAuthentication(); + helpers = + new CowWrapperHelpers(ICowAuthentication(address(wrapperAuth)), ICowAuthentication(address(solverAuth))); + + mockSettlement = new MockCowSettlement(address(wrapperAuth)); + + // Create mock wrappers + wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), 4); + wrapper2 = new MockWrapper(ICowSettlement(address(mockSettlement)), 8); + wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), 0); + + // Add wrappers as solvers + wrapperAuth.setSolver(address(wrapper1), true); + wrapperAuth.setSolver(address(wrapper2), true); + wrapperAuth.setSolver(address(wrapper3), true); + } + + function test_verifyAndBuildWrapperData_EmptyArrays() public view { + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](0); + + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls); + + // Should be empty + assertEq(result, hex""); + } + + function test_verifyAndBuildWrapperData_SingleWrapper() public view { + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); + + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls); + + // Should contain: data[0] only (no settlement appended) + bytes memory expected = abi.encodePacked(uint16(4), hex"deadbeef"); + assertEq(result, expected); + } + + 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""}); + + bytes memory result = helpers.verifyAndBuildWrapperData(wrapperCalls); + + // Should contain: data[0] + target[1] + data[1] + target[2] + data[2] (no settlement) + bytes memory expected = abi.encodePacked( + uint16(4), + hex"deadbeef", + address(wrapper2), + uint16(8), + hex"cafebabe12345678", + address(wrapper3), + uint16(0), + hex"" + ); + assertEq(result, expected); + } + + function test_verifyAndBuildWrapperData_RevertsOnNotAWrapper() public { + address notAWrapper = makeAddr("notAWrapper"); + + 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(wrapperCalls); + } + + function test_verifyAndBuildWrapperData_RevertsOnNotAWrapper_SecondWrapper() public { + 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""}); + + vm.expectRevert( + abi.encodeWithSelector(CowWrapperHelpers.NotAWrapper.selector, 1, notAWrapper, address(wrapperAuth)) + ); + helpers.verifyAndBuildWrapperData(wrapperCalls); + } + + function test_verifyAndBuildWrapperData_RevertsOnWrapperDataNotFullyConsumed() public { + 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(wrapperCalls); + } + + function test_verifyAndBuildWrapperData_RevertsOnWrapperDataMalformed() public { + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + 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(abi.encodeWithSelector(CowWrapperHelpers.WrapperDataMalformed.selector, 0, errorData)); + helpers.verifyAndBuildWrapperData(wrapperCalls); + } + + function test_verifyAndBuildWrapperData_RevertsOnSettlementContractShouldNotBeSolver() public { + // Add settlement as a solver (which should not be allowed) + solverAuth.setSolver(address(mockSettlement), true); + + CowWrapperHelpers.WrapperCall[] memory wrapperCalls = new CowWrapperHelpers.WrapperCall[](1); + wrapperCalls[0] = CowWrapperHelpers.WrapperCall({target: address(wrapper1), data: hex"deadbeef"}); + + vm.expectRevert( + abi.encodeWithSelector( + CowWrapperHelpers.SettlementContractShouldNotBeSolver.selector, + address(mockSettlement), + address(solverAuth) + ) + ); + helpers.verifyAndBuildWrapperData(wrapperCalls); + } + + function test_verifyAndBuildWrapperData_EmptyWrapperData() public view { + 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); + + // Should contain: data[0] + target[1] + data[1] + bytes memory expected = abi.encodePacked(uint16(0), hex"", address(wrapper3), uint16(0), hex""); + assertEq(result, expected); + } + + function test_verifyAndBuildWrapperData_MixedWrapperDataSizes() public view { + 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); + + bytes memory expected = abi.encodePacked( + uint16(0), + hex"", + address(wrapper1), + uint16(4), + hex"deadbeef", + address(wrapper2), + uint16(8), + hex"cafebabe12345678" + ); + assertEq(result, expected); + } + + function test_verifyAndBuildWrapperData_RevertsOnSettlementMismatch() public { + MockCowSettlement differentSettlement = new MockCowSettlement(address(wrapperAuth)); + MockWrapper differentWrapper = new MockWrapper(ICowSettlement(address(differentSettlement)), 4); + wrapperAuth.setSolver(address(differentWrapper), true); + + 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(ICowSettlement(address(mockSettlement)), 65536); + wrapperAuth.setSolver(address(largeWrapper), true); + + 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(ICowSettlement(address(mockSettlement)), 65536); + wrapperAuth.setSolver(address(largeWrapper), true); + + 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(ICowSettlement(address(mockSettlement)), 65535); + wrapperAuth.setSolver(address(largeWrapper), true); + + 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); + } +} diff --git a/test/unit/mocks/MockCowProtocol.sol b/test/unit/mocks/MockCowProtocol.sol index aaffbd9..e586abb 100644 --- a/test/unit/mocks/MockCowProtocol.sol +++ b/test/unit/mocks/MockCowProtocol.sol @@ -65,4 +65,14 @@ contract MockWrapper is CowWrapper { 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):]; + } }