Skip to content

Commit 9b89be2

Browse files
committed
add autobalancer callback
1 parent 72540c5 commit 9b89be2

5 files changed

Lines changed: 130 additions & 6 deletions

File tree

cadence/contracts/interfaces/DeFiActions.cdc

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,14 @@ access(all) contract DeFiActions {
687687
}
688688
}
689689

690+
/// Callback invoked every time an AutoBalancer executes (runs rebalance).
691+
///
692+
access(all) resource interface AutoBalancerExecutionCallback {
693+
/// Called at the end of each rebalance run.
694+
/// @param balancerUUID: The AutoBalancer's UUID
695+
access(all) fun onExecuted(balancerUUID: UInt64)
696+
}
697+
690698
/// AutoBalancer
691699
///
692700
/// A resource designed to enable permissionless rebalancing of value around a wrapped Vault. An
@@ -728,6 +736,8 @@ access(all) contract DeFiActions {
728736
access(self) var _recurringConfig: AutoBalancerRecurringConfig?
729737
/// ScheduledTransaction objects used to manage automated rebalances
730738
access(self) var _scheduledTransactions: @{UInt64: FlowTransactionScheduler.ScheduledTransaction}
739+
/// Optional callback invoked every time rebalance() runs
740+
access(self) var _executionCallback: Capability<&{AutoBalancerExecutionCallback}>?
731741
/// An optional UniqueIdentifier tying this AutoBalancer to a given stack
732742
access(contract) var uniqueID: UniqueIdentifier?
733743

@@ -772,6 +782,7 @@ access(all) contract DeFiActions {
772782
self._recurringConfig = recurringConfig
773783
self._recurringConfig?.setAssignedAutoBalancer(self.uuid)
774784
self._scheduledTransactions <- {}
785+
self._executionCallback = nil
775786
self.uniqueID = uniqueID
776787

777788
emit CreatedAutoBalancer(
@@ -939,6 +950,11 @@ access(all) contract DeFiActions {
939950
}
940951
self._rebalanceRange = range
941952
}
953+
/// Sets the optional callback invoked every time this AutoBalancer runs rebalance.
954+
/// Pass nil to clear the callback.
955+
access(Set) fun setExecutionCallback(_ cap: Capability<&{AutoBalancerExecutionCallback}>?) {
956+
self._executionCallback = cap
957+
}
942958
/// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
943959
/// a DeFiActions stack. See DeFiActions.align() for more information.
944960
access(contract) view fun copyID(): UniqueIdentifier? {
@@ -1039,7 +1055,7 @@ access(all) contract DeFiActions {
10391055
// execute as declared, otherwise execute as currently configured, otherwise default to false
10401056
let dataDict = data as? {String: AnyStruct} ?? {}
10411057
let force = dataDict["force"] as? Bool ?? self._recurringConfig?.forceRebalance as? Bool ?? false
1042-
1058+
10431059
self.rebalance(force: force)
10441060

10451061
// If configured as recurring, schedule the next execution only if this is an internally-managed
@@ -1060,10 +1076,18 @@ access(all) contract DeFiActions {
10601076
}
10611077
}
10621078
}
1079+
if let cap = self._executionCallback {
1080+
if cap.check() {
1081+
if let callback = cap.borrow() {
1082+
callback.onExecuted(balancerUUID: self.uuid)
1083+
}
1084+
}
1085+
}
10631086
// clean up internally-managed historical scheduled transactions
10641087
self._cleanupScheduledTransactions()
10651088
}
1066-
/// Schedules the next execution of the rebalance if the AutoBalancer is configured as such and there is not
1089+
1090+
/// Schedules the next execution of the rebalance if the AutoBalancer is configured as such and there is not
10671091
/// already a scheduled transaction within the desired interval. This method is written to fail as gracefully as
10681092
/// possible, reporting any failures to schedule the next execution to the as an event. This allows
10691093
/// `executeTransaction` to continue execution even if the next execution cannot be scheduled while still
@@ -1171,14 +1195,14 @@ access(all) contract DeFiActions {
11711195
///
11721196
/// @param id: The ID of the scheduled transaction
11731197
///
1174-
/// @return &FlowTransactionScheduler.ScheduledTransaction?: The reference to the scheduled transaction, or nil
1198+
/// @return &FlowTransactionScheduler.ScheduledTransaction?: The reference to the scheduled transaction, or nil
11751199
/// if the scheduled transaction is not found
11761200
///
11771201
access(all) view fun borrowScheduledTransaction(id: UInt64): &FlowTransactionScheduler.ScheduledTransaction? {
11781202
return &self._scheduledTransactions[id]
11791203
}
11801204
/// Calculates the next execution timestamp for a recurring rebalance if the AutoBalancer is configured as such.
1181-
/// Returns nil if either unconfigured for recurring rebalancing or the interval is greater than the maximum
1205+
/// Returns nil if either unconfigured for recurring rebalancing or the interval is greater than the maximum
11821206
/// possible timestamp.
11831207
///
11841208
/// @return UFix64?: The next execution timestamp, or nil if a recurring rebalance is not configured

cadence/tests/AutoBalancer_test.cdc

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "test_helpers.cdc"
44

55
import "DeFiActions"
66
import "FlowTransactionScheduler"
7+
import "ExecutionCallbackRecorder"
78

89
import "TokenA"
910
import "TokenB"
@@ -69,6 +70,12 @@ access(all) fun setup() {
6970
arguments: [tokenAIdentifier], // unitOfAccountIdentifier
7071
)
7172
Test.expect(err, Test.beNil())
73+
err = Test.deployContract(
74+
name: "ExecutionCallbackRecorder",
75+
path: "./contracts/ExecutionCallbackRecorder.cdc",
76+
arguments: [],
77+
)
78+
Test.expect(err, Test.beNil())
7279

7380
// set TokenB price in MockOracle
7481
let setRes = executeTransaction(
@@ -161,6 +168,55 @@ access(all) fun test_SetRebalanceSourceSucceeds() {
161168
Test.assertEqual(0.0, tokenBBalance!)
162169
}
163170

171+
access(all) fun test_ExecutionCallbackRuns() {
172+
if snapshot < getCurrentBlockHeight() {
173+
Test.reset(to: snapshot)
174+
}
175+
let user = Test.createAccount()
176+
transferFlow(signer: serviceAccount, recipient: user.address, amount: 100.0)
177+
let lowerThreshold = 0.9
178+
let upperThreshold = 1.1
179+
180+
// create AutoBalancer
181+
let setupRes = executeTransaction(
182+
"../transactions/auto-balance-adapter/create_auto_balancer.cdc",
183+
[tokenAIdentifier, nil, lowerThreshold, upperThreshold, tokenBIdentifier, autoBalancerStoragePath, autoBalancerPublicPath],
184+
user
185+
)
186+
Test.expect(setupRes, Test.beSucceeded())
187+
188+
let createdEvts = Test.eventsOfType(Type<DeFiActions.CreatedAutoBalancer>())
189+
Test.assertEqual(1, createdEvts.length)
190+
let createdEvt = createdEvts[0] as! DeFiActions.CreatedAutoBalancer
191+
let balancerUUID = createdEvt.uuid
192+
193+
let interval: UInt64 = 10
194+
let executionEffort: UInt64 = 1_000
195+
let priority: UInt8 = 2
196+
let configRes = executeTransaction(
197+
"../transactions/auto-balance-adapter/set_recurring_config.cdc",
198+
[autoBalancerStoragePath, interval, priority, executionEffort, true],
199+
user
200+
)
201+
Test.expect(configRes, Test.beSucceeded())
202+
203+
// set execution callback (invoked only from executeTransaction, not from rebalance())
204+
let setCbRes = executeTransaction(
205+
"./transactions/auto-balance-adapter/set_execution_callback.cdc",
206+
[autoBalancerStoragePath],
207+
user
208+
)
209+
Test.expect(setCbRes, Test.beSucceeded())
210+
211+
// advance time so scheduler runs executeTransaction on the AutoBalancer
212+
Test.moveTime(by: 11.0)
213+
214+
let invokedEvts = Test.eventsOfType(Type<ExecutionCallbackRecorder.Invoked>())
215+
Test.assertEqual(1, invokedEvts.length)
216+
let invokedEvt = invokedEvts[0] as! ExecutionCallbackRecorder.Invoked
217+
Test.assertEqual(balancerUUID, invokedEvt.balancerUUID)
218+
}
219+
164220
access(all) fun test_ForceRebalanceToSinkSucceeds() {
165221
Test.reset(to: snapshot)
166222
let user = Test.createAccount()
@@ -628,7 +684,7 @@ access(all) fun test_RecurringRebalanceToSinkSucceeds() {
628684
now = getCurrentBlockTimestamp()
629685
schedEvts = Test.eventsOfType(Type<FlowTransactionScheduler.Scheduled>())
630686
Test.assertEqual(2, schedEvts.length)
631-
687+
632688
schedEvts = Test.eventsOfType(Type<FlowTransactionScheduler.Scheduled>())
633689
schedEvt = schedEvts[schedEvts.length - 1] as! FlowTransactionScheduler.Scheduled
634690
Test.assertEqual(user.address, schedEvt.transactionHandlerOwner)
@@ -760,7 +816,7 @@ access(all) fun test_RecurringRebalanceFromSourceSucceeds() {
760816
now = getCurrentBlockTimestamp()
761817
schedEvts = Test.eventsOfType(Type<FlowTransactionScheduler.Scheduled>())
762818
Test.assertEqual(2, schedEvts.length)
763-
819+
764820
schedEvts = Test.eventsOfType(Type<FlowTransactionScheduler.Scheduled>())
765821
schedEvt = schedEvts[schedEvts.length - 1] as! FlowTransactionScheduler.Scheduled
766822
Test.assertEqual(user.address, schedEvt.transactionHandlerOwner)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import "DeFiActions"
2+
3+
///
4+
/// Test-only contract: resource that implements AutoBalancerExecutionCallback
5+
/// and emits an event so tests can assert the callback ran.
6+
///
7+
access(all) contract ExecutionCallbackRecorder {
8+
9+
access(all) event Invoked(balancerUUID: UInt64)
10+
11+
access(all) fun createRecorder(): @Recorder {
12+
return <- create Recorder()
13+
}
14+
15+
access(all) resource Recorder: DeFiActions.AutoBalancerExecutionCallback {
16+
access(all) fun onExecuted(balancerUUID: UInt64) {
17+
emit Invoked(balancerUUID: balancerUUID)
18+
}
19+
}
20+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import "DeFiActions"
2+
import "ExecutionCallbackRecorder"
3+
4+
/// Test helper: creates an ExecutionCallbackRecorder, saves it, and sets it as the AutoBalancer's execution callback.
5+
///
6+
/// @param autoBalancerStoragePath: storage path of the AutoBalancer
7+
///
8+
transaction(autoBalancerStoragePath: StoragePath) {
9+
10+
prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController) &Account) {
11+
let recorder <- ExecutionCallbackRecorder.createRecorder()
12+
signer.storage.save(<-recorder, to: /storage/autoBalancerExecutionCallback)
13+
let cap = signer.capabilities.storage.issue<&{DeFiActions.AutoBalancerExecutionCallback}>(/storage/autoBalancerExecutionCallback)
14+
let ab = signer.storage.borrow<auth(DeFiActions.Set) &DeFiActions.AutoBalancer>(from: autoBalancerStoragePath)
15+
?? panic("AutoBalancer not found at \(autoBalancerStoragePath)")
16+
ab.setExecutionCallback(cap)
17+
}
18+
}

flow.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@
144144
"testing": "0000000000000009"
145145
}
146146
},
147+
"ExecutionCallbackRecorder": {
148+
"source": "cadence/tests/contracts/ExecutionCallbackRecorder.cdc",
149+
"aliases": {
150+
"testing": "0000000000000009"
151+
}
152+
},
147153
"MorphoERC4626SinkConnectors": {
148154
"source": "cadence/contracts/connectors/evm/morpho/MorphoERC4626SinkConnectors.cdc",
149155
"aliases": {

0 commit comments

Comments
 (0)