L-1: Wrappers lack sufficient post-settlement validation
Summary:
All three wrappers lack adequate verification that the CoW settlement actually executed the user's trade. A compromised or malicious solver can manipulate the settlement to bypass existing checks, consuming wrapper-level authorization and degrading the user's position.
Description:
Since settleData is fully solver-controlled, a solver can call settle() with empty trades (or trades that exclude the user's order), or use the settlement's interactions to send trivial token amounts that bypass existing checks.
Close position: The existing NoSwapOutput check (CowEvcClosePositionWrapper.sol#L222-L224) is trivially bypassed — a solver can send 1 wei of the borrow asset to the Inbox via settlement interactions. The check passes, the wrapper repays 1 wei of debt, and the unused collateral vault tokens are returned to the user's account (L248-252). Since the collateral vault remains enabled, the health check passes with the position essentially unchanged, but the wrapper authorization is consumed.
Open position: No post-settlement check exists in _evcInternalSettle. The batch items before settlement execute (enableCollateral, enableController, deposit collateral, borrow tokens). Without the swap converting borrowed tokens to additional collateral, the EVC health check will likely revert the batch due to undercollateralization. However, if the user's initial collateralAmount is large relative to borrowAmount, the health check could pass — leaving the user with an open debt position and unswapped borrowed tokens. The wrapper's pre-approved hash or permit nonce is consumed.
Collateral swap: No post-settlement check exists in _evcInternalSettle. When owner == account and disableSourceCollateral == false, the only pre-settlement action is enableCollateral(toVault). A no-op settlement means nothing changes, the health check passes trivially, and the wrapper-level authorization is consumed for nothing.
In all cases, the CoW order itself is not invalidated on the settlement contract (filledAmount is not updated), but the wrapper-level authorization is burned, forcing the user to sign a new permit or submit a new pre-approved hash.
Impact:
Low. Requires a compromised or malicious solver (bonded actors subject to slashing). No direct fund loss — users retain their assets in all scenarios (close position collateral is recoverable from the Inbox). The impact is limited to griefing: consuming wrapper-level authorizations, forcing users to re-authorize on-chain, and potentially degrading health factors.
Recommendation:
Add a user-specified minimum output field to each wrapper's params struct. Since params are included in both the pre-approved hash and the EVC permit signature, the minimum is automatically authenticated without any changes to the authorization flows.
Close position — add minDebtAssetOut to ClosePositionParams and replace the existing NoSwapOutput check in _evcInternalSettle:
struct ClosePositionParams {
// ... existing fields ...
uint256 minDebtAssetOut; // minimum borrow asset received from swap
}
// In _evcInternalSettle, replace the NoSwapOutput check:
uint256 swapOutput = swapResultBalance - swapBeforeResultBalance;
require(
swapOutput >= params.minDebtAssetOut,
InsufficientSwapOutput(swapOutput, params.minDebtAssetOut)
);
Open position — add minCollateral to OpenPositionParams and check in _evcInternalSettle:
struct OpenPositionParams {
// ... existing fields ...
uint256 minCollateral; // minimum collateral vault token balance after settlement
}
// In _evcInternalSettle:
_next(settleData, remainingWrapperData);
require(
IERC20(collateralVault).balanceOf(params.account) >= params.minCollateral,
InsufficientCollateral(params.account)
);
Collateral swap — add minCollateral to CollateralSwapParams and check in _evcInternalSettle:
struct CollateralSwapParams {
// ... existing fields ...
uint256 minCollateral; // minimum destination vault token balance after settlement
}
// In _evcInternalSettle:
_next(settleData, remainingWrapperData);
require(
IERC20(params.toVault).balanceOf(params.account) >= params.minCollateral,
InsufficientCollateral(params.account)
);
This provides explicit wrapper-level slippage protection, prevents no-op and trivial-amount settlement griefing, and is automatically covered by both authorization flows since the new fields are part of the hashed/signed params struct.
Developer Response:
Acknowledged.