Skip to Content
⚠️ Alert: Sylan is under active development—only trust contract/wallet addresses announced on our official channels; we will never DM you, ask for funds, or run surprise airdrops/presales.
ContractsUUPS Upgradeability

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 then delegatecall(data) (commonly a reinitialize(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 deployment
  • reinitialize(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 __gap storage 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 reinitialize version when new state is added
  • Write migration tests: v1 → v2 with live proxy storage

Access control & pausing

  • Upgrades are gated by _authorizeUpgrade which restricts to the owner (multisig).
  • Pausing is orthogonal to upgrades. You may choose to pause() before an upgrade and unpause() after, but it’s optional.

Verifying a proxy on the explorer

  1. Verify the implementation contract (standard source verification).
  2. Verify the proxy as an EIP‑1967 proxy (many explorers detect automatically).
  3. 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 upgradeTo or upgradeToAndCall via 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’s initialize() 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 versioned reinitialize.
  • Bricking the proxyproxiableUUID check 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
Last updated on