Author @eug33ne
Status Completed
Created Jun 18 2025 4:23 PM
Updated Jun 26 2025 4:00 PM
DiscussionGithub Discussion
Original authors: ProvableHQ
As the Aleo ecosystem matures, it is apparent that applications will need to be upgraded over their lifetime. The Aleo Virtual Machine (AVM) provides a method for program-driven upgrades. This method, while sound, has properties that may be undesirable for some applications. This proposal offers a new method for program upgrades that seeks to be timely and cost-effective, while preventing fragmentation of application state.
We propose a system for program upgrades with the following properties:
function or finalizestructs, records, or mappings but cannot modify or remove existing ones.functions and closuresfunction’s input and output interfacefinalize’s input interfaceclosure. This is because a change in a closure would invalidate all dependent proving and verifying keys.structs, records, or mappings| Program Component | Delete | Modify | Add |
|---|---|---|---|
import | ❌ | ❌ | ✅ |
struct | ❌ | ❌ | ✅ |
record | ❌ | ❌ | ✅ |
mapping | ❌ | ❌ | ✅ |
closure | ❌ | ❌ | ✅ |
function | ❌ | ✅ (logic) | ✅ |
finalize | ❌ | ✅ (logic) | ✅ |
constructor | ❌ | ❌ | ❌ |
At a high-level, our proposed design introduces constructors and operands so that users can programmatically define upgrades with the above design goals in mind.
The Aleo Virtual Machine will support the following new operands:
<PROGRAM_ID>/checksum<PROGRAM_ID>/edition<PROGRAM_ID>/program_ownerThese specific operands can only be used in off-chain AND on-chain contexts.
checksum is defined as the SHA3-256 hash of the program string.function and finalize scope.[u8; 32u32] and is optionally declared in a Deployment transaction.checksum is NOT defined in a Deployment.finalize scopeedition denotes the version of a program.function and finalize scope.u16 literal and is explicitly declared in a Deployment transaction.edition must be 0 when a program is first deployed and must be incremented by 1 on each upgrade.finalize scopeprogram_owner is the address that deployed the program.finalize scope.address and is optionally declared in a Deployment transaction.program_owner is NOT defined in a Deployment.program_owner is defined in a Deployment.Programs can define constructors which are immutable (they cannot be upgraded) sequences of code that are always run on-chain as part of a deployment or upgrade. Here is an example:
program foo.aleo;
...
constructor:
assert.eq foo.aleo/edition 0u16;
In this example, the constructor checks that its edition is always zero, ensuring that the program cannot be upgraded.
Additionally, constructors have the following rules:
A core idea of this design is to allow developers to define rich instantiation and upgrade logic that suits their needs. Refer to the Usage section for more examples.
To support upgrades, deployments are checked using the additional rules:
NoneNoneconstructor or any of the new operands.Some and that it matches the program.Some and that it matches the transactionconstructoredition is zero (N::EDITION), then check that:
ProcessProcessIn order to ensure that upgrades are correct and preserve existing state, we also require that:
A constructor uses the same cost model as on-chain execution, with a multiplier. Currently, this multiplier is set to 100.
The tracking issue for the implementation can be found here.
The protocol must ensure that:
Upgrades are a powerful mechanism for improving program usability and management. However, mutability fundamentally introduces new risks for users and dependent applications. Mutable applications can be modified in malicious ways. For example, a developer can modify program logic to introduce a vulnerability or freeze the function. They could even introduce a new function to drain funds. In the general case, there is not much a user can do. Users should validate that applications are written correctly and build trust in developers. Developers can demonstrate good intent to users by relying on governance mechanisms or restrictions to what an upgrade can do. Developers can use the upgrade mechanism to “ossify” a program by setting the authority to a “null” value, like the zero address. Ultimately, these are only mitigations and the burden falls on the user to do due-diligence. Dependent programs have some defenses available to them. In Usage, we detail a mechanism where upgrades must be explicitly approved. This allows developers to fix dependent programs and issue a migration when a dependent makes a divergent change. It is worth noting that protocols like Sui do not atomically apply upgrades; the dependent application defaults to the old logic until the developer issues an upgrade. This option, while mitigating risks of malicious upgrades, prevents security patches or critical features from being applied immediately, which can be an issue. Developers also need to be mindful of the fact that constructors are immutable. This means that a bug in the constructor logic cannot be fixed by rolling out a new upgrade. Developers should take special care and perform security audits. The protocol provides some application security by requiring that all executions contains signatures over the checksums of the programs they intend to execute. This reduces the likelihood that a malicious developer front-runs an execution with an upgrade that a user did not know about.
Audits have been completed and their findings are addressed in this PR: ProvableHQ/snarkVM#2758. The reports will be made public.
In this section, we detail how constructors and metadata declarations can be used to implement a variety of upgrades.
Goal. Define a program that cannot be upgraded.
program foo.aleo;
... // A program without a constructor cannot be upgraded.
program foo.aleo;
...
constructor:
assert.eq foo.aleo/edition 0u16; // Upgrades will be rejected as their editions will always be nonzero.
Goal. Define a program that anyone can upgrade.
program foo.aleo;
...
constructor:
assert.eq true true:
...
Goal. Require that a dependent program is on a specific version. Important. If using this pattern, we recommend you make your program upgradable, in case your function is locked due to a dependency upgrade.
Goal. Allow a developer to lock a program from future upgrades. Important. Note that this pattern can be generalized to support ‘toggle-able’ upgrades.
program foo.aleo;
...
mapping locked: // Once an entry is set, assuming that there is no code that removes it, the program will be locked from future upgrades.
key as boolean;
value as boolean;
...
constructor:
contains locked[true] into r0;
assert.eq r0 false;
...
Goal. Define a program that only an authorized admin can upgrade. Important. Note that the admin account can NEVER be changed. This pattern should be used IFF the private key associated with the admin is properly secured.
program foo.aleo;
...
constructor:
assert.eq program_owner <ADMIN_ADDRESS>; // You can add any number of admin addresses.
...
Goal. Define a program that can only be upgraded to a program with some specific contents. This helps ensure that an upgrade has some pre-determined logic.
program foo.aleo;
...
mapping expected:
key as boolean;
value as u128;
...
constructor:
branch.eq foo.aleo/edition 0u16 to end;
get expected[true] into r0; // Assume that there was some mechanism to set the expected value.
assert.eq foo.aleo/checksum r0;
position end;
...
Goal. Define a program that only an authorized admin can upgrade, with the option to swap admins and declare programs upfront. This pattern combines the ideas behind Content-Locked Upgrades with the notion of an administrator.
program foo.aleo;
...
mapping admin:
key as boolean.public;
value as address.public;
...
mapping expected:
key as boolean;
value as [u8; 32u32];
...
constructor:
branch.neq foo.aleo/edition 0u16 to rest;
set aleo1... into admin[true]; // Set the admin.
branch.eq true true to end;
position rest;
get expected[true] into r0;
assert.eq foo.aleo/checksum r0; // Check that the checksum matches.
position end;
...
function set_expected:
input r0 as [u8; 32u32].public;
async set_expected self.caller r0 into r1;
output r1 as foo.aleo/set_expected.future;
finalize set_expected:
input r0 as address.public; // The caller.
input r1 as [u8; 32u32].public; // The expected checksum.
get admin[true] into r2; // Get the admin.
assert.eq r0 r2; // Check that the caller is an admin.
set r1 into expected[true]; // Set the next expected upgrade.
...
Goal. Define a program that can only be upgraded by an approving vote from a governance contract. \
import governor.aleo; // Assume `governer.aleo` has some logic for voting on upgrades. Accepted votes are recorded in a mapping `accepted` which contains an expected checksum.
program foo.aleo;
...
constructor:
branch.eq foo.aleo/edition 0u16 to end;
get governor.aleo/accepted[true] into r0;
assert.eq foo.aleo/checksum r0;
position end;
Goal. Define a program that can only be upgraded after a specific block height.
program foo.aleo;
...
constructor:
gte block.height 10u32 into r0;
assert.eq r0 true; // Upgrades can be made by anyone, only after block 10.
...