Skip to content

Commit 21fe323

Browse files
committed
chore: more tests, added natspec
1 parent bf350d1 commit 21fe323

2 files changed

Lines changed: 91 additions & 52 deletions

File tree

src/types/ComposableCowPoller.sol

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// SPDX-License-Identifier: GPL-3.0
22
pragma solidity >=0.8.0 <0.9.0;
33

4-
import {IConditionalOrderGenerator} from "../interfaces/IConditionalOrder.sol";
4+
import {IConditionalOrderGenerator} from "src/interfaces/IConditionalOrder.sol";
55

66
/// @title ComposableCowPoller - Just-in-time funding for composable conditional orders.
77
contract ComposableCowPoller {
8+
/// @notice Parameters for a JIT funding schedule.
9+
/// @dev A schedule is uniquely identified by its funder, handler, owner, and salt.
810
struct Schedule {
911
IConditionalOrderGenerator handler; // the conditional-order handler to poll (e.g. the TWAP type)
1012
address funder; // source of funds; the only registrant
@@ -17,11 +19,23 @@ contract ComposableCowPoller {
1719
/// order's `appData`.
1820
mapping(bytes32 => Schedule) public schedules;
1921

22+
/// @notice Thrown when someone other than the schedule funder registers or updates a schedule.
2023
error OnlyFunder();
2124

25+
/// @notice Emitted when a schedule is registered or updated.
26+
/// @param id The deterministic key of the schedule.
27+
/// @param owner The conditional-order owner and pull destination.
28+
/// @param funder The token source that registered the schedule.
2229
event ScheduleRegistered(bytes32 indexed id, address indexed owner, address indexed funder);
2330

24-
/// @notice Deterministic, appData-independent schedule key.
31+
/// @notice Computes the deterministic, appData-independent schedule key.
32+
/// @dev `staticInput` is excluded because its appData can depend on this key.
33+
/// Use a different salt for concurrent schedules with the same funder, handler, and owner.
34+
/// @param funder The token source.
35+
/// @param handler The conditional-order generator.
36+
/// @param owner The conditional-order owner.
37+
/// @param salt The conditional-order salt.
38+
/// @return The schedule key.
2539
function scheduleId(address funder, IConditionalOrderGenerator handler, address owner, bytes32 salt)
2640
public
2741
pure
@@ -30,8 +44,11 @@ contract ComposableCowPoller {
3044
return keccak256(abi.encode(funder, handler, owner, salt));
3145
}
3246

33-
/// @notice Register or update a schedule. Only the funds source may register, and the `id` is
34-
/// namespaced by `funder`.
47+
/// @notice Registers or updates a schedule.
48+
/// @dev Registering the same funder, handler, owner, and salt replaces the stored schedule.
49+
/// Only the funds source may register, and the ID is namespaced by the funder.
50+
/// @param schedule The schedule to store.
51+
/// @return id The deterministic key of the stored schedule.
3552
function register(Schedule calldata schedule) external returns (bytes32 id) {
3653
if (msg.sender != schedule.funder) revert OnlyFunder();
3754
id = scheduleId(schedule.funder, schedule.handler, schedule.owner, schedule.salt);

test/ComposableCowPoller.t.sol

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// SPDX-License-Identifier: GPL-3.0
22
pragma 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

Comments
 (0)