Skip to main content

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:

ModeDescription
@noupgradeThe program is not upgradable.
@adminUpgrades are controlled by a single, hardcoded admin address.
@checksumUpgrades are governed by an on-chain checksum, often managed by a separate program (e.g., a DAO).
@customYou 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 the constructor logic fails (e.g., a failed assert), 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.

OperandLeo TypeDescription
self.editionu16The 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_owneraddressThe 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 and async functions blocks.
  • Add new structs, records, mappings, transitions, and functions.

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, or mapping.
  • Delete any existing program component.
Program ComponentDeleteModifyAdd
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.