Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a78f94a
feat: CowWrapper helpers
kaze-cow Oct 9, 2025
878ec33
clean up, add lots of documentation to wrappers, verify test coverage
kaze-cow Oct 9, 2025
45c1692
switch to more natural call object pattern
kaze-cow Oct 9, 2025
bfecf38
add docs
kaze-cow Oct 9, 2025
843c402
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 9, 2025
71fe841
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 9, 2025
a078b57
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 13, 2025
5a29003
renames, cleanups, make it build
kaze-cow Oct 13, 2025
4fd6194
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 13, 2025
7f82df8
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 17, 2025
d690b2f
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 17, 2025
4c91567
add memory safe annotation
kaze-cow Oct 20, 2025
5d8c295
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 22, 2025
5877000
chore: apply formatting
kaze-cow Oct 27, 2025
23911c6
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Oct 27, 2025
fc6501a
fix claude suggestions
kaze-cow Oct 27, 2025
11ce253
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Nov 4, 2025
6be9b27
clean up the tests
kaze-cow Nov 4, 2025
97af3e3
switch to be more like the rest of the unit testing files
kaze-cow Nov 5, 2025
66b9090
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Nov 5, 2025
a9918c6
format
kaze-cow Nov 5, 2025
57e52de
Merge branch 'feat/wrapper-base' into feat/wrapper-helpers
kaze-cow Nov 6, 2025
07b897a
changes from upstream
kaze-cow Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/CowWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation clarity: The NatSpec is excellent. One minor suggestion - consider adding an example of what a valid implementation might look like:

/// @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.
///      Example implementation:
///      ```solidity
///      function parseWrapperData(bytes calldata wrapperData)
///          external pure returns (bytes calldata remainingWrapperData)
///      {
///          // Decode first 32 bytes as our data
///          require(wrapperData.length >= 32, "Insufficient data");
///          // myData = abi.decode(wrapperData[:32], (MyStruct));
///          return wrapperData[32:];
///      }
///      ```

Though given the complexity of different wrapper implementations, the current documentation may be sufficient.

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.
Expand Down
136 changes: 136 additions & 0 deletions src/CowWrapperHelpers.sol
Original file line number Diff line number Diff line change
@@ -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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation note: Since this contract is intended for off-chain use only, consider adding a note about deployment strategy. Should this be deployed on all networks where wrappers will be used? Or is it expected to be called via eth_call (staticcall) without actual deployment?

If it's meant for off-chain static calls only, you might also consider adding a comment explaining that the contract doesn't need to be deployed for its intended use case.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor optimization opportunity: The settlement contract is fetched multiple times in the loop (once per wrapper). Consider fetching it once outside the loop and storing it, then just comparing inside the loop.

// Get the expected settlement from the first wrapper (already done)
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)
    );
    // ...
}

This is already quite efficient - the external call is only made once per wrapper. If further optimization is desired, you could skip checking i==0 since it's comparing with itself.

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 (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Wrapper data validation design is sound

The parseWrapperData validation approach effectively prevents malicious data injection:

  • Wrapper contracts must fully consume their data (line 111-112)
  • Any unconsumed data is rejected (WrapperDataNotFullyConsumed)
  • Invalid/malformed data causes WrapperDataMalformed revert (line 114-115)

This design forces wrapper authors to be explicit about their data format and prevents users from injecting unauthorized wrapper addresses into the chain.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent security design: The validation approach here is well thought out:

  1. Forces wrapper authors to implement parseWrapperData explicitly
  2. Catches malformed data through try-catch
  3. Prevents injection attacks by ensuring all data is consumed

This design elegantly solves the security concern raised in earlier reviews about users potentially injecting unauthorized wrapper addresses into the chain.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security consideration: This check is valuable but worth noting its limitations. The settlement contract being a "solver" is checked via SOLVER_AUTHENTICATOR, which is a separate authenticator from WRAPPER_AUTHENTICATOR.

This assumes:

  1. The two authenticators are properly managed
  2. The settlement contract is never added to SOLVER_AUTHENTICATOR

Consider documenting this assumption in the contract-level NatSpec or constructor, especially since misconfiguration here could bypass wrapper logic entirely.

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);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also check that the last wrapper is a solver? It's needed to settle but it could be nice to skip this check when testing. Maybe @MartinquaXD can comment on this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming you are referring to settlementContract as being the "last wrapper"... it has to not be a solver

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean the last wrapper in the chain (so wrapperData = encodePacked(..., lastWrapperAddress, lastWrapperData, settlementAddress)), this is possibly the only one that needs to be a solver. (Well, right now all wrappers should be solvers, but in principle they could just check that the caller is a solver or a wrapper.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, the helper isn't as "final" as the wrapper design, we can always deploy a new one and use that if we need to. I'd say we should check that the wrappers can settle, but maybe that's unneeded.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea true this is very malleable and view only

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok so I had a chance to slowly read this again and its making a bit more sense now.

in my mind, I have always thought of all wrappers as being solvers (effectively, a "wrapper" is a subset of "solver"). this is because wrappers can be specified in any order or with/without other wrappers, so it doesn't really make sense to assume one wrapper could not be a solver. this helper contract I suppose makes that same assumption.

require(wrapperCalls[i].data.length < 65536, WrapperDataTooLong(i, wrapperCalls[i].data.length));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great fix! The uint16 overflow edge case from the previous review has been properly addressed with:

  1. Clear error with diagnostic info (WrapperDataTooLong)
  2. Validation before encoding (require(wrapperCalls[i].data.length < 65536, ...))
  3. Comprehensive test coverage including edge cases at 65535 and 65536 bytes

This prevents silent truncation and makes debugging much easier.

wrapperData = abi.encodePacked(wrapperData, uint16(wrapperCalls[i].data.length), wrapperCalls[i].data);
}

return wrapperData;
}
}
12 changes: 11 additions & 1 deletion test/EmptyWrapper.sol
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
}
}
51 changes: 0 additions & 51 deletions test/helpers/CowWrapperHelpers.sol

This file was deleted.

63 changes: 28 additions & 35 deletions test/unit/CowWrapper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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),
Expand All @@ -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);
}
}
Loading
Loading