Skip to content

Commit 11769e4

Browse files
committed
add supervisor for rebalancing
1 parent 1a0a59f commit 11769e4

8 files changed

Lines changed: 378 additions & 13 deletions

File tree

RebalanceArchitecture.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
The core philosophy is **decoupling**: each component operates independently with the least privilege necessary.
44

5-
The **Supervisor** is currently in the design phase (not yet implemented).
6-
75
### Key Principles
86

97
* **Isolation:** FCM, Rebalancer, and Supervisor are fully independent.
@@ -34,7 +32,7 @@ sequenceDiagram
3432
anyone->>FCMHelper: createPosition()
3533
FCMHelper->>FCM: createPosition()
3634
FCMHelper->>AB: createRebalancer(rebalanceCapability)
37-
FCMHelper->>Supervisor: supervise(publicCapability)
35+
FCMHelper->>Supervisor: addRebalancer(uuid)
3836
```
3937

4038
### while running
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import "FlowTransactionScheduler"
2+
import "FlowCreditMarketRebalancerPaidV1"
3+
import "FlowCron"
4+
5+
access(all) contract FlowCreditMarketSupervisorV1 {
6+
7+
access(all) event Executed(id: UInt64)
8+
9+
access(all) resource Supervisor: FlowTransactionScheduler.TransactionHandler {
10+
11+
access(all) let rebalancers: {UInt64: Bool}
12+
13+
init() {
14+
self.rebalancers = {}
15+
}
16+
17+
access(all) fun addPaidRebalancer(uuid: UInt64) {
18+
self.rebalancers[uuid] = true
19+
}
20+
21+
access(all) fun removePaidRebalancer(uuid: UInt64): Bool? {
22+
return self.rebalancers.remove(key: uuid)
23+
}
24+
25+
access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
26+
emit Executed(id: id)
27+
for rebalancerUUID in self.rebalancers.keys {
28+
FlowCreditMarketRebalancerPaidV1.fixReschedule(uuid: rebalancerUUID)
29+
}
30+
}
31+
32+
access(all) view fun getViews(): [Type] {
33+
return [Type<StoragePath>(), Type<PublicPath>()]
34+
}
35+
36+
access(all) fun resolveView(_ view: Type): AnyStruct? {
37+
switch view {
38+
case Type<StoragePath>():
39+
return /storage/MyRecurringTaskHandler
40+
case Type<PublicPath>():
41+
return /public/MyRecurringTaskHandler
42+
default:
43+
return nil
44+
}
45+
}
46+
}
47+
48+
access(all) fun createSupervisor(): @Supervisor {
49+
return <- create Supervisor()
50+
}
51+
}

cadence/tests/paid_auto_balance_test.cdc

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "test_helpers_rebalance.cdc"
66
import "FlowCreditMarketRebalancerV1"
77
import "FlowTransactionScheduler"
88
import "MOET"
9+
import "FlowCreditMarketSupervisorV1"
910

1011
access(all) let protocolAccount = Test.getAccount(0x0000000000000007)
1112
access(all) let protocolConsumerAccount = Test.getAccount(0x0000000000000008)
@@ -47,6 +48,13 @@ access(all) fun setup() {
4748
createWrappedPosition(signer: userAccount, amount: 100.0, vaultStoragePath: flowVaultStoragePath, pushToDrawDownSink: false)
4849
depositToWrappedPosition(signer: userAccount, amount: 100.0, vaultStoragePath: flowVaultStoragePath, pushToDrawDownSink: false)
4950
addPaidRebalancerToWrappedPosition(signer: userAccount)
51+
createSupervisor(
52+
signer: userAccount,
53+
cronExpression: "0 * * * *",
54+
cronHandlerStoragePath: /storage/myRecurringTaskHandler,
55+
keeperExecutionEffort: 1000,
56+
executorExecutionEffort: 1000
57+
)
5058

5159
snapshot = getCurrentBlockHeight()
5260
}
@@ -238,8 +246,74 @@ access(all) fun test_public_fix_reschedule() {
238246
var evts = Test.eventsOfType(Type<FlowCreditMarketRebalancerV1.Rebalanced>())
239247
Test.assertEqual(1, evts.length)
240248
let e = evts[0] as! FlowCreditMarketRebalancerV1.Rebalanced
241-
let publicPath = PublicPath(identifier: "paidRebalancerV1\(e.uuid)")!
242249

243250
let randomAccount = Test.createAccount()
244251
fixPaidReschedule(signer: randomAccount, uuid: e.uuid)
252+
}
253+
254+
access(all) fun test_supervisor_executed() {
255+
safeReset()
256+
257+
Test.moveTime(by: 100.0)
258+
Test.commitBlock()
259+
260+
var evts = Test.eventsOfType(Type<FlowCreditMarketSupervisorV1.Executed>())
261+
Test.assertEqual(1, evts.length)
262+
263+
Test.moveTime(by: 60.0 * 60.0)
264+
Test.commitBlock()
265+
266+
evts = Test.eventsOfType(Type<FlowCreditMarketSupervisorV1.Executed>())
267+
Test.assertEqual(2, evts.length)
268+
}
269+
270+
access(all) fun test_supervisor() {
271+
safeReset()
272+
273+
Test.moveTime(by: 100.0)
274+
Test.commitBlock()
275+
276+
var evts = Test.eventsOfType(Type<FlowCreditMarketSupervisorV1.Executed>())
277+
Test.assertEqual(1, evts.length)
278+
279+
evts = Test.eventsOfType(Type<FlowCreditMarketRebalancerV1.Rebalanced>())
280+
Test.assertEqual(1, evts.length)
281+
let e = evts[0] as! FlowCreditMarketRebalancerV1.Rebalanced
282+
283+
addPaidRebalancerToSupervisor(signer: userAccount, uuid: e.uuid)
284+
285+
// drain the funding contract so the transaction reverts
286+
let balance = getBalance(address: protocolAccount.address, vaultPublicPath: /public/flowTokenBalance)!
287+
actuallyTransferFlowTokens(from: protocolAccount, to: userAccount, amount: balance)
288+
289+
Test.moveTime(by: 100.0)
290+
Test.commitBlock()
291+
292+
// it still executed once but should have no transaction scheduled
293+
evts = Test.eventsOfType(Type<FlowCreditMarketRebalancerV1.Rebalanced>())
294+
Test.assertEqual(2, evts.length)
295+
296+
Test.moveTime(by: 1000.0)
297+
Test.commitBlock()
298+
evts = Test.eventsOfType(Type<FlowCreditMarketRebalancerV1.Rebalanced>())
299+
Test.assertEqual(2, evts.length)
300+
301+
// now we fix the missing funds and call fix reschedule
302+
mintFlow(to: protocolAccount, amount: 1000.0)
303+
Test.moveTime(by: 60.0* 100.0)
304+
Test.commitBlock()
305+
306+
// now supervisor will fix the rebalancer
307+
evts = Test.eventsOfType(Type<FlowCreditMarketSupervisorV1.Executed>())
308+
Test.assertEqual(2, evts.length)
309+
310+
evts = Test.eventsOfType(Type<FlowCreditMarketRebalancerV1.FixReschedule>())
311+
Test.assertEqual(2, evts.length)
312+
313+
Test.moveTime(by: 10.0)
314+
Test.commitBlock()
315+
316+
// now rebalancer could run the transaction again
317+
evts = Test.eventsOfType(Type<FlowCreditMarketRebalancerV1.Rebalanced>())
318+
Test.assertEqual(3, evts.length)
245319
}

cadence/tests/test_helpers.cdc

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,33 @@ fun deployContracts() {
166166
)
167167
Test.expect(err, Test.beNil())
168168

169-
err = Test.deployContract(
169+
err = Test.deployContract(
170170
name: "SimpleSinkSource",
171171
path: "../contracts/mocks/SimpleSinkSource.cdc",
172172
arguments: []
173173
)
174174
Test.expect(err, Test.beNil())
175+
176+
err = Test.deployContract(
177+
name: "FlowCronUtils",
178+
path: "../../imports/6dec6e64a13b881e/FlowCronUtils.cdc",
179+
arguments: []
180+
)
181+
Test.expect(err, Test.beNil())
182+
183+
err = Test.deployContract(
184+
name: "FlowCron",
185+
path: "../../imports/6dec6e64a13b881e/FlowCron.cdc",
186+
arguments: []
187+
)
188+
Test.expect(err, Test.beNil())
189+
190+
err = Test.deployContract(
191+
name: "FlowCreditMarketSupervisorV1",
192+
path: "../contracts/FlowCreditMarketSupervisorV1.cdc",
193+
arguments: []
194+
)
195+
Test.expect(err, Test.beNil())
175196
}
176197

177198
/* --- Script Helpers --- */

cadence/tests/test_helpers_rebalance.cdc

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ fun changePaidInterval(
6464
Test.expect(setRes, expectFailure ? Test.beFailed() : Test.beSucceeded())
6565
}
6666

67-
6867
access(all)
6968
fun deletePaidRebalancer(
7069
signer: Test.TestAccount,
@@ -75,4 +74,33 @@ fun deletePaidRebalancer(
7574
signer
7675
)
7776
Test.expect(setRes, Test.beSucceeded())
77+
}
78+
79+
access(all)
80+
fun createSupervisor(
81+
signer: Test.TestAccount,
82+
cronExpression: String,
83+
cronHandlerStoragePath: StoragePath,
84+
keeperExecutionEffort: UInt64,
85+
executorExecutionEffort: UInt64
86+
) {
87+
let setRes = _executeTransaction(
88+
"./transactions/rebalancer/create_supervisor.cdc",
89+
[cronExpression, cronHandlerStoragePath, keeperExecutionEffort, executorExecutionEffort],
90+
signer
91+
)
92+
Test.expect(setRes, Test.beSucceeded())
93+
}
94+
95+
access(all)
96+
fun addPaidRebalancerToSupervisor(
97+
signer: Test.TestAccount,
98+
uuid: UInt64
99+
) {
100+
let setRes = _executeTransaction(
101+
"./transactions/rebalancer/add_rebalancer_to_supervisor.cdc",
102+
[uuid],
103+
signer
104+
)
105+
Test.expect(setRes, Test.beSucceeded())
78106
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import "FlowTransactionScheduler"
2+
import "FlowTransactionSchedulerUtils"
3+
import "FungibleToken"
4+
import "FlowToken"
5+
import "FlowCron"
6+
import "FlowCreditMarketSupervisorV1"
7+
8+
transaction(
9+
uuid: UInt64
10+
) {
11+
let signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue) &Account
12+
let supervisor: Capability<&FlowCreditMarketSupervisorV1.Supervisor>
13+
14+
prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue) &Account) {
15+
self.supervisor = signer.capabilities.storage.issue<&FlowCreditMarketSupervisorV1.Supervisor>(/storage/flowCreditMarketSupervisor)
16+
self.signer = signer
17+
}
18+
19+
execute {
20+
self.supervisor.borrow()!.addPaidRebalancer(uuid: uuid)
21+
}
22+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import "FlowTransactionScheduler"
2+
import "FlowTransactionSchedulerUtils"
3+
import "FungibleToken"
4+
import "FlowToken"
5+
import "FlowCron"
6+
import "FlowCreditMarketSupervisorV1"
7+
8+
transaction(
9+
cronExpression: String,
10+
cronHandlerStoragePath: StoragePath,
11+
keeperExecutionEffort: UInt64,
12+
executorExecutionEffort: UInt64,
13+
) {
14+
let signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue) &Account
15+
let feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
16+
17+
prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue) &Account) {
18+
self.feeProviderCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &FlowToken.Vault>(/storage/flowTokenVault)
19+
self.signer = signer
20+
}
21+
22+
execute {
23+
let supervisor <- FlowCreditMarketSupervisorV1.createSupervisor()
24+
self.signer.storage.save(<-supervisor, to: /storage/flowCreditMarketSupervisor)
25+
let wrappedHandlerCap =
26+
self.signer.capabilities.storage.issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}
27+
>(/storage/flowCreditMarketSupervisor)
28+
assert(wrappedHandlerCap.check(), message: "Invalid wrapped handler capability")
29+
self.signer.storage.save(<-FlowTransactionSchedulerUtils.createManager(), to: FlowTransactionSchedulerUtils.managerStoragePath)
30+
let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}> = self.signer.capabilities.storage.issue<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(
31+
FlowTransactionSchedulerUtils.managerStoragePath
32+
)
33+
let manager = self.signer.storage.borrow<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(
34+
from: FlowTransactionSchedulerUtils.managerStoragePath
35+
) ?? panic("Cannot borrow manager")
36+
assert(schedulerManagerCap.check(), message: "Invalid scheduler manager capability")
37+
let cronHandler <- FlowCron.createCronHandler(
38+
cronExpression: cronExpression,
39+
wrappedHandlerCap: wrappedHandlerCap,
40+
feeProviderCap: self.feeProviderCap,
41+
schedulerManagerCap: schedulerManagerCap
42+
)
43+
self.signer.storage.save(<-cronHandler, to: cronHandlerStoragePath)
44+
let cronHandlerCap = self.signer.capabilities.storage.issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(cronHandlerStoragePath)
45+
assert(cronHandlerCap.check(), message: "Invalid cron handler capability")
46+
47+
let executorEstimate = FlowTransactionScheduler.estimate(
48+
data: nil,
49+
timestamp: UFix64(getCurrentBlock().timestamp + 1.0),
50+
priority: FlowTransactionScheduler.Priority.Low,
51+
executionEffort: executorExecutionEffort
52+
)
53+
54+
let executorFee = executorEstimate.flowFee! * 2.0
55+
56+
let keeperEstimate = FlowTransactionScheduler.estimate(
57+
data: nil,
58+
timestamp: UFix64(getCurrentBlock().timestamp + 2.0),
59+
priority: FlowTransactionScheduler.Priority.Low,
60+
executionEffort: keeperExecutionEffort
61+
)
62+
63+
let keeperFee = keeperEstimate.flowFee! * 2.0
64+
65+
let totalFee = executorFee + keeperFee
66+
67+
// Borrow fee vault and check balance
68+
let feeVault = self.signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
69+
?? panic("Flow token vault not found")
70+
71+
if feeVault.balance < totalFee {
72+
panic("Insufficient funds: required ".concat(totalFee.toString()).concat(" FLOW (executor: ").concat(executorFee.toString()).concat(", keeper: ").concat(keeperFee.toString()).concat("), available ").concat(feeVault.balance.toString()))
73+
}
74+
75+
// Withdraw fees for BOTH transactions
76+
let executorFees <- feeVault.withdraw(amount: executorFee) as! @FlowToken.Vault
77+
let keeperFees <- feeVault.withdraw(amount: keeperFee) as! @FlowToken.Vault
78+
79+
let executorContext = FlowCron.CronContext(
80+
executionMode: FlowCron.ExecutionMode.Executor,
81+
executorPriority: FlowTransactionScheduler.Priority.Low,
82+
executorExecutionEffort: executorExecutionEffort,
83+
keeperExecutionEffort: keeperExecutionEffort,
84+
wrappedData: nil
85+
)
86+
87+
let executorTxID = manager.schedule(
88+
handlerCap: cronHandlerCap,
89+
data: executorContext,
90+
timestamp: UFix64(getCurrentBlock().timestamp + 1.0),
91+
priority: FlowTransactionScheduler.Priority.Low,
92+
executionEffort: executorExecutionEffort,
93+
fees: <-executorFees
94+
)
95+
96+
97+
let keeperContext = FlowCron.CronContext(
98+
executionMode: FlowCron.ExecutionMode.Keeper,
99+
executorPriority: FlowTransactionScheduler.Priority.Low,
100+
executorExecutionEffort: executorExecutionEffort,
101+
keeperExecutionEffort: keeperExecutionEffort,
102+
wrappedData: nil
103+
)
104+
105+
// Schedule KEEPER transaction (1 second after executor to prevent race conditions)
106+
let keeperTxID = manager.schedule(
107+
handlerCap: cronHandlerCap,
108+
data: keeperContext,
109+
timestamp: UFix64(getCurrentBlock().timestamp + 2.0),
110+
priority: FlowCron.keeperPriority,
111+
executionEffort: keeperExecutionEffort,
112+
fees: <-keeperFees
113+
)
114+
}
115+
}

0 commit comments

Comments
 (0)