Beacon Proxy Pattern
Problem
I need to deploy many instances of the same contract (e.g., per-user vaults, per-pool AMMs) that can all be upgraded to a new implementation in a single transaction.
Solution
// The beacon holds the current implementation address
contract VaultBeacon {
field owner: address = deployer
field implementation: address
event Upgraded(new_impl: address indexed)
error NotOwner()
error ZeroAddress()
action set_implementation(new_impl: address) only owner {
given new_impl != address(0) or revert_with ZeroAddress()
implementation = new_impl
emit Upgraded(new_impl)
}
action get_implementation() returns (impl: address) {
return implementation
}
}
// Each vault instance is a beacon proxy
@proxy_compatible
contract UserVault {
field owner: address
field beacon: address
field balance: amount = 0 tokens
event Initialized(owner: address indexed)
event Deposited(amount: amount)
event Withdrawn(to: address indexed, amount: amount)
error NotOwner()
error InsufficientBalance()
action initialize(vault_owner: address, beacon_addr: address) {
owner = vault_owner
beacon = beacon_addr
emit Initialized(vault_owner)
}
action deposit(amount: amount) {
balance = balance + amount
emit Deposited(amount)
}
action withdraw(to: address, amount: amount) only owner {
given amount <= balance or revert_with InsufficientBalance()
balance = balance - amount
transfer(to, amount)
emit Withdrawn(to, amount)
}
}
Explanation
- The beacon stores the implementation address. All proxy instances query the beacon at runtime.
@proxy_compatibleannotates the implementation contract, enabling the compiler to generate the beacon proxy delegation code.- Upgrading all vaults: call
beacon.set_implementation(new_impl_address)once. All proxies immediately use the new implementation. - Each proxy stores its own state (owner, balance) but delegates logic to the current implementation.
Deployment Script
# 1. Deploy implementation
covenant deploy ./out/UserVault.artifact.json --network sepolia
# 2. Deploy beacon with implementation address
covenant deploy ./out/VaultBeacon.artifact.json --constructor-args \'["<IMPL_ADDRESS>"]\' --network sepolia
# 3. Deploy proxies for each user (in a factory)
covenant deploy ./out/VaultProxy.artifact.json --constructor-args \'["<BEACON_ADDRESS>"]\' --network sepolia
Gas Estimate
| Operation | Gas |
|---|---|
| Beacon deployment | ~150,000 |
| Implementation deployment | ~300,000+ |
| Proxy deployment (per instance) | ~100,000 |
set_implementation (upgrades all) | ~30,000 |
Common Pitfalls
- Not using
@proxy_compatible: Without the annotation, the compiler does not generate the delegation jump table. - Initializing via constructor: Proxies cannot use constructors. Always use an
initializeaction. - Beacon not access-controlled: The beacon owner can upgrade all proxies. Use multisig or timelock.
- State layout changes: V2 must not change field order — beacon proxies share a delegatecall model where storage slot positions are fixed.