Skip to content

Proxy Contracts ​

Automatic deployment of proxy contracts can be enabled in Forc.toml.

We recommend that you use fuels deploy to deploy and upgrade your contract using a proxy as it will take care of everything for you. However, if you want to deploy a proxy contract manually, you can follow the guide below.

Manually Deploying and Upgrading by Proxy ​

As mentioned above, we recommend using fuels deploy to deploy and upgrade your contract as everything is handled under the hood. But the below guide will detail this process should you want to implement it yourself.

We recommend using the SRC14 compliant owned proxy contract as the underlying proxy and that is the one we will use in this guide, as is the one used by fuels deploy.

The overall process is as follows:

  1. Deploy your contract
  2. Deploy the proxy contract
  3. Set the target of the proxy contract to your deployed contract
  4. Make calls to the contract via the proxy contract ID
  5. Upgrade the contract by deploying a new version of the contract and updating the target of the proxy contract

Note: When new storage slots are added to the contract, they must be initialized in the proxy contract before they can be read from. This can be done by first writing to the new storage slot in the proxy contract. Failure to do so will result in the transaction being reverted.

For example, lets imagine we want to deploy the following counter contract:

rs
contract;

abi Counter {
    #[storage(read)]
    fn get_count() -> u64;

    #[storage(write, read)]
    fn increment_count(amount: u64) -> u64;

    #[storage(write, read)]
    fn decrement_count(amount: u64) -> u64;
}

storage {
    counter: u64 = 0,
}

impl Counter for Contract {
    #[storage(read)]
    fn get_count() -> u64 {
        storage.counter.try_read().unwrap_or(0)
    }

    #[storage(write, read)]
    fn increment_count(amount: u64) -> u64 {
        let current = storage.counter.try_read().unwrap_or(0);
        storage.counter.write(current + amount);
        storage.counter.read()
    }

    #[storage(write, read)]
    fn decrement_count(amount: u64) -> u64 {
        let current = storage.counter.try_read().unwrap_or(0);
        storage.counter.write(current - amount);
        storage.counter.read()
    }
}
See code in context

Let's deploy and interact with it by proxy. First let's setup the environment and deploy the counter contract:

ts
import { Provider, Wallet } from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../env';
import {
  Counter,
  CounterFactory,
  CounterV2,
  CounterV2Factory,
  Proxy,
  ProxyFactory,
} from '../typegend';

const provider = await Provider.create(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const counterContractFactory = new CounterFactory(wallet);
const { waitForResult: waitForCounterContract } =
  await counterContractFactory.deploy();
const { contract: counterContract } = await waitForCounterContract();
See code in context

Now let's deploy the SRC14 compliant proxy contract and initialize it by setting its target to the counter target ID.

ts
/**
 * It is important to pass all storage slots to the proxy in order to
 * initialize the storage slots.
 */
const storageSlots = Counter.storageSlots.concat(Proxy.storageSlots);

/**
 * These configurables are specific to our recommended SRC14 compliant
 * contract. They must be passed on deploy and then `initialize_proxy`
 * must be called to setup the proxy contract.
 */
const configurableConstants = {
  INITIAL_TARGET: { bits: counterContract.id.toB256() },
  INITIAL_OWNER: {
    Initialized: { Address: { bits: wallet.address.toB256() } },
  },
};

const proxyContractFactory = new ProxyFactory(wallet);
const { waitForResult: waitForProxyContract } =
  await proxyContractFactory.deploy({
    storageSlots,
    configurableConstants,
  });
const { contract: proxyContract } = await waitForProxyContract();

const { waitForResult: waitForProxyInit } = await proxyContract.functions
  .initialize_proxy()
  .call();
await waitForProxyInit();
See code in context

Finally, we can call our counter contract using the contract ID of the proxy.

ts
/**
 * Make sure to use only the contract ID of the proxy when instantiating
 * the contract as this will remain static even with future upgrades.
 */
const initialContract = new Counter(proxyContract.id, wallet);

const { waitForResult: waitForIncrement } = await initialContract.functions
  .increment_count(1)
  .call();
await waitForIncrement();

const { value: count } = await initialContract.functions.get_count().get();
See code in context

Now let's make some changes to our initial counter contract by adding an additional storage slot to track the number of increments and a new get method that retrieves its value:

rs
contract;

abi Counter {
    #[storage(read)]
    fn get_count() -> u64;

    #[storage(read)]
    fn get_increments() -> u64;

    #[storage(write, read)]
    fn increment_count(amount: u64) -> u64;

    #[storage(write, read)]
    fn decrement_count(amount: u64) -> u64;
}

storage {
    counter: u64 = 0,
    increments: u64 = 0,
}

impl Counter for Contract {
    #[storage(read)]
    fn get_count() -> u64 {
        storage.counter.try_read().unwrap_or(0)
    }

    #[storage(read)]
    fn get_increments() -> u64 {
        storage.increments.try_read().unwrap_or(0)
    }

    #[storage(write, read)]
    fn increment_count(amount: u64) -> u64 {
        let current = storage.counter.try_read().unwrap_or(0);
        storage.counter.write(current + amount);

        let current_iteration: u64 = storage.increments.try_read().unwrap_or(0);
        storage.increments.write(current_iteration + 1);

        storage.counter.read()
    }

    #[storage(write, read)]
    fn decrement_count(amount: u64) -> u64 {
        let current = storage.counter.try_read().unwrap_or(0);
        storage.counter.write(current - amount);
        storage.counter.read()
    }
}
See code in context

We can deploy it and update the target of the proxy like so:

ts
const { waitForResult: waitForCounterContractV2 } =
  await CounterV2Factory.deploy(wallet);
const { contract: counterContractV2 } = await waitForCounterContractV2();

const { waitForResult: waitForUpdateTarget } = await proxyContract.functions
  .set_proxy_target({ bits: counterContractV2.id.toB256() })
  .call();

await waitForUpdateTarget();
See code in context

Then, we can instantiate our upgraded contract via the same proxy contract ID:

ts
/**
 * Again, we are instantiating the contract with the same proxy ID
 * but using a new contract instance.
 */
const upgradedContract = new CounterV2(proxyContract.id, wallet);
const { waitForResult: waitForSecondIncrement } =
  await upgradedContract.functions.increment_count(1).call();
await waitForSecondIncrement();

const { value: increments } = await upgradedContract.functions
  .get_increments()
  .get();
const { value: secondCount } = await upgradedContract.functions
  .get_count()
  .get();
See code in context

For more info, please check these docs: