Upgrade Safety

Documentation snapshot 18 October 2026. Deployment · Audit Status
On this page

fact Two kinds of code change exist. A UUPS upgrade replaces the implementation behind one of the four periphery proxies (Treasury, BondDepository, RevenueRouter, Desk). A core replacement deploys a new immutable NoteCore and moves new issuance to it through SeriesRegistry. Existing note escrow remains in the immutable core, but upgrades can change periphery behaviour and introduce loss or availability risk. The design is described under Upgradeability and recovery; this page is the safety view.

Controls

fact Controls
ControlUUPS upgradeCore replacement
Who can executeOnly upgrade governance via _authorizeUpgrade; every other caller rejects unauthorised callsOnly the 48-hour governance may register and deprecate
Who can proposeUpgradeGovernor (veNOTE vote, quorum 8%, 7-day vote)NoteGovernor (quorum 4%, 5-day vote)
Who can vetoGuardian, CANCELLER_ROLE on upgrade governanceGuardian, CANCELLER_ROLE on governance
Minimum notice on-chain7 days queued, after a 7-day vote and 1-day delay48 hours queued
Blast radiusOne proxy's logic; state preserved in ERC-7201 namespacesNone for live series; only which core accepts createSeries
RollbackSchedule the previous implementation through the same 7-day pathregister the old core again as a new version (deprecation is irreversible per version)
What it cannot doMove funds without a matching function in V2 that governance would also have to call; shorten its own delay without a 7-day governance-authority configurationChange terms of a live series; withdraw escrow

The upgrade governor enforces minimum quorum, voting-period and governance-delay settings at construction. A configuration below those floors is rejected.

Storage-layout Gate

Every UUPS module stores state in a named ERC-7201 namespace; the slots are listed under Upgradeability and recovery. contracts/storage-layout/check_layout.py is the gate:

python3 contracts/storage-layout/check_layout.py snapshot   # regenerate contracts/storage-layout/<Contract>.json
python3 contracts/storage-layout/check_layout.py check      # CI: fail if any existing slot changed
python3 contracts/storage-layout/check_layout.py check --contract Desk --probe Desk=DeskStorageProbeV2Append --impl Desk=DeskV2   # must PASS
python3 contracts/storage-layout/check_layout.py check --contract Desk --probe Desk=DeskStorageProbeV2Reorder                    # must FAIL

A layout is compatible only if every existing member keeps its (slot, offset, type, label), new members are appended, and each namespace base slot equals both the snapshot and the *_STORAGE_LOCATION constant in source. Exit code 1 on any violation. test/upgrade/token/Upgrade.t.sol::test_storageLocations_matchErc7201Formula asserts the constants against the ERC-7201 formula on-chain, and the same suite performs a full V1 to V2 upgrade through a real governance, checking every namespace member survives.

Test Gate

fact Test Gate Reference
LayerWhat must pass before an upgrade proposal
UnitFull token suite including GovernedUpgradeable.t.sol and DeployToken.t.sol
UpgradeUpgrade.t.sol: direct upgradeToAndCall from owner, guardian and EOA rejects unauthorised calls; initialize re-run reverts InvalidInitialization; non-UUPS target reverts ERC1967InvalidImplementation or UUPSUnsupportedProxiableUUID; state intact after upgrade
Layoutcheck_layout.py check against the live snapshot with the V2 implementation and probe
InvariantsEscrow, Desk, Treasury and governance invariant suites unchanged or extended; see Invariants
StaticSlither and the configured detectors, see Static analysis

For a core replacement the gate is the full core suite on the new NoteCore including Governance.t.sol (schedule, warp 48 h, execute) and the NoteCore invariant run including invariant_unwoundClaimsMatchSnapshot.

Review Checklist

Before voting for or executing an upgrade, the public evidence should support every line from on-chain data and the published source.

  1. The proposal description begins with # <title> and has a ## Risk section naming the contracts touched and the version bump.
  2. The queued operation on upgrade governance targets the expected proxy address (see Addresses) and calls upgradeToAndCall(v2, calldata); the calldata is either empty or a reinitializer(2) call whose arguments are stated in the proposal.
  3. The V2 implementation is verified on the explorer at the commit named in the proposal, its constructor arguments equal V1’s immutables, and version() on the new implementation is higher than the proxy’s current version().
  4. check_layout.py check passes against the committed snapshot with the V2 implementation and probe; the CI run is linked.
  5. No state variable was added to inherited non-upgradeable OpenZeppelin contracts (Desk ERC-20 slots 0 to 4).
  6. The operation does not call governance-authority configuration, renounceOwnership or transferOwnership unless that is the stated purpose of the proposal.
  7. Pause status: if the module is paused for the upgrade, the proposal states who unpauses and when. A pause does not itself make a live note redeemable; each exit still requires its documented state and available transfers.
  8. Timing: the operation’s execute time is at least 7 days after schedule, and the guardian’s veto window has been publicly announced.

Signals to Watch

  • Upgraded(address implementation) on any proxy without a matching executed proposal.
  • CallScheduled on either governance whose target or selector is not in the proposal.
  • RoleGranted or RoleRevoked on either governance; roles should only change through a proposal.
  • CoreRegistered or CoreDeprecated on SeriesRegistry.
  • ModuleUpdated with an effectiveAt sooner than 24 hours from the event, which cannot happen and would indicate a different contract than the audited one.

Indexers can subscribe to these through the Events reference.

A little more clarity?

Keep exploring, or revisit this topic from the navigation.