diff --git a/templates/README.md b/templates/README.md index b84e8765..b72643c3 100644 --- a/templates/README.md +++ b/templates/README.md @@ -83,6 +83,13 @@ To be valid, a template must contain: - `src/` directory - Source code - `src/lib.rs` - Main contract file +## Example Templates + +Built-in example templates are provided under `templates/examples/`: + +- `simple-counter`: A basic smart contract demonstrating storage usage by incrementing, getting, and resetting a counter. +- `token-allowlist`: A smart contract for managing an allowlist of approved addresses, controlled by an administrator. + ## Template Placeholders Templates can use placeholders that will be replaced during scaffolding: diff --git a/templates/examples/token-allowlist/Cargo.toml b/templates/examples/token-allowlist/Cargo.toml new file mode 100644 index 00000000..57324939 --- /dev/null +++ b/templates/examples/token-allowlist/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "{{PROJECT_NAME}}" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "21.0.0" + +[dev-dependencies] +soroban-sdk = { version = "21.0.0", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true diff --git a/templates/examples/token-allowlist/README.md b/templates/examples/token-allowlist/README.md new file mode 100644 index 00000000..87b6ffe9 --- /dev/null +++ b/templates/examples/token-allowlist/README.md @@ -0,0 +1,56 @@ +# {{PROJECT_NAME}} + +A token allowlist smart contract for Soroban. It enables managing a list of approved addresses that are permitted to perform actions (like transfer/receive tokens, or participate in a DAO). + +## Features + +- Initialize contract with an admin +- Check if an address is allowlisted +- Add addresses to the allowlist (admin only) +- Remove addresses from the allowlist (admin only) +- Update admin address (admin only) + +## Build + +```bash +stellar contract build +``` + +## Test + +```bash +cargo test +``` + +## Deploy + +```bash +starforge deploy \ + --wasm target/wasm32-unknown-unknown/release/{{PROJECT_NAME_SNAKE}}.wasm \ + --network testnet +``` + +## Usage + +```bash +# Initialize the contract +stellar contract invoke \ + --id \ + --network testnet \ + -- initialize \ + --admin + +# Add user to allowlist +stellar contract invoke \ + --id \ + --network testnet \ + -- add \ + --address + +# Check allowlist status +stellar contract invoke \ + --id \ + --network testnet \ + -- is_allowed \ + --address +``` diff --git a/templates/examples/token-allowlist/src/lib.rs b/templates/examples/token-allowlist/src/lib.rs new file mode 100644 index 00000000..23a9ee1e --- /dev/null +++ b/templates/examples/token-allowlist/src/lib.rs @@ -0,0 +1,89 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Admin, + Allowed(Address), +} + +#[contract] +pub struct {{PROJECT_NAME_PASCAL}}; + +#[contractimpl] +impl {{PROJECT_NAME_PASCAL}} { + /// Initialize the contract with an admin address + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic!("already initialized"); + } + env.storage().instance().set(&DataKey::Admin, &admin); + } + + /// Check if an address is in the allowlist + pub fn is_allowed(env: Env, address: Address) -> bool { + env.storage().persistent().get(&DataKey::Allowed(address)).unwrap_or(false) + } + + /// Add an address to the allowlist (admin only) + pub fn add(env: Env, address: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("not initialized"); + admin.require_auth(); + env.storage().persistent().set(&DataKey::Allowed(address), &true); + } + + /// Remove an address from the allowlist (admin only) + pub fn remove(env: Env, address: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("not initialized"); + admin.require_auth(); + env.storage().persistent().set(&DataKey::Allowed(address), &false); + } + + /// Update the admin address (admin only) + pub fn set_admin(env: Env, new_admin: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("not initialized"); + admin.require_auth(); + env.storage().instance().set(&DataKey::Admin, &new_admin); + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::testutils::Address as _; + + #[test] + fn test_allowlist_flow() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, {{PROJECT_NAME_PASCAL}}); + let client = {{PROJECT_NAME_PASCAL}}Client::new(&env, &contract_id); + + let admin = Address::generate(&env); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + + // Initialize contract + client.initialize(&admin); + + // Should not be allowed initially + assert!(!client.is_allowed(&user1)); + assert!(!client.is_allowed(&user2)); + + // Add user1 to allowlist + client.add(&user1); + assert!(client.is_allowed(&user1)); + assert!(!client.is_allowed(&user2)); + + // Add user2 to allowlist + client.add(&user2); + assert!(client.is_allowed(&user2)); + + // Remove user1 + client.remove(&user1); + assert!(!client.is_allowed(&user1)); + assert!(client.is_allowed(&user2)); + } +}