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

refactor(interchain-token): use contractstorage #245

Merged
merged 5 commits into from
Feb 9, 2025
Merged
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
107 changes: 32 additions & 75 deletions contracts/stellar-interchain-token/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
use soroban_token_sdk::TokenUtils;
use stellar_axelar_std::events::Event;
use stellar_axelar_std::interfaces::{CustomMigratableInterface, OwnableInterface};
use stellar_axelar_std::ttl::{extend_instance_ttl, extend_persistent_ttl};
use stellar_axelar_std::ttl::extend_instance_ttl;
use stellar_axelar_std::{ensure, interfaces, Upgradable};

use crate::error::ContractError;
use crate::event::{MinterAddedEvent, MinterRemovedEvent};
use crate::interface::InterchainTokenInterface;
use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey};
use crate::storage::{self, AllowanceDataKey, AllowanceValue};

#[contract]
#[derive(Upgradable)]
Expand All @@ -30,12 +30,10 @@

Self::write_metadata(&env, token_metadata);

env.storage().instance().set(&DataKey::TokenId, &token_id);
storage::set_token_id(&env, &token_id);

if let Some(minter) = minter {
env.storage()
.instance()
.set(&DataKey::Minter(minter.clone()), &());
storage::set_minter_status(&env, minter.clone());

MinterAddedEvent { minter }.emit(&env);
}
Expand Down Expand Up @@ -96,14 +94,11 @@
#[contractimpl]
impl InterchainTokenInterface for InterchainToken {
fn token_id(env: &Env) -> BytesN<32> {
env.storage()
.instance()
.get(&DataKey::TokenId)
.expect("token id not found")
storage::token_id(env)
}

fn is_minter(env: &Env, minter: Address) -> bool {
env.storage().instance().has(&DataKey::Minter(minter))
storage::is_minter(env, minter)
}

fn mint_from(
Expand Down Expand Up @@ -133,23 +128,15 @@
fn add_minter(env: &Env, minter: Address) {
Self::owner(env).require_auth();

env.storage()
.instance()
.set(&DataKey::Minter(minter.clone()), &());

extend_instance_ttl(env);
storage::set_minter_status(env, minter.clone());

MinterAddedEvent { minter }.emit(env);
}

fn remove_minter(env: &Env, minter: Address) {
Self::owner(env).require_auth();

env.storage()
.instance()
.remove(&DataKey::Minter(minter.clone()));

extend_instance_ttl(env);
storage::remove_minter_status(env, minter.clone());

MinterRemovedEvent { minter }.emit(env);
}
Expand Down Expand Up @@ -183,8 +170,7 @@
}

fn balance(env: Env, id: Address) -> i128 {
extend_instance_ttl(&env);
Self::read_balance(&env, id)
storage::try_balance(&env, id).unwrap_or_default()
}

fn transfer(env: Env, from: Address, to: Address, amount: i128) {
Expand Down Expand Up @@ -258,26 +244,23 @@
}

fn read_allowance(env: &Env, from: Address, spender: Address) -> AllowanceValue {
let key = DataKey::Allowance(AllowanceDataKey { from, spender });
env.storage()
.temporary()
.get::<_, AllowanceValue>(&key)
.map_or(
AllowanceValue {
amount: 0,
expiration_ledger: 0,
},
|allowance| {
if allowance.expiration_ledger < env.ledger().sequence() {
AllowanceValue {
amount: 0,
expiration_ledger: allowance.expiration_ledger,
}
} else {
allowance
let key = AllowanceDataKey { from, spender };
storage::try_allowance(env, key).map_or(
AllowanceValue {
amount: 0,
expiration_ledger: 0,
},
|allowance| {
if allowance.expiration_ledger < env.ledger().sequence() {
AllowanceValue {
amount: 0,
expiration_ledger: allowance.expiration_ledger,

Check warning on line 257 in contracts/stellar-interchain-token/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/stellar-interchain-token/src/contract.rs#L255-L257

Added lines #L255 - L257 were not covered by tests
}
},
)
} else {
allowance
}
},
)
}

fn write_allowance(
Expand All @@ -298,17 +281,15 @@
ContractError::InvalidExpirationLedger
);

let key = DataKey::Allowance(AllowanceDataKey { from, spender });
env.storage().temporary().set(&key, &allowance);
let key = AllowanceDataKey { from, spender };
storage::set_allowance(env, key.clone(), &allowance);

if amount > 0 {
let live_for = expiration_ledger
.checked_sub(env.ledger().sequence())
.unwrap();

env.storage()
.temporary()
.extend_ttl(&key, live_for, live_for)
storage::extend_allowance_ttl(env, key, live_for, live_for);
}
}

Expand All @@ -335,45 +316,21 @@
}
}

fn read_balance(env: &Env, addr: Address) -> i128 {
let key = DataKey::Balance(addr);
env.storage()
.persistent()
.get::<_, i128>(&key)
.inspect(|_| {
// Extend the TTL of the balance entry when the balance is successfully retrieved.
extend_persistent_ttl(env, &key);
})
.unwrap_or_default()
}

fn receive_balance(env: &Env, addr: Address, amount: i128) {
let key = DataKey::Balance(addr);
let current_balance = storage::try_balance(env, addr.clone()).unwrap_or_default();

env.storage()
.persistent()
.update(&key, |balance: Option<i128>| {
balance.unwrap_or_default() + amount
});
storage::set_balance(env, addr, &(current_balance + amount));
}

fn spend_balance(env: &Env, addr: Address, amount: i128) {
let balance = Self::read_balance(env, addr.clone());
let balance = storage::try_balance(env, addr.clone()).unwrap_or_default();

assert_with_error!(env, balance >= amount, ContractError::InsufficientBalance);

Self::write_balance(env, addr, balance - amount);
storage::set_balance(env, addr, &(balance - amount));
}

fn write_metadata(env: &Env, metadata: TokenMetadata) {
TokenUtils::new(env).metadata().set_metadata(&metadata);
}

fn write_balance(env: &Env, addr: Address, amount: i128) {
let key = DataKey::Balance(addr);

env.storage().persistent().set(&key, &amount);

extend_persistent_ttl(env, &key);
}
}
2 changes: 1 addition & 1 deletion contracts/stellar-interchain-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ cfg_if::cfg_if! {
pub use interface::{InterchainTokenClient, InterchainTokenInterface};
} else {
pub mod event;
mod storage_types;
mod storage;
mod contract;

pub use contract::{InterchainToken, InterchainTokenClient};
Expand Down
36 changes: 36 additions & 0 deletions contracts/stellar-interchain-token/src/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use soroban_sdk::{contracttype, Address, BytesN};
use stellar_axelar_std::contractstorage;

#[derive(Clone)]
#[contracttype]
pub struct AllowanceDataKey {
pub from: Address,
pub spender: Address,
}

#[contracttype]
pub struct AllowanceValue {
pub amount: i128,
pub expiration_ledger: u32,
}

/// Do not use the symbol `METADATA` as a key as it is reserved for token metadata.
#[contractstorage]
#[derive(Clone)]
pub enum DataKey {
#[temporary]
#[value(AllowanceValue)]
Allowance { key: AllowanceDataKey },

#[persistent]
#[value(i128)]
Balance { address: Address },

#[instance]
#[status]
Minter { minter: Address },

#[instance]
#[value(BytesN<32>)]
TokenId,
}
24 changes: 0 additions & 24 deletions contracts/stellar-interchain-token/src/storage_types.rs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// Do not use the symbol `METADATA` as a key as it is reserved for token metadata.
#[derive(Clone)]
pub enum DataKey {

#[temporary]
#[value(AllowanceValue)]
Allowance { key: AllowanceDataKey },

#[persistent]
#[value(i128)]
Balance { address: Address },

#[instance]
#[status]
Minter { minter: Address },

#[instance]
#[value(BytesN<32>)]
TokenId,
}
84 changes: 84 additions & 0 deletions contracts/stellar-interchain-token/src/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,87 @@ fn clawback_fails() {

token.clawback(&token.owner(), &1);
}

#[test]
fn allowance_returns_zero_when_expired() {
let env = Env::default();

let user1 = Address::generate(&env);
let user2 = Address::generate(&env);
let amount = 1000;

let (token, _) = setup_token(&env);

// Set current ledger to 100
let current_ledger = 100;
env.ledger().set_sequence_number(current_ledger);

// Set allowance to expire at ledger 200
let expiration_ledger = 200;
assert_auth!(
user1,
token.approve(&user1, &user2, &amount, &expiration_ledger)
);
assert_eq!(token.allowance(&user1, &user2), amount);

// Move to ledger after expiration
env.ledger().set_sequence_number(expiration_ledger + 1);

// Allowance should be 0 after expiration
assert_eq!(token.allowance(&user1, &user2), 0);
}

#[test]
#[should_panic(expected = "HostError: Error(Contract, #7)")] // InvalidExpirationLedger
fn approve_fails_with_expired_ledger() {
let env = Env::default();

let user1 = Address::generate(&env);
let user2 = Address::generate(&env);
let amount = 1000;

let (token, _) = setup_token(&env);

// Set current ledger to 100
let current_ledger = 100;
env.ledger().set_sequence_number(current_ledger);

// Try to set allowance with already expired ledger (before current)
let expired_ledger = current_ledger - 1;

token
.mock_all_auths()
.approve(&user1, &user2, &amount, &expired_ledger);
}

#[test]
#[should_panic(expected = "HostError: Error(Contract, #7)")] // InvalidExpirationLedger
fn allowance_preserves_expiration_when_expired() {
let env = Env::default();
let user1 = Address::generate(&env);
let user2 = Address::generate(&env);
let amount = 1000;
let (token, _) = setup_token(&env);

// Set current ledger to 100
let current_ledger = 100;
env.ledger().set_sequence_number(current_ledger);

// Set allowance to expire at ledger 200
let expiration_ledger = current_ledger + 100;
assert_auth!(
user1,
token.approve(&user1, &user2, &amount, &expiration_ledger)
);

// Move past expiration
env.ledger().set_sequence_number(expiration_ledger + 1);

// First check returns 0
assert_eq!(token.allowance(&user1, &user2), 0);

// Try to set new allowance with the same expired ledger - should fail
token
.mock_all_auths()
.approve(&user1, &user2, &amount, &expiration_ledger);
}
Loading
Loading