Skip to content

feat(collateral): harden dual-state collateral contract and validator integration#372

Open
itzlambda wants to merge 133 commits intomainfrom
collateral-integration
Open

feat(collateral): harden dual-state collateral contract and validator integration#372
itzlambda wants to merge 133 commits intomainfrom
collateral-integration

Conversation

@itzlambda
Copy link
Copy Markdown
Contributor

@itzlambda itzlambda commented Feb 16, 2026

Summary

This PR hardens and completes Basilica’s collateral integration across:

  1. the upgradeable collateral contract,
  2. validator event ingestion and state persistence, and
  3. collateral tooling used in localnet workflows.

Primary outcomes are stronger reclaim/slash invariants, correct dual-asset state handling (TAO + alpha), and end-to-end SHA-256 evidence consistency.

What Changed

1) Contract hardening and lifecycle correctness (crates/collateral-contract)

  • Strengthened core invariants in CollateralUpgradeable for deposit, reclaim, deny, and slash flows.
  • Fixed edge cases around pending reclaims and slashes so collateral cannot be stranded by lifecycle races.
  • Removed mutable coldkey setter behavior and moved to derived/immutable contract coldkey initialization.
  • Added taoDepositsEnabled and alphaDepositsEnabled toggles.
  • Added minAlphaCollateralIncrease.
  • Standardized evidence fields/checksums to SHA-256 (bytes32) across contract events and calls.
  • Aligned naming for clarity (collaterals -> taoCollaterals, CONTRACT_HOTKEY -> validatorHotkey).
  • Removed stale/legacy contract artifacts and shifted upgrade testing to a dedicated upgrade mock flow.

2) Validator integration and state consistency (crates/basilica-validator)

  • Expanded collateral event handling to cover full lifecycle events (Deposit, ReclaimProcessStarted, Denied, Reclaimed, Slashed).
  • Updated persistence to track TAO and alpha separately, including pending reclaim state and reclaim request linkage.
  • Made per-block collateral event application transactional to avoid partial state application.
  • Updated slash execution path to use full SHA-256 checksum payloads and aligned slash fraction behavior with percentage semantics.
  • Added optional collateral rpc_url override support for validator configuration.

3) CLI/bindings and API surface alignment (collateral-cli + Rust library)

  • Updated Rust bindings and CLI command flow to match the new contract ABI and event model.
  • Reclaim/deny/slash checksum inputs now use SHA-256 naming and types.
  • Query/API naming is aligned to explicit TAO vs alpha collateral reads.
  • Improved network/contract resolution through env-aware CLI defaults.

4) Localnet collateral workflow improvements (scripts/*)

  • Added collateral setup/deploy/integration/e2e scripts for repeatable local testing.
  • Wired collateral deployment into localnet startup flow and validator contract-address patching.
  • Updated localnet bootstrap behavior to better support collateral lifecycle testing.

Behavioral/API Changes to Validate

  • Evidence checksum schema changed to SHA-256 (bytes32) end-to-end.
  • Reclaim/slash behavior now enforces stricter pending-state consistency.
  • ABI/event payloads changed; downstream clients should use updated bindings.
  • Validator collateral config supports rpc_url override.

Summary by CodeRabbit

  • New Features

    • Full dual-state collateral UX: TAO + Alpha deposits, reclaim (deny/finalize), trustee slashing, and CLI collateral commands.
    • New collateral queries: all-collaterals, all-reclaims, active counts, and pagination.
    • Local tooling and tests: localnet setup, deploy/upgrade helpers, e2e validator tests, and utility scripts.
  • Improvements

    • Alpha unit handling standardized to RAO; clearer token formatting in CLI outputs.
    • Checksum handling switched to SHA-256 and display improvements; expanded documentation and examples.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Integrates dual-state collateral (TAO + alpha) across contracts, validator, CLI, and scripts; replaces grace-period exclusion with preference-based logic and RPC-driven background syncing; adds comprehensive CLI collateral commands, localnet deployment and E2E scripts, and large Solidity contract refactors (upgradeable pattern, trustee role, SHA‑256 evidence).

Changes

Cohort / File(s) Summary
CLI Collateral
crates/basilica-cli/Cargo.toml, crates/basilica-cli/src/cli/commands.rs, crates/basilica-cli/src/cli/args.rs, crates/basilica-cli/src/cli/handlers/collateral.rs, crates/basilica-cli/src/cli/handlers/mod.rs
Add full collateral CLI subtree (balance/receive/deposit/status/reclaim/send), new enums/types, handler entrypoint, interactive prompts and key resolution.
Validator: scan & evaluator
crates/basilica-validator/src/collateral/collateral_scan.rs, .../evaluator.rs, .../manager.rs, .../slash_executor.rs, .../mod.rs
Switch from event-driven scanning to RPC background sync; add start/stop/sync loop, compute preferences from GPU info and alpha price; remove grace/exclusion plumbing; adjust on-chain collaterals() to return (tao, alpha); migrate checksums to SHA‑256.
Persistence & DB migrations
crates/basilica-validator/migrations/*, .../persistence/collateral_persistence.rs, .../persistence/miner_nodes.rs, .../persistence/mod.rs
Add dual-collateral and reclaims migrations; implement bulk sync_all_collateral_nodes/sync_all_reclaims; add NodeGpuInfo and batch_update_collateral_preferences; remove collateral_grace persistence module.
Grace-period removal
crates/basilica-validator/src/collateral/grace_tracker.rs, multiple callers
Remove GracePeriodTracker and all callers; delete excluded-state flows and related fields across evaluator, manager, weight-setter, service wiring, and tests.
Service wiring & runtime
crates/basilica-validator/src/service.rs, .../bittensor_core/weight_setter.rs, .../rental/mod.rs
Rewire collateral components: propagate CollateralEvaluator, remove grace-tracker from constructors, adjust runtime handles; rental ranking now uses CollateralPreference and no longer relies on exclusion logic.
Solidity contract & ABI overhaul
crates/collateral-contract/src/CollateralUpgradeable.sol, .../CollateralUpgradeableABI.json, .../CollateralUpgradeableV2.sol, .../Collateral.sol
Major contract refactor to upgradeable pattern: trustee role, ownerColdkeys, tao/alpha deposit toggles, active node/reclaim tracking, SHA‑256 evidence, new paginated getters (getAllCollaterals/getAllReclaims); remove legacy Collateral.sol and V2 artifacts.
Contract CLI & tooling
crates/collateral-contract/deploy.sh, upgrade.sh, foundry.toml, update_abi.py, src/main.rs, src/tests.rs
Update deployment and upgrade scripts, change CLI commands and checksum parsing (MD5→SHA‑256), consolidate ABI tooling to primary ABI, bump solc and adjust test helpers.
Tests & test suites
crates/basilica-validator/tests/*, crates/collateral-contract/test/*, crates/collateral-contract/test/mocks/*
Update Rust tests for API/parameter renames; add comprehensive Solidity tests and mocks (NodeOwnershipEdgeCases, upgrade mocks); update E2E tests and test fixtures to new collaterals() signature and SHA‑256 checksums.
Localnet scripts & examples
scripts/collateral/*, scripts/localnet/*, scripts/subtensor-local/*, scripts/lib/common.sh
Add scripts to setup localnet collateral, deploy contract, run validator E2E tests, compute node IDs, manage wallets; integrate collateral deployment into start flow; add read_hotkey and compute_node_id helpers.
Utilities & miner changes
scripts/collateral/bigint_compare.py, crates/basilica-miner/src/main.rs, crates/basilica-common/examples/compute_node_id.rs
Add bigint formatting/comparison tool; change miner alpha conversions from wei (1e18) to RAO (1e9) and update tests; add NodeId example binary.
Config & docs
scripts/localnet/configs/validator.example.toml, crates/basilica-validator/src/config/collateral.rs, docs/*, crates/collateral-contract/README.md, CLAUDE.md, PR_372_REVIEW.md
Add validator collateral config (rpc_url, sync_page_size, per-GPU USD thresholds), remove grace-related config, default shadow_mode true; extensive docs and review notes reflecting new flows and terminology.
Build / workspace updates
.gitignore, top-level Cargo.toml, crates/basilica-validator/Cargo.toml, crates/collateral-contract/foundry.toml
Ignore local wallet envs and add exceptions for examples/docs; pin tabled dependency with ansi feature; add once_cell & wiremock deps; bump solc and adjust EVM versions.
Removed legacy artifacts
crates/collateral-contract/src/Collateral.json, .../CollateralUpgradableV2ABI.json, script/DeployUpgradeable.s.sol, flow.sh, test/CollateralTestnet.t.sol
Delete old ABIs, Forge deploy scripts, legacy testnet scripts and V2 ABI/update paths—consolidate around new upgradeable contract and tooling.

Sequence Diagram(s)

sequenceDiagram
    participant Validator as Validator(Service)
    participant RPC as Blockchain RPC
    participant DB as Database
    participant Evaluator as CollateralEvaluator
    participant API as TokenPriceAPI

    loop periodic sync
        Validator->>RPC: get_finalized_block()
        RPC-->>Validator: block_number

        Validator->>RPC: get_all_collaterals_at_block(block)
        RPC-->>Validator: list<(minerHotkey,nodeId,tao,alpha)>

        Validator->>DB: sync_all_collateral_nodes(nodes)
        DB-->>Validator: ack

        Validator->>RPC: get_all_reclaims_at_block(block)
        RPC-->>Validator: reclaims

        Validator->>DB: sync_all_reclaims(reclaims)
        DB-->>Validator: ack

        Validator->>DB: get_all_nodes_with_gpu_info()
        DB-->>Validator: nodes_with_gpu

        Validator->>API: fetch_alpha_price()
        API-->>Validator: alpha_price

        loop per node
            Validator->>Evaluator: compute_preference(node, alpha_price)
            Evaluator-->>Validator: CollateralPreference
        end

        Validator->>DB: batch_update_collateral_preferences(updates)
        DB-->>Validator: ack
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • epappas
  • distributedstatemachine

"🐰
I hopped into code, both big and small,
TAO and Alpha answer the call.
Grace slipped away, preferences bloom,
RPC sync hums across the room.
Contracts upgraded — tests dance and sprawl."

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch collateral-integration

@itzlambda itzlambda force-pushed the collateral-integration branch 2 times, most recently from bb23d5d to 5a6753e Compare February 16, 2026 13:44
Fix finalizeReclaim to decrement alphaCollateralUnderPendingReclaims,
cap transfers to available balance instead of reverting after slash,
and enforce proper CEI ordering. Fix denyReclaimRequest to recognize
alpha-only reclaims. Add regression tests for partial/full slash
scenarios and pending counter lifecycle.
…reclaims

Check collateralUnderPendingReclaims and alphaCollateralUnderPendingReclaims
before clearing nodeToMiner in both finalizeReclaim and slashCollateral, closing
a theft vector where a new depositor's funds could be drained by stale reclaim
finalizations. Added regression tests for the attack path.
…taking details

Add coldkey/hotkey model, StakingV2 function reference, alpha token
economics, dual-mode collateral state, and delegatecall vs call semantics.
Add uint256[50] storage gap for upgrade safety and guard deposit()
against msg.value == 0 && alphaAmount == 0 to prevent ownership
griefing without collateral.
…s are zero

After a full slash with a pending reclaim, denyReclaimRequest zeroed
pending trackers but never cleared nodeToMiner, permanently locking
the node to the old owner.
@itzlambda itzlambda force-pushed the collateral-integration branch from 56bbbbb to 3931232 Compare February 18, 2026 09:12
The variable holds the validator's hotkey where all alpha collateral
is consolidated, not a generic contract key.
Clarify that withdrawAlpha uses transferStake (changes coldkey ownership
only), not removeStake. Alpha remains staked under VALIDATOR_HOTKEY.
Also fix TAO slash description: sent to address(0), not trustee.
Mainnet whitelist is already disabled (open to all). Localnet
now matches by auto-disabling via sudo during initialization.
- set Foundry solc to 0.8.24 and evm_version to cancun
- align contract/script/test pragmas to ^0.8.24
- fix deploy/query helper script shebang lines
Use OpenZeppelin ReentrancyGuardUpgradeable on collateral lifecycle entrypoints.

Also block constructor-based first-claim bypasses and verify alpha slash transfers to trustee coldkey with regression tests.
@itzlambda itzlambda changed the title fix(collateral): alpha reclaim accounting fixes and validator integration feat(collateral): harden dual-state collateral contract and validator integration Feb 20, 2026
Enforce required env inputs in the Foundry deploy script for trustee/admin/hotkey and numeric params, removing unsafe defaults. Update collateral docs and audit report to reflect trustee-only authority semantics and remediation status.
Refresh generated collateral ABI and embedded bytecode from the current Solidity build, and align miner tests with lowerCamel getter bindings.
Switch trustee-gated paths to TRUSTEE_ROLE, replace custom math with OpenZeppelin Math/SafeCast, and replace string requires with custom errors while keeping current operational flow.
Automate localnet contract bring-up by creating/funding a deployer wallet, deploying proxy+implementation, and generating scripts/collateral/.env.local.

Also replace forge script deployment with deploy.sh/forge create and update collateral/localnet docs and configs.
…isplay

DRY up time-remaining formatting into format_time_remaining() and add
Display impl for SendToken to simplify interactive token selection.
Replace Option<PathBuf> with PathBuf for key_file in collateral
commands, letting clap enforce the requirement instead of manual
validation.
Replace parse_hotkey (hex-based) with existing parse_ss58_address so
users can pass natural SS58 hotkeys (e.g. 5GQfz…) directly.
…r convention

Bittensor's official precompile interfaces use `hotkey` as the canonical
term. Disambiguate via qualified naming (`minerHotkey`), consistent with
`validatorHotkey` and `alphaHotkey`.
…nment

Display alpha hotkey in human-readable SS58 format with "Staked under"
label instead of raw hex, and align deposit summary columns consistently.
@itzlambda itzlambda force-pushed the collateral-integration branch from fff44d2 to 3f6f9a2 Compare March 9, 2026 14:04
…ad of rental hot path

Move collateral preference evaluation from per-bid-candidate lookups in
RentalManager to a periodic background computation in the collateral
scanner. Preferences are stored in a new `collateral_preference` column
on `miner_nodes` and read directly during bid selection, eliminating the
CollateralManager dependency from the rental path.
@itzlambda itzlambda marked this pull request as ready for review March 10, 2026 10:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/basilica-validator/src/collateral/slash_executor.rs (1)

267-287: ⚠️ Potential issue | 🟠 Major

Don't quantize slash_fraction to whole percent.

* 100 + round() changes the configured policy before you apply it: 0.006 becomes 1%, 0.0149 becomes 1%, and 0.995 becomes 100%. That can materially over- or under-slash on-chain. Keep finer precision here (for example basis points) and only clamp to a 1-unit minimum at the end.

⚙️ Proposed fix
-        let numerator = (self.config.slash_fraction * Decimal::from(100u64))
+        let numerator = (self.config.slash_fraction * Decimal::from(10_000u64))
             .round()
             .to_u64()
             .unwrap_or(0);
         if numerator == 0 {
             return U256::from(1u64);
         }
-        if numerator >= 100 {
+        if numerator >= 10_000 {
             return collateral;
         }
-        let amount = collateral * U256::from(numerator) / U256::from(100u64);
+        let amount = collateral * U256::from(numerator) / U256::from(10_000u64);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/collateral/slash_executor.rs` around lines 267
- 287, compute_slash_amount currently quantizes self.config.slash_fraction by
multiplying by 100 and rounding, which changes policy; instead, keep full
precision by converting collateral into a Decimal, multiply it directly by
self.config.slash_fraction, then convert the resulting Decimal back to a U256
(floor/round toward zero) and clamp: if self.config.slash_fraction.is_zero()
return U256::from(1u64) (after the zero check), if self.config.slash_fraction >=
Decimal::ONE return collateral, and after computing amount ensure amount >= 1
(return U256::from(1u64) if zero) and never exceed collateral; update
compute_slash_amount to use Decimal arithmetic on collateral and avoid the *100
+ round() pathway so tiny fractions are preserved.
crates/basilica-validator/tests/collateral_e2e.rs (1)

164-169: ⚠️ Potential issue | 🟡 Minor

The checksum assertion no longer proves SHA-256 correctness.

assert_ne!(calls[0].checksum, [0u8; 32]) only shows that some non-zero bytes were passed through. A wrong payload hash or padded legacy digest would still pass. Please compare it to the expected 32-byte SHA-256 digest for the evidence payload this test writes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/tests/collateral_e2e.rs` around lines 164 - 169,
Replace the weak inequality check on calls[0].checksum with an exact SHA-256
comparison: compute the expected 32-byte digest of the evidence payload that
this test writes (use the same payload bytes used earlier in the test), e.g. by
hashing that payload with sha2::Sha256 and taking the 32-byte result, then
assert_eq!(calls[0].checksum, expected_digest); keep the other assertions
(alpha_amount, private_key, url) intact and use calls() / calls[0].checksum to
locate the assertion to update.
🟡 Minor comments (12)
scripts/lib/common.sh-66-75 (1)

66-75: ⚠️ Potential issue | 🟡 Minor

Validate the hotkey format before returning it.

This helper advertises a 0x-prefixed bytes32, but Line 74 just echoes whatever publicKey contains. Invalid wallet JSON will slip through here and fail much later in the contract/CLI path.

Proposed hardening
 read_hotkey() {
     local pubfile="$1"
     if [[ ! -f "$pubfile" ]]; then
         log_error "Hotkey file not found: $pubfile"
         return 1
     fi
-    python3 -c "import json,sys; print(json.load(sys.stdin)['publicKey'])" < "$pubfile"
+    python3 - "$pubfile" <<'PY'
+import json, re, sys
+
+path = sys.argv[1]
+try:
+    with open(path, "r", encoding="utf-8") as fh:
+        value = json.load(fh)["publicKey"]
+except (OSError, ValueError, KeyError) as exc:
+    sys.stderr.write(f"Failed to read publicKey from {path}: {exc}\n")
+    raise SystemExit(1)
+
+if not isinstance(value, str) or not re.fullmatch(r"0x[0-9a-fA-F]{64}", value):
+    sys.stderr.write(f"Invalid publicKey in {path}: expected 0x-prefixed 32-byte hex\n")
+    raise SystemExit(1)
+
+print(value)
+PY
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/lib/common.sh` around lines 66 - 75, In read_hotkey(), validate and
normalize the wallet JSON publicKey before printing: load publicKey, ensure it
is hex characters only, prepend "0x" if missing, and verify it is a bytes32 hex
string (0x followed by exactly 64 hex chars); if validation fails, emit a clear
log_error and return non-zero. Update the inline python3 snippet (or replace
with a short bash validation) to perform the checks and normalization and only
print the validated 0x-prefixed bytes32 value, referencing the read_hotkey
function and its pubfile argument so the failing path is detected immediately.
scripts/collateral/bigint_compare.py-32-41 (1)

32-41: ⚠️ Potential issue | 🟡 Minor

Handle signed values explicitly in the formatters.

// and % floor on negative integers in Python, so fmt_tao(-1) currently renders as -1.999999999999999999 TAO instead of -0.000000000000000001. The same bug exists in fmt_alpha, which makes signed deltas misleading.

💡 Proposed fix
+def _format_units(value: int, scale: int, decimals: int, symbol: str, raw_unit: str) -> str:
+    sign = "-" if value < 0 else ""
+    raw = abs(value)
+    whole = raw // scale
+    frac = raw % scale
+    return f"{sign}{whole}.{frac:0{decimals}d} {symbol} ({value} {raw_unit})"
+
+
 def fmt_tao(wei: int) -> str:
-    whole = wei // 10**18
-    frac = wei % 10**18
-    return f"{whole}.{frac:018d} TAO ({wei} wei)"
+    return _format_units(wei, 10**18, 18, "TAO", "wei")
 
 
 def fmt_alpha(rao: int) -> str:
-    whole = rao // 10**9
-    frac = rao % 10**9
-    return f"{whole}.{frac:09d} alpha ({rao} rao)"
+    return _format_units(rao, 10**9, 9, "alpha", "rao")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/collateral/bigint_compare.py` around lines 32 - 41, The formatters
fmt_tao and fmt_alpha mishandle negative inputs because // and % floor toward
negative infinity; update both functions to detect the sign (e.g., sign = "-" if
value < 0 else ""), operate on the absolute value (abs(wei) / abs(rao)) to
compute whole and frac, then prefix the sign to the formatted string so
fmt_tao(-1) becomes -0.000...001 TAO and similarly for fmt_alpha; keep types and
return format unchanged and apply this change to both fmt_tao and fmt_alpha.
scripts/collateral/bigint_compare.py-44-72 (1)

44-72: ⚠️ Potential issue | 🟡 Minor

Enforce exact arity and convert parse failures into usage errors.

Right now fmt_tao 1 extra succeeds, and fmt_tao not-a-number raises a traceback instead of returning the documented usage exit code 2. For a script that will be called from shell tooling, that makes invocation bugs harder to detect.

💡 Proposed fix
 def main() -> int:
-    if len(sys.argv) < 3:
+    if len(sys.argv) < 2:
         print(__doc__, file=sys.stderr)
         return 2
 
     cmd = sys.argv[1]
-
-    if cmd == "fmt_tao":
-        print(fmt_tao(parse_int(sys.argv[2])))
-        return 0
-
-    if cmd == "fmt_alpha":
-        print(fmt_alpha(parse_int(sys.argv[2])))
-        return 0
-
-    if len(sys.argv) < 4:
-        print(__doc__, file=sys.stderr)
-        return 2
-
-    a = parse_int(sys.argv[2])
-    b = parse_int(sys.argv[3])
+    try:
+        if cmd == "fmt_tao":
+            if len(sys.argv) != 3:
+                print(__doc__, file=sys.stderr)
+                return 2
+            print(fmt_tao(parse_int(sys.argv[2])))
+            return 0
+
+        if cmd == "fmt_alpha":
+            if len(sys.argv) != 3:
+                print(__doc__, file=sys.stderr)
+                return 2
+            print(fmt_alpha(parse_int(sys.argv[2])))
+            return 0
+
+        if cmd not in {"gt", "gte", "eq", "lt", "lte"} or len(sys.argv) != 4:
+            print(__doc__, file=sys.stderr)
+            return 2
+
+        a = parse_int(sys.argv[2])
+        b = parse_int(sys.argv[3])
+    except ValueError as exc:
+        print(f"Invalid integer input: {exc}", file=sys.stderr)
+        return 2
 
     ops = {"gt": a > b, "gte": a >= b, "eq": a == b, "lt": a < b, "lte": a <= b}
-
-    if cmd not in ops:
-        print(f"Unknown command: {cmd}", file=sys.stderr)
-        return 2
 
     return 0 if ops[cmd] else 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/collateral/bigint_compare.py` around lines 44 - 72, The main()
function accepts wrong arity and lets parse_int raise exceptions; update it to
enforce exact argument counts for each command and to convert parse_int failures
into usage errors (exit code 2) by catching ValueError (or the specific
exception parse_int raises), printing __doc__ (or a clear usage message) to
stderr and returning 2; specifically check that fmt_tao and fmt_alpha are called
with exactly one operand and the comparison commands
("gt","gte","eq","lt","lte") are called with exactly two operands before calling
parse_int(fmt/parse), and wrap calls to parse_int used for fmt_tao, fmt_alpha
and the two comparison operands in try/except to handle invalid numbers by
returning 2.
crates/collateral-contract/README.md-187-187 (1)

187-187: ⚠️ Potential issue | 🟡 Minor

Use actual SHA-256 examples for --url-content-sha256.

These samples are duplicated MD5-looking digests (d41d8cd…, 5d41402…, 098f6bcd…) stretched to 64 hex chars. That contradicts the SHA-256 migration this README is documenting and makes the evidence format easy to misunderstand.

Also applies to: 212-212, 234-234

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/README.md` at line 187, Replace the bogus MD5-like
64-hex placeholders used for the --url-content-sha256 examples in the README
(instances near the shown diff and at the other noted locations) with real
SHA-256 example digests (64 hex characters produced by a real SHA-256 of sample
content), so examples match the documented migration to SHA-256 and avoid
confusion; update each occurrence of the flag's sample value to a valid SHA-256
hex string (e.g., digest of the actual example content) while leaving the
surrounding command text unchanged.
crates/collateral-contract/README.md-160-161 (1)

160-161: ⚠️ Potential issue | 🟡 Minor

Avoid private-key-shaped literals in docs.

Line 161 uses a realistic 32-byte hex key, which already trips secret scanning and normalizes exporting raw keys into shell history. An obvious placeholder like 0x<YOUR_PRIVATE_KEY> is safer and avoids tooling noise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/README.md` around lines 160 - 161, Replace the
realistic 32-byte hex literal used for the PRIVATE_KEY example with a non-secret
placeholder to avoid secret-scanning and accidental key leaks; specifically
update the example that exports PRIVATE_KEY (the line showing export
PRIVATE_KEY=0x1234...ef12) to use a clearly non-sensitive placeholder such as
0x<YOUR_PRIVATE_KEY> or <REPLACE_WITH_PRIVATE_KEY> and add a short comment
advising users not to paste real keys into shell history.
crates/collateral-contract/README.md-40-53 (1)

40-53: ⚠️ Potential issue | 🟡 Minor

Honor RPC_URL in the manual deployment example.

This snippet hard-codes the testnet RPC twice, even though the repo now documents RPC_URL as an input for the deployment flow. Copy/pasting it will silently ignore localnet/custom endpoints and undermines the new override behavior.

Suggested doc fix
+export RPC_URL=https://test.chain.opentensor.ai
+
 impl_out="$(forge create src/CollateralUpgradeable.sol:CollateralUpgradeable \
-  --rpc-url https://test.chain.opentensor.ai \
+  --rpc-url "$RPC_URL" \
   --private-key "$PRIVATE_KEY" \
   --broadcast)"

 ...

 proxy_out="$(forge create lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
-  --rpc-url https://test.chain.opentensor.ai \
+  --rpc-url "$RPC_URL" \
   --private-key "$PRIVATE_KEY" \
   --broadcast \
   --constructor-args "$IMPLEMENTATION_ADDRESS" "$INIT_DATA")"

Also applies to: 370-374

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/README.md` around lines 40 - 53, The README
hard-codes the RPC endpoint in the forge/cast commands (see invocations that set
impl_out and proxy_out) which ignores the documented RPC_URL override; update
those commands to use the RPC_URL environment variable instead of the literal
https://test.chain.opentensor.ai (also update the second occurrence noted around
the proxy creation block) so both the impl_out (forge create ... --rpc-url ...)
and proxy_out forge create lines read --rpc-url "$RPC_URL" to honor local/custom
endpoints.
scripts/collateral/README.md-51-72 (1)

51-72: ⚠️ Potential issue | 🟡 Minor

Add documentation for the min-alpha-collateral-increase query command.

The README documents min-collateral-increase and collaterals, which are correct and will work. However, the min-alpha-collateral-increase query command exists and is not documented here. Include it alongside the TAO variant to give users visibility into both minimum collateral thresholds:

cc query min-collateral-increase
cc query min-alpha-collateral-increase
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/collateral/README.md` around lines 51 - 72, The README is missing
documentation for the min-alpha-collateral-increase query; add it next to the
existing min-collateral-increase entry so both TAO and alpha variants are shown.
Update the “Contract metadata” snippet to list both commands (cc query
min-collateral-increase and cc query min-alpha-collateral-increase) and, if
applicable, mirror any example usage or explanation you have for
min-collateral-increase so users can see both minimum collateral thresholds;
ensure the new line appears alongside the existing cc query
min-collateral-increase and cc query collaterals entries.
scripts/localnet/start.sh-236-236 (1)

236-236: ⚠️ Potential issue | 🟡 Minor

sed -i syntax is macOS-specific and will fail on Linux.

The sed -i '' syntax works on macOS but Linux's GNU sed requires sed -i without the empty string argument. This will cause the script to fail on Linux systems.

🔧 Proposed fix for cross-platform sed
-    sed -i '' "s|^contract_address = .*|contract_address = \"${address}\"|" "$config_file"
+    # Cross-platform sed in-place edit
+    if [[ "$OSTYPE" == "darwin"* ]]; then
+        sed -i '' "s|^contract_address = .*|contract_address = \"${address}\"|" "$config_file"
+    else
+        sed -i "s|^contract_address = .*|contract_address = \"${address}\"|" "$config_file"
+    fi

Alternatively, use a temp file approach that works on both platforms:

-    sed -i '' "s|^contract_address = .*|contract_address = \"${address}\"|" "$config_file"
+    sed "s|^contract_address = .*|contract_address = \"${address}\"|" "$config_file" > "${config_file}.tmp" && mv "${config_file}.tmp" "$config_file"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/localnet/start.sh` at line 236, The sed invocation using `sed -i ''
"s|^contract_address = .*|contract_address = \"${address}\"|" "$config_file"` is
macOS-specific and will fail on GNU sed; replace it with a cross-platform
approach: create a temporary file (via mktemp or similar), run sed without
in-place editing to write the modified content into that temp file, then
atomically move/replace the original `$config_file` with the temp file;
reference the existing variables `config_file` and `address` and the current sed
substitution to ensure the same replacement semantics.
crates/basilica-cli/src/cli/args.rs-339-342 (1)

339-342: ⚠️ Potential issue | 🟡 Minor

Thread the global --json mode into the collateral path.

This is the only top-level command branch that drops self.json, so basilica --json collateral ... cannot honor the CLI's global output contract.

💡 Proposed change
-                handlers::collateral::handle_collateral(action, key_file).await?;
+                handlers::collateral::handle_collateral(action, key_file, self.json).await?;

That will require updating handle_collateral and any downstream helpers that format output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-cli/src/cli/args.rs` around lines 339 - 342, The collateral
command branch is not passing the global JSON flag (self.json) into the
collateral handler, so calls to Commands::Collateral { action, key_file } should
call handlers::collateral::handle_collateral with the json mode; update the call
site to pass self.json (or equivalent) and then update
handlers::collateral::handle_collateral signature and all downstream helper
functions that format output to accept a json: bool (or OutputMode) parameter
and switch formatting based on that flag so `basilica --json collateral ...`
honors the global output contract.
crates/collateral-contract/foundry.toml-5-6 (1)

5-6: ⚠️ Potential issue | 🟡 Minor

Add FOUNDRY_PROFILE=local to README.md forge test examples.

The README.md documentation at lines 23, 359, 362, 365, and 368 shows bare forge test commands that will compile against the default Cancun EVM. Update these to include FOUNDRY_PROFILE=local so developers following the docs test against the London profile that matches the localnet configuration.

The actual deployment scripts already use FOUNDRY_PROFILE=local correctly in deploy.sh, but documentation examples should be consistent.

Examples to update in README.md
# Current (incorrect for localnet)
forge test
forge test --match-test testDeposit
forge test -vvv
forge test --gas-report

# Should be
FOUNDRY_PROFILE=local forge test
FOUNDRY_PROFILE=local forge test --match-test testDeposit
FOUNDRY_PROFILE=local forge test -vvv
FOUNDRY_PROFILE=local forge test --gas-report

Also applies to: 17-20

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/foundry.toml` around lines 5 - 6, Update the
README.md test command examples by prefixing every bare "forge test" invocation
(including variants with "--match-test testDeposit", "-vvv", and "--gas-report")
with "FOUNDRY_PROFILE=local" so documentation matches the localnet London
profile used by deploy.sh; replace lines that show "forge test", "forge test
--match-test testDeposit", "forge test -vvv", and "forge test --gas-report" with
"FOUNDRY_PROFILE=local forge test" equivalents throughout the file.
crates/basilica-cli/src/cli/handlers/collateral.rs-392-413 (1)

392-413: ⚠️ Potential issue | 🟡 Minor

Apply --hotkey before rendering aggregate status.

The aggregate path ignores params.hotkey, so basilica collateral status --hotkey ... still prints every collateral row and every pending reclaim. Filter both collections before building the tables.

Also applies to: 423-458

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-cli/src/cli/handlers/collateral.rs` around lines 392 - 413,
The listing ignores params.hotkey; before mapping `all` to `rows` filter `all`
by `params.hotkey` (if Some) using the same key format as
`hotkey_to_ss58(&n.miner_hotkey)` so only matching collaterals are displayed;
likewise locate the pending-reclaims collection (used later around the 423-458
region) and filter that collection by `params.hotkey` before building its table
so pending reclaims are also restricted to the requested hotkey. Ensure you
apply the filter prior to constructing `CollateralRow` and the pending-reclaim
rows so rendering respects the CLI `--hotkey` flag.
crates/basilica-validator/src/collateral/collateral_scan.rs-116-121 (1)

116-121: ⚠️ Potential issue | 🟡 Minor

Partial sync failures leave inconsistent state.

If sync_all_collateral_nodes succeeds but sync_all_reclaims fails (or vice versa), the database will be in an inconsistent state with mismatched node and reclaim data. Additionally, update_last_scanned_block_number is called after both operations, so a failure in reclaims will prevent the block number update but leave nodes already synced.

Consider wrapping all three operations in a transaction or using a more atomic approach where all operations succeed or fail together.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/collateral/collateral_scan.rs` around lines 116
- 121, The three persistence calls (self.persistence.sync_all_collateral_nodes,
self.persistence.sync_all_reclaims, and
self.persistence.update_last_scanned_block_number) must be executed atomically
to avoid partial commits; wrap them in a single database transaction via the
persistence layer (e.g., begin_transaction / commit / rollback or
transaction(async |tx| { ... }) API), call sync_all_collateral_nodes and
sync_all_reclaims against the transaction/tx and then
update_last_scanned_block_number on success, commit the transaction, and ensure
any error from any of these methods triggers a rollback and returns the error
instead of leaving partially applied state.
🧹 Nitpick comments (19)
scripts/subtensor-local/init.py (1)

91-91: Consider adding wait_for_finalization=True for consistency.

Other extrinsic submissions in this file (e.g., register_subnet, burned_register, start_call) use both wait_for_inclusion=True and wait_for_finalization=True. For consistency and to ensure the whitelist is fully disabled before any dependent operations:

Suggested change
-        receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
+        receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True, wait_for_finalization=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/subtensor-local/init.py` at line 91, The call to
substrate.submit_extrinsic that assigns receipt currently only uses
wait_for_inclusion=True; update that call (the substrate.submit_extrinsic(...)
that returns receipt) to also pass wait_for_finalization=True so it matches
other submissions like register_subnet, burned_register, and start_call and
ensures the whitelist-disable extrinsic is finalized before proceeding.
scripts/localnet/init-subnet.sh (2)

265-265: Balance parsing may mishandle scientific notation.

The sed 's/[^0-9.]//g' approach strips all non-numeric characters, which could misparse values in scientific notation (e.g., "1e10" becomes "110" instead of 10000000000). For a localnet script where balances are typically whole numbers, this is unlikely to cause issues, but worth noting.

📝 Optional: Use jq's numeric coercion instead
-    current_balance=$(echo "$balance_json" | jq -r '.balance.free // "0"' 2>/dev/null | sed 's/[^0-9.]//g')
+    current_balance=$(echo "$balance_json" | jq -r '(.balance.free // 0) | tonumber' 2>/dev/null || echo "0")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/localnet/init-subnet.sh` at line 265, The current_balance assignment
strips non-digits with sed which mangles scientific notation; update the parsing
to let jq coerce to a numeric value instead of post-processing with sed: change
the pipeline that reads balance_json and sets current_balance (the line
referencing balance_json and current_balance) to use jq's numeric coercion
(e.g., use '.balance.free // 0 | tonumber' or similar) and remove the sed step
so the value is emitted as a proper numeric string (fallback to 0 on missing
values).

357-358: Hardcoded sleep is coupled to tempo configuration.

The sleep 45 assumes tempo=20 with ~2s block times. If the tempo setting on line 161 fails (it only prints a WARNING) or block times vary, the sleep may be insufficient for subtoken activation. For a localnet script this is acceptable, but a polling approach would be more robust.

📝 Optional: Poll for subtoken activation instead of fixed sleep
 echo "  Waiting for next epoch to activate subtokens (tempo=20, ~40s)..."
-sleep 45
+# Poll until subtokens are active (up to 60s)
+for i in {1..30}; do
+    if uvx --python 3.12 --from bittensor-cli btcli subnet list --network local --json-output 2>/dev/null | jq -e '.[] | select(.netuid == 1 and .subtokens_enabled == true)' >/dev/null 2>&1; then
+        echo "  Subtokens activated"
+        break
+    fi
+    sleep 2
+done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/localnet/init-subnet.sh` around lines 357 - 358, The script uses a
hardcoded sleep ("sleep 45") tied to the tempo setting; replace this with a
polling loop that repeatedly queries the subnet/subtoken activation state until
it becomes active or a sensible timeout is reached (e.g., 2-3× expected epoch
time), rather than sleeping a fixed 45s. Locate the "sleep 45" call and the
tempo configuration (the printed WARNING about tempo around line 161) and
implement: poll every few seconds (using sleep N) calling the existing CLI/API
used elsewhere in the script to check subtoken activation status, break when
active, and fail with a clear error if timeout elapses; optionally compute the
timeout from the tempo variable if present. Ensure the loop is idempotent and
logs progress instead of relying on the fixed "sleep 45".
crates/collateral-contract/test/IStakingIntegration.t.sol (3)

213-218: Validate the SHA-256 evidence path in the slash test.

The PR explicitly changes slash evidence handling to SHA-256, but this test only checks balances. If checksum persistence/emission regressed, this would still pass. Consider asserting the emitted slash event or stored slash record includes TEST_SHA256.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/IStakingIntegration.t.sol` around lines 213 -
218, Update the test to validate that the SHA-256 evidence is persisted/emitted
by asserting the slash evidence contains TEST_SHA256 after calling
collateral.slashCollateral(HOTKEY_1, EXECUTOR_ID_1, 0, slashAlphaAmount,
TEST_URL, TEST_SHA256); for example, capture the emitted Slash event (or read
the stored slash record) and add an assertion that the event/record's checksum
or evidence field equals TEST_SHA256 so the test fails if SHA-256 handling
regresses; locate the call to collateral.slashCollateral and add the event
assertion (or storage lookup) immediately after the balance assertions.

194-199: Assert that finalizeReclaim clears internal alpha accounting.

This test proves stake moves back to Alice, but it never checks that collateral.alphaCollaterals(HOTKEY_1, EXECUTOR_ID_1) is zero afterward. A stale internal balance would still let this pass while breaking later slash/eligibility logic.

Suggested assertion
         collateral.finalizeReclaim(0);

+        assertEq(collateral.alphaCollaterals(HOTKEY_1, EXECUTOR_ID_1), 0);
         assertEq(mockStaking.getStake(VALIDATOR_HOTKEY, contractColdkey, NETUID), 0);
         assertEq(mockStaking.getStake(VALIDATOR_HOTKEY, ALICE_COLDKEY, NETUID), ALPHA_AMOUNT);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/IStakingIntegration.t.sol` around lines 194 -
199, After calling collateral.finalizeReclaim(0) and the existing stake
assertions, add an assertion that the internal alpha accounting is cleared by
checking collateral.alphaCollaterals(...) equals zero; specifically call
alphaCollaterals with the validator hotkey constant (e.g., VALIDATOR_HOTKEY or
HOTKEY_1) and the executor id constant (EXECUTOR_ID_1) to ensure
collateral.alphaCollaterals(VALIDATOR_HOTKEY, EXECUTOR_ID_1) == 0 is asserted
immediately after finalizeReclaim.

40-50: Tighten the mock precompile's mutator surface.

applyTransferStake, applyMoveStake, and setStake are callable on the etched precompile address, so any caller can bypass the caller-derived coldkey path and mutate stake state directly. That makes this mock less strict than the real integration and can hide selector/caller-context bugs in the contract under test.

Also applies to: 70-80, 87-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/IStakingIntegration.t.sol` around lines 40 -
50, The three mutator functions (applyTransferStake, applyMoveStake, setStake)
on the mock precompile are currently callable by any caller at the etched
precompile address; tighten them by enforcing the same caller-derived coldkey
check used by the real integration: add a require that msg.sender equals the
canonical precompile-caller address (the expected caller constant used for this
integration, e.g. ISTAKING_PRECOMPILE_CALLER) at the top of applyTransferStake,
applyMoveStake and setStake before delegating to
_applyTransferStake/_applyMoveStake/_setStake, so only the legitimate precompile
caller can mutate stake state.
crates/collateral-contract/deploy.sh (1)

29-33: Make address extraction more robust than parsing human output.

Both addresses are scraped from forge create stdout. If Foundry output changes after a successful broadcast, this script will fail late and reruns can create extra implementations/proxies. Prefer structured output if your Foundry version supports it, or at least validate against an address regex instead of only checking for non-empty strings.

Also applies to: 59-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/deploy.sh` around lines 29 - 33, The current
extraction of IMPLEMENTATION_ADDRESS from impl_output is fragile; change the
script to request structured output from forge (e.g., use --json/--silent if
available) and parse the implementation and proxy addresses with a JSON parser
(jq) or, if JSON isn't available, validate extracted values against an Ethereum
address regex (^0x[0-9a-fA-F]{40}$) instead of only checking for non-empty
strings; apply the same robust extraction/validation for the proxy address
extraction (the proxy_output/PROXY_ADDRESS logic around lines 59-63) so you fail
fast on invalid parses and avoid accidental redeploys.
scripts/subtensor-local/docker-compose.yml (1)

3-3: Consider pinning to a specific image version instead of latest for reproducibility.

Using the latest tag makes builds non-reproducible—the image can change upstream without notice. Even for localnet development tooling, pinning to a specific version ensures consistent environments across the team. The ghcr.io/opentensor/subtensor-localnet image has multiple available versions including v3.2.8-320, v3.2.6, and other stable tags that can be used instead.

♻️ Suggested fix: pin to a specific version
 services:
   alice:
-    image: ghcr.io/opentensor/subtensor-localnet:latest
+    image: ghcr.io/opentensor/subtensor-localnet:v3.2.8-320
     container_name: subtensor-alice
   bob:
-    image: ghcr.io/opentensor/subtensor-localnet:latest
+    image: ghcr.io/opentensor/subtensor-localnet:v3.2.8-320
     container_name: subtensor-bob

Also applies to: 29-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/subtensor-local/docker-compose.yml` at line 3, Replace the floating
image tag "ghcr.io/opentensor/subtensor-localnet:latest" with a pinned, specific
version tag (e.g., "ghcr.io/opentensor/subtensor-localnet:v3.2.8-320") in the
docker-compose image declaration so builds are reproducible; update the image
value where it appears (the image: ghcr.io/opentensor/subtensor-localnet:latest
line) and choose a stable released tag your team agrees on.
crates/basilica-validator/src/grpc/registration.rs (1)

41-41: Grace period field deprecated - consider documenting proto deprecation.

The grace_period_remaining field now always returns an empty string. If this field is no longer used, consider adding a comment noting the deprecation or updating the proto definition to mark the field as deprecated for client awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/grpc/registration.rs` at line 41, Update the
code handling the registration response to indicate that grace_period_remaining
is deprecated: add a clear comment next to the grace_period_remaining
initialization in registration.rs (the field named grace_period_remaining)
stating that the field is deprecated and intentionally left as an empty string,
and/or update the associated protobuf definition to mark the field as deprecated
(using the deprecated option in the proto) so clients are aware; ensure the
comment references the proto field name and the code path that currently sets
String::new() so future maintainers know why it remains present.
crates/collateral-contract/src/config.rs (1)

37-48: Consider case-insensitive parsing for better UX.

The FromStr implementation only accepts lowercase strings ("mainnet", "testnet", "local"). Users might provide "Mainnet" or "MAINNET" from CLI or config files.

♻️ Suggested improvement
 impl std::str::FromStr for Network {
     type Err = anyhow::Error;

     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
+        match s.to_lowercase().as_str() {
             "mainnet" => Ok(Network::Mainnet),
             "testnet" => Ok(Network::Testnet),
             "local" => Ok(Network::Local),
             other => Err(anyhow::anyhow!("Unsupported collateral network: {}", other)),
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/src/config.rs` around lines 37 - 48, Update the
impl of std::str::FromStr for Network (the from_str method) to perform
case-insensitive parsing so inputs like "Mainnet" or "MAINNET" are accepted;
e.g., normalize the input (or use eq_ignore_ascii_case) before matching the
variants Network::Mainnet, Network::Testnet, and Network::Local and keep the
error branch returning anyhow::anyhow! with the original input preserved in the
message for clarity.
scripts/collateral/e2e-validator-test.sh (1)

48-72: Remove unused constants.

Several constants are defined but never used in the script: ADDRESS_MAPPING, ZERO_ADDR, TAO_5, TAO_10. These add noise and may confuse future maintainers.

♻️ Remove unused constants
-STAKING_PRECOMPILE="0x0000000000000000000000000000000000000805"
-ADDRESS_MAPPING="0x000000000000000000000000000000000000080C"
+STAKING_PRECOMPILE="0x0000000000000000000000000000000000000805"

 # Query NETUID and VALIDATOR_HOTKEY from the deployed contract (set during initialize()).
 NETUID="$(cast call --rpc-url "$RPC_URL" "$PROXY" "netuid()(uint16)")"
 VALIDATOR_HOTKEY="$(cast call --rpc-url "$RPC_URL" "$PROXY" "validatorHotkey()(bytes32)")"

 LOCALNET_WALLETS_DIR="${REPO_ROOT}/scripts/localnet/wallets"
 MINER_PUBKEY="$(read_hotkey "${LOCALNET_WALLETS_DIR}/miner_1/hotkeys/defaultpub.txt")"

 ZERO_BYTES32="0x0000000000000000000000000000000000000000000000000000000000000000"
-ZERO_ADDR="0x0000000000000000000000000000000000000000"

 NODE_ID_1="$(compute_node_id "127.0.0.1")"  # bytes16
 NODE_ID_2="$(compute_node_id "127.0.0.2")"  # bytes16

 # Hex with 0x prefix — how the validator stores them in SQLite (RPC snapshot: format!("0x{}", hex::encode(...)))
 HEX_MINER_PUBKEY="${MINER_PUBKEY}"
 HEX_NODE_ID_1="${NODE_ID_1}"
 HEX_NODE_ID_2="${NODE_ID_2}"

 # TAO amounts in wei (1 TAO = 1e18 wei)
 TAO_1="1000000000000000000"
 TAO_2="2000000000000000000"
-TAO_5="5000000000000000000"
-TAO_10="10000000000000000000"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/collateral/e2e-validator-test.sh` around lines 48 - 72, Remove the
unused constant declarations to reduce noise: delete the variables
ADDRESS_MAPPING, ZERO_ADDR, TAO_5, and TAO_10 from the script (the lines that
assign values to those names). Before removing, search for any references to
ADDRESS_MAPPING, ZERO_ADDR, TAO_5, or TAO_10 elsewhere in the file or called
helper scripts (e.g., around read_hotkey, compute_node_id usage) to ensure no
dependent code breaks; if found, refactor those call sites instead of deleting.
After removal, tidy nearby comments/whitespace so the remaining constants
(NETUID, VALIDATOR_HOTKEY, MINER_PUBKEY, NODE_ID_1/2, HEX_*, TAO_1/2) remain
clear and correctly grouped.
crates/collateral-contract/test/CollateralBasic.t.sol (1)

106-114: Avoid baking raw storage slot numbers into the test harness.

These helpers now depend on the exact proxy storage layout (5, 8, 10, field offsets, etc.). The next state-variable insertion or reorder can make the regressions write the wrong field and fail opaquely—or pass for the wrong reason. A small harness exposing test-only setters/getters would be much more stable across upgrades.

Also applies to: 459-476

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/CollateralBasic.t.sol` around lines 106 -
114, The test currently hardcodes raw storage slot math in _nodeToMinerSlot and
uses vm.store in _seedNodeOwner (and other positions) which brittlely depends on
proxy layout; add a test-only setter/getter pair on the Collateral contract
(e.g., functions setNodeOwner(bytes32 hotkey, bytes16 nodeId, address owner) and
getNodeOwner(...)) that encapsulate the storage location, update the test to
call collateral.setNodeOwner(...) instead of vm.store and remove the hardcoded
slot arithmetic in _nodeToMinerSlot/_seedNodeOwner; this keeps tests stable
across state-variable reorders while still allowing seeding and inspection of
node owner state.
scripts/collateral/integration-test.sh (1)

433-456: Assert the exact alpha balance here, not just > 0.

Both deposits request a fixed RAO_4_ALPHA, so assert_gt ... 0 is too weak. A RAO/alpha unit bug or partial transfer would still pass.

Proposed tightening
 alpha_col="$(cast_query "$PROXY" "alphaCollaterals(bytes32,bytes16)(uint256)" "$MINER_PUBKEY" "$NODE_ID_3")"
 log_info "  alphaCollaterals = ${alpha_col} RAO (requested ${RAO_4_ALPHA})"
-assert_gt "$alpha_col" "0" "alphaCollaterals(MINER_PUBKEY, NODE_ID_3) > 0"
+assert_eq "$alpha_col" "$RAO_4_ALPHA" "alphaCollaterals(MINER_PUBKEY, NODE_ID_3) == ${RAO_4_ALPHA} RAO"
@@
 alpha_col="$(cast_query "$PROXY" "alphaCollaterals(bytes32,bytes16)(uint256)" "$MINER_PUBKEY" "$NODE_ID_4")"
 log_info "  alphaCollaterals = ${alpha_col} RAO"
-assert_gt "$alpha_col" "0" "alphaCollaterals(MINER_PUBKEY, NODE_ID_4) > 0"
+assert_eq "$alpha_col" "$RAO_4_ALPHA" "alphaCollaterals(MINER_PUBKEY, NODE_ID_4) == ${RAO_4_ALPHA} RAO"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/collateral/integration-test.sh` around lines 433 - 456, Replace the
loose checks that alpha_col > 0 with exact equality to the requested amount:
when you call deposit(...) with RAO_4_ALPHA and then fetch alpha_col via
cast_query (the alphaCollaterals(bytes32,bytes16) calls that populate the
alpha_col variable for NODE_ID_3 and NODE_ID_4), change the assert_gt
"$alpha_col" "0" assertions to assert_eq "$alpha_col" "$RAO_4_ALPHA" so the test
verifies the full RAO_4_ALPHA was transferred rather than just being non‑zero.
crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol (1)

94-103: Avoid hard-coding proxy storage slots in this helper.

This assumes nodeToMiner will stay at slot 5 forever. Any storage-layout change above it will make the test mutate unrelated proxy state and fail for the wrong reason. Prefer stdstore or a dedicated test-only hook on a mock implementation here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol` around lines 94
- 103, The helper functions _nodeToMinerSlot and _seedNodeOwner are hardcoding
the nodeToMiner storage slot (5); replace this fragile approach by locating the
mapping slot dynamically (e.g., use stdstore utilities to target the nodeToMiner
mapping key or add a test-only setter on the collateral mock) and then use
vm.store/stdstore to write the owner value; update _seedNodeOwner to use that
dynamic slot resolution (or the mock hook) rather than the constant uint256
nodeToMinerSlot = 5 so tests don’t break if storage layout changes.
crates/basilica-validator/src/config/collateral.rs (1)

93-96: Consider documenting the slash_fraction precision constraint.

The validation now requires slash_fraction to be within [0.01, 1.0], which addresses the precision concern raised in PR comments about small fractional percentages rounding to zero. Consider adding a doc comment explaining this constraint and why sub-1% fractions are disallowed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/config/collateral.rs` around lines 93 - 96, Add
a doc comment on the collateral configuration explaining that slash_fraction
must be between 0.01 and 1.0 and why: note that values below 1% are disallowed
to avoid Decimal precision/rounding to zero and to ensure meaningful slashing
amounts; attach this comment near the collateral struct/field definition for
slash_fraction (and optionally above the validation block that checks
one_percent and self.slash_fraction) so future readers see the constraint and
rationale together with the code.
crates/basilica-validator/src/collateral/collateral_scan.rs (1)

45-56: Spawned task handle is discarded; shutdown cannot be awaited.

The start() method spawns a background task but does not store the JoinHandle. While stop() cancels the token, there's no way to await the task's completion during shutdown. This could lead to incomplete cleanup if the validator shuts down while a sync operation is in progress.

Consider storing the JoinHandle and providing an async stop method that cancels the token and awaits the task:

♻️ Proposed refactor
 pub struct Collateral {
     // ... existing fields ...
     cancellation_token: CancellationToken,
+    task_handle: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
 }

-    pub fn start(&self) {
+    pub fn start(&self) {
         let scanner = self.clone();
-        tokio::spawn(async move {
+        let handle = tokio::spawn(async move {
             scanner.sync_loop().await;
         });
+        *self.task_handle.lock().unwrap() = Some(handle);
     }

-    pub fn stop(&self) {
+    pub async fn stop(&self) {
         self.cancellation_token.cancel();
+        if let Some(handle) = self.task_handle.lock().unwrap().take() {
+            let _ = handle.await;
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/collateral/collateral_scan.rs` around lines 45
- 56, The start() method currently spawns a tokio task and drops the JoinHandle
so shutdown cannot await completion; change the struct to hold an
Option<JoinHandle<()>> (behind a Mutex/RwLock if needed) and update start() to
store the JoinHandle returned by tokio::spawn; change stop() into an async fn
stop(&self) that first calls self.cancellation_token.cancel() and then takes and
.awaits the stored JoinHandle (handling JoinError/None appropriately) to ensure
sync_loop() completes before shutdown; reference the start(), stop(),
sync_loop(), cancellation_token, and the JoinHandle storage when making the
changes.
crates/basilica-validator/src/service.rs (1)

443-458: Duplicate CollateralEvaluator instantiation.

A CollateralEvaluator is already created at line 312 within init_collateral_components and passed to CollateralManager. Here, a second instance is created for the Collateral scanner. Consider reusing the evaluator from init_collateral_components by passing it through TaskInputs to avoid inconsistent state if the evaluator ever becomes stateful.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/service.rs` around lines 443 - 458, The code
creates a second CollateralEvaluator for the Collateral scanner causing
duplicate instances; instead reuse the evaluator constructed in
init_collateral_components by threading it through TaskInputs and using that
shared Arc<CollateralEvaluator> for both CollateralManager and the Collateral
scanner. Update init_collateral_components to attach the evaluator to TaskInputs
(e.g., inputs.collateral_evaluator), remove the local
Arc::new(CollateralEvaluator::new(...)) in the scanner block, and construct
Collateral with the existing inputs.collateral_evaluator so both components
share the same evaluator instance.
crates/basilica-validator/src/persistence/collateral_persistence.rs (1)

192-206: Inefficient deletion pattern fetches all rows then iterates.

The current approach fetches all existing DB rows, then iterates checking against a HashSet. For large collateral sets, this could be slow. Consider using a single DELETE statement with a subquery or temporary table, or marking rows with a sync timestamp and deleting stale ones.

♻️ Alternative approach using sync timestamp
+        // Delete rows that weren't updated in this sync (stale)
+        sqlx::query("DELETE FROM collateral_status WHERE updated_at < ?")
+            .bind(&now)
+            .execute(&mut *tx)
+            .await?;
-        // Delete DB rows that are no longer on-chain (zero-balance nodes)
-        let db_nodes: Vec<(String, String)> =
-            sqlx::query_as("SELECT hotkey, node_id FROM collateral_status")
-                .fetch_all(&mut *tx)
-                .await?;
-
-        for (hotkey, node_id) in &db_nodes {
-            if !on_chain_keys.contains(&(hotkey.clone(), node_id.clone())) {
-                sqlx::query("DELETE FROM collateral_status WHERE hotkey = ? AND node_id = ?")
-                    .bind(hotkey)
-                    .bind(node_id)
-                    .execute(&mut *tx)
-                    .await?;
-            }
-        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/persistence/collateral_persistence.rs` around
lines 192 - 206, The loop that selects all rows then deletes them one-by-one is
inefficient; instead perform a single set-based delete against collateral_status
using the on_chain_keys collection: either (A) create a temporary table (or use
a table-valued parameter) and bulk insert all (hotkey,node_id) from
on_chain_keys then run one DELETE ... WHERE (hotkey,node_id) NOT IN (SELECT
hotkey,node_id FROM temp_table) using the same &mut *tx transaction, or (B)
add/update a sync_timestamp column for collateral_status, write all
on_chain_keys by upsert to set the current timestamp, then execute one DELETE
FROM collateral_status WHERE sync_timestamp <> current_sync_ts; reference the
variables on_chain_keys, tx and the collateral_status table in
collateral_persistence.rs and replace the fetch_all + per-row DELETE logic with
the single set-based DELETE approach.
crates/basilica-validator/src/rental/mod.rs (1)

567-579: Consider removing unused parameters.

The _gpu_category and _gpu_count parameters are not used in the ranking logic. If they're not needed for future extensibility, consider removing them to avoid confusion.

♻️ Proposed cleanup
     async fn rank_bid_candidates(
         &self,
         candidates: Vec<NodeBidCandidate>,
-        _gpu_category: &str,
-        _gpu_count: u32,
     ) -> Vec<NodeBidCandidate> {

And update the call site accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/rental/mod.rs` around lines 567 - 579, The
rank_bid_candidates function contains unused parameters _gpu_category and
_gpu_count; remove these parameters from the async fn rank_bid_candidates
signature and update all call sites that invoke rank_bid_candidates (and any
trait declarations or impl blocks that define it) to stop passing gpu_category
and gpu_count so the function only takes &self and candidates:
Vec<NodeBidCandidate>; ensure you also update any trait/interface definitions
(if rank_bid_candidates is part of a trait) to keep signatures consistent across
implementations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ed11d3d-9cbd-4854-b224-20f28708be60

📥 Commits

Reviewing files that changed from the base of the PR and between ec02285 and 0b3f052.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (73)
  • .gitignore
  • Cargo.toml
  • crates/basilica-cli/Cargo.toml
  • crates/basilica-cli/src/cli/args.rs
  • crates/basilica-cli/src/cli/commands.rs
  • crates/basilica-cli/src/cli/handlers/collateral.rs
  • crates/basilica-cli/src/cli/handlers/mod.rs
  • crates/basilica-common/examples/compute_node_id.rs
  • crates/basilica-miner/src/main.rs
  • crates/basilica-miner/tests/mod.rs
  • crates/basilica-validator/migrations/017_dual_collateral_event_state.sql
  • crates/basilica-validator/migrations/018_collateral_reclaims_index.sql
  • crates/basilica-validator/migrations/019_add_collateral_preference_to_miner_nodes.sql
  • crates/basilica-validator/src/basilica_api/mod.rs
  • crates/basilica-validator/src/bittensor_core/weight_setter.rs
  • crates/basilica-validator/src/collateral/collateral_scan.rs
  • crates/basilica-validator/src/collateral/evaluator.rs
  • crates/basilica-validator/src/collateral/grace_tracker.rs
  • crates/basilica-validator/src/collateral/manager.rs
  • crates/basilica-validator/src/collateral/mod.rs
  • crates/basilica-validator/src/collateral/slash_executor.rs
  • crates/basilica-validator/src/config/collateral.rs
  • crates/basilica-validator/src/grpc/registration.rs
  • crates/basilica-validator/src/persistence/collateral_grace.rs
  • crates/basilica-validator/src/persistence/collateral_persistence.rs
  • crates/basilica-validator/src/persistence/miner_nodes.rs
  • crates/basilica-validator/src/persistence/mod.rs
  • crates/basilica-validator/src/rental/mod.rs
  • crates/basilica-validator/src/service.rs
  • crates/basilica-validator/tests/collateral_e2e.rs
  • crates/basilica-validator/tests/collateral_slash_flow.rs
  • crates/collateral-contract/CLAUDE.md
  • crates/collateral-contract/README.md
  • crates/collateral-contract/deploy.sh
  • crates/collateral-contract/flow.sh
  • crates/collateral-contract/foundry.toml
  • crates/collateral-contract/query.sh
  • crates/collateral-contract/script/DeployUpgradeable.s.sol
  • crates/collateral-contract/src/Collateral.json
  • crates/collateral-contract/src/Collateral.sol
  • crates/collateral-contract/src/CollateralUpgradableV2ABI.json
  • crates/collateral-contract/src/CollateralUpgradeable.sol
  • crates/collateral-contract/src/CollateralUpgradeableABI.json
  • crates/collateral-contract/src/CollateralUpgradeableV2.sol
  • crates/collateral-contract/src/config.rs
  • crates/collateral-contract/src/lib.rs
  • crates/collateral-contract/src/main.rs
  • crates/collateral-contract/src/tests.rs
  • crates/collateral-contract/test/CollateralBasic.t.sol
  • crates/collateral-contract/test/CollateralTestnet.t.sol
  • crates/collateral-contract/test/CollateralUpgradeable.t.sol
  • crates/collateral-contract/test/IStakingIntegration.t.sol
  • crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol
  • crates/collateral-contract/test/NodeOwnershipEdgeCases.tree
  • crates/collateral-contract/test/mocks/CollateralUpgradeableUpgradeMock.sol
  • crates/collateral-contract/update_abi.py
  • docs/collateral-contract.md
  • docs/miner.md
  • scripts/collateral/.env.local.example
  • scripts/collateral/README.md
  • scripts/collateral/bigint_compare.py
  • scripts/collateral/e2e-validator-test.sh
  • scripts/collateral/integration-test.sh
  • scripts/collateral/setup-localnet-env.sh
  • scripts/lib/common.sh
  • scripts/localnet/configs/validator.example.toml
  • scripts/localnet/docker-compose.yml
  • scripts/localnet/init-subnet.sh
  • scripts/localnet/register-nodes.sh
  • scripts/localnet/start.sh
  • scripts/localnet/stop.sh
  • scripts/subtensor-local/docker-compose.yml
  • scripts/subtensor-local/init.py
💤 Files with no reviewable changes (10)
  • crates/basilica-validator/src/collateral/mod.rs
  • crates/basilica-validator/src/persistence/mod.rs
  • crates/collateral-contract/script/DeployUpgradeable.s.sol
  • crates/basilica-validator/src/collateral/grace_tracker.rs
  • crates/collateral-contract/src/CollateralUpgradableV2ABI.json
  • crates/collateral-contract/test/CollateralTestnet.t.sol
  • crates/basilica-validator/src/persistence/collateral_grace.rs
  • crates/collateral-contract/src/CollateralUpgradeableV2.sol
  • crates/collateral-contract/src/Collateral.sol
  • crates/collateral-contract/src/Collateral.json

- Remove `deposit_with_config` which was identical to `deposit` with zero callers
- Replace `block_in_place`/`block_on` with direct `.await` in `start_rental_inner`
  since it is already async (H-1 and H-2 from PR review)
- Share CollateralEvaluator via Arc instead of creating two instances
- Replace f64 with Decimal in CLI alpha/rao conversions
- Lazy-compile regex in extract_gpu_memory_gb using once_cell
- Use config warning_threshold_multiplier in warning message
- Remove unnecessary async from evaluate()
- Clean up unused_imports allow and re-export in collateral/mod.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
crates/basilica-cli/src/cli/handlers/collateral.rs (3)

79-83: ⚠️ Potential issue | 🟠 Major

Honor the collateral RPC override here.

resolve_params() still passes None as the RPC URL, so BASILICA_COLLATERAL_RPC_URL never takes effect and local/custom endpoints stay pinned to the built-in network defaults.

🛠️ Suggested fix
     // Resolve contract address: env var -> built-in default
     let contract_address = std::env::var("BASILICA_COLLATERAL_CONTRACT_ADDRESS").ok();
+    let rpc_url = std::env::var("BASILICA_COLLATERAL_RPC_URL").ok();
 
-    let network_config = CollateralNetworkConfig::from_network(&network, contract_address, None)
-        .map_err(|e| eyre!("{}", e))?;
+    let network_config =
+        CollateralNetworkConfig::from_network(&network, contract_address, rpc_url)
+            .map_err(|e| eyre!("{}", e))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-cli/src/cli/handlers/collateral.rs` around lines 79 - 83, The
code calls CollateralNetworkConfig::from_network(&network, contract_address,
None) so the BASILICA_COLLATERAL_RPC_URL override is ignored; update the call to
pass the resolved RPC URL instead of None by reading
std::env::var("BASILICA_COLLATERAL_RPC_URL").ok() (or use the existing
resolve_params() result) and pass that Option<String> as the third argument to
CollateralNetworkConfig::from_network so custom/local endpoints are honored.

165-176: ⚠️ Potential issue | 🟠 Major

Keep transfer and deposit amounts as strings until unit scaling.

Both flows still parse user input as f64 first. By the time Decimal::try_from(amount) or tao_to_wei(amount) runs, the original decimal string has already been rounded, and large/exponential inputs can overflow or saturate before you ever hit the chain.

Also applies to: 270-301, 661-669, 672-766

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-cli/src/cli/handlers/collateral.rs` around lines 165 - 176,
The prompt currently parses user input to f64 (prompt_amount) which loses
precision and can overflow; change the prompt to read and validate the raw
string instead (e.g., return Result<String, CliError> or add a new
prompt_amount_str) by using
Input::<String>::with_theme(...).with_prompt(...).validate_with(|input: &String|
{ try parsing with Decimal::from_str(input) or check regex and ensure >0 }) and
only convert to Decimal or wei (Decimal::try_from / tao_to_wei) at the callsites
that perform unit scaling; update all usages (the other prompt sites noted
around the ranges 270-301, 661-669, 672-766) to accept the string and perform
precise conversion later. Ensure error messages remain user-friendly and
preserve the original decimal string for scaling.

367-377: ⚠️ Potential issue | 🟠 Major

Status output still hides TAO collateral.

This handler already has access to both assets, but the TAO side is dropped immediately (let (_tao, alpha)) and the rendered rows only show alpha. Mixed-collateral nodes will be under-reported in status and reclaim selection output.

Also applies to: 387-398, 409-418, 493-502

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-cli/src/cli/handlers/collateral.rs` around lines 367 - 377,
The status output drops TAO collateral because the tuple destructuring uses let
(_tao, alpha) and CollateralRow only contains alpha_collateral; update
CollateralRow (add a tao_collateral field with a tabled rename like "TAO") and
change all destructurings from let (_tao, alpha) to let (tao, alpha) so you
capture both values, then populate both tao_collateral and alpha_collateral when
building rows in the handlers (the same changes are needed in the other
occurrences mentioned around the blocks that previously ignored TAO: 387-398,
409-418, and 493-502). Ensure the table rendering code references the new
tao_collateral field so mixed-collateral nodes display both values.
crates/basilica-validator/src/persistence/miner_nodes.rs (1)

931-944: ⚠️ Potential issue | 🟠 Major

Return one deterministic GPU summary per node before batching updates.

This query groups by gua.gpu_name, so a mixed-GPU node produces multiple NodeGpuInfo rows with the same node_id. batch_update_collateral_preferences() then overwrites that node several times, and the final preference depends on whichever GPU-name row happens to be processed last.

Also applies to: 954-960, 978-983

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/persistence/miner_nodes.rs` around lines 931 -
944, The SQL groups by gua.gpu_name which yields multiple rows per node when a
node has mixed GPUs, causing batch_update_collateral_preferences() to overwrite
the same node multiple times nondeterministically; fix by making the query
return exactly one deterministic GPU summary per node—remove gua.gpu_name from
the GROUP BY and replace it with an aggregate (e.g., MAX(gua.gpu_name) AS
gpu_name) while keeping COUNT(DISTINCT gua.gpu_uuid) AS gpu_count and grouping
only by me.miner_id, me.node_id, m.hotkey (update the query string assigned to
query), and apply the same change to the other similar queries used by
batch_update_collateral_preferences() so each node_id yields a single row.
🧹 Nitpick comments (1)
crates/basilica-validator/src/rental/mod.rs (1)

567-579: Consider removing unused parameters.

The _gpu_category and _gpu_count parameters are unused in the current implementation. If they're not planned for near-term use, consider removing them to reduce API surface noise.

However, if these are retained for future collateral-based ranking enhancements (e.g., checking collateral sufficiency per GPU requirements), the current approach is acceptable.

♻️ Optional: Remove unused parameters
 async fn rank_bid_candidates(
     &self,
     candidates: Vec<NodeBidCandidate>,
-    _gpu_category: &str,
-    _gpu_count: u32,
 ) -> Vec<NodeBidCandidate> {

And update the call site at line 477:

-            .rank_bid_candidates(candidates, &request.gpu_category, request.gpu_count)
+            .rank_bid_candidates(candidates)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/rental/mod.rs` around lines 567 - 579, Remove
the unused parameters _gpu_category and _gpu_count from the async fn
rank_bid_candidates signature in mod.rs and update all related
declarations/callers (e.g., the trait/impl that declares rank_bid_candidates and
any places that invoke it) so they no longer accept or pass those arguments;
ensure function references like rank_bid_candidates and the corresponding
trait/method definitions (e.g., the NodeRental/impl) are kept consistent across
the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/basilica-cli/src/cli/handlers/collateral.rs`:
- Around line 399-420: The all-nodes branch ignores params.hotkey so status
--hotkey doesn't filter results; update the code after
collateral_contract::get_all_collaterals(...) and the pending-reclaim handling
(the blocks building Vec<CollateralRow> and the pending-reclaim iteration around
lines ~430-468) to filter `all` (and pending reclaim list) by `params.hotkey`
when Some(hk) — e.g., compare hotkey_to_ss58(&n.miner_hotkey) or the raw
miner_hotkey bytes against params.hotkey (normalize both to the same form)
before mapping to CollateralRow; ensure the same filter logic is applied in both
the "all" collaterals mapping and the pending reclaim mapping so --hotkey
consistently limits displayed rows.

In `@crates/basilica-validator/src/collateral/collateral_scan.rs`:
- Around line 116-121: Nodes, reclaims and last_scanned_block_number are part of
one logical snapshot but are persisted with three separate calls
(sync_all_collateral_nodes, sync_all_reclaims, update_last_scanned_block_number)
which can leave readers with a partial/mixed snapshot on failure; fix this by
performing all three writes inside a single database transaction: add or use a
transactional API on the persistence layer (e.g., persistence.transaction or a
new method like persist_snapshot_transaction) that accepts the nodes, reclaims
and snapshot_block and commits them atomically, and replace the three separate
calls with one transactional call that writes nodes, writes reclaims, and
updates last_scanned_block_number within the same transaction.
- Around line 68-75: The loop currently calls compute_and_store_preferences()
even when self.sync_collateral_state().await fails, allowing preference writes
from stale snapshots; change the control flow in the interval tick handler (the
block containing self.sync_collateral_state() and
compute_and_store_preferences()) so that compute_and_store_preferences() only
runs when sync_collateral_state() returns Ok — i.e., after calling
self.sync_collateral_state().await, if it Errs log the error and skip calling
compute_and_store_preferences() (and similarly update the analogous block later
in the file around compute_and_store_preferences() in the 126-160 region) to
avoid refreshing eligibility from stale DB snapshots.

In `@crates/basilica-validator/src/service.rs`:
- Around line 491-493: The shutdown currently calls
handles.collateral_scanner.stop() which only cancels a token and returns
immediately, risking concurrent DB ops during teardown; change the scanner
lifecycle so shutdown waits for the background task: either modify
Collateral::start() to return and store a JoinHandle (add the handle to
RuntimeHandles) and then on shutdown call .abort() or .await the JoinHandle, or
make Collateral::stop() async and have it await the spawned sync_loop task
completion (join it) before returning; update callers of start()/stop() (e.g.,
where handles.collateral_scanner is created and where it is stopped) to use the
chosen pattern so the scanner's sync_loop is fully finished before proceeding
with persistence teardown.

---

Duplicate comments:
In `@crates/basilica-cli/src/cli/handlers/collateral.rs`:
- Around line 79-83: The code calls
CollateralNetworkConfig::from_network(&network, contract_address, None) so the
BASILICA_COLLATERAL_RPC_URL override is ignored; update the call to pass the
resolved RPC URL instead of None by reading
std::env::var("BASILICA_COLLATERAL_RPC_URL").ok() (or use the existing
resolve_params() result) and pass that Option<String> as the third argument to
CollateralNetworkConfig::from_network so custom/local endpoints are honored.
- Around line 165-176: The prompt currently parses user input to f64
(prompt_amount) which loses precision and can overflow; change the prompt to
read and validate the raw string instead (e.g., return Result<String, CliError>
or add a new prompt_amount_str) by using
Input::<String>::with_theme(...).with_prompt(...).validate_with(|input: &String|
{ try parsing with Decimal::from_str(input) or check regex and ensure >0 }) and
only convert to Decimal or wei (Decimal::try_from / tao_to_wei) at the callsites
that perform unit scaling; update all usages (the other prompt sites noted
around the ranges 270-301, 661-669, 672-766) to accept the string and perform
precise conversion later. Ensure error messages remain user-friendly and
preserve the original decimal string for scaling.
- Around line 367-377: The status output drops TAO collateral because the tuple
destructuring uses let (_tao, alpha) and CollateralRow only contains
alpha_collateral; update CollateralRow (add a tao_collateral field with a tabled
rename like "TAO") and change all destructurings from let (_tao, alpha) to let
(tao, alpha) so you capture both values, then populate both tao_collateral and
alpha_collateral when building rows in the handlers (the same changes are needed
in the other occurrences mentioned around the blocks that previously ignored
TAO: 387-398, 409-418, and 493-502). Ensure the table rendering code references
the new tao_collateral field so mixed-collateral nodes display both values.

In `@crates/basilica-validator/src/persistence/miner_nodes.rs`:
- Around line 931-944: The SQL groups by gua.gpu_name which yields multiple rows
per node when a node has mixed GPUs, causing
batch_update_collateral_preferences() to overwrite the same node multiple times
nondeterministically; fix by making the query return exactly one deterministic
GPU summary per node—remove gua.gpu_name from the GROUP BY and replace it with
an aggregate (e.g., MAX(gua.gpu_name) AS gpu_name) while keeping COUNT(DISTINCT
gua.gpu_uuid) AS gpu_count and grouping only by me.miner_id, me.node_id,
m.hotkey (update the query string assigned to query), and apply the same change
to the other similar queries used by batch_update_collateral_preferences() so
each node_id yields a single row.

---

Nitpick comments:
In `@crates/basilica-validator/src/rental/mod.rs`:
- Around line 567-579: Remove the unused parameters _gpu_category and _gpu_count
from the async fn rank_bid_candidates signature in mod.rs and update all related
declarations/callers (e.g., the trait/impl that declares rank_bid_candidates and
any places that invoke it) so they no longer accept or pass those arguments;
ensure function references like rank_bid_candidates and the corresponding
trait/method definitions (e.g., the NodeRental/impl) are kept consistent across
the codebase.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: afea8583-baa1-4e6b-8531-43b396a81a39

📥 Commits

Reviewing files that changed from the base of the PR and between 0b3f052 and 894b51e.

📒 Files selected for processing (12)
  • PR_372_REVIEW.md
  • crates/basilica-cli/src/cli/handlers/collateral.rs
  • crates/basilica-validator/Cargo.toml
  • crates/basilica-validator/src/collateral/collateral_scan.rs
  • crates/basilica-validator/src/collateral/evaluator.rs
  • crates/basilica-validator/src/collateral/manager.rs
  • crates/basilica-validator/src/collateral/mod.rs
  • crates/basilica-validator/src/persistence/miner_nodes.rs
  • crates/basilica-validator/src/rental/mod.rs
  • crates/basilica-validator/src/service.rs
  • crates/basilica-validator/tests/collateral_e2e.rs
  • crates/collateral-contract/src/lib.rs

…tworks

Remove hardcoded default contract address and deployed block constants.
Testnet and Local networks now require an explicit contract address,
and scan status initializes from block 0. Also wire through the
BASILICA_COLLATERAL_RPC_URL env var in the CLI.
Eliminates floating-point precision loss in tao_to_wei and throughout
the deposit/send flows by parsing CLI amounts as strings directly into
rust_decimal::Decimal, matching the pattern already used in alpha_to_rao.
The manual test script is no longer needed.
Use separate min_alpha_collateral_increase and asymmetric deposit toggles
to catch potential ABI ordering/storage bugs that identical values would mask.
Replace default placeholder values in deploy.sh with mandatory env vars
using bash :? syntax. Suppress asm-keccak256 foundry lint. Rename local
test variables to camelCase.
Add upgrade.sh for safe UUPS proxy upgrades with pre-flight checks
(role verification, version comparison) and post-upgrade validation.
Restructure README deployment section into clear env var tables.
Add missing rules covering initial value declarations, parent contract
gaps, parent initializer calls, upgrade mechanism preservation,
selfdestruct/delegatecall dangers, ABI compatibility, fork testing,
and multisig recommendations.
Simplify `collateral status` to always show all collaterals and reclaims.
The single-node query branch and its helpers (require_hotkey, parse_node_id)
are removed as they are no longer needed.
Prevent compute_and_store_preferences() from running after a failed
sync_collateral_state(), avoiding preference writes derived from stale
DB snapshots.
Store the JoinHandle from the collateral scanner's spawned task and
await it during shutdown so the sync_loop finishes before persistence
teardown begins.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (11)
crates/collateral-contract/foundry.toml (1)

5-6: EVM version split between test and deployment profiles may be unintentional.

The default profile uses evm_version = "cancun" (for tests), while [profile.local] uses evm_version = "london" (for deployment via deploy.sh). Verification shows the contract does not currently use Cancun-specific opcodes, so there's no immediate compatibility issue. However, the mismatch could be confusing or risky for future maintenance if new code inadvertently relies on Cancun features.

Either align the EVM versions to match, or add a comment to foundry.toml explaining why the profiles intentionally target different EVM versions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/foundry.toml` around lines 5 - 6, The evm_version
is inconsistent between the default profile (evm_version = "cancun") and
[profile.local] (evm_version = "london") in foundry.toml, which can be
confusing; either set both to the same value (e.g., change [profile.local]
evm_version to "cancun" or change the default to "london") or add a clear
comment in foundry.toml next to the evm_version settings explaining the
intentional difference and why deploy.sh targets "london" so future changes
won't accidentally rely on Cancun-only opcodes.
crates/basilica-validator/src/persistence/simple_persistence.rs (1)

99-101: Note: last_scanned_block_number is currently write-only.

The initialization binds 0i64, but per context snippet 1 (collateral_scan.rs:85-128), sync_collateral_state always fetches the current finalized block via RPC and never reads the stored value. The get_last_scanned_block_number method exists but appears unused.

This is likely intentional for the RPC-based full-sync architecture, but consider either:

  1. Documenting this as a placeholder for future incremental sync, or
  2. Removing the unused storage if full-sync is the permanent design.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/persistence/simple_persistence.rs` around lines
99 - 101, The code inserts a hardcoded 0 into last_scanned_block_number (see
simple_persistence::insert of last_scanned_block_number) while
sync_collateral_state (in collateral_scan.rs) always reads finalized block from
RPC and never calls get_last_scanned_block_number, leaving this field
write-only; either document this as an intentional placeholder for future
incremental sync (update comments on get_last_scanned_block_number,
simple_persistence and collateral_scan.rs to explain why we persist but don’t
read) or remove the unused storage and associated methods (remove
last_scanned_block_number column handling and
get_last_scanned_block_number/insert logic in simple_persistence and any schema
migration), and ensure tests and schema migrations are updated accordingly.
crates/basilica-validator/src/persistence/collateral_persistence.rs (1)

80-88: Consider using an enum instead of string matching for column selection.

The column parameter uses string matching which could silently break if column names change. An enum would provide compile-time safety.

♻️ Suggested improvement
enum CollateralColumn {
    Tao,
    Alpha,
}

impl CollateralColumn {
    fn as_str(&self) -> &'static str {
        match self {
            Self::Tao => "tao_collateral",
            Self::Alpha => "alpha_collateral",
        }
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-validator/src/persistence/collateral_persistence.rs` around
lines 80 - 88, Replace the runtime string matching on the `column` parameter
with a typed enum to provide compile-time safety: add an enum (e.g., `enum
CollateralColumn { Tao, Alpha }`) with a helper like `fn as_str(&self) ->
&'static str` that returns `"tao_collateral"` or `"alpha_collateral"`, change
the function that currently takes `column: &str` to take `column:
CollateralColumn` (or parse the incoming string to this enum at the call site),
and then use `column.as_str()` (or match on the enum) to build the `query`
instead of matching raw strings and returning the `Unsupported collateral
column` error; update any call sites to pass the enum or handle parse errors
appropriately.
crates/collateral-contract/src/config.rs (1)

34-41: Consider case-insensitive parsing for network names.

The FromStr implementation only accepts lowercase variants ("mainnet", "testnet", "local"), but users might reasonably provide "Mainnet" or "MAINNET". This could cause unexpected failures in config files or environment variables.

♻️ Suggested fix
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
+        match s.to_lowercase().as_str() {
             "mainnet" => Ok(Network::Mainnet),
             "testnet" => Ok(Network::Testnet),
             "local" => Ok(Network::Local),
             other => Err(anyhow::anyhow!("Unsupported collateral network: {}", other)),
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/src/config.rs` around lines 34 - 41, The FromStr
implementation for Network (fn from_str) only matches lowercase literals; update
it to parse case-insensitively by normalizing the input or using
eq_ignore_ascii_case comparisons (e.g., compare s.to_lowercase().as_str()
against "mainnet"/"testnet"/"local" or use s.eq_ignore_ascii_case("mainnet")
etc.), and keep the existing error path returning the original input on failure
(Err(anyhow::anyhow!("Unsupported collateral network: {}", other))) so
unexpected-casing inputs like "Mainnet" or "MAINNET" succeed.
crates/basilica-cli/src/cli/handlers/collateral.rs (1)

373-400: status command shows all collaterals without hotkey filtering.

The handle_status function fetches and displays all collaterals regardless of the user's wallet. Consider filtering by the caller's EVM address (similar to handle_reclaim at line 458) to show only relevant entries, or add a --all flag for the full list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/basilica-cli/src/cli/handlers/collateral.rs` around lines 373 - 400,
handle_status currently calls collateral_contract::get_all_collaterals and
prints every collateral; change it to either filter results by the caller's EVM
address (like handle_reclaim does) or honor a new --all flag on
CollateralParams. Specifically: add a boolean flag on CollateralParams (e.g.,
all_collaterals) or reuse an existing caller evm field, then in handle_status
after fetching all (collateral_contract::get_all_collaterals) filter the
iterator by matching each collateral's evm/caller field to the wallet/caller
address used in handle_reclaim (use the same wallet-to-evm resolution logic as
handle_reclaim) when the flag is false, and only skip filtering when the new
--all flag is true; update the CLI param parsing accordingly so the default
remains filtered by caller.
crates/collateral-contract/test/CollateralBasic.t.sol (2)

430-491: Storage slot manipulation test is thorough but fragile.

This test validates an important fix (alpha-only reclaim denial), but relies on hardcoded storage slot indices (lines 459, 473). The inline comments documenting the layout are helpful, but consider adding a comment noting this test needs updating if the contract storage layout changes, or adding a runtime assertion to verify the slot calculation.

The test logic is correct: it mutates a TAO reclaim into an alpha-only reclaim and verifies that denyReclaimRequest succeeds (the fix changed amount == 0 to amount == 0 && alphaAmount == 0).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/CollateralBasic.t.sol` around lines 430 -
491, The test testDenyAlphaOnlyReclaim uses hardcoded storage-slot math
(reclaimsSlot, baseSlot, amountSlot, alphaAmountSlot) which is fragile; add a
runtime assertion that the computed slots actually map to the struct fields by
comparing vm.load(address(collateral), amountSlot) and
vm.load(address(collateral), alphaAmountSlot) to the values read from
collateral.reclaims(0) before mutating them, and add a short comment above the
slot calculations warning that these indices must be updated if the contract
storage layout changes (or replace with a helper that computes the keccak slots
consistently).

1278-1290: Upgrade test does not verify persistence of new state variables.

The test verifies the upgrade mechanism works, but the CollateralUpgradeableUpgradeMock.initializeUpgradeMock() is empty (see context snippet 2). This means the test doesn't verify that taoDepositsEnabled and alphaDepositsEnabled persist correctly across upgrades.

Consider adding assertions to verify state persistence:

💡 Suggested enhancement
 function testUpgrade() public {
+    // Verify pre-upgrade state
+    assertTrue(collateral.taoDepositsEnabled());
+    assertTrue(collateral.alphaDepositsEnabled());
+    
     CollateralUpgradeableUpgradeMock newImplementation = new CollateralUpgradeableUpgradeMock();

     vm.expectEmit(true, true, false, false, address(collateral));
     emit ContractUpgraded(2, address(newImplementation));

     vm.prank(ADMIN);
     collateral.upgradeToAndCall(address(newImplementation), "");

     assertEq(collateral.getVersion(), 2);
+    
+    // Verify state persists after upgrade
+    assertTrue(collateral.taoDepositsEnabled());
+    assertTrue(collateral.alphaDepositsEnabled());
+    assertEq(collateral.netuid(), NETUID);
+    assertEq(collateral.trustee(), TRUSTEE);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/CollateralBasic.t.sol` around lines 1278 -
1290, The testUpgrade currently upgrades to CollateralUpgradeableUpgradeMock but
never verifies persistence of new storage vars; modify the test to (1) set known
pre-upgrade values for taoDepositsEnabled and alphaDepositsEnabled on the
original collateral, (2) encode a call to
CollateralUpgradeableUpgradeMock.initializeUpgradeMock (or implement that
function to set the expected post-upgrade values) and pass that calldata into
collateral.upgradeToAndCall(address(newImplementation), initCalldata) under
vm.prank(ADMIN), and (3) assert collateral.taoDepositsEnabled() and
collateral.alphaDepositsEnabled() both before and after the upgrade and assert
getVersion() == 2 after upgrade. Ensure you reference the initializeUpgradeMock
function on CollateralUpgradeableUpgradeMock and the collateral.upgradeToAndCall
and collateral.getVersion methods when making these changes.
crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol (2)

793-799: Non-deterministic getStake mock could cause flaky tests if used.

The mock returns type(uint256).max - gasleft() which varies based on execution context. While current tests don't appear to use this, future tests relying on getStake will have non-deterministic behavior. Consider returning a fixed value or making it configurable.

💡 Suggested fix for deterministic behavior
 contract _StakingV2Mock {
+    uint256 public stakeReturnValue = 1000 ether;
+
     function transferStake(bytes32, bytes32, uint256, uint256, uint256) external payable {}
     function moveStake(bytes32, bytes32, uint256, uint256, uint256) external payable {}
     function getStake(bytes32, bytes32, uint256) external view returns (uint256) {
-        return type(uint256).max - gasleft();
+        return stakeReturnValue;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol` around lines
793 - 799, The getStake implementation in _StakingV2Mock is non-deterministic
(returns type(uint256).max - gasleft()); change it to return a deterministic
value by replacing that expression with a fixed constant or a configurable state
variable (e.g., a uint256 stakeValue with a setter) so tests behave predictably;
update the _StakingV2Mock contract to include the constant or state + setter and
have getStake(bytes32, bytes32, uint256) return that deterministic stakeValue.

94-104: Storage slot assumption is fragile and may break on contract layout changes.

The helper _nodeToMinerSlot hardcodes slot 5 for nodeToMiner. While this works now, any reordering of state variables in CollateralUpgradeable will silently break these tests without compile-time errors. Consider adding a comment documenting the expected layout or adding a sanity check assertion in setUp().

💡 Suggested sanity check
 function setUp() public {
     // ... existing setup ...
     
+    // Sanity check: verify slot assumption by depositing and checking storage
+    // If this fails, the storage layout has changed and _nodeToMinerSlot needs updating
+    vm.prank(ALICE);
+    collateral.deposit{value: MIN_DEPOSIT}(HOTKEY_1, EXECUTOR_ID_1, ALPHA_HOTKEY, 0);
+    assertEq(
+        address(uint160(uint256(vm.load(address(collateral), _nodeToMinerSlot(HOTKEY_1, EXECUTOR_ID_1))))),
+        ALICE,
+        "Storage slot assumption for nodeToMiner is incorrect"
+    );
+    // Reset state for tests - or adjust setUp to account for this
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol` around lines 94
- 104, The test helper hardcodes slot 5 in _nodeToMinerSlot which is fragile;
add a sanity check in setUp() that verifies the hardcoded slot matches the
contract's actual mapping layout by using _seedNodeOwner(hotkey, nodeId, owner)
with a sentinel address and then reading back via the contract's accessor (e.g.,
call the CollateralUpgradeable mapping/getter for nodeToMiner using the same
hotkey/nodeId) to assert the stored owner equals the sentinel; if the read value
mismatches, fail the setup with a clear message so future contract layout
changes surface immediately.
crates/collateral-contract/src/tests.rs (2)

17-18: Differentiate the validator hotkey from the deposit hotkey in this test.

The initializer field here is the contract-wide validator hotkey, but the test later reuses the same [2u8; 32] value as the per-deposit alpha hotkey. With identical values, a wiring mix-up between those two paths would still pass. Use distinct names/values and assert the stored validator hotkey explicitly.

Also applies to: 86-87, 94-105, 116-126

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/src/tests.rs` around lines 17 - 18, The test
reuses the same bytes value for the contract-wide validator hotkey (initializer
parameter passed to initialize) and the per-deposit alpha hotkey, which masks
wiring bugs; change the test in tests.rs to use distinct byte values/names
(e.g., validator_hotkey_vs deposit_alpha_hotkey) when calling initialize and
when creating deposits, then add an explicit assertion that the contract-stored
validator hotkey (the initializer field passed to initialize) equals the
validator_hotkey value you set; update all affected blocks mentioned (around the
initialize call and deposit creation at the referenced locations) to use these
distinct symbols and assertions.

121-155: Exercise the disabled alpha path, not just the stored flag.

This test proves alphaDepositsEnabled was initialized to false, but it never attempts a non-zero alpha deposit under that configuration. A regression in deposit() that ignores the toggle would still pass here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/src/tests.rs` around lines 121 - 155, The test
currently only checks the alphaDepositsEnabled flag but never exercises the
disabled path; add a call to proxied.deposit using a non-zero alpha deposit
(e.g., FixedBytes::from_slice(&alpha_hotkey) with a non-zero value such as
min_alpha_collateral_increase or similar) while alphaDepositsEnabled is false
and assert that the call fails/reverts (expect an error/unwrap_err or revert
assertion) to ensure deposit() enforces the toggle; reference proxied.deposit,
alphaDepositsEnabled, alpha_hotkey, and min_alpha_collateral_increase when
adding this failing-case assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/basilica-cli/src/cli/handlers/collateral.rs`:
- Around line 138-141: The wei_to_tao function currently casts U256 -> u128 ->
f64 losing precision for large balances; change it to convert the U256 to a
Decimal (using the same Decimal type used for alpha conversions), perform the
division by Decimal::from_str("1e18") or Decimal::new accordingly, and return a
Decimal (or string) instead of f64; then update any callers/format strings that
used wei_to_tao (e.g., formatted with "{:.4}") to format the Decimal
consistently (matching alpha formatting) so precision is preserved for large TAO
balances.

In `@crates/basilica-validator/src/collateral/collateral_scan.rs`:
- Around line 199-213: The current use of unwrap_or_else on
persistence.get_alpha_collateral_amount masks DB errors and converts them into a
zero collateral value; change this so that failures from
get_alpha_collateral_amount are distinguished from a genuine “no collateral”
None: instead of swallowing the error in unwrap_or_else, propagate the error
(return Err) or convert it into a sentinel (e.g., collateral_lookup_failed) by
returning a Result<Option<U256>, _> up from this function, then handle that case
where collateral_alpha is computed (the u256_to_alpha call) so transient DB
failures do not default to Decimal::ZERO and cause incorrect Fallback decisions.
Ensure you update the call sites and signatures that use
get_alpha_collateral_amount/persistence and adjust the collateral_alpha logic to
branch on lookup error vs Some(None).

In `@crates/collateral-contract/README.md`:
- Around line 229-235: Replace the real-looking private key literal used for the
PRIVATE_KEY example in the README with an obviously fake placeholder (e.g.,
0x<private-key> or 0xDEADBEEF...), so update the export line that sets
PRIVATE_KEY in the README example and any matching usage in the adjacent
collateral-cli invocation; ensure the placeholder clearly cannot be mistaken for
a real secret to avoid gitleaks alerts while keeping the example shape the same.

In `@crates/collateral-contract/upgrade.sh`:
- Around line 11-13: The script currently prints the full RPC_URL which may
contain embedded secrets; update the upgrade.sh print logic to redact or replace
sensitive parts of RPC_URL before echoing (e.g., strip credentials and API keys
or derive and echo only the host) — locate the echo statements referencing the
RPC_URL variable (the lines with echo "RPC URL:       $RPC_URL") and change them
to use a redacted variable (compute a safe_rpc_url from RPC_URL by removing
userinfo/query params or only extracting the host) and print that instead.
- Around line 58-62: The script fails when invoked from outside its directory
because the forge command uses a relative path; modify upgrade.sh so it first
changes to the script directory (or runs the forge command via a subshell that
cds into the script directory) before running the FOUNDRY_PROFILE=local forge
create src/CollateralUpgradeable.sol:CollateralUpgradeable --rpc-url "$RPC_URL"
--private-key "$PRIVATE_KEY" --legacy --broadcast command; ensure you reference
the script's directory using a robust construct (e.g., dirname "$0") so
CollateralUpgradeable is resolved correctly when the script is run from the repo
root or CI.

---

Nitpick comments:
In `@crates/basilica-cli/src/cli/handlers/collateral.rs`:
- Around line 373-400: handle_status currently calls
collateral_contract::get_all_collaterals and prints every collateral; change it
to either filter results by the caller's EVM address (like handle_reclaim does)
or honor a new --all flag on CollateralParams. Specifically: add a boolean flag
on CollateralParams (e.g., all_collaterals) or reuse an existing caller evm
field, then in handle_status after fetching all
(collateral_contract::get_all_collaterals) filter the iterator by matching each
collateral's evm/caller field to the wallet/caller address used in
handle_reclaim (use the same wallet-to-evm resolution logic as handle_reclaim)
when the flag is false, and only skip filtering when the new --all flag is true;
update the CLI param parsing accordingly so the default remains filtered by
caller.

In `@crates/basilica-validator/src/persistence/collateral_persistence.rs`:
- Around line 80-88: Replace the runtime string matching on the `column`
parameter with a typed enum to provide compile-time safety: add an enum (e.g.,
`enum CollateralColumn { Tao, Alpha }`) with a helper like `fn as_str(&self) ->
&'static str` that returns `"tao_collateral"` or `"alpha_collateral"`, change
the function that currently takes `column: &str` to take `column:
CollateralColumn` (or parse the incoming string to this enum at the call site),
and then use `column.as_str()` (or match on the enum) to build the `query`
instead of matching raw strings and returning the `Unsupported collateral
column` error; update any call sites to pass the enum or handle parse errors
appropriately.

In `@crates/basilica-validator/src/persistence/simple_persistence.rs`:
- Around line 99-101: The code inserts a hardcoded 0 into
last_scanned_block_number (see simple_persistence::insert of
last_scanned_block_number) while sync_collateral_state (in collateral_scan.rs)
always reads finalized block from RPC and never calls
get_last_scanned_block_number, leaving this field write-only; either document
this as an intentional placeholder for future incremental sync (update comments
on get_last_scanned_block_number, simple_persistence and collateral_scan.rs to
explain why we persist but don’t read) or remove the unused storage and
associated methods (remove last_scanned_block_number column handling and
get_last_scanned_block_number/insert logic in simple_persistence and any schema
migration), and ensure tests and schema migrations are updated accordingly.

In `@crates/collateral-contract/foundry.toml`:
- Around line 5-6: The evm_version is inconsistent between the default profile
(evm_version = "cancun") and [profile.local] (evm_version = "london") in
foundry.toml, which can be confusing; either set both to the same value (e.g.,
change [profile.local] evm_version to "cancun" or change the default to
"london") or add a clear comment in foundry.toml next to the evm_version
settings explaining the intentional difference and why deploy.sh targets
"london" so future changes won't accidentally rely on Cancun-only opcodes.

In `@crates/collateral-contract/src/config.rs`:
- Around line 34-41: The FromStr implementation for Network (fn from_str) only
matches lowercase literals; update it to parse case-insensitively by normalizing
the input or using eq_ignore_ascii_case comparisons (e.g., compare
s.to_lowercase().as_str() against "mainnet"/"testnet"/"local" or use
s.eq_ignore_ascii_case("mainnet") etc.), and keep the existing error path
returning the original input on failure (Err(anyhow::anyhow!("Unsupported
collateral network: {}", other))) so unexpected-casing inputs like "Mainnet" or
"MAINNET" succeed.

In `@crates/collateral-contract/src/tests.rs`:
- Around line 17-18: The test reuses the same bytes value for the contract-wide
validator hotkey (initializer parameter passed to initialize) and the
per-deposit alpha hotkey, which masks wiring bugs; change the test in tests.rs
to use distinct byte values/names (e.g., validator_hotkey_vs
deposit_alpha_hotkey) when calling initialize and when creating deposits, then
add an explicit assertion that the contract-stored validator hotkey (the
initializer field passed to initialize) equals the validator_hotkey value you
set; update all affected blocks mentioned (around the initialize call and
deposit creation at the referenced locations) to use these distinct symbols and
assertions.
- Around line 121-155: The test currently only checks the alphaDepositsEnabled
flag but never exercises the disabled path; add a call to proxied.deposit using
a non-zero alpha deposit (e.g., FixedBytes::from_slice(&alpha_hotkey) with a
non-zero value such as min_alpha_collateral_increase or similar) while
alphaDepositsEnabled is false and assert that the call fails/reverts (expect an
error/unwrap_err or revert assertion) to ensure deposit() enforces the toggle;
reference proxied.deposit, alphaDepositsEnabled, alpha_hotkey, and
min_alpha_collateral_increase when adding this failing-case assertion.

In `@crates/collateral-contract/test/CollateralBasic.t.sol`:
- Around line 430-491: The test testDenyAlphaOnlyReclaim uses hardcoded
storage-slot math (reclaimsSlot, baseSlot, amountSlot, alphaAmountSlot) which is
fragile; add a runtime assertion that the computed slots actually map to the
struct fields by comparing vm.load(address(collateral), amountSlot) and
vm.load(address(collateral), alphaAmountSlot) to the values read from
collateral.reclaims(0) before mutating them, and add a short comment above the
slot calculations warning that these indices must be updated if the contract
storage layout changes (or replace with a helper that computes the keccak slots
consistently).
- Around line 1278-1290: The testUpgrade currently upgrades to
CollateralUpgradeableUpgradeMock but never verifies persistence of new storage
vars; modify the test to (1) set known pre-upgrade values for taoDepositsEnabled
and alphaDepositsEnabled on the original collateral, (2) encode a call to
CollateralUpgradeableUpgradeMock.initializeUpgradeMock (or implement that
function to set the expected post-upgrade values) and pass that calldata into
collateral.upgradeToAndCall(address(newImplementation), initCalldata) under
vm.prank(ADMIN), and (3) assert collateral.taoDepositsEnabled() and
collateral.alphaDepositsEnabled() both before and after the upgrade and assert
getVersion() == 2 after upgrade. Ensure you reference the initializeUpgradeMock
function on CollateralUpgradeableUpgradeMock and the collateral.upgradeToAndCall
and collateral.getVersion methods when making these changes.

In `@crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol`:
- Around line 793-799: The getStake implementation in _StakingV2Mock is
non-deterministic (returns type(uint256).max - gasleft()); change it to return a
deterministic value by replacing that expression with a fixed constant or a
configurable state variable (e.g., a uint256 stakeValue with a setter) so tests
behave predictably; update the _StakingV2Mock contract to include the constant
or state + setter and have getStake(bytes32, bytes32, uint256) return that
deterministic stakeValue.
- Around line 94-104: The test helper hardcodes slot 5 in _nodeToMinerSlot which
is fragile; add a sanity check in setUp() that verifies the hardcoded slot
matches the contract's actual mapping layout by using _seedNodeOwner(hotkey,
nodeId, owner) with a sentinel address and then reading back via the contract's
accessor (e.g., call the CollateralUpgradeable mapping/getter for nodeToMiner
using the same hotkey/nodeId) to assert the stored owner equals the sentinel; if
the read value mismatches, fail the setup with a clear message so future
contract layout changes surface immediately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3d80515-bda5-4619-b6d6-bd2f95cc0c56

📥 Commits

Reviewing files that changed from the base of the PR and between 894b51e and 8766abc.

📒 Files selected for processing (17)
  • crates/basilica-cli/src/cli/commands.rs
  • crates/basilica-cli/src/cli/handlers/collateral.rs
  • crates/basilica-validator/src/collateral/collateral_scan.rs
  • crates/basilica-validator/src/persistence/collateral_persistence.rs
  • crates/basilica-validator/src/persistence/simple_persistence.rs
  • crates/basilica-validator/src/service.rs
  • crates/collateral-contract/README.md
  • crates/collateral-contract/deploy.sh
  • crates/collateral-contract/flow.sh
  • crates/collateral-contract/foundry.toml
  • crates/collateral-contract/src/config.rs
  • crates/collateral-contract/src/tests.rs
  • crates/collateral-contract/test/CollateralBasic.t.sol
  • crates/collateral-contract/test/NodeOwnershipEdgeCases.t.sol
  • crates/collateral-contract/test/NodeOwnershipEdgeCases.tree
  • crates/collateral-contract/upgrade.sh
  • docs/collateral-contract.md
💤 Files with no reviewable changes (1)
  • crates/collateral-contract/flow.sh

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/collateral-contract/README.md`:
- Around line 364-377: Reorder and edit the examples so the README no longer
leads with unbounded full-chain scans; replace the first examples of
"collateral-cli --network testnet events scan" with bounded scans using the
--from-block and --to-block flags (e.g., recent range or specific block window)
and keep the provided "last 100 blocks" example as the primary pattern; add a
short note describing chunked backfills for long ranges (recommend splitting
large ranges into smaller windows and iterating with --from-block/--to-block to
avoid oversized eth_getLogs requests), and ensure the custom-contract and
JSON-format examples still show bounded scans or explicitly call out that
unbounded/default full-range scans may fail on real RPC providers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 101993c5-ab56-4788-8dd9-3fd6d14d7ffb

📥 Commits

Reviewing files that changed from the base of the PR and between 8766abc and 8d57d34.

📒 Files selected for processing (1)
  • crates/collateral-contract/README.md

The deploy script defaulted to 1 second, which is only useful for
testing. Localnet already overrides this via setup-localnet-env.sh,
so the deploy.sh default should be production-safe (86400s = 1 day).
Only alpha token collateral is supported for now. TAO deposits can
still be enabled by setting TAO_DEPOSITS_ENABLED=true.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/collateral-contract/deploy.sh (1)

15-19: Let callers choose the Foundry profile.

Line 15 and Line 44 force FOUNDRY_PROFILE=local, so a non-local deploy still compiles with the local profile even when the caller exports a different one. That is brittle for a generic deployment script.

Suggested fix
+FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-default}"
+
-impl_output="$(FOUNDRY_PROFILE=local forge create src/CollateralUpgradeable.sol:CollateralUpgradeable \
+impl_output="$(FOUNDRY_PROFILE="$FOUNDRY_PROFILE" forge create src/CollateralUpgradeable.sol:CollateralUpgradeable \
   --rpc-url "$RPC_URL" \
   --private-key "$PRIVATE_KEY" \
   --legacy \
   --broadcast)"
@@
-proxy_output="$(FOUNDRY_PROFILE=local forge create lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
+proxy_output="$(FOUNDRY_PROFILE="$FOUNDRY_PROFILE" forge create lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
   --rpc-url "$RPC_URL" \
   --private-key "$PRIVATE_KEY" \
   --legacy \
   --broadcast \
   --constructor-args "$IMPLEMENTATION_ADDRESS" "$INIT_DATA")"

Also applies to: 44-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/deploy.sh` around lines 15 - 19, The script forces
FOUNDRY_PROFILE=local inline before the forge commands (e.g., the impl_output
assignment that runs `forge create
src/CollateralUpgradeable.sol:CollateralUpgradeable`) which prevents callers
from supplying a different profile; remove the inline `FOUNDRY_PROFILE=local`
prefix (or replace it with a default like
`FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-local}"` at the top of the script) so the
forge invocations respect the environment-provided FOUNDRY_PROFILE, and apply
the same change to the second forge invocation block (the verify/broadcast
section around lines 44-49) so both impl_output and the later verify/broadcast
commands use the caller-specified profile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/collateral-contract/deploy.sh`:
- Around line 1-12: The deploy.sh script fails when run from repo root because
it uses project-relative forge paths (e.g.,
"src/CollateralUpgradeable.sol:CollateralUpgradeable" and
"lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy")
but never switches to the Foundry project directory; fix by adding a defensive
directory change near the top of deploy.sh (before any forge invocation) such as
changing into the script's directory (use dirname "$0" or similar) and erroring
out if the cd fails so all subsequent forge commands resolve foundry.toml and
the relative contract paths correctly.

In `@crates/collateral-contract/README.md`:
- Line 256: Replace the duplicated MD5-like digest used with the
--url-content-sha256 example in the README: the value
"d41d8cd98f00b204e9800998ecf8427ed41d8cd98f00b204e9800998ecf8427e" is not a
valid SHA-256; update the example to a genuine 64-hex SHA-256 digest (or
explicitly mark it as a placeholder like "<SHA256_HEX_PLACEHOLDER>") and make
the same change for the other two occurrences of this bogus digest in the file
so the README shows valid SHA-256 examples for the --url-content-sha256 flag.

---

Nitpick comments:
In `@crates/collateral-contract/deploy.sh`:
- Around line 15-19: The script forces FOUNDRY_PROFILE=local inline before the
forge commands (e.g., the impl_output assignment that runs `forge create
src/CollateralUpgradeable.sol:CollateralUpgradeable`) which prevents callers
from supplying a different profile; remove the inline `FOUNDRY_PROFILE=local`
prefix (or replace it with a default like
`FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-local}"` at the top of the script) so the
forge invocations respect the environment-provided FOUNDRY_PROFILE, and apply
the same change to the second forge invocation block (the verify/broadcast
section around lines 44-49) so both impl_output and the later verify/broadcast
commands use the caller-specified profile.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc096cd0-da52-41db-84f1-8d80a6d361c8

📥 Commits

Reviewing files that changed from the base of the PR and between 8d57d34 and 9acd8a4.

📒 Files selected for processing (2)
  • crates/collateral-contract/README.md
  • crates/collateral-contract/deploy.sh

--node-id 9e0a4d34-3110-48d1-b3c5-580f44270f13 \
--url "https://proof-server.testnet.com/evidence/456" \
--url-content-md5-checksum d41d8cd98f00b204e9800998ecf8427e
--url-content-sha256 d41d8cd98f00b204e9800998ecf8427ed41d8cd98f00b204e9800998ecf8427e
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use actual SHA-256 examples here.

These values are duplicated MD5 digests, not realistic SHA-256 examples. That undercuts the README’s SHA-256 migration guidance and makes copy/paste validation harder. Replace them with genuine 64-hex SHA-256 digests or label them as placeholders.

Suggested doc fix
-  --url-content-sha256 d41d8cd98f00b204e9800998ecf8427ed41d8cd98f00b204e9800998ecf8427e
+  --url-content-sha256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
...
-  --url-content-sha256 5d41402abc4b2a76b9719d911017c5925d41402abc4b2a76b9719d911017c592
+  --url-content-sha256 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
...
-  --url-content-sha256 098f6bcd4621d373cade4e832627b4f6098f6bcd4621d373cade4e832627b4f6
+  --url-content-sha256 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

Also applies to: 281-281, 303-303

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/collateral-contract/README.md` at line 256, Replace the duplicated
MD5-like digest used with the --url-content-sha256 example in the README: the
value "d41d8cd98f00b204e9800998ecf8427ed41d8cd98f00b204e9800998ecf8427e" is not
a valid SHA-256; update the example to a genuine 64-hex SHA-256 digest (or
explicitly mark it as a placeholder like "<SHA256_HEX_PLACEHOLDER>") and make
the same change for the other two occurrences of this bogus digest in the file
so the README shows valid SHA-256 examples for the --url-content-sha256 flag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants