@@ -28,7 +28,7 @@ contract ComposableCowPollerTest is BaseComposableCoWTest {
2828
2929 twap = new TWAP (composableCow);
3030 currentBlockTimestampFactory = new CurrentBlockTimestampFactory ();
31- poller = new ComposableCowPoller ();
31+ poller = new ComposableCowPoller (composableCow );
3232 funder = makeAddr ("funder " );
3333
3434 // The owner (safe1) starts with no sell token: funds arrive just-in-time.
@@ -54,7 +54,7 @@ contract ComposableCowPollerTest is BaseComposableCoWTest {
5454 /// @dev Creates a JIT-funded TWAP: order via context, the funder funds + approves the poller,
5555 /// and the schedule is registered. Returns the order's cabinet key `ctx`
5656 /// (`ComposableCoW.hash(params)`, used for cabinet/remove) and the appData-independent
57- /// poller schedule key `id` (used for topUp /revoke).
57+ /// poller schedule key `id` (used for pollFunds /revoke).
5858 function _setupSchedule ()
5959 internal
6060 returns (
@@ -134,4 +134,158 @@ contract ComposableCowPollerTest is BaseComposableCoWTest {
134134 })
135135 );
136136 }
137+
138+ /// @dev A single poll moves exactly the current part into the owner.
139+ function test_pollFunds_fundsCurrentPart () public {
140+ (, bytes32 ctx , bytes32 id ) = _setupSchedule ();
141+ vm.warp (_t0 (ctx));
142+
143+ assertEq (
144+ token0.balanceOf (address (safe1)),
145+ 0 ,
146+ "owner empty before pull "
147+ );
148+
149+ poller.pollFunds (id);
150+
151+ assertEq (
152+ token0.balanceOf (address (safe1)),
153+ PART,
154+ "owner funded with exactly one part "
155+ );
156+ assertEq (
157+ token0.balanceOf (funder),
158+ PART * N - PART,
159+ "exactly one part left the funder "
160+ );
161+ }
162+
163+ /// @dev Funds move unconditionally: even if the owner already holds a balance (e.g. from another
164+ /// concurrent order), the full part is still pulled, so orders never share funding.
165+ function test_pollFunds_movesFullAmountUnconditionally () public {
166+ (, bytes32 ctx , bytes32 id ) = _setupSchedule ();
167+ vm.warp (_t0 (ctx));
168+
169+ // The owner already holds an unrelated balance (e.g. funded for another order).
170+ deal (address (token0), address (safe1), PART);
171+
172+ poller.pollFunds (id);
173+
174+ assertEq (
175+ token0.balanceOf (address (safe1)),
176+ PART * 2 ,
177+ "full part pulled on top of the existing balance "
178+ );
179+ assertEq (
180+ token0.balanceOf (funder),
181+ PART * N - PART,
182+ "a full part left the funder "
183+ );
184+ }
185+
186+ /// @dev Repeated calls for the same part are a no-op (guarded by the order digest).
187+ function test_pollFunds_idempotentWithinPart () public {
188+ (, bytes32 ctx , bytes32 id ) = _setupSchedule ();
189+ vm.warp (_t0 (ctx));
190+
191+ poller.pollFunds (id);
192+ poller.pollFunds (id); // no-op: this part has already been funded
193+
194+ assertEq (
195+ token0.balanceOf (address (safe1)),
196+ PART,
197+ "still exactly one part "
198+ );
199+ assertEq (token0.balanceOf (funder), PART * N - PART, "no extra pull " );
200+ }
201+
202+ /// @dev The headline flow: each part is funded JIT and the owner holds nothing in between.
203+ function test_pollFunds_fundsEachPartAcrossSchedule () public {
204+ (, bytes32 ctx , bytes32 id ) = _setupSchedule ();
205+ uint256 t0 = _t0 (ctx);
206+
207+ for (uint256 part = 0 ; part < N; part++ ) {
208+ vm.warp (t0 + part * FREQ);
209+
210+ assertEq (
211+ token0.balanceOf (address (safe1)),
212+ 0 ,
213+ "owner empty before part "
214+ );
215+ poller.pollFunds (id);
216+ assertEq (token0.balanceOf (address (safe1)), PART, "part funded " );
217+
218+ // Simulate the part settling: the owner's balance is consumed.
219+ vm.prank (address (safe1));
220+ token0.transfer (bob.addr, PART);
221+
222+ assertEq (
223+ token0.balanceOf (funder),
224+ PART * N - PART * (part + 1 ),
225+ "one part funded per window "
226+ );
227+ }
228+ }
229+
230+ /// @dev The anti-premature-execution guard: once a part is funded, the *next* part's funds
231+ /// cannot be pulled until time advances into its window — even after the part settles and
232+ /// drains the owner. Without the per-order guard, the drained owner would be refilled
233+ /// immediately and that balance would be sold as the next part, a full interval early.
234+ function test_pollFunds_cannotFundFuturePartEarly () public {
235+ (, bytes32 ctx , bytes32 id ) = _setupSchedule ();
236+ vm.warp (_t0 (ctx)); // part 0 window
237+
238+ poller.pollFunds (id);
239+ assertEq (token0.balanceOf (address (safe1)), PART, "part 0 funded " );
240+
241+ // Simulate the part settling: the owner's balance is consumed.
242+ vm.prank (address (safe1));
243+ token0.transfer (bob.addr, PART);
244+ assertEq (
245+ token0.balanceOf (address (safe1)),
246+ 0 ,
247+ "owner drained by the fill "
248+ );
249+
250+ // Still inside part 0's window: a fresh pull must NOT refill.
251+ poller.pollFunds (id);
252+
253+ assertEq (
254+ token0.balanceOf (address (safe1)),
255+ 0 ,
256+ "next part not funded early "
257+ );
258+ assertEq (
259+ token0.balanceOf (funder),
260+ PART * N - PART,
261+ "exactly one part ever left the funder "
262+ );
263+ }
264+
265+ /// @dev The pull is bounded to the schedule window: after it ends, `getTradeableOrder` reverts.
266+ function test_pollFunds_RevertWhen_scheduleEnded () public {
267+ (, bytes32 ctx , bytes32 id ) = _setupSchedule ();
268+ vm.warp (_t0 (ctx) + N * FREQ);
269+
270+ vm.expectRevert (); // IConditionalOrder.OrderNotValid(...) from the handler
271+ poller.pollFunds (id);
272+ }
273+
274+ /// @dev An unregistered schedule cannot be polled.
275+ function test_pollFunds_RevertWhen_noSchedule () public {
276+ vm.expectRevert (ComposableCowPoller.NoSchedule.selector );
277+ poller.pollFunds (keccak256 ("unknown " ));
278+ }
279+
280+ /// @dev Cancelling the order flips `singleOrders` false, which disables the poller for free.
281+ function test_remove_killsPoller () public {
282+ (, bytes32 ctx , bytes32 id ) = _setupSchedule ();
283+ vm.warp (_t0 (ctx));
284+
285+ vm.prank (address (safe1));
286+ composableCow.remove (ctx);
287+
288+ vm.expectRevert (ComposableCowPoller.OrderNotLive.selector );
289+ poller.pollFunds (id);
290+ }
137291}
0 commit comments