Skip to content
156 changes: 82 additions & 74 deletions contract-dev/techniques/upgrades.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@ title: "Upgrading contracts"

import { Aside } from '/snippets/aside.jsx';

The address of a contract is determined by its [initial code and state](/foundations/status). A contract can upgrade its code while preserving its address. This is useful for fixing bugs, adding features, or adapting to protocol changes without migrating to a new address.
The address of a contract is determined by its [initial code and state](/foundations/status).
However, with upgrades it is possible to change the code of a contract while keeping its initial address. This allows developers to fix bugs, add features, and adapt to protocol changes without migrating to a new address, which is required for contracts that are already referenced by other contracts or have users interacting with them.

Upgrades are critical when other contracts reference the contract being upgraded. For example, [NFT items](/standard/tokens/nft/how-it-works) reference their collection contract. The collection admin cannot modify these references stored in existing NFT items. Without upgrades, fixing bugs or adding features would require deploying a new collection and migrating all items—an expensive and complex process. Upgrades solve this by allowing the collection contract to evolve in place while preserving all existing references.
For example, [NFT item contracts](/standard/tokens/nft/how-it-works) reference their collection contract. If the collection contract address changes, all item contracts would need to point to the new address, but the collection admin cannot modify existing item contracts. With upgrades the collection contract address stays the same, so all item contracts continue to reference it without any changes. A collection admin can then upgrade the collection contract code to fix bugs or add features without affecting the item contracts.

The pattern is also essential for [vanity](/contract-dev/vanity) contracts and protocols such as distributed exchange (DEX) where preserving the contract address is critical.
The upgrade pattern is also required for [vanity](/contract-dev/vanity) contracts and protocols such as distributed exchanges (DEXs) that are referenced by many other contracts.

## How upgrades work

Tolk provides two functions for upgrades.
Tolk provides two functions for upgrades, one for code and one for data:

- `contract.setCodePostponed(code: cell)` — schedules the code to be replaced during the [action phase](/foundations/phases#action-phase). The new code takes effect after the current transaction completes.
- `contract.setData(data: cell)` — immediately replaces the contract's persistent storage. This happens during the [compute phase](/foundations/phases#compute-phase), before the transaction ends.

Key difference: `setCodePostponed()` applies changes _after_ the current transaction, while `setData()` applies changes _immediately_. This means the new code won't run until the next message arrives, but the new data is already active.
- `contract.setCodePostponed(code: cell)` schedules a code replacement during the [action phase](/foundations/phases#action-phase). The new code is available **after the current transaction completes**.
- `contract.setData(data: cell)` immediately replaces the contract's persistent storage. This happens during the [compute phase](/foundations/phases#compute-phase), **before the transaction ends**.

<Aside
type="caution"
Expand All @@ -30,27 +29,37 @@ Key difference: `setCodePostponed()` applies changes _after_ the current transac
type="caution"
title="Ethics"
>
Use delayed upgrades to allow users to [react to compromised admin keys](https://blog.trailofbits.com/2025/06/25/maturing-your-smart-contracts-beyond-private-key-risk/) or [unwanted updates](https://en.wikipedia.org/wiki/Exit_scam).
Use delayed upgrades to allow users to react to compromised admin keys or unwanted updates.

[The Trail of Bits blog](https://blog.trailofbits.com/2025/06/25/maturing-your-smart-contracts-beyond-private-key-risk/) and [Wikipedia](https://en.wikipedia.org/wiki/Exit_scam) provide additional information on this topic.
</Aside>

## Basic upgrade pattern

The contract accepts upgrade messages containing new code and data. Only the admin can trigger upgrades.
Upgradable contracts accept upgrade messages containing new code and data. Only an admin can trigger upgrades.

### How it works

1. Admin sends upgrade message. The message contains new code, data, or both.
1. Contract verifies sender. Checks that the sender is the admin address.
1. Code is scheduled. If new code is provided, `setCodePostponed()` schedules it for replacement.
1. Data is upgraded. If new data is provided, `setData()` immediately replaces the storage.
1. Transaction completes. The action phase executes, applying the new code.
1. Next message uses new code. Subsequent messages execute with the upgraded logic.
1. Send an upgrade message to the contract that contains new code, data, or both.
1. Verify that the message comes from an admin address.
1. If the message contains code, schedule the code replacement with `setCodePostponed()`.
1. If the message contains data, replace the existing data with `setData()`.
1. During the action phase, apply the scheduled code replacement.
1. Process subsequent messages with the new code after the transaction completes.

The upgrade runs in a single transaction. New code becomes active after the transaction completes, and new data is available when the transaction ends. If the message does not provide enough Toncoin to run both the compute phase and the action phase, the entire transaction is aborted and no state changes from the upgrade are applied. Test the upgrade script to estimate gas requirements, and send enough Toncoin to execute the full upgrade transaction.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering about this one technically. The stdlib reference says contract.setCodePostponed() creates an output action that takes effect only after successful termination, while contract.setData() writes storage during the compute phase. If the compute phase succeeds (so setData commits) but the action phase then runs out of Toncoin, I'd expect the storage change to persist while the SETCODE action does not - leaving the contract with the new data schema but still the old code.

If that's right, then "the entire transaction is aborted and no state changes from the upgrade are applied" is misleading, and this is actually a fairly sharp footgun worth calling out directly. Could be worth verifying against the actual TVM semantics and either correcting the claim or adding a warning.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

As far as the docs explain it:

"If an action fails (for example, due to a lack of funds), the transaction may revert or skip the action, depending on its mode. For example, mode = 0, flag = 2 means that any errors arising while processing this message during the action phase are ignored."

Depending on these modes, it seems both can be true.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that by default, everything is reverted, but setting the SendIgnoreErrors flag for a message will ignore the insufficient funds error, and can lead to a situation where the storage is updated, but the code is not.

I added a caution aside below to explain it, because it is possible, but not the default behavior.


<Aside
type="caution"
>
Sending a transaction with the `SendIgnoreErrors` flag will ignore the insufficient funds error, and the transaction will succeed. This can leave a contract in an unusable state with a new storage structure but old code that cannot read it.

The upgrade happens immediately in a single transaction. The new code becomes active after the transaction completes. Any data replacement happens during the compute phase, so the new data is immediately available when the transaction ends.
Learn more about [execution phases](/foundations/phases) and [sending modes](/foundations/messages/modes) in the docs.
</Aside>

If there's not enough Toncoin to execute action phase with the code update, but enough to execute compute phase with data update, the contract state changes will be reverted. Make sure to thoroughly test the upgrade script for possible gas issues, and provide enough Toncoin to execute the upgrade transaction completely.
### Example contract

### Example
The following contract accepts `UpgradeContract` messages that contain new code or data. Only admins can trigger upgrades.

```tolk title="Tolk" expandable
struct (0x1111) UpgradeContract {
Expand Down Expand Up @@ -96,25 +105,25 @@ fun Storage.save(self) {
}
```

## Delayed upgrades for production safety

When upgrading protocols that are already running and have users, delayed upgrades are a best practice. This provides additional security layers: if an admin is compromised, there is time to react. Users can also see the upgrade and withdraw funds from the protocol if it has been compromised.
## Delayed upgrade pattern

The pattern adds a time delay between requesting and approving an upgrade. The admin must first request an upgrade, wait for a timeout period, then approve it.
Consider using the delayed upgrade pattern for production contracts with active users. This pattern adds a time delay between requesting and approving an upgrade, providing an additional security layer. The delay allows users to withdraw funds or exit positions if they do not trust the upgrade or if an admin account is compromised. It also gives users time to review the proposed changes before they take effect.

### How it works

1. Admin requests upgrade. Sends `RequestUpgrade` message with new code and data
1. Contract verifies and stores. Validates admin, ensures no pending request, stores upgrade details with timestamp
1. Timeout period. The contract enforces a waiting period before approval
1. Admin approves upgrade. Sends `ApproveUpgrade` message after timeout expires
1. Contract verifies timeout. Checks that enough time has passed since the request
1. Upgrade applies. Schedules new code with `setCodePostponed()` and upgrades data with `setData()`
1. Request cleared. Removes the pending request from storage
1. An admin sends a `RequestUpgrade` message with new code, new data, or both.
1. The contract verifies the message came from an admin and stores the upgrade details with a timestamp.
1. The contract waits for the specified timeout, before accepting approvals.
1. An admin sends an `ApproveUpgrade` message after the timeout expires.
1. The contract checks that enough time has passed since the request.
1. If the request is approved, the contract schedules new code with `setCodePostponed()` and upgrades data with `setData()`.
1. The contract removes the pending request from storage.
Comment thread
kay-is marked this conversation as resolved.

The admin can also send `RejectUpgrade` at any time to cancel a pending upgrade. This three-message flow (request → wait → approve or reject) gives users time to review changes and react if the admin account is compromised.
Admins can also send `RejectUpgrade` at any time to cancel a pending upgrade. This three-message flow (request → wait → approve or reject) gives users time to review changes and react if an admin account is compromised.

### Example
### Example contract

The following code illustrates the delayed upgrade pattern. The contract accepts `RequestUpgrade`, `RejectUpgrade`, and `ApproveUpgrade` messages. Only admins can trigger these actions.

```tolk title="Tolk" expandable
struct UpgradeContract {
Expand Down Expand Up @@ -215,36 +224,36 @@ fun Storage.save(self) {
}
```

## Hot upgrades for frequently upgraded contracts
## Hot upgrade pattern

Standard upgrade methods fail when a contract receives frequent updates. For example, DEX pools that update prices every second or lending protocols that continuously adjust interest rates. The problem: it is not possible to predict what data will be in storage when the upgrade transaction executes.
The standard upgrade methods fail when contracts receive frequent updates. For example, DEX pools that update prices every second or lending protocols that continuously adjust interest rates. The problem: it is not possible to predict what data will be in storage when the upgrade transaction executes.

When an upgrade message with new code and data is sent, other transactions may execute before the upgrade arrives. By the time the upgrade applies, the prepared data may be stale. For a DEX pool, this can overwrite current price data with outdated values, breaking the protocol.
Other transactions might execute before an upgrade arrives. By the time the upgrade applies, the prepared data may be stale. For a DEX pool, this can lead to outdated values, breaking the protocol.

Hot upgrades solve this by scheduling a code change and immediately calling a migration function with the new code. The migration function runs in the same transaction that applies the upgrade. It reads the old storage structure, transforms it to match the new schema, and writes the upgraded storage. This preserves all state changes that happened between preparing the upgrade and executing it.
Hot upgrades solve this by scheduling a code change and immediately calling a migration function with the new code. The migration function runs in the same transaction that applies the upgrade. It reads the old storage structure, transforms it to match the new schema, and writes the upgraded storage to preserve all state changes that happened between preparing the upgrade and executing it.

### How it works

1. Admin sends upgrade message. The message contains new code cell and optional additional data
1. Contract verifies sender. Checks that the sender is the admin address
1. Schedule code change. `setCodePostponed()` schedules the code replacement
1. Switch to new code. `setTvmRegisterC3()` immediately activates the new code in register [C3](/tvm/registers#c3-—-function-selector)
1. Call migration. Invoke `hotUpgradeData()` which now runs with the new code
1. Migration executes. The function reads old storage, transforms it, and writes new storage
1. Send an upgrade message with the new code cell and optional additional data.
1. Verify the message comes from an admin address.
1. Call `setCodePostponed()` to schedule the code replacement.
1. Call `setTvmRegisterC3()` to activate the new code in register [C3](/tvm/registers#c3-—-function-selector) immediately.
1. Call `hotUpgradeData()` to run the migration with the new code.

The key mechanism: `setTvmRegisterC3()` switches the code register so the migration function executes with the new code in the same transaction. The migration reads the current storage state (preserving all updates), transforms it to the new schema, and saves it. When the transaction completes, the new code becomes permanent through `setCodePostponed()`.
The `setTvmRegisterC3()` is the key to hot upgrades. It replaces the current code immediately so the following command (e.g., `hotUpgradeData()`) runs the new code. The migration function reads the current storage, transforms it to the new schema, and saves it. After the transaction completes, the new code becomes permanent through `setCodePostponed()`.

<Aside
type="caution"
title="Migration risks"
>
Hot upgrades require careful migration logic. Test migrations thoroughly on testnet. If the migration function fails, the contract becomes unusable. The `hotUpgradeData()` function runs only during upgrade messages, not on regular messages, preventing accidental repeated migrations.
Hot upgrades require careful migration logic. Test migrations thoroughly on testnet. If the migration function throws, the upgrade transaction aborts and no state changes are applied. If the migration succeeds but writes invalid storage, the contract can become unusable. The `hotUpgradeData()` function runs only during upgrade messages, not on regular messages, preventing accidental repeated migrations.
</Aside>

### Example
### Example code

Comment thread
kay-is marked this conversation as resolved.
The example shows a counter contract that adds a metadata field through a hot upgrade. The storage structure changes: the original version stores only `adminAddress` and `counter`. The new version adds `metadata` and reorders fields.
The example shows a counter contract that changes the storage structure through a hot upgrade. The original version stores only `adminAddress` and `counter`; the new version adds `metadata` and reorders fields.

Original contract:
The original contract code before the upgrade:

```tolk title="main.tolk" expandable
import "@stdlib/tvm-lowlevel"
Expand Down Expand Up @@ -311,15 +320,11 @@ fun Storage.save(self) {
}
```

The `hotUpgradeData()` function in the original code returns `null` because it does not perform any migration. When the upgrade message arrives:

1. `contract.setCodePostponed(msg.code)` schedules the new code
1. `setTvmRegisterC3()` switches register C3 to the new code immediately
1. `hotUpgradeData(msg.additionalData)` is called and runs with the new code
The contract never executes the original `hotUpgradeData()` function because it is immediately replaced by the new code during the upgrade. The new code defines the actual migration logic. That is why the migration function must have a `method_id` that is stable across versions, so the runtime can call it after the upgrade.

New contract with migration:
New contract code that applies the hot upgrade:

```tolk title="new.tolk"
```tolk title="new.tolk" expandable
import "@stdlib/tvm-lowlevel"

struct (0x00001111) HotUpgrade {
Expand All @@ -336,7 +341,7 @@ type AllowedMessages =
// migration function must have method_id
@method_id(2121)
fun hotUpgradeData(additionalData: cell?) {
var oldStorage = lazy oldStorage.load();
var oldStorage = lazy OldStorage.load();

assert (additionalData != null) throw 1112;

Expand All @@ -349,13 +354,13 @@ fun hotUpgradeData(additionalData: cell?) {
contract.setData(storage.toCell());
}

struct oldStorage {
struct OldStorage {
adminAddress: address
counter: uint32
}

fun oldStorage.load() {
return oldStorage.fromCell(contract.getData());
fun OldStorage.load() {
return OldStorage.fromCell(contract.getData());
}


Expand Down Expand Up @@ -411,35 +416,38 @@ fun Storage.save(self) {
}
```

In the new version, `hotUpgradeData()` performs the migration:
The new version of `hotUpgradeData()` function is what is called after the code was switched with `setTvmRegisterC3()` and performs the migration.
Comment thread
kay-is marked this conversation as resolved.

The migration logic follows these steps:

1. Loads storage using the old structure (`oldStorage` with `adminAddress` and `counter`)
1. Creates new storage with the additional `metadata` field from `additionalData`
1. Reorders fields (`counter` moves before `adminAddress`)
1. Writes the migrated storage immediately with `contract.setData()`
1. Load the storage using the old structure (e.g., `OldStorage` with `adminAddress` and `counter`).
1. Create new storage with the additional `metadata` field from `additionalData`.
1. Reorder the fields to move the `counter` before the `adminAddress`.
1. Write the migrated storage immediately with `contract.setData()`.

The migration runs in the same transaction as the upgrade message. Any counter increments that happened between preparing the upgrade and executing it remain in storage because the migration reads the current state, not a pre-prepared snapshot. The migration function explicitly handles the structure change by reading fields from the old layout and writing them in the new layout.

### When to use hot upgrades

Use hot upgrades when:
Use hot upgrades in the following scenarios:

- The contract receives frequent state updates (DEX pools, oracles, lending protocols)
- Storage changes between preparing and applying the upgrade would cause data loss
- You need to preserve all intermediate state transitions
- The contract receives frequent state updates (DEX pools, oracles, lending protocols.)
- Storage changes between preparing and applying the upgrade would cause data loss.
- All intermediate state transitions must be preserved during the upgrade.
Comment thread
kay-is marked this conversation as resolved.

Use standard upgrades when:
Use standard upgrades instead when:

- The contract upgrades infrequently
- You can predict storage state at upgrade time
- Simpler upgrade logic reduces risk
- The contract upgrades infrequently.
- Storage state at upgrade time is predictable.
- Simpler upgrade logic would reduce risk.
Comment thread
kay-is marked this conversation as resolved.

## Combining delayed and hot upgrades

You can combine delayed upgrades with hot upgrades for production protocols that require both safety and structure migration. The delayed pattern provides time for users to review changes, while the hot upgrade mechanism handles storage migration without data loss.
Combine delayed upgrades with hot upgrades for production protocols that require both safety and structure migration. The delayed upgrade pattern provides time for users to review changes, while the hot upgrade mechanism handles storage migration without data loss.

<Aside
title={"📁 Complete Example Code"}
type="tip"
title="Complete example code"
>
You can find full working examples demonstrating all upgrade patterns in our [GitHub repository](https://github.com/ton-org/docs-examples/tree/main/contract-dev/Upgrading). This includes implementations for basic, delayed, and hot upgrade patterns.
The [TON Examples GitHub repository](https://github.com/ton-org/docs-examples/tree/main/contract-dev/Upgrading) contains full working examples demonstrating all upgrade patterns. This includes implementations for basic, delayed, and hot upgrade patterns.
Comment thread
kay-is marked this conversation as resolved.
</Aside>
Loading