Skip to content

Repatriate residual stake and locks on lease termination#2709

Open
l0r1s wants to merge 14 commits into
devnet-readyfrom
fix-lease-handoff
Open

Repatriate residual stake and locks on lease termination#2709
l0r1s wants to merge 14 commits into
devnet-readyfrom
fix-lease-handoff

Conversation

@l0r1s

@l0r1s l0r1s commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Repatriate residual stake and locks on lease termination

Closes #2663.

Problem

do_terminate_lease handed subnet ownership to the beneficiary but left the
accumulated owner-cut alpha and lock state on the lease's derived coldkey
(blake2_256("leasing/coldkey", lease_id)). That coldkey has no private key and
the beneficiary proxy can't extract stake, so every fixed-end lease termination
permanently stranded the owner emissions accrued during the lease.

Change

Before tearing down the lease coldkey, termination now:

  • moves the lease coldkey's lock to the beneficiary's hotkey, preserving the
    conviction earned during the lease (do_move_lock(..., preserve_conviction));
  • re-points the subnet owner aggregate buckets to the new owner hotkey
    (reassign_subnet_owner_lock_aggregates, extracted so it's shared with the
    existing owner-change path);
  • repatriates the residual alpha stake to the beneficiary;
  • moves any remaining lock to the beneficiary (transfer_full_lock_to_coldkey);
  • clears the lease coldkey's storage references and SubnetUidToLeaseId;
  • removes the beneficiary proxy before dec_providers, then sweeps the
    unreserved proxy deposit to the beneficiary, so the keyless coldkey is reaped
    cleanly with no dangling account, refcount, or stranded funds.

Post-condition: no alpha, lock, or ownership rows remain under the lease coldkey.

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment thread pallets/subtensor/src/subnets/leasing.rs
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

LOW scrutiny: l0r1s has repo write permission, a 2018 account, substantial contribution history, matching commit authors, no Gittensor allowlist hit found, and branch is fix-lease-handoff -> devnet-ready.

No .github/ai-review/*, copilot-instruction, dependency, build-script, or lockfile changes were present. Static review only; no build or tests were run per Skeptic policy.

Findings

Sev File Finding
HIGH pallets/crowdloan/src/lib.rs:626 Failed finalize can persist finalized state and trap funds inline
HIGH pallets/subtensor/src/macros/dispatches.rs:1956 Lease termination weight does not cover the new worst-case work inline
MEDIUM pallets/subtensor/src/subnets/leasing.rs:238 Lease termination can still commit a partial handoff on lock conflict inline

Prior-comment reconciliation

  • d9d76a12: not addressed — The code still writes finalized = true before fallible finalization work without adding a visible production transaction boundary around finalize; the new test uses with_storage_layer locally rather than changing production behavior.
  • d85d1a80: not addressed — The fallible transfer_full_lock_to_coldkey call still occurs after ownership and aggregate mutations with no visible production transaction wrapper around terminate_lease / do_terminate_lease.

Conclusion

This head still has non-atomic fallible runtime handoffs, and the expanded lease termination path is not reflected in generated weights. Failed crowdloan finalization can persist finalized/current-id state, lease termination can commit partial handoff state, and undercharging the new worst-case termination work can create an overweight block path.


📜 Previous run (superseded)
Sev File Finding Status
HIGH pallets/crowdloan/src/lib.rs:626 Failed finalize can persist finalized state and trap funds ➡️ Carried forward to current findings
The code still writes finalized = true before fallible finalization work without adding a visible production transaction boundary around finalize; the new test uses with_storage_layer locally rather than changing production behavior.
MEDIUM pallets/subtensor/src/subnets/leasing.rs:238 Lease termination can still commit a partial handoff on lock conflict ➡️ Carried forward to current findings
The fallible transfer_full_lock_to_coldkey call still occurs after ownership and aggregate mutations with no visible production transaction wrapper around terminate_lease / do_terminate_lease.

# 🔍 AI Review — Auditor (domain review) has not yet run on this PR.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment thread pallets/subtensor/src/subnets/leasing.rs
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

gztensor
gztensor previously approved these changes Jun 2, 2026
Comment thread pallets/subtensor/src/subnets/leasing.rs

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment thread pallets/subtensor/src/subnets/leasing.rs
Comment thread pallets/subtensor/src/subnets/leasing.rs
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment thread pallets/subtensor/src/subnets/leasing.rs Outdated
Comment thread pallets/subtensor/src/subnets/leasing.rs
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@l0r1s l0r1s added the skip-cargo-audit This PR fails cargo audit but needs to be merged anyway label Jun 8, 2026

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment thread pallets/subtensor/src/subnets/leasing.rs Outdated
Comment thread pallets/subtensor/src/subnets/leasing.rs
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment thread pallets/crowdloan/src/lib.rs
Comment thread pallets/subtensor/src/subnets/leasing.rs
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@l0r1s l0r1s requested review from evgeny-s and gztensor June 9, 2026 22:43
gztensor
gztensor previously approved these changes Jun 10, 2026
JohnReedV
JohnReedV previously approved these changes Jun 10, 2026
@l0r1s l0r1s dismissed stale reviews from JohnReedV and gztensor via f7283f1 June 10, 2026 20:29

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment on lines +623 to +626
// Mark finalized *before* dispatching the creator-controlled call so re-entrant
// withdraw/refund/dissolve are rejected mid-dispatch. Reverts if the dispatch fails.
crowdloan.finalized = true;
Crowdloans::<T>::insert(crowdloan_id, &crowdloan);

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.

[HIGH] Failed finalize can persist finalized state and trap funds

This writes crowdloan.finalized = true before several fallible operations: preimage lookup/drop, dispatch of the creator-controlled call, target transfer, and invalid-config return. I do not see #[transactional] on finalize or a production with_storage_layer around the whole body, so a later Err can leave the crowdloan marked finalized while funds were not delivered and contributors can no longer refund/withdraw. The new test only wraps one path in with_storage_layer; the dispatchable itself needs the rollback boundary, or the state write must happen only after all fallible finalization work succeeds while still blocking reentrancy by another mechanism.

Comment on lines 231 to +238
SubnetOwner::<T>::insert(lease.netuid, lease.beneficiary.clone());
// Set the owner hotkey before moving locks so the destination lock is
// accounted in the subnet-owner aggregate, not the regular hotkey aggregate.
Self::set_subnet_owner_hotkey(lease.netuid, &hotkey)?;
Self::reassign_subnet_owner_lock_aggregates(lease.netuid, &lease.hotkey, &hotkey);

Self::repatriate_lease_coldkey_alpha(&lease, &hotkey)?;
Self::transfer_full_lock_to_coldkey(

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.

[MEDIUM] Lease termination can still commit a partial handoff on lock conflict

The production terminate_lease dispatchable still mutates owner state and aggregate lock buckets before later fallible calls such as repatriate_lease_coldkey_alpha, transfer_full_lock_to_coldkey, perform_hotkey_swap_on_one_subnet, proxy removal, and the balance sweep. If any later call rejects, for example transfer_full_lock_to_coldkey returning LockHotkeyMismatch, there is no visible #[transactional] or production with_storage_layer on this path, so the lease can remain open with subnet ownership and lock aggregates already changed. Wrap the whole handoff in a storage transaction or preflight every fallible condition before the first state mutation.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@github-actions github-actions Bot left a comment

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.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment on lines +623 to +626
// Mark finalized *before* dispatching the creator-controlled call so re-entrant
// withdraw/refund/dissolve are rejected mid-dispatch. Reverts if the dispatch fails.
crowdloan.finalized = true;
Crowdloans::<T>::insert(crowdloan_id, &crowdloan);

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.

[HIGH] Failed finalize can persist finalized state and trap funds

finalize writes finalized = true before fallible preimage lookup / creator-call dispatch, but the production dispatchable still has no visible #[transactional], with_transaction, or with_storage_layer boundary. In FRAME, returning Err after storage writes is not enough to undo them unless a transaction layer is active. A missing preimage or failing creator-controlled call can therefore leave the crowdloan marked finalized; in the call path it can also leave CurrentCrowdloanId set because kill() is after the fallible dispatch. The new test manually wraps one case in with_storage_layer, but that does not add the production rollback boundary. Wrap the whole finalization mutation/dispatch section in a transaction layer or make the dispatchable transactional before moving this guard write ahead of the fallible work.

Comment on lines +1953 to +1956
#[pallet::weight(
<T as crate::pallet::Config>::WeightInfo::terminate_lease(T::MaxContributors::get())
.saturating_add(<T as crate::pallet::Config>::WeightInfo::swap_hotkey())
)]

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.

[HIGH] Lease termination weight does not cover the new worst-case work

The dispatch weight is still just generated terminate_lease(k) plus swap_hotkey(), but the PR adds more worst-case work to do_terminate_lease: lock migration, owner-lock aggregate reassignment, stake repatriation, full-lock transfer, proxy removal, currency sweep, and extra storage cleanup. The benchmark scenario was updated, but pallets/subtensor/src/weights.rs is not changed in this PR, so the runtime still charges the old generated weight. Undercharging a runtime extrinsic can let a valid lease termination exceed its declared block weight. Regenerate and commit the weights for the expanded benchmark, or add a conservative explicit weight component that accounts for the added reads/writes and balance/proxy operations.

Comment on lines 231 to +238
SubnetOwner::<T>::insert(lease.netuid, lease.beneficiary.clone());
// Set the owner hotkey before moving locks so the destination lock is
// accounted in the subnet-owner aggregate, not the regular hotkey aggregate.
Self::set_subnet_owner_hotkey(lease.netuid, &hotkey)?;
Self::reassign_subnet_owner_lock_aggregates(lease.netuid, &lease.hotkey, &hotkey);

Self::repatriate_lease_coldkey_alpha(&lease, &hotkey)?;
Self::transfer_full_lock_to_coldkey(

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.

[MEDIUM] Lease termination can still commit a partial handoff on lock conflict

This sequence mutates subnet ownership and owner-lock aggregates before the later transfer_full_lock_to_coldkey / hotkey swap / proxy-removal calls can return Err. I do not see a production #[transactional], with_transaction, or with_storage_layer wrapper on terminate_lease or do_terminate_lease, so an error after these writes can leave the subnet partially handed off while the lease remains present. The new rollback test asserts the desired behavior, but the production call path still needs an explicit transaction boundary around the mutation sequence or all fallible conflict checks must happen before any state writes.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

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

Labels

skip-cargo-audit This PR fails cargo audit but needs to be merged anyway

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants