Repository navigation
feat: harden upgrades, metadata validation, and add WASM size benchmarking - #1240
Merged
Hexstar-labs merged 6 commits intoSep 28, 2026
Merged
Conversation
…es, Cargo.toml names, test imports, symbol compliance
…ull disbursement, over-withdrawal, fund-depletion boundary
…checked_mul_div_i128
…st/blacklist/emergency/limit combinations
- BrainTease#1169: admin-gated upgrade authorization, zero-address/zero-hash/timelock validation, once-only initialize, direct-upgrade bypass disabled, invariant docs and authorization tests (unauthorized callers, partial multisig, timelock execution) - BrainTease#1170: shared validation predicates (is_/require_ pairs, is_non_empty_string), centralized credential metadata validation on all write paths, completed is_metadata_valid (course name, grade, non-empty IPFS hash, expiry >= completion), NFT course-name/royalty rules, invalid+valid metadata tests - BrainTease#1171: scripts/wasm-size-benchmark.sh (19-contract inventory, isolated SDK-major builds, size + SHA256, baseline + regression detection, --contract/--threshold/--update, exit codes), nft dev-dependencies fix so the WASM builds, generated docs/contracts/wasm-size-baseline.md (nft = 44133 bytes)
|
@yachamdaniel1-alt Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…ing and market syntax - registry: add Debug derive to VerificationLevel; testbuild needs std extern crate; add std::vec::Vec in fuzz_tests; add testutils::Ledger import; replace removed ledger.set_timestamp() with ledger.with_mut(|l| l.timestamp = ...) (sdk21 API) - nft: budget.reset_unlimited() in the 120-mint over-limit test - market: fix 13 pre-existing comment-banner syntax errors (# -> //) so the crate at least parses - format: align imports/functions with rustfmt (0 introduced violations)
This branch had an error being deployed
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.
Summary
This PR addresses three related security, validation, and engineering-quality issues:
The changes harden the timelocked upgrade path end-to-end, move every metadata rule into the shared validation module (one source of truth for both the NFT and the credential contracts), and add a repeatable benchmark that records and diffs release WASM binary size for all 19 contracts.
Branch lineage: this branch is stacked on top of the pending #1172–#1175 branch (PR #1239), because that branch restores the shared-crate module declarations (
upgrade,validation,access,multisig, …) andSharedContractscaffolding the #1169/#1170 code builds on. Until PR #1239 merges, this PR's diff therefore also shows those commits; the changes belonging to this PR are exactly the 19 files listed under Files / Areas Changed, and the diff shrinks to those alone once #1239 lands.#1169 — Upgrade Authorization Security
Proposer authorization.
governance::propose_upgradekeepsproposer.require_auth(), requires a future voting end and a non-zero timelock, and now additionally rejects upgrade targets that are the zero address (both the all-zero contract strkeyC…and the all-zero account strkeyG…), so a proposal can never be pointed at nothing.Admin-only approval.
docs/contract-interfaces.mddocumentsapprove_upgradeas the admin approval gate, but the implementation took no caller at all — anyone could flipapprovedon a proposal that had merely finished voting.approve_upgrade(env, admin, upgrade_id)now takes the approving admin and enforcesaccess::require_adminbefore marking a passed proposal approved.execute_upgradestays permissionless exactly as documented, but only after the proposal is approved, the timelock has expired, and it has not already been executed.Zero-address rejection. Contract and account zero strkeys are validated at proposal time, so no upgrade proposal can target the zero address.
Initialization takeover prevention.
SharedContract::initializehad no once-only guard: any address could call it, become the stored admin, and then schedule and execute an upgrade — a complete takeover of the upgrade authority. It now refuses a second call with"Already initialized", matching the convention already used bygovernanceandcredential_metadata.Disabling the direct upgrade bypass.
SharedContract::upgradeswapped the WASM immediately for the admin, with no timelock, no pending record and no history — bypassing every documented invariant. The admin authorization check still runs first (unauthorized callers keep getting"Unauthorized: admin required"), but the swap itself now refuses with"Direct upgrade disabled: use schedule_upgrade then execute_upgrade".schedule → approve → timelock → execute flow. The enforced lifecycle is now exactly the one the docs describe: admin
schedule_upgrade→ voting passes → adminapprove_upgrade→ timelock expires → anyoneexecute_upgrade, withcancel_upgradeavailable to the admin before execution.Authorization checks inside the upgrade helpers. Every helper in
contracts/shared/src/upgrade.rs(schedule_upgrade,execute_upgrade,cancel_upgrade) now re-checksaccess::require_adminitself (defense in depth), so no entry point — present or future — can reach upgrade logic without the check.Zero WASM hash validation. Scheduling the all-zero 32-byte hash (which would replace the contract with nothing) is rejected with
"Invalid WASM hash".Timelock validation.
timelock_ledgers == 0would make an upgrade executable in the very ledger it was scheduled; a minimum of one ledger is now enforced ("Timelock must be at least 1 ledger").Documentation.
contracts/shared/src/upgrade.rscarries an invariants section plus a "Findings addressed (#1169)" list, anddocs/contract-upgrades.mdwas rewritten around the required timelocked flow, the CLI example for schedule/execute, and the authorization invariants.Tests. New coverage for: unauthorized direct schedule/execute/cancel, wrong-account and role-based (Instructor/Student) rejection, missing signature, module-level bypass of the entry-point checks, partial multisig scenarios (a 2-of-3
execute_proposalwith only 2 approvals leaves no upgrade state; a multisig co-signer still cannot schedule), immediate-upgrade bypass, re-initialization admin takeover, non-adminapprove_upgradeon an already-passed proposal, admin approval marking the proposal approved, execution before the timelock (rejected), execution after the timelock (succeeds), and double execution (rejected).#1170 — NFT/Credential Metadata Validation
Shared validation predicates.
brain_storm_shared::validationis now the single source of truth: every rule is oneis_*predicate plus onerequire_*wrapper that is literallyassert!(is_*(value), MESSAGE), so write paths and read paths can never disagree. Original error messages are preserved.is_non_empty_string. New shared predicate (require_non_empty_stringnow delegates to it) used for the fields that need presence rather than a format rule.Centralized metadata validation. New
validate_metadata_fields(course_name, grade, ipfs_hash)performsrequire_valid_course_name+require_valid_grade+require_non_empty_string(ipfs_hash)and is called from every credential metadata write path —issue_with_nft,store_metadataandupdate_metadata(which now loads the stored record first so it can validate the record's existing IPFS hash alongside the new values).NFT course-name and royalty validation.
nft::mint_course_nftreplaced its inline royalty assertion with the sharedrequire_valid_royalty_basisand gainedrequire_valid_course_name(it had no course-name rule at all).Validation of stored records through
is_metadata_valid.is_metadata_valid(env, credential_id)was not correct before. It now returnsfalsefor a nonexistent credential without panicking, and validates every stored field using the same shared predicates the write paths use: course name, grade, non-empty IPFS hash, andexpiry_timestamp >= completion_date.Missing-record handling. A credential id with no record yields
false(reader contract), not a panic.Expiry/completion-date validation. A credential whose
expiry_timestampprecedes itscompletion_dateis reported invalid, on both the write and the read side.IPFS hash validation. The IPFS hash is enforced to be present on every write path (it is the record's only content pointer) and is checked when reading; no hash-format rule was added, because fixtures across the repo legitimately use short placeholders.
Tests. New and updated tests in both contracts: empty/overlong/short course name and empty grade rejected on store and update, empty IPFS hash rejected on write,
validate_metadata_fieldsaccepting valid values, plus reader tests proving invalid records already in storage (bad course name, empty hash, expiry before completion, missing record) reportfalse, and the happy path still reportstrue.#1171 — WASM Binary-Size Benchmark
scripts/wasm-size-benchmark.sh(bash 3.2 compatible), the entry point for the benchmark.wasm32-unknown-unknown --release, and reports size and status for all of them.target/wasm-size-bench/(copiedCargo.lock+rust-toolchain.toml), because the repository workspace does not resolve (see Pre-existing Issues). Repository files are never modified by the script.--updatewritesdocs/contracts/wasm-size-baseline.md(committed with this PR), documenting toolchain, rustc version, profile flags, thresholds, per-contract status/size/hash and how to run locally.--threshold <pct>(default 10%).--contract <name>filtering: build and compare a single contract.0= no regressions,1= regression or status change,2= usage error (unknown contract, bad flag).contracts/nft/Cargo.tomllistedsoroban-sdk/testutilsunder[dependencies], which is a hardcompile_erroron thewasm32target, so the NFT contract could never be built or measured.testutilsmoved to[dev-dependencies](same21.5.0version, tests unchanged) so the release artifact builds and is included in the baseline (44133 bytes).docs/contracts/wasm-size-baseline.md.Verification
The following verification was actually performed:
rustfmt --edition 2021 --checkon every touched file, diffed against a pristine copy of the base revision → 0 formatting violations introduced by this work../scripts/wasm-size-benchmark.sh --update) → succeeded, baseline written../scripts/wasm-size-benchmark.sh) → succeeded, all deltas+0.0%,no regressions vs baseline, exit 0../scripts/wasm-size-benchmark.sh --contract nft) →ok 44133 bytes, exit 0.30000) →+47.1% !! size regression above 10%, exit 1 (baseline then restored).--contract nope) → exit 2,--helpprints usage.Unit tests were run in an isolated copy of the workspace (the repository workspace itself does not resolve — see below):
brain-storm-shared102 passed / 4 failed,brain-storm-governance50 / 8,brain-storm-nft38 / 3,credential_metadata40 / 9 — every remaining failure is on the pre-existing baseline list (the same failures exist before this branch; none were introduced here, and one pre-existing shared failure, re-initialization, is fixed by this PR).The repository has pre-existing workspace/SDK resolution issues. They were NOT changed as part of this PR.
Pre-existing Issues / Scope
Explicitly out of scope for #1169–#1171 and not modified by this PR:
contracts/marketpinssoroban-sdk 20.0.0(ed25519-dalek =2.0.0) while the rest of the workspace uses 21.x (^2.1.1), so no in-repocargocommand resolves. RootCargo.toml/Cargo.lockand all SDK versions are untouched.admin/events/oracle, governancevoting::tests::test_e2e_*+ vote-power benchmark, nft budget benchmarks, credential linkage/admin/compact/migrate) were not introduced by this PR — they reproduce on the base revision. The full repository test suite does not pass and is not claimed to.credential_metadatacontract-macro parameter limitation:issue_with_nftdeclares 12 parameters against the macro's maximum of 10 — a pre-existing defect; its signature is public API and was not changed.soroban-sdk/testutilsin production[dependencies](badges,buyback,dispute,liquidity_pool,registry,reputation),markethas a syntax error atsrc/lib.rs:9,credential_metadatahits the 12-parameter limit,integrationhas nocdylib.Files / Areas Changed
contracts/shared/src/upgrade.rs— module-level authorization, zero-hash / zero-timelock validation, invariants + findings documentation.contracts/shared/src/upgrade_tests.rs,contracts/shared/src/tests.rs— authorization, bypass, multisig, timelock and takeover tests.contracts/shared/src/lib.rs— once-onlyinitialize, directupgraderefusal, wrappers that only delegate.contracts/shared/src/validation.rs—is_*/require_*predicates,is_non_empty_string.contracts/governance/src/lib.rs— zero-address target check, admin-gatedapprove_upgrade, approval/execution tests.contracts/nft/src/{lib.rs,tests.rs}— shared course-name and royalty validation, rejection tests.contracts/nft/Cargo.toml—testutilsmoved to[dev-dependencies]so the NFT WASM builds.contracts/credential_metadata/src/{lib.rs,validation.rs,validation_tests.rs}— centralized write-path validation, completedis_metadata_valid, reader/writer tests.docs/contract-upgrades.md,docs/contract-interfaces.md— upgrade flow and authorization documentation.scripts/wasm-size-benchmark.sh,docs/contracts/wasm-size-baseline.md— new benchmark and its committed baseline.Issue Closure
Closes #1168
Closes #1169
Closes #1170
Closes #1171