UUPS Upgradeability
Sylan contracts use the UUPS pattern (EIP‑1822) with EIP‑1967 storage slots. Each proxy holds a single pointer to the current implementation, while the implementation defines the upgrade logic. Upgrades are gated by the contract owner and instrumented by our shared base, SylanUUPSUpgradeable.
Always interact with the proxy address published under Architecture → Addresses & ABIs. Implementation addresses change as you upgrade.
Why UUPS (vs Transparent/Beacon)
- Minimal proxy: less bytecode/gas than Transparent.
- Explicit upgrades: logic contract owns
_authorizeUpgrade(); easier to centralize policy. - Composable: works well with Pausable/ReentrancyGuard/AccessControl.
See also: OpenZeppelin’s UUPS design—Sylan follows the same guardrails (initializers, storage gaps, ERC1822 proxiable UUID).
Anatomy (EIP‑1967)
A UUPS proxy stores the implementation at the EIP‑1967 slot:
implementation slot = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
admin slot = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)Our contracts emit both OZ’s Upgraded(address implementation) and a project event from the base:
event ContractUpgraded(address newImplementation, address caller, uint256 timestamp);Upgrade surface (public)
Every upgradeable contract shares the same functions (inherited):
upgradeTo(address newImplementation)— onlyOwner (via_authorizeUpgrade)upgradeToAndCall(address newImplementation, bytes data)— onlyOwner; performs the upgrade and thendelegatecall(data)(commonly areinitialize(V))proxiableUUID() → bytes32— ERC‑1822 UUID; prevents bricking proxies by ensuring the new impl supports UUPS
Initializers
initialize(...)— called once on the proxy after first deploymentreinitialize(uint8 version)— optional, for adding modules on later impls- Implementations call
_disableInitializers()in their constructor to prevent misuse
Typical upgrade flow
Storage layout rules (critical)
- Append only: add new state variables at the end of the storage layout; never reorder or remove.
- Preserve inheritance order: do not change base‐class order in a way that shifts storage.
- Keep gaps: each contract reserves
__gapstorage slots to allow future variables:
uint256[50] private __gap; // do not touch existing gaps- No self‑destruct in implementations. Avoid introducing immutable variables after initial shipping.
Pre‑upgrade checklist
- Diff storage with a tool (e.g., Foundry
forge inspect, OZ storage layout, or Slither) - Increase the
reinitializeversion when new state is added - Write migration tests:
v1 → v2with live proxy storage
Access control & pausing
- Upgrades are gated by
_authorizeUpgradewhich restricts to the owner (multisig). - Pausing is orthogonal to upgrades. You may choose to
pause()before an upgrade andunpause()after, but it’s optional.
Verifying a proxy on the explorer
- Verify the implementation contract (standard source verification).
- Verify the proxy as an EIP‑1967 proxy (many explorers detect automatically).
- Inspect slots to confirm wiring:
- Implementation slot holds the latest impl address.
- Admin slot holds the proxy admin/owner.
Foundry helper (optional)
# Read implementation slot
cast storage <PROXY_ADDRESS> $(cast index eip1967.proxy.implementation)On Polygon Amoy/Mainnet, explorers display the Implementation tab for EIP‑1967 proxies; your dapp should always call the proxy.
Operational runbook
- Staging first: deploy the new impl on testnet; run end‑to‑end flows.
- Prepare calldata: if you need to initialize new state, encode a
reinitialize(V)call. - Multisig proposal: submit
upgradeToorupgradeToAndCallvia the Safe. - Post‑upgrade checks: read key views and fire a canary call; monitor events
Upgraded+ContractUpgraded. - Changelog: record impl address, commit hash, layout diff, and init calldata under Deploy → Verifications.
Testing
- Proxy‑aware tests: deploy proxy (v1) → write state → upgrade to v2 → assert state intact and new functions live.
- Initializer guards: direct calls to
initialize()on impl should revert; proxy’sinitialize()should succeed once. - Reentrancy & pause: ensure protections stay in place across upgrades.
Risks & mitigations
- Storage collisions → use layout diffing tools; keep
__gap. - Wrong impl address → multisig review + two‑person rule; dry‑run on testnet.
- Initializer abuse →
_disableInitializers()in impl constructors; use versionedreinitialize. - Bricking the proxy →
proxiableUUIDcheck ensures the new impl is UUPS‑compatible.
FAQ
Q: Do users ever see the implementation address?
A: No—UIs/SDKs should only expose the proxy. The impl address is for audits/verification.
Q: Can we downgrade?
A: Technically yes (if storage matches), but avoid unless for emergency rollback and document it.
Q: Do upgrades change event histories?
A: No. Past events remain; new logic may emit additional events going forward.
Out of scope for this page
- Contract‑specific ABIs or admin surfaces → see each contract page
- Addresses → Architecture → Addresses & ABIs
- Governance policies (who signs upgrades) → Concepts → Security