Skip to main content

Deployer

The deployer example demonstrates how to deploy contracts using a contract.

Open in Gitpod

info

In this example there are two contracts that are compiled separately, and the tests deploy one with the other.

Run the Example

First go through the Setup process to get your development environment configured, then clone the v0.6.0 tag of soroban-examples repository:

git clone -b v0.6.0 https://github.com/stellar/soroban-examples

Or, skip the development environment setup and open this example in Gitpod.

To run the tests for the example, navigate to the deployer/deployer directory, and use cargo test.

cd deployer/deployer
cargo test

You should see the output:

running 1 test
test test::test ... ok

Code

deployer/deployer/src/lib.rs
pub struct Deployer;

#[contractimpl]
impl Deployer {
/// Deploy the contract using WASM hash and after deployment invoke the init
/// function of the contract with the given arguments. Returns the
/// contract ID and result of the init function.
pub fn deploy(
env: Env,
salt: Bytes,
wasm_hash: BytesN<32>,
init_fn: Symbol,
init_args: Vec<RawVal>,
) -> (BytesN<32>, RawVal) {
// Deploy the contract using the installed WASM code with given hash.
let id = env.deployer().with_current_contract(salt).deploy(wasm_hash);
// Invoke the init function with the given arguments.
let res: RawVal = env.invoke_contract(&id, &init_fn, init_args);
// Return the contract ID of the deployed contract and the result of
// invoking the init result.
(id, res)
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v0.6.0/deployer

How it Works

Contracts can deploy other contracts by using the SDK.

The contract ID of the deployed contract is deterministic and is derived from the deploying contract ID and the provided salt.

Open the deployer/deployer/src/lib.rs file to follow along.

Contract Code Installation

Before deploying the new contract instances, the WASM code needs to be installed on-chain. Then it can be used to deploy an arbitrary number of contract instances. The installation should typically happen outside of the deployer contract, as it needs to happen just once. while the deployer contract can be called multiple times.

See the tests for an example of installing the contract code in tests. For the actual on-chain installation see the general deployment tutorial.

Deployer

The Deployer contract deploys other contracts. It accepts a salt that will be used to derive a contract ID and a hash-based identifier of the installed WASM (note, that this is not just the hash of the WASM file). It also accepts an initialization function and arguments to pass to that function.

The contract first gets a deployer using the salt, then calls deploy with the WASM hash. The contract ID for the new contract is returned. The implementation of the new contract is defined by the WASM file installed under wasm_hash ( only the wasm_hash itself is stored per contract ID thus saving the ledger space and fees).

The contract ID is deterministic and is derived from the deploying contract and the salt.

let id = env.deployer().with_current_contract(salt).deploy(wasm_hash);

The contract invokes the new contract's initialization function and passes through the arguments.

let res: RawVal = env.invoke_contract(&id, &init_fn, init_args);

The contract returns the new contract ID and the result of the initialization function.

(id, res)

Tests

Open the deployer/deployer/src/test.rs file to follow along.

deployer/deployer/src/test.rs
// The contract that will be deployed by the deployer contract.
mod contract {
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm"
);
}

#[test]
fn test() {
let env = Env::default();
let client = DeployerClient::new(&env, &env.register_contract(None, Deployer));

// Install the WASM that will be deployed by the deployer contract.
let wasm_hash = env.install_contract_wasm(contract::WASM);

// Deploy contract using deployer, and include an init function to call.
let salt = Bytes::from_array(&env, &[0; 32]);
let init_fn = symbol!("init");
let init_fn_args = (5u32,).into_val(&env);
let (contract_id, init_result) = client.deploy(&salt, &wasm_hash, &init_fn, &init_fn_args);
assert!(init_result.is_void());

// Invoke contract to check that it is initialized.
let client = contract::Client::new(&env, &contract_id);
let sum = client.value();
assert_eq!(sum, 5);
}

The test imports a test contract from the soroban_deployer_test_contract.wasm file.

mod contract {
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm"
);
}

That contract contains the following code, which exports a couple functions that sets a value in the initialization function and allows the value to be retrieved from another function.

deployer/contract/src/lib.rs
pub struct Contract;

const KEY: Symbol = symbol!("value");

#[contractimpl]
impl Contract {
pub fn init(env: Env, value: u32) {
env.storage().set(KEY, value);
}
pub fn value(env: Env) -> u32 {
env.storage().get_unchecked(KEY).unwrap()
}
}

The test contract will be used when testing the deployer. The deployer contract deploys the test contract and invokes its init function.

In any test the first thing that is always required is an Env, which is the Soroban environment that the contract will run in.

let env = Env::default();

Install the code of the test contract that we have imported above via contractimport! and get the hash of the installed WASM code:

// Install the WASM that will be deployed by the deployer contract.
let wasm_hash = env.install_contract_wasm(contract::WASM);

Register the deployer contract with the environment.

env.register_contract(&deployer_id, Deployer);

Create a client to invoke the Deployer contract.

let client = DeployerClient::new(&env, &deployer_id);

All public functions within an impl block that is annotated with the #[contractimpl] attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with Client appended. For example, in our contract the contract type is Deployer, and the client is named DeployerClient.

The client is used to invoke the deploy function. The contract will deploy the test contract using the hash of its WASM code, call the "init" function, and pass in a single 5u32 argument.

// Deploy contract using deployer, and include an init function to call.
let salt = Bytes::from_array(&env, &[0; 32]);
let init_fn = symbol!("init");
let init_fn_args = (5u32,).into_val(&env);
let (contract_id, init_result) = client.deploy(&salt, &wasm_hash, &init_fn, &init_fn_args);
assert!(init_result.is_void());

The test checks that the test contract was deployed by using its client to invoke it and get back the value set during initialization.

let client = contract::Client::new(&env, &contract_id);
let sum = client.value();
assert_eq!(sum, 5);

Build the Contracts

To build the contract into a .wasm file, use the cargo build command. Build both the deployer contract and the test contract.

cargo build --target wasm32-unknown-unknown --release

Both .wasm files should be found in the ../target directory after building both contracts:

../target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm
../target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm

Run the Contract

If you have soroban-cli installed, you can invoke the contract function to deploy the test contract.

Before deploying the test contract with the deployer, install the test contract WASM using the install command. The install command will print out the hash derived from the WASM file (it's not just the hash of the WASM file itself though) which should be used by the deployer.

soroban contract install --wasm target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm

The command prints out the hash as hex. It will look something like edce149b183ea03dfd896a263d2e17c1f08229da52afa6f822d9cbc67c103806.

Then the deployer contract may be invoked with the WASM hash value above.

soroban contract invoke \
--wasm target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm \
--id 0 \
--fn deploy \
-- \
--salt 0000000000000000000000000000000000000000000000000000000000000000 \
--wasm_hash edce149b183ea03dfd896a263d2e17c1f08229da52afa6f822d9cbc67c103806 \
--init_fn init \
--init_args '[{"u32":5}]'

And then invoke the deployed test contract using the identifier returned from the previous command.

soroban contract invoke \
--id ead19f55aec09bfcb555e09f230149ba7f72744a5fd639804ce1e934e8fe9c5d \
--fn value

The following output should occur using the code above.

5