Soroban Contracts 101 : Auth

yuzurush

yuzurush

Posted on March 17, 2023

Soroban Contracts 101 : Auth

Hi there! Welcome to my seventh 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 seventh post of the series, I'll be covering soroban contract auth funcionalities. Authentication (auth) in smart contracts refers to restricting access to contract methods (functions) to only authorized callers. In Soroban, contract methods can require authentication (auth) to only allow authorized callers to execute them. This is done using the require_auth and require_auth_for_args methods. For example user.require_auth() would require authentication by a user address to continue execution. Only a caller that has been authorized as that user would be able to proceed past this point.require_auth_for_args allows also restricting based on function arguments, for more fine-grained control.

The Contract Code

#[contracttype]
pub enum DataKey {
    Counter(Address),
}

pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
    /// Increment increments a counter for the user, and returns the value.
    pub fn increment(env: Env, user: Address, value: u32) -> u32 {

        user.require_auth();

        let key = DataKey::Counter(user.clone());

        let mut count: u32 = env
            .storage()
            .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 += value;

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

        // Return the count to the caller.
        count
    }
}
Enter fullscreen mode Exit fullscreen mode

This contract code implements an increment contract that requires authentication to use. The key points of the contract :

  • It uses the require_auth and require_auth_for_args methods to enforce that only authorized callers can execute the increment function
  • It stores counter values under a key derived from the user's address (DataKey::Counter(user.clone())), so each user has their own counter
  • It gets, increments, and stores the counter for a user
  • It returns the new counter value

So this contract demonstrates using Soroban's authentication functionality to restrict access to contract function. Only authorized users (in this case, a single predefined user) can call the increment method and manage their own counter.

The Test Code

#[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 = Address::random(&env);
    let user_2 = Address::random(&env);

    assert_eq!(client.increment(&user_1, &5), 5);
    // Verify that the user indeed had to authorize a call of `increment` with
    // the expected arguments:
    assert_eq!(
        env.recorded_top_authorizations(),
        std::vec![(
            // Address for which auth is performed
            user_1.clone(),
            // Identifier of the called contract
            contract_id.clone(),
            // Name of the called function
            symbol!("increment"),
            // Arguments used to call `increment` (converted to the env-managed vector via `into_val`)
            (user_1.clone(), 5_u32).into_val(&env)
        )]
    );

    // Do more `increment` calls. It's not necessary to verify authorizations
    // for every one of them as we don't expect the auth logic to change from
    // call to call.
    assert_eq!(client.increment(&user_1, &2), 7);
    assert_eq!(client.increment(&user_2, &1), 1);
    assert_eq!(client.increment(&user_1, &3), 10);
    assert_eq!(client.increment(&user_2, &4), 5);
}

Enter fullscreen mode Exit fullscreen mode

This test code is a unit test that thoroughly test the authentication logic of our increment contract. It:

  • Creates an Env and registers the contract
  • Creates a client to call the contract
  • Calls increment with two different users (randomly generated addresses)
  • Verifies the authorization record after the first call, checking that the expected user/function/args were authenticated
  • Makes additional calls without verifying the auth record each time (since the logic won't change)

So this test ensures that the contract's authentication works as expected - only allowing a given user to increment their own counter, and recording the correct authorization information.
Testing a contract's authentication (and other security) logic is important to ensure it will behave correctly and securely when deployed to a real blockchain.

Running Contract Tests

To ensure that the contract functions as intended, you can run the contract tests using the following command:

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

To build the contract, use the following command:

cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode

This should output a .wasm file in the ../target directory:

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

Invoking The Contract

We will do invocation using 2 different account. First Account :

$ soroban contract invoke \
>     --source GAAX4DJPNZJNWN3QOSBAJIFTT4XXPQ4B274MBEYODWW3ZMHLQK6NKKG4 \
>     --wasm ../target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
>     --id 1 \
>     -- \ 
>     increment \
>     --user GAAX4DJPNZJNWN3QOSBAJIFTT4XXPQ4B274MBEYODWW3ZMHLQK6NKKG4 \
>     --value 5
5
Enter fullscreen mode Exit fullscreen mode

Second account :

$ soroban contract invoke \
>     --source GC4E5NTKIV7PDFDNYFOLUHB32BXYURJOVNDFZ5X67XKQXVHXDLGZBOCU \
>     --wasm ../target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
>     --id 1 \
>     -- \ 
>     increment \
>     --user GC4E5NTKIV7PDFDNYFOLUHB32BXYURJOVNDFZ5X67XKQXVHXDLGZBOCU \
>     --value 10
10
Enter fullscreen mode Exit fullscreen mode

Then we will use soroban read to check the our contract current contract-data ledger entry value :

$ soroban contract read --id 1
"[""Counter"",""GAAX4DJPNZJNWN3QOSBAJIFTT4XXPQ4B274MBEYODWW3ZMHLQK6NKKG4""]",5
"[""Counter"",""GC4E5NTKIV7PDFDNYFOLUHB32BXYURJOVNDFZ5X67XKQXVHXDLGZBOCU""]",10
Enter fullscreen mode Exit fullscreen mode

Conclusion

We explored how to implement authentication (auth) in Soroban smart contracts. Auth is important to restrict sensitive contract methods to only authorized callers. Soroban provides the require_auth and require_auth_for_args methods to easily enforce that only designated addresses can execute certain methods. Stay tuned for more post in this "Soroban Contracts 101" Series where we will dive deeper into Soroban Contracts and their functionalities.

💖 💪 🙅 🚩
yuzurush
yuzurush

Posted on March 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related