From 5a87a8d1b6b94be472b4b571205d3510f4349c26 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Thu, 30 Jul 2026 09:48:25 +0000 Subject: [PATCH] feat!: separate swap-guard restriction from the generator verdict --- src/ComposableCow.sol | 32 +++++++++++++++++++++++++------- test/ComposableCow.guards.t.sol | 14 ++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/ComposableCow.sol b/src/ComposableCow.sol index f1655887..8b61eaf9 100644 --- a/src/ComposableCow.sol +++ b/src/ComposableCow.sol @@ -78,10 +78,25 @@ contract ComposableCow is ISafeSignatureVerifier { * @param filledAmount raw `GPv2Settlement.filledAmount` for the discrete * order (`type(uint256).max` when invalidated) */ + /** + * @dev Registry-level restriction overlay. Like the fill overlay, this is + * orthogonal to the handler's verdict and never overwrites it: a + * restricted order keeps its true generator verdict (a guarded POST + * stays visible) but no signature is built while restricted. With + * restriction expressed here, `INVALID` is uniformly terminal - the + * swap guard is owner-reversible and its lifecycle is observable via + * `SwapGuardSet` (clear = address(0)). + */ + enum Restriction { + NONE, // No registry-level restriction + SWAP_GUARD // The owner's swap guard rejected the order + } + struct PollResult { IConditionalOrderGenerator.GeneratorResult generator; FillStatus fill; uint256 filledAmount; + Restriction restriction; } // --- events @@ -303,10 +318,9 @@ contract ComposableCow is ISafeSignatureVerifier { return (result, bytes("")); } - // Check with the swap guard if the order is restricted or not - if (!_guardCheck(owner, ctx, params, offchainInput, result.generator.order)) { - result.generator.code = IConditionalOrderGenerator.GeneratorResultCode.INVALID; - result.generator.reasonCode = SwapGuardRestricted.selector; + // A restricted order is never signed; the generator verdict is + // preserved in the result (restriction is an overlay, not a verdict) + if (result.restriction != Restriction.NONE) { return (result, bytes("")); } @@ -316,8 +330,9 @@ contract ComposableCow is ISafeSignatureVerifier { /** * Check the current polling state of a conditional order * @dev Returns the same composed result as `getTradeableOrderWithSignature`, - * without building the signature. Note the swap guard is not consulted - * here; it is enforced at signature build time and during settlement. + * without building the signature. The swap guard is consulted and + * reported via `restriction`; enforcement (signature withheld, revert + * at settlement) happens at signature build and during `verify`. * @param owner of the order * @param params `ConditionalOrderParams` for the order * @param offchainInput any dynamic off-chain input for generating the discrete order @@ -427,8 +442,11 @@ contract ComposableCow is ISafeSignatureVerifier { result.generator = IConditionalOrderGenerator(address(params.handler)) .poll(owner, msg.sender, ctx, params.staticInput, offchainInput); - // The fill overlay is only meaningful for a postable order + // The fill and restriction overlays are only meaningful for a postable order if (result.generator.code == IConditionalOrderGenerator.GeneratorResultCode.POST) { + if (!_guardCheck(owner, ctx, params, offchainInput, result.generator.order)) { + result.restriction = Restriction.SWAP_GUARD; + } uint256 filledAmount = _getFilledAmount(owner, result.generator.order); result.filledAmount = filledAmount; if (filledAmount == 0) { diff --git a/test/ComposableCow.guards.t.sol b/test/ComposableCow.guards.t.sol index b016db72..d0d68c43 100644 --- a/test/ComposableCow.guards.t.sol +++ b/test/ComposableCow.guards.t.sol @@ -84,8 +84,11 @@ contract ComposableCowGuardsTest is BaseComposableCowTest { // should not return a signature as the swap guard doesn't allow it (ComposableCow.PollResult memory guardedRes, bytes memory guardedSig) = composableCow.getTradeableOrderWithSignature(address(safe1), params, bytes(""), proof); - assertEq(uint256(guardedRes.generator.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.INVALID)); - assertEq(guardedRes.generator.reasonCode, ComposableCow.SwapGuardRestricted.selector); + // The generator verdict survives the restriction: POST stays visible, + // the overlay carries the guard, and no signature is built + assertEq(uint256(guardedRes.generator.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.POST)); + assertEq(guardedRes.generator.reasonCode, bytes4(0)); + assertEq(uint256(guardedRes.restriction), uint256(ComposableCow.Restriction.SWAP_GUARD)); assertEq(guardedSig.length, 0); // should set the swap guard to the odd swap guard @@ -141,8 +144,11 @@ contract ComposableCowGuardsTest is BaseComposableCowTest { (ComposableCow.PollResult memory guardedRes, bytes memory guardedSig) = composableCow.getTradeableOrderWithSignature( address(safe1), params, abi.encode(orderOtherReceiver), new bytes32[](0) ); - assertEq(uint256(guardedRes.generator.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.INVALID)); - assertEq(guardedRes.generator.reasonCode, ComposableCow.SwapGuardRestricted.selector); + // The generator verdict survives the restriction: POST stays visible, + // the overlay carries the guard, and no signature is built + assertEq(uint256(guardedRes.generator.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.POST)); + assertEq(guardedRes.generator.reasonCode, bytes4(0)); + assertEq(uint256(guardedRes.restriction), uint256(ComposableCow.Restriction.SWAP_GUARD)); assertEq(guardedSig.length, 0); // should revert as the receiver is not the safe