feat: vault + lending market covenant system (Morpho-style)#26
feat: vault + lending market covenant system (Morpho-style)#26
Conversation
Three vault contracts: - vault_covenant.ark: ERC-4626-style recursive vault with PPS monotonicity - strategy_fragment.ark: independent per-strategy VTXO with keeper attestation - composite_router.ark: atomic weighted yield settlement (vault + 2 fragments) Five lending contracts: - debt_note.ark: transferable borrower debt receipt (DebtToken VTXO) - credit_note.ark: transferable lender credit receipt (CreditToken VTXO) - lending_market.ark: full lending market with borrowExogenous/borrowSynthetic, repay (partial/full with if/else), keeper-only liquidate (5% fee waterfall), transferCredit for secondary market settlement. Synthetic path mints both DebtToken and CreditToken as fresh asset group issuances gated by control assets. - supply_flow.ark: one-shot VaultCovenant → LendingMarket atomic transfer - repay_flow.ark: one-shot LendingMarket → VaultCovenant yield accretion All contracts verified with `cargo run -- <file>` and `cargo test`. Integer division floors noted throughout; systematic LLTV bias analysis documented. https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
migrate() placed no constraint on tx.outputs[0].scriptPubKey, making the covenant custodial to ownerPk — any holder could redirect the VTXO to an arbitrary script and break the covenant. Removed the function and the now-dead upgradeRoot constructor parameter from VaultCovenant and all referencing contracts (composite_router, supply_flow, repay_flow). https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
…ubfolder - Move all 8 .ark files into examples/vault_lending/ - Update generate_contracts.sh to scan one level of subdirs, exporting as <folder>_<name> (e.g. vault_lending_vault_covenant) - Add vault_lending project to playground/main.js so the stack appears as a grouped folder in the playground explorer sidebar https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 8 minutes and 42 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughThis PR introduces a complete vault lending system comprising six new Ark smart contracts implementing a DeFi infrastructure: Changes
Possibly Related PRs
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Remove borrowSynthetic, isSynthetic flag, and the five token-related constructor params (debtTokenId, creditTokenId, debtTokenCtrlId, creditTokenCtrlId). Remove if/isSynthetic blocks from repay and liquidate. Simplify transferCredit to a pure credit-holder update with no CreditNote output. Remove debt_note.ark and credit_note.ark from the vault_lending folder — synthetic token mechanics will live in a separate lending market contract. lending_market.ark: 15 → 10 constructor params, 5 → 4 functions. supply_flow.ark: 17 → 12 constructor params. https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (2)
playground/generate_contracts.sh (1)
20-39: Make the generator actually recursive.The banner/header says
examples/**/*.ark, but Lines 27-37 only descend one level. A futureexamples/foo/bar/baz.arkwill be silently omitted fromcontracts.js.♻️ Suggested recursive walk
const entries = []; - -// Root-level .ark files -for (const f of fs.readdirSync(dir).sort()) { - if (f.endsWith('.ark')) { - entries.push({ name: f.replace('.ark', ''), file: path.join(dir, f) }); - } -} - -// One level of subdirectories — each subdir becomes a namespace prefix -for (const d of fs.readdirSync(dir).sort()) { - const subdir = path.join(dir, d); - if (fs.statSync(subdir).isDirectory()) { - for (const f of fs.readdirSync(subdir).sort()) { - if (f.endsWith('.ark')) { - entries.push({ name: d + '_' + f.replace('.ark', ''), file: path.join(subdir, f) }); - } - } - } -} + +function collectArkFiles(currentDir, prefix = []) { + const dirents = fs.readdirSync(currentDir, { withFileTypes: true }) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const dirent of dirents) { + if (dirent.isDirectory()) { + collectArkFiles(path.join(currentDir, dirent.name), [...prefix, dirent.name]); + continue; + } + + if (dirent.isFile() && dirent.name.endsWith('.ark')) { + const base = dirent.name.replace(/\.ark$/, ''); + entries.push({ + name: [...prefix, base].join('_'), + file: path.join(currentDir, dirent.name), + }); + } + } +} + +collectArkFiles(dir);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@playground/generate_contracts.sh` around lines 20 - 39, The current generator only scans the root and one-level subdirectories (loops over dir and subdir) so deeper .ark files (e.g., examples/foo/bar/baz.ark) are missed; replace the two-loop logic with a recursive directory walk (e.g., implement a walkDir(dirPath, prefix) that uses fs.readdirSync + fs.statSync and calls itself for directories) that pushes entries with the proper namespace prefix (use prefix + '_' + baseName for nested folders) into the existing entries array; update usage of variables dir, subdir, d, f, and name construction to use the prefix parameter so all nested .ark files are discovered while preserving the existing header/out generation.examples/vault_lending/lending_market.ark (1)
74-90: Consider verifying output 1 value equalsborrowAmount.Output 0's value is verified to equal
collateral, but output 1 (borrowed funds to borrower) has no value check. While the funds flow may be constrained by the transaction construction, explicitly verifyingtx.outputs[1].value == borrowAmountwould strengthen the contract's guarantees.Proposed fix
// Output 1: borrowed funds flow to borrower + require(tx.outputs[1].value == borrowAmount, "borrowed amount mismatch"); require( tx.outputs[1].scriptPubKey == new SingleSig(borrowerPk), "borrower output mismatch" );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/vault_lending/lending_market.ark` around lines 74 - 90, Add an explicit value check for the borrower output: inside the same validation block that asserts tx.outputs[1].scriptPubKey == new SingleSig(borrowerPk), also require that tx.outputs[1].value == borrowAmount to ensure the borrowed funds amount is enforced; update the LendingMarket successor validation area (the block that checks tx.outputs[0] and tx.outputs[1]) to include this additional require referencing tx.outputs[1], borrowAmount and SingleSig(borrowerPk).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/vault_lending/composite_router.ark`:
- Around line 30-44: The keeper signatures are replayable because route() only
checks that keeperSig0/1 sign the caller-supplied reportHash0/1 without proving
those hashes commit to strategyId0/1, reported0/1 or
currentTotalAssets/currentTotalShares; fix by recomputing and verifying the
commitment that was actually signed: compute a canonical hash (include
strategyId0/strategyId1, reported0/reported1, currentTotalAssets,
currentTotalShares, and a domain/tag) and require reportHash0 == computedHash0
and reportHash1 == computedHash1 before calling checkSigFromStack; use the same
canonical format for both keepers to prevent reuse across different inputs.
- Around line 25-26: Enforce the invariant that weight0 + weight1 == 10000
whenever the router's weights are set or before they are used to compute
newTotalAssets: add an on-chain check (assert/require/abort) that validates
weight0 + weight1 == 10000 in the function or constructor that receives/stores
weight0 and weight1 (and any setter), and also add the same check right before
any logic that uses weight0/weight1 (e.g., where newTotalAssets is computed) so
misconfigured routers cannot be deployed or invoked; apply this to the two
parameter sites shown (the weight0/weight1 declarations around lines 25-26 and
the other occurrence around 43-44).
In `@examples/vault_lending/credit_note.ark`:
- Around line 25-33: The transfer entrypoint currently lets a holder reassign a
CreditNote without updating the authoritative creditHolder in LendingMarket;
replace this by forbidding standalone holder changes and require transfers to be
performed via LendingMarket.transferCredit(): in the transfer function (and/or
remove it) ensure that any output with a different holder must be produced as
the result of a LendingMarket.transferCredit() call by checking the transaction
includes an input/output pair that matches the LendingMarket transfer action
(i.e., verify the tx includes the LendingMarket transfer invocation tied to
marketRef and that the new output CreditNote's holder equals the holder set by
that LendingMarket action), and otherwise reject the transfer so
repayment/liquidation continue to rely on the single authoritative creditHolder
in LendingMarket.
In `@examples/vault_lending/lending_market.ark`:
- Around line 59-72: The oracle signature verifies priceHash but the contract
never ties priceHash to the numeric price, allowing signature replay with
arbitrary price values; compute the canonical hash of the provided price inside
the function (e.g. derive expectedPriceHash from price using the same hashing
scheme used off-chain) and then either require(expectedPriceHash == priceHash)
before calling checkSigFromStack(oracleSig, oraclePk, priceHash) or call
checkSigFromStack(oracleSig, oraclePk, expectedPriceHash) directly so the
oracleSig is validated against the actual price value; update the logic around
priceHash/price and the checkSigFromStack call to enforce this binding (refer to
symbols priceHash, price, oracleSig, oraclePk, and checkSigFromStack).
- Around line 178-182: The repayment output check only verifies
tx.outputs[1].scriptPubKey but not the amount; update the contract to also
validate tx.outputs[1].value equals the expected repayAmount (or use a >=
repayAmount if partial overpayment allowed) by adding a require that combines or
follows the existing SingleSig(creditHolder) check so the credit holder cannot
be sent less than repayAmount; reference tx.outputs[1].value, repayAmount, and
the existing require that checks tx.outputs[1].scriptPubKey == new
SingleSig(creditHolder) when making this change.
- Around line 127-135: The successor construction for LendingMarket in the
borrowSynthetic flow incorrectly reuses the current isSynthetic value; update
the successor to force the synthetic flag to 1 so the new market is always
synthetic. Locate the successor creation that calls new LendingMarket(...) in
the borrowSynthetic handler and replace the isSynthetic argument with the
literal 1 (or true if the code uses booleans) so the successor's isSynthetic
field is explicitly set to synthetic. Ensure you only change the isSynthetic
argument and leave all other LendingMarket constructor parameters (borrowerPk,
oraclePk, vaultKeeperPk, creditHolder, collateral, borrowAmount, lltv,
collateralAssetId, loanAssetId, oracleHash, debtTokenId, creditTokenId,
debtTokenCtrlId, creditTokenCtrlId) unchanged.
- Around line 191-202: The partial-repay branch currently only checks successor
script equality (LendingMarket(...)) but does not ensure the collateral UTXO's
value is preserved; add a require that tx.outputs[0].value == collateralAmount
alongside the existing require in the else branch so the successor output
created by LendingMarket(borrowerPk, oraclePk, vaultKeeperPk, creditHolder,
collateralAmount, newDebtAmount, lltv, isSynthetic, collateralAssetId,
loanAssetId, oracleHash, debtTokenId, creditTokenId, debtTokenCtrlId,
creditTokenCtrlId) must also carry the unchanged collateralAmount to prevent
collateral draining.
- Around line 236-254: The liquidation waterfall can produce a negative residual
when collateralAmount < fee + debtAmount; add an explicit guard in the
liquidation block (before checking outputs) that validates collateralAmount >=
fee + debtAmount (or equivalently that residual >= 0) and fail with a clear
message so tx.outputs[2].value >= residual cannot be bypassed; update the block
around the fee/residual calculation (symbols: collateralAmount, fee, debtAmount,
residual, tx.outputs) to enforce this precondition and prevent negative residual
edge cases.
In `@examples/vault_lending/repay_flow.ark`:
- Around line 31-42: The vault's accounting must tie returnAmount to the actual
funds arriving in the vault: replace the current unconditional value check with
a requirement that the net increase in the vault output equals returnAmount
(i.e. assert tx.outputs[0].value - tx.input.current.value == returnAmount or
equivalently that the asset-group delta flowing into the VaultCovenant equals
returnAmount), keeping the existing newVaultAssets calculation and VaultCovenant
equality (newVaultAssets, totalAssets, returnAmount, tx.outputs[0].value,
tx.input.current.value are the symbols to adjust).
In `@examples/vault_lending/strategy_fragment.ark`:
- Around line 28-36: The keeper signature must be bound to the actual reported
state: compute a canonical report digest inside report (e.g., hashReport =
keccak(strategyId, reportedAmount) with any domain/version prefix) and verify
the signature against that digest instead of an arbitrary caller-supplied
reportHash; then require that the provided reportHash (if kept) equals the
computed digest or simply remove the reportHash parameter and call
checkSigFromStack(keeperSig, keeperPk, hashReport). Update the checkSigFromStack
call in function report and ensure the subsequent successor check still uses the
local reportedAmount and strategyId so the keeper signature authorizes exactly
those values.
In `@examples/vault_lending/supply_flow.ark`:
- Around line 42-68: Ensure supplyAmount is validated and the exact amount
leaves the vault by adding two checks: require supplyAmount >= 0 && supplyAmount
<= totalAssets (prevent negative/overflow and over-withdrawal from
VaultCovenant), and require the outputs reflect the transfer of that exact
amount by asserting tx.outputs[0].value == tx.input.current.value - supplyAmount
(vault lost exactly supplyAmount) and tx.outputs[1].value == supplyAmount
(market received exactly supplyAmount). Reference symbols: supplyAmount,
totalAssets, VaultCovenant / tx.outputs[0].value, tx.outputs[1].value, and
tx.input.current.value.
In `@playground/main.js`:
- Around line 18-29: The vault_lending fixture references eight missing exports
(contracts.vault_lending_vault_covenant,
contracts.vault_lending_strategy_fragment,
contracts.vault_lending_composite_router, contracts.vault_lending_debt_note,
contracts.vault_lending_credit_note, contracts.vault_lending_lending_market,
contracts.vault_lending_supply_flow, contracts.vault_lending_repay_flow) but
playground/contracts.js is absent, causing undefined values at runtime;
regenerate the contract artifacts by running ./playground/generate_contracts.sh
and then ./playground/build.sh to recreate playground/contracts.js, verify that
it exports the above symbols, and commit the generated playground/contracts.js
so vault_lending can import the concrete contract strings.
---
Nitpick comments:
In `@examples/vault_lending/lending_market.ark`:
- Around line 74-90: Add an explicit value check for the borrower output: inside
the same validation block that asserts tx.outputs[1].scriptPubKey == new
SingleSig(borrowerPk), also require that tx.outputs[1].value == borrowAmount to
ensure the borrowed funds amount is enforced; update the LendingMarket successor
validation area (the block that checks tx.outputs[0] and tx.outputs[1]) to
include this additional require referencing tx.outputs[1], borrowAmount and
SingleSig(borrowerPk).
In `@playground/generate_contracts.sh`:
- Around line 20-39: The current generator only scans the root and one-level
subdirectories (loops over dir and subdir) so deeper .ark files (e.g.,
examples/foo/bar/baz.ark) are missed; replace the two-loop logic with a
recursive directory walk (e.g., implement a walkDir(dirPath, prefix) that uses
fs.readdirSync + fs.statSync and calls itself for directories) that pushes
entries with the proper namespace prefix (use prefix + '_' + baseName for nested
folders) into the existing entries array; update usage of variables dir, subdir,
d, f, and name construction to use the prefix parameter so all nested .ark files
are discovered while preserving the existing header/out generation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a1aaf364-f87f-4e43-b111-ea61c983d0b7
📒 Files selected for processing (10)
examples/vault_lending/composite_router.arkexamples/vault_lending/credit_note.arkexamples/vault_lending/debt_note.arkexamples/vault_lending/lending_market.arkexamples/vault_lending/repay_flow.arkexamples/vault_lending/strategy_fragment.arkexamples/vault_lending/supply_flow.arkexamples/vault_lending/vault_covenant.arkplayground/generate_contracts.shplayground/main.js
| function route( | ||
| signature keeperSig0, | ||
| signature keeperSig1, | ||
| bytes32 reportHash0, | ||
| bytes32 reportHash1, | ||
| int reported0, | ||
| int reported1, | ||
| int currentTotalAssets, | ||
| int currentTotalShares | ||
| ) { | ||
| require(checkSigFromStack(keeperSig0, keeper0, reportHash0), "invalid keeper0"); | ||
| require(checkSigFromStack(keeperSig1, keeper1, reportHash1), "invalid keeper1"); | ||
|
|
||
| int newTotalAssets = (reported0 * weight0 + reported1 * weight1) / 10000; | ||
| require(newTotalAssets >= currentTotalAssets, "PPS decrease forbidden"); |
There was a problem hiding this comment.
The keeper attestations are replayable as written.
Lines 40-41 verify signatures over caller-supplied hashes, but the script never proves those hashes commit to strategyId0/1, reported0/1, or the currentTotal* values that feed output 0. Any stale keeper signature can therefore be reused to authorize a different routing result.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/vault_lending/composite_router.ark` around lines 30 - 44, The keeper
signatures are replayable because route() only checks that keeperSig0/1 sign the
caller-supplied reportHash0/1 without proving those hashes commit to
strategyId0/1, reported0/1 or currentTotalAssets/currentTotalShares; fix by
recomputing and verifying the commitment that was actually signed: compute a
canonical hash (include strategyId0/strategyId1, reported0/reported1,
currentTotalAssets, currentTotalShares, and a domain/tag) and require
reportHash0 == computedHash0 and reportHash1 == computedHash1 before calling
checkSigFromStack; use the same canonical format for both keepers to prevent
reuse across different inputs.
| bytes32 priceHash, | ||
| signature oracleSig, | ||
| int price, | ||
| int borrowAmount, | ||
| int collateral | ||
| ) { | ||
| require(checkSig(borrowerSig, borrowerPk), "invalid borrower"); | ||
| require(checkSigFromStack(oracleSig, oraclePk, priceHash), "invalid oracle"); | ||
|
|
||
| // Collateral ratio check: (collateral * price / 10000) >= (borrowAmount * 10000 / lltv) | ||
| // Division floors toward zero — see contract header note. | ||
| int lhs = collateral * price / 10000; | ||
| int rhs = borrowAmount * 10000 / lltv; | ||
| require(lhs >= rhs, "insufficient collateral ratio"); |
There was a problem hiding this comment.
Same binding vulnerability: priceHash not verified to commit to price.
Similar to the reportHash issue in vault_covenant.ark, the oracle signs priceHash but the contract doesn't verify that priceHash actually commits to the price value. An attacker could reuse a single oracle signature with arbitrary price values.
Consider computing priceHash from price within the contract or documenting the required off-chain verification.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/vault_lending/lending_market.ark` around lines 59 - 72, The oracle
signature verifies priceHash but the contract never ties priceHash to the
numeric price, allowing signature replay with arbitrary price values; compute
the canonical hash of the provided price inside the function (e.g. derive
expectedPriceHash from price using the same hashing scheme used off-chain) and
then either require(expectedPriceHash == priceHash) before calling
checkSigFromStack(oracleSig, oraclePk, priceHash) or call
checkSigFromStack(oracleSig, oraclePk, expectedPriceHash) directly so the
oracleSig is validated against the actual price value; update the logic around
priceHash/price and the checkSigFromStack call to enforce this binding (refer to
symbols priceHash, price, oracleSig, oraclePk, and checkSigFromStack).
| function report(signature keeperSig, bytes32 reportHash, int reportedAmount) { | ||
| require(reportedAmount > 0, "reported amount must be positive"); | ||
| require(checkSigFromStack(keeperSig, keeperPk, reportHash), "invalid keeper"); | ||
| require( | ||
| tx.outputs[0].scriptPubKey == new StrategyFragment( | ||
| keeperPk, reportedAmount, strategyId | ||
| ), | ||
| "successor mismatch" | ||
| ); |
There was a problem hiding this comment.
Bind the keeper signature to the reported state.
Line 30 verifies an arbitrary caller-supplied reportHash, but nothing proves that hash commits to strategyId or reportedAmount. Any previously signed keeper message can be replayed here to authorize a different report.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/vault_lending/strategy_fragment.ark` around lines 28 - 36, The
keeper signature must be bound to the actual reported state: compute a canonical
report digest inside report (e.g., hashReport = keccak(strategyId,
reportedAmount) with any domain/version prefix) and verify the signature against
that digest instead of an arbitrary caller-supplied reportHash; then require
that the provided reportHash (if kept) equals the computed digest or simply
remove the reportHash parameter and call checkSigFromStack(keeperSig, keeperPk,
hashReport). Update the checkSigFromStack call in function report and ensure the
subsequent successor check still uses the local reportedAmount and strategyId so
the keeper signature authorizes exactly those values.
| vault_lending: { | ||
| name: 'Vault + Lending', | ||
| description: 'ERC-4626 vault with yield strategies, lending market, and DebtToken/CreditToken synthetics', | ||
| files: { | ||
| 'vault_covenant.ark': contracts.vault_lending_vault_covenant, | ||
| 'strategy_fragment.ark': contracts.vault_lending_strategy_fragment, | ||
| 'composite_router.ark': contracts.vault_lending_composite_router, | ||
| 'debt_note.ark': contracts.vault_lending_debt_note, | ||
| 'credit_note.ark': contracts.vault_lending_credit_note, | ||
| 'lending_market.ark': contracts.vault_lending_lending_market, | ||
| 'supply_flow.ark': contracts.vault_lending_supply_flow, | ||
| 'repay_flow.ark': contracts.vault_lending_repay_flow, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
from pathlib import Path
wanted = [
'vault_lending_vault_covenant',
'vault_lending_strategy_fragment',
'vault_lending_composite_router',
'vault_lending_debt_note',
'vault_lending_credit_note',
'vault_lending_lending_market',
'vault_lending_supply_flow',
'vault_lending_repay_flow',
]
path = Path('playground/contracts.js')
if not path.exists():
raise SystemExit('playground/contracts.js is missing')
text = path.read_text()
missing = [name for name in wanted if f'export const {name} =' not in text]
if missing:
raise SystemExit('Missing exports: ' + ', '.join(missing))
print('All vault_lending exports are present in playground/contracts.js')
PYRepository: arkade-os/compiler
Length of output: 96
Regenerate playground/contracts.js—the file is missing entirely.
The new vault_lending project references eight contracts.vault_lending_* exports (lines 18–29), but playground/contracts.js does not exist in the repository. This will cause all these references to resolve as undefined at runtime.
Regenerate the artifacts using ./playground/generate_contracts.sh and ./playground/build.sh.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@playground/main.js` around lines 18 - 29, The vault_lending fixture
references eight missing exports (contracts.vault_lending_vault_covenant,
contracts.vault_lending_strategy_fragment,
contracts.vault_lending_composite_router, contracts.vault_lending_debt_note,
contracts.vault_lending_credit_note, contracts.vault_lending_lending_market,
contracts.vault_lending_supply_flow, contracts.vault_lending_repay_flow) but
playground/contracts.js is absent, causing undefined values at runtime;
regenerate the contract artifacts by running ./playground/generate_contracts.sh
and then ./playground/build.sh to recreate playground/contracts.js, verify that
it exports the above symbols, and commit the generated playground/contracts.js
so vault_lending can import the concrete contract strings.
- Add examples/vault_lending/lending_pool.ark: permissionless token-layer
pooled lending market with supply() and borrow() functions using asset
group token mechanics (supplyTokenId/borrowTokenId + control assets)
- lending_market.ark: add missing value checks
- borrow: require outputs[1].value == borrowAmount
- repay: require outputs[1].value == repayAmount; require outputs[0].value
== collateralAmount in partial repay branch
- liquidate: guard residual >= 0 before waterfall
- supply_flow.ark: bound supplyAmount <= totalAssets; prove exact per-output
value splits (vault gets inputVal - supplyAmount, market gets supplyAmount)
- repay_flow.ark: bind returnAmount to tx.input.current.value so vault
accounting cannot drift from actual settled value
- composite_router.ark: enforce weight0 + weight1 == 10000 on-chain
- playground/main.js: add lending_pool.ark to vault_lending project
https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
Playground PreviewA live preview of this PR's playground is available at:
|
Pooled lending conflicts with the Morpho vault+market design where VaultCovenant allocates to isolated LendingMarket instances. A pooled model socialises risk and duplicates what the vault layer already provides. https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
creditHolder in LendingMarket changes from pubkey to bytes32 (a script hash). At supply time, SupplyFlow sets it to the precomputed scriptPubKey of RepayFlow(keeperPk, ownerPk, totalAssets - supplyAmount, totalShares). Repayments and liquidation proceeds therefore land directly in the correct RepayFlow covenant — the keeper cannot redirect them. RepayFlow drops returnAmount from its constructor; reclaim() derives the return amount from tx.input.current.value, binding vault accounting to the actual settled value with no caller input. transferCredit becomes keeper-only (checkSig against vaultKeeperPk) since creditHolder is now a script and cannot be checked with checkSig directly. Add examples/vault_lending/FLOWS.md with Mermaid diagrams of every UTXO spending path across the vault+lending system. https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
Replace flowchart+subgraph with plain graph LR nodes — avoids the chunk-loading failure GitHub shows with nested direction declarations. https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
If the keeper is unresponsive after the 144-block exit timelock, the LP (ownerPk) can call reclaimExpired() unilaterally — no keeper co-sign required. The LP supplies the current vault totalAssets/totalShares (observable from the vault VTXO scriptPubKey on-chain); the covenant verifies only that all value flows to a valid VaultCovenant successor. This gives LPs a self-sovereign exit for deployed capital, matching the liveness guarantee borrowers already have via the exit clause. Liquidations remain keeper-gated (unavoidable without keeper oracle access). Updated FLOWS.md: added diagram 6b (reclaimExpired path) and a liveness tradeoff table covering all participants. https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
There was a problem hiding this comment.
Ark Protocol Review — Vault + Lending Market Covenant System
Reviewed all 6 covenant files, FLOWS.md, and playground integration across all 11 commits.
Architecture — Well-Designed
The Morpho-style isolated-market architecture is a strong fit for UTXO covenants. Key strengths:
- No shared risk between positions. Each
LendingMarketis per-borrower with its own collateral and debt, which avoids the pooled-lending antipattern where one bad position drags down the pool. creditHolderasbytes32script hash is the right call. Binding repayment destination at supply time (via precomputedRepayFlowscriptPubKey) eliminates an entire class of keeper misdirection attacks.returnAmount = tx.input.current.valueinRepayFlowis elegant — vault accounting is bound to actual settled value, not a caller-supplied number.reclaimExpired()after 144 blocks closes the LP liveness gap. Good that this was added in the final commit.
Security Findings
PROTOCOL-CRITICAL — Flagging for human review. This code handles collateral, debt, and fund flows. Several items warrant attention:
1. Oracle price binding is still weak (lending_market.ark, borrow + liquidate)
The oracle signature verifies priceHash via checkSigFromStack(oracleSig, oraclePk, priceHash), but the contract never verifies that priceHash is actually the hash of price. The caller supplies both priceHash and price independently. This means:
- A borrower could replay a stale oracle signature with a favorable
pricevalue - A keeper could liquidate with a manipulated price
This was flagged by CodeRabbit but does not appear to be addressed in the current code. The security of the entire collateral ratio check depends on this binding. Without it, the LLTV check is essentially unenforced.
I understand the .ark language may not have an on-chain hash opcode available for this — if binding is enforced off-chain or by the oracle message format, that should be documented explicitly in the contract header.
2. reclaimExpired() — LP supplies arbitrary vault state (repay_flow.ark:30)
The LP provides currentTotalAssets and currentTotalShares with no on-chain verification. The comment says these are "observable from the vault VTXO scriptPubKey on-chain," but the covenant does not verify them. A malicious LP could supply fabricated values to create a VaultCovenant successor with incorrect accounting.
In practice this may be acceptable because:
- The LP is the
ownerPkand would only harm themselves - The resulting VaultCovenant still needs valid keeper co-signatures for subsequent operations
But if the vault serves multiple LPs (which the single ownerPk design currently prevents), this would be a critical vulnerability. Worth documenting this as a known trust assumption.
3. Liquidation waterfall — >= vs == (lending_market.ark:135-146)
The liquidation outputs use >= checks (tx.outputs[0].value >= fee, etc.) rather than ==. This means a keeper could overpay themselves on the fee output by reducing the residual output, as long as the residual stays >= the computed residual. Since the computed residual is collateralAmount - fee - debtAmount, and fee is fixed at 5%, the keeper could extract extra value from the transaction's other inputs.
Using == for at least the fee and debt outputs would be stricter. The >= on residual is fine since any surplus goes to the borrower.
4. Self-import pattern (lending_market.ark:6, vault_covenant.ark:12, strategy_fragment.ark:5)
Several contracts import themselves (import "lending_market.ark" inside lending_market.ark). This appears to be needed for recursive covenant construction (new LendingMarket(...) in successor checks). If the compiler handles this correctly, it's fine, but it's unusual and worth confirming it doesn't cause circular dependency issues.
5. StrategyFragment.allocate() — no output value check (strategy_fragment.ark:17-25)
The allocate() function verifies the successor script but does not check tx.outputs[0].value. A keeper could drain value from the fragment UTXO while updating the allocation amount upward.
6. Composite router PPS monotonicity check may be circumventable
CompositeRouter.route() requires newTotalAssets >= currentTotalAssets, but currentTotalAssets is supplied by the caller. A keeper could supply a deflated currentTotalAssets to pass the check while actually decreasing PPS. The vault output is verified against newTotalAssets, but currentTotalAssets is not verified against the actual current vault state.
Test Coverage
No tests are included in this PR. For financial covenant code of this complexity, this is a significant gap. At minimum, I'd expect:
- Unit tests for each covenant function (happy path + revert cases)
- Integration tests for the full lifecycle (deposit -> supply -> borrow -> repay -> reclaim -> withdraw)
- Edge case tests for liquidation waterfall (residual = 0, collateral barely underwater)
- Oracle price manipulation scenarios
Code Quality
- Documentation is excellent — FLOWS.md with Mermaid diagrams and the liveness table are very helpful
- Division/rounding notes in each contract header are good practice
- The iterative cleanup across commits (removing synthetic paths, removing lending_pool, simplifying credit flow) shows solid design evolution
- Playground integration is clean
Summary
The architecture is sound and the design decisions (isolated markets, script-hash credit binding, value-derived return amounts) are well-reasoned. However, several security items need attention before this should be merged — particularly the oracle price binding (#1) and the lack of test coverage. The >= vs == in liquidation (#3) and the missing value check in allocate() (#5) are lower priority but should also be addressed.
Recommendation: Request changes. Items #1 (oracle binding) and #3 (liquidation output values) need fixes or explicit documentation of trust assumptions. Tests should be added before merge. This is protocol-critical financial code — bugs here lose real money.
…docs, tests Security fixes: - lending_market liquidate(): fee and debt outputs changed from >= to == preventing keeper from over-extracting from other tx inputs while keeping residual >= (surplus goes to borrower is correct) - strategy_fragment allocate(): add tx.outputs[0].value == tx.input.current.value to prevent keeper draining fragment value while updating allocation amount Documentation of trust assumptions: - lending_market: add oracle price binding caveat — checkSigFromStack verifies priceHash but language has no on-chain hash opcode to bind priceHash to price; deployments must ensure oracle message format commits to a canonical price encoding - composite_router: document that currentTotalAssets is caller-supplied and the PPS monotonicity check can be circumvented by a keeper supplying deflated values Tests (tests/vault_lending_test.rs — 22 tests): - Compilation smoke test for all vault+lending contracts - ABI structure: parameter names, types, counts, function variants - Security regressions: creditHolder is bytes32, borrower output value checked, repay output value checked, liquidation uses exact == amounts, residual guarded, allocate preserves value, transferCredit is keeper-only with bytes32 newHolder - Lifecycle checks: reclaim requires keeperSig, reclaimExpired requires ownerSig with currentTotalAssets/currentTotalShares params, both produce VaultCovenant https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
There was a problem hiding this comment.
Follow-up Review — New Commits (ff7021e, 0091b0a)
Reviewed the two commits pushed in response to my previous review. All six findings were addressed.
Status of Previous Findings
| # | Finding | Resolution | ✓ |
|---|---|---|---|
| 1 | Oracle price binding weak | Documented as trust assumption in lending_market.ark header (language has no on-chain hash opcode — correct limitation) |
✅ |
| 2 | reclaimExpired() LP supplies arbitrary vault state |
Documented in repay_flow.ark:63-67 — LP bears accounting risk, single-owner vault limits blast radius |
✅ |
| 3 | Liquidation >= vs == on fee/debt outputs |
Fixed: lending_market.ark:155,158 now use == fee and == debtAmount. Residual correctly stays >= |
✅ |
| 4 | Self-import pattern | Not a bug — needed for recursive covenant construction | ✅ |
| 5 | StrategyFragment.allocate() missing value check |
Fixed: strategy_fragment.ark:20 — tx.outputs[0].value == tx.input.current.value |
✅ |
| 6 | Composite router PPS monotonicity circumventable | Documented as trust assumption in composite_router.ark:8-12 — not constrainable without spending vault UTXO in same tx |
✅ |
Test Coverage
364 lines of Rust tests added (tests/vault_lending_test.rs). Tests cover:
- Compilation smoke tests for all 4 contracts
- ABI structural checks (parameter types, function counts, variants)
- Security regression tests: OP_EQUAL in liquidation, value introspection in allocate/borrow/repay, OP_CHECKSIGFROMSTACK in oracle paths
These are compiler-level tests (correct scope for this repo). Runtime behavior testing belongs in the VM/integration test suite.
Minor Remaining Notes (non-blocking)
-
StrategyFragment.report()has no output value check — unlikeallocate()which was fixed,report()still doesn't verifytx.outputs[0].value. A keeper could drain value during a report. However, the keeper already controls the attestation, so this is self-sabotage rather than an external attack vector. Non-blocking, but worth noting for completeness. -
CompositeRouter.route()doesn't verify output values — scripts are checked but nottx.outputs[N].valuefor any of the 3 outputs. Value preservation depends on the transaction builder. Acceptable for a routing covenant, but worth documenting.
PROTOCOL-CRITICAL: This code handles collateral, debt, and fund flows. Human review still required before merge. The trust assumptions are now clearly documented, the liquidation waterfall is tightened, and regression tests are in place. Approving the code quality — a human must sign off on the protocol design decisions (oracle trust model, keeper trust for PPS reporting).
…put values strategy_fragment report(): add tx.outputs[0].value == tx.input.current.value for consistency with allocate() — a keeper could otherwise drain fragment value during a report attestation. composite_router: document that output values are not checked by the covenant; value conservation across the 3 outputs relies on the Ark round mechanism. tests: add test_strategy_fragment_report_preserves_value (23 tests total). https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
There was a problem hiding this comment.
Follow-up Review — Commit 505a162
Reviewed the single commit pushed after my approval. It addresses both non-blocking notes:
| Note | Fix | ✓ |
|---|---|---|
report() missing value check |
strategy_fragment.ark:32 — tx.outputs[0].value == tx.input.current.value added |
✅ |
CompositeRouter.route() output values undocumented |
composite_router.ark:14-17 — documented Ark round mechanism enforces value conservation |
✅ |
| Test coverage | test_strategy_fragment_report_preserves_value() added — checks value introspection in compiled output |
✅ |
No new issues. Approval stands.
There was a problem hiding this comment.
Follow-up Review — Commit 505a162
Reviewed the single commit pushed after my approval. It addresses both non-blocking notes:
| Note | Fix | ✓ |
|---|---|---|
report() missing value check |
strategy_fragment.ark:32 — tx.outputs[0].value == tx.input.current.value added |
✅ |
CompositeRouter.route() output values undocumented |
composite_router.ark:14-17 — documented Ark round mechanism enforces value conservation |
✅ |
| Test coverage | test_strategy_fragment_report_preserves_value() added — checks value introspection in compiled output |
✅ |
No new issues. Approval stands.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
examples/vault_lending/lending_market.ark (1)
17-24:⚠️ Potential issue | 🔴 Critical
priceHashstill does not commit to thepriceused in the checks.Line 58 and Line 143 verify a signature over
priceHash, but Line 62-Line 64 and Line 146-Line 148 use the separately suppliedprice. Anyone with one valid oracle signature can replay it with a different numeric price and bypass both the borrow collateral-ratio gate and the liquidation-underwater check. This needs a signed preimage the covenant actually derives from the same price witness.Also applies to: 57-64, 142-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/vault_lending/lending_market.ark` around lines 17 - 24, The contract currently verifies checkSigFromStack(oracleSig, oraclePk, priceHash) but then uses the separately supplied price value, allowing signature replay with a different price; fix by deriving the signed commitment on-chain from the provided price (e.g. compute priceHash = hash(canonical_encode(price)) using the same canonical encoding the oracle signs) and then call checkSigFromStack(oracleSig, oraclePk, priceHash) before using price in the borrow and liquidation checks so the signature binds to the exact numeric price; ensure you use the same canonical encoding in the oracle off-chain signer and in the covenant code and update every place that accepts price (the checks that currently reference price and priceHash) to compute-and-verify rather than trusting an externally supplied priceHash.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/vault_lending/lending_market.ark`:
- Around line 169-183: transferCredit currently lets the keeper arbitrarily set
newHolder (in transferCredit) with only the keeper signature
(checkSig(keeperSig, vaultKeeperPk)), enabling redirection of
repay()/liquidate() proceeds; fix by requiring authorization from the current
credit beneficiary or a deterministic whitelist: either require an additional
signature from the current creditHolder (e.g., verify a signature over newHolder
from the existing creditHolder) or enforce that tx.outputs[0].scriptPubKey
equals a RepayFlow address deterministically derived from known state (e.g.,
derive allowed RepayFlow from current creditHolder + rotation nonce) instead of
accepting any newHolder value so repay()/liquidate() cannot be redirected by the
keeper alone.
- Around line 66-81: The borrow() covenant currently doesn't tie the input's
surplus to the two created outputs, allowing extra input value to be diverted
and positions to be shrunk; fix by adding explicit value-accounting and an
anti-reborrow guard: require tx.input.current.value (or sum of relevant funding
inputs) equals collateral + borrowAmount (plus any fees) and/or require
sum(tx.outputs[*].value) == tx.input.current.value so no surplus can be routed
elsewhere, and ensure the successor LendingMarket state (LendingMarket(...)
fields like collateralAmount and debtAmount) reflects correct non-decreasing
position invariants (e.g., successor.collateralAmount >= collateral and
successor.debtAmount == existing.debtAmount + borrowAmount or at least not less
than previous values) to prevent rewriting an open position. Use the identifiers
tx.input.current.value, tx.outputs[0].value, tx.outputs[1].value, LendingMarket,
collateral, borrowAmount, collateralAmount and debtAmount to locate and apply
these checks.
In `@tests/vault_lending_test.rs`:
- Around line 482-489: The test test_all_vault_lending_contracts_compile()
currently lists only VaultCovenant, StrategyFragment, RepayFlow, and
LendingMarket, omitting the two new contracts CompositeRouter and SupplyFlow;
update the contracts array in that test to also include ("CompositeRouter",
COMPOSITE_ROUTER_SRC) and ("SupplyFlow", SUPPLY_FLOW_SRC) so the smoke test
compiles all six vault-lending contracts and covers the new integration
surfaces.
- Around line 5-126: The test currently uses hand-maintained string literals
(VAULT_COVENANT_SRC, STRATEGY_FRAGMENT_SRC, REPAY_FLOW_SRC, LENDING_MARKET_SRC)
that can drift from the real example contracts; change each constant to load the
real example source with include_str! for the corresponding example files so the
tests compile the actual contracts (replace the literal values assigned to
VAULT_COVENANT_SRC, STRATEGY_FRAGMENT_SRC, REPAY_FLOW_SRC and LENDING_MARKET_SRC
with include_str! calls pointing at the matching example .ark files), ensuring
ABI/opcode assertions remain tied to the real sources.
- Around line 394-457: The current ASM assertions in tests
test_lending_market_borrow_checks_borrower_output_value,
test_lending_market_repay_checks_repay_output_value,
test_lending_market_liquidate_uses_exact_fee_and_debt_amounts, and
test_lending_market_liquidate_guards_residual are too permissive; replace the
loose .any(op == ...) checks with concrete requirement checks via
AbiFunction.require (from src/models/mod.rs:85-105) or assert exact opcode
sequences/counts (e.g., require OP_INSPECTOUTPUTVALUE followed immediately by
OP_EQUAL for borrow/repay, require two distinct OP_EQUAL outputs for liquidate
fees/debt, and require the specific OP_0 + OP_GREATERTHANOREQUAL sequence for
residual) so each test asserts the precise opcode pattern expected rather than
any occurrence.
---
Duplicate comments:
In `@examples/vault_lending/lending_market.ark`:
- Around line 17-24: The contract currently verifies
checkSigFromStack(oracleSig, oraclePk, priceHash) but then uses the separately
supplied price value, allowing signature replay with a different price; fix by
deriving the signed commitment on-chain from the provided price (e.g. compute
priceHash = hash(canonical_encode(price)) using the same canonical encoding the
oracle signs) and then call checkSigFromStack(oracleSig, oraclePk, priceHash)
before using price in the borrow and liquidation checks so the signature binds
to the exact numeric price; ensure you use the same canonical encoding in the
oracle off-chain signer and in the covenant code and update every place that
accepts price (the checks that currently reference price and priceHash) to
compute-and-verify rather than trusting an externally supplied priceHash.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3f9ac722-807d-4c20-9c25-f9800f3da1d4
📒 Files selected for processing (8)
examples/vault_lending/FLOWS.mdexamples/vault_lending/composite_router.arkexamples/vault_lending/lending_market.arkexamples/vault_lending/repay_flow.arkexamples/vault_lending/strategy_fragment.arkexamples/vault_lending/supply_flow.arkplayground/main.jstests/vault_lending_test.rs
✅ Files skipped from review due to trivial changes (3)
- playground/main.js
- examples/vault_lending/FLOWS.md
- examples/vault_lending/composite_router.ark
🚧 Files skipped from review as they are similar to previous changes (3)
- examples/vault_lending/supply_flow.ark
- examples/vault_lending/strategy_fragment.ark
- examples/vault_lending/repay_flow.ark
…quality lending_market borrow(): - Add require(debtAmount == 0) + require(collateralAmount == 0) to prevent re-borrowing over an existing position (anti-reborrow guard) - Add require(tx.input.current.value == borrowAmount) to enforce value conservation: borrower must draw the full available liquidity, no surplus can be routed to arbitrary outputs lending_market transferCredit(): - Add require(debtAmount == 0) so creditHolder is immutable once a position is open; keeper can only rotate the RepayFlow target before the borrower draws, preventing post-borrow redirection of repay()/liquidate() proceeds tests/vault_lending_test.rs: - Switch all source constants from hand-maintained literals to include_str! pointing at the real example files — tests now compile the actual contracts - Add CompositeRouter and SupplyFlow to smoke test (was missing 2 of 6) - Add asm_contains_sequence() helper for precise opcode subsequence assertions - Replace loose .any(op == ...) checks with exact ASM window matching: borrow reborrow guards, value conservation, borrower output value, repay output value, liquidation exact fee/debt, residual guard, transferCredit open-position block - 25 tests total (was 23) https://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
There was a problem hiding this comment.
Follow-up Review — Commit 0a9d361
Reviewed the single commit since my last approval (505a162→0a9d361). This addresses CodeRabbit findings with three protocol-critical fixes in lending_market.ark and a significant test quality upgrade.
Contract Changes (lending_market.ark)
1. Anti-reborrow guard in borrow() — ✅ Correct & critical
require(debtAmount == 0, "position already open");
require(collateralAmount == 0, "position already has collateral");
Without this, a borrower could call borrow() on an already-open position, resetting collateral and debtAmount to new values. The collateralAmount == 0 check is technically redundant (can't have collateral without debt in normal flow) but is good defense-in-depth.
2. Value conservation in borrow() — ✅ Correct & critical
require(tx.input.current.value == borrowAmount, "borrow amount must equal available liquidity");
Without this, if the LendingMarket UTXO held more sats than borrowAmount, the excess would be lost as miner fees (outputs only account for collateral + borrowAmount). Good fix.
3. transferCredit() blocked on open positions — ✅ Correct & critical
require(debtAmount == 0, "cannot rotate credit on open position");
This closes a keeper rug vector: without it, a malicious keeper could redirect repay()/liquidate() proceeds to a different RepayFlow script after the borrower has already drawn funds. Now creditHolder is immutable once borrow() is called.
Test Changes (vault_lending_test.rs)
include_str! refactor — Excellent. Tests now compile the actual contract files instead of hand-maintained inline copies. This eliminates the snapshot-drift problem where tests could pass against stale copies while the real contracts have changed.
Sequence-based assertions (asm_contains_sequence) — Much stronger than the previous any(|op| op.contains(...)) pattern. Verifying opcode sequences (e.g. ["<debtAmount>", "0", "OP_EQUAL"]) catches incorrect operand ordering that single-opcode checks would miss.
New tests for reborrow guard, value conservation, and transferCredit lockdown — all properly verify the new contract invariants at the ASM level.
Smoke test expanded to include CompositeRouter and SupplyFlow — good, all 6 contracts now covered.
Verdict
All three contract fixes address real attack vectors. Tests are materially stronger. No issues found. Approved.
Summary
Morpho-style vault + isolated lending market system for Ark. Vault LPs deposit into an ERC-4626-style
VaultCovenant; the keeper allocates to isolatedLendingMarketpositions; repayments flow back through a pre-committedRepayFlowcovenant. No shared risk between positions.Full UTXO spending diagrams + liveness table:
examples/vault_lending/FLOWS.mdContracts
vault_covenant.arktotalAssets/totalShares. Keeper-attested yield viareportYield.strategy_fragment.arkreportedAmountbinding.composite_router.arkweight0 + weight1 == 10000).supply_flow.arkVaultCovenant→ freshLendingMarket. SetscreditHolderto precomputedRepayFlowscript.lending_market.arkrepay_flow.arkreclaim()(keeper) andreclaimExpired()(LP unilateral after 144-block timelock).Key Design Decisions
Repayment routing —
creditHolderasbytes32scriptLendingMarket.creditHolderis abytes32script hash (not apubkey). At supply time,SupplyFlowsets it to the precomputedscriptPubKeyof:Repayments and liquidation proceeds therefore land directly in the correct
RepayFlowcovenant. The keeper cannot redirect them to any other script.RepayFlow.reclaim()derives the return amount fromtx.input.current.value— vault accounting is bound to the actual settled value, not a caller-supplied number.Liveness — LP self-sovereign exit for deployed capital
reclaim()needs keeperreclaimExpired()after 144 blocksRepayFlow.reclaimExpired(ownerSig, currentTotalAssets, currentTotalShares)is callable without keeper co-sign after the 144-block exit timelock. The LP supplies the current vault state (observable from the vault VTXO scriptPubKey on-chain) and recovers all deployed capital. Liquidations remain keeper-gated — if the keeper is offline while positions are underwater, the vault absorbs the loss.Supplier / Borrower exit
VaultCovenant.withdraw()for idle assets any time;reclaimExpired()for deployed assets after 144 blocks.SingleSig(borrowerPk), repayment toRepayFlow.LendingMarketwith reduced debt; collateral stays locked.RepayFlow, residual → borrower.UTXO Flow (abbreviated)
Security fixes (addressed during review)
lending_marketborrow:outputs[1].value == borrowAmountlending_marketrepay:outputs[1].value == repayAmount;outputs[0].value == collateralAmountin partial branchlending_marketliquidate:residual >= 0guard before waterfallsupply_flow:supplyAmount <= totalAssetsbound; exact per-output value splits provedrepay_flow:returnAmountderived fromtx.input.current.value(not constructor param)composite_router:weight0 + weight1 == 10000enforced on-chainhttps://claude.ai/code/session_01SatYwcP7Swjvrfy4w49ad2
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests