Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token supply #302

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion token/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::admin::{has_administrator, read_administrator, write_administrator};
use crate::allowance::{read_allowance, spend_allowance, write_allowance};
use crate::balance::{read_balance, receive_balance, spend_balance};
use crate::supply::{has_supply, read_supply, write_supply, decrement_supply, increment_supply};
use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata};
#[cfg(test)]
use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey};
Expand All @@ -23,7 +24,7 @@ pub struct Token;

#[contractimpl]
impl Token {
pub fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String) {
pub fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String, supply: i128) {
if has_administrator(&e) {
panic!("already initialized")
}
Expand All @@ -32,6 +33,10 @@ impl Token {
panic!("Decimal must fit in a u8");
}

if supply > 0 {
write_supply(&e, &supply);
}

write_metadata(
&e,
TokenMetadata {
Expand All @@ -47,11 +52,18 @@ impl Token {
let admin = read_administrator(&e);
admin.require_auth();

if has_supply(&e) && amount > read_supply(&e) {
panic!("Amount greater than remaining supply");
}

e.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

receive_balance(&e, to.clone(), amount);
if has_supply(&e) {
decrement_supply(&e, &amount);
}
TokenUtils::new(&e).events().mint(admin, to, amount);
}

Expand All @@ -73,6 +85,18 @@ impl Token {
let allowance = e.storage().temporary().get::<_, AllowanceValue>(&key);
allowance
}

pub fn supply(e: Env) -> i128 {
if !has_supply(&e) {
return 0;
}

e.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

read_supply(&e)
}
}

#[contractimpl]
Expand Down Expand Up @@ -145,6 +169,10 @@ impl token::Interface for Token {
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

spend_balance(&e, from.clone(), amount);
if has_supply(&e) {
increment_supply(&e, &amount);
}

TokenUtils::new(&e).events().burn(from, amount);
}

Expand All @@ -159,6 +187,10 @@ impl token::Interface for Token {

spend_allowance(&e, from.clone(), spender, amount);
spend_balance(&e, from.clone(), amount);
if has_supply(&e) {
increment_supply(&e, &amount);
}

TokenUtils::new(&e).events().burn(from, amount)
}

Expand Down
1 change: 1 addition & 0 deletions token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod balance;
mod contract;
mod metadata;
mod storage_types;
mod supply;
mod test;

pub use crate::contract::TokenClient;
1 change: 1 addition & 0 deletions token/src/storage_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ pub enum DataKey {
Nonce(Address),
State(Address),
Admin,
Supply
}
33 changes: 33 additions & 0 deletions token/src/supply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use soroban_sdk::{Env};
use crate::storage_types::DataKey;

pub fn has_supply(e: &Env) -> bool {
let key = DataKey::Supply;
e.storage().instance().has(&key)
}

pub fn read_supply(e: &Env) -> i128 {
let key = DataKey::Supply;
e.storage().instance().get(&key).unwrap()
}

pub fn write_supply(e: &Env, amount: &i128) {
let key = DataKey::Supply;
e.storage().instance().set(&key, amount);
}
Comment on lines +4 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supply might be necessary for some tokens, but is probably not ideal to include on most tokens on Soroban.

Soroban is designed to execute invocations concurrently in the future. This is why transactions have a footprint as the environment uses the footprint to figure out which contracts needed to execute serially and which can execute concurrently. Invocations need to occur serially if they write to the same data. An example of this is when account A makes a payment to account B, and account C also makes a payment to account B in the same ledger. Both payments will be processed serially because they both write to account B's balance data entry. If an operation requires writing to a single/global/instance data entry, every invocation using that operation will be serialised and won't be able to executed concurrently. The concurrent part of Soroban isn't implemented yet, but we can imagine that when it is transactions that must be executed serially may be more expensive as there will be less capacity to execute a lot of serial transactions as there are to execute parallel.

For this reason I'm not sure we should add the supply to the token example. We've seen plenty of token implementations start by forking the example, and if they forked the example with supply implemented then every token would carry this serialisation limitation without necessarily needing it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add another example that is a token example with supply? But with disclaimers as to the cost / impact.

Copy link
Author

@icolomina icolomina Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand. I was thinking in those situations where supply is required but i did not think about that cost / impact you've commented. I can send another pull request to the soroban examples with a new folder holding the token with supply example. This way it wouldn't affect to the existing one.
What do you think ?
Thanks in advance

Copy link
Member

@leighmcculloch leighmcculloch Apr 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomerweller @kalepail Is supply a common feature that you see folks using in other ecosystems, or needing that we should have an example for? We're definitely avoiding it for tokens that need to scale because a global value like supply will prevent ops that write to the supply value to be parallelised, but I recognise that shouldn't constrain everything.


pub fn decrement_supply(e: &Env, amount: &i128) {
let key = DataKey::Supply;
let current_supply: i128 = e.storage().instance().get(&key).unwrap();
let new_supply = current_supply - amount;

e.storage().instance().set(&key, &new_supply);
}

pub fn increment_supply(e: &Env, amount: &i128) {
let key = DataKey::Supply;
let current_supply: i128 = e.storage().instance().get(&key).unwrap();
let new_supply = current_supply + amount;

e.storage().instance().set(&key, &new_supply);
}
78 changes: 69 additions & 9 deletions token/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use soroban_sdk::{
Address, Env, IntoVal, Symbol,
};

fn create_token<'a>(e: &Env, admin: &Address) -> TokenClient<'a> {
fn create_token<'a>(e: &Env, admin: &Address, supply: &i128) -> TokenClient<'a> {
let token = TokenClient::new(e, &e.register_contract(None, Token {}));
token.initialize(admin, &7, &"name".into_val(e), &"symbol".into_val(e));
token.initialize(admin, &7, &"name".into_val(e), &"symbol".into_val(e), &supply);
token
}

Expand All @@ -24,7 +24,7 @@ fn test() {
let user1 = Address::generate(&e);
let user2 = Address::generate(&e);
let user3 = Address::generate(&e);
let token = create_token(&e, &admin1);
let token = create_token(&e, &admin1, &0);

token.mint(&user1, &1000);
assert_eq!(
Expand Down Expand Up @@ -145,7 +145,7 @@ fn test_burn() {
let admin = Address::generate(&e);
let user1 = Address::generate(&e);
let user2 = Address::generate(&e);
let token = create_token(&e, &admin);
let token = create_token(&e, &admin, &0);

token.mint(&user1, &1000);
assert_eq!(token.balance(&user1), 1000);
Expand Down Expand Up @@ -202,7 +202,7 @@ fn transfer_insufficient_balance() {
let admin = Address::generate(&e);
let user1 = Address::generate(&e);
let user2 = Address::generate(&e);
let token = create_token(&e, &admin);
let token = create_token(&e, &admin, &0);

token.mint(&user1, &1000);
assert_eq!(token.balance(&user1), 1000);
Expand All @@ -220,7 +220,7 @@ fn transfer_from_insufficient_allowance() {
let user1 = Address::generate(&e);
let user2 = Address::generate(&e);
let user3 = Address::generate(&e);
let token = create_token(&e, &admin);
let token = create_token(&e, &admin, &0);

token.mint(&user1, &1000);
assert_eq!(token.balance(&user1), 1000);
Expand All @@ -236,9 +236,9 @@ fn transfer_from_insufficient_allowance() {
fn initialize_already_initialized() {
let e = Env::default();
let admin = Address::generate(&e);
let token = create_token(&e, &admin);
let token = create_token(&e, &admin, &0);

token.initialize(&admin, &10, &"name".into_val(&e), &"symbol".into_val(&e));
token.initialize(&admin, &10, &"name".into_val(&e), &"symbol".into_val(&e), &0);
}

#[test]
Expand All @@ -252,6 +252,7 @@ fn decimal_is_over_max() {
&(u32::from(u8::MAX) + 1),
&"name".into_val(&e),
&"symbol".into_val(&e),
&0
);
}

Expand All @@ -264,8 +265,67 @@ fn test_zero_allowance() {
let admin = Address::generate(&e);
let spender = Address::generate(&e);
let from = Address::generate(&e);
let token = create_token(&e, &admin);
let token = create_token(&e, &admin, &0);

token.transfer_from(&spender, &from, &spender, &0);
assert!(token.get_allowance(&from, &spender).is_none());
}

#[test]
fn test_no_supply() {
let e = Env::default();
e.mock_all_auths();

let admin = Address::generate(&e);
let token = create_token(&e, &admin, &0);
assert_eq!(token.supply(), 0_i128);
}

#[test]
fn test_supply() {
let e = Env::default();
e.mock_all_auths();

let admin = Address::generate(&e);
let address = Address::generate(&e);
let token = create_token(&e, &admin, &100);

token.mint(&address, &50);
assert_eq!(token.supply(), 50_i128);

}

#[test]
#[should_panic(expected = "Amount greater than remaining supply")]
fn test_mint_greater_than_supply() {
let e = Env::default();
e.mock_all_auths();

let admin = Address::generate(&e);
let address = Address::generate(&e);
let token = create_token(&e, &admin, &100);

token.mint(&address, &101);
}

#[test]
fn test_burn_with_supply() {
let e = Env::default();
e.mock_all_auths();

let admin = Address::generate(&e);
let address1 = Address::generate(&e);
let address2 = Address::generate(&e);
let address3 = Address::generate(&e);

let token = create_token(&e, &admin, &100);

token.mint(&address1, &10);
token.mint(&address2, &20);
token.mint(&address3, &30);

assert_eq!(token.supply(), 40_i128);

token.burn(&address3, &25);
assert_eq!(token.supply(), 65_i128);
}