Soroban Quest : Asset Interop
Tags: sorobanquest, soroban, stellar, smartcontract
Published: 2023-03-26 23:20:10

Hi there! Welcome to my "Soroban Quest" blog post series. [Soroban Quest](https://quest.stellar.org/soroban) 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:

```bash
sq play 6
```
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](https://laboratory.stellar.org/#account-creator?network=futurenet), 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 

```rust
#![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;

```
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:
```bash
cd quests/6-asset-interop
cargo build --target wasm32-unknown-unknown --release
```
This should output a .wasm file in the ../target directory:
```bash
../target/wasm32-unknown-unknown/release/soroban_asset_interop_contract.wasm
```

##Deploying The Contract as `Parent_Account`
To deploy `AllowanceContract` contract, use this following command :

```bash 
soroban contract deploy --wasm /workspace/soroban-quest/quests/4-cross-contract/soroban_asset_interop_contract.wasm
```
Save the contract ID for later.

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

```bash
soroban contract invoke --id <YourContracID> -- init --parent <ParentPublicKey> --child <ChildPublicKey> --token_id 'd93f5c7bb0ebc4a9c8f727c5cebc4e41194d38257e1d0d910356b43bfc528813'  --amount <LumensAmount> --step <WithdrwarRateInSeconds>
```
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 :

```bash
soroban contract invoke --id d93f5c7bb0ebc4a9c8f727c5cebc4e41194d38257e1d0d910356b43bfc528813 -- incr_allow --from <ParentPublicKey> --spender '{"object":{"address":{"contract":"'<AllowanceContractID>'"}}}' --amount <LumensAmount>
```

![Allowance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ccjcoqfl8c7n7qxnjvb6.png)

##Withdrawing the Allowance
To withdraw the Allowance, use this command format :

```bash
soroban contract invoke --id <AllowanceContractID> -- withdraw --invoker <ParentorChildPublicKey>
```

![Withdraw](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lm4pf3m7ma64k40pcmcx.png)

##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
```

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