Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/rebalancer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,17 @@ bun run start

Passing `--legacy` exits with an error; it cannot start the retired Pendulum/Moonbeam flow.

## Resetting a stuck run

Each Base flow persists its phase in Supabase Storage and resumes it on the next cron run before
anything new is quoted. If a run can no longer complete (for example its funds were moved back
manually), reconcile the funds first, then suspend the Render cron job and make sure no
rebalancer process is running: the script and a live run write the same Supabase object
without locking. Then inspect and reset the USDC → BRLA → USDC state:

```bash
bun run reset:usdc-base-state # prints the persisted state, changes nothing
bun run reset:usdc-base-state --confirm # resets it to idle, keeping history
```

This project was created using `bun init` in bun v1.2.6. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
1 change: 1 addition & 0 deletions apps/rebalancer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dev": "bun run src/index.ts",
"format": "biome check --write --unsafe --no-errors-on-unmatched",
"prepare": "husky",
"reset:usdc-base-state": "bun run src/scripts/resetUsdcBaseState.ts",
"serve": "bun dist/index.js",
"start": "bun run build && bun run serve",
"test": "bun test",
Expand Down
147 changes: 57 additions & 90 deletions apps/rebalancer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import {
type RebalancingCostPolicyDecision,
shouldTriggerOpportunisticUsdcToBrla
} from "./rebalance/usdc-brla-usdc-base/guards.ts";
import { checkInitialUsdcBalanceOnBase, compareRoutesUpfront } from "./rebalance/usdc-brla-usdc-base/steps.ts";
import { getBaseNablaCoverageRatio } from "./services/indexer";
import {
BrlaToUsdcBaseRebalancePhase,
BrlaToUsdcBaseStateManager,
UsdcBaseRebalancePhase,
UsdcBaseStateManager,
type WinningRoute
} from "./services/stateManager.ts";
checkInitialUsdcBalanceOnBase,
compareRoutesUpfront,
getUsdcBalanceOnBaseRaw
} from "./rebalance/usdc-brla-usdc-base/steps.ts";
import { resumeInFlightRebalance, runRebalanceCycle, shouldQuoteProfitableAmount } from "./rebalanceCycle.ts";
import { getBaseNablaCoverageRatio } from "./services/indexer";
import { BrlaToUsdcBaseStateManager, UsdcBaseStateManager, type WinningRoute } from "./services/stateManager.ts";
import { getConfig } from "./utils/config.ts";

const args = process.argv.slice(2);
Expand Down Expand Up @@ -106,14 +105,6 @@ async function evaluateCurrentRunDailyLimit(
return { dailyVolume, decision: dailyLimitDecision };
}

function calculateCoverageDeviationBps(coverageRatio: number, triggerBound: number): number {
return Number(
Big(Math.abs(coverageRatio - triggerBound))
.mul(10_000)
.toFixed(2)
);
}

function getQuoteForRoute(
route: Exclude<WinningRoute, null>,
quotes: {
Expand Down Expand Up @@ -264,7 +255,13 @@ async function selectUsdcToBrlaPolicyAmount(coverageDeviationBps: number): Promi
}

const profitableAmountRaw = toUsdcRaw(config.rebalancingProfitableUsdToBrlAmount);
if (profitableAmountRaw === standardAmountRaw) {
const quoteProfitableAmount = await shouldQuoteProfitableAmount({
getBaseUsdcRaw: getUsdcBalanceOnBaseRaw,
mode: config.rebalancingCostPolicy.mode,
profitableAmountRaw,
standardAmountRaw
});
if (!quoteProfitableAmount) {
return { amountUsdcRaw: standardAmountRaw, policyDecision: standardPolicyDecision };
}

Expand Down Expand Up @@ -343,89 +340,59 @@ async function evaluateBrlaToUsdcPolicy(
}

async function runUsdcToBrla(coverageDeviationBps: number) {
const config = getConfig();
const amountUsdcRaw = toUsdcRaw(manualAmount || config.rebalancingUsdToBrlAmount);

const stateManager = new UsdcBaseStateManager();
const state = await stateManager.getState();
const isResuming = !forceRestart && state && state.currentPhase !== UsdcBaseRebalancePhase.Idle;

if (!isResuming) {
const selectedAmount = await selectUsdcToBrlaPolicyAmount(coverageDeviationBps);
const policyDecision = selectedAmount.policyDecision;
if (!policyDecision.shouldExecute) return;
await executeUsdcToBrlaRebalance(selectedAmount.amountUsdcRaw, coverageDeviationBps, policyDecision);
return;
}

await rebalanceUsdcBrlaUsdcBase(amountUsdcRaw, forceRestart, forcedRoute);
const selectedAmount = await selectUsdcToBrlaPolicyAmount(coverageDeviationBps);
const policyDecision = selectedAmount.policyDecision;
if (!policyDecision.shouldExecute) return;
await executeUsdcToBrlaRebalance(selectedAmount.amountUsdcRaw, coverageDeviationBps, policyDecision);
}

async function runBrlaToUsdc(coverageDeviationBps: number) {
const config = getConfig();
const amountUsdc = manualAmount || config.rebalancingBrlToUsdAmount;
const amountUsdcRaw = multiplyByPowerOfTen(new Big(amountUsdc), 6).toFixed(0, 0);

const stateManager = new BrlaToUsdcBaseStateManager();
const state = await stateManager.getState();
const isResuming = !forceRestart && state && state.currentPhase !== BrlaToUsdcBaseRebalancePhase.Idle;

if (!isResuming) {
const policyDecision = await evaluateBrlaToUsdcPolicy(amountUsdcRaw, coverageDeviationBps);
if (!policyDecision.shouldExecute) return;

const dailyLimitEvaluation = await evaluateCurrentRunDailyLimit(amountUsdcRaw, policyDecision.profitable);
if (dailyLimitEvaluation.decision?.shouldSkip) return;

const rebalancerUsdcBalance = await checkInitialUsdcBalanceOnBase(amountUsdcRaw);
if (config.rebalancingBrlToUsdMinBalance && rebalancerUsdcBalance.lt(config.rebalancingBrlToUsdMinBalance)) {
throw new Error(
`Rebalancer USDC balance ${rebalancerUsdcBalance} is below the minimum required balance of ${config.rebalancingBrlToUsdMinBalance} to perform rebalancing.`
);
}
await rebalanceBrlaToUsdcBase(amountUsdcRaw, forceRestart, {
config: config.rebalancingCostPolicy,
dailyLimitDecision: dailyLimitEvaluation.decision,
dailyVolume: dailyLimitEvaluation.dailyVolume,
decision: policyDecision.decision,
deviationBps: coverageDeviationBps,
fallbackRequiresProfit: policyDecision.profitable
});
return;
}

await rebalanceBrlaToUsdcBase(amountUsdcRaw, forceRestart);
}

async function checkForRebalancing() {
const config = getConfig();
const coverage = await getBaseNablaCoverageRatio();

if (!coverage) throw new Error("Failed to fetch Base Nabla coverage ratio.");
const amountUsdcRaw = toUsdcRaw(manualAmount || config.rebalancingBrlToUsdAmount);

const lowerBound = 1 - config.rebalancingThresholdBrlaToUsdc;
const upperBound = 1 + config.rebalancingThresholdUsdcToBrla;
const policyDecision = await evaluateBrlaToUsdcPolicy(amountUsdcRaw, coverageDeviationBps);
if (!policyDecision.shouldExecute) return;

if (coverage.brlaCoverageRatio >= lowerBound && coverage.brlaCoverageRatio <= upperBound) {
if (await tryOpportunisticUsdcToBrla()) return;
console.log(`BRLA coverage ${coverage.brlaCoverageRatio} in range [${lowerBound}, ${upperBound}]. No rebalancing needed.`);
return;
}
const dailyLimitEvaluation = await evaluateCurrentRunDailyLimit(amountUsdcRaw, policyDecision.profitable);
if (dailyLimitEvaluation.decision?.shouldSkip) return;

if (coverage.brlaCoverageRatio < lowerBound) {
const deviationBps = calculateCoverageDeviationBps(coverage.brlaCoverageRatio, lowerBound);
console.log(
`BRLA coverage ${coverage.brlaCoverageRatio} < ${lowerBound}. Evaluating BRLA->USDC (${deviationBps} bps deviation).`
const rebalancerUsdcBalance = await checkInitialUsdcBalanceOnBase(amountUsdcRaw);
if (config.rebalancingBrlToUsdMinBalance && rebalancerUsdcBalance.lt(config.rebalancingBrlToUsdMinBalance)) {
throw new Error(
`Rebalancer USDC balance ${rebalancerUsdcBalance} is below the minimum required balance of ${config.rebalancingBrlToUsdMinBalance} to perform rebalancing.`
);
await runBrlaToUsdc(deviationBps);
return;
}
await rebalanceBrlaToUsdcBase(amountUsdcRaw, forceRestart, {
config: config.rebalancingCostPolicy,
dailyLimitDecision: dailyLimitEvaluation.decision,
dailyVolume: dailyLimitEvaluation.dailyVolume,
decision: policyDecision.decision,
deviationBps: coverageDeviationBps,
fallbackRequiresProfit: policyDecision.profitable
});
}

const deviationBps = calculateCoverageDeviationBps(coverage.brlaCoverageRatio, upperBound);
console.log(
`BRLA coverage ${coverage.brlaCoverageRatio} > ${upperBound}. Evaluating USDC->BRLA (${deviationBps} bps deviation).`
);
await runUsdcToBrla(deviationBps);
async function checkForRebalancing() {
const config = getConfig();

await runRebalanceCycle({
lowerBound: 1 - config.rebalancingThresholdBrlaToUsdc,
readCoverage: getBaseNablaCoverageRatio,
resumeInFlight: () =>
resumeInFlightRebalance({
forceRestart,
getBrlaToUsdcState: () => new BrlaToUsdcBaseStateManager().getState(),
getUsdcBaseState: () => new UsdcBaseStateManager().getState(),
mode: config.rebalancingCostPolicy.mode,
resumeBrlaToUsdc: () => rebalanceBrlaToUsdcBase(toUsdcRaw(manualAmount || config.rebalancingBrlToUsdAmount), false),
resumeUsdcBase: () =>
rebalanceUsdcBrlaUsdcBase(toUsdcRaw(manualAmount || config.rebalancingUsdToBrlAmount), false, forcedRoute)
}),
runBrlaToUsdc,
runUsdcToBrla,
tryOpportunisticUsdcToBrla,
upperBound: 1 + config.rebalancingThresholdUsdcToBrla
});
}

console.log("Using Base rebalancing flow.");
Expand Down
Loading
Loading