Skip to content

Commit 391fdef

Browse files
authored
Merge pull request #37 from AhaLabs/feat/oz
feat!: use OZ FT contract
2 parents 6b47696 + f25a6d3 commit 391fdef

File tree

18 files changed

+865
-1852
lines changed

18 files changed

+865
-1852
lines changed

Cargo.lock

Lines changed: 258 additions & 202 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@ members = [
44
"contracts/*",
55
]
66

7+
[workspace.package]
8+
authors = ["AhaLabs"]
9+
edition = "2021"
10+
license = "Apache-2.0"
11+
repository = "https://github.com/AhaLabs/scaffold-stellar"
12+
version = "0.0.1"
13+
714
[workspace.dependencies]
815
soroban-sdk = { version = "22.0.7" }
916
soroban-token-sdk = { version = "22.0.1" }
17+
stellar-pausable = { git = "https://github.com/OpenZeppelin/stellar-contracts", tag = "v0.2.0" }
18+
stellar-pausable-macros = { git = "https://github.com/OpenZeppelin/stellar-contracts", tag = "v0.2.0" }
19+
stellar-fungible = { git = "https://github.com/OpenZeppelin/stellar-contracts", tag = "v0.2.0" }
1020

1121
[profile.release]
1222
opt-level = "z"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "fungible-token-interface-example"
3+
edition.workspace = true
4+
license.workspace = true
5+
repository = "https://github.com/OpenZeppelin/stellar-contracts"
6+
publish = false
7+
version.workspace = true
8+
9+
[lib]
10+
crate-type = ["cdylib"]
11+
doctest = false
12+
13+
[dependencies]
14+
soroban-sdk = { workspace = true }
15+
stellar-pausable = { workspace = true }
16+
stellar-pausable-macros = { workspace = true }
17+
stellar-fungible = { workspace = true }
18+
19+
[dev-dependencies]
20+
soroban-sdk = { workspace = true, features = ["testutils"] }
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! Fungible Pausable Example Contract.
2+
//!
3+
//! This contract replicates the functionality of the contract in
4+
//! "examples/fungible-pausable", offering the same features. The key difference
5+
//! lies in how SEP-41 compliance is achieved. The contract in "contract.rs"
6+
//! accomplishes this by implementing
7+
//! [`stellar_fungible::fungible::FungibleToken`] and
8+
//! [`stellar_fungible::burnable::FungibleBurnable`], whereas this
9+
//! version directly implements [`soroban_sdk::token::TokenInterface`].
10+
//!
11+
//! Ultimately, it is up to the user to choose their preferred approach to
12+
//! creating a SEP-41 token. We suggest the approach in
13+
//! "examples/fungible-pausable" for better organization of the code,
14+
//! consistency and ease of inspection/debugging.
15+
16+
use soroban_sdk::{
17+
contract, contracterror, contractimpl, panic_with_error, symbol_short, token::TokenInterface,
18+
Address, Env, String, Symbol,
19+
};
20+
use stellar_fungible::{self as fungible, mintable::FungibleMintable};
21+
use stellar_pausable::{self as pausable, Pausable};
22+
use stellar_pausable_macros::when_not_paused;
23+
24+
pub const OWNER: Symbol = symbol_short!("OWNER");
25+
26+
#[contract]
27+
pub struct ExampleContract;
28+
29+
#[contracterror]
30+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
31+
#[repr(u32)]
32+
pub enum ExampleContractError {
33+
Unauthorized = 1,
34+
}
35+
36+
#[contractimpl]
37+
impl ExampleContract {
38+
pub fn __constructor(e: &Env, owner: Address, initial_supply: i128) {
39+
fungible::metadata::set_metadata(
40+
e,
41+
18,
42+
String::from_str(e, "My Token"),
43+
String::from_str(e, "TKN"),
44+
);
45+
fungible::mintable::mint(e, &owner, initial_supply);
46+
e.storage().instance().set(&OWNER, &owner);
47+
}
48+
49+
/// `TokenInterface` doesn't require implementing `total_supply()` because
50+
/// of the need for backwards compatibility with Stellar classic assets.
51+
pub fn total_supply(e: &Env) -> i128 {
52+
fungible::total_supply(e)
53+
}
54+
}
55+
56+
#[contractimpl]
57+
impl Pausable for ExampleContract {
58+
fn paused(e: &Env) -> bool {
59+
pausable::paused(e)
60+
}
61+
62+
fn pause(e: &Env, caller: Address) {
63+
// When `ownable` module is available,
64+
// the following checks should be equivalent to:
65+
// `ownable::only_owner(&e);`
66+
let owner: Address = e.storage().instance().get(&OWNER).expect("owner should be set");
67+
if owner != caller {
68+
panic_with_error!(e, ExampleContractError::Unauthorized);
69+
}
70+
71+
pausable::pause(e, &caller);
72+
}
73+
74+
fn unpause(e: &Env, caller: Address) {
75+
// When `ownable` module is available,
76+
// the following checks should be equivalent to:
77+
// `ownable::only_owner(&e);`
78+
let owner: Address = e.storage().instance().get(&OWNER).expect("owner should be set");
79+
if owner != caller {
80+
panic_with_error!(e, ExampleContractError::Unauthorized);
81+
}
82+
83+
pausable::unpause(e, &caller);
84+
}
85+
}
86+
87+
#[contractimpl]
88+
impl TokenInterface for ExampleContract {
89+
fn balance(e: Env, account: Address) -> i128 {
90+
fungible::balance(&e, &account)
91+
}
92+
93+
fn allowance(e: Env, owner: Address, spender: Address) -> i128 {
94+
fungible::allowance(&e, &owner, &spender)
95+
}
96+
97+
#[when_not_paused]
98+
fn transfer(e: Env, from: Address, to: Address, amount: i128) {
99+
fungible::transfer(&e, &from, &to, amount);
100+
}
101+
102+
#[when_not_paused]
103+
fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) {
104+
fungible::transfer_from(&e, &spender, &from, &to, amount);
105+
}
106+
107+
fn approve(e: Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) {
108+
fungible::approve(&e, &owner, &spender, amount, live_until_ledger);
109+
}
110+
111+
#[when_not_paused]
112+
fn burn(e: Env, from: Address, amount: i128) {
113+
fungible::burnable::burn(&e, &from, amount)
114+
}
115+
116+
#[when_not_paused]
117+
fn burn_from(e: Env, spender: Address, from: Address, amount: i128) {
118+
fungible::burnable::burn_from(&e, &spender, &from, amount)
119+
}
120+
121+
fn decimals(e: Env) -> u32 {
122+
fungible::metadata::decimals(&e)
123+
}
124+
125+
fn name(e: Env) -> String {
126+
fungible::metadata::name(&e)
127+
}
128+
129+
fn symbol(e: Env) -> String {
130+
fungible::metadata::symbol(&e)
131+
}
132+
}
133+
134+
#[contractimpl]
135+
impl FungibleMintable for ExampleContract {
136+
#[when_not_paused]
137+
fn mint(e: &Env, account: Address, amount: i128) {
138+
// When `ownable` module is available,
139+
// the following checks should be equivalent to:
140+
// `ownable::only_owner(&e);`
141+
let owner: Address = e.storage().instance().get(&OWNER).expect("owner should be set");
142+
owner.require_auth();
143+
144+
fungible::mintable::mint(e, &account, amount);
145+
}
146+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#![no_std]
2+
#![allow(dead_code)]
3+
4+
mod contract;
5+
mod test;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#![cfg(test)]
2+
3+
extern crate std;
4+
5+
use soroban_sdk::{testutils::Address as _, Address, Env, String};
6+
7+
use crate::contract::{ExampleContract, ExampleContractClient};
8+
9+
fn create_client<'a>(e: &Env, owner: &Address, initial_supply: i128) -> ExampleContractClient<'a> {
10+
let address = e.register(ExampleContract, (owner, initial_supply));
11+
ExampleContractClient::new(e, &address)
12+
}
13+
14+
#[test]
15+
fn initial_state() {
16+
let e = Env::default();
17+
let owner = Address::generate(&e);
18+
let client = create_client(&e, &owner, 1000);
19+
20+
assert_eq!(client.total_supply(), 1000);
21+
assert_eq!(client.balance(&owner), 1000);
22+
assert_eq!(client.symbol(), String::from_str(&e, "TKN"));
23+
assert_eq!(client.name(), String::from_str(&e, "My Token"));
24+
assert_eq!(client.decimals(), 18);
25+
assert!(!client.paused());
26+
}
27+
28+
#[test]
29+
fn transfer_works() {
30+
let e = Env::default();
31+
let owner = Address::generate(&e);
32+
let recipient = Address::generate(&e);
33+
let client = create_client(&e, &owner, 1000);
34+
35+
e.mock_all_auths();
36+
client.transfer(&owner, &recipient, &100);
37+
assert_eq!(client.balance(&owner), 900);
38+
assert_eq!(client.balance(&recipient), 100);
39+
}
40+
41+
#[test]
42+
#[should_panic(expected = "Error(Contract, #100)")]
43+
fn transfer_fails_when_paused() {
44+
let e = Env::default();
45+
let owner = Address::generate(&e);
46+
let recipient = Address::generate(&e);
47+
let client = create_client(&e, &owner, 1000);
48+
49+
e.mock_all_auths();
50+
client.pause(&owner);
51+
client.transfer(&owner, &recipient, &100);
52+
}
53+
54+
#[test]
55+
fn transfer_from_works() {
56+
let e = Env::default();
57+
let owner = Address::generate(&e);
58+
let spender = Address::generate(&e);
59+
let recipient = Address::generate(&e);
60+
let client = create_client(&e, &owner, 1000);
61+
62+
e.mock_all_auths();
63+
client.approve(&owner, &spender, &200, &100);
64+
client.transfer_from(&spender, &owner, &recipient, &200);
65+
assert_eq!(client.balance(&owner), 800);
66+
assert_eq!(client.balance(&recipient), 200);
67+
}
68+
69+
#[test]
70+
#[should_panic(expected = "Error(Contract, #100)")]
71+
fn transfer_from_fails_when_paused() {
72+
let e = Env::default();
73+
let owner = Address::generate(&e);
74+
let spender = Address::generate(&e);
75+
let recipient = Address::generate(&e);
76+
let client = create_client(&e, &owner, 1000);
77+
78+
e.mock_all_auths();
79+
client.pause(&owner);
80+
client.transfer_from(&spender, &owner, &recipient, &200);
81+
}
82+
83+
#[test]
84+
fn mint_works() {
85+
let e = Env::default();
86+
let owner = Address::generate(&e);
87+
let client = create_client(&e, &owner, 1000);
88+
89+
e.mock_all_auths();
90+
client.mint(&owner, &500);
91+
assert_eq!(client.total_supply(), 1500);
92+
assert_eq!(client.balance(&owner), 1500);
93+
}
94+
95+
#[test]
96+
#[should_panic(expected = "Error(Contract, #100)")]
97+
fn mint_fails_when_paused() {
98+
let e = Env::default();
99+
let owner = Address::generate(&e);
100+
let client = create_client(&e, &owner, 1000);
101+
102+
e.mock_all_auths();
103+
client.pause(&owner);
104+
client.mint(&owner, &500);
105+
}
106+
107+
#[test]
108+
fn burn_works() {
109+
let e = Env::default();
110+
let owner = Address::generate(&e);
111+
let client = create_client(&e, &owner, 1000);
112+
113+
e.mock_all_auths();
114+
client.burn(&owner, &200);
115+
assert_eq!(client.total_supply(), 800);
116+
assert_eq!(client.balance(&owner), 800);
117+
}
118+
119+
#[test]
120+
#[should_panic(expected = "Error(Contract, #100)")]
121+
fn burn_fails_when_paused() {
122+
let e = Env::default();
123+
let owner = Address::generate(&e);
124+
let client = create_client(&e, &owner, 1000);
125+
126+
e.mock_all_auths();
127+
client.pause(&owner);
128+
client.burn(&owner, &200);
129+
}

contracts/hello_world/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
[package]
22
name = "soroban-hello-world-contract"
3-
version = "0.0.0"
4-
edition = "2021"
3+
description = "A simple hello world contract"
4+
edition.workspace = true
5+
license.workspace = true
6+
repository.workspace = true
57
publish = false
6-
repository = "https://github.com/stellar/soroban-examples/tree/65d25d24028f71b2c2acb93e09791fcbe4b97142/hello_world"
8+
version.workspace = true
79

810
[lib]
911
crate-type = ["cdylib"]

contracts/token/Cargo.toml

Lines changed: 0 additions & 16 deletions
This file was deleted.

contracts/token/src/admin.rs

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)