本文档说明 LiYield 后端测试体系。这里的“后端”不是单指 HTTP server,而是三层:
contracts:链上状态层,负责 vault、ETF token、Yield Swap 仓位、结算、清算。keeper:链下执行层,负责根据策略结果生成调仓任务、生成 allocation plan,后续会负责执行 LI.FI/Earn 交易、清算 watcher、settlement 发布。server:API 与数据层,负责 ETF/strategy 数据、价格历史、snapshot、LI.FI quote 封装、keeper/admin 接口。
目标是每个模块都能独立测试,之后再逐步接入:
- 先测纯函数和模块行为。
- 再测单个服务的输入输出。
- 再测跨模块业务场景。
- 最后测真实链上/外部 API 集成。
运行所有后端测试:
npm run backend:test只运行 server + keeper 测试:
npm run server:test只运行合约测试:
cd packages/contracts
forge test类型检查:
npm run typecheck -w @liyield/server| 模块 | 代码位置 | 当前职责 | 当前测试方式 |
|---|---|---|---|
LiYieldVault |
packages/contracts/src/LiYieldVault.sol |
ERC-4626 vault,用户存入底层资产并获得 liYIELD ETF token;记录 target/position 级 idle/deployed accounting;支持 keeper 部署/回收、oracle 估值、management fee 和 emergency idle redeem |
Foundry 合约测试、链上场景测试 |
SettlementIndex |
packages/contracts/src/SettlementIndex.sol |
发布某个 term 的 realized strategy yield,作为 Yield Swap 到期结算基准 | Foundry 合约测试、链上场景测试 |
YieldSwapMarket |
packages/contracts/src/YieldSwapMarket.sol |
抵押 liYIELD、AMM-like fixed rate quote、开多/开空收益率、盯市、清算、到期结算 |
Foundry 合约测试、链上场景测试 |
strategyEngine |
packages/server/src/services/strategyEngine.ts |
按过去 7 天 APY 排名、选择 Top N、计算权重、生成 strategy summary | Node 单元测试 |
allocationPlan |
packages/server/src/services/allocationPlan.ts |
按权重拆分 treasury 可投资金额,对每个目标生成 LI.FI quote 计划 | Node 单元测试、场景测试,quote fetcher 使用 mock |
earn |
packages/server/src/services/earn.ts |
接 LI.FI Earn Data API,读取 vault APY、TVL、transactional capability 和 portfolio positions,并提供静态 fallback | Node 单元测试,外部 fetch 使用 mock |
earnSnapshot |
packages/server/src/services/earnSnapshot.ts、earnSnapshotStore.ts |
定时采集候选 Earn targets 的 APY/TVL/权重/排名并持久化历史快照 | Node 单元测试、API 测试,持久化使用临时文件隔离 |
rebalancePlanner |
packages/server/src/keeper/rebalancePlanner.ts |
把 snapshot diff 转换成 keeper 可执行任务:exit、enter、rebalance |
Node 单元测试、场景测试 |
keeperExecution |
packages/server/src/keeper/execution.ts、orchestrator.ts、reconcile.ts、onchain.ts、runStore.ts、lock.ts、service.ts |
把 allocation plan / rebalance diff 转换成 keeper 执行状态机,支持 dry-run、vault position 注册、LI.FI route 执行、exit reconciliation、自动调仓、执行锁和本地 run 存储 | Node 单元测试、场景测试;链上 adapter 已实现,默认测试使用 mock |
oracleReporter |
packages/server/src/oracle/reporter.ts、reportStore.ts、service.ts、onchain.ts |
汇总 position value、生成 target NAV 报告、计算 settlement realized rate、读取 LI.FI portfolio 作为估值输入、发布 oracle 报告、报告 hash 和本地报告存储 | Node 单元测试、场景测试,默认使用 mock publisher / value source |
marketOracle |
packages/server/src/oracle/market.ts、onchain.ts |
计算 active term 的 markRateBps、ETF collateral USD price,支持每小时 scheduler 和上链发布到 YieldSwapMarket |
Node 单元测试、API 测试 |
app |
packages/server/src/app.ts |
HTTP API 路由,不启动 listener,可直接用 app.request() 测试 |
Node API 测试 |
refresh/price/snapshot |
packages/server/src/services/refresh.ts、price.ts、snapshotFile.ts |
定时刷新 ETF snapshot、记录 price history、读写本地 SQLite | 后续需要补独立数据库路径测试 |
lifi |
packages/server/src/services/lifi.ts |
封装 LI.FI token metadata 和 quote API | 当前通过 mock quote 间接测试;真实 API 集成测试待补 |
职责:
- 接收底层资产,例如 demo 里的
dUSDC。 - 根据 ERC-4626 规则铸造 vault share,也就是
liYIELD。 - 为 Yield Swap 提供可抵押资产。
- 通过
TREASURY_ROLE管理白名单 treasury/executor recipient。 - 通过
KEEPER_ROLE把 idle assets 调拨到白名单 recipient,并记录 divestment。 - 通过
VALUATION_ORACLE_ROLE上报 deployed positions 的估值变化。 - 记录 managed position id、目标链、executor、外部 position id、principal、reported value 和状态。
- 通过
FEE_MANAGER_ROLE配置 management fee,并以增发 share 的方式计提。 - 通过
EMERGENCY_ROLE启动紧急模式,允许用户按 share 比例赎回 idle assets。 - 通过
PAUSER_ROLE暂停用户存取和 keeper 资金操作。
当前限制:
- keeper execution service 已有可测试状态机,但真实链上交易 adapter 尚未接入。
- LI.FI Composer 执行目前是可注入 adapter,测试使用 mock,尚未接生产私钥/RPC。
- 还没有真实 LI.FI execution id / receipt token 校验。
- oracle report service 已有可测试报告与发布流程,但外部 position value source、签名验证、quorum 或 dispute window 尚未接入。
- 还没有 performance fee 和已部署仓位的 emergency unwind。
职责:
SETTLEMENT_ORACLE_ROLE发布某个termId的最终 realized yield。YieldSwapMarket结算时读取该 realized yield。
当前限制:
- 当前是 trusted oracle role 发布。
- 没有 dispute window、oracle 多签、延迟生效、链下证明。
职责:
- 用户把
liYIELD存为 collateral。 - 用户开
LongYield或ShortYield仓位。 - 合约根据 term 的 AMM-like 曲线计算每笔仓位的
fixedRateBps。 MARK_RATE_ORACLE_ROLE更新 mark rate,合约计算 account health。- 不健康仓位可以被部分清算。
- 到期后根据
SettlementIndex.realizedRateBps逐仓结算 PnL。
关键状态:
collateralBalance[user]:用户总抵押余额。lockedCollateral[user]:被仓位占用的抵押余额。settlementLiquidity:协议作为对手盘支付盈利的一层资金。insuranceFundBalance:保险金,吸收坏账或补充正 PnL。termMarkets[termId]:每个 term 的 base rate、rate scalar、mark rate、long/short open interest。positions[positionId]:每笔 Yield Swap 仓位。
文件:packages/contracts/test/LiYield.t.sol
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
test_openLongYieldAndSettleProfit |
用户做多未来收益率,到期 realized rate 高于 fixed rate,用户盈利 | 仓位 settled;用户 collateral 增加;locked collateral 归零 |
test_openShortYieldAndSettleLoss |
用户做空未来收益率,到期 realized rate 高于 fixed rate,用户亏损 | 用户 collateral 减少;locked collateral 归零 |
test_initialAdminHasAllProtocolRoles |
初始 admin 拥有所有协议角色 | SettlementIndex 和 YieldSwapMarket 的角色都授予给 admin |
test_vaultKeeperDeploysAndRecordsDivestment |
vault keeper 部署资金并记录退出 | idle/deployed/totalAssets accounting 正确 |
test_vaultKeeperCanReportTargetValueChange |
valuation oracle 上报 target value 改变 | target deployed value 和 totalAssets 正确反映收益 |
test_vaultKeeperCannotDeployToUnapprovedTreasuryRecipient |
keeper 不能向未白名单 recipient 调拨资金 | 调用 deployToTarget revert TreasuryRecipientNotApproved |
test_vaultKeeperAndValuationOracleRolesAreSeparated |
keeper 和 valuation oracle 权限分离 | keeper 不能上报估值,oracle 不能记录 divestment |
test_vaultPositionAccountingTracksExecutionMetadataAndTargetIndex |
managed position metadata 和 target index | position 记录目标链、executor、外部 position id;target 可查询对应 position id |
test_vaultPositionValueReportAndDivestmentUpdateAggregates |
position 级估值和退出同步聚合记账 | reportPositionValue、部分退出、关闭仓位都会同步 target/total deployed accounting |
test_vaultCanCloseZeroValuePositionAfterOracleMarksLoss |
归零估值仓位可关闭 | oracle 上报 position value 为 0 后,keeper 可用零金额关闭仓位 |
test_vaultAccruesManagementFeeAsShares |
management fee 计提 | fee recipient 获得新增 shares;totalSupply 增加 |
test_vaultEmergencyRedeemIdleAllowsProRataIdleExitWhilePaused |
emergency idle redeem | emergency mode 下用户按 share 比例赎回 idle assets,shares 被 burn |
test_vaultRedeemCapacityIsLimitedByIdleAssets |
vault 资产已部署后限制赎回能力 | maxWithdraw/maxRedeem 受 idle assets 限制 |
test_vaultRolesAreEnforced |
vault 角色权限校验 | 非 keeper/pauser 不能执行管理动作 |
test_vaultPauseBlocksUserAndKeeperFlows |
vault pause 行为 | pause 后 deposit/withdraw/deploy 都失败,unpause 后恢复 |
test_settlementOracleRoleCanBeGrantedSeparately |
settlement oracle 角色可独立授予 | 被授权地址可以发布 settlement |
test_nonSettlementOracleCannotPublishSettlement |
非 settlement oracle 不能发布 settlement | 未授权地址调用 setSettlement revert |
test_marketRolesAreEnforcedIndependently |
market 的 term/mark/risk/treasury 角色独立校验 | 非授权地址调用管理函数全部 revert |
test_grantedMarketRolesCanPerformOnlyTheirOwnActions |
被授予的 market 角色只能执行对应动作 | term manager 可配 term,mark oracle 可更新 mark,不能越权 |
test_cannotWithdrawLockedCollateral |
用户不能提走被仓位锁住的 collateral | withdrawCollateral revert InsufficientAvailableCollateral |
test_longQuoteMovesHigherAfterLongOpenInterest |
long OI 上升后,继续开 long 的 fixed rate 变贵 | 第二次 long quote 大于第一次 long quote |
test_openLongYieldRevertsWhenRateMovesPastLimit |
stale quote 不能绕过滑点保护 | 第二个用户用旧 quote 开 long 时 revert RateSlippage |
test_getAccountHealthBecomesLiquidatable |
mark rate 不利变化后,账户健康度跌破维持保证金 | isLiquidatable == true |
test_liquidatePositionPartial |
可清算仓位可以被部分清算 | position notional 减半;liquidator 获得奖励;insurance fund 增加;剩余仓位未 settled |
test_cannotLiquidateHealthyPosition |
健康仓位不能被清算 | revert PositionNotLiquidatable |
文件:packages/contracts/test/LiYieldScenario.t.sol
| 场景编号 | 测试用例 | 场景说明 | 覆盖模块 |
|---|---|---|---|
| C-SC-001 | test_scenarioVaultDepositCollateralLongYieldAndSettlementWithdrawal |
用户存入底层资产拿到 liYIELD,抵押 liYIELD,开 long yield,到期 realized yield 高于 fixed rate,结算盈利并提走 collateral |
Vault、Market、SettlementIndex |
| C-SC-002 | test_scenarioShortYieldWinsWhenRealizedYieldFalls |
用户做空未来收益率,到期 realized yield 低于 fixed rate,short 仓位盈利 | Market、SettlementIndex |
| C-SC-003 | test_scenarioPositionCannotSettleBeforeSettlementIndexPublishes |
用户开仓后,SettlementIndex 尚未发布 realized yield,提前 settle 必须失败 | Market、SettlementIndex |
| C-SC-004 | test_scenarioLiquidationCannotCloseMoreThanConfiguredMaxPortion |
仓位已可清算,但 liquidator 试图一次性关闭超过最大比例,必须失败 | Market 风控 |
| C-SC-005 | test_scenarioAdverseMarkRateLiquidatesBeforeSettlementThenRemainingPositionSettles |
用户开 long 后 mark rate 不利变化,仓位被部分清算,剩余仓位之后到期结算 | Market 清算、SettlementIndex |
| C-SC-006 | test_scenarioLpProvidesLiquidityTraderPaysFeesAndLpWithdrawsProfit |
LP 存入 liYIELD 提供流动性,交易员开仓并支付开/平仓 fee,LP 在 open interest 归零后提取更多 assets |
LP 池、Market fee、开平仓流程 |
| C-SC-007 | test_scenarioCollateralOpenReduceCloseAndWithdrawAll |
用户抵押后开 long,oracle 更新 mark rate 后先减仓再平仓,最后提走全部可用 collateral | 抵押、开仓、减仓、平仓、提取 collateral |
| C-SC-008 | test_scenarioMaturedTermSettlesAfterOraclePublishesRealizedRate |
带 maturity 的 term 到期后,SettlementIndex 发布 realized rate,用户结算并提走 collateral | Term maturity、SettlementIndex、到期结算 |
| C-SC-009 | test_scenarioOracleUpdatesMoveHealthAndStaleOracleBlocksRiskActions |
oracle 更新 mark rate / collateral price 后账户进入可清算状态;oracle 过期时健康度、平仓、清算全部被阻止;刷新后恢复 | Oracle 更新、健康度、OracleDataStale、清算前置条件 |
| C-SC-010 | test_scenarioMultipleLpsShareTraderLossProRata |
两个 LP 按不同比例提供流动性,交易员亏损后 LP 池增厚,两个 LP 按 share 比例提取收益 | 多 LP、LP 份额、亏损回流 LP |
| C-SC-011 | test_scenarioQuoteCurveMovesWithLongShortSkewAcrossTraders |
第一位交易员开 long 后,fresh long / short 报价随 skew 改变;第二位交易员开对手 short 后,报价回到初始水平 | 多交易员、long-short skew、AMM 报价曲线 |
| C-SC-012 | test_scenarioLargeTraderProfitConsumesLpThenSettlementThenInsurance |
大额盈利仓位到期后,正 PnL 依次消耗 LP pool、settlement liquidity 和 insurance fund,验证三层偿付顺序 | 大仓位结算、LP 池、协议准备金、保险金 |
职责:
- 输入:两次 ETF snapshot 的权重差异
RebalanceDiff[]。 - 输出:keeper 可执行任务:
exit:上期有权重,本期权重为 0。enter:上期权重为 0,本期有权重。rebalance:两期都有权重,但权重发生变化。null:权重无变化,不需要任务。
当前限制:
- 只做任务分类,不直接执行交易。
- 执行状态机在
keeperExecution中处理。 - 持久化和执行锁在
keeperExecution中处理。 - 还没有自动重试 worker。
职责:
- 输入 treasury 可投资金额和当前 ETF target weights。
- 按
weightBps拆分资金。 - 对每个目标池调用 quote fetcher,生成可执行路由信息。
- 某个目标 quote 失败时,不让整个 plan 失败,而是在该 allocation item 上挂
error。
当前限制:
- 只生成计划,不执行 LI.FI/Earn 交易。
- 当前测试里 quote fetcher 是 mock,不打真实 LI.FI。
职责:
- 输入 allocation plan。
- 将每个 allocation item 转换成 keeper execution task。
- 支持
enter和exit两类执行任务。 - quote 缺失或金额为 0 的 allocation 自动标记为
skipped。 - 支持 dry-run,不调用 vault/LI.FI adapter。
- 非 dry-run 时按任务执行:
- 生成
externalPositionId。 - 调用 vault position registrar,生产环境对应
LiYieldVault.deployToTargetWithPosition。 - 调用 route executor,生产环境对应 LI.FI Composer / Earn 执行。
- 记录
positionId、vault tx hash、route execution id、route tx hash。 - exit task 会记录后续 reconciliation 所需的 position reductions。
- 单个任务失败标记为
failed,不吞掉错误原因。
- 生成
当前限制:
- 已实现基于
viem的 signer/RPC adapter,支持 vault 调用和 LI.FItransactionRequest提交。 - route status reconciliation 已实现,但默认仍依赖 LI.FI status API,不包含更复杂的 watcher / alerting。
- 当前持久化默认使用本地 SQLite,尚未接生产级数据库拓扑和高可用。
- 已有执行锁和手动 retry,但尚未实现自动 retry/backoff worker。
职责:
- 从上次 snapshot 和当前 snapshot 生成 exit / enter 调仓动作。
- 自动读取 vault idle assets,构建真正可执行的 enter allocation plan。
- 读取 keeper executor 在 LI.FI Earn 中的 portfolio positions,推导 exit route 的
fromToken和amount。 - 读取 vault active managed positions,推导 exit 完成后应执行的
recordPositionDivestment列表。 - 轮询 LI.FI status;当 exit route 完成后,再把退出金额记回 vault deployed accounting。
- 提供自动 scheduler,周期性执行 reconcile + rebalance。
当前限制:
- 当前 exit route 仍然依赖 target
toToken和 keeper executor portfolio 的匹配;更复杂的多 receipt-token/多 wallet 归属仍需额外抽象。 - scheduler 目前是单进程模式,分布式锁和多实例协调尚未接入。
职责:
- 输入 active managed positions 和 value source,生成 position valuation report。
- 按
targetId汇总 target value。 - 支持 position 级发布,生产环境对应
LiYieldVault.reportPositionValue。 - 支持 target 级发布,生产环境对应
LiYieldVault.reportTargetValue。 - 根据
startValue/endValue/netFlows计算 realized rate bps。 - 发布 settlement report,生产环境对应
SettlementIndex.setSettlement。
当前限制:
- 当前 value source 以 LI.FI Earn portfolio positions 为主,还没有做更强的多 source 校验、链下签名证明或 receipt-token 级核验。
- 已接入基于
viem的真实 oracle signer/RPC adapter,但默认测试仍使用 mock publisher。 - settlement realized rate 默认将负收益 clamp 到 0,因为当前
SettlementIndex使用uint256 realizedRateBps。 - 当前报告存储默认使用本地 SQLite,尚未接生产级数据库拓扑和高可用。
- 还没有 oracle quorum、签名报告、延迟生效和 dispute window。
职责:
- 读取当前 ETF index snapshot,得到最新 ETF
priceUsd。 - 读取当前 active trading term。
- 根据入选 vault 的加权
recentApy7d计算markRateBps。 - 当没有可用入选 vault 时,回退到 active term 的
baseRateBps。 - 支持把
markRateBps和collateralPriceUsd写到链上的YieldSwapMarket。 - 支持独立 scheduler,默认按 1 小时周期执行。
当前限制:
- 当前
markRateBps来源是策略选中池子的加权recentApy7d,还不是更复杂的 forward curve / term structure。 - 当前
collateralPriceUsd取自 ETF index snapshot,而不是独立价格预言机网络。 - 当前没有单独的 market oracle 历史存储,主要依赖链上事件和 server 日志。
文件:packages/server/src/keeper/rebalancePlanner.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
classifyRebalanceDiff maps diff rows to keeper task kinds |
验证 diff 到 keeper task kind 的分类逻辑 | 正确返回 exit、enter、rebalance 或 null |
buildKeeperRebalancePlan groups exits, entries, and weight changes |
验证从一组 diff 生成分组任务计划 | exits/entries/rebalances 分组正确 |
文件:packages/server/src/keeper/execution.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
buildKeeperExecutionTasks creates executable tasks and skips failed quotes |
allocation item 到 execution task 的转换 | quote 成功的任务为 pending,quote 失败的任务为 skipped |
executeKeeperAllocationPlan dry-run does not call execution adapters |
dry-run 行为 | 不调用 vault/route adapter,任务状态为 dry_run |
executeKeeperAllocationPlan registers vault position and executes route |
成功执行路径 | 调用 vault registrar 和 route executor,记录 position id 与 execution id |
executeKeeperAllocationPlan records failed adapter execution without stopping other task accounting |
失败执行路径 | adapter 抛错后任务为 failed,错误信息保留 |
retryFailedKeeperExecutionRun retries only failed tasks |
失败任务重试 | 只重试 failed task,成功后 attempts 增加 |
文件:packages/server/src/keeper/orchestrator.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
buildKeeperExitTasks builds exit routes and divestment reductions from snapshot diff |
基于 snapshot diff 构建 exit task | 生成 exit route,退出金额和 recordPositionDivestment 列表正确 |
runKeeperCycle builds enter allocation from vault idle assets and executes mocked tasks |
自动 keeper cycle | 自动读取 investable assets,生成 enter plan,并执行 enter run |
文件:packages/server/src/keeper/reconcile.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
reconcileKeeperExecutionRun finalizes completed exit routes |
exit route 完成后的对账 | LI.FI status 为 DONE 时,keeper 调用 exit finalizer 并把 reconciliation 标记为 completed |
reconcileKeeperExecutionRun marks failed LI.FI routes as failed |
route 失败的对账 | LI.FI status 为 FAILED 时,reconciliation 标记为 failed |
文件:packages/server/src/keeper/runStore.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
keeper run store appends, filters, and returns newest runs first |
keeper run 本地存储 | run 可追加、按 ETF 过滤、按时间倒序读取 |
keeper run store upserts by run id |
keeper run 状态更新 | 相同 runId 可被覆盖更新,用于 reconciliation 回写 |
文件:packages/server/src/oracle/reporter.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
buildValuationReport aggregates position values by target |
position value 汇总为 target value | 同一 target 的 position value 正确相加 |
collectValuationReport fetches only active positions from a value source |
从外部 value source 采集估值 | closed position 被跳过 |
publishPositionValuations and publishTargetValuations call the configured publisher |
oracle publisher 调用 | position/target publisher 收到正确 value |
computeRealizedRateBps subtracts net flows and clamps losses by default |
settlement realized rate 计算 | 净流入被扣除,负收益默认归 0 |
publishSettlementReport publishes realized rate through configured publisher |
settlement 发布 | publisher 收到计算后的 realized rate |
文件:packages/server/src/oracle/reportStore.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
oracle report store persists valuation reports with hashes |
oracle valuation 本地存储 | report hash 正确生成并持久化 |
oracle report store filters settlement reports by term |
oracle settlement 本地存储 | 可按 termId 查询历史报告 |
文件:packages/server/src/oracle/service.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
buildLifiPortfolioValuationReport maps LI.FI portfolio values onto managed positions |
LI.FI portfolio 到 vault positions 的估值映射 | 按 target token + chain 匹配,并按 position basis 比例分配 target value |
runOracleSettlementCycle derives settlement from stored valuation reports |
从历史 valuation report 推导 settlement | 读取起止 report,总和 target NAV,计算 realized rate 并写入 settlement store |
文件:packages/server/src/scenarios/weeklyRebalanceScenario.test.ts
| 场景编号 | 测试用例 | 场景说明 | 覆盖模块 |
|---|---|---|---|
| K-SC-001 | scenario: weekly strategy rotation creates one exit and one entry keeper task |
上周 Top 3 是 ethena/morpho/renzo,本周 Top 3 是 morpho/renzo/etherfi;keeper 应生成一个 exit、一个 enter、一个 rebalance |
diffSnapshots、rebalancePlanner |
| K-SC-002 | scenario: first keeper rebalance treats every selected target as an entry |
系统首次运行,没有 previous snapshot;当前所有 selected targets 都应被视为 entry | diffSnapshots、rebalancePlanner |
| K-SC-003 | scenario: unchanged weekly top 3 creates no keeper tasks |
上周和本周成分、权重都没变;keeper 不应生成任何任务 | diffSnapshots、rebalancePlanner |
| K-SC-004 | scenario: keeper allocation plan quotes every selected target and preserves per-target amounts |
treasury 有 1000 单位资金,三个目标权重约等于 1/3;allocation plan 应拆成 333/333/334 并逐个 quote |
allocationPlan、mock quote fetcher |
| K-SC-005 | scenario: keeper execution creates positions, oracle reports values, settlement rate is published |
keeper 执行 allocation 创建两个 position;oracle 采集 position value 并发布;settlement oracle 计算并发布 realized yield | keeperExecution、oracleReporter、mock publisher |
文件:packages/server/src/scenarios/keeperOracleOnchainScenario.test.ts
| 场景编号 | 测试用例 | 场景说明 | 覆盖模块 |
|---|---|---|---|
| K-SC-006 | scenario: contracts, keeper, and oracle link together through on-chain adapters |
本地启动 Anvil,部署真实合约;keeper 通过 LiYieldVault.deployToTargetWithPosition 注册并部署仓位;oracle 基于 LI.FI portfolio mock 生成 valuation report 并调用链上 publisher 更新 position value;最后根据两份 valuation report 生成 settlement 并写入 SettlementIndex |
Deploy.s.sol、keeper/onchain.ts、keeper/execution.ts、oracle/service.ts、oracle/onchain.ts、LiYieldVault、SettlementIndex |
| K-SC-007 | scenario: oracle loss path marks position down and settlement clamps negative yield to zero |
本地启动 Anvil,keeper 建仓后 oracle 将 position value 从 1000 下调到 920;结算层验证负收益会被 clamp 为 0,并写入链上 SettlementIndex |
keeper/onchain.ts、oracle/service.ts、oracle/onchain.ts、LiYieldVault、SettlementIndex |
| K-SC-008 | scenario: target-level oracle publish updates aggregate target value while position accounting stays unchanged |
keeper 建仓后 oracle 使用 target 级发布模式,将 target deployed value 从 1000 更新到 1040;position 级 reported value 保持原值,用于验证 target publish 路径 | oracle/service.ts、oracle/onchain.ts、LiYieldVault |
| K-SC-009 | scenario: multi-position oracle valuation distributes target value proportionally across on-chain positions |
keeper 在同一 target 下创建两笔 on-chain positions;oracle 根据单个 target portfolio value 按 principal 比例分配到两笔 position,并逐笔上链发布 | keeper/execution.ts、oracle/service.ts、oracle/reporter.ts、oracle/onchain.ts、LiYieldVault |
职责:
- 提供 HTTP API。
- 把 API 路由和 server listener 分离,便于测试。
- 当前对外接口包括:
/health/api/etfs/api/index/api/earn/targets/api/earn/vaults/api/earn/portfolio/:address/api/earn/snapshots/api/etfs/:etfId/price/api/etfs/:etfId/price/history/api/keeper/allocation-plan/api/keeper/execute-allocation-plan/api/keeper/runs/api/keeper/retry-run/api/oracle/valuation-report/api/oracle/auto-valuation/api/oracle/settlement-report/api/oracle/settlement-from-valuations/api/oracle/valuation-reports/api/oracle/settlement-reports/api/keeper/rebalance/api/admin/refresh/api/admin/earn-snapshot/api/admin/snapshot
当前限制:
- admin auth 只是本地可用的 bearer token 保护;未设置 token 时默认开放。
- API 测试覆盖不依赖外网的基础接口、keeper dry-run / rebalance / reconcile 接口、oracle valuation / auto-valuation / settlement 接口,以及 Earn snapshot 手动落盘接口。
职责:
- 按过去 7 天 APY 排序。
- 在
strategy模式下选 Top N。 - 在
equal模式下返回所有候选 targets。 - 计算等权
weightBps。 - 生成 strategy summary 和 next rebalance 时间。
职责:
- 读取 ETF 配置和 strategy 配置。
- 解析候选 target。
- 选择目标 target 并生成 ETF snapshot。
- 拉取 LI.FI token metadata,补充 symbol、price 等信息。
当前限制:
- Earn targets 的候选范围仍由本地配置控制。
- APY、TVL、LP token price 和 Composer capability 会优先从 LI.FI Earn Data API 拉取,失败时 fallback 到静态配置。
- 真实 pool discovery endpoint 已封装为
/api/earn/vaults,但还没有自动把所有 LI.FI vault 纳入策略候选集。 priceUsd仍不是链上 vault NAV 驱动。
职责:
- 调用 LI.FI Earn Data API 的 vault detail / vault list / portfolio positions。
- 将 Earn 返回结构归一化为服务内部字段:
apyTotal、apy7d、tvlUsd、lpTokenPriceUsd、isTransactional、isRedeemable。 - 将小数 APY 转换为百分比 APY,例如
0.0825 -> 8.25。 - 为
indexEngine提供 target runtime data,驱动 strategy 排名和 dashboard 展示。 - API 失败时返回空数据或静态配置 fallback,保证本地 demo 不因为外部网络失败而不可用。
当前限制:
- 当前没有把 LI.FI 全量 vault discovery 自动写回本地 strategy candidate list。
- portfolio endpoint 已封装,并已接入 oracle 自动估值流程;更强的链上校验和多 source 验证仍待补。
- 真实 LI.FI Earn API 的外部集成测试不进默认 CI,默认测试使用 mock fetch。
职责:
- 基于
indexEngine采集每个 ETF 的候选 Earn targets。 - 记录每个 target 的 APY、7d APY、TVL、LP token price、是否可交易、是否被策略选中、weight、rank 和数据来源。
- 定时将 Earn 数据快照 append 到本地 SQLite store。
- 支持按
etfId、targetId和limit查询历史快照。 - 支持每个 ETF 保留最近 N 条快照,避免本地文件无限增长。
当前限制:
- 当前 store 是本地 SQLite,不是生产级数据库集群。
- 定时任务采集的是本地 strategy candidate list,不是 LI.FI 全量 vault discovery。
- 没有补历史 backfill、压缩归档、去重或数据质量告警。
职责:
refresh定时生成 snapshot 并追加 price history。price读取实时 price 或历史 price。snapshotFile读写 SQLite 中的 latest snapshot 和 price history。
当前限制:
- price 当前仍然是
navUsd / totalShares。 - 文件系统测试还没有隔离临时目录。
文件:packages/server/src/app.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
GET /health returns service status without starting a listener |
不启动实际端口,直接测试 Hono app | 返回 ok: true 和 service: liyield-server |
GET /api/etfs returns configured ETFs without external network calls |
测试 ETF 注册表接口,不依赖外部网络 | 返回 liyield-core 和 weekly-top3-apy7d |
POST /api/keeper/execute-allocation-plan returns dry-run execution plan |
keeper dry-run API | 返回 execution summary,任务进入 dry_run |
POST /api/oracle/valuation-report aggregates and publishes position values |
oracle valuation API | position value 汇总为 target value,并调用 mock publisher |
POST /api/oracle/auto-valuation runs the LI.FI-backed oracle valuation cycle |
oracle 自动估值 API | 调用 LI.FI portfolio-backed valuation runner,并返回 owner / report / published / stored |
POST /api/oracle/settlement-report computes and publishes realized yield |
oracle settlement API | 计算 realized rate 并调用 mock publisher |
POST /api/oracle/settlement-from-valuations derives settlement from stored valuation history |
oracle 自动结算 API | 根据 valuation report id 生成 settlement,并返回 publish 结果 |
POST /api/admin/earn-snapshot persists Earn data snapshots and exposes history |
Earn 数据手动落盘 API | 手动采集后写入 store,GET /api/earn/snapshots 可读出历史 |
POST /api/keeper/execute-rebalance persists exit and enter runs from the keeper cycle |
自动 keeper cycle API | keeper cycle 返回的 exit / enter run 会被持久化 |
POST /api/keeper/reconcile-runs returns updated keeper runs |
keeper reconciliation API | 返回已完成对账的 updated runs |
文件:packages/server/src/services/strategyEngine.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
rankTargetsByApy7d sorts targets by trailing 7d yield descending |
APY 排序正确 | 返回顺序为高 APY 到低 APY |
selectTargets selects top N for strategy mode and all ranked targets for equal mode |
策略模式和 equal 模式行为不同 | strategy 只取 Top N;equal 返回所有候选 |
computeEqualWeights always sums to 10000 bps |
等权权重精度 | 任意 count 权重总和为 10000 |
buildStrategySummary includes cadence, selection metadata, and next rebalance |
strategy summary 正确 | selected/candidate 数量和 next rebalance 时间正确 |
文件:packages/server/src/services/allocationPlan.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
splitWeightedAmounts allocates rounding remainder to the last target |
整数拆分资金时处理尾差 | 100 按 3334/3333/3333 拆成 33/33/34 |
buildAllocationPlan splits treasury amount and calls quote fetcher per target |
allocation plan 正确调用 quote fetcher | 每个 target 有一个 quote call;金额按权重拆分 |
buildAllocationPlan keeps failed LI.FI quote as item-level error |
单个 quote 失败不影响整体计划 | 失败 target 的 quote: null,并保留 error message |
文件:packages/server/src/services/earn.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
fetchEarnVault normalizes LI.FI Earn vault analytics into percent APY and USD TVL |
vault detail 数据归一化 | APY 小数转百分比,TVL、protocol、capability 正确 |
fetchEarnVaults sends filtering query params and normalizes list responses |
vault list 查询参数和返回解析 | chainId/asset/protocol/sortBy/limit 参数正确,列表归一化 |
fetchEarnPortfolioPositions normalizes portfolio positions |
portfolio positions 解析 | position 的链、协议、资产、余额字段正确 |
mergeEarnVaultData falls back to static target data when Earn data is unavailable |
Earn API fallback | API 无数据时继续使用静态 APY 和 composerReady |
文件:packages/server/src/services/earnSnapshot.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
buildEarnDataSnapshot records selected targets and all candidate Earn vault rows |
单次 Earn 数据采集 | snapshot 记录 selected target ids,并包含全部候选 vault rows |
recordEarnDataSnapshot persists the collected Earn snapshot |
采集并落盘 | store 中能读到刚采集的 Earn snapshot |
文件:packages/server/src/services/earnSnapshotStore.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
earn snapshot store appends, filters, and trims per ETF |
Earn snapshot 本地持久化 | 可 append、按 ETF / target 过滤、按时间倒序读取,并按每个 ETF 上限裁剪 |
文件:packages/server/src/services/indexEngine.test.ts
| 测试用例 | 测试目标 | 关键断言 |
|---|---|---|
buildIndexSnapshot ranks targets using LI.FI Earn APY data when available |
index snapshot 使用 LI.FI Earn APY 驱动策略排名 | mock Earn APY 最高的三个 vault 被选为 Top 3,且 target 标记为 earnDataSource: lifi-earn |
下面是建议长期维护的场景测试库。状态说明:
implemented:已经有自动化测试。next:下一批最值得实现。planned:后续生产化阶段应补。
| ID | 状态 | 场景 | 期望结果 |
|---|---|---|---|
| ETF-SC-001 | implemented | 首次启动,没有 previous snapshot | 当前 Top 3 全部生成 enter 任务 |
| ETF-SC-002 | implemented | 周调仓后一个旧池掉出,一个新池进入 | 生成一个 exit、一个 enter,保留池如果权重变动则生成 rebalance |
| ETF-SC-003 | implemented | 周调仓后 Top 3 完全不变,权重也不变 | keeper 不生成任务 |
| ETF-SC-004 | implemented | allocation plan 对三个目标等权拆分 1000 | 金额为 333/333/334,每个目标都有 quote |
| ETF-SC-005 | implemented-server | LI.FI Earn vault detail 返回 APY/TVL/capability | 数据归一化后可用于 snapshot 和 strategy ranking |
| ETF-SC-006 | implemented-server | LI.FI Earn vault list 按 chain/asset/protocol/sortBy/limit 查询 | 查询参数正确,返回 vault 列表被归一化 |
| ETF-SC-007 | implemented-server | LI.FI Earn portfolio positions 返回用户仓位 | position balance 和资产字段被归一化 |
| ETF-SC-008 | implemented-server | LI.FI Earn 数据不可用 | target 使用静态 APY fallback,demo 仍可运行 |
| ETF-SC-008A | implemented-server | 单次采集 Earn 数据快照 | 记录 selected target ids,并包含全部候选 vault rows |
| ETF-SC-008B | implemented-server | Earn snapshot 持久化后查询历史 | 可按 ETF / target 过滤,按时间倒序返回,并按上限裁剪 |
| ETF-SC-009 | next | Top 3 完全轮换,例如 A/B/C 变成 D/E/F |
生成 3 个 exit 和 3 个 enter |
| ETF-SC-010 | next | 某个目标 quote 失败,其余目标 quote 成功 | plan 返回成功,失败项带 error,keeper 可选择跳过或重试 |
| ETF-SC-011 | next | 可投资金额为 0 | 不应生成有效 quote 任务,或明确返回 validation error |
| ETF-SC-012 | next | 候选池数量少于 Top N | 策略只选择实际存在的池,权重仍总和 10000 |
| ETF-SC-013 | planned | 策略切换,例如 Top 3 APY 切换为风险加权策略 | snapshot 中 strategy id 和 target 选择符合新策略 |
| ETF-SC-014 | implemented-server | keeper 执行过程中某条 LI.FI 交易失败 | 任务进入 failed,错误原因被记录 |
| ETF-SC-015 | implemented-server | keeper 重试成功 | 任务从 failed/retrying 变为 succeeded,并记录最终 tx hash |
| ETF-SC-016 | implemented-server | rebalance 正在执行时又触发新的 rebalance | 应有执行锁,避免重复部署资金 |
| ETF-SC-017 | implemented-server | 周调仓产生 exit task | keeper 根据 portfolio balance 和 managed positions 生成 exit route 与 divestment reductions |
| ETF-SC-018 | implemented-server | exit route 完成后做 reconciliation | LI.FI status 为 DONE 后,vault 执行 recordPositionDivestment |
| ID | 状态 | 场景 | 期望结果 |
|---|---|---|---|
| NAV-SC-001 | implemented-contract | vault 有 idle cash,没有 deployed positions | totalAssets 等于 idle assets |
| NAV-SC-002 | implemented-contract | 一个 deployed target 上报收益 | totalAssets 随 reportTargetValue 上升 |
| NAV-SC-003 | implemented-contract | 某个 managed position 上报收益、部分退出、最终关闭 | position、target、total deployed accounting 同步变化 |
| NAV-SC-004 | implemented-contract | 某个 managed position 被 oracle 标记为 0 | keeper 可关闭归零仓位,deployed accounting 归零 |
| NAV-SC-005 | implemented-contract | management fee 经过时间后计提 | fee recipient shares 增加,totalSupply 增加 |
| NAV-SC-006 | implemented-contract | emergency mode 下用户赎回 idle assets | 用户按 share 比例拿回 idle assets,常规存取保持暂停 |
| NAV-SC-007 | implemented-server | oracle 采集多个 position value 并汇总 target value | valuation report 输出 position 明细和 target 汇总 |
| NAV-SC-008 | planned | keeper 调仓产生执行滑点 | NAV 扣除滑点损耗 |
| NAV-SC-009 | planned | price refresh 多次执行 | history 按时间追加,limit 参数只返回最近 N 条 |
| NAV-SC-010 | planned | totalShares 为 0 | price 返回 0 或明确错误,不能除零 |
| NAV-SC-011 | next | 某个 Earn position 亏损或可赎回价值下降 | NAV 下降,price history 反映价格下跌 |
| ID | 状态 | 场景 | 期望结果 |
|---|---|---|---|
| YS-SC-001 | implemented | 用户 deposit vault -> 抵押 liYIELD -> long yield -> 到期盈利 -> withdraw |
用户最终 liYIELD 增加,locked collateral 归零 |
| YS-SC-002 | implemented | short yield,realized yield 低于 fixed rate | short 用户盈利 |
| YS-SC-003 | implemented | settlement 尚未发布就尝试结算 | revert SettlementNotAvailable |
| YS-SC-004 | implemented | 不利 mark rate 触发部分清算,剩余仓位再结算 | position notional 减少;liquidator 收奖励;剩余仓位可 settle |
| YS-SC-005 | implemented | 清算关闭比例超过最大允许比例 | revert InvalidLiquidationSize |
| YS-SC-006 | implemented | long OI 增加后继续开 long | fixed rate 抬高 |
| YS-SC-007 | implemented | 使用 stale quote 开仓 | revert RateSlippage |
| YS-SC-008 | implemented | 健康仓位被清算 | revert PositionNotLiquidatable |
| YS-SC-009 | next | 同一用户同一 term 下有多笔 long/short 仓位 | health 计算聚合全部未结算仓位 |
| YS-SC-010 | next | settlement liquidity 不足但 insurance 足够 | 用户盈利优先从 settlement liquidity 支付,不足部分由 insurance 支付 |
| YS-SC-011 | next | settlement liquidity 和 insurance 都不足 | settle 应 revert InsufficientSettlementLiquidity |
| YS-SC-012 | next | 仓位被完全清算 | position settled == true,OI 完全扣减 |
| YS-SC-013 | planned | mark rate 未发布就读取 health | revert MarkRateNotAvailable |
| YS-SC-014 | planned | term 未配置就 quote/open | revert TermNotConfigured |
| YS-SC-015 | planned | RISK_MANAGER_ROLE 更新 risk parameters |
新参数影响后续清算判断 |
| YS-SC-016 | planned | 多用户同时开 long 和 short | AMM quote 围绕 open interest skew 动态变化 |
| ID | 状态 | 场景 | 期望结果 |
|---|---|---|---|
| API-SC-001 | implemented | 不启动 listener,直接请求 /health |
返回服务状态 |
| API-SC-002 | implemented | 请求 /api/etfs |
返回已配置 ETF |
| API-SC-003 | implemented | 请求 /api/keeper/execute-allocation-plan dry-run |
返回 allocation plan 和 execution dry-run summary |
| API-SC-004 | implemented | 请求 /api/oracle/valuation-report |
返回 position 明细和 target 汇总 |
| API-SC-005 | implemented | 请求 /api/oracle/settlement-report |
返回 realized rate,并可通过 publisher 发布 |
| API-SC-005A | implemented | 请求 /api/oracle/auto-valuation |
返回基于 LI.FI portfolio 的 valuation report,并可选发布 |
| API-SC-005B | implemented | 请求 /api/admin/earn-snapshot 后再请求 /api/earn/snapshots |
Earn 数据快照被持久化并可读取 |
| API-SC-005C | implemented | 请求 /api/keeper/execute-rebalance |
返回 keeper cycle,并持久化 exit / enter runs |
| API-SC-005D | implemented | 请求 /api/keeper/reconcile-runs |
返回对账后更新的 keeper runs |
| API-SC-005E | implemented | 请求 /api/oracle/settlement-from-valuations |
基于历史 valuation report 生成 settlement 并返回 publish 结果 |
| API-SC-006 | next | 请求 /api/index?mode=strategy |
返回 Top 3 strategy snapshot |
| API-SC-007 | next | 请求 /api/index?mode=equal |
返回所有候选 targets 且等权 |
| API-SC-008 | next | 请求不存在的 ETF id | 返回明确错误 |
| API-SC-009 | next | admin token 设置后,未带 token 请求 keeper/admin 接口 | 返回 401 |
| API-SC-010 | next | admin token 设置后,带正确 token 请求 keeper/admin 接口 | 返回 200 |
| API-SC-011 | planned | /api/admin/refresh 写入 snapshot 和 price history |
文件中出现对应记录 |
| API-SC-012 | planned | /api/etfs/:id/price/history?limit=3 |
只返回最近 3 条 |
| Area | Test Count | 状态 |
|---|---|---|
| Contracts unit / behavior | 25 | Passing |
| Contracts scenarios | 5 | Passing |
| Keeper unit | 13 | Passing |
| Keeper / oracle scenarios | 9 | Passing |
| Oracle services | 9 | Passing |
| Server API | 10 | Passing |
| Server services | 15 | Passing |
| Total backend tests | 86 | Passing |
- LI.FI Earn Data API 已接入数据层并有 mock 测试;Earn snapshot 已支持定时落盘到本地 SQLite;真实外部 API 集成测试未进入默认 CI。
- Keeper 已实现基于
viem的 signer/RPC adapter、vault position 注册、LI.FI route 提交、exit reconciliation、手动 retry、自动 scheduler 和执行锁,但生产数据库、分布式锁、自动重试 worker 仍未接入。 - Earn snapshot store 当前默认使用本地 SQLite,还没有接生产数据库、去重、backfill、归档和数据质量告警。
- 合约层已有 vault deployed accounting 测试,但 server price 还没有接链上 NAV,目前仍基于配置里的
navUsd / totalShares。 - Oracle 已有 LI.FI portfolio-backed valuation source、基于
viem的链上 publisher、valuation/settlement report、report hash、本地 SQLite 报告存储和自动估值测试,但还没有签名报告、quorum、dispute window 和生产数据库。 - Vault 已有 position accounting、management fee、emergency idle redeem 测试,但还没有真实 LI.FI execution id、receipt token 和跨链 unwind 集成测试。
- Price history 文件系统测试尚未隔离临时目录。
- Yield Swap 还没有 fuzz / invariant 测试。
- SettlementIndex 还没有 oracle 争议、延迟窗口、多签发布等生产级测试。
- 前端 UI 测试不在当前范围内。