Skip to content

Add Ownable2Step base for safe two-step ownership transfer#1770

Merged
shargon merged 15 commits into
neo-project:master-n3from
Jim8y:feat/ownable-two-step
Jul 4, 2026
Merged

Add Ownable2Step base for safe two-step ownership transfer#1770
shargon merged 15 commits into
neo-project:master-n3from
Jim8y:feat/ownable-two-step

Conversation

@Jim8y

@Jim8y Jim8y commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

The framework only shipped single-step Ownable: SetOwner assigns the new owner immediately, with no witness check on the incoming account. Passing a mistyped or otherwise unusable address loses contract control irrecoverably — the classic single-step ownership-transfer footgun that OpenZeppelin introduced Ownable2Step to fix.

This adds a standalone Ownable2Step base (the OpenZeppelin Ownable2Step analog, adapted to Neo's static-method model). Ownership changes hands only in two steps:

  1. the owner proposes a pending owner via TransferOwnership, and
  2. the transfer completes only when that account itself calls AcceptOwnership and proves control with Runtime.CheckWitness.

A mistyped address can never take ownership because it can never sign the acceptance.

API

  • TransferOwnership(newOwner) / AcceptOwnership() — the two-step flow. Re-proposing supersedes any in-flight pending and emits a cancellation event for the dropped candidate.
  • CancelOwnershipTransfer() — owner clears an in-flight pending.
  • RenounceOwnership() — the one deliberate, irreversible action; removes the owner and clears any pending so a previously-proposed account cannot seize an abandoned contract.
  • GetOwner() / GetPendingOwner()[Safe] read-only accessors.
  • InitializeOwner(data, update) — call from _deploy; defaults to the tx sender, no-op on update.

Design notes

  • Standalone, not extending Ownable — so it does not re-expose the unsafe single-step SetOwner (Neo has no static-method override to hide it).
  • Storage prefixes 0xFD (owner) / 0xFC (pending), distinct from Ownable (0xFF) and Pausable (0xFE).
  • Absent pending = no transfer in flight is the single source of truth, which collapses double-accept, accept-with-no-pending, and stale-accept-after-renounce to one null check. Storage is written before events on every transition.
  • [Safe] is only on the two pure-read accessors; every state-mutating method is non-[Safe] and reaches CheckWitness.

Testing

Ownable2StepTest (17 cases) covers the happy path plus every adversarial edge: transfer to self / to zero / by non-owner; accept with no pending / by the wrong account (incl. the current owner) / twice; renounce-then-stale-accept; cancel with nothing pending / by non-owner; post-renounce lockout; re-propose supersession; full event emission; and the manifest safe-flag attribution. Full framework suite green (270).

The design and this implementation were each put through an independent adversarial security review (ownership-theft, bricking, witness-bypass, storage/event ordering, stale-pending seizure) before submission.

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.70968% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.96%. Comparing base (13012cb) to head (8ce375c).

Files with missing lines Patch % Lines
src/Neo.SmartContract.Framework/Ownable2Step.cs 88.70% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           master-n3    #1770      +/-   ##
=============================================
+ Coverage      82.95%   82.96%   +0.01%     
=============================================
  Files            306      307       +1     
  Lines          27372    27434      +62     
  Branches        3826     3848      +22     
=============================================
+ Hits           22706    22761      +55     
- Misses          3497     3501       +4     
- Partials        1169     1172       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Jim8y Jim8y requested review from Wi1l-B0t and shargon June 23, 2026 14:03
Comment thread src/Neo.SmartContract.Framework/Ownable2Step.cs Outdated
@Jim8y Jim8y requested a review from shargon June 24, 2026 09:14
ajara87
ajara87 previously approved these changes Jun 24, 2026
Comment thread src/Neo.SmartContract.Framework/Ownable2Step.cs Outdated
shargon
shargon previously approved these changes Jun 25, 2026
Comment thread src/Neo.SmartContract.Framework/Ownable2Step.cs Outdated
Wi1l-B0t
Wi1l-B0t previously approved these changes Jun 26, 2026
ajara87
ajara87 previously approved these changes Jun 26, 2026
@shargon shargon dismissed stale reviews from ajara87, Wi1l-B0t, and themself via 892243a June 26, 2026 17:32
Comment thread src/Neo.SmartContract.Framework/Ownable2Step.cs
@Jim8y Jim8y force-pushed the feat/ownable-two-step branch from 892243a to cdd7270 Compare June 27, 2026 01:39
ajara87
ajara87 previously approved these changes Jun 28, 2026
@Jim8y

Jim8y commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Updated with latest master. Could you please take another look?

@Jim8y Jim8y requested a review from shargon June 29, 2026 00:25
Wi1l-B0t
Wi1l-B0t previously approved these changes Jun 29, 2026
Comment thread src/Neo.SmartContract.Framework/Ownable2Step.cs Outdated
@Jim8y Jim8y dismissed stale reviews from Wi1l-B0t and ajara87 via a06bd36 June 30, 2026 17:43
@Jim8y Jim8y requested a review from shargon July 1, 2026 03:13
Comment thread src/Neo.SmartContract.Framework/Ownable2Step.cs Outdated
@Jim8y Jim8y requested a review from shargon July 1, 2026 20:36
Jim8y added 3 commits July 2, 2026 14:47
The framework only shipped single-step Ownable: SetOwner assigns the new
owner immediately, with no witness check on the incoming account. Passing a
mistyped or otherwise unusable address loses contract control irrecoverably
— the classic single-step ownership-transfer footgun.

Add a standalone Ownable2Step base (the OpenZeppelin Ownable2Step analog,
adapted to Neo's static-method model). Ownership changes hands only in two
steps: the owner proposes a pending owner via TransferOwnership, and the
transfer completes only when that account itself calls AcceptOwnership and
proves control with Runtime.CheckWitness. A mistyped address can never take
ownership because it can never sign the acceptance.

The class is standalone rather than extending Ownable so it does not
re-expose the unsafe single-step SetOwner (Neo has no static-method
override). It adds:

- TransferOwnership / AcceptOwnership: the two-step flow; re-proposing
  supersedes any in-flight pending and emits a cancellation event for it.
- CancelOwnershipTransfer: owner clears an in-flight pending.
- RenounceOwnership: the one deliberate irreversible action; removes the
  owner and clears any pending so a previously-proposed account cannot
  seize an abandoned contract.
- GetOwner / GetPendingOwner: [Safe] read-only accessors.

Storage uses reserved prefixes 0xFD (owner) and 0xFC (pending), distinct
from Ownable (0xFF) and Pausable (0xFE). Pending is the single source of
truth for an in-flight transfer (absent = none), which collapses
double-accept, accept-with-no-pending, and stale-accept-after-renounce to
one null check. Storage is written before events on every transition.

Adds Ownable2StepTest covering the happy path plus every adversarial edge:
transfer to self/zero/by non-owner, accept with no pending / by the wrong
account / twice, renounce-then-stale-accept, cancel with nothing pending /
by non-owner, post-renounce lockout, re-propose supersession, event
emission, and the manifest safe-flag attribution.
@Jim8y Jim8y force-pushed the feat/ownable-two-step branch from 1bf6c2f to d38e873 Compare July 2, 2026 06:48
@Jim8y Jim8y requested a review from shargon July 3, 2026 18:46
@shargon shargon merged commit 37e45fa into neo-project:master-n3 Jul 4, 2026
5 checks passed
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.

4 participants