12 — UUPS Upgradeable

UUPS (EIP-1822 / EIP-1967) places the upgrade logic in the implementation contract rather than the proxy. Covenant provides the @upgradeable(uups) decorator.

Implementation contract

@upgradeable(uups)
contract TokenV1 {
  field balances: Map<Address, u256>
  field total: u256

  action mint(to: Address, amount: u256) {
    only(owner);
    self.balances[to] += amount;
    self.total        += amount;
  }

  // Required by UUPS: upgrade logic lives here
  action upgrade_to(new_impl: Address) {
    only(owner);
    _upgrade(new_impl);   // built-in: validates ERC-1822, writes EIP-1967 slot
  }
}

Proxy deployment

# Deploy implementation
covenant deploy TokenV1 --out impl.json

# Deploy ERC-1967 proxy pointing at the implementation
covenant deploy-proxy --impl $(cat impl.json | jq -r .address) --out proxy.json

All user interactions go through the proxy address. The proxy’s storage holds the EIP-1967 slot and delegates all calls to the implementation.

Upgrading

# Deploy new implementation
covenant deploy TokenV2 --out impl_v2.json

# Call upgrade_to through the proxy (owner wallet required)
covenant call $(cat proxy.json | jq -r .address) upgrade_to $(cat impl_v2.json | jq -r .address)

Storage layout safety

Covenant enforces storage layout compatibility checks at compile time. If TokenV2 reorders or removes fields from TokenV1, the compiler emits an error:

Error: storage layout incompatible with V1
  V1: field balances @ slot 0
  V2: field owners   @ slot 0   ← collision!
Hint: add new fields after existing ones, or use a layout gap.

Use a gap field to reserve space:

@upgradeable(uups)
contract TokenV1 {
  field balances: Map<Address, u256>
  field total:    u256
  field _gap:     u256[48]   // 48 reserved slots for future fields
}

Initialiser pattern

UUPS contracts cannot use Solidity-style constructors (proxy doesn’t run them). Use the init block with an initialised guard:

field initialised: Bool = false

action initialise(name: String) {
  require(!self.initialised, AlreadyInitialised);
  self.initialised = true;
  self.name = name;
}