#Basic deposit and withdrawal

72 messages · Page 1 of 1 (latest)

tiny bluff
#

I can't seem to find a proper working example as to how to make a function in a smart contract to deposit xlm to contract and withdraw it. I found one old example, that uses soroban-auth cargo module, but it seems to be deprecated and cannot be installed. Where should I look?

teal hollow
# tiny bluff I can't seem to find a proper working example as to how to make a function in a ...

It's not exactly that, but you can rather easily modify the liquidity pool contract example to work as you say. It might even be better this way for learning as you'll need to think which parts are not needed for simple deposit/withdrawal functionality 🙂 Here's the example: https://developers.stellar.org/docs/build/smart-contracts/example-contracts/liquidity-pool

Write a constant-product liquidity pool smart contract.

#

Maybe try it out and ask here again if you run in to any problems

tiny bluff
#

Thanks a bunch! Be right back when I try it out!

covert roverBOT
pale blaze
#

The custom account example

tiny bluff
pale blaze
#

Let me check it might not actually have deposit and withdraw now that I am thinking about it tho

#

They are in here

#

I mean it's pretty simple tho because there's a token standard so you just need to implement the transfer calls and require auth

tiny bluff
#

Yeah, I know this repo, it just doesn't have "custom account" folder

#

Simple account?

pale blaze
#

Seems to have been pruned there use to be more examples

#

There's an example called account though

tiny bluff
#

See that, it has authentication stuff

pale blaze
#

Yes well generally auth is the more difficult part. Deposit and withdraw would simply be like

User.require_auth()
Then a token.transfer to the contract for deposit. Keep a data key that can track balances. And for withdraw you require auth of the contract and transfer to the user and update the balance data.

#

I have a guide I started with a deposit and withdraw example but it's not finished and there might be some errors in it

#

But I can link you that it might help as it has some code examples

#

The focus of the guide is also auth tho

#

Like I said that guide isn't final so I can't guarantee there's no errors yet

#

But if u ignore all the stuff about auth and multisig you will see there's a deposit and withdraw function

tiny bluff
#

Thanks! Will give it a shot!

pale blaze
# tiny bluff Thanks! Will give it a shot!

I used my soroban trained gpt along with some prompts I wrote to get a simplified example. I think this might help.

use soroban_sdk::{
    contract, contractimpl, symbol_short, token::Client as TokenClient, Address, BytesN, Env, Map,
    Symbol, TryIntoVal, Vec,
};

#[contract]
pub struct SimpleAccountContract;

#[derive(Clone)]
#[contracttype]
pub struct BalanceInfo {
    pub token: Address,
    pub amount: i128,
}

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
    Balance(BytesN<32>),
}

#[contractimpl]
impl SimpleAccountContract {
    pub fn deposit(env: Env, from: Address, token: Address, amount: i128) -> Result<BalanceInfo, ()> {
        from.require_auth();

        // Transfer token from `from` to this contract.
        TokenClient::new(&env, &token).transfer(&from, &env.current_contract_address(), &amount);

        // Create a unique identifier for this balance entry.
        let balance_id = env.crypto().sha256(&(from.clone(), token.clone()).try_into_val(&env));

        // Retrieve or initialize balance information.
        let mut balance: BalanceInfo = env.storage().instance().get(&DataKey::Balance(balance_id.clone()))
            .unwrap_or(BalanceInfo {
                token: token.clone(),
                amount: 0,
            });

        // Update the balance.
        balance.amount += amount;

        // Store the updated balance.
        env.storage().instance().set(&DataKey::Balance(balance_id.clone()), &balance);

        Ok(balance)
    }

    pub fn withdraw(env: Env, balance_id: BytesN<32>, to: Address, amount: i128) -> Result<BalanceInfo, ()> {
        // Require the current contract to authorize the withdrawal.
        env.current_contract_address().require_auth();

        // Retrieve balance information.
        let mut balance: BalanceInfo = env.storage().instance().get(&DataKey::Balance(balance_id.clone()))
            .ok_or(())?;

        // Check if the balance is sufficient.
        if amount > balance.amount {
            return Err(());
        }

        // Update the balance.
        balance.amount -= amount;
        env.storage().instance().set(&DataKey::Balance(balance_id.clone()), &balance);

        // Transfer the token to the `to` address.
        TokenClient::new(&env, &balance.token).transfer(&env.current_contract_address(), &to, &amount);

        Ok(balance)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use soroban_sdk::{testutils::{Address as _, AuthorizedInvocation}, token, Address, Env};

    #[test]
    fn test_deposit_withdraw() {
        let env = Env::default();
        let contract_id = env.register_contract(None, SimpleAccountContract);
        let client = SimpleAccountContractClient::new(&env, &contract_id);

        let user = Address::random(&env);
        let token = token::Client::new(&env, &Address::random(&env));
        token.mint(&user, &1000);

        let balance = client.deposit(&user, &token.address, &1000).unwrap();
        assert_eq!(balance.amount, 1000);

        let balance = client.withdraw(&balance_id, &user, &500).unwrap();
        assert_eq!(balance.amount, 500);
    }
}```
#

This would allow multiple users to deposit and withdraw balances to a contract. I'd suggest probably using a factory and deploy it as a custom account contract per user tho

#

Also I doubt that code will run exactly as it sits, it's intent is as more of a guide to get you started. Thanks

tiny bluff
#

I tried the code from your guide and have a small error. Though this bit of code has the same line too.

.sha256(&(from.clone(), token.clone()).try_into_val(&env));
expected &Bytes, found &Result<_, _>

pale blaze
#

Thank you! Let me look I must be using the method wrong

#

That's the balance Id. We need to unwrap the result

#

One sec

#

Should be able to do something like this

#
        // Create a unique identifier for this balance entry.
        let balance_id_data = (from.clone(), token.clone()).into_val(&env);
        let balance_id = env.crypto().sha256(&balance_id_data);

        // Retrieve or initialize balance information.
        let mut balance: BalanceInfo = env.storage().instance().get(&DataKey::Balance(balance_id.clone()))
            .unwrap_or(BalanceInfo {
                token: token.clone(),
                amount: 0,
            });

I'm on my mobile right now so I can't test but if u get another error lmk thank you

#

That would be in the deposit function

#

In withdraw it wouldn't need to initialize it

#

Oh withdraw takes in the balanceid so

#

Also import IntoVal from the soroban sdk

tiny bluff
#

Now it's this one (I imported IntoVal)

let balance_id_data = (from.clone(), token.clone()).into_val(&env);
| -------- ^^^^ the trait TryFromVal<Env, (soroban_sdk::Address, soroban_sdk::Address)> is not implemented for soroban_sdk::Bytes, which is required by (soroban_sdk::Address, soroban_sdk::Address): IntoVal<_, _>

pale blaze
#

Hmm that one is a bit more confusing I need to look when I'm at my computer

#

You could simply try using an incremental balance id instead of the hash

#

To get it working for testing faster

#

It just needs to be a unique identifier for each user's balance

tiny bluff
#

Sounds good for now, I'm away now, will try as soon as I'm back

pale blaze
#

Yeah i'm gonna have to actually write it i think to get that to build

#

it has other errors too

novel maple
pale blaze
#

I think i just got confused bc i didn't look first lol

#

yeah timelock woulda been much more relevant in hindsight

novel maple
#

probably we should try to add tags to the examples, so that's it's easy to demonstrate different principles (token transfers, storage, auth etc.)

#

I'll add this to our backlog

#

(to be clear, I'm not being critical here, just trying to understand the gaps in docs organization)

pale blaze
#

to be clear tho

#

you can deposit any tokens to a contract without the actual method, you just should track who the balance belongs to so

#

so i do wonder, if somehow maybe this is done a lot differently in EVM or other chains since it seems like a point of confusion

novel maple
#

well, depends on the entry point. in question here the deposit from contract to a contract is in question, so for the sender there needs to be a token contract interaction.
I agree that we maybe need a 'how to' recipe on token transfers

#

I've added both ideas to the backlog

pale blaze
tiny bluff
novel maple
#

XLM token has an address as well, as any other token

#

but I don't recall where to get it off the top of my head

toxic mauve
#

XLM address can be gotten by running the command:

soroban lab token id --asset native --source-account <SOURCE_ACCOUNT> --network <NETWORK>

#

These are the respective addresses:

Xlm Public Network: CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA

Xlm testnet: CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
Futurenet: CB64D3G7SM2RTH6JSGG34DDTFTQ5CFDKVDZJZSODMCX4NJ2HV2KN7OHT

pale blaze
#

Good job

tiny bluff
#

Thanks for the addresses!
I knew that it must have addresses, but couldn't find them in public access, so I thought there must be another way.
I will try it out when I get home!

tiny bluff
#

Ok, deposit done!
Now I have a withdraw problem:
Missing signing key for account <My contract address>

I get that current_contract_address().require_auth means that this contract authorization is required, but what kind of sign keys is it asking for? When I invoke I have my secret key (from which I was deploying that contract) in the source field. Do I need something else ?

novel maple
#

contracts don't sign anything (there are custom accounts, which are contracts, but they also verify the externally provided signatures). you can just do token_client.transfer(env.current_contract_address(), to, ...)

#

env.current_contract_address().require_auth() is a very special use case that requires current contract to have authorized a call on the current contract, which may only work inside custom accounts. that's a pretty advanced topic and is not really relevant for anything but custom accounts.

pale blaze
#

Yeah that is like authorizing in the sense of the check_auth i don't think u need to worry about it at this stage, that's why i said ignore most of that tutorial i posted lol