Prepared by:
HALBORN
Last Updated 07/01/2026
Date of Engagement: June 8th, 2026 - June 19th, 2026
100% of all REPORTED Findings have been addressed
All findings
30
Critical
0
High
1
Medium
9
Low
16
Informational
4
The Alpend project team engaged Halborn to conduct a security assessment of the alpend-protocol repository (https://github.com/AlpendHQ/alpend-protocol), a privacy-first, multi-asset money market implemented in Daml for the Canton Network. Alpend follows an Aave v3 style design: users supply assets into per-asset reserves, optionally flag deposits as collateral, borrow other assets against that collateral basket, and may be liquidated when their position becomes unhealthy. Interest accrues through scaled balances and compounding indices, and every credit decision is recomputed on-chain from a Chainlink Data Streams price oracle.
The assessment covered the lending core, the per-asset reserve accounting and interest-rate model, the on-chain risk engine (health-factor and borrow-capacity recompute), the Chainlink Data Streams oracle integration including its on-chain secp256k1 report verifier, the liquidation lifecycle, the legacy-to-Daml migration path, the position registry, and the supporting token and decoding utilities. The audit was performed against commit bdaa5e7 of the Daml multi-package workspace, which contains:
lending (primary target, approximately 2,512 lines of Daml across 10 modules): pool orchestration, reserve accounting, interest model, oracle, positions, and migration.
common (approximately 142 lines): shared types and utilities.
cip56-token (approximately 390 lines): a reference CIP-56 (Canton Network Token Standard) token used by the test harness; the lending core builds against the Splice Token Standard interfaces rather than this concrete token.
lending-tests: the project's Daml Script test harness, which the assessment extended with executable proof-of-concept and property tests.
Because a lending protocol concentrates user funds and bases every credit decision on price and accounting state, the security boundary extends well beyond functional correctness. On Canton, the Daml runtime removes entire EVM vulnerability classes: there is no re-entrancy across the atomic transaction model, no gas, and authorization is enforced by signatories and controllers rather than by caller-address checks. The risk therefore concentrates in protocol logic the runtime cannot protect: the consistency between the protocol's internal accounting (scaled balances, indices, and the booked totalLiquidity) and the real on-ledger token holdings; the correctness and manipulability of the oracle integration; the soundness of the liquidation and health-factor engine; and the completeness of the operator-controlled risk parameters and lifecycle controls.
Accounting versus real-token desynchronization: several findings stem from the protocol's booked accounting drifting away from real on-ledger holdings, or from per-position principal drifting away from the reserve's aggregate counters. Holding-fee decay is recorded as withdrawable protocol revenue; position principal is re-based on unification and partial withdrawal while the face counter is not; and a caller-supplied holdings subset can orphan real pool holdings from a reserve.
Oracle integration gaps: the Chainlink Data Streams path keys prices by the raw report feed id while reserves are configured with human-readable keys, the two-step deviation guard can freeze a feed during a sustained move, the pending-price buffer has no expiry, the report version is never checked, and there is no observation-timestamp monotonicity. These were validated end to end against the real on-chain secp256k1 verifier.
Liquidation and risk-engine completeness: liquidating a deeply-underwater position can lower its health factor and accelerate bad debt, a single stale basket feed blocks all liquidations, and an unbounded position list lets a borrower inflate the cost of liquidating their own account.
Operator-trust and risk-control completeness: reserve deactivation does not actually remove collateral power, risk parameters can be re-pointed and tightened instantly, migrated positions carry no on-chain custody proof, and a bad-debt write-off does not haircut the supplier index. These reinforce the single-operator trust assumption that underlies the design.
Reconcile booked accounting to real holdings at every value-bearing step: reduce totalLiquidity in lock-step with holding-fee index decay, derive extractable revenue from the actual summed on-ledger holdings, and add post-operation invariants that fail closed when summed holdings fall below what suppliers are owed.
Keep per-position principal consistent with the reserve face counter: avoid re-basing a position's principal into accrued terms without symmetrically updating totalDeposited, so unification and partial withdrawal cannot strand funds or block liquidation.
Harden the oracle integration: canonicalize the feed key on both the push and read sides, bound the pending-price window with a TTL, enforce observation-timestamp monotonicity, wire the report-version check into UpdatePrice, and define a liquidation-specific staleness policy so one unrelated stale feed cannot block all liquidations.
Validate caller-supplied holdings and inputs: require that any caller-supplied holdings set represents the reserve's complete holdings (or derive liquidity from it), and add the missing instrument-match and bounds checks across the collateral and reserve choices.
Complete the operator risk controls: make reserve deactivation actually remove collateral and borrow power, bound the number of positions per account, and define an explicit, fair loss-socialization model for bad-debt write-offs.
Halborn performed a combination of exhaustive manual static review, targeted dynamic testing with executable proofs of concept, property-based and conservation fuzzing, and a purpose-built harness that drives the production oracle path through its real on-chain cryptographic verifier. The assessment was structured to maximize coverage of the protocol logic while concentrating effort on the highest-impact surfaces: solvency, the risk engine, and the oracle.
The following phases were performed:
Baseline and inventory: full workspace inventory, build reproduction with the pinned Daml SDK 3.4.8 toolchain, and establishment of the review baseline including the commit hash and the existing Daml Script test-suite status.
Static review track: line-by-line manual review organized into seven related groups covering the pure interest-rate and decoding math, the CIP-56 token layer, the oracle and Chainlink integration, the per-asset reserve accounting, the position and registry templates, the pool orchestration and on-chain risk engine, and the legacy migration path.
Dynamic testing track: executable Daml Script proofs of concept for each candidate finding, written so that a passing test demonstrates the bug is present on the audited commit. The proofs cover phantom revenue and reserve bricking under holding-fee decay, the totalDeposited re-basing desync (via unification, partial withdrawal, and liquidation impairment), the risk-engine integrity checks, the collateral and reserve lifecycle, and the migration custody model.
Solvency and conservation fuzzing: a deterministic multi-actor sequence that drives supply, borrow, repay, withdraw, accrual, and price updates while asserting the core non-decaying invariants (booked liquidity versus real holdings, the debt identity, and index monotonicity) at every step.
Real Chainlink Verifier harness: because the production UpdatePrice path runs an on-chain secp256k1 report verification the simulated ledger cannot bypass, Halborn built an off-ledger generator that produces genuinely signed Chainlink Data Streams V3 reports for a signer key registered in a test-controlled verifier configuration. This unblocked end-to-end proofs for the feed-key mismatch, the deviation freeze, the missing version check, and the timestamp-rollback findings. Negative-control tests (a tampered-price report and a wrong-signer report, both rejected) confirm the verification is genuine and not a no-op.
Canton sandbox parity: selected flows were executed against a live Canton sandbox to confirm that the behavior observed on the simulated ledger holds on a real Canton participant.
Skeptical reassessment: every candidate finding was re-evaluated with a deliberately skeptical lens, separating true protocol vulnerabilities from operator-trust assumptions and design trade-offs. Several leads were refuted with executable evidence (for example, the registry double-count, the debt-reserve omission, and the index-projection parity were all shown to be safe).
This document provides the technical baseline for the assessment of the Alpend protocol. It describes what the system is, how it is implemented in Daml, how value and authority flow through it, and which architectural characteristics are most relevant to the audit. It is not a threat model or a findings report; it is the implementation and architecture profile that supports the separate attack-surface and threat-model documents.
Alpend is a multi-asset, over-collateralized money market on the Canton Network, written in Daml. Its design follows Aave v3:
users supply assets into per-asset reserves and earn interest through a rising liquidity index;
deposits are flagged as collateral (flag-based, not locked) and grant borrowing power weighted by a loan-to-value ratio;
users borrow other assets against their collateral basket, with debt tracked through a compounding variable-borrow index;
positions whose health factor falls below one can be liquidated for a bonus;
the protocol earns a reserve-factor spread that a treasury role may withdraw.
Two characteristics distinguish Alpend from a typical EVM money market. First, it targets Canton assets through the Splice Token Standard, including assets such as Canton Coin that lose value over time through a holding fee, which the interest model accounts for explicitly. Second, all valuation is driven by Chainlink Data Streams with on-chain secp256k1 verification of signed reports.
The repository is a Daml multi-package workspace (multi-package.yaml) with four packages:
lending: the primary target. Approximately 2,512 lines of Daml across 10 modules.
common: shared types and helpers (approximately 142 lines).
cip56-token: a reference CIP-56 token (approximately 390 lines) used by the harness.
lending-tests: the Daml Script test harness and fixtures.
Pool.daml: the central orchestration template (LendingPool). It is the entry point for supply, withdraw, borrow, repay, liquidation, collateral toggling, revenue withdrawal, bad-debt write-off, and administration. It also hosts the on-chain risk engine (computeAccountData, accSupplies, accDebts, healthFactor).
AssetReserve.daml: per-asset reserve state and accounting. It tracks totalLiquidity, totalDeposited, scaled supplied and borrowed totals, the liquidity and variable-borrow indices, and the holding CIDs that back the reserve. It implements interest accrual and the record choices invoked by the pool.
InterestRate.daml: pure interest math: the two-slope (kinked) utilization curve, the compounding variable-borrow index, the linear liquidity index, the net supply rate including holding-fee drag, and the indexed-amount helpers.
Oracle.daml: the price oracle (PriceOracle). It verifies signed Chainlink Data Streams reports, scales prices, enforces staleness and a two-step deviation guard, and serves prices to the pool.
ReportDataV3.daml and HexToDecimal.daml: decoding of the verified V3 report payload and hex-to-decimal conversion.
Deposit.daml and Borrow.daml: the supply and borrow position templates, each co-signed by the user and the operator and reduced or closed during liquidation.
UserPosition.daml: a keyless registry of a user's open deposit and borrow position CIDs. It holds no USD value; valuation is always recomputed on-chain.
Migration.daml: the propose-accept flow that materializes legacy positions on-chain without moving tokens.
Authority is enforced by Daml signatories and choice controllers, with a deliberate role separation:
poolOperator: the pool signatory. It holds custody authority and controls all administrative and accounting choices. It is the trusted party in the design.
oraclePusher: a separate hot key that may push prices and retarget the oracle but cannot move funds.
treasuryParty: the revenue recipient, which may withdraw only earned protocol revenue.
user (supplier, borrower, liquidator): interacts permissionlessly through the pool choices. Positions are co-signed by the user and the operator, so they can only be created inside a pool gate that also moved tokens.
Each reserve maintains scaled balances and two indices. A supplier's claim is suppliersOwed = scaledTotalSupplied * liquidityIndex; outstanding debt is scaledTotalBorrowed * variableBorrowIndex. The reserve also books totalLiquidity (free liquidity) and totalDeposited (face principal, used for the deposit cap). Protocol solvency is derived purely from on-chain state: poolEntitled = totalLiquidity + scaledTotalBorrowed * variableBorrowIndex, the net value is poolEntitled - suppliersOwed, and extractable revenue and deficit fall out of that net. The integrity of these figures, and their correspondence to the real summed holding CIDs, is the protocol's central solvency property.
Borrow rates follow a two-slope kinked curve around an optimal utilization. The variable-borrow index compounds (a third-order Taylor approximation of continuous compounding), while the liquidity index accrues linearly at the net supply rate. The supply rate is the borrow rate scaled by utilization and by one minus the reserve factor, so suppliers earn only what borrowers pay minus the protocol's spread. For decaying assets, the net supply rate subtracts a holding-fee drag proportional to the idle fraction, which can drive the liquidity index downward.
Prices originate from Chainlink Data Streams. UpdatePrice verifies the signed report on-chain through a secp256k1 verifier (f+1 signatures against a configured signer set), parses the V3 payload, scales the benchmark price from 18-decimal fixed point, and enforces validity (valid-from, expiry, staleness) and a two-step deviation guard that holds large single-report moves pending corroboration. A separate manual SetPrice path exists, gated by a configuration flag, and bypasses the deviation guard.
All risk decisions are recomputed on-chain in-transaction. computeAccountData iterates the account's deposit and borrow positions, values each through the position's reserve (using projectIndices for non-primary reserves so they do not all need archiving) and the live oracle price, and produces the weighted-collateral, liquidation-threshold-collateral, and debt aggregates. The health factor is liquidation-threshold-weighted collateral over debt; borrow capacity is loan-to-value-weighted. There is no cached USD aggregate, so a caller cannot inject a stale valuation.
The project builds with the Daml Package Manager (dpm) and Daml SDK 3.4.8 pinned in daml.yaml. The lending-tests package contains an extensive Daml Script suite (42 test modules at the end of the assessment) executed with dpm test. The assessment extended this suite with executable proofs of concept and property tests.
Atomic transaction model: Daml transactions are atomic and there is no implicit external-call re-entrancy, which removes the EVM re-entrancy class. There is also no gas, so denial of service shifts to transaction-size limits and unbounded iteration.
Authorization by signatories and controllers: positions are co-signed by user and operator, and administrative choices are controller-gated, so forging collateral or acting out of band is not possible without the appropriate authority.
Value handled as holdings: tokens are Splice Token Standard holdings transferred through a transfer factory, and the reserve tracks holding CIDs. The correspondence between booked accounting and the real holding set is therefore a first-order concern.
Single-operator trust: the operator holds custody and risk authority. Several findings are bounded by, and reinforce, this trust assumption.
Off-ledger backend supplies CIDs: callers pass current reserve, oracle, position, and holding CIDs into each choice. The protocol validates ownership and pool membership of these inputs, which makes input validation a meaningful surface.
This technical profile establishes the implementation baseline for the audit: what Alpend implements, which modules are responsible for the core behavior, how authority and value flow, and how the accounting, interest, oracle, and risk-engine subsystems fit together. The remaining documents build on this baseline to map the attack surface, model the threats, and report the dynamic testing campaign.
This document describes the step-by-step execution flow of each major protocol operation. For each flow it identifies the inputs and their validation, the internal steps in execution order, the accounting effects, and the failure modes. It is a reference for the shared understanding of operational behavior, not a threat model or findings report.
Every state-changing pool choice follows the same skeleton: it validates the caller-supplied UserPosition ownership, accrues interest on the primary reserve first (the Aave v3 update-state pattern), validates that the reserve and oracle belong to the pool, recomputes the account valuation on-chain from the live oracle where a health-factor or capacity check is required, performs the token transfer through the transfer factory, and records the result into the reserve and the position registry. Reserve and pool CIDs are consumed and recreated, so callers must thread the new CIDs returned by each operation.
Choice: LendingPool.SupplyTSWithPosition
deposits must not be paused; at least one holding is required; the supply amount must be positive;
the UserPosition must belong to the supplier; the reserve must belong to the pool and be active;
if a deposit cap is set, totalDeposited + supplyAmount must not exceed it;
holdings must be owned by the supplier and match the reserve instrument.
Accrue interest on the reserve.
Transfer supplyAmount from the supplier to the pool through the transfer factory.
Record the deposit: increase totalLiquidity and totalDeposited by the supply amount and scaledTotalSupplied by the scaled amount.
Create a new deposit position (or, if unification is requested, merge into an existing one), and register it in the user's position list.
Supplying never lowers the health factor, so no health-factor check is performed.
Choice: LendingPool.BorrowTSWithPosition
Borrows must not be paused; the borrow reserve must belong to the pool and be active.
Accrue the borrow reserve and read its oracle price.
Recompute the borrower's account valuation on-chain over the supplied collateral reserve set, and compute available borrow capacity as loan-to-value-weighted collateral minus current debt.
Validate the requested amount against capacity (or compute the maximum when none is specified), enforce the borrow cap and pool liquidity.
Create or unify the borrow position, transfer the borrowed tokens to the borrower, and record the borrow into the reserve.
Because capacity is loan-to-value-weighted while the health factor is liquidation-threshold-weighted (and the threshold is at least the loan-to-value per asset), staying within capacity guarantees a health factor of at least one without a separate post-borrow check.
Choice: LendingPool.WithdrawTSWithPosition
Withdrawals must not be paused; the deposit and position must belong to the supplier.
Accrue the reserve and compute the deposit's accrued value.
Determine the withdrawal amount (an explicit amount, or a smart maximum-safe amount when none is given and collateral backs a borrow), capped by accrued value and pool liquidity.
Transfer tokens out, create a reduced residual deposit when the remainder exceeds a dust threshold, and record the withdrawal into the reserve.
Recompute the account valuation after the withdrawal and require a health factor of at least one.
The withdrawal records a face-principal reduction and a scaled reduction; the residual deposit re-bases its principal to the remaining accrued value at the current index.
Choice: LendingPool.RepayTSWithPosition
The repay path accrues the reserve, computes the borrower's accrued debt, caps the repayment at that accrued debt, transfers the repayment to the pool, and either closes the borrow position or creates a reduced residual. The reserve records the repayment by increasing totalLiquidity and reducing the scaled borrowed total. Repaying never lowers the health factor.
Choice: LendingPool.LiquidateTS
Liquidations must not be paused. Accrue both the debt and collateral reserves (once if they are the same).
Compute the borrower's accrued debt and the 50% close-factor cap; validate the repayment amount against it.
Recompute the borrower's health factor on-chain from the live oracle and reserve indices and require it to be below one.
Compute the seize amount as the repaid value times one plus the liquidation bonus, divided by the collateral price; require the seized collateral to be available.
Transfer the repayment from the liquidator to the pool and the seized collateral from the pool to the liquidator.
Reduce or close the borrow and collateral positions, and record the repayment and the seizure into the reserves.
An invariant enforced at reserve configuration time keeps the liquidation-threshold times one plus the bonus at or below one, so a liquidation at the threshold does not seize more value than the collateral covers.
Choice: PriceOracle.UpdatePrice
Verify the signed report on-chain through the secp256k1 verifier (f+1 signatures over the report digest).
Parse the V3 payload and scale the benchmark price from 18-decimal fixed point.
Enforce validity: positive price, valid-from not in the future, expiry not in the past, and observation age within the staleness bound.
Apply the deviation guard: accept a small move immediately; hold a large move pending until a second report corroborates it.
Reads through GetPrice re-check staleness and serve the active price. A separate manual SetPrice path, gated by a configuration flag, sets a fresh price with no deviation bound.
Choices: MigrationSnapshot and MigrationAccept
The operator prepares a per-user snapshot of legacy collateral and debt. The user accepts it with a single signature; the operator's authority flows through signatory propagation. Acceptance accrues each referenced reserve, creates the deposit and borrow positions from the snapshot amounts, bumps the reserve totals without moving any tokens (the tokens are assumed already in custody from the legacy system), and creates a fresh position registry. The snapshot is consumed on acceptance, preventing replay.
The primary attack surface is the set of LendingPool choices that ordinary users invoke, because they are where untrusted data enters the protocol. The user-facing choices are supply, withdraw, borrow, repay, liquidation, and collateral enable/disable. Each accepts caller-supplied amounts, holding CIDs, a position registry CID, a primary reserve CID, and a set of account reserve CIDs used for the on-chain valuation. Liquidation is the most sensitive user-facing surface because the liquidator supplies the borrower position, the debt and collateral reserves, the repayment holdings, and the seize parameters.
Amounts: supply, borrow, repay, withdraw, and liquidation amounts are caller-controlled and validated against capacity, accrued value, close factor, and liquidity.
Account reserve sets: callers pass the current CIDs of every reserve their positions touch so the valuation can be recomputed. The protocol validates pool membership and de-duplicates the primary instrument; omitting a reserve aborts rather than under-counting.
Holding CIDs: callers pass the holdings used for transfers, and for ephemeral assets they may pass fresh pool holdings for the payout. These are validated for owner and instrument but not, in general, for completeness, which is a meaningful surface for the reserve holding accounting.
Position registry CIDs: a keyless registry means a user can hold more than one, and the registry choices are co-controlled by the user and the operator so they can only be mutated inside a pool gate.
Oracle reports: signed Chainlink Data Streams reports are the attacker-relevant input to the oracle, gated by on-chain secp256k1 verification against the configured signer set.
The oraclePusher role controls UpdatePrice, the manual SetPrice path (when enabled), and oracle retargeting. It cannot move funds, but it selects which signed reports are submitted and when, and it can retarget the pool's oracle reference. The price path is therefore an important surface even though the pusher cannot directly extract value.
The poolOperator controls reserve creation, risk-parameter and interest-parameter updates, reserve and oracle retargeting, pause flags, protocol-revenue extraction (through the treasury role), bad-debt write-off, and migration. This surface is trusted by design, but its breadth and the instant effect of several parameter changes make it relevant to the threat model.
Because callers supply current CIDs for reserves, oracle, positions, and holdings, the protocol assumes an off-ledger backend tracks and provides these. The protocol validates ownership and pool membership of supplied contracts, but the correctness of which CIDs are supplied (for example, the complete reserve holding set) depends on the backend and is part of the effective input surface.
Value moves through Splice Token Standard holdings and a transfer factory authored by the asset admin. The pool trusts the factory output it receives. A buggy or hostile admin factory is outside the user attack surface but is part of the overall trust surface.
The core security questions for Alpend are whether the protocol remains solvent (every supplier can ultimately be repaid what they are owed), whether credit decisions are made on correct prices and correct accounting, and whether the liquidation engine can always clear unhealthy positions.
Supplied tokens held by the pool are the highest-value asset. Solvency means the real summed holdings, plus outstanding debt, cover what suppliers are owed. The main threats are accounting that drifts away from real holdings (holding-fee decay recorded as revenue, orphaned holdings) and per-position principal that drifts away from the reserve face counter, which can strand funds or block exits.
Borrowing power must reflect only sound, currently-valued collateral. Threats include collateral that keeps full power after a reserve is deactivated, and risk parameters that can be changed instantly.
Every valuation depends on the oracle. Threats include a feed-key mismatch that makes prices unreadable, a deviation guard that freezes a feed during a crash, a pending buffer with no expiry, a missing version check, and the absence of timestamp monotonicity. The deviation and verification logic were exercised against the real on-chain verifier.
The protocol must be able to liquidate unhealthy positions. Threats include liquidations that lower the health factor of deeply-underwater positions, a single stale basket feed that blocks all liquidations, and a position-count bloat that inflates the cost of liquidating an account.
Anyone can supply, borrow, repay, withdraw, and liquidate permissionlessly. The most direct threats come from crafted amounts, crafted holding sets, position bloat, and timing around oracle updates. These actors cannot forge authority but can exercise every public choice with adversarial inputs.
A compromised or careless oraclePusher can select and order signed reports, replay an older still-recent report, retarget the oracle to a junk reference and cause a denial of service, or (if the manual price flag is enabled) set arbitrary prices. It cannot move funds directly.
The poolOperator is trusted with custody and risk. Many findings are bounded by this trust, but the breadth of instant, unbounded controls (risk re-pointing, parameter tightening, migration with no custody proof, revenue extraction) means operator error or compromise has a high blast radius.
A backend that supplies incorrect CIDs (for example, an incomplete reserve holding set) can corrupt reserve accounting even without malicious on-chain authority.
Between users and the pool: every amount, holding, reserve set, and position registry crosses into the pool, which validates ownership, pool membership, and bounds, but relies on callers for completeness of some holding sets.
Between the pool and the oracle: prices are trusted only after on-chain verification and validity checks; the feed key, deviation, and timestamp handling are the residual risk.
Between the protocol and the token layer: value moves through an admin-authored transfer factory whose output the pool trusts.
Between on-chain state and the off-ledger backend: the backend provides the CIDs the protocol operates on.
the poolOperator is honest and its key is secure;
the Chainlink Data Streams signer set and the on-chain verifier are correct and the configured signer keys are genuine;
the off-ledger backend supplies correct and complete CIDs for reserves, oracle, positions, and holdings;
the asset admin and its transfer factory are honest;
reserves are configured with feed keys and parameters consistent with how prices are pushed.
The highest security concern is concentrated in the accounting-versus-real-holdings consistency (solvency) and the oracle integration, followed by the liquidation and health-factor engine. The operator-controlled risk and lifecycle surface is broad and trusted, so its completeness and blast radius are important even where individual issues are within the stated trust model. This is a planning view and should not be read as a statement that each area contains a confirmed critical vulnerability.
Alongside manual static review, Halborn built an executable dynamic-testing campaign on top of the project's Daml Script harness. Every candidate finding was reduced to a proof-of-concept test written so that a passing run demonstrates the bug is present on the audited commit. At the end of the assessment the full suite ran with 350 Daml Script scenarios passing and zero failing, including the audit proofs added in modules Section21 through Section41.
Proof-of-concept tests: each finding was encoded as a focused scenario that sets up the minimal state, performs the operation, and asserts the defective outcome (for example, that a full withdrawal reverts on an accounting underflow while liquidity is sufficient, or that a liquidation seizing more than the booked face counter reverts while a smaller one succeeds). Contrast tests were used to isolate the exact cause and rule out unrelated failure reasons.
Solvency and conservation fuzzing: a deterministic multi-actor sequence drove supply, borrow, repay, withdraw, accrual, and price updates while asserting the core invariants at every step (booked liquidity equal to real summed holdings, the debt identity, and index monotonicity).
Risk-engine integrity checks: dedicated tests confirmed that the on-chain valuation cannot be tricked by omitting a debt reserve, that index projection used for non-primary reserves matches a real accrual, and that the borrow capacity guarantees a healthy position by construction.
The production UpdatePrice path runs an on-chain secp256k1 verification of signed Chainlink Data Streams reports, which the simulated ledger cannot bypass. Halborn built an off-ledger generator that produces genuinely signed V3 reports for a signer key the test registers in its own verifier configuration (fault tolerance zero, so a single signature is required), and ABI-encodes the report exactly as the on-chain parser expects. This unblocked end-to-end proofs through the real verifier for:
the feed-key mismatch (a price stored under the raw report feed id is unreadable through a human-readable key);
the deviation freeze (a sustained divergent move never corroborates, freezes the active price, and then ages out, blocking all reads);
the pending buffer having no expiry (an ancient pending value corroborates a much later report);
the missing report-version check (a non-version-three report is accepted);
the timestamp rollback (an older still-recent report overwrites a newer price).
Negative controls. To prove the verifier is genuinely enforced and the oracle proofs are not passing because verification is a no-op, two control reports are submitted from the same valid baseline: one whose payload is tampered (the signature covers a different price than the shipped data) and one signed by a key not registered in the configuration. Both are rejected, confirming real signature checking underneath the oracle-logic proofs.
Selected flows were also executed against a live Canton sandbox (static-time mode for time-advancing scenarios) to confirm that the behavior observed on the in-memory script ledger holds on a real Canton participant.
The campaign confirmed the reported solvency, accounting, oracle, liquidation, and risk-control findings with executable evidence, and refuted several leads as safe (for example, the position-registry double-count, the omission of a debt reserve from the valuation set, the index-projection parity, and the borrow-capacity health guarantee). The conservation fuzzer surfaced no new non-decaying-invariant violation beyond the documented holding-fee and re-basing findings.
The proof-of-concept and fuzzing campaign demonstrate the presence of the reported issues; like all dynamic testing, a clean run on untested input combinations does not prove the absence of other issues.
The simulated ledger does not enforce Canton's per-transaction node limits, so the position-bloat denial of service is demonstrated as unbounded order-N growth in the liquidation recompute rather than as a hard transaction-build failure at a specific count.
Behaviors that depend on the live Chainlink signer set, the production verifier configuration, or real Canton Coin holding-fee mechanics were reasoned about and, where possible, modeled, but not exercised against production infrastructure.
| EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL VALUE |
|---|---|---|
| Attack Origin (AO) | Arbitrary (AO:A) Specific (AO:S) | 1 0.2 |
| Attack Cost (AC) | Low (AC:L) Medium (AC:M) High (AC:H) | 1 0.67 0.33 |
| Attack Complexity (AX) | Low (AX:L) Medium (AX:M) High (AX:H) | 1 0.67 0.33 |
| IMPACT METRIC () | METRIC VALUE | NUMERICAL VALUE |
|---|---|---|
| Confidentiality (C) | None (C:N) Low (C:L) Medium (C:M) High (C:H) Critical (C:C) | 0 0.25 0.5 0.75 1 |
| Integrity (I) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
| Availability (A) | None (A:N) Low (A:L) Medium (A:M) High (A:H) Critical (A:C) | 0 0.25 0.5 0.75 1 |
| Deposit (D) | None (D:N) Low (D:L) Medium (D:M) High (D:H) Critical (D:C) | 0 0.25 0.5 0.75 1 |
| Yield (Y) | None (Y:N) Low (Y:L) Medium (Y:M) High (Y:H) Critical (Y:C) | 0 0.25 0.5 0.75 1 |
| SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
|---|---|---|
| Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
| Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
| Severity | Score Value Range |
|---|---|
| Critical | 9 - 10 |
| High | 7 - 8.9 |
| Medium | 4.5 - 6.9 |
| Low | 2 - 4.4 |
| Informational | 0 - 1.9 |
Critical
0
High
1
Medium
9
Low
16
Informational
4
| Security analysis | Risk level | Remediation Date |
|---|---|---|
| Holding-Fee Decay Is Recorded as Protocol Revenue, Letting Treasury Withdrawals Drain Supplier Funds | High | Solved - 06/23/2026 |
| Position Unification Corrupts totalDeposited Accounting, Causing Withdrawal and Liquidation Reverts | Medium | Solved - 06/23/2026 |
| Liquidating a Deeply-Underwater Position Lowers Its Health Factor, Accelerating Bad Debt | Medium | Solved - 06/23/2026 |
| Oracle UpdatePrice Trusts a Caller-Supplied Verifier/VerifierConfig, Letting the Price Pusher Forge Arbitrary Prices | Medium | Solved - 06/23/2026 |
| Holding-Fee Decay Can Drive the Liquidity Index Non-Positive, Permanently Bricking a Reserve | Medium | Solved - 06/23/2026 |
| Oracle Feed-Key Mismatch Between the Chainlink Path and Reserve Configuration Disables All Price Reads | Medium | Solved - 06/23/2026 |
| CIP-56 TransferFactory Silently Skips the Whitelist/Compliance Check | Medium | Solved - 06/23/2026 |
| Deviation-Confirmation Delay Serves the Pre-Move Price During Crashes; pendingPrices Has No Expiry | Medium | Solved - 06/23/2026 |
| Unbounded UserPosition Lists Enable a Position-Bloat Liquidation Denial of Service | Medium | Solved - 06/23/2026 |
| Unvalidated freshReserveHoldingCids Lets a Caller Orphan Reserve Holdings, Desyncing totalLiquidity and Stranding Funds | Medium | Solved - 06/23/2026 |
| RefreshHoldings and ConsolidateReserveHoldings Advance the Clock Without Accruing Interest | Low | Solved - 06/23/2026 |
| Borrow-Index Taylor Approximation Under-Charges Borrowers and Is Accrual-Cadence Dependent | Low | Solved - 06/23/2026 |
| A Single Stale Basket Feed Aborts the Health-Factor Recompute and Blocks Liquidation | Low | Solved - 06/23/2026 |
| Scaled-Total Counters Are Silently Clamped to Zero, Masking Accounting Underflows | Low | Solved - 06/23/2026 |
| UpdateOracleCid Retargets the Pool to an Unvalidated Oracle, Enabling Hot-Key Denial of Service | Low | Solved - 06/23/2026 |
| Risk Parameters Can Be Re-Pointed/Tightened Instantly and RefreshHoldings Does Not De-Duplicate Holdings | Low | Solved - 06/23/2026 |
| Reserve Deactivation (isActive=False) Does Not Remove Collateral Power, and Collateral in a Deactivated Reserve Cannot Be Disabled | Low | Solved - 06/23/2026 |
| UpdatePrice Lacks Observation-Timestamp Monotonicity, Allowing an Older Report to Overwrite a Newer Price | Low | Solved - 06/23/2026 |
| Bad-Debt Write-Off Does Not Haircut the Liquidity Index, Creating a Bank-Run Loss Distribution | Low | Solved - 06/23/2026 |
| Missing liquidityIndex Positivity Guard in UpdateRiskParams/UpdateInterestRateParams Persists a Non-Positive Index | Low | Solved - 06/23/2026 |
| Unbounded reserveHoldingCids Growth Can Block Borrow and Withdraw Operations (DoS) | Low | Solved - 06/23/2026 |
| LiquidateTS Lacks a Collateral-Pool Liquidity Check, Allowing Liquidations to Be Griefed via Collateral-Asset Illiquidity | Low | Solved - 06/23/2026 |
| CIP-56 TransferFactory Does Not Validate Per-Input Instrument Identity | Low | Solved - 06/23/2026 |
| Pool Does Not Re-Validate TransferFactory Output Holdings Before Updating Accounting | Low | Solved - 06/23/2026 |
| MigrateRecordBorrow Silently Floors totalLiquidity at Zero | Low | Solved - 06/23/2026 |
| EnableCollateral and DisableCollateral Do Not Verify the Passed Reserve Matches the Deposit's Instrument | Low | Solved - 06/23/2026 |
| UserPosition Registry Append Choices Do Not Enforce CID Uniqueness | Informational | Solved - 06/23/2026 |
| allowManualPrice Enables Unbounded Manual Price Setting by the Oracle Pusher | Informational | Solved - 06/23/2026 |
| Legacy Migration Creates Positions With No On-Chain Proof of the Mirrored Custody | Informational | Solved - 06/23/2026 |
| Unused/Dead Code and Missing Report-Version Check in the Oracle Decoder | Informational | Solved - 06/23/2026 |
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
Smart Contract Assessment
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed