A Developer's Guide to Upgradability in Leo
This guide provides a practical overview of Aleo's program upgradability framework, tailored for developers using the Leo language. You'll learn how to configure your program, implement common upgrade patterns, and follow best practices for writing secure, maintainable applications. For more details on the underlying protocol, refer to the Aleo docs.
1. Getting Started: The Upgrade Policy
Your program's upgrade policy is defined by an annotation on a constructor (see below) in the Leo program. The Leo compiler reads the annotation to understand your intent and generate the appropriate underlying code.
There are four primary upgrade modes:
Mode | Description |
---|---|
@noupgrade | The program is not upgradable. |
@admin | Upgrades are controlled by a single, hardcoded admin address. |
@checksum | Upgrades are governed by an on-chain checksum, often managed by a separate program (e.g., a DAO). |
@custom | You write the entire upgrade logic from scratch in the constructor . |
2. Core Mechanics
Upgradability revolves around a special constructor
function and on-chain program metadata.
The constructor
The constructor
is a special function that runs on-chain during every deployment and upgrade. Think of it as the gatekeeper for your program.
There are two key properties of the constructor
related to upgradability:
- Foundational: All programs must be deployed with a
constructor
. If theconstructor
logic fails (e.g., a failedassert
), the entire deployment or upgrade transaction is rejected. - Immutable: The logic inside the
constructor
is set in stone at the first deployment. It can never be changed by a future upgrade. Any bugs introduced here are permanent, so audit your constructor carefully.
Program Metadata Operands
Within a constructor
, you can access on-chain metadata about the program using the self
keyword.
Operand | Leo Type | Description |
---|---|---|
self.edition | u16 | The program's version number. Starts at 0 and is incremented by 1 for each upgrade. The edition is tracked automatically on the network. |
self.program_owner | address | The address that submitted the deployment transaction. |
self.checksum | [u8, 32] | The program's checksum, which is a unique identifier for the program's code. |
You may also refer to other program's metadata by qualifying the operand with the program name, like Program::edition(credits.aleo)
, Program::program_owner(foo.aleo)
.
You will need to import the program in your Leo file to use this syntax.
Note. Programs deployed before the upgradability feature (i.e. using Leo version < v3.1.0) do not have a program_owner
. Attempting to access it will result in a runtime error.
3. Upgrade Patterns in Leo
Below are some common upgrade patterns in Leo.
You may also refer to the working Leo examples.
Pattern 1: Non-Upgradable
Goal: Explicitly prevent all future upgrades.
main.leo
The Leo compiler automatically generates a constructor that locks the program to its initial version.
// The 'noupgrade_example' program.
program noupgrade_example.aleo {
// This constructor is for the "noupgrade" mode.
// It is immutable and prevents any future upgrades.
@noupgrade
async constructor() {
// The Leo compiler automatically generates the constructor logic.
}
transition main(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}
}
The corresponding AVM code is:
constructor:
assert.eq edition 0u16
Pattern 2: Admin-Driven Upgrade
Goal: Restrict upgrades to a single, hardcoded admin address.
main.leo
// The 'admin_example' program.
program admin_example.aleo {
// This constructor is for the "admin" mode.
// It ensures that only the designated admin can upgrade the program.
@admin(address="aleo1rhgdu77hgyqd3xjj8ucu3jj9r2p3lam3tc3h0nvv2d3k0rp2ca5sqsceh7")
async constructor() {
// The Leo compiler automatically generates the constructor logic.
}
transition main(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}
}
The corresponding AVM code is:
constructor:
assert.eq program_owner aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px;
Pattern 3: Checksum-Driven (Vote Example)
Goal: Delegate upgrade authority to a separate governance program that manages a list of approved code checksums.
main.leo
The compiler uses the mapping
and key
fields to generate a constructor that looks up the approved checksum from the basic_voting.aleo
program.
// The 'vote_example' program.
program vote_example.aleo {
// This constructor is for the "checksum" mode.
@checksum(mapping="basic_voting.aleo/approved_checksum", key="true")
async constructor() {
// The Leo compiler automatically generates the constructor logic.
}
transition main(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}
}
The corresponding AVM code is:
constructor:
branch.eq edition 0u16 to end;
get basic_voting.aleo/approved_checksum[true] into r0;
assert.eq checksum r0;
position end;
Pattern 4: Custom Logic (Time-lock Example)
Goal: Enforce a time delay before an upgrade is allowed. No pre-defined mode is available for this so we'll have to write our own upgrade policy
main.leo
With the @custom
constructor, you are responsible for writing the entire constructor logic yourself.
// The 'timelock_example' program.
program timelock_example.aleo {
@custom
async constructor() {
// For upgrades (edition > 0), enforce a block height condition on when the constructor can be called successfully
if self.edition > 0u16 {
assert(block.height >= 1300u32);
}
}
transition main(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}
}
The corresponding AVM code is:
constructor:
gt edition 0u16 into r0;
branch.eq r0 false to end_then_0_0;
gte block.height 1300u32 into r1;
assert.eq r1 true;
branch.eq true true to end_otherwise_0_1;
position end_then_0_0;
position end_otherwise_0_1;
4. The Rules: What You Can and Cannot Change
The protocol enforces strict rules to ensure that upgrades don't break dependent applications or corrupt existing state.
An upgrade can:
- Change the internal logic of existing
transition
andasync functions
blocks. - Add new
struct
s,record
s,mapping
s,transition
s, andfunction
s.
An upgrade cannot:
- Change the input or output signatures of any existing
transition
,function
,async function
. - Change the input signature of any existing
async function
block. - Change the logic within a non-inline
function
. - Modify or delete any existing
struct
,record
, ormapping
. - Delete any existing program component.
Program Component | Delete | Modify | Add |
---|---|---|---|
import | ❌ | ❌ | ✅ |
struct | ❌ | ❌ | ✅ |
record | ❌ | ❌ | ✅ |
mapping | ❌ | ❌ | ✅ |
function | ❌ | ❌ | ✅ |
transition | ❌ | ✅ (logic) | ✅ |
async function | ❌ | ✅ (logic) | ✅ |
constructor | ❌ | ❌ | ❌ |
5. Security Checklist
Program mutability introduces new risks. Keep these points in mind:
- Audit the
constructor
intensely. Its logic is permanent and cannot be fixed after deployment. - Prefer multi-sig or DAO governance over a single admin. A single point of failure is risky.
- Implement time-locks for major upgrades. Giving users a window to react builds trust.
- Plan for "ossification". Provide a way to make your program immutable (e.g., by transferring admin rights to a burn address) to give users long-term certainty.
6. Legacy Programs: The Final Word
If you have a program that was deployed before the upgradability feature was enabled (or any program deployed without a constructor
):
It is permanently non-upgradable.
There is no migration path to make a legacy program upgradable. If you need to add new features, you must deploy an entirely new program and have your users migrate to it.