Repository navigation
Executable review follow-up: realistic fixtures, vector replay, report re-audit, CI - #21
Merged
Merged
Conversation
phroi
force-pushed
the
audit-follow-up
branch
from
September 26, 2026 13:08
36be348 to
1682aff
Compare
The suite wrote CKB amounts as `1_000 * SHANNONS`, which reads as 1,000 shannons although it means 1,000 CKB in shannons. The constant is now CKB, so the same amount reads `1_000 * CKB`. No value or behavior changes.
The stack's testkit checks its TypeScript contract oracle against golden vectors computed by the contracts' own Rust logic. The generator lived only as untracked files in a local checkout, so the stack's fixture could not be regenerated from any commit. vector-gen compiles c256.rs and ickb_logic/constants.rs straight from the contract sources and holds byte-exact copies of the private deposit_to_ickb and validate, which drift_checks asserts are still substrings of their sources. The cases and expected values live in src/vectors.rs so the contract tests can replay them; src/main.rs writes them as JSON. Its output parses to the same data as the stack's committed fixture. The crate stays outside the contracts workspace. Its lockfile uses format 3, since the pinned Rust 1.74 cannot read format 4.
ckb-testtool runs scripts only, so fixtures could build cells or withdrawals a node would reject and still pass. Many did: order cells were sized for 73 bytes of order data instead of 89, xUDT cells sat 26 CKB below their occupied capacity, owner, master and funding cells held a few hundred shannons, and named locks carried args longer than any real lock. What those tests showed depended on the fixture rather than on the scripts. Tests now verify through VerifyRealistic::verify. It asserts every input and output holds at least its occupied capacity, and, for transactions the scripts accept, the node's DaoScriptSizeVerifier rule that a deposit and its withdrawal request use locks of the same size. Self-tests show both checks fire. The fixtures follow: - typed cells take occupied_capacity(lock, type, data length); - CKB amounts written in shannons become CKB; - named locks take a secp lock's 20-byte args, so mainnet replay capacities fit; - DAO transactions whose outputs exceed their inputs gain a funding input, appended so input and output indices still pair; - foreign deposits wrapped by Owned Owner use a lock of its size. Existing tests now state what real cells show: - a terminal CKB->UDT fill passes at exact occupied capacity; - a fulfilled order continuation fails as InvalidMatch; - a lock-only order returns the typed MissingUdtType, where the undersized fixtures had trapped with -1; - two cloned live orders permuted in a match strand neither master; - the real-order stranding uses a foreign-token fake order; - the spread-path test follows one verified trajectory from receipt to claim.
The golden vectors had only ever been computed by copies of contract functions, never checked against the compiled scripts. The contract tests now include vector-gen/src/vectors.rs and replay its rows as real transactions: - each deposit vector mints exactly its expected iCKB at phase 2, while one shannon more or less fails with AmountMismatch; - each match vector gets the copied validate's verdict from the binary, which also parses the order the copies skip; - drift_checks runs with the suite, so a copy that stops matching its source fails here too. Two match rows shrink a fulfilled CKB->UDT order below its occupied capacity, which consensus forbids. They are skipped by name, so a new unconstructible row breaks the test instead of vanishing; this also records that AttemptToChangeFulfilled cannot be reached on chain. vector-gen's README records how to regenerate the stack's fixture, including the Prettier step that makes it byte-identical.
Each test backs a report conclusion that had no executable evidence: - receiptless deposits of 500 CKB and 0 CKB unoccupied are created without the deposit bounds, which iCKB Logic checks only when a receipt makes it run, and are withdrawn at exactly their iCKB value; - withdrawing a 1,000 CKB deposit returns more CKB per iCKB than a 100k CKB one, because the claim also returns the occupied capacity the valuation leaves out; - under a non-genesis AR, ten shannons past the shifted soft-cap edge mint cap+9, which only normalization before the haircut yields; - a withdrawal request locked by iCKB Logic fails with ScriptMisuse, with a user-locked control; - withdrawing and re-depositing in one transaction verifies with the request at the deposit's index and fails with DAO error -20 when the two swap; - a deposit and receipt created in the same block and spent together mint nothing, while minting 1 fails with AmountMismatch; - a phase 2 conversion with 512 unrelated outputs stays within the cycle budget.
- Two owned withdrawal requests with a single owner cell fail with Mismatch, the reverse of the existing two-owners-for-one-request case. - Phase 1 consumes two noncontiguous inputs under the same secp lock, then phase 2 spends the signed owner cell and completes the DAO claim; a signing test shows the noncontiguous group signature verifies and a tampered witness does not. The signing helper is generalized from a contiguous input range to any input indices for it.
A master cell carries limit_order as its type, so the script runs at mint and pairs every order with exactly one master. One order with its master mints; two orders on one master fail with SameMaster; a master without an order and an order pointing at a cell that is no master fail with InvalidConfiguration. This bounds the Limit Order confusion attack: it needs order cells created in a transaction with no master, where the script does not run.
The report is updated to the tests added before it, and corrected where a re-audit found it wrong or unsupported. Contract-source citations stay pinned to the reviewed commit 454cfa9, whose code is deployed and immutable. Findings and conclusions: - LO-01 now names its cause: limit_order runs only when a transaction creates or spends a master or spends an order, so order cells created alone go unchecked. Phantom and fake orders come from there, and a fake order can strand a real one. Permuting identical orders and a minter pairing its own orders and masters are listed as harmless. - Script Grouping states that each iCKB script validates the whole transaction whenever it runs, so its only gaps come from transactions where it does not run. - Deposit bounds apply only to receipted deposits. The pool can hold deposits of any size, and value no minted iCKB matches is effectively burned. - The aggregate-deposit spread path costs its user, not the reverse. - Small deposits return their occupied capacity to the withdrawer, so smallest-first is optimal within the bounds and below the soft cap. - Scenarios 4F and 6G, iCKB-locked requests, one order per master and the unreachable AttemptToChangeFulfilled and DuplicatedMaster now cite tests or code. - Foreign DAO wrapping is limited by the node's lock-size rule for deposits since block 10,000,000. - Four narrowed claims: double execution, the node (not the VM) enforcing occupied capacity, the xUDT witness tests, and the verifier's scope. Citations: - The dead links to nervosnetwork/ckb@6730f80, a commit that does not exist, now pin v0.210.0 with re-derived lines. The DAO lock-size rule is equality. - The deployed NervosDAO source is identified through ckb-system-scripts v0.5.4. - Whitepaper links point at master, since both repositories evolve, and one quote follows the whitepaper's current wording. - The stack's connector filter, which exists only on its unmerged branch, is described without a link. - The 180-epoch cycle cites RFC 0023. - The executed test count is 230. The report's Markdown tables are realigned.
The report is an evolving review, revised since its first completion on 2026-05-01, so a single date in its name and header misdescribes it. It is now ICKB-Audit-Report.md with a review period of 2026-05-01 to 2026-09-26; the repository history records each revision. Both README links follow the rename.
phroi
force-pushed
the
audit-follow-up
branch
3 times, most recently
from
September 26, 2026 13:45
9962f64 to
7f4ea4c
Compare
phroi
force-pushed
the
audit-follow-up
branch
from
September 26, 2026 14:07
7f4ea4c to
e82ec73
Compare
The suite spelled out cells with multi-line CellOutput builders (932), inputs with CellInput builders (540), and each expected failure as verify, unwrap_err and assert_script_error (120). Deposit, UDT and receipt cells repeated the same create_cell shape about 140 times. cell(capacity, lock, type) replaces 930 of the cell builders; the two left measure occupied capacity or omit a capacity on purpose. input(out_point) replaces the 481 inputs without a since. fail(context, tx, code) replaces the 120 failure checks. create_deposit, create_udt and create_receipt build the recurring live cells at the sizes the fixtures already used. Needless double borrows are removed. The suite drops from 16,412 to 12,160 lines. No test changes what it checks. A temporary log of every verification, run before and after, recorded each transaction's inputs, outputs, data, since values, header deps and witness lengths, with random out points masked, together with its result. Of the 230 tests, 206 verify transactions: the 184 whose transactions are deterministic built identical ones, and the 22 that use random keys or hash-ordered headers returned the same results. As a control, changing one funding cell by 1 CKB makes that log differ.
The previous check stopped short of two node rules, so some passing fixtures described transactions a node would reject, and one rule was applied more strictly than the node does. - CapacityVerifier: outputs may not hold more than inputs, unless a DAO input is present and the DAO script checks compensation instead. Thirty tests minted, melted, matched or replayed vectors while creating CKB; each such transaction now gains a funding input, appended so existing input indices keep their meaning. The vector replay funds exactly the CKB each order gains. A signed phase 2 test now signs only its secp input group. - DaoScriptSizeVerifier exempts deposits committed before block 10,000,000, which the check ignored. It now honors that exemption. The two foreign-wrapping tests keep their original user locks, valid again for their legacy deposits. The self-test rejects a post-10,000,000 withdrawal and a control accepts a legacy one. Every assertion is unchanged. Two crosswire tests already accept either of two error codes, which vary between runs with the random out points. The report's scope statement and the tests README describe both rules, and the test count becomes 231.
Scenario 3B said withdrawing the smallest deposits first is optimal. Below the soft cap, the claim per iCKB burned is AR_n / AR_0 plus 82 CKB divided by the deposit's iCKB value, and that value depends on the deposit's accumulated rate as well as its unoccupied capacity. At a withdrawal AR of 1.3e16, a 1,050 CKB deposit made at 1.2e16 pays more per iCKB than a 1,000 CKB deposit made at 1.1e16. The report now says to withdraw the deposits with the smallest iCKB value first.
Nothing ran the suite outside a local checkout, so a change could break the tests or the vector generator unnoticed. The workflow runs the test crate, which executes the committed release binaries, checks their deployment hashes and replays the golden vectors. It also builds the vector-gen writer, which the suite does not compile. rustup takes the pinned Rust from scripts/rust-toolchain; the checkout action is pinned by commit, and the job has read-only permissions.
phroi
force-pushed
the
audit-follow-up
branch
from
September 26, 2026 14:24
e82ec73 to
be28246
Compare
Member
Author
|
LGTM!! Phroi %304 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR revisits our executable review of the deployed scripts. No contract code or release binary changes: only the tests, the golden-vector generator, the report and a CI workflow.
Tests
scripts/vector-gen. The suite replays every constructible vector against the release binaries.SHANNONSis renamedCKB, so1_000 * CKBreads as the amount it is.cell,input,fail,create_deposit,create_udt,create_receipt) replace the repeated builders, taking the suite from 16,412 to 12,160 lines. A before/after log of every verified transaction shows the refactor builds the same transactions with the same results.Report
limit_orderruns only when a transaction holds a master or spends an order, so order cells created alone go unchecked.master, since both repositories evolve.ICKB-Audit-Report.mdwith a review period, since it keeps evolving.CI
.github/workflows/tests.ymlruns on pull requests and pushes tomaster. It runscargo test --locked -p testswith the pinned Rust fromscripts/rust-toolchain, then builds the vector generator, which the suite does not compile. The checkout action is pinned by commit and the job has read-only permissions.From
scripts/,cargo test -p testspasses all 231 tests, and every commit passes on its own.Merging renames the report, so the whitepaper's link to
20260501-ICKB-Audit-Report.mdneeds its prepared update right after.