// SPDX-License-Identifier: MIT pragma solidity 0.8.28; import {BaseAdapter} from "../BaseAdapter.sol"; import {AgentRegistry} from "../AgentRegistry.sol"; interface IERC20 { function approve(address spender, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); } interface IGPv2Settlement { function domainSeparator() external view returns (bytes32); function vaultRelayer() external view returns (address); function setPreSignature(bytes calldata orderUid, bool signed) external; } /// @title CowPresignAdapter — place a CoW Protocol order with NO handed-out signature. /// /// CoW accepts an ON-CHAIN pre-signature: the account calls /// `setPreSignature(uid, true)` and the order becomes valid without anyone signing /// a hash. It is the right shape for this account for the reason the swap adapter /// wanted `recipient: account` — except CoW hides the recipient inside a HASH. /// /// A CoW order uid is `orderDigest(32) ++ owner(20) ++ validTo(4)`, and /// `orderDigest` is an EIP-712 hash over the whole order, RECEIVER INCLUDED. So an /// adapter that accepted a raw uid would be back at the blind-hash drain that /// `AGENT_KEY_ATTACK_VECTORS.md` (Class B) is about: B hands over a uid whose /// receiver is the attacker, the account presigns it, a solver settles it, the /// funds leave. This adapter therefore NEVER takes a uid. It takes the order's /// typed fields, forces `receiver = account`, and rebuilds the uid itself. The /// only uid this contract will ever presign is the one that delivers to the /// account — B cannot express a different receiver. /// /// CoW's own `setPreSignature` requires the uid's owner == msg.sender, and here /// msg.sender is the account (executeFromExecutor). Combined with owner forced to /// the account, the account can only ever presign its OWN, self-receiving orders. /// /// THE ONE DEPARTURE from every other adapter: this one leaves a standing /// allowance. Settlement is asynchronous and gives no callback, so the vault /// relayer must keep an allowance until a solver fills. That allowance is bounded /// to exactly the sell amount, is to CoW's immutable vault relayer (not an /// arbitrary spender), and can only be pulled inside a settlement of the presigned /// order — whose receiver is the account and whose floor is signed. So the /// allowance and the presignature together authorise exactly one fully specified, /// self-receiving order; `cancel` revokes both. One resting order per sell token /// at a time, because the allowance is fungible and a second `approve` would /// clobber the first — a ladder of orders is future work, not this version. contract CowPresignAdapter is BaseAdapter { /// What A authorises at install to enable each operation. Installing the /// module and authorising what it does are separate statements. bytes32 public constant OP_PRESIGN = "cow.presign"; bytes32 public constant OP_CANCEL = "cow.cancel"; /// CoW's GPv2Order type hash, PINNED to the value the deployed /// GPv2Settlement carries. /// /// This was transcribed from CoW's SOLIDITY STRUCT rather than from its /// EIP-712 type string, and the two differ: `kind`, `sellTokenBalance` and /// `buyTokenBalance` are declared `string` in the type string and stored as /// `bytes32` in the struct, because EIP-712 encodes a `string` member as /// `keccak256(bytes(x))` — which is exactly what `keccak256("sell")` and /// `keccak256("erc20")` already are. So the VALUES carried here were right /// and only the declared types were wrong: /// /// bytes32 variant: 0x1a59c8ff…252b51 (what this used to compute) /// string variant: 0xd5a25ba2…17e489 (CoW's GPv2Order.TYPE_HASH) /// /// It failed SILENTLY, which is why it survived. `setPreSignature` checks /// only that the uid is 56 bytes and that its embedded owner is /// `msg.sender` — never the digest — so the wrong uid stored fine and /// emitted, and the solver, computing the real uid, found no pre-signature /// there. Every pre-signed order was unfillable, at the cost of an epoch's /// cap, a standing vault-relayer allowance, and a `_live` slot that made /// every later presign of that token revert `OrderAlreadyResting`. /// /// A LITERAL, not a `keccak256` of a re-typed string, because the bug was a /// re-typed string. `test_TheOrderTypeHashIsCowsOwn` checks it against the /// deployed settlement contract's bytecode, which is the only source that /// cannot be transcribed wrongly here. bytes32 internal constant ORDER_TYPE_HASH = 0xd5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489; /// Exposed so a test can pin it against the deployed GPv2Settlement rather /// than against a copy of the line above. function orderTypeHash() external pure returns (bytes32) { return ORDER_TYPE_HASH; } bytes32 internal constant KIND_SELL = keccak256("sell"); bytes32 internal constant KIND_BUY = keccak256("buy"); bytes32 internal constant BALANCE_ERC20 = keccak256("erc20"); /// What B signs. NOTE the absence of a `receiver` field: it is not B's to /// choose. The adapter sets it to the account, always. /// /// Presign and Cancel are DISTINCT primary types over the same fields. A /// shared digest let a cancel signature verify on `presign`: after a full /// fill the allowance is already 0, `OrderAlreadyResting` does not fire, /// `_clearFilled` clears the old row, and the approve re-arms the relayer. /// The cancel taskId is then spent; the owner cannot revoke. bytes32 internal constant PRESIGN_TYPEHASH = keccak256( "Presign(address account,address owner,bytes32 taskId,uint256 deadline,bytes32 venue," "address sellToken,address buyToken,uint256 sellAmount,uint256 buyAmount,uint32 validTo," "bytes32 appData,uint256 feeAmount,bool sell,bool partiallyFillable)" ); bytes32 internal constant CANCEL_TYPEHASH = keccak256( "Cancel(address account,address owner,bytes32 taskId,uint256 deadline,bytes32 venue," "address sellToken,address buyToken,uint256 sellAmount,uint256 buyAmount,uint32 validTo," "bytes32 appData,uint256 feeAmount,bool sell,bool partiallyFillable)" ); struct Order { address account; // receiver AND uid-owner are both forced to this address owner; // whose account — checked against ownerOf(account) bytes32 taskId; uint256 deadline; bytes32 venue; // resolves to GPv2Settlement, from the set A installed address sellToken; address buyToken; uint256 sellAmount; // for a sell order the exact size; for a buy order the ceiling uint256 buyAmount; // the floor: least of buyToken the order accepts uint32 validTo; bytes32 appData; uint256 feeAmount; bool sell; // sell-kind (true) vs buy-kind (false) bool partiallyFillable; } /// The sell amount a live presigned order has approved, keyed by the order's /// uid. Written at presign, read-and-cleared at cancel. Its job is to make a /// cancellation provable: `cancel` acts only on an order THIS adapter /// actually presigned, and reports how much of it went unfilled. mapping(address account => mapping(bytes32 uidKey => uint256 reserved)) internal _resting; /// The live uid per sell token, so a full fill (allowance already 0) can /// still be found and cleared on the next `presign`. Without this, `_resting` /// is keyed only by uid and the filled order's row would be left behind. mapping(address account => mapping(address sellToken => bytes32 uidKey)) internal _live; /// The install generation each row above was written under. /// /// WITHOUT THIS, UNINSTALL AND REINSTALL MERGED. `onUninstall` deletes /// `_config`, which returns `gen` to zero, so the next install is generation /// one again — the same number the previous one had. Every other row in this /// system carries a generation for exactly that reason; these did not, /// so a reservation from a previous install was still readable afterwards /// and `cancel` / `_clearFilled` would act on it under the NEW install. /// A reinstall is a restatement of authority, not a merge with the last one. /// /// PER ROW, NOT PER ACCOUNT, and that distinction was a High. `_liveGen` /// used to be one slot per account and `_liveToken` one sell token per /// account, while `_live` and `_resting` were already keyed per token — /// the design anticipated several orders and the teardown pointer did not. /// A second presign on a different sell token passed the /// `OrderAlreadyResting` guard (that token's allowance is zero) and /// OVERWROTE the pointer. On reinstall `_wipeAuthority` revoked only the /// most recent token and deleted the single `_liveGen` regardless, so the /// surviving order read `reserved == 0`, `cancel` reverted `NoRestingOrder`, /// and a fillable order with a live allowance became permanently /// unrevokable — while B could not re-presign that token either. mapping(address account => mapping(bytes32 uidKey => uint64 gen)) internal _restingGen; mapping(address account => mapping(address sellToken => uint64 gen)) internal _liveTokenGen; /// `_charge(sellToken)` and an asset with no cap row reverts /// Every sell token with a live resting order, so the teardown can reach /// all of them rather than the last one. /// /// BOUNDED BY `MAX_LIVE_TOKENS`, AND IT HAS TO BE BOUNDED BY SOMETHING. /// Until 2026-09-20 the bound was the account's cap table: `presign` /// charged `_spendCap(sellToken)` and an asset the owner had not listed /// reverted, so B could not grow this past A's own list. Spend caps were /// then removed, and `sellToken` is a field B signs — so without a bound of /// its own this array grows as far as B likes, and `_wipeAuthority` makes an /// external call PER ENTRY. A long enough list costs more than a block, and /// the account can never uninstall or reinstall the adapter again. /// /// One live order per token is still the `OrderAlreadyResting` rule. /// `_liveIndex` holds position+1 so a removal is O(1) rather than a scan, /// and zero means absent. mapping(address account => address[] sellTokens) internal _liveTokens; mapping(address account => mapping(address sellToken => uint256 indexPlusOne)) internal _liveIndex; /// The vault relayer that holds the standing allowance, captured at presign. /// /// STORED RATHER THAN LOOKED UP, because the settlement contract is reached /// through a venue NAME carried on the signed order, and `_wipeAuthority` /// has no order in front of it. Storing the resolved relayer is also the /// safer read: it is the address the allowance was actually granted to, /// rather than whatever the venue table happens to say at wipe time. /// /// Keyed per sell token for the same reason as everything else here: two /// resting orders can have been placed through two venues. mapping(address account => mapping(address sellToken => address relayer)) internal _liveRelayer; /// WHERE `presign`'S CHARGE LANDED, keyed like `_resting`: the tag /// `AgentRegistry.charge` returned — nothing, an epoch, or a plan. /// /// This adapter makes the system's only refund in a LATER transaction than /// its charge, and the row used to keep only the amount. So `cancel` /// refunded into whatever was current when it ran: a day-1 order cancelled /// after one wei charged on day 2 forgave day-2 spend (twice the cap in a /// day); an order placed while the token was uncapped un-spent charges once /// the owner capped it; an order charged outside any plan, or under plan /// P1, refunded into the plan open at cancel time, which then spent past /// its proved total. With the tag the refund goes back to where the charge /// went, or nowhere. Deleted wherever the row is. mapping(address account => mapping(bytes32 uidKey => uint256 tag)) internal _restingTag; event Presigned( address indexed account, address indexed sellToken, address indexed buyToken, uint256 sellAmount, uint256 buyAmount, bytes uid ); event Cancelled(address indexed account, bytes uid, uint256 refunded); error NoSellAmount(); error NoFloor(); error SameAsset(); error ApproveFailed(); error OrderAlreadyResting(address account, address sellToken); error NoRestingOrder(); /// More simultaneous resting orders than the teardown loop can afford. See /// `_liveTokens` — this is the bound that used to be the cap table's. error TooManyRestingOrders(uint256 have, uint256 most); /// `validTo` already past, or further out than MAX_ORDER_TTL. error BadValidTo(uint32 validTo, uint256 earliest, uint256 latest); /// A non-zero `feeAmount` is value B directs to a solver out of the sell /// side, under the same cap charge as the swap itself. error FeeMustBeZero(uint256 feeAmount); /// `appData` other than zero or the empty document. See `presign`. error AppDataNotEmpty(bytes32 appData); /// keccak256("{}"): CoW's empty app-data document. bytes32 internal constant EMPTY_APP_DATA = 0xb48d38f93eaa084033fc5970bf96e559c33c4cdc07d889ab00b4d63f9590739d; /// The furthest out a resting order may be valid. /// /// `validTo` is a raw `uint32` that B fills in and nothing bounded it — not /// even `> block.timestamp`. So B could leave an order for the full epoch /// cap, at any price it liked, with an ~80-year lifetime, and NEITHER /// uninstalling the adapter NOR de-appointing the agent revokes it: the /// pre-signature and the allowance live on CoW and on the token, and /// `_wipeAuthority` can only act on the install path (Kernel clears the /// executor before `onUninstall`, so `executeFromExecutor` is already /// closed there — see the note on `_wipeAuthority`). /// /// A constant rather than an install parameter because `onInstall`'s payload /// is shared by all seventeen adapters and its shape is part of every /// account's genesis preimage. Seven days is longer than any order this /// system places and short enough that the residual an uninstall cannot /// reach expires on its own. uint256 internal constant MAX_ORDER_TTL = 7 days; /// The most sell tokens one account may have resting at once. /// /// `_wipeAuthority` revokes an allowance per entry — an external call each — /// so this number is what keeps an uninstall affordable. Sixteen is far more /// simultaneous orders than any plan this system writes, and roughly 16 /// approvals plus the surrounding wipe, which fits a block with room over. uint256 internal constant MAX_LIVE_TOKENS = 16; constructor(AgentRegistry registry_) BaseAdapter(registry_) {} function _domainNameAndVersion() internal pure override returns (string memory, string memory) { return ("DeFiLoopsCowPresignAdapter", "1"); } /// The adapter's own EIP-712 digest over the intent — distinct from the CoW /// order digest computed inside `_uid`. /// /// The encode is split in two and concatenated rather than done in one call: /// fourteen calldata fields overrun the stack with `via_ir` off (pinned, for /// the byte-identical model), and every field here is a STATIC type, so /// `encode(first) ++ encode(rest)` is byte-for-byte `encode(all)`. function digest(Order calldata o) public view returns (bytes32) { return _hashTypedData( keccak256(bytes.concat(_intentHead(PRESIGN_TYPEHASH, o), _intentTail(o))) ); } /// Same fields as `digest`, hashed under `CANCEL_TYPEHASH`. A cancel /// signature therefore cannot verify on `presign`. function cancelDigest(Order calldata o) public view returns (bytes32) { return _hashTypedData(keccak256(bytes.concat(_intentHead(CANCEL_TYPEHASH, o), _intentTail(o)))); } function _intentHead(bytes32 typehash, Order calldata o) internal pure returns (bytes memory) { return abi.encode(typehash, o.account, o.owner, o.taskId, o.deadline, o.venue, o.sellToken); } function _intentTail(Order calldata o) internal pure returns (bytes memory) { return abi.encode( o.buyToken, o.sellAmount, o.buyAmount, o.validTo, o.appData, o.feeAmount, o.sell, o.partiallyFillable ); } /// The uid an off-chain submitter posts to CoW for this order. Public because /// the agent needs it to place the order once the account has presigned it, /// and because it is the value a reviewer checks the receiver against without /// trusting a comment: it is derived from `receiver = account`, in the open. function uidFor(Order calldata o) external view returns (bytes memory) { return _uid(IGPv2Settlement(_venueOf(o.account, o.venue)), o); } /// Rebuild CoW's order digest and pack the uid, with `receiver` forced to the /// account. Every field but the receiver comes from the intent B signed. function _uid(IGPv2Settlement cow, Order calldata o) internal view returns (bytes memory) { // Split for the same stack reason as `digest` above; all fields static. bytes memory head = abi.encode( ORDER_TYPE_HASH, o.sellToken, o.buyToken, o.account, // receiver == account, NOT a parameter o.sellAmount, o.buyAmount, o.validTo ); bytes memory tail = abi.encode( o.appData, o.feeAmount, o.sell ? KIND_SELL : KIND_BUY, o.partiallyFillable, BALANCE_ERC20, BALANCE_ERC20 ); bytes32 structHash = keccak256(bytes.concat(head, tail)); bytes32 orderDigest = keccak256(abi.encodePacked(hex"1901", cow.domainSeparator(), structHash)); return abi.encodePacked(orderDigest, o.account, o.validTo); // 32 + 20 + 4 } /// Approve the vault relayer for exactly the sell amount and pre-sign the /// order. The cap is charged HERE, not at settlement: settlement is async and /// gives no callback, so this is the only point that can bound per-epoch /// outflow. It is conservative — an order that never fills still consumed cap /// for the epoch — and `cancel` is how that reservation is released. function presign(Order calldata o, bytes calldata signature) external { if (o.sellAmount == 0) revert NoSellAmount(); if (o.buyAmount == 0) revert NoFloor(); if (o.sellToken == o.buyToken) revert SameAsset(); // A validTo already in the past is a cheap grief on its own: it charges // the cap, occupies the sell token's presign slot, and is only // clearable through OP_CANCEL, which A may not have installed. if ( uint256(o.validTo) <= block.timestamp || uint256(o.validTo) > block.timestamp + MAX_ORDER_TTL ) { revert BadValidTo(o.validTo, block.timestamp + 1, block.timestamp + MAX_ORDER_TTL); } if (o.feeAmount != 0) revert FeeMustBeZero(o.feeAmount); // THE ORDER CARRIES NO METADATA — finding 16 of the 2026-09-26 audit. // // `appData` is the hash of a JSON document the orderbook resolves, and // that document can name a PARTNER FEE — a cut of the order, paid to an // address it names — while `feeAmount` stays zero. The keeper signs // `appData`, so a stolen key could have paid itself out of every order // it placed. Go has only ever signed zero (internal/adapters/digest.go); // the one other value accepted is the hash of `{}`, the standard empty // document, because CoW has said it will stop accepting the bare-hash // form zero is announced in, and allowing both now means that day needs // no redeploy. Neither can carry a fee: zero has no preimage and `{}` // is empty. if (o.appData != bytes32(0) && o.appData != EMPTY_APP_DATA) { revert AppDataNotEmpty(o.appData); } _consume(o.account, o.owner, o.taskId, o.deadline, digest(o), signature); _requireOperation(o.account, OP_PRESIGN); // THE ASSET BOUGHT MUST BE ONE THE OWNER LISTED. Only `sellToken` // needed a cap row, so B could name any token at all as the buy side — // and the cap is charged on what is SOLD while the loss lands on what // is bought, with `buyAmount` a field B signs that is only checked for // being non-zero. See BaseAdapter._requireCuratedAsset. _requireCuratedAsset(o.buyToken); IGPv2Settlement cow = IGPv2Settlement(_venueOf(o.account, o.venue)); address relayer = cow.vaultRelayer(); // One resting order per sell token. A second approve would overwrite the // first order's allowance and quietly break its fillability, so a live // allowance is refused rather than clobbered. // // AGAINST THE RELAYER THAT ACTUALLY HOLDS IT — M-10 of the round-2 // audit. H-9's fix added the TOKEN axis to this bookkeeping and not the // VENUE axis, and both questions below used the relayer of the venue // being pre-signed on RIGHT NOW. // // So: pre-sign on venue 1, then pre-sign the same sell token on venue 2. // `allowance(account, R2)` is zero, the guard passed, and `_settleLive` // — asked about R2 — concluded order A had been FULLY FILLED, deleted // its reservation and overwrote `_liveRelayer`. Order A was still // pre-signed with a live allowance to R1, `cancel` reverted // `NoRestingOrder` because its record was gone, and `_revokeLive` would // have revoked the wrong allowance. Fillable and unrevokable for the // rest of its `validTo`, with the cap over-charged twice. // // Precisely the failure the header says was closed, along the axis the // fix did not cover. address prior = _liveRelayer[o.account][o.sellToken]; if (prior != address(0) && _liveTokenGen[o.account][o.sellToken] == _config[o.account].gen) { if (IERC20(o.sellToken).allowance(o.account, prior) != 0) { revert OrderAlreadyResting(o.account, o.sellToken); } // A full fill zeroes the allowance but leaves `_live`. Clear it // before this token takes a new order. _clearFilled(o.account, o.sellToken); } if (IERC20(o.sellToken).allowance(o.account, relayer) != 0) { revert OrderAlreadyResting(o.account, o.sellToken); } uint256 tag = _charge(o.account, o.sellToken, o.sellAmount); bytes memory ok = _exec( o.account, o.sellToken, 0, abi.encodeCall(IERC20.approve, (relayer, o.sellAmount)) ); if (ok.length != 0 && !abi.decode(ok, (bool))) revert ApproveFailed(); bytes memory uid = _uid(cow, o); bytes32 k = keccak256(uid); uint64 gen = _config[o.account].gen; _resting[o.account][k] = o.sellAmount; _restingGen[o.account][k] = gen; _restingTag[o.account][k] = tag; _live[o.account][o.sellToken] = k; _liveTokenGen[o.account][o.sellToken] = gen; _liveRelayer[o.account][o.sellToken] = relayer; _addLiveToken(o.account, o.sellToken); _exec( o.account, address(cow), 0, abi.encodeCall(IGPv2Settlement.setPreSignature, (uid, true)) ); emit Presigned(o.account, o.sellToken, o.buyToken, o.sellAmount, o.buyAmount, uid); } /// Revoke a resting order and clear its standing allowance. /// /// The unfilled remainder it reports is the allowance the relayer has NOT /// pulled — which equals the sell amount minus whatever settled, because the /// approve was for exactly that amount and only a settlement of this order /// can spend it. `_resting` gates the whole thing: an order this adapter /// never presigned has no row, so its "cancel" is refused rather than /// allowed to revoke an allowance somebody else granted. function cancel(Order calldata o, bytes calldata signature) external { _consume(o.account, o.owner, o.taskId, o.deadline, cancelDigest(o), signature); _requireOperation(o.account, OP_CANCEL); IGPv2Settlement cow = IGPv2Settlement(_venueOf(o.account, o.venue)); bytes memory uid = _uid(cow, o); bytes32 k = keccak256(uid); // A reservation from a PREVIOUS install is not this install's to refund. // Asked of the ROW, so a second resting order on another sell token is // still cancellable after the first one was torn down. uint256 reserved = _restingGen[o.account][k] == _config[o.account].gen ? _resting[o.account][k] : 0; if (reserved == 0) revert NoRestingOrder(); // Relayer approved at presign — read before `_clearLive` deletes it. address relayer = _liveRelayer[o.account][o.sellToken]; if (relayer == address(0)) relayer = cow.vaultRelayer(); uint256 unfilled = IERC20(o.sellToken).allowance(o.account, relayer); if (unfilled > reserved) unfilled = reserved; // never refund more than was charged // Where presign's charge landed — read before `_clearLive` deletes it. uint256 tag = _restingTag[o.account][k]; _clearLive(o.account, o.sellToken, k); _exec( o.account, address(cow), 0, abi.encodeCall(IGPv2Settlement.setPreSignature, (uid, false)) ); _requireNoResidual(o.account, o.sellToken, relayer); // Mirror `_revokeLive`: release the unused reservation — into the epoch // or plan that presign charged, not whichever is current now. _settleReservation(o.account, o.sellToken, reserved, unfilled, tag); emit Cancelled(o.account, uid, unfilled); } /// TEAR THE STANDING ORDER DOWN, not just the cap table. /// /// `presign` leaves three things behind: an allowance to CoW's vault /// relayer, `setPreSignature(uid, true)` on the settlement contract, and the /// two bookkeeping rows above. BaseAdapter's wipe clears caps, venues and /// operations and knows nothing about any of them, so an uninstall left a /// LIVE ORDER on the book — fillable, against an allowance the account had /// forgotten it granted, after the owner had removed the adapter precisely /// to stop that. /// /// WHERE THIS CAN ACT, AND WHERE IT CANNOT. Kernel registers the executor /// BEFORE calling `onInstall` and clears it BEFORE calling `onUninstall`, so /// `executeFromExecutor` works on one path and not the other — and only the /// account may call `setPreSignature` or `approve`, because CoW checks the /// uid's owner is `msg.sender`. So the on-chain half runs when this is /// reached from an install (which is the reinstall case, where a stale order /// would otherwise outlive its configuration), and the bookkeeping half runs /// always. /// /// The residual on the uninstall path is real and is the owner's to close: /// the order stays fillable until the account revokes it, which the owner /// can do from their root path or by reinstalling this adapter. It is /// bounded by the allowances `presign` granted — one sell amount per sell /// token, at most MAX_LIVE_TOKENS of them — it cannot grow, because nothing /// can presign again without an install, and it expires on its own within /// MAX_ORDER_TTL. /// /// "Exactly one sell amount, for one token" is what this used to say, and /// the code never enforced it: `_live` was keyed per sell token from the /// start. It is one per token B chose. function _wipeAuthority(address account) internal override { // ONLY WHEN THE TEARDOWN CAN ACTUALLY RUN, which is the install path. // // Clearing the bookkeeping on the uninstall path would erase the // pointers the next install needs to find the orders — and the next // install is the only place this contract can still make the account // revoke them. Left alone, the rows are unreachable anyway: every entry // point goes through `_consume`, which refuses `NotInstalled`. // // `onInstall` calls this BEFORE it bumps the generation, and the // teardown below reads NO generation at all: it walks `_liveTokens`, // which the uninstall left in place. So a reinstall clears every row // the previous install wrote — and revokes its allowances — before // writing the new authority, whatever the counter says by then. (This // used to claim the OLD gen was still in `_config` here. It never was // after an uninstall: `onUninstall` deleted the counter to zero, and // now moves it forward.) Nothing from the previous install survives // into the new one. // // EVERY LIVE TOKEN, not the most recent one. Revoking one and deleting // the generation for all of them is what stranded the others: fillable // forever, and `cancel` refusing to touch them. if (!_moduleInstalledOnCaller()) { super._wipeAuthority(account); return; } address[] storage tokens = _liveTokens[account]; for (uint256 i = tokens.length; i > 0; i--) { address sellToken = tokens[i - 1]; _revokeLive(account, sellToken); bytes32 k = _live[account][sellToken]; delete _resting[account][k]; delete _restingGen[account][k]; delete _restingTag[account][k]; delete _live[account][sellToken]; delete _liveTokenGen[account][sellToken]; delete _liveRelayer[account][sellToken]; delete _liveIndex[account][sellToken]; } delete _liveTokens[account]; super._wipeAuthority(account); } /// Record that this sell token now has a live resting order. function _addLiveToken(address account, address sellToken) internal { if (_liveIndex[account][sellToken] != 0) return; uint256 have = _liveTokens[account].length; if (have >= MAX_LIVE_TOKENS) revert TooManyRestingOrders(have, MAX_LIVE_TOKENS); _liveTokens[account].push(sellToken); _liveIndex[account][sellToken] = _liveTokens[account].length; } /// Drop every row for one (account, sellToken, uid), including its place in /// the live set. Swap-and-pop, so the set stays dense. function _clearLive(address account, address sellToken, bytes32 uidKey) internal { delete _resting[account][uidKey]; delete _restingGen[account][uidKey]; delete _restingTag[account][uidKey]; delete _live[account][sellToken]; delete _liveTokenGen[account][sellToken]; delete _liveRelayer[account][sellToken]; uint256 at = _liveIndex[account][sellToken]; if (at == 0) return; address[] storage tokens = _liveTokens[account]; uint256 last = tokens.length; if (at != last) { address moved = tokens[last - 1]; tokens[at - 1] = moved; _liveIndex[account][moved] = at; } tokens.pop(); delete _liveIndex[account][sellToken]; } /// Clear the relayer's standing allowance, as the account. /// /// THE ALLOWANCE IS WHAT MAKES AN ORDER FILLABLE, so revoking it is the /// teardown — a pre-signature with nothing to pull settles nothing. /// Un-presigning as well would be tidier and costs a stored uid (variable /// length, one more write on every presign) to remove a signature that /// already authorises nothing. /// /// The account is the caller because it has to be: CoW checks the uid's /// owner is `msg.sender`, and the allowance is the account's to give and to /// take back. /// AND THE ALLOWANCE IS RE-READ — M-16 of the round-2 audit. /// /// This discarded `approve`'s return and never looked at the allowance /// afterwards, and the caller then deletes the bookkeeping. So a token whose /// `approve` returns false — or one that ignores a zero-approve, which /// several do — left a RESTING ORDER ALIVE with a live allowance, and /// unreachable by `cancel`, because the record it needs has just been /// erased. A teardown that cannot prove it tore anything down is a teardown /// that has to revert instead. function _revokeLive(address account, address sellToken) internal { address relayer = _liveRelayer[account][sellToken]; if (relayer == address(0)) return; // WHAT IS STILL APPROVED IS WHAT NEVER LEFT. `presign` charged the whole // sell amount up front, because a resting order is spendable by CoW at // any moment and a budget that only moves when a fill lands bounds // nothing. Read the unfilled remainder BEFORE zeroing and give it back. uint256 unfilled = IERC20(sellToken).allowance(account, relayer); bytes memory ok = _exec(account, sellToken, 0, abi.encodeCall(IERC20.approve, (relayer, 0))); if (ok.length != 0 && !abi.decode(ok, (bool))) revert ApproveFailed(); uint256 left = IERC20(sellToken).allowance(account, relayer); if (left != 0) revert ResidualAllowance(sellToken, relayer, left); // The sell amount `presign` charged for the order resting on this token // (zero if there is none), and where it landed. bytes32 k = _live[account][sellToken]; if (k == 0) return; _settleReservation(account, sellToken, _resting[account][k], unfilled, _restingTag[account][k]); } /// H-1 OF THE PASHOV AUDIT, AND IT STILL MUST NOT REVERT. /// /// The finding was that settling a fill could revert `CapExceeded` and so /// brick `cancel` for an order that had ALREADY left the account. Nothing /// here can revert now: `unfilled` is clamped to what was charged, which is /// the only condition `refundCharge` refuses, and an epoch that rolled in /// between, a plan that settled or a charge that landed nowhere is a no-op /// rather than an underflow. /// /// `tag` is where presign's charge went; the refund goes there and nowhere /// else. See `_restingTag`. function _settleReservation( address account, address asset, uint256 reserved, uint256 unfilled, uint256 tag ) internal { if (reserved == 0) return; if (unfilled > reserved) unfilled = reserved; _refund(account, asset, unfilled, reserved, tag); } /// H-1 OF THE PASHOV AUDIT WAS ANSWERED BY DELETION, not by a fix. /// /// It was: `_settleReservation` could revert `CapExceeded` while attributing /// a post-epoch fill, which bricked cancel and re-presign for an order that /// had ALREADY left the account. The audit's fix saturated at the cap /// instead of reverting. Spend caps were removed on 2026-09-20, so there is /// no charge to make and nothing that can revert here; the function below is /// what is left of that path and it only clears a row. /// /// A FULLY FILLED ORDER LEAVES ITS BOOKKEEPING BEHIND. The allowance is /// zero, so nothing is stranded on chain, but `_live` still names the uid /// and `OrderAlreadyResting` is decided on the allowance rather than on /// this row. Clearing it here is what lets the sell token carry a new /// order; leaving it would strand the ROW, which is what `cancel` needs to /// prove the order was ours. function _clearFilled(address account, address sellToken) internal { if (_liveTokenGen[account][sellToken] != _config[account].gen) return; bytes32 k = _live[account][sellToken]; if (k == 0) return; _clearLive(account, sellToken, k); } /// What a reviewer (or a test) needs to see that no order was stranded. function liveSellTokens(address account) external view returns (address[] memory) { return _liveTokens[account]; } /// The sell amount this account has approved for a resting order, or zero if /// this install never presigned it. function restingFor(address account, bytes calldata uid) external view returns (uint256) { bytes32 k = keccak256(uid); if (_restingGen[account][k] != _config[account].gen) return 0; return _resting[account][k]; } }