Upgrade Safety
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
| Control | UUPS upgrade | Core replacement |
|---|---|---|
| Who can execute | Only upgrade governance via _authorizeUpgrade; every other caller rejects unauthorised calls | Only the 48-hour governance may register and deprecate |
| Who can propose | UpgradeGovernor (veNOTE vote, quorum 8%, 7-day vote) | NoteGovernor (quorum 4%, 5-day vote) |
| Who can veto | Guardian, CANCELLER_ROLE on upgrade governance | Guardian, CANCELLER_ROLE on governance |
| Minimum notice on-chain | 7 days queued, after a 7-day vote and 1-day delay | 48 hours queued |
| Blast radius | One proxy's logic; state preserved in ERC-7201 namespaces | None for live series; only which core accepts createSeries |
| Rollback | Schedule the previous implementation through the same 7-day path | register the old core again as a new version (deprecation is irreversible per version) |
| What it cannot do | Move funds without a matching function in V2 that governance would also have to call; shorten its own delay without a 7-day governance-authority configuration | Change 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
| Layer | What must pass before an upgrade proposal |
|---|---|
| Unit | Full token suite including GovernedUpgradeable.t.sol and DeployToken.t.sol |
| Upgrade | Upgrade.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 |
| Layout | check_layout.py check against the live snapshot with the V2 implementation and probe |
| Invariants | Escrow, Desk, Treasury and governance invariant suites unchanged or extended; see Invariants |
| Static | Slither 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.
- The proposal description begins with
# <title>and has a## Risksection naming the contracts touched and the version bump. - The queued operation on upgrade governance targets the expected proxy address (see Addresses) and calls
upgradeToAndCall(v2, calldata); thecalldatais either empty or areinitializer(2)call whose arguments are stated in the proposal. - 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 currentversion(). check_layout.py checkpasses against the committed snapshot with the V2 implementation and probe; the CI run is linked.- No state variable was added to inherited non-upgradeable OpenZeppelin contracts (
DeskERC-20 slots 0 to 4). - The operation does not call governance-authority configuration,
renounceOwnershiportransferOwnershipunless that is the stated purpose of the proposal. - 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.
- Timing: the operation’s
executetime is at least 7 days afterschedule, and the guardian’s veto window has been publicly announced.
Signals to Watch
Upgraded(address implementation)on any proxy without a matching executed proposal.CallScheduledon either governance whose target or selector is not in the proposal.RoleGrantedorRoleRevokedon either governance; roles should only change through a proposal.CoreRegisteredorCoreDeprecatedonSeriesRegistry.ModuleUpdatedwith aneffectiveAtsooner 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.
Keep exploring, or revisit this topic from the navigation.