Skip to main content

Auth

The auth example demonstrates how to tell who has invoked a contract, and verify that a contract has been invoked by an account or contract. This example is an extension of the storing data example.

Open in Gitpod

The participant who invoked a contract is an Address containing one-of a:

  • Account ID
  • Contract ID

In this example, data is stored associated with an Address after authorization has been verified.

info

For contracts that would benefit from supporting auth with presigned invocations, or ed25519 keys independent of accounts and contracts, see the advanced auth example.

Run the Example

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

git clone -b v0.2.1 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 auth directory, and use cargo test.

cd auth
cargo test

You should see the output:

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

Code

auth/src/lib.rs
#[contracttype]
pub enum DataKey {
Counter(Address),
}

pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
/// Increment increments a counter for the invoker, and returns the value.
pub fn increment(env: Env) -> u32 {
// Construct a key for the data being stored. Use an enum to set the
// contract up well for adding other types of data to be stored.
let invoker = env.invoker();
let key = DataKey::Counter(invoker);

// Get the current count for the invoker.
let mut count: u32 = env
.data()
.get(&key)
.unwrap_or(Ok(0)) // If no value set, assume 0.
.unwrap(); // Panic if the value of COUNTER is not u32.

// Increment the count.
count += 1;

// Save the count.
env.data().set(&key, count);

// Return the count to the caller.
count
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v0.2.1/auth

How it Works

The example contract tracks a counter for each invoker.

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

Invoker

The invoker is an Address of the account or contract that directly invoked the contract function.

let invoker = env.invoker();

The env.invoker() always returns the invoker of the currently executing contract. It will return either:

  • Account with an AccountId if the contract was invoked directly by an account.
  • Contract with a BytesN<32> contract ID if the contract was invoked by another contract.

Contracts should not need to unpack the Address for most use cases. Contracts should treat the value as opaque wherever possible so that they are interoperable with accounts and contracts.

The Address value can be used as a key in storage to store data for a user. The example contract only reads and writes data for the authenticated invoker, and so the only thing that can access or change the data for an invoker, is the invoker themselves.

tip

Enum keys like DataKey can be a helpful way to store a variety of different types of data keyed against the same or similar things. In the example the counter for each address is stored against DataKey::Counter(Address). If the contract needs to start storing other types of data for addresses, it can do so by adding additional variants to the enum.

#[contracttype]
pub enum DataKey {
Counter(Address),
}
let invoker = env.invoker();
let key = DataKey::Counter(invoker);

Tests

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

auth/src/test.rs
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);

let user_1 = env.accounts().generate();
let user_2 = env.accounts().generate();

assert_eq!(client.with_source_account(&user_1).increment(), 1);
assert_eq!(client.with_source_account(&user_1).increment(), 2);
assert_eq!(client.with_source_account(&user_2).increment(), 1);
assert_eq!(client.with_source_account(&user_1).increment(), 3);
}

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();

The contract is registered with the environment using the contract type.

let contract_id = env.register_contract(None, IncrementContract);

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 IncrementContract, and the client is named IncrementContractClient.

let client = IncrementContractClient::new(&env, &contract_id);

The contract generates account IDs for two users.

The generate function creates a new account ID that the test can use to represent an account interacting with the contract.

let _ = env.accounts().generate();

The increment function is invoked twice with user_1 configured as the source account and therefore the invoker. The results are asserted to be 1 and 2 because it is the first and second increments for the user.

assert_eq!(client.with_source_account(&user_1).increment(), 1);
assert_eq!(client.with_source_account(&user_1).increment(), 2);

The increment function is invoked once with user_2 configured as the source account. The results are asserted to be 1 because it is the first increment for the user. The first users data is separate to that of the second users. The users have no access to each others data.

assert_eq!(client.with_source_account(&user_2).increment(), 1);
tip

Functions invoked in tests can use with_source_account(account_id) to simulate an invocation from an account, and the invoked function will see env.invoker() as Address::Account(account_id).

Build the Contract

To build the contract into a .wasm file, use the cargo build command.

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

The .wasm file should be found in the ../target directory after building:

target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm

Run the Contract

If you have soroban-cli installed, you can invoke functions on the contract.

soroban invoke \
--wasm ../target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
--account GDQHNBKFCO666SPX4RS62VTDY7H5W2QXHVVVQCDTADTOI3IYZGEOZL6V \
--fn increment
soroban invoke \
--wasm ../target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
--account GC24I42QMKKR4NE6IYNPCQHUO4PXWXDGNZ7QVMMSR5EWAYSGKBHPLGHH \
--fn increment

Run these commands several times to increment the counters for each account.

View the data that has been stored against each user with soroban read.

soroban read --id 1
"[""Counter"",[""Account"",""GC24I42QMKKR4NE6IYNPCQHUO4PXWXDGNZ7QVMMSR5EWAYSGKBHPLGHH""]]",1
"[""Counter"",[""Account"",""GDQHNBKFCO666SPX4RS62VTDY7H5W2QXHVVVQCDTADTOI3IYZGEOZL6V""]]",3