11// SPDX-License-Identifier: GPL-3.0
22pragma solidity >= 0.8.0 < 0.9.0 ;
33
4- import {IConditionalOrder, IValueFactory, BaseComposableCoWTest} from ". /ComposableCoW.base.t.sol " ;
4+ import {IConditionalOrder, IValueFactory, BaseComposableCoWTest} from "test /ComposableCoW.base.t.sol " ;
55
6- import {TWAP} from "../ src/types/twap/TWAP.sol " ;
7- import {TWAPOrder} from "../ src/types/twap/libraries/TWAPOrder.sol " ;
8- import {ComposableCowPoller} from "../ src/types/ComposableCowPoller.sol " ;
9- import {CurrentBlockTimestampFactory} from "../ src/value_factories/CurrentBlockTimestampFactory.sol " ;
10- import {IConditionalOrderGenerator} from "../ src/interfaces/IConditionalOrder.sol " ;
6+ import {TWAP} from "src/types/twap/TWAP.sol " ;
7+ import {TWAPOrder} from "src/types/twap/libraries/TWAPOrder.sol " ;
8+ import {ComposableCowPoller} from "src/types/ComposableCowPoller.sol " ;
9+ import {CurrentBlockTimestampFactory} from "src/value_factories/CurrentBlockTimestampFactory.sol " ;
10+ import {IConditionalOrderGenerator} from "src/interfaces/IConditionalOrder.sol " ;
1111
1212/// @title ComposableCowPoller unit tests
1313/// @notice Exercises registering a schedule for a composable TWAP created via `createWithContext`.
@@ -17,6 +17,7 @@ contract ComposableCowPollerTest is BaseComposableCoWTest {
1717 uint256 constant N = 3 ;
1818 uint256 constant FREQ = 1 hours ;
1919 bytes32 constant SALT = keccak256 ("twap " );
20+ bytes32 constant SECOND_SALT = keccak256 ("second twap " );
2021
2122 ComposableCowPoller poller;
2223 IValueFactory currentBlockTimestampFactory;
@@ -36,19 +37,18 @@ contract ComposableCowPollerTest is BaseComposableCoWTest {
3637 }
3738
3839 function _bundle () internal view returns (TWAPOrder.Data memory ) {
39- return
40- TWAPOrder.Data ({
41- sellToken: token0,
42- buyToken: token1,
43- receiver: address (0 ), // the owner itself
44- partSellAmount: PART,
45- minPartLimit: LIMIT,
46- t0: 0 , // resolved from the cabinet via createWithContext
47- n: N,
48- t: FREQ,
49- span: 0 , // valid for the whole epoch
50- appData: keccak256 ("dca.pull " )
51- });
40+ return TWAPOrder.Data ({
41+ sellToken: token0,
42+ buyToken: token1,
43+ receiver: address (0 ), // protocol shorthand for the Safe owner
44+ partSellAmount: PART,
45+ minPartLimit: LIMIT,
46+ t0: 0 , // resolved from the cabinet via createWithContext
47+ n: N,
48+ t: FREQ,
49+ span: 0 , // each part is valid for its full FREQ interval
50+ appData: keccak256 ("dca.pull " )
51+ });
5252 }
5353
5454 /// @dev Creates a JIT-funded TWAP: order via context, the funder funds + approves the poller,
@@ -57,20 +57,10 @@ contract ComposableCowPollerTest is BaseComposableCoWTest {
5757 /// poller schedule key `id` (used for topUp/revoke).
5858 function _setupSchedule ()
5959 internal
60- returns (
61- IConditionalOrder.ConditionalOrderParams memory params ,
62- bytes32 ctx ,
63- bytes32 id
64- )
60+ returns (IConditionalOrder.ConditionalOrderParams memory params , bytes32 ctx , bytes32 id )
6561 {
6662 params = super .createOrder (twap, SALT, abi.encode (_bundle ()));
67- _createWithContext (
68- address (safe1),
69- params,
70- currentBlockTimestampFactory,
71- bytes ("" ),
72- false
73- );
63+ _createWithContext (address (safe1), params, currentBlockTimestampFactory, bytes ("" ), false );
7464 ctx = composableCow.hash (params);
7565
7666 // Capital lives in the funder (the EOA), which approves the poller for the full notional.
@@ -82,39 +72,71 @@ contract ComposableCowPollerTest is BaseComposableCoWTest {
8272 // the funds source, the destination, the order's `salt` (so the poller can rebuild `ctx`
8373 // on-chain) and its `staticInput`. The key is appData-independent so the funding hook can
8474 // live in the order's own appData.
75+ id = _register (SALT, abi.encode (_bundle ()));
76+
77+ assertEq (
78+ id,
79+ poller.scheduleId (funder, IConditionalOrderGenerator (address (twap)), address (safe1), SALT),
80+ "id matches the off-chain derivation "
81+ );
82+ }
83+
84+ function _register (bytes32 salt , bytes memory staticInput ) internal returns (bytes32 id ) {
8585 vm.prank (funder);
8686 id = poller.register (
8787 ComposableCowPoller.Schedule ({
8888 handler: IConditionalOrderGenerator (address (twap)),
8989 funder: funder,
9090 owner: address (safe1),
91- salt: SALT ,
92- staticInput: abi.encode ( _bundle ())
91+ salt: salt ,
92+ staticInput: staticInput
9393 })
9494 );
95-
96- assertEq (
97- id,
98- poller.scheduleId (
99- funder,
100- IConditionalOrderGenerator (address (twap)),
101- address (safe1),
102- SALT
103- ),
104- "id matches the off-chain derivation "
105- );
10695 }
10796
10897 /// @dev A registered schedule is stored under its appData-independent id.
10998 function test_register_storesSchedule () public {
110- (, , bytes32 id ) = _setupSchedule ();
111-
112- (IConditionalOrderGenerator handler , address scheduleFunder , address owner , bytes32 salt ,) =
113- poller.schedules (id);
99+ (,, bytes32 id ) = _setupSchedule ();
100+
101+ (
102+ IConditionalOrderGenerator handler ,
103+ address scheduleFunder ,
104+ address owner ,
105+ bytes32 salt ,
106+ bytes memory staticInput
107+ ) = poller.schedules (id);
114108 assertEq (address (handler), address (twap), "handler stored " );
115109 assertEq (scheduleFunder, funder, "funder stored " );
116110 assertEq (owner, address (safe1), "owner stored " );
117111 assertEq (salt, SALT, "salt stored " );
112+ assertEq (staticInput, abi.encode (_bundle ()), "static input stored " );
113+ }
114+
115+ /// @dev Distinct salts allow concurrent schedules with the same funder, handler, and owner.
116+ function test_register_storesSchedulesWithDifferentSalts () public {
117+ bytes memory staticInput = abi.encode (_bundle ());
118+ bytes32 firstId = _register (SALT, staticInput);
119+ bytes32 secondId = _register (SECOND_SALT, staticInput);
120+
121+ assertTrue (firstId != secondId, "different salts create different ids " );
122+
123+ (,,, bytes32 firstSalt ,) = poller.schedules (firstId);
124+ (,,, bytes32 secondSalt ,) = poller.schedules (secondId);
125+ assertEq (firstSalt, SALT, "first schedule remains stored " );
126+ assertEq (secondSalt, SECOND_SALT, "second schedule stored " );
127+ }
128+
129+ /// @dev Registering the same key updates the one schedule stored under that id.
130+ function test_register_replacesScheduleWithSameId () public {
131+ bytes32 id = _register (SALT, abi.encode (_bundle ()));
132+ TWAPOrder.Data memory replacement = _bundle ();
133+ replacement.appData = keccak256 ("updated dca pull " );
134+ bytes memory updatedStaticInput = abi.encode (replacement);
135+
136+ assertEq (_register (SALT, updatedStaticInput), id, "same key keeps same id " );
137+
138+ (,,,, bytes memory staticInput ) = poller.schedules (id);
139+ assertEq (staticInput, updatedStaticInput, "replacement input stored " );
118140 }
119141
120142 /// @dev Only the funds source may register a schedule that draws on its own funds.
0 commit comments