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
3 changes: 0 additions & 3 deletions .github/pull_request_template.md

This file was deleted.

7 changes: 5 additions & 2 deletions apps/bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ The bot still aims to minimize excess iCKB holdings so more liquidity stays avai

## Docs

- [iCKB Deposit Pool Rebalancing Algorithm](pool_rebalancing.md)
- [iCKB Deposit Pool Snapshot Encoding](pool_snapshot.md)
- [Current Bot Rebalancing Policy](docs/current_rebalancing_policy.md)
- Future improvement ideas:
- [iCKB Deposit Pool Rebalancing Algorithm](docs/pool_rebalancing.md)
- [iCKB Deposit Pool Snapshot Encoding](docs/pool_snapshot.md)

## Environment

Expand Down Expand Up @@ -63,6 +65,7 @@ The start script keeps the existing JSON log format and writes one log file per
- Distribute liquidity across multiple isolated bots to limit blast radius.
- Keep at least roughly 130k CKB worth of capital available for the bot to operate comfortably.
- The bot relies on shared CCC packages for protocol-specific transaction content, but it still owns final iCKB completion, fee completion, signing, and send.
- The interface-side maturity estimate contract now lives with `@ickb/sdk`, because the SDK owns how bot liquidity and pool maturities are summarized for UI consumers.

## Licensing

Expand Down
6 changes: 6 additions & 0 deletions apps/bot/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@ This directory hosts comprehensive documentation outlining the inner workings of

## Documents

### Current runtime behavior

- [Current Bot Rebalancing Policy](current_rebalancing_policy.md)

### Future improvement ideas

- [iCKB Deposit Pool Rebalancing Algorithm](pool_rebalancing.md)
- [iCKB Deposit Pool Snapshot Encoding](pool_snapshot.md)
75 changes: 75 additions & 0 deletions apps/bot/docs/current_rebalancing_policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Current Bot Rebalancing Policy

This document describes the policy currently implemented in `apps/bot/src/policy.ts`.

## Goal

The bot keeps enough liquid iCKB to keep matching and redemption paths responsive, while leaving as much capital as practical in CKB.

The live policy is intentionally small:

- keep a minimum iCKB inventory
- refill that inventory with one direct deposit when it gets too low
- request withdrawals from ready pool deposits when iCKB inventory drifts too high
- do nothing when output space or balances make the action unsafe

## Inputs

`planRebalance(...)` decides from five inputs:

- `outputSlots`: how many transaction output slots remain before the bot would hit its DAO-safe output cap
- `ickbBalance`: currently available iCKB after pending order matches are applied
- `ckbBalance`: currently available CKB after pending order matches are applied
- `depositCapacity`: the current CKB capacity required for one standard iCKB deposit at the live exchange ratio
- `readyDeposits`: ready pool deposits that the bot can request for withdrawal now

## Constants

The current policy is shaped by three constants in `apps/bot/src/policy.ts`:

- `CKB_RESERVE = 1000 CKB`: the bot keeps this much extra CKB after making a new deposit
- `MIN_ICKB_BALANCE = 2000 iCKB`: if iCKB falls below this line, the bot tries to replenish it
- `TARGET_ICKB_BALANCE = 100000 iCKB + 20000 iCKB`: if iCKB rises above this target band, the bot tries to convert excess iCKB back toward CKB through ready deposit withdrawals

The current withdrawal request cap is `30` deposits per transaction.

## Decision Order

The policy is deliberately greedy and local.

1. If fewer than two output slots remain, do nothing.
2. If available iCKB is below `MIN_ICKB_BALANCE`:
- request one new deposit if available CKB is at least `depositCapacity + CKB_RESERVE`
- otherwise do nothing
3. If available iCKB is at or above `MIN_ICKB_BALANCE`, compute `excessIckb = ickbBalance - TARGET_ICKB_BALANCE`.
4. If `excessIckb <= 0`, do nothing.
5. Otherwise, pick a bounded subset of ready deposits whose total `udtValue` stays within `excessIckb`, and request withdrawals for that subset.

## Ready Deposit Selection

`selectReadyDeposits(...)` is intentionally simple.

- It walks the ready deposits in the order they were prepared by the bot state reader.
- It skips any deposit that would push the cumulative selected `udtValue` above the current excess target.
- It stops once it reaches the request limit.

This keeps the live policy predictable and cheap. It does not try to globally optimize pool shape.

## Ownership Boundary

This file describes bot-owned operating policy only.

- The bot owns when to add one more deposit.
- The bot owns when to request ready withdrawals.
- `@ickb/sdk` owns UI-side maturity estimation from live stack state.
- The older pool snapshot idea is not part of the current runtime path.

## Non-Goals

This policy does not try to:

- maintain a global optimal distribution of deposits over the full 180-epoch clock
- encode a snapshot summary for interface use
- predict or coordinate other bots' behavior beyond acting on current visible state

Those may still be useful research directions, but they are not the current live contract.
2 changes: 2 additions & 0 deletions apps/bot/docs/pool_rebalancing.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# iCKB Deposit Pool Rebalancing Algorithm

Future improvement idea: this document captures a more ambitious rebalancing design that is not the current live bot policy. The current implemented behavior is documented in `current_rebalancing_policy.md`.

For simplicity, let's model:

- NervosDAO 180 epoch cycle as a circular clock.
Expand Down
2 changes: 2 additions & 0 deletions apps/bot/docs/pool_snapshot.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# iCKB Deposit Pool Snapshot Encoding

Future improvement idea: this document captures a possible snapshot-based estimate path for large deposit pools. The current live runtime path is documented in `packages/sdk/docs/pool_maturity_estimates.md` and still uses direct deposit scans.

## Introduction

Efficient asset conversion timing is paramount for the iCKB protocol, particularly when converting from iCKB to CKB. Although CKB-to-iCKB conversion timings are relatively simple to predict, the reverse process is influenced by factors like Bot CKB availability and, critically, the maturity of iCKB deposits available for withdrawal.
Expand Down
3 changes: 2 additions & 1 deletion apps/bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@ckb-ccc/core": "catalog:",
"@ickb/core": "workspace:*",
"@ickb/order": "workspace:*",
"@ickb/sdk": "workspace:*"
"@ickb/sdk": "workspace:*",
"@ickb/utils": "workspace:*"
}
}
17 changes: 9 additions & 8 deletions apps/bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type OrderGroup,
} from "@ickb/order";
import { getConfig, IckbSdk, type SystemState } from "@ickb/sdk";
import { isPlainCapacityCell } from "@ickb/utils";
import { CKB, planRebalance } from "./policy.js";

const MATCH_STEP_DIVISOR = 100n;
Expand Down Expand Up @@ -41,7 +42,7 @@ interface BotState {
availableIckbBalance: bigint;
unavailableCkbBalance: bigint;
totalCkbBalance: bigint;
depositAmount: bigint;
depositCapacity: bigint;
minCkbBalance: bigint;
}

Expand Down Expand Up @@ -210,7 +211,7 @@ async function readBotState(runtime: Runtime): Promise<BotState> {
(group) => group.ckbValue,
);
const totalCkbBalance = availableCkbBalance + unavailableCkbBalance;
const depositAmount = convert(false, ICKB_DEPOSIT_CAP, system.exchangeRatio);
const depositCapacity = convert(false, ICKB_DEPOSIT_CAP, system.exchangeRatio);

return {
accountLocks,
Expand All @@ -225,8 +226,8 @@ async function readBotState(runtime: Runtime): Promise<BotState> {
availableIckbBalance,
unavailableCkbBalance,
totalCkbBalance,
depositAmount,
minCkbBalance: (21n * depositAmount) / 20n,
depositCapacity,
minCkbBalance: (21n * depositCapacity) / 20n,
};
}

Expand Down Expand Up @@ -272,7 +273,7 @@ async function buildTransaction(
state.system.exchangeRatio,
{
feeRate: state.system.feeRate,
ckbAllowanceStep: maxBigInt(1n, state.depositAmount / MATCH_STEP_DIVISOR),
ckbAllowanceStep: maxBigInt(1n, state.depositCapacity / MATCH_STEP_DIVISOR),
},
);
if (match.partials.length > 0) {
Expand All @@ -283,14 +284,14 @@ async function buildTransaction(
outputSlots: maxInt(0, MAX_OUTPUTS_BEFORE_CHANGE - tx.outputs.length),
Comment thread
phroi marked this conversation as resolved.
ickbBalance: state.availableIckbBalance + match.udtDelta,
ckbBalance: state.availableCkbBalance + match.ckbDelta,
depositAmount: state.depositAmount,
depositCapacity: state.depositCapacity,
readyDeposits: state.readyPoolDeposits,
});
if (rebalance.kind === "deposit") {
tx = await runtime.managers.logic.deposit(
tx,
rebalance.quantity,
state.depositAmount,
state.depositCapacity,
runtime.primaryLock,
runtime.client,
);
Expand Down Expand Up @@ -343,7 +344,7 @@ async function collectCapacityCells(
"asc",
400,
)) {
if (cell.cellOutput.type !== undefined || cell.outputData !== "0x") {
if (!isPlainCapacityCell(cell)) {
continue;
}
Comment thread
phroi marked this conversation as resolved.
cells.push(cell);
Expand Down
10 changes: 5 additions & 5 deletions apps/bot/src/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("planRebalance", () => {
outputSlots: 1,
ickbBalance: 0n,
ckbBalance: 2000n * 100000000n,
depositAmount: 1000n * 100000000n,
depositCapacity: 1000n * 100000000n,
readyDeposits: [],
}),
).toEqual({ kind: "none" });
Expand All @@ -40,7 +40,7 @@ describe("planRebalance", () => {
outputSlots: 4,
ickbBalance: 0n,
ckbBalance: 2000n * 100000000n,
depositAmount: 1000n * 100000000n,
depositCapacity: 1000n * 100000000n,
readyDeposits: [],
}),
).toEqual({ kind: "deposit", quantity: 1 });
Expand All @@ -52,7 +52,7 @@ describe("planRebalance", () => {
outputSlots: 4,
ickbBalance: 0n,
ckbBalance: 1999n * 100000000n,
depositAmount: 1000n * 100000000n,
depositCapacity: 1000n * 100000000n,
readyDeposits: [],
}),
).toEqual({ kind: "none" });
Expand All @@ -63,7 +63,7 @@ describe("planRebalance", () => {
outputSlots: 6,
ickbBalance: TARGET_ICKB_BALANCE + 9n,
ckbBalance: 0n,
depositAmount: 1000n,
depositCapacity: 1000n,
readyDeposits: [
{ udtValue: 4n },
{ udtValue: 6n },
Expand All @@ -83,7 +83,7 @@ describe("planRebalance", () => {
outputSlots: 6,
ickbBalance: TARGET_ICKB_BALANCE + 3n,
ckbBalance: 0n,
depositAmount: 1000n,
depositCapacity: 1000n,
readyDeposits: [{ udtValue: 4n }] as never[],
}),
).toEqual({ kind: "none" });
Expand Down
12 changes: 9 additions & 3 deletions apps/bot/src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,24 @@ export function planRebalance(options: {
outputSlots: number;
ickbBalance: bigint;
ckbBalance: bigint;
depositAmount: bigint;
depositCapacity: bigint;
readyDeposits: readonly IckbDepositCell[];
}): RebalancePlan {
const { outputSlots, ickbBalance, ckbBalance, depositAmount, readyDeposits } =
const {
outputSlots,
ickbBalance,
ckbBalance,
depositCapacity,
readyDeposits,
} =
options;

if (outputSlots < 2) {
return { kind: "none" };
}

if (ickbBalance < MIN_ICKB_BALANCE) {
if (ckbBalance >= depositAmount + CKB_RESERVE) {
if (ckbBalance >= depositCapacity + CKB_RESERVE) {
return { kind: "deposit", quantity: 1 };
}
return { kind: "none" };
Expand Down
2 changes: 2 additions & 0 deletions apps/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"dev": "pnpm --filter @ickb/core --filter @ickb/order --filter @ickb/sdk --filter @ickb/utils build && vite",
"build": "pnpm --filter @ickb/core --filter @ickb/order --filter @ickb/sdk --filter @ickb/utils build && tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ci": "vitest run",
"lint": "eslint ./src",
"clean": "rm -fr dist",
"clean:deep": "rm -fr dist node_modules"
Expand Down
52 changes: 52 additions & 0 deletions apps/interface/src/queries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ccc } from "@ckb-ccc/ccc";
import { describe, expect, it } from "vitest";
import { isPlainCapacityCell } from "@ickb/utils";

function byte32FromByte(hexByte: string): `0x${string}` {
if (!/^[0-9a-f]{2}$/iu.test(hexByte)) {
throw new Error("Expected exactly one byte as two hex chars");
}
return `0x${hexByte.repeat(32)}`;
}

function script(codeHashByte: string): ccc.Script {
return ccc.Script.from({
codeHash: byte32FromByte(codeHashByte),
hashType: "type",
args: "0x",
});
}

describe("isPlainCapacityCell", () => {
it("accepts only no-type empty-data cells", () => {
const plain = ccc.Cell.from({
outPoint: { txHash: byte32FromByte("11"), index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(1000),
lock: script("22"),
},
outputData: "0x",
});
const typed = ccc.Cell.from({
outPoint: { txHash: byte32FromByte("33"), index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(2000),
lock: script("22"),
type: script("44"),
},
outputData: "0x",
});
const dataCell = ccc.Cell.from({
outPoint: { txHash: byte32FromByte("55"), index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(3000),
lock: script("22"),
},
outputData: "0xab",
});

expect(isPlainCapacityCell(plain)).toBe(true);
expect(isPlainCapacityCell(typed)).toBe(false);
expect(isPlainCapacityCell(dataCell)).toBe(false);
});
});
6 changes: 3 additions & 3 deletions apps/interface/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ccc } from "@ckb-ccc/ccc";
import type { WithdrawalGroup } from "@ickb/core";
import { type OrderGroup } from "@ickb/order";
import { type SystemState } from "@ickb/sdk";
import { collect, sum } from "@ickb/utils";
import { collect, isPlainCapacityCell, sum, unique } from "@ickb/utils";
import {
buildTransactionPreview,
type TransactionContext,
Expand Down Expand Up @@ -76,7 +76,7 @@ export async function getL1State(
),
]);

const capacityCells = accountCells.filter((cell) => cell.cellOutput.type === undefined);
const capacityCells = accountCells.filter(isPlainCapacityCell);
const udtCells = accountCells.filter((cell) =>
walletConfig.managers.ickbUdt.isUdt(cell),
);
Expand Down Expand Up @@ -169,7 +169,7 @@ export async function getL1State(
async function getAccountCells(walletConfig: WalletConfig): Promise<ccc.Cell[]> {
const cells: ccc.Cell[] = [];

for (const lock of walletConfig.accountLocks) {
for (const lock of unique(walletConfig.accountLocks)) {
for await (const cell of walletConfig.cccClient.findCellsOnChain(
{
script: lock,
Expand Down
Loading
Loading