Chapter 13 / 15
Beacon Proxy Pattern
@proxy_compatible, deploy_proxy — upgrading many instances atomically.
The beacon proxy pattern (EIP-1967) lets one upgrade transaction update the
logic for hundreds of proxy instances simultaneously. Covenant's
@proxy_compatible decorator ensures the implementation contract
has no constructor-side effects (safe for delegate calls), and
deploy_proxy deploys a new proxy pointing at the beacon.
implementation.cov — proxy-safe logic
// @proxy_compatible disables the constructor and adds
// an initializer pattern instead
@proxy_compatible
record Vault {
owner: address;
balance: amount;
initialized: bool;
error AlreadyInitialized();
// Called once by the factory after proxy deployment
action initialize(owner: address) when(!self.initialized) {
self.owner = owner;
self.initialized = true;
}
action deposit() {
self.balance += msg.value;
}
action withdraw(to: address, value: amount) only(self.owner) {
self.balance -= value;
transfer(to, value);
}
}beacon.cov — single upgrade point
record VaultBeacon {
implementation: address;
owner: address;
event Upgraded(new_impl: address);
action upgrade(new_impl: address) only(self.owner) {
self.implementation = new_impl;
emit Upgraded(new_impl);
}
view get_implementation() -> address {
return self.implementation;
}
}factory.cov — deploy many proxies
record VaultFactory {
beacon: address;
vaults: map(address => address); // user => vault proxy
count: u64;
event VaultCreated(user: address, vault: address);
action create_vault() {
// deploy_proxy creates a minimal proxy pointing at beacon
let vault_addr = deploy_proxy(self.beacon);
// Initialize the vault for the caller
Vault(vault_addr).initialize(msg.sender);
self.vaults[msg.sender] = vault_addr;
self.count += 1;
emit VaultCreated(msg.sender, vault_addr);
}
view vault_of(user: address) -> address {
return self.vaults[user];
}
}Annotations
@proxy_compatible | adds a compile-time check that the implementation has no immutable constructor arguments and uses an initializer pattern. |
deploy_proxy(beacon) | deploys a minimal EIP-1967 proxy whose implementation slot points at the beacon's get_implementation() result. |
| Atomic upgrade | Calling VaultBeacon.upgrade(newImpl) immediately affects all proxies — they delegate to the new implementation on the next call. |
| Storage slots | All proxies share the same storage layout as the implementation. Never change field order in an implementation upgrade. |
Key takeaways
- Beacon proxy is ideal for protocols that deploy many identical contract instances (lending pools, vaults, subgraphs).
- One upgrade transaction affects all instances — a huge operational advantage over per-proxy upgrades.
- Use
@proxy_compatibleto catch constructor-safety issues at compile time.