DEV Community

yuzurush
yuzurush

Posted on • Updated on

Soroban Contracts 101 : Deployer

Hi there! Welcome to my ninth post of my series called "Soroban Contracts 101", where I'll be explaining the basics of Soroban contracts, such as data storage, authentication, custom types, and more. All the code that we're gonna explain throughout this series will mostly come from soroban-contracts-101 github repository.

In this ninth post of the series, I'll be covering soroban deployer. Deployer is a contract that can deploy and initialize other contracts. Some key benefits of using a deployer contract are:

Abstraction - the deployment logic is separated from main contracts, which can simplify their design
Privileged deployment - only designated deployer accounts/contracts can deploy other contracts, which can be important for security
Standardization - having a common deployer contract interface could allow interoperable deployment of contracts from various sources

We're gonna using 2 contract, Deployer contract act as the "contract deployer", and Contract contract act as the "test contract" that are going to be deployed by our Deployer contract.

The Contracts Code

Test Contract Code

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

Our Test Contract code defines a simple contract that:

  • Stores a value at a constant KEY in init
  • Exposes a value function to retrieve the stored value

This is a very basic contract that shows storing and retrieving a value from contract storage.

Deployer Contract Code

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Our deployer contract can deploy and initialize other contracts. It:

  • Defines a Deployer struct
  • Implements a contractimpl on Deployer
  • Defines a deploy function that:
  • Takes various input parameters (salt, WASM hash, init function name, init arguments)
  • Uses the Env's deployer to deploy a contract from the given WASM hash
  • Calls the init function on the deployed contract, passing in the init arguments
  • Returns the contract ID and init result

The Deployer Contract Test Code

// 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);
}

Enter fullscreen mode Exit fullscreen mode

Our test code here exercises deploying and initializing a contract using Deployer contract. It:

  • Creates an Env
  • Registers the Deployer contract
  • Creates a Deployer client
  • Installs the WASM for the deployed Contract contract
  • Calls deploy on the Deployer, passing in:
  • Salt
  • TestDeployer WASM hash
  • init function name
  • init argument (5)
  • Asserts that the init returns () (is void)
  • Creates a contract client and verifies that it returns the expected value (5), showing it was properly initialized

So this test thoroughly exercises the deployer/deployed contract scheme, and verifies that the deployed contract was initialized as expected.

Running Contract Tests

Test conducted in deployer directory

cd deployer/deployer
cargo test
Enter fullscreen mode Exit fullscreen mode

If the tests are successful, you should see an output similar to:

running 1 test
test test::test ... ok
Enter fullscreen mode Exit fullscreen mode

Building The Contract

We need to build both Deployer contract and Contract contract, to build Deployer contract use the following command:

cd .../deployer/deployer
cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode

To build Contract contract use the following command:

cd .../deployer/contract
cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode

Both .wasm files should be found in the ../target directory:

../target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm
../target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm
Enter fullscreen mode Exit fullscreen mode

Initializing The Contract contract

Before a contract can be deployed from its WASM code, the WASM must be installed on the blockchain. The WASM installation happens outside of the deployer contract, and only needs to occur once.
After the WASM is installed, the deployer contract can deploy any number of instances of the contract from that single installed WASM. To initialize our Contract contract use the following command :

soroban contract install --wasm target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm
Enter fullscreen mode Exit fullscreen mode

Returning our WASM hash, in my case :

77c33183640b62da4a65c453047d29c846b6aace87ee64a8f532623388b6b06e
Enter fullscreen mode Exit fullscreen mode

Invoking The Contracts

We will use our WASM hash value from previous step,passing it to deploy function from Deployer contract. The command will be :

soroban contract invoke \
    --wasm target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm \
    --id 1 \
    -- \
    deploy \
    --salt 0000000000000000000000000000000000000000000000000000000000000000 \
    --wasm_hash 77c33183640b62da4a65c453047d29c846b6aace87ee64a8f532623388b6b06e\
    --init_fn init \
    --init_args '[{"u32":10}]'
Enter fullscreen mode Exit fullscreen mode

Above command will return contract id of our deployed Contract contract, in my case :

["ef370e4a3694cde1525f48d7c7666dc71da119aee5b446b0cbd1a274069ab8c8",null]
Enter fullscreen mode Exit fullscreen mode

Then we will invoke our Contract contract using this following command :

soroban contract invoke \
    --id ef370e4a3694cde1525f48d7c7666dc71da119aee5b446b0cbd1a274069ab8c8\
    --fn value
Enter fullscreen mode Exit fullscreen mode

You should see the following output:

10
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it! We explored the deployer contract in Soroban. The key point is the WASM hash for the deployed contract need to installed once before deploying it using the Deployer contract. Stay tuned for more post in this "Soroban Contracts 101" Series where we will dive deeper into Soroban Contracts and their functionalities.

Top comments (0)