
Understanding the four Legion Score pillars
What each score represents, how it is calculated, and what it takes to reach the top

Concrete Vaults: the most accessible path to real yield in DeFi
A beginner-friendly introduction to automated DeFi strategies powered by Concrete.

Deploying your first Solidity Contract on Arc Testnet
Deploying your first Solidity Contract on Arc Testnet

Understanding the four Legion Score pillars
What each score represents, how it is calculated, and what it takes to reach the top

Concrete Vaults: the most accessible path to real yield in DeFi
A beginner-friendly introduction to automated DeFi strategies powered by Concrete.

Deploying your first Solidity Contract on Arc Testnet
Deploying your first Solidity Contract on Arc Testnet

Subscribe to Colliseum

Subscribe to Colliseum


<100 subscribers
<100 subscribers
Hi, my name is Heorhii. I help teams build private, secure applications on Aleo. One of the most common questions developers ask is how to upgrade a Leo program safely without breaking existing contracts or exposing vulnerabilities. In this article, I’ll walk through how upgradability works on Aleo, how to define an upgrade policy, the main patterns you can use, and the best practices to keep your program secure and maintainable.
Upgrades begin with the constructor. Every Aleo program includes a special function called a constructor. It runs on-chain each time you deploy or upgrade your program. Think of it as the gatekeeper that decides whether an upgrade is allowed or not.
https://docs.leo-lang.org/guides/upgradability
Two rules define how constructors work:
A constructor is required. Without it, your deployment will fail.
The constructor is immutable. Once deployed, its logic can never be changed. If there’s a bug in your constructor, it will live forever. Audit it carefully before deployment.
Inside the constructor, you can access built-in metadata about your program using the self keyword. This includes:
self.edition: the program’s version number, starting at zero and increasing with each upgrade.
self.program_owner: the address of the account that deployed the program.
self.checksum: a unique hash representing the program’s code.
You can also reference metadata from other programs if you import them in your Leo file, for example Program::edition(credits.aleo) or Program::program_owner(foo.aleo).
Programs deployed before Leo version 3.1.0 do not include program_owner, so trying to access it will cause a runtime error.
https://github.com/ProvableHQ/leo-examples/tree/main/upgrades
Defining an upgrade policy. An upgrade policy defines how your program can evolve over time. In Leo, you define it by adding an annotation above the constructor. The compiler reads this annotation and generates the required validation code automatically.
There are four upgrade modes you can choose from:
1. Non-upgradable (@noupgrade). Use this mode if you want your program to remain fixed forever. It is the safest option for production systems that should never change.
program noupgrade_example.aleo {
@noupgrade
async constructor() {
// Compiler ensures no future upgrades are allowed
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
2. Admin-controlled (@admin). This mode allows only a single, predefined address to upgrade the program. It’s useful for early-stage development but risky for production if the admin key is compromised.
program admin_example.aleo {
@admin(address="aleo1rhgdu77hgyqd3xjj8ucu3jj9r2p3lam3tc3h0nvv2d3k0rp2ca5sqsceh7")
async constructor() {
// Compiler ensures only the specified admin can perform upgrades
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
3. Checksum-governed (@checksum). In this mode, the upgrade is verified against an on-chain checksum that is managed by another program, such as a DAO or governance system. This lets the community decide which versions are approved.
program vote_example.aleo {
@checksum(mapping="basic_voting.aleo/approved_checksum", key="true")
async constructor() {
// Compiler ensures only approved checksums are accepted
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
This pattern is ideal for decentralized governance, where multiple parties vote on upgrades before they go live.
4. Custom logic (@custom). If you want full control, you can write your own upgrade logic. For example, you might require that upgrades can only happen after a certain block height to create a time delay.
program timelock_example.aleo {
@custom
async constructor() {
if self.edition > 0u16 {
assert(block.height >= 1300u32);
}
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
This approach gives you flexibility to build advanced conditions such as multi-signature approvals, staking-based governance, or even time-based locks.
What you can and cannot change. When upgrading, the Aleo protocol enforces strict rules to maintain backward compatibility and prevent corruption of existing state.
You can:
Update the internal logic of transitions and async functions.
Add new transitions, records, mappings, structs, or functions.
You cannot:
Change input or output parameters of existing transitions or functions.
Modify the logic inside non-inline functions.
Delete or modify existing structs, records, or mappings.
Delete or rewrite the constructor.
Plan your public interfaces carefully before deploying. Once they’re live, only internal logic and new features can evolve.
Security best practices. Program upgradability introduces new risks. Follow these principles to stay safe:
Audit the constructor thoroughly. It is permanent and unchangeable.
Avoid single points of failure. Use multi-signature wallets or DAO governance instead of one admin key.
Use timelocks for major upgrades. Give users time to respond or opt out.
Plan for immutability. When your project matures, you can lock it permanently by transferring admin rights to a burn address.
Test every upgrade locally. Use Aleo’s testnet or localnet to simulate upgrades before deploying to mainnet.
Legacy programs. If your program was deployed before upgradability was introduced, it is permanently non-upgradable. There is no way to add an upgrade path retroactively. If you need new features, you must deploy a new version of your program and migrate users to it.
More info you can find here:
https://developer.aleo.org/guides/program_upgradability/
Final thoughts. Upgradability is one of the most powerful features in Aleo. It gives developers the ability to evolve their applications without losing state or starting from scratch. But with that power comes responsibility.
Start simple. Use @admin or @custom for early experimentation. As your project grows, move to community-driven governance with @checksum, and eventually lock critical programs with @noupgrade for full trust.
Building responsibly means balancing innovation with security. By understanding how Aleo’s upgrade framework works, you can design systems that are both adaptable and reliable and that’s the foundation of any great decentralized application.
To know more about Aleo, join now!
Aleo Twitter
Aleo Discord
Aleo Website
List of Aleo and Leo code and resourses
Prepared by Colliseum
Hi, my name is Heorhii. I help teams build private, secure applications on Aleo. One of the most common questions developers ask is how to upgrade a Leo program safely without breaking existing contracts or exposing vulnerabilities. In this article, I’ll walk through how upgradability works on Aleo, how to define an upgrade policy, the main patterns you can use, and the best practices to keep your program secure and maintainable.
Upgrades begin with the constructor. Every Aleo program includes a special function called a constructor. It runs on-chain each time you deploy or upgrade your program. Think of it as the gatekeeper that decides whether an upgrade is allowed or not.
https://docs.leo-lang.org/guides/upgradability
Two rules define how constructors work:
A constructor is required. Without it, your deployment will fail.
The constructor is immutable. Once deployed, its logic can never be changed. If there’s a bug in your constructor, it will live forever. Audit it carefully before deployment.
Inside the constructor, you can access built-in metadata about your program using the self keyword. This includes:
self.edition: the program’s version number, starting at zero and increasing with each upgrade.
self.program_owner: the address of the account that deployed the program.
self.checksum: a unique hash representing the program’s code.
You can also reference metadata from other programs if you import them in your Leo file, for example Program::edition(credits.aleo) or Program::program_owner(foo.aleo).
Programs deployed before Leo version 3.1.0 do not include program_owner, so trying to access it will cause a runtime error.
https://github.com/ProvableHQ/leo-examples/tree/main/upgrades
Defining an upgrade policy. An upgrade policy defines how your program can evolve over time. In Leo, you define it by adding an annotation above the constructor. The compiler reads this annotation and generates the required validation code automatically.
There are four upgrade modes you can choose from:
1. Non-upgradable (@noupgrade). Use this mode if you want your program to remain fixed forever. It is the safest option for production systems that should never change.
program noupgrade_example.aleo {
@noupgrade
async constructor() {
// Compiler ensures no future upgrades are allowed
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
2. Admin-controlled (@admin). This mode allows only a single, predefined address to upgrade the program. It’s useful for early-stage development but risky for production if the admin key is compromised.
program admin_example.aleo {
@admin(address="aleo1rhgdu77hgyqd3xjj8ucu3jj9r2p3lam3tc3h0nvv2d3k0rp2ca5sqsceh7")
async constructor() {
// Compiler ensures only the specified admin can perform upgrades
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
3. Checksum-governed (@checksum). In this mode, the upgrade is verified against an on-chain checksum that is managed by another program, such as a DAO or governance system. This lets the community decide which versions are approved.
program vote_example.aleo {
@checksum(mapping="basic_voting.aleo/approved_checksum", key="true")
async constructor() {
// Compiler ensures only approved checksums are accepted
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
This pattern is ideal for decentralized governance, where multiple parties vote on upgrades before they go live.
4. Custom logic (@custom). If you want full control, you can write your own upgrade logic. For example, you might require that upgrades can only happen after a certain block height to create a time delay.
program timelock_example.aleo {
@custom
async constructor() {
if self.edition > 0u16 {
assert(block.height >= 1300u32);
}
}
transition main(public a: u32, b: u32) -> u32 {
let c = a + b;
return c;
}
}
This approach gives you flexibility to build advanced conditions such as multi-signature approvals, staking-based governance, or even time-based locks.
What you can and cannot change. When upgrading, the Aleo protocol enforces strict rules to maintain backward compatibility and prevent corruption of existing state.
You can:
Update the internal logic of transitions and async functions.
Add new transitions, records, mappings, structs, or functions.
You cannot:
Change input or output parameters of existing transitions or functions.
Modify the logic inside non-inline functions.
Delete or modify existing structs, records, or mappings.
Delete or rewrite the constructor.
Plan your public interfaces carefully before deploying. Once they’re live, only internal logic and new features can evolve.
Security best practices. Program upgradability introduces new risks. Follow these principles to stay safe:
Audit the constructor thoroughly. It is permanent and unchangeable.
Avoid single points of failure. Use multi-signature wallets or DAO governance instead of one admin key.
Use timelocks for major upgrades. Give users time to respond or opt out.
Plan for immutability. When your project matures, you can lock it permanently by transferring admin rights to a burn address.
Test every upgrade locally. Use Aleo’s testnet or localnet to simulate upgrades before deploying to mainnet.
Legacy programs. If your program was deployed before upgradability was introduced, it is permanently non-upgradable. There is no way to add an upgrade path retroactively. If you need new features, you must deploy a new version of your program and migrate users to it.
More info you can find here:
https://developer.aleo.org/guides/program_upgradability/
Final thoughts. Upgradability is one of the most powerful features in Aleo. It gives developers the ability to evolve their applications without losing state or starting from scratch. But with that power comes responsibility.
Start simple. Use @admin or @custom for early experimentation. As your project grows, move to community-driven governance with @checksum, and eventually lock critical programs with @noupgrade for full trust.
Building responsibly means balancing innovation with security. By understanding how Aleo’s upgrade framework works, you can design systems that are both adaptable and reliable and that’s the foundation of any great decentralized application.
To know more about Aleo, join now!
Aleo Twitter
Aleo Discord
Aleo Website
List of Aleo and Leo code and resourses
Prepared by Colliseum
Share Dialog
Share Dialog
No activity yet