DEV Community

yuzurush
yuzurush

Posted on • Updated on

Soroban Quest : Asset Interop

Hi there! Welcome to my "Soroban Quest" blog post series. Soroban Quest is a gamified educational course where you’ll learn Soroban (new smart contract platform by Stellar Foundation) and earn badge rewards!. In this series, i will explain steps to complete every soroban quest and help you understand more about soroban smart contract itself.

The 6th quest is called 'Asset Interop'. This quest will provide an example of how asset interoperability works in Soroban.

Joining The Quest

To join the quest and get a Quest Account, use this command:

sq play 6
Enter fullscreen mode Exit fullscreen mode

And dont forget to fund the quest account right away.

Examining README.md

After examining README.md we will need 2 account for this quest, our quest account act as Parent_Account and the other account act as Child_Account. To create account use Stellar Laboratory, and don't forget to fund it. Also the asset that we're going to use is native token(XLM/lumens).

The tasks for the 6th quest is :

  • Deploy the AllowanceContract using Parent_Account
  • Invoke the init function of the AllowanceContract
  • Invoke the incr_allow function of the native token contract to allow your AllowanceContract to make proxy transfers from the Parent_Account to the Child_Account
  • Invoke the withdraw function of the AllowanceContract using either the Child_Account or Parent_Account

Native Token Contract ID : d93f5c7bb0ebc4a9c8f727c5cebc4e41194d38257e1d0d910356b43bfc528813

The Contract Code

#![no_std]

use soroban_sdk::{contracterror, contractimpl, contracttype, Address, BytesN, Env};


mod token {
    soroban_sdk::contractimport!(file = "./soroban_token_spec.wasm");
}


#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
    ContractAlreadyInitialized = 1,
    ContractNotInitialized = 2,
    InvalidAuth = 3,
    ChildAlreadyWithdrawn = 4,
    InvalidInvoker = 5,
    InvalidArguments = 6,
}


#[contracttype]
#[derive(Clone)]
pub enum StorageKey {
    Parent,  // Address
    Child,   // Address
    TokenId, // BytesN<32>
    Amount,  // i128
    Step,    // u64
    Latest,  // u64
}


const SECONDS_IN_YEAR: u64 = 365 * 24 * 60 * 60; // = 31,536,000 seconds (fyi)

pub struct AllowanceContract;


pub trait AllowanceTrait {

    fn init(
        e: Env,
        parent: Address,      // the parent account giving the allowance
        child: Address,       // the child account receiving the allowance
        token_id: BytesN<32>, // the id of the token being transferred as an allowance
        amount: i128,         // the total allowance amount given for the year
        step: u64,            // how frequently (in seconds) a withdrawal can be made
    ) -> Result<(), Error>;

    fn withdraw(e: Env, invoker: Address) -> Result<(), Error>;
}

#[contractimpl]
impl AllowanceTrait for AllowanceContract {

    fn init(
        e: Env,
        parent: Address,
        child: Address,
        token_id: BytesN<32>,
        amount: i128,
        step: u64,
    ) -> Result<(), Error> {

        let token_key = StorageKey::TokenId;
        if e.storage().has(&token_key) {
            return Err(Error::ContractAlreadyInitialized);
        }

        parent.require_auth();


        if step == 0 {
            return Err(Error::InvalidArguments);
        }


        if (amount * step as i128) / SECONDS_IN_YEAR as i128 == 0 {
            return Err(Error::InvalidArguments);
        }


        e.storage().set(&token_key, &token_id);
        e.storage().set(&StorageKey::Parent, &parent);
        e.storage().set(&StorageKey::Child, &child);
        e.storage().set(&StorageKey::Amount, &amount);
        e.storage().set(&StorageKey::Step, &step);

        let current_ts = e.ledger().timestamp();
        e.storage().set(&StorageKey::Latest, &(current_ts - step));

        Ok(())
    }

    fn withdraw(e: Env, invoker: Address) -> Result<(), Error> {

        let key = StorageKey::TokenId;
        if !e.storage().has(&key) {
            return Err(Error::ContractNotInitialized);
        }

        let child: Address = e.storage().get(&StorageKey::Child).unwrap().unwrap();
        let parent: Address = e.storage().get(&StorageKey::Parent).unwrap().unwrap();

        if invoker != child && invoker != parent {
            return Err(Error::InvalidAuth);
        }

        invoker.require_auth();


        let token_id: BytesN<32> = e.storage().get(&key).unwrap().unwrap();
        let client = token::Client::new(&e, &token_id);

        let step: u64 = e.storage().get(&StorageKey::Step).unwrap().unwrap();
        let iterations = SECONDS_IN_YEAR / step;
        let amount: i128 = e.storage().get(&StorageKey::Amount).unwrap().unwrap();
        let withdraw_amount = amount / iterations as i128;


        let latest: u64 = e.storage().get(&StorageKey::Latest).unwrap().unwrap();
        if latest + step > e.ledger().timestamp() {
            return Err(Error::ChildAlreadyWithdrawn);
        }


        client.xfer_from(
            &e.current_contract_address(),
            &parent,
            &child,
            &withdraw_amount,
        );


        let new_latest = latest + step;
        e.storage().set(&StorageKey::Latest, &new_latest);

        Ok(())
    }
}

mod test;

Enter fullscreen mode Exit fullscreen mode

The Allowance contract allows a "parent" account to set up an allowance for a "child" account. The parent specifies:

  • The token being transferred (token ID)
  • The total amount allowed for the year
  • How frequently withdrawals can be made (in seconds)

The child can then withdraw their allowance in increments, as long as it's been at least the specified number of seconds since their last withdrawal.

The contract is located in lib.rs and contains two functions:

  • init() - Initialized the allowance details
  • withdraw() - Transfers funds from the parent to the child

The init() function:

  • Accepts the parent address, child address, token ID, total amount, and withdrawal frequency (step)
  • Checks that the contract has not already been initialized
  • Requires the parent address to authorize the initialization
  • Checks that the step and total amount are valid (not 0 and would result in a non-zero annual amount)
  • Stores the allowance details by calling env.storage().set()
  • Returns Ok if successful This function sets up the initial allowance details and ensures the inputs are valid.

The withdraw() function:

  • Accepts the invoker address (who is calling the function) and contract environment
  • Checks that the contract has been initialized
  • Retrieves the parent, child, and withdrawal details from storage
  • Checks that the invoker is the parent or child
  • Calculates the withdrawal amount and checks that the last withdrawal was at least step seconds ago
  • Creates a token contract client and transfers the funds from the parent to the child
  • Updates the last withdrawal time
  • Returns Ok if successful

Building The Contract

To build the contract, use the following command:

cd quests/6-asset-interop
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_asset_interop_contract.wasm
Enter fullscreen mode Exit fullscreen mode

Deploying The Contract as Parent_Account

To deploy AllowanceContract contract, use this following command :

soroban contract deploy --wasm /workspace/soroban-quest/quests/4-cross-contract/soroban_asset_interop_contract.wasm
Enter fullscreen mode Exit fullscreen mode

Save the contract ID for later.

Initializing AllowanceContract

To invoke init function from AllowanceContract, use this command format:

soroban contract invoke --id <YourContracID> -- init --parent <ParentPublicKey> --child <ChildPublicKey> --token_id 'd93f5c7bb0ebc4a9c8f727c5cebc4e41194d38257e1d0d910356b43bfc528813'  --amount <LumensAmount> --step <WithdrwarRateInSeconds>
Enter fullscreen mode Exit fullscreen mode

With the AllowanceContract already initialized, the Parent_Account needs to authorize the contract to make proxy transfers on its behalf to the Child_Account.

Allowing AllowanceContract

To authorize the contract to make proxy transfers, use this command format :

soroban contract invoke --id d93f5c7bb0ebc4a9c8f727c5cebc4e41194d38257e1d0d910356b43bfc528813 -- incr_allow --from <ParentPublicKey> --spender '{"object":{"address":{"contract":"'<AllowanceContractID>'"}}}' --amount <LumensAmount>
Enter fullscreen mode Exit fullscreen mode

Allowance

Withdrawing the Allowance

To withdraw the Allowance, use this command format :

soroban contract invoke --id <AllowanceContractID> -- withdraw --invoker <ParentorChildPublicKey>
Enter fullscreen mode Exit fullscreen mode

Withdraw

Checking the Quest

We already completed every step to complete the quest and this is the last thing you need to do. Check your quest and claim your badge reward. To check use the following command :

sq check 6
Enter fullscreen mode Exit fullscreen mode

Congratulations on completing all 6 Soroban quests! You've come a long way, and should feel very accomplished. Keep up the great work!

Top comments (0)