Skip to content

Commit 6966303

Browse files
committed
feat: register a polling schedule
1 parent 6b6c223 commit 6966303

2 files changed

Lines changed: 156 additions & 6 deletions

File tree

src/types/ComposableCowPoller.sol

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
// SPDX-License-Identifier: GPL-3.0
22
pragma solidity >=0.8.0 <0.9.0;
33

4+
import {IConditionalOrderGenerator} from "../interfaces/IConditionalOrder.sol";
5+
46
/// @title ComposableCowPoller - Just-in-time funding for composable conditional orders.
5-
contract ComposableCowPoller {}
7+
contract ComposableCowPoller {
8+
struct Schedule {
9+
IConditionalOrderGenerator handler; // the conditional-order handler to poll (e.g. the TWAP type)
10+
address funder; // source of funds; the only registrant
11+
address owner; // order owner; the pull destination
12+
bytes32 salt; // the conditional order's salt
13+
bytes staticInput; // the order's staticInput
14+
}
15+
16+
/// @dev Keyed by `id == scheduleId(funder, handler, owner, salt)`, which is independent of the
17+
/// order's `appData`.
18+
mapping(bytes32 => Schedule) public schedules;
19+
20+
error OnlyFunder();
21+
22+
event ScheduleRegistered(bytes32 indexed id, address indexed owner, address indexed funder);
23+
24+
/// @notice Deterministic, appData-independent schedule key.
25+
function scheduleId(address funder, IConditionalOrderGenerator handler, address owner, bytes32 salt)
26+
public
27+
pure
28+
returns (bytes32)
29+
{
30+
return keccak256(abi.encode(funder, handler, owner, salt));
31+
}
32+
33+
/// @notice Register or update a schedule. Only the funds source may register, and the `id` is
34+
/// namespaced by `funder`.
35+
function register(Schedule calldata schedule) external returns (bytes32 id) {
36+
if (msg.sender != schedule.funder) revert OnlyFunder();
37+
id = scheduleId(schedule.funder, schedule.handler, schedule.owner, schedule.salt);
38+
schedules[id] = schedule;
39+
emit ScheduleRegistered(id, schedule.owner, schedule.funder);
40+
}
41+
}

test/ComposableCowPoller.t.sol

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

4-
import {BaseComposableCoWTest} from "./ComposableCoW.base.t.sol";
4+
import {IConditionalOrder, IValueFactory, BaseComposableCoWTest} from "./ComposableCoW.base.t.sol";
55

6+
import {TWAP} from "../src/types/twap/TWAP.sol";
7+
import {TWAPOrder} from "../src/types/twap/libraries/TWAPOrder.sol";
68
import {ComposableCowPoller} from "../src/types/ComposableCowPoller.sol";
9+
import {CurrentBlockTimestampFactory} from "../src/value_factories/CurrentBlockTimestampFactory.sol";
10+
import {IConditionalOrderGenerator} from "../src/interfaces/IConditionalOrder.sol";
711

812
/// @title ComposableCowPoller unit tests
9-
/// @notice Base scaffolding for the poller. Feature-specific tests (register / revoke / topUp) are
10-
/// added in follow-up PRs.
13+
/// @notice Exercises registering a schedule for a composable TWAP created via `createWithContext`.
1114
contract ComposableCowPollerTest is BaseComposableCoWTest {
15+
uint256 constant PART = 100e18;
16+
uint256 constant LIMIT = 1e18;
17+
uint256 constant N = 3;
18+
uint256 constant FREQ = 1 hours;
19+
bytes32 constant SALT = keccak256("twap");
20+
1221
ComposableCowPoller poller;
22+
IValueFactory currentBlockTimestampFactory;
23+
24+
address funder;
1325

1426
function setUp() public virtual override(BaseComposableCoWTest) {
1527
super.setUp();
1628

29+
twap = new TWAP(composableCow);
30+
currentBlockTimestampFactory = new CurrentBlockTimestampFactory();
1731
poller = new ComposableCowPoller();
32+
funder = makeAddr("funder");
33+
34+
// The owner (safe1) starts with no sell token: funds arrive just-in-time.
35+
deal(address(token0), address(safe1), 0);
36+
}
37+
38+
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+
});
52+
}
53+
54+
/// @dev Creates a JIT-funded TWAP: order via context, the funder funds + approves the poller,
55+
/// and the schedule is registered. Returns the order's cabinet key `ctx`
56+
/// (`ComposableCoW.hash(params)`, used for cabinet/remove) and the appData-independent
57+
/// poller schedule key `id` (used for topUp/revoke).
58+
function _setupSchedule()
59+
internal
60+
returns (
61+
IConditionalOrder.ConditionalOrderParams memory params,
62+
bytes32 ctx,
63+
bytes32 id
64+
)
65+
{
66+
params = super.createOrder(twap, SALT, abi.encode(_bundle()));
67+
_createWithContext(
68+
address(safe1),
69+
params,
70+
currentBlockTimestampFactory,
71+
bytes(""),
72+
false
73+
);
74+
ctx = composableCow.hash(params);
75+
76+
// Capital lives in the funder (the EOA), which approves the poller for the full notional.
77+
deal(address(token0), funder, PART * N);
78+
vm.prank(funder);
79+
token0.approve(address(poller), PART * N);
80+
81+
// Register the schedule (only the funder may do this). The schedule carries the handler,
82+
// the funds source, the destination, the order's `salt` (so the poller can rebuild `ctx`
83+
// on-chain) and its `staticInput`. The key is appData-independent so the funding hook can
84+
// live in the order's own appData.
85+
vm.prank(funder);
86+
id = poller.register(
87+
ComposableCowPoller.Schedule({
88+
handler: IConditionalOrderGenerator(address(twap)),
89+
funder: funder,
90+
owner: address(safe1),
91+
salt: SALT,
92+
staticInput: abi.encode(_bundle())
93+
})
94+
);
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+
);
106+
}
107+
108+
function _t0(bytes32 ctx) internal view returns (uint256) {
109+
return uint256(composableCow.cabinet(address(safe1), ctx));
110+
}
111+
112+
/// @dev A registered schedule is stored under its appData-independent id.
113+
function test_register_storesSchedule() public {
114+
(, , bytes32 id) = _setupSchedule();
115+
116+
(IConditionalOrderGenerator handler, address scheduleFunder, address owner, bytes32 salt,) =
117+
poller.schedules(id);
118+
assertEq(address(handler), address(twap), "handler stored");
119+
assertEq(scheduleFunder, funder, "funder stored");
120+
assertEq(owner, address(safe1), "owner stored");
121+
assertEq(salt, SALT, "salt stored");
18122
}
19123

20-
function test_deployment() public {
21-
assertTrue(address(poller) != address(0), "poller deployed");
124+
/// @dev Only the funds source may register a schedule that draws on its own funds.
125+
function test_register_RevertWhen_notFunder() public {
126+
vm.expectRevert(ComposableCowPoller.OnlyFunder.selector);
127+
poller.register(
128+
ComposableCowPoller.Schedule({
129+
handler: IConditionalOrderGenerator(address(twap)),
130+
funder: funder, // attacker points at someone else's funds
131+
owner: address(safe1),
132+
salt: SALT,
133+
staticInput: abi.encode(_bundle())
134+
})
135+
);
22136
}
23137
}

0 commit comments

Comments
 (0)