iCKB Contracts Revisited: Old Code, New Audit

This post mirrors the original security review report published in ickb/contracts: any question or feedback is more than welcomed :hugs:

  • Review completion date: 2026-05-01.
  • Reviewed contracts commit: 454cfa9. This is the last commit that changed scripts/contracts/** or scripts/Cargo.toml.
  • Executable test evidence: current scripts/tests/** suite in this repository state.
  • Scope: iCKB Logic, Owned Owner, Limit Order, and the shared utils crate.
  • Cross-references: the iCKB whitepaper and Nervos L1 reference implementations.
  • Prior external audit: Scalebit (2024-09-10). That audit reported three issues: two informational and one minor.

Executive Summary

Under the current deployment assumptions, the review confirmed one live issue: the known Limit Order confusion attack.

  • iCKB Logic has a provenance-blind path: receiptless DAO-shaped outputs can later be treated as deposits, and a separately funded aggregate-deposit path can realize the split-vs-aggregate soft-cap spread.
  • Even so, the current iCKB Logic tests do not show theft, duplicated principal, or a standalone profit path beyond assets the caller already controls.
  • Limit Order remains vulnerable to phantom-order continuation, real-order stranding through fake match-state cells, and master rebinding when cloned or otherwise indistinguishable orders are cross-wired at mint or during match.
  • Owned Owner preserves its pairing rules under the current whole-transaction-binding lock model; the remaining weak-lock claim-reassignment cases stay at the integration boundary.
  • All other candidate issues are blocked paths, generic CKB model constraints, or boundary cases relevant only to future integrations.

Findings Summary

One known live issue remains in scope.

ID Status Component Finding
LO-01 Known Limit Order CKB does not execute output locks at creation time, and limit_order accepts swapped mint pairings, so phantom and cross-wired order/master lineages can survive into later match or melt flows.

Protocol Model and Assumptions

The component analyses below rely on the following protocol model and deployment assumptions.

iCKB is an inflation-protected CKB token (xUDT) that tokenizes NervosDAO deposits into a liquid, fungible asset. The protocol owns all CKB deposits in a shared pool. Users deposit CKB and receive iCKB; later, they can burn iCKB to withdraw from any mature deposit in that pool.

Three on-chain scripts govern that flow:

  1. iCKB Logic (dual-role): lock script on deposit cells, type script on receipt cells. Enforces the core balance equation and deposit-receipt accounting.
  2. Owned Owner: pairs withdrawal request cells with owner cells so users can claim NervosDAO phase 2 withdrawals.
  3. Limit Order: enables order-book-style exchange between CKB and UDTs, abstracting over NervosDAO and iCKB protocol constraints.

Key Flows

Deposit (two phases):

  • Phase 1: CKB locked into NervosDAO deposit cells (lock = iCKB Logic, type = DAO). Receipts (type = iCKB Logic) track the deposits.
  • Phase 2: Receipts converted to iCKB xUDT tokens using the deposit block’s accumulated rate (AR).

Withdrawal:

  • Phase 1: iCKB burned to release deposits from the pool into NervosDAO withdrawal requests.
  • Phase 2: Standard NervosDAO withdrawal (outside iCKB scope).

Mixed transactions: A single transaction can combine deposit phase 1, phase 2, and withdrawal operations.

The next four execution rules explain why later findings hold or fail.

Script Grouping

Mixed transactions execute iCKB Logic twice, but both runs see the same global cells and apply the same checks, so the dual execution does not create a desync path. CKB groups scripts by both hash and role. From script/src/types.rs:179-180:

A cell can have a lock script and an optional type script. Even they reference the same script, lock script and type script will not be grouped together.

Lock groups and type groups are stored in separate BTreeMaps (types.rs:657-659). iCKB Logic uses the same {code_hash, Data1, empty_args} for both roles, so a mixed transaction produces one lock group for deposit cells and one type group for receipt cells.

Both groups still see the same global cell set (Source::Input and Source::Output, not group-local sources) and apply the same balance checks, so duplicate execution does not create a path where one run succeeds and the other fails.

Script group construction at types.rs:716-740: lock groups are built from input locks only; type groups are built from both input and output types.

xUDT Owner Mode

xUDT owner mode stays within iCKB accounting because, in the deployed configuration, only two owner-mode routes are live and both co-execute iCKB Logic.

iCKB xUDT args are built as [ickb_logic_hash, XUDT_ARGS_FLAGS] with XUDT_ARGS_FLAGS = 0x80000000.

RFC 0052’s owner-mode update has four triggers: matching input lock, matching input type, matching output type, and a witness owner_script whose hash matches the owner hash in args.

In the deployed configuration, the upstream xudt_rce.c flag parsing enables input lock by default, enables input type when flags & 0x80000000 is non-zero, and enables output type only when flags & 0x40000000 is non-zero. It also falls back to the witness owner_script path.

The witness owner_script path requires an exported validate symbol loaded via ckb_dlsym, while iCKB Logic is built as a CKB entrypoint (program_entry via ckb_std::entry!), not an xUDT validate extension. Under iCKB’s deployed args [ickb_logic_hash, 0x80000000], the live owner-mode paths are:

  • A receipt (input type = iCKB Logic): needed for phase 2 minting
  • A deposit (input lock = iCKB Logic): fires during withdrawal

In both cases iCKB Logic co-executes, as a type script for receipts or a lock script for deposits. Under this design, xUDT cannot enter owner mode without iCKB Logic also running.

NervosDAO Interaction

The deposit phase 1 section of the iCKB whitepaper pins NervosDAO to the historical 814eb82 dao.c. That version enforces the known 64-output-cell limit with an output_withdrawing_mask bitset. iCKB inherits that as a platform constraint, but its own correctness does not depend on the limit.

Key NervosDAO constraints confirmed against the whitepaper-pinned dao.c:

Header Access

Header access explains why iCKB uses a two-phase deposit flow. From script/src/syscalls/load_header.rs:

  • For Source::Input: returns the header of the block containing the cell’s creation transaction. The block hash must appear in header_deps.
  • For Source::Output: always returns INDEX_OUT_OF_BOUND (load_header.rs:80). Deposits require two phases.

Two shared boundaries matter in the later sections: authorization and accounting.

Authorization Boundary

Several candidate issues turn on the boundary between lock-script authorization and type-script accounting. The lock script / type script split means ownership is enforced by the user lock, while ickb_logic enforces value conservation. Its balance equation and cell classification never inspect who receives the output iCKB or the phase-1 DAO claim.

Under the current whole-transaction-binding user-lock assumption, that division is acceptable because the user’s signature commits to the full transaction. The deployment model used for this review assumes current user-facing iCKB flows use strong transaction-binding locks and does not treat delegated or adopted OTX flows as in-scope present-day integrations.

The weak-lock tests in this report do not replay a current wallet path. They model future or custom delegated, OTX, or other non-output-binding integrations, where recipient binding is an integration invariant rather than a guarantee provided by the iCKB contracts.

Executed regressions show why that stays a boundary case rather than a current finding:

The same ancillary scripts section already warns that delegated and OTX ownership patterns have their own pitfalls.

Accounting Basis and Build Setting

The main accounting claims in this report rest on three properties:

  1. Balance equation: entry.rs:32 enforces in_udt + in_receipts == out_udt + in_deposits.
  2. Deposit-receipt accounting: entry.rs:132-137 requires deposited == receipted per amount bucket.
  3. Overflow checks: overflow-checks = true keeps the release-build accumulators from silently wrapping.

Together, these preserve protocol accounting across deposit, conversion, withdrawal, and mixed transactions.

Scope and Methodology

This section pins the reviewed sources and the checks behind the conclusions.

Reference Repositories

Methodology

The review prioritized executable behavior over static plausibility and classified issues by their concrete impact.

  • Reviewed the deployed release binaries and the transaction semantics they actually enforce, not just the latest source-level intent.
  • Reproduced script behavior locally with ckb-testtool, using executable transaction tests rather than relying on inspection alone.
  • Reused and extended the existing test suite, including replayed transaction shapes from observed protocol flows.
  • Computed NervosDAO accounting with the exact DAO withdrawal math before classifying any claim-path discrepancy as a finding.
  • Separated findings that affect the current deployment from weak-lock assumptions, operator mistakes, or broader integration-only scenarios.
  • Removed or downgraded candidate issues that did not yield a harmful state transition, exploitable profit path, or realistic user loss.
  • Explicitly tested mixed transactions and script-group interactions because iCKB safety depends on whole-transaction execution rather than isolated cell intent.

Test Coverage

A module wiring overview plus the test layout note show three layers:

A fresh cargo test -p tests run passed 214 tests with no failures. Those tests cover deployment-hash sanity checks, helper encodings, core flows, blocked-path regressions, and replayed transaction shapes across iCKB Logic, Owned Owner, and Limit Order.

The strong-lock regressions in ickb_logic, owned_owner, and signing replay secp256k1_blake160_sighash_all as the representative whole-transaction-binding lock. This repo does not include a QRL fixture or harness, so conclusions for other strong-lock deployments rely on the same binding property and the stated deployment assumptions rather than a separate in-repo replay.


Limit Order

The deployed Limit Order binary still carries one live issue: the known confusion attack. No other candidate path in this component validates as a live issue.

Design

Limit Order supports three operations: Mint (create order plus master), Match (partial or full fill), and Melt (destroy order plus master). Orders store exchange ratios (ckb_to_udt, udt_to_ckb) and a minimum match size.

Key Validations

Value conservation (entry.rs:103-105): uses C256 (checked U256) to prevent overflow.

if i.ckb * ckb_mul + i.udt * udt_mul > o.ckb * ckb_mul + o.udt * udt_mul

Concave ratio check (entry.rs:233-235): ensures round-trip conversion doesn’t lose value for the maker:

if c2u.ckb_mul * u2c.udt_mul < c2u.udt_mul * u2c.ckb_mul

Minimum match enforcement (entry.rs:108-129): prevents dust-level partial matches.

Strict data length on execution (entry.rs:166): when limit_order executes, order data must be exactly UDT_SIZE + ORDER_SIZE bytes, so trailing data fails validation.

Assessment

Executed tests confirm several live confusion manifestations under the current deployment assumptions, and separate tests bound the rebinding path.

ID Status Finding
LO-01 Known CKB does not execute output locks at creation time, and limit_order’s mint branch accepts swapped mint pairings, so mint-time output creation can seed phantom or cross-wired order/master lineages that later pass match or melt validation.

Confirmed live manifestations:

Bounding evidence: separate tests show that the rebinding path is narrower than arbitrary master rewriting:

The UDT → CKB zero-UDT fulfilled-order shape still fails at the outer InvalidMatch check rather than surfacing a dedicated fulfilled-order guard.


iCKB Logic

The deployed iCKB Logic binary is provenance-blind at cell classification, but once a cell is inside the scripted flow it still preserves receipt and xUDT accounting. The subsections below explain both properties and why the currently executed provenance-blind paths still stop short of a confirmed theft or profit finding.

Core Invariant

entry.rs:32:

if in_udt_ickb + in_receipts_ickb != out_udt_ickb + in_deposits_ickb {
    return Err(Error::AmountMismatch);
}

  • Deposit phase 1: 0 + 0 == 0 + 0 (deposits and receipts handled by accounting check only)
  • Deposit phase 2: 0 + receipt_value == out_udt + 0
  • Withdrawal: in_udt + 0 == out_udt + deposit_value
  • Mixed: any combination

Cell Classification

celltype.rs:60-82 classifies every cell in the transaction by examining lock and type script hashes, plus the DAO data shape when the DAO hash is present:

Lock Type Classification Line
iCKB Logic DAO deposit (8 zero bytes) Deposit L69
iCKB Logic anything else ScriptMisuse (error) L72
other iCKB Logic Receipt L75
other iCKB xUDT Udt L78
DAO deposit (8 zero bytes) as lock any ScriptMisuse (error) L62
iCKB xUDT as lock any ScriptMisuse (error) L63
other other Unknown (ignored) L81

ScriptType::None is only synthesized for missing type scripts; script_type() never returns None for locks, so the (ScriptType::None, _) arm is unreachable. NervosDAO withdrawal requests, which have non-zero data, correctly classify as Unknown via is_deposit_data.

Deposit-Receipt Accounting (check_output)

entry.rs:86-140 uses a BTreeMap<u64, Accounting> to ensure every group of same-sized output deposits is matched by equal receipt counts.

It also validates:

iCKB Conversion (deposit_to_ickb)

entry.rs:71-84 uses the same conversion for minting and withdrawal:

let ickb_amount = amount * ar_0 / ar_m;
if ickb_amount > ICKB_SOFT_CAP_PER_DEPOSIT {
    return Ok(ickb_amount - (ickb_amount - ICKB_SOFT_CAP_PER_DEPOSIT) / 10);
}

  • Division by zero: impossible (the RFC 0023 accumulated-rate rule sets AR_0 = 10 ^ 16 and AR_i = AR_{i-1} + floor(AR_{i-1} * s_i / C_{i-1}), so AR_m is non-zero).
  • Overflow: u64 * u128 fits in u128 (~1.8e19 * 1e16 = ~1.8e35 < 3.4e38).
  • Precision: integer division loses at most 1 shannon per operation.
  • Fee/discount symmetry: the same function is used for both input receipts (fee, entry.rs:58-59) and input deposits (discount, entry.rs:52). The protocol breaks even: minted amount = burned amount.

has_empty_args Validation

Cell identification relies on the exact deployed script hash. utils/utils.rs:14-36 enforces empty args on:

  • The current script instance (covers input lock, input type, output type via CKB’s execution model)
  • All output locks matching the same code_hash + hash_type (utils.rs:29-31)

Output types don’t need explicit checking because they trigger execution and self-validate.

Assessment

iCKB Logic has one state-admission blind spot plus several boundary points:

On current executable evidence, that provenance-blind path is real, but it still falls short of a confirmed theft or standalone profit finding.


Owned Owner

The deployed Owned Owner binary preserves its pairing invariant under the current assumptions. The remaining questions concern which cells can be paired and who controls the pair at creation time.

Design

Owned Owner pairs owned cells, namely NervosDAO withdrawal requests, with owner cells through a MetaPoint derived from the owner cell’s owned_distance field:

  • Mint: the owner cell stores a signed distance to the owned cell, so owned_index == owner_index + owned_distance.
  • Melt: both cells must be consumed together.

Validation

For inputs and outputs separately, the script enforces the same pairing rules:

  • Each metapoint must have exactly owned == 1 and owner == 1 (entry.rs:57-60).
  • Owned cells must be NervosDAO withdrawal requests: DAO type + non-zero data (entry.rs:45-47).
  • A cell using the script as both lock and type is rejected (entry.rs:53).

Assessment

Owned Owner leaves four boundary points. The live claim-rotation path stays blocked in the currently modeled flows:


Deployment Context and Documented Risks

Witness Malleability (Documented)

Witness malleability is a documented property, not a new finding. All three scripts use the script-as-lock (unsigned) plus script-as-type (controller) pattern, and none of them reads witnesses.

The whitepaper states the consequence directly: “if a script in a transaction needs to store data in the witness and this data can be tampered without the transaction becoming invalid, then this transaction must not employ the scripts presented in the current whitepaper.”

Non-Upgradable Deployment

The reviewed deployment is non-upgradable under the observed script references.

Under RFC 0022 and RFC 0032, the deployed scripts in transactions use hash_type = data1. Referencing scripts locate code from cell_deps by cell data hash and run it on CKB VM v1. That reference mode pins validation to the exact deployed binary bytes.

This pinning is not a property stored in the binary cell itself. It is a property of how scripts in transactions refer to that cell.

By contrast, hash_type = type would locate code by type-script hash, allowing a replacement code cell with the same type script and different contents, with upgrade policy then governed by that code cell’s lock script. Separately, the published binary cells themselves are locked with a secp256k1_blake160 zero lock, an unspendable lock, so no trusted operator key remains as an owner of the binaries.

Under the hash_type = data1 reference mode, fixing a post-deployment bug would require migration to new script deployments and a new dep group.


Conclusion

Under the current deployment assumptions, only the known Limit Order confusion attack remains live.

The iCKB Logic provenance-blind path exists, but the executed tests still stop at a self-funded edge case.

The remaining Owned Owner and cross-script cases are blocked or confined to integration/deployment boundaries.

3 Likes

Appendix: Scenario Analysis

The appendix records the case-by-case traces that support the component assessments and the findings summary.

Class 1: Token Inflation (minting iCKB without backing)

These scenarios test whether iCKB can be created without the corresponding deposit-side accounting.

1A. Direct xUDT minting bypass

Attack: trigger xUDT owner mode without iCKB Logic validation.

Trace: under iCKB’s deployed [ickb_logic_hash, XUDT_ARGS_FLAGS] with XUDT_ARGS_FLAGS = 0x80000000, xUDT owner mode has two live iCKB routes: a matching input type for receipts and a matching input lock for deposits (RFC 0052, xudt_rce.c).

The only other upstream owner-mode route is the witness owner_script fallback, but that path looks up the exported validate symbol via ckb_dlsym. iCKB Logic is built as a CKB entrypoint instead of an xUDT extension. In the two live cases, iCKB Logic co-executes, so the balance equation still applies. The output-witness owner-script fallback test and the input-witness owner-script fallback test reproduce that attempted route and still fail under xUDT amount checks.

Result: Blocked.

1B. Forge a high-value receipt

Attack: create a receipt claiming more deposits than actually exist.

Trace: check_output accounting requires deposited == receipted per amount in a BTreeMap. The over-counted receipt test shows that a receipt cannot claim more same-sized deposits than the transaction actually creates.

Result: Blocked.

1C. Inflate receipt value via AR manipulation

Attack: make a receipt appear to be from an older block (lower AR = higher iCKB value).

Trace: extract_accumulated_rate calls load_header, which reads the block hash from CellMeta.transaction_info.block_hash and only succeeds when that block is also present in header_deps. The transaction creator cannot override that linkage.

Result: Blocked.

1D. Create receipt without prior deposits

Attack: consume a “receipt” that was never backed by actual deposits.

Trace: the receipt uses iCKB Logic as its type script. Creating it as an output triggers iCKB Logic, which validates deposit-receipt accounting. CKB’s UTXO model then makes the created cell immutable. No valid receipt can exist without corresponding deposits.

Result: Blocked.

Class 2: Token Theft (unauthorized withdrawal)

These scenarios test whether pool deposits can be released without paying the corresponding iCKB cost.

2A. Withdraw deposits without burning iCKB

Attack: consume a deposit cell without providing sufficient iCKB.

Trace: the balance equation (entry.rs:32) requires in_udt + in_receipts == out_udt + in_deposits. With no UDT or receipts on input: 0 + 0 != 0 + deposit_value → AmountMismatch.

Result: Blocked.

2B. Bypass iCKB Logic during deposit consumption

Attack: consume a deposit cell through NervosDAO directly, without iCKB Logic running.

Trace: deposits use iCKB Logic as their lock. CKB always executes input lock scripts (the lock-script rule), so there is no way to consume the input without triggering that lock.

Result: Blocked.

2C. Exploit cell_dep for value extraction

Attack: reference a deposit as a cell_dep to extract data without consuming it.

Trace: cells used in cell_deps remain live and are not considered dead like inputs. CKB executes lock scripts for inputs and type scripts for inputs and outputs, so referencing a deposit as a cell dep neither consumes it nor triggers its scripts.

Result: No attack vector.

Class 3: Economic Exploitation

These paths matter only if they create a repeatable profit or a material pool imbalance.

3A. Oversized deposit fee/discount arbitrage

Attack: deposit oversized, get iCKB with a 10% fee, then immediately withdraw the same deposit with a 10% discount.

Trace: both fee and discount use the same deposit_to_ickb function with the same parameters. For a deposit at block M:

  • Phase 2 mints: deposit_to_ickb(amount, AR_m) = X iCKB
  • Withdrawal costs: deposit_to_ickb(amount, AR_m) = X iCKB

Net profit = 0.

Result: No arbitrage. Fee and discount are symmetric by construction.

3B. Cherry-pick cheapest deposits

Attack: scan the pool for deposits that require the least nominal iCKB to withdraw and then withdraw those deposits first.

Trace: deposit_to_ickb computes amount * AR_0 / AR_m. RFC 0023’s maximum-withdrawable-capacity formula scales the later DAO claim by AR_n / AR_m, and the whitepaper’s exchange-rate calculation values the same deposit from block m at 100000 CKB * 10 ^ 16 / AR_m iCKB, excluding occupied capacity.

Deposits with higher AR_m require less nominal iCKB because the formula values them lower in iCKB, not because the pool applies a discount.

Result: No mispricing. Each deposit is priced by the same formula, up to integer-division rounding.

3C. Integer rounding exploitation

Attack: create many small deposits to accumulate rounding errors.

Trace: the rounding mint test and the rounding withdrawal test show that the reported whole-CKB or rounded claims fail with AmountMismatch unless the exact shannon-precision value is used. The supposed extraction path does not validate on chain.

Result: Blocked by exact shannon-precision accounting.

Class 4: Data Manipulation

These cases test whether malformed cells can enter the accounting flow with misleading metadata.

4A. Receipt with zero deposit_quantity

Trace: entry.rs:114-116 rejects zero-quantity receipts. The zero-quantity receipt test shows the output creation failure directly, so input receipts with zero quantity cannot exist as live cells.

Result: Blocked.

4B. Receipt amount without matching deposit

Trace: check_output uses a BTreeMap keyed by deposit_amount. The over-counted receipt test shows that a receipt cannot claim more same-sized deposits than the transaction actually creates, and the unmatched receipt-amount test shows that a receipt amount with no matching deposit bucket yields ReceiptMismatch.

Result: Blocked.

4C. Cell with iCKB Logic lock + non-DAO type

Trace: once iCKB Logic executes, celltype.rs:72 rejects (ScriptType::IckbLogic, _) with ScriptMisuse. The lock-only non-DAO output test shows that such an output can still be created because output locks do not execute at creation time, but spending it later fails with ScriptMisuse.

Result: Cannot be spent successfully.

4D. NervosDAO withdrawal request classified as deposit

Trace: is_deposit_data checks for exactly 8 zero bytes. Withdrawal requests have non-zero data that stores the block number, so they fall through to ScriptType::Unknown and then CellType::Unknown.

Result: Blocked.

4E. Deposit capacity manipulation

Attack: inflate extract_unused_capacity by manipulating cell fields.

Trace: CKB requires occupied capacity to fit within cell capacity, and extract_unused_capacity subtracts that VM-computed occupied capacity from the actual cell capacity. For the standard iCKB deposit shape, the whitepaper’s exchange-rate calculation uses c_o = 82 CKB. That occupied capacity is fixed by the deposit structure, not chosen independently by the attacker.

Result: Blocked.

4F. Deposit interest/maturity reset (whitepaper#18)

Attack: consume an iCKB deposit and create a new deposit at the same output index, effectively resetting the deposit’s age and accrued interest in the pool. This would lower pool-wide returns at minimal cost.

Trace: the specific same-index reset described here is blocked by NervosDAO phase 1. The receiptless aggregate-deposit variant is a different claim.

  1. NervosDAO blocks it: when a DAO deposit is consumed, validate_withdrawing_cell requires the output at the same index to be a withdrawal request with non-zero data containing the deposit block number. A new deposit with 8 zero bytes fails the stored_block_number != deposit_header.block_number check because genesis block number 0 does not match the real deposit block. The transaction is rejected.

  2. By contrast, the broader provenance-blind path is a different claim. The receiptless deposit-admission test shows that ickb_logic later accepts a receiptless DAO-shaped output as a structurally valid deposit input once it exists.

The delta-only spread test and the oversized spread test show that the contract accepts a self-funded soft-cap spread, while the deposit-alone spread block test blocks the deposit-only variant.

The narrowing comment in the phase-2 claim test and the self-funded principal claim test make the limit explicit: the executed path still does not prove receipt-backed-principal theft or duplicated recovery.

So the specific “same output index” reset is blocked by NervosDAO. The receiptless aggregate-deposit variant stays a self-funded provenance-blind edge case rather than a confirmed double-claim exploit.

Result: Blocked for the same-index reset described here. The receiptless aggregate-deposit path exists, but the current tests still stop short of proving receipt-backed-principal theft or duplicated recovery.

Class 5: Cross-Script Interactions

These scenarios check whether behavior that is safe in isolation breaks once multiple scripts share a transaction.

5A. Witness malleability during withdrawal (whitepaper#22, credit: @XuJiandong)

Attack: tamper with witness data for the iCKB Logic lock group (unsigned lock). All three witness fields in the group (lock, input_type, output_type) are malleable because iCKB Logic does not use signature-based verification.

Trace: iCKB Logic does not read witnesses. NervosDAO does read input_type from the witness to locate the deposit header (dao.c:63-98). If an attacker tampers with that header index, NervosDAO later compares the claimed deposit block number with the actual deposit header and fails at deposited_block_number != deposit_data.block_number.

The iCKB malformed witness test and the Owned Owner malformed witness test reproduce that malformed header-index witness failure path in iCKB Logic and Owned Owner withdrawal flows.

The whitepaper’s unsigned-lock-witnesses section already documents that general malleability risk and warns against combining these scripts with witness-dependent logic.

Result: Can cause transaction failure (griefing), cannot cause fund loss. This is a liveness issue, not a safety issue. Documented in the whitepaper.

5B. Separate lock/type group execution desync

Attack: exploit dual execution in the hope that one run passes while the other fails.

Trace: CKB requires ALL script groups to pass (the verifier loop returns an error if any verify_script_group(...) call fails). Both lock and type executions see identical cells at absolute indices and perform the same balance checks. If either fails, the transaction is rejected.

Result: Blocked.

5C. Non-empty-args iCKB Logic variant

Attack: create a cell with {iCKB Logic code_hash, Data1, [0x01]} as type, hoping to bypass validation.

Trace: different args produce a different script hash, so the cell is not recognized by script_type(). Even if it executes as its own type group, it still fails has_empty_args(). The non-empty-args poisoning regressions in phase2_sibling_classification.rs cover both output-sibling and input-sibling variants.

Result: Blocked.

5D. Orphaned Owned Owner cell

Trace: owned_owner/entry.rs:57-60 requires every metapoint to have exactly owned==1, owner==1. The orphan-owner rejection test shows that an orphan type-script owner cell is rejected immediately. A separate lock-only orphan case can still be created at output-lock creation time and later strand, as shown by the orphan withdrawal-request dead-state test.

Result: Blocked when Owned Owner executes. Lock-only look-alikes can still be created and later fail.

5E. Limit Order value decrease

Trace: limit_order/entry.rs:103-105 enforces value conservation with C256 (checked U256). Any decrease is rejected.

Result: Blocked.

5F. Limit Order confusion attack

Trace: CKB builds lock groups only from input locks, while type groups come from both input and output types (types.rs:716-739). A limit-order cell is the lock-only (true, false) case in limit_order/entry.rs:48-56, so an attacker can create phantom order outputs with arbitrary master outpoints without running limit_order at creation time. One manifestation is a fake order that shares a real master outpoint; if a user later melts the wrong order, the real order becomes permanently stranded.

Result: Known vulnerability. Documented in whitepaper. The real-order stranding test confirms the later real-order stranding path on the deployed binary.

The whitepaper’s mitigation is front-end lineage checking from the original mint transaction. Under the deployed lock-only design, the chain does not validate phantom orders at creation time because the order cell is created as an output lock-only cell.

Class 6: Edge Cases

The remaining scenarios are boundary checks, platform constraints, or known economic limitations.

6A. Boundary values

Trace: the minimum deposit check uses < (entry.rs:101), so exactly 1000 CKB is allowed. The maximum uses > (entry.rs:104), so exactly 1M CKB is also allowed. The boundary-value regressions in deposit_bounds.rs cover both edges.

Result: Correct.

6B. u128 overflow in balance equation

Trace: overflow-checks = true is enabled in scripts/Cargo.toml. Overflow panics and rejects the transaction.

Result: Blocked (by the compiler setting and practical bounds).

6C. Division by zero in deposit_to_ickb

Trace: the RFC 0023 accumulated-rate rule sets AR_0 = 10 ^ 16 and AR_i = AR_{i-1} + floor(AR_{i-1} * s_i / C_{i-1}), so every deposit header has non-zero AR_m. deposit_to_ickb cannot divide by zero.

Result: Impossible.

6D. extract_unused_capacity underflow

Trace: CKB VM enforces capacity >= occupied_capacity for all cells. The subtraction at utils.rs:48 is safe.

Result: Impossible.

6E. Chain reorganization between deposit phases (whitepaper#15)

Attack: deposit phase 1 is included in block B. Before phase 2, a chain reorg removes B. The phase 2 transaction still references B’s header in header_deps, but B no longer exists in the canonical chain.

Trace: CKB consensus requires all blocks listed in header_deps to exist in the canonical chain (RFC 0022: “the transaction can only be added to the chain if all the block listed in header_deps are already in the chain (uncles excluded)”). If B is reorged out, then (a) the phase 1 deposit cell no longer exists as a live cell, so it cannot be referenced, and (b) the header_dep pointing to B is invalid. The phase 2 transaction is rejected.

Result: Blocked by CKB consensus rules. Reorgs cannot create phantom deposits.

6F. Busywork / pool dilution attack (whitepaper#8)

Attack: an attacker with large capital repeatedly deposits CKB in standard deposits and then withdraws, cycling through the pool. This shifts the maturity distribution: the remaining deposits are younger, so users must wait longer for a mature deposit.

Trace: the attack requires sustained capital commitment. Per the whitepaper analysis, controlling the first available epoch requires about 0.6% of pool capital, while controlling the first 3 days requires about 10%. With a 0.3% APR per 180 epochs, a user blocked for 1 epoch loses about 0.0017% interest.

The attacker earns nothing because the cycled CKB returns to them, still pays transaction fees, and must lock capital for 180 epochs per cycle. As the pool grows, the capital requirement rises proportionally while the impact per unit of capital falls.

Result: Requires sustained capital and yields less impact as the pool grows. This is a known limitation, not a vulnerability.

6G. Same-block receipt and deposit consumption

Attack: create a deposit plus receipt in TX1, then consume both in TX2 (receipt for phase 2 plus deposit for withdrawal) in the hope that the AR difference yields free iCKB.

Trace: both were created in the same block (TX1). extract_accumulated_rate returns the same AR for both. Since receipt_value == deposit_value, the balance equation yields 0 + receipt_value == out_udt + deposit_value → out_udt = 0. No net iCKB is minted.

Result: No profit.

6H. Race between receipt creation and deposit withdrawal

Scenario: a user creates deposit D plus receipt R in phase 1. Before the user performs phase 2, someone else withdraws D.

Trace: R still exists and is valid, and its iCKB value is fixed by the creation-block AR. D being consumed by someone else is expected pool behavior, so the user still converts R to iCKB normally in phase 2.

The protocol remains balanced because the withdrawer paid deposit_to_ickb(D) iCKB, and the receipt holder receives the equivalent receipt_iCKB_value(R).

Result: Pool accounting stays balanced.

3 Likes

Great update.
This appendix is almost exactly the kind of scenario map I would need before turning iCKB into a CellScript maturity benchmark. The attack-class structure, traces, expected results, and links to concrete tests make it much easier.

4 Likes