Skip to content

feat: Just in time polling of funds#116

Draft
anxolin wants to merge 6 commits into
mainfrom
feat/composable-cow-poller
Draft

feat: Just in time polling of funds#116
anxolin wants to merge 6 commits into
mainfrom
feat/composable-cow-poller

Conversation

@anxolin

@anxolin anxolin commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Adds ComposableCowPoller, a small contract that funds a conditional order just in time, right before each part settles, instead of locking up the full amount when the order is created.

I don't expect this PR to be merged as it is, my goal is to create a PoC and test the results of the exploration done on ways to implement JIT polling of funds (see private notes).

This is a follow up on cowdao-grants/cow-shed#53 and anxolin/cow-sdk-scripts#12 (slack)

Why

For order types that run over long periods (e.g. TWAP, specially when using it for DCA), pre-funding the entire thing up front is capital-inefficient and sometimes impossible: the funds may only arrive shortly before each part needs to trade.

This contract lets capital stay in the user's own account (an EOA or treasury) and pulls only what the current part needs, only when it needs it.

How it works

A funder registers a schedule for a given order, pointing at:

  • the order's handler (e.g. the TWAP type),
  • the funds source (funder),
  • the order owner that will hold and sell the funds,
  • the order's salt, and
  • the order's staticInput.

The schedule is stored under an appData-independent key id = scheduleId(funder, handler, owner, salt).

Anyone can then call topUp(id) (designed to run as a CoW pre-hook). The contract re-derives the order's ctx on-chain from the stored params and pulls exactly the current part's sellAmount from the funder into the owner. It is deliberately generic: it works for any conditional order, with no order-type-specific logic of its own.

Design note: making topUp embeddable in the order's own appData

The natural identifier for a schedule is the order's ctx (ComposableCoW.hash(params)). But the goal is to trigger topUp from a pre-hook in the order's own appData, so each discrete part funds itself just-in-time — and that creates a cycle:

ctx contains the order's appData hash → but the hook (which references the schedule) lives inside that appData. Any order-derived identifier (ctx, staticInput, or the order digest) has the same problem, since they all transitively contain appData. keccak256 has no findable fixed point, so a topUp(ctx) hook can never be embedded in the appData it commits to.

The fix is to key the schedule by data that is independent of appDatafunder, handler, owner, salt — and to store salt so the poller can reconstruct ctx on-chain inside topUp. The hook calldata topUp(id) then no longer depends on appData, breaking the cycle. Every discrete part inherits the parent order's appData, so each part self-funds via its own pre-hook.

topUp is permissionless on purpose — safety comes from constraining what the call can do, not who makes it:

  1. The order must still be authorised in ComposableCoW (cancelling it disables the poller). ctx is re-derived on-chain from the stored params, never taken from the caller.
  2. The amount, sell token, and validity window are read from the handler's own getTradeableOrder — never from the caller. Outside an active window that call reverts, so nothing can be pulled when no part is tradeable.
  3. Funds can only ever move to the owner registered in the schedule.
  4. Each part is funded at most once, keyed by its unique order digest. This prevents a future part's funds from being pulled early.
  5. The top-up is balance-capped to one part, so repeated calls are idempotent.
  6. The schedule id is namespaced by funder, so nobody can register, overwrite, or squat another funder's schedule.

Only the funder can register or revoke a schedule, so nobody can point a pull at someone else's funds.

How to review

  • Review src/types/ComposableCowPoller.sol which is the main contract. You might want to review my private notes on the exploration I did for the options to arrive to this proposal.
  • A contract was deployed at https://gnosisscan.io/address/0x3ba140fae6a1688aaddc8ad37ea0f6230d6b6178 — note this is the earlier ctx-keyed version, before the appData-independent id change described above. The new shape needs a redeploy.
  • Rest of the PR is some deployment scripts and unit tests, which cover:
    • the happy path (each part funded just in time across the whole schedule)
    • idempotency within a part
    • the guard against funding a future part early
    • reverting outside the schedule window or with no schedule
    • disabling via order cancellation
    • the funder-only register/revoke checks.

Test plan

forge test --match-contract ComposableCowPollerTest -vv

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@anxolin anxolin changed the title feat: ComposableCowPoller — just-in-time funding for conditional orders feat: Just in time polling of funds Jun 30, 2026
@anxolin

anxolin commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant