├── clippy.toml ├── packages ├── tokens │ ├── src │ │ ├── rwa │ │ │ ├── utils │ │ │ │ └── mod.rs │ │ │ ├── extensions │ │ │ │ └── mod.rs │ │ │ └── identity_verifier │ │ │ │ └── mod.rs │ │ ├── non_fungible │ │ │ ├── utils │ │ │ │ ├── mod.rs │ │ │ │ └── sequential │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── test.rs │ │ │ │ │ └── storage.rs │ │ │ └── extensions │ │ │ │ ├── mod.rs │ │ │ │ └── burnable │ │ │ │ └── storage.rs │ │ ├── fungible │ │ │ ├── utils │ │ │ │ ├── mod.rs │ │ │ │ ├── sac_admin_generic │ │ │ │ │ ├── README.md │ │ │ │ │ └── mod.rs │ │ │ │ └── sac_admin_wrapper │ │ │ │ │ └── README.md │ │ │ └── extensions │ │ │ │ ├── mod.rs │ │ │ │ ├── capped │ │ │ │ ├── mod.rs │ │ │ │ └── storage.rs │ │ │ │ ├── burnable │ │ │ │ ├── storage.rs │ │ │ │ ├── mod.rs │ │ │ │ └── test.rs │ │ │ │ ├── allowlist │ │ │ │ └── mod.rs │ │ │ │ └── blocklist │ │ │ │ └── mod.rs │ │ └── lib.rs │ └── Cargo.toml ├── governance │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── accounts │ ├── src │ │ ├── verifiers │ │ │ ├── test │ │ │ │ └── mod.rs │ │ │ ├── utils │ │ │ │ ├── mod.rs │ │ │ │ ├── extract_from_bytes.rs │ │ │ │ └── base64_url.rs │ │ │ └── ed25519.rs │ │ ├── smart_account │ │ │ └── test │ │ │ │ └── mod.rs │ │ ├── policies │ │ │ └── test │ │ │ │ └── mod.rs │ │ └── lib.rs │ ├── docs │ │ ├── ExternalSigner.png │ │ └── SmartAccount-ContextRules.png │ └── Cargo.toml ├── contract-utils │ ├── src │ │ ├── crypto │ │ │ ├── test │ │ │ │ ├── mod.rs │ │ │ │ └── hashable.rs │ │ │ ├── mod.rs │ │ │ ├── error.rs │ │ │ ├── sha256.rs │ │ │ ├── keccak.rs │ │ │ ├── hasher.rs │ │ │ ├── proptest-regressions │ │ │ │ └── merkle.txt │ │ │ └── hashable.rs │ │ ├── math │ │ │ ├── test │ │ │ │ └── mod.rs │ │ │ └── i256_fixed_point.rs │ │ ├── lib.rs │ │ ├── upgradeable │ │ │ ├── test.rs │ │ │ └── storage.rs │ │ ├── pausable │ │ │ ├── test.rs │ │ │ └── storage.rs │ │ └── merkle_distributor │ │ │ └── mod.rs │ └── Cargo.toml ├── access │ ├── src │ │ ├── lib.rs │ │ └── role_transfer │ │ │ ├── mod.rs │ │ │ └── storage.rs │ └── Cargo.toml ├── macros │ ├── Cargo.toml │ └── src │ │ ├── pausable.rs │ │ └── helpers.rs ├── test-utils │ ├── event-assertion │ │ └── Cargo.toml │ └── default-impl-macro-test │ │ ├── Cargo.toml │ │ ├── src │ │ └── lib.rs │ │ └── tests │ │ ├── ownable.rs │ │ ├── access_control.rs │ │ ├── non_fungible_enumerable.rs │ │ ├── fungible_burnable.rs │ │ └── non_fungible_burnable.rs └── fee-abstraction │ ├── Cargo.toml │ └── README.md ├── examples ├── upgradeable │ ├── v2 │ │ ├── src │ │ │ ├── lib.rs │ │ │ └── contract.rs │ │ └── Cargo.toml │ ├── upgrader │ │ ├── src │ │ │ ├── lib.rs │ │ │ ├── contract.rs │ │ │ └── test.rs │ │ └── Cargo.toml │ ├── v1 │ │ ├── src │ │ │ ├── lib.rs │ │ │ ├── test.rs │ │ │ └── contract.rs │ │ └── Cargo.toml │ └── testdata │ │ ├── upgradeable_v1_example.wasm │ │ └── upgradeable_v2_example.wasm ├── merkle-voting │ ├── src │ │ ├── lib.rs │ │ ├── test.rs │ │ └── contract.rs │ └── Cargo.toml ├── sac-admin-generic │ ├── src │ │ ├── lib.rs │ │ └── test.rs │ └── Cargo.toml ├── fungible-vault │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ └── Cargo.toml ├── fungible-merkle-airdrop │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ └── Cargo.toml ├── multisig-smart-account │ ├── ed25519-verifier │ │ ├── src │ │ │ ├── lib.rs │ │ │ └── contract.rs │ │ └── Cargo.toml │ ├── threshold-policy │ │ ├── src │ │ │ ├── lib.rs │ │ │ └── contract.rs │ │ └── Cargo.toml │ ├── spending-limit-policy │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── account │ │ ├── src │ │ │ ├── lib.rs │ │ │ └── test.rs │ │ └── Cargo.toml │ └── webauthn-verifier │ │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ │ └── Cargo.toml ├── ownable │ ├── src │ │ ├── lib.rs │ │ ├── contract.rs │ │ └── test.rs │ └── Cargo.toml ├── pausable │ ├── src │ │ ├── lib.rs │ │ ├── test.rs │ │ └── contract.rs │ └── Cargo.toml ├── timelock-controller │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── nft-enumerable │ ├── src │ │ ├── lib.rs │ │ ├── test.rs │ │ └── contract.rs │ └── Cargo.toml ├── rwa │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ └── Cargo.toml ├── fungible-blocklist │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ └── Cargo.toml ├── fungible-capped │ ├── src │ │ ├── lib.rs │ │ ├── contract.rs │ │ └── test.rs │ └── Cargo.toml ├── fungible-pausable │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── nft-access-control │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ └── Cargo.toml ├── nft-consecutive │ ├── src │ │ ├── lib.rs │ │ ├── test.rs │ │ └── contract.rs │ └── Cargo.toml ├── nft-royalties │ ├── src │ │ ├── lib.rs │ │ ├── test.rs │ │ └── contract.rs │ └── Cargo.toml ├── sac-admin-wrapper │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ └── Cargo.toml ├── fungible-allowlist │ ├── src │ │ ├── lib.rs │ │ └── contract.rs │ └── Cargo.toml ├── nft-sequential-minting │ ├── src │ │ ├── lib.rs │ │ ├── contract.rs │ │ └── test.rs │ └── Cargo.toml ├── fee-forwarder-permissionless │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── fee-forwarder-permissioned │ ├── Cargo.toml │ └── src │ └── lib.rs ├── rust-toolchain.toml ├── audits ├── Stellar Contracts Library 0.1.0-RC Audit.pdf ├── Stellar Contracts Library v0.2.0 Audit.pdf ├── Stellar Contracts Library v0.5.0 Audit.pdf ├── Stellar Contracts Library v0.5.0 Re-Audit.pdf └── Stellar Contracts Library v0.3.0-rc.2 Audit.pdf ├── codecov.yml ├── .github ├── workflows │ ├── typos.yml │ └── generic.yml ├── ISSUE_TEMPLATE │ ├── core_implementation.yml │ ├── feature_request.yml │ └── bug_report.yml └── pull_request_template.md ├── .gitignore ├── rustfmt.toml ├── LICENSE ├── SECURITY.md └── Cargo.toml /clippy.toml: -------------------------------------------------------------------------------- 1 | allow-unwrap-in-tests = true 2 | -------------------------------------------------------------------------------- /packages/tokens/src/rwa/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token_binder; 2 | -------------------------------------------------------------------------------- /packages/governance/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub mod timelock; 4 | -------------------------------------------------------------------------------- /packages/tokens/src/non_fungible/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sequential; 2 | -------------------------------------------------------------------------------- /packages/accounts/src/verifiers/test/mod.rs: -------------------------------------------------------------------------------- 1 | mod ed25519; 2 | mod utils; 3 | mod webauthn; 4 | -------------------------------------------------------------------------------- /examples/upgradeable/v2/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | -------------------------------------------------------------------------------- /examples/merkle-voting/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | pub mod contract; 3 | #[cfg(test)] 4 | mod test; 5 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sac_admin_generic; 2 | pub mod sac_admin_wrapper; 3 | -------------------------------------------------------------------------------- /examples/sac-admin-generic/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | pub mod contract; 3 | #[cfg(test)] 4 | mod test; 5 | -------------------------------------------------------------------------------- /examples/fungible-vault/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | mod test; 6 | -------------------------------------------------------------------------------- /examples/upgradeable/upgrader/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | 4 | #[cfg(test)] 5 | mod test; 6 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/test/mod.rs: -------------------------------------------------------------------------------- 1 | mod hashable; 2 | mod keccak; 3 | mod merkle; 4 | mod sha256; 5 | -------------------------------------------------------------------------------- /examples/fungible-merkle-airdrop/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub mod contract; 4 | 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/ed25519-verifier/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/threshold-policy/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | -------------------------------------------------------------------------------- /examples/ownable/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/pausable/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/timelock-controller/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | mod contract; 4 | mod test; 5 | 6 | pub use contract::*; 7 | -------------------------------------------------------------------------------- /packages/access/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub mod access_control; 4 | pub mod ownable; 5 | pub mod role_transfer; 6 | -------------------------------------------------------------------------------- /packages/accounts/src/smart_account/test/mod.rs: -------------------------------------------------------------------------------- 1 | mod context_rules; 2 | mod fingerprints; 3 | mod signers_and_policies; 4 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/spending-limit-policy/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | -------------------------------------------------------------------------------- /examples/nft-enumerable/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/rwa/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | mod contract; 4 | mod identity_registry_storage; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/fungible-blocklist/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/fungible-capped/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/fungible-pausable/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/nft-access-control/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/nft-consecutive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/nft-royalties/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | pub mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/sac-admin-wrapper/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /examples/upgradeable/v1/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | pub mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /packages/accounts/src/policies/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod simple_threshold; 2 | pub mod spending_limit; 3 | pub mod weighted_threshold; 4 | -------------------------------------------------------------------------------- /packages/contract-utils/src/math/test/mod.rs: -------------------------------------------------------------------------------- 1 | mod fixed_point; 2 | mod i128_fixed_point; 3 | mod i256_fixed_point; 4 | mod wad; 5 | -------------------------------------------------------------------------------- /examples/fungible-allowlist/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | 6 | #[cfg(test)] 7 | mod test; 8 | -------------------------------------------------------------------------------- /examples/nft-sequential-minting/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod allowlist; 2 | pub mod blocklist; 3 | pub mod burnable; 4 | pub mod capped; 5 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | targets = ["wasm32v1-none"] 4 | components = ["rustfmt", "clippy", "rust-src"] 5 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/account/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | pub mod contract; 5 | #[cfg(test)] 6 | mod test; 7 | -------------------------------------------------------------------------------- /packages/accounts/docs/ExternalSigner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/packages/accounts/docs/ExternalSigner.png -------------------------------------------------------------------------------- /packages/tokens/src/non_fungible/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod burnable; 2 | pub mod consecutive; 3 | pub mod enumerable; 4 | pub mod royalties; 5 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/webauthn-verifier/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(dead_code)] 3 | 4 | pub mod contract; 5 | 6 | #[cfg(test)] 7 | mod test; 8 | -------------------------------------------------------------------------------- /audits/Stellar Contracts Library 0.1.0-RC Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/audits/Stellar Contracts Library 0.1.0-RC Audit.pdf -------------------------------------------------------------------------------- /audits/Stellar Contracts Library v0.2.0 Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/audits/Stellar Contracts Library v0.2.0 Audit.pdf -------------------------------------------------------------------------------- /audits/Stellar Contracts Library v0.5.0 Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/audits/Stellar Contracts Library v0.5.0 Audit.pdf -------------------------------------------------------------------------------- /audits/Stellar Contracts Library v0.5.0 Re-Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/audits/Stellar Contracts Library v0.5.0 Re-Audit.pdf -------------------------------------------------------------------------------- /packages/accounts/docs/SmartAccount-ContextRules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/packages/accounts/docs/SmartAccount-ContextRules.png -------------------------------------------------------------------------------- /packages/contract-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub mod crypto; 4 | pub mod math; 5 | pub mod merkle_distributor; 6 | pub mod pausable; 7 | pub mod upgradeable; 8 | -------------------------------------------------------------------------------- /packages/tokens/src/non_fungible/utils/sequential/mod.rs: -------------------------------------------------------------------------------- 1 | mod storage; 2 | #[cfg(test)] 3 | mod test; 4 | 5 | pub use self::storage::{increment_token_id, next_token_id}; 6 | -------------------------------------------------------------------------------- /audits/Stellar Contracts Library v0.3.0-rc.2 Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/audits/Stellar Contracts Library v0.3.0-rc.2 Audit.pdf -------------------------------------------------------------------------------- /examples/upgradeable/testdata/upgradeable_v1_example.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/examples/upgradeable/testdata/upgradeable_v1_example.wasm -------------------------------------------------------------------------------- /examples/upgradeable/testdata/upgradeable_v2_example.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/HEAD/examples/upgradeable/testdata/upgradeable_v2_example.wasm -------------------------------------------------------------------------------- /packages/accounts/src/verifiers/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod base64_url; 2 | mod extract_from_bytes; 3 | 4 | pub use base64_url::base64_url_encode; 5 | pub use extract_from_bytes::extract_from_bytes; 6 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod hashable; 3 | pub mod hasher; 4 | pub mod keccak; 5 | pub mod merkle; 6 | pub mod sha256; 7 | 8 | #[cfg(test)] 9 | mod test; 10 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "packages/macros/*" 3 | - "packages/**/test/*" 4 | - "packages/**/test.rs" 5 | - "packages/tokens/src/fungible/impl_token_interface_macro.rs" 6 | - "packages/tokens/src/fungible/overrides.rs" 7 | - "packages/tokens/src/non-fungible/overrides.rs" 8 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | name: Check for typos 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check-for-typos: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout the repository 10 | uses: actions/checkout@v4 11 | 12 | - name: Check for typos 13 | uses: crate-ci/typos@v1.24.5 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | /.idea 3 | .DS_Store 4 | .thumbs.db 5 | .vscode 6 | 7 | # These are backup files generated by rustfmt 8 | **/*.rs.bk 9 | 10 | # Environments 11 | .env 12 | .venv 13 | env/ 14 | venv/ 15 | ENV/ 16 | env.bak/ 17 | venv.bak/ 18 | 19 | # Code Coverage 20 | htmlcov/ 21 | lcov.info 22 | coverage/ 23 | 24 | **/test_snapshots/ 25 | -------------------------------------------------------------------------------- /packages/accounts/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Soroban Smart Accounts 2 | //! 3 | //! A flexible and modular smart account framework for Soroban that enables 4 | //! advanced authentication and authorization patterns through composable rules, 5 | //! signers, and policies. 6 | #![no_std] 7 | 8 | pub mod policies; 9 | pub mod smart_account; 10 | pub mod verifiers; 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/core_implementation.yml: -------------------------------------------------------------------------------- 1 | name: 🏗️ Core Implementation 2 | description: Create a report to help us improve 3 | title: "🏗️ [Core Feature]: " 4 | labels: ["core", "needs triage"] 5 | body: 6 | - type: textarea 7 | id: feature 8 | attributes: 9 | label: What is the key feature we're aiming to implement? 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /packages/macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-macros" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | version.workspace = true 7 | publish = true 8 | description = "Macros for Stellar contracts." 9 | 10 | 11 | [lib] 12 | proc-macro = true 13 | doctest = false 14 | 15 | [dependencies] 16 | proc-macro2 = { workspace = true } 17 | quote = { workspace = true } 18 | syn = { workspace = true } 19 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/error.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::contracterror; 2 | 3 | #[contracterror] 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 5 | #[repr(u32)] 6 | pub enum CryptoError { 7 | /// The merkle proof length is out of bounds. 8 | MerkleProofOutOfBounds = 1400, 9 | /// The index of the leaf is out of bounds. 10 | MerkleIndexOutOfBounds = 1401, 11 | /// No data in hasher state. 12 | HasherEmptyState = 1402, 13 | } 14 | -------------------------------------------------------------------------------- /examples/fungible-capped/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fungible-capped-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-tokens = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | -------------------------------------------------------------------------------- /examples/nft-consecutive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft-consecutive-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-tokens = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | -------------------------------------------------------------------------------- /examples/merkle-voting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "merkle-voting-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-contract-utils = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | -------------------------------------------------------------------------------- /examples/ownable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ownable-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | -------------------------------------------------------------------------------- /packages/governance/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-governance" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | version.workspace = true 7 | publish = true 8 | description = "Governance Utilities for Stellar contracts." 9 | 10 | 11 | [lib] 12 | crate-type = ["lib", "cdylib"] 13 | doctest = false 14 | 15 | [dependencies] 16 | soroban-sdk = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/ed25519-verifier/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multisig-ed25519-verifier-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-accounts = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/threshold-policy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multisig-threshold-policy-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-accounts = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | -------------------------------------------------------------------------------- /examples/pausable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pausable-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-contract-utils = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | -------------------------------------------------------------------------------- /examples/nft-enumerable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft-enumerable-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-tokens = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | -------------------------------------------------------------------------------- /examples/upgradeable/v1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upgradeable-v1-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = false 7 | version = "1.0.0" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | doctest = false 12 | 13 | [dependencies] 14 | soroban-sdk = { workspace = true } 15 | stellar-contract-utils = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | -------------------------------------------------------------------------------- /examples/upgradeable/v2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upgradeable-v2-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = false 7 | version = "2.0.0" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | doctest = false 12 | 13 | [dependencies] 14 | soroban-sdk = { workspace = true } 15 | stellar-contract-utils = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/spending-limit-policy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multisig-spending-limit-policy-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-accounts = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | -------------------------------------------------------------------------------- /examples/sac-admin-generic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sac-admin-generic-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-tokens = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | ed25519-dalek = { workspace = true } 20 | -------------------------------------------------------------------------------- /examples/nft-sequential-minting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft-sequential-minting-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = false 7 | version.workspace = true 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | doctest = false 12 | 13 | [dependencies] 14 | stellar-macros = { workspace = true } 15 | soroban-sdk = { workspace = true } 16 | stellar-tokens = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | -------------------------------------------------------------------------------- /examples/rwa/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rwa-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | stellar-tokens = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /packages/test-utils/event-assertion/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-event-assertion" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = false 7 | version.workspace = true 8 | 9 | [lib] 10 | crate-type = ["lib"] 11 | doctest = false 12 | 13 | # This crate is only used for testing, and not compatible with `wasm32` targets. 14 | [target.'cfg(not(target_family = "wasm"))'.dependencies] 15 | soroban-sdk = { workspace = true, features = ["testutils"] } 16 | stellar-tokens = { workspace = true } 17 | -------------------------------------------------------------------------------- /examples/nft-royalties/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft-royalties-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | stellar-tokens = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/fungible-vault/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fungible-vault-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | stellar-tokens = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/fungible-allowlist/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fungible-allowlist-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | stellar-tokens = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/fungible-blocklist/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fungible-blocklist-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | stellar-tokens = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/nft-access-control/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft-access-control-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | stellar-tokens = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/sac-admin-wrapper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sac-admin-wrapper-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-tokens = { workspace = true } 16 | stellar-access = { workspace = true } 17 | stellar-macros = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/upgradeable/upgrader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upgrader-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-contract-utils = { workspace = true } 16 | stellar-access = { workspace = true } 17 | stellar-macros = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/fungible-pausable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fungible-pausable-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-contract-utils = { workspace = true } 16 | stellar-macros = { workspace = true } 17 | stellar-tokens = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /examples/timelock-controller/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "timelock-controller-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-access = { workspace = true } 16 | stellar-governance = { workspace = true } 17 | stellar-macros = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | -------------------------------------------------------------------------------- /packages/access/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-access" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | version.workspace = true 7 | publish = true 8 | description = "Access Control, Ownable, and Role Transfer utilities for Stellar contracts." 9 | 10 | 11 | [lib] 12 | crate-type = ["lib", "cdylib"] 13 | doctest = false 14 | 15 | [dependencies] 16 | soroban-sdk = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | stellar-event-assertion = { workspace = true } 21 | -------------------------------------------------------------------------------- /packages/access/src/role_transfer/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module only acts as a utility crate for `Access Control` and `Ownable`. 2 | //! It is not intended to be used directly. 3 | 4 | use soroban_sdk::contracterror; 5 | 6 | mod storage; 7 | 8 | pub use storage::{accept_transfer, transfer_role}; 9 | 10 | #[contracterror] 11 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 12 | #[repr(u32)] 13 | pub enum RoleTransferError { 14 | NoPendingTransfer = 2200, 15 | InvalidLiveUntilLedger = 2201, 16 | InvalidPendingAccount = 2202, 17 | } 18 | 19 | #[cfg(test)] 20 | mod test; 21 | -------------------------------------------------------------------------------- /examples/fee-forwarder-permissionless/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fee-forwarder-permissionless-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-fee-abstraction = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | stellar-tokens = { workspace = true } 20 | stellar-macros = { workspace = true } 21 | -------------------------------------------------------------------------------- /packages/fee-abstraction/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-fee-abstraction" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | version.workspace = true 7 | publish = true 8 | description = "Fee abstraction utilities for Stellar contracts." 9 | 10 | [lib] 11 | crate-type = ["lib", "cdylib"] 12 | doctest = false 13 | 14 | [dependencies] 15 | soroban-sdk = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | stellar-tokens = { workspace = true } 20 | stellar-macros = { workspace = true } 21 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/account/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multisig-account-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-accounts = { workspace = true } 16 | stellar-contract-utils = { workspace = true } 17 | stellar-macros = { workspace = true } 18 | 19 | 20 | [dev-dependencies] 21 | soroban-sdk = { workspace = true, features = ["testutils"] } 22 | 23 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/webauthn-verifier/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multisig-webauthn-verifier-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-accounts = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | p256 = { workspace = true, features = ["ecdsa"] } 20 | hex-literal = { workspace = true } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🎁 Feature Request 2 | description: Suggest an idea for this project ⚡️ 3 | title: "🎁 [Feature Request]: " 4 | labels: ["enhancement", "needs triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this feature request! 10 | - type: textarea 11 | id: feature 12 | attributes: 13 | label: What is the feature you would like to see? 14 | description: Is your feature request related to a specific problem? Is it just a crazy idea? Tell us about it! 15 | validations: 16 | required: true 17 | -------------------------------------------------------------------------------- /packages/contract-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-contract-utils" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | version.workspace = true 7 | publish = true 8 | description = "Utilities for Stellar contracts." 9 | 10 | 11 | [lib] 12 | crate-type = ["lib", "cdylib"] 13 | doctest = false 14 | 15 | [dependencies] 16 | soroban-sdk = { workspace = true } 17 | 18 | [dev-dependencies] 19 | soroban-sdk = { workspace = true, features = ["testutils"] } 20 | proptest = { workspace = true } 21 | hex-literal = { workspace = true } 22 | stellar-event-assertion = { workspace = true } 23 | -------------------------------------------------------------------------------- /examples/fee-forwarder-permissioned/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fee-forwarder-permissioned-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-fee-abstraction = { workspace = true } 16 | stellar-access = { workspace = true } 17 | stellar-macros = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | stellar-tokens = { workspace = true } 22 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | 2 | format_macro_bodies = true 3 | format_macro_matchers = true 4 | format_strings = true 5 | imports_granularity = "Crate" 6 | match_arm_blocks = false 7 | reorder_impl_items = true 8 | group_imports = "StdExternalCrate" 9 | use_field_init_shorthand = true 10 | use_small_heuristics = "Max" 11 | wrap_comments = true 12 | format_code_in_doc_comments = true 13 | 14 | # most of these are unstable, so we enable them 15 | unstable_features = true 16 | 17 | 18 | # wanted to enable below, but they are removing the documentation comments if there is an empty line in between 19 | # wrap_comments = true 20 | # format_code_in_doc_comments = true 21 | -------------------------------------------------------------------------------- /packages/tokens/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Stellar Tokens 2 | //! 3 | //! This crate provides implementations for both fungible and non-fungible 4 | //! tokens for use in Soroban smart contracts on the Stellar network. 5 | //! 6 | //! ## Modules 7 | //! 8 | //! - `fungible`: Implementation of fungible tokens (similar to ERC-20) 9 | //! - `non_fungible`: Implementation of non-fungible tokens (similar to ERC-721) 10 | //! 11 | //! Each module provides its own set of traits, functions, and extensions for 12 | //! working with the respective token type. 13 | 14 | #![no_std] 15 | 16 | pub mod fungible; 17 | pub mod non_fungible; 18 | pub mod rwa; 19 | pub mod vault; 20 | -------------------------------------------------------------------------------- /examples/fungible-merkle-airdrop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fungible-merkle-airdrop-example" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 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-tokens = { workspace = true } 16 | stellar-access = { workspace = true } 17 | stellar-contract-utils = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | hex-literal = { workspace = true } 22 | stellar-macros = { workspace = true } 23 | -------------------------------------------------------------------------------- /packages/test-utils/default-impl-macro-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-default-impl-macro-test" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = false 7 | version.workspace = true 8 | 9 | [lib] 10 | crate-type = ["lib"] 11 | doctest = false 12 | 13 | # This crate is only used for testing, and not compatible with `wasm32` targets. 14 | [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] 15 | soroban-sdk = { workspace = true, features = ["testutils"] } 16 | stellar-access = { workspace = true } 17 | stellar-macros = { workspace = true } 18 | stellar-event-assertion = { workspace = true } 19 | stellar-tokens = { workspace = true } 20 | -------------------------------------------------------------------------------- /packages/test-utils/default-impl-macro-test/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate is a test crate for the default-impl-macro. 2 | //! Proc-macros cannot be tested within their own crate due to Rust's 3 | //! limitations, hence a separate crate for testing is used for testing the 4 | //! proc-macro. 5 | //! 6 | //! This crate is not intended for use in any other context. And this `lib.rs` 7 | //! file is empty on purpose. 8 | 9 | // A conditional attribute that applies `no_std` only for wasm targets. 10 | // This prevents Cargo from implicitly injecting std::prelude imports into empty crates 11 | // when building for wasm targets that don't support std (like wasm32v1-none). 12 | #![cfg_attr(target_family = "wasm", no_std)] 13 | -------------------------------------------------------------------------------- /examples/fee-forwarder-permissioned/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Permissioned fee forwarder example. 2 | //! 3 | //! This example shows how to integrate the `stellar-fee-abstraction` helpers in 4 | //! a **permissioned** setup: 5 | //! 6 | //! - Only **trusted executors** configured by the contract are allowed to call 7 | //! the `forward` entrypoint. 8 | //! - The **forwarder contract itself** collects the fees, which can later be 9 | //! swept or otherwise managed according to the contract logic. 10 | //! 11 | //! This pattern is suitable for environments with a curated set of executors 12 | //! with tighter operational control. 13 | 14 | #![no_std] 15 | #![allow(clippy::too_many_arguments)] 16 | 17 | mod contract; 18 | 19 | #[cfg(test)] 20 | mod test; 21 | -------------------------------------------------------------------------------- /packages/tokens/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-tokens" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = true 7 | version.workspace = true 8 | description = "Fungible and NonFungible Tokens for the Stellar contracts." 9 | 10 | [lib] 11 | crate-type = ["lib", "cdylib"] 12 | doctest = false 13 | 14 | [dependencies] 15 | soroban-sdk = { workspace = true } 16 | stellar-contract-utils = { workspace = true } 17 | 18 | [dev-dependencies] 19 | ed25519-dalek = { workspace = true } 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | soroban-test-helpers = { workspace = true } 22 | stellar-event-assertion = { workspace = true } 23 | k256 = { workspace = true, features = ["ecdsa"] } 24 | p256 = { workspace = true, features = ["ecdsa"] } 25 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/capped/mod.rs: -------------------------------------------------------------------------------- 1 | /// Unlike other extensions, the `capped` extension does not provide a separate 2 | /// trait. This is because its methods are not intended to be used 3 | /// independently, like [`crate::extensions::burnable::burn()`]. 4 | /// Instead, the `capped` extension modifies the business logic of the `mint` 5 | /// function to enforce a supply cap. 6 | /// 7 | /// This module provides the following helper functions: 8 | /// - `set_cap`: Sets the maximum token supply. 9 | /// - `query_cap`: Returns the maximum token supply. 10 | /// - `check_cap`: Panics if minting a specified `amount` would exceed the cap. 11 | /// Should be used before calling `mint()`. 12 | mod storage; 13 | pub use self::storage::{check_cap, query_cap, set_cap, CAP_KEY}; 14 | #[cfg(test)] 15 | mod test; 16 | -------------------------------------------------------------------------------- /packages/accounts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellar-accounts" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = true 7 | version.workspace = true 8 | description = "Smart Account Contracts and Utilities." 9 | 10 | [lib] 11 | crate-type = ["lib", "cdylib"] 12 | doctest = false 13 | 14 | [dependencies] 15 | soroban-sdk = { workspace = true } 16 | serde = { workspace = true, features = ["derive"] } 17 | serde-json-core = { workspace = true } 18 | 19 | [dev-dependencies] 20 | soroban-sdk = { workspace = true, features = ["testutils"] } 21 | soroban-test-helpers = { workspace = true } 22 | stellar-event-assertion = { workspace = true } 23 | ed25519-dalek = { workspace = true } 24 | p256 = { workspace = true, features = ["ecdsa"] } 25 | hex-literal = { workspace = true } 26 | -------------------------------------------------------------------------------- /examples/fee-forwarder-permissionless/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Permissionless fee forwarder example. 2 | //! 3 | //! This example shows how to integrate the `stellar-fee-abstraction` helpers in 4 | //! a **permissionless** setup: 5 | //! 6 | //! - **Anyone** can call the `forward` entrypoint; there is no executor 7 | //! allowlist. 8 | //! - The **relayer** (the transaction submitter) receives the collected fee. 9 | //! 10 | //! In contrast, the permissioned example restricts `forward` to trusted 11 | //! executors and has the contract itself collect fees. This pattern is suitable 12 | //! for open environments where any party can become a relayer and be 13 | //! economically incentivized to forward user transactions. 14 | 15 | #![no_std] 16 | #![allow(clippy::too_many_arguments)] 17 | 18 | mod contract; 19 | 20 | #[cfg(test)] 21 | mod test; 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fixes #??? 7 | 8 | 9 | 10 | 11 | #### PR Checklist 12 | 13 | 14 | 15 | 16 | 17 | - [ ] Tests 18 | - [ ] Documentation 19 | 20 | -------------------------------------------------------------------------------- /packages/macros/src/pausable.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_macro_input, ItemFn}; 4 | 5 | use crate::parse_env_arg; 6 | 7 | pub fn generate_pause_check(item: TokenStream, check_fn: &str) -> TokenStream { 8 | let input_fn = parse_macro_input!(item as ItemFn); 9 | let env_arg = parse_env_arg(&input_fn); 10 | 11 | let fn_vis = &input_fn.vis; 12 | let fn_sig = &input_fn.sig; 13 | let fn_block = &input_fn.block; 14 | let fn_attrs = &input_fn.attrs; 15 | 16 | let check_ident = syn::Ident::new(check_fn, proc_macro2::Span::call_site()); 17 | let output = quote! { 18 | #(#fn_attrs)* // retain other macros 19 | #fn_vis #fn_sig { 20 | stellar_contract_utils::pausable::#check_ident(#env_arg); 21 | 22 | #fn_block 23 | } 24 | }; 25 | 26 | output.into() 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Create a report to help us improve 3 | title: "🐞 [Bug]: " 4 | labels: ["bug", "needs triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! Briefly describe the issue you're experiencing. Tell us what you were trying to do and what happened instead. Remember, this is not a place to ask for help debugging code. For that, we welcome you in the OpenZeppelin Community Forum: https://forum.openzeppelin.com/. 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: if you want, you can include screenshots as well 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: expected 19 | attributes: 20 | label: Expected behavior 21 | description: What should have happened instead? 22 | -------------------------------------------------------------------------------- /packages/tokens/src/rwa/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! # RWA Extensions Module 2 | //! 3 | //! This module contains optional extensions for RWA (Real World Assets) tokens 4 | //! that provide additional functionality beyond the core token implementation. 5 | //! 6 | //! ## Available Extensions 7 | //! 8 | //! - **Document Manager**: Provides document management capabilities following 9 | //! the ERC-1643 standard, allowing contracts to attach, update, and retrieve 10 | //! documents with associated metadata. 11 | //! 12 | //! ## Usage 13 | //! 14 | //! Extensions are designed to be optional and can be implemented selectively 15 | //! based on the specific requirements of your RWA token contract. 16 | //! 17 | //! ```rust 18 | //! use crate::{rwa::extensions::doc_manager::DocumentManager, token::Token}; 19 | //! 20 | //! #[contractimpl] 21 | //! impl DocumentManager for MyTokenContract { 22 | //! // Implementation of document management functions 23 | //! } 24 | //! ``` 25 | 26 | pub mod doc_manager; 27 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/sha256.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{panic_with_error, Bytes, BytesN, Env}; 2 | 3 | use crate::crypto::{error::CryptoError, hasher::Hasher}; 4 | 5 | /// Struct to store bytes that will be consumed by the sha256 [`Hasher`] 6 | /// implementation. 7 | pub struct Sha256 { 8 | state: Option, 9 | env: Env, 10 | } 11 | 12 | impl Hasher for Sha256 { 13 | type Output = BytesN<32>; 14 | 15 | fn new(e: &Env) -> Self { 16 | Sha256 { state: None, env: e.clone() } 17 | } 18 | 19 | fn update(&mut self, input: Bytes) { 20 | match &mut self.state { 21 | None => self.state = Some(input), 22 | Some(state) => state.append(&input), 23 | } 24 | } 25 | 26 | fn finalize(self) -> Self::Output { 27 | let data = self 28 | .state 29 | .unwrap_or_else(|| panic_with_error!(&self.env, CryptoError::HasherEmptyState)); 30 | self.env.crypto().sha256(&data).to_bytes() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/keccak.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{panic_with_error, Bytes, BytesN, Env}; 2 | 3 | use crate::crypto::{error::CryptoError, hasher::Hasher}; 4 | 5 | /// Struct to store bytes that will be consumed by the keccak256 [`Hasher`] 6 | /// implementation. 7 | pub struct Keccak256 { 8 | state: Option, 9 | env: Env, 10 | } 11 | 12 | impl Hasher for Keccak256 { 13 | type Output = BytesN<32>; 14 | 15 | fn new(e: &Env) -> Self { 16 | Keccak256 { state: None, env: e.clone() } 17 | } 18 | 19 | fn update(&mut self, input: Bytes) { 20 | match &mut self.state { 21 | None => self.state = Some(input), 22 | Some(state) => state.append(&input), 23 | } 24 | } 25 | 26 | fn finalize(self) -> Self::Output { 27 | let data = self 28 | .state 29 | .unwrap_or_else(|| panic_with_error!(&self.env, CryptoError::HasherEmptyState)); 30 | self.env.crypto().keccak256(&data).to_bytes() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/contract-utils/src/upgradeable/test.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contract, Env}; 2 | 3 | use crate::upgradeable::storage::{ 4 | can_complete_migration, complete_migration, enable_migration, ensure_can_complete_migration, 5 | }; 6 | 7 | #[contract] 8 | struct MockContract; 9 | 10 | #[test] 11 | fn upgrade_flow_works() { 12 | let e = Env::default(); 13 | let address = e.register(MockContract, ()); 14 | 15 | e.as_contract(&address, || { 16 | assert!(!can_complete_migration(&e)); 17 | 18 | enable_migration(&e); 19 | assert!(can_complete_migration(&e)); 20 | 21 | complete_migration(&e); 22 | assert!(!can_complete_migration(&e)); 23 | }); 24 | } 25 | 26 | #[test] 27 | #[should_panic(expected = "Error(Contract, #1100)")] 28 | fn upgrade_ensure_can_complete_migration_panics_if_not_migrating() { 29 | let e = Env::default(); 30 | let address = e.register(MockContract, ()); 31 | 32 | e.as_contract(&address, || { 33 | complete_migration(&e); 34 | ensure_can_complete_migration(&e); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OpenZeppelin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/ownable/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Ownable Example Contract. 2 | //! 3 | //! Demonstrates an example usage of `ownable` module by 4 | //! implementing `#[only_owner]` macro on a sensitive function. 5 | 6 | use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; 7 | use stellar_access::ownable::{set_owner, Ownable}; 8 | use stellar_macros::{default_impl, only_owner}; 9 | 10 | #[contracttype] 11 | pub enum DataKey { 12 | Owner, 13 | Counter, 14 | } 15 | 16 | #[contract] 17 | pub struct ExampleContract; 18 | 19 | #[contractimpl] 20 | impl ExampleContract { 21 | pub fn __constructor(e: &Env, owner: Address) { 22 | set_owner(e, &owner); 23 | e.storage().instance().set(&DataKey::Counter, &0); 24 | } 25 | 26 | #[only_owner] 27 | pub fn increment(e: &Env) -> i32 { 28 | let mut counter: i32 = 29 | e.storage().instance().get(&DataKey::Counter).expect("counter should be set"); 30 | 31 | counter += 1; 32 | 33 | e.storage().instance().set(&DataKey::Counter, &counter); 34 | 35 | counter 36 | } 37 | } 38 | 39 | #[default_impl] 40 | #[contractimpl] 41 | impl Ownable for ExampleContract {} 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Security vulnerabilities should be disclosed to the project maintainers through [Immunefi], or alternatively by email to security@openzeppelin.com. 4 | 5 | [Immunefi]: https://immunefi.com/bug-bounty/openzeppelin-stellar 6 | 7 | ## Bug Bounty 8 | 9 | Responsible disclosure of security vulnerabilities is rewarded through a bug bounty program on [Immunefi]. 10 | 11 | ## Legal 12 | 13 | Blockchain is a nascent technology and carries a high level of risk and uncertainty. OpenZeppelin makes certain software available under open source licenses, which disclaim all warranties in relation to the project and which limits the liability of OpenZeppelin. Subject to any particular licensing terms, your use of the project is governed by the terms found at [www.openzeppelin.com/tos](https://www.openzeppelin.com/tos) (the "Terms"). As set out in the Terms, you are solely responsible for any use of the project and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an ongoing duty by any contributor, including OpenZeppelin, to correct any issues or vulnerabilities or alert you to all or any of the risks of utilizing the project. 14 | -------------------------------------------------------------------------------- /packages/tokens/src/non_fungible/utils/sequential/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{contract, Env}; 4 | 5 | use crate::non_fungible::sequential::storage::{ 6 | increment_token_id, next_token_id, NFTSequentialStorageKey, 7 | }; 8 | 9 | #[contract] 10 | pub struct MockContract; 11 | 12 | #[test] 13 | fn sequential_token_id_counter_increments() { 14 | let e = Env::default(); 15 | let address = e.register(MockContract, ()); 16 | 17 | e.as_contract(&address, || { 18 | assert_eq!(next_token_id(&e), 0); 19 | 20 | let id1 = increment_token_id(&e, 10); 21 | assert_eq!(id1, 0); 22 | assert_eq!(next_token_id(&e), 10); 23 | 24 | let id2 = increment_token_id(&e, 5); 25 | assert_eq!(id2, 10); 26 | assert_eq!(next_token_id(&e), 15); 27 | }); 28 | } 29 | 30 | #[test] 31 | #[should_panic(expected = "Error(Contract, #206)")] 32 | fn sequential_increment_token_id_fails_on_overflow() { 33 | let e = Env::default(); 34 | let address = e.register(MockContract, ()); 35 | 36 | e.as_contract(&address, || { 37 | e.storage().instance().set(&NFTSequentialStorageKey::TokenIdCounter, &u32::MAX); 38 | let _ = increment_token_id(&e, 1); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /examples/rwa/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! RWA Example Contract. 2 | 3 | use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String}; 4 | use stellar_access::access_control::{self as access_control, AccessControl}; 5 | use stellar_macros::default_impl; 6 | use stellar_tokens::{ 7 | fungible::{Base, FungibleToken}, 8 | rwa::RWA, 9 | }; 10 | 11 | #[contract] 12 | pub struct ExampleContract; 13 | 14 | #[contractimpl] 15 | impl ExampleContract { 16 | pub fn __constructor( 17 | e: &Env, 18 | name: String, 19 | symbol: String, 20 | admin: Address, 21 | manager: Address, 22 | initial_supply: i128, 23 | ) { 24 | Base::set_metadata(e, 18, name, symbol); 25 | 26 | access_control::set_admin(e, &admin); 27 | 28 | // create a role "manager" and grant it to `manager` 29 | access_control::grant_role_no_auth(e, &admin, &manager, &symbol_short!("manager")); 30 | 31 | // Mint initial supply to the admin 32 | RWA::mint(e, &admin, initial_supply); 33 | } 34 | } 35 | 36 | #[default_impl] 37 | #[contractimpl] 38 | impl FungibleToken for ExampleContract { 39 | type ContractType = RWA; 40 | } 41 | 42 | #[default_impl] 43 | #[contractimpl] 44 | impl AccessControl for ExampleContract {} 45 | -------------------------------------------------------------------------------- /packages/accounts/src/verifiers/utils/extract_from_bytes.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Bound, RangeBounds}; 2 | 3 | use soroban_sdk::{Bytes, BytesN, Env}; 4 | 5 | /// Extracts and returns a fixed-size array as `Option>` from a 6 | /// `Bytes` object or `None` if range is out of bounds or N is too small to fit 7 | /// the extracted slice. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `e` - The Soroban environment. 12 | /// * `data` - The Bytes object to extract from. 13 | /// * `r` - The range of bytes to extract. 14 | pub fn extract_from_bytes( 15 | e: &Env, 16 | data: &Bytes, 17 | r: impl RangeBounds, 18 | ) -> Option> { 19 | let start = match r.start_bound().cloned() { 20 | Bound::Unbounded => 0, 21 | Bound::Included(n) | Bound::Excluded(n) => n, 22 | }; 23 | let end = match r.end_bound().cloned() { 24 | Bound::Unbounded => data.len(), 25 | Bound::Included(n) => n + 1, 26 | Bound::Excluded(n) => n, 27 | }; 28 | if end > data.len() || end - start != N as u32 { 29 | return None; 30 | } 31 | 32 | let buf = data.slice(r).to_buffer::(); 33 | let mut items = [0u8; N]; 34 | items.copy_from_slice(buf.as_slice()); 35 | 36 | Some(BytesN::::from_array(e, &items)) 37 | } 38 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/hasher.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{Bytes, Env}; 2 | 3 | /// A trait for hashing an arbitrary stream of bytes. 4 | /// 5 | /// Instances of `Hasher` usually represent state that is changed while hashing 6 | /// data. 7 | /// 8 | /// `Hasher` provides a fairly basic interface for retrieving the generated hash 9 | /// (with [`Hasher::finalize`]), and absorbing an arbitrary number of bytes 10 | /// (with [`Hasher::update`]). Most of the time, [`Hasher`] instances are used 11 | /// in conjunction with the [`crate::crypto::hashable::Hashable`] trait. 12 | pub trait Hasher { 13 | type Output; 14 | 15 | /// Creates a new [`Hasher`] instance. 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `e` - Access to the Soroban environment. 20 | fn new(e: &Env) -> Self; 21 | 22 | /// Absorbs additional input. Can be called multiple times. 23 | /// 24 | /// # Arguments 25 | /// 26 | /// * `input` - Bytes to be added to the internal state. 27 | fn update(&mut self, input: Bytes); 28 | 29 | /// Outputs the hashing algorithm state. 30 | /// 31 | /// # Errors 32 | /// 33 | /// * [`crate::crypto::error::CryptoError::HasherEmptyState`] - When the 34 | /// state is empty. 35 | fn finalize(self) -> Self::Output; 36 | } 37 | -------------------------------------------------------------------------------- /examples/nft-sequential-minting/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Non-Fungible Vanilla Example Contract. 2 | //! 3 | //! Demonstrates an example usage of the NFT default base implementation. 4 | 5 | use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; 6 | use stellar_macros::default_impl; 7 | use stellar_tokens::non_fungible::{burnable::NonFungibleBurnable, Base, NonFungibleToken}; 8 | 9 | #[contracttype] 10 | pub enum DataKey { 11 | Owner, 12 | } 13 | 14 | #[contract] 15 | pub struct ExampleContract; 16 | 17 | #[contractimpl] 18 | impl ExampleContract { 19 | pub fn __constructor(e: &Env, uri: String, name: String, symbol: String, owner: Address) { 20 | e.storage().instance().set(&DataKey::Owner, &owner); 21 | Base::set_metadata(e, uri, name, symbol); 22 | } 23 | 24 | pub fn mint(e: &Env, to: Address) -> u32 { 25 | let owner: Address = 26 | e.storage().instance().get(&DataKey::Owner).expect("owner should be set"); 27 | owner.require_auth(); 28 | Base::sequential_mint(e, &to) 29 | } 30 | } 31 | 32 | #[default_impl] 33 | #[contractimpl] 34 | impl NonFungibleToken for ExampleContract { 35 | type ContractType = Base; 36 | } 37 | 38 | #[default_impl] 39 | #[contractimpl] 40 | impl NonFungibleBurnable for ExampleContract {} 41 | -------------------------------------------------------------------------------- /examples/nft-sequential-minting/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{testutils::Address as _, Address, Env, String}; 4 | 5 | use crate::contract::{ExampleContract, ExampleContractClient}; 6 | 7 | fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { 8 | let uri = String::from_str(e, "www.mytoken.com"); 9 | let name = String::from_str(e, "My Token"); 10 | let symbol = String::from_str(e, "TKN"); 11 | let address = e.register(ExampleContract, (uri, name, symbol, owner)); 12 | ExampleContractClient::new(e, &address) 13 | } 14 | 15 | #[test] 16 | fn transfer_works() { 17 | let e = Env::default(); 18 | let owner = Address::generate(&e); 19 | let recipient = Address::generate(&e); 20 | let client = create_client(&e, &owner); 21 | 22 | e.mock_all_auths(); 23 | client.mint(&owner); 24 | client.transfer(&owner, &recipient, &0); 25 | assert_eq!(client.balance(&owner), 0); 26 | assert_eq!(client.balance(&recipient), 1); 27 | } 28 | 29 | #[test] 30 | fn burn_works() { 31 | let e = Env::default(); 32 | let owner = Address::generate(&e); 33 | let client = create_client(&e, &owner); 34 | 35 | e.mock_all_auths(); 36 | client.mint(&owner); 37 | client.burn(&owner, &0); 38 | assert_eq!(client.balance(&owner), 0); 39 | } 40 | -------------------------------------------------------------------------------- /examples/upgradeable/v1/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use contract_v2::Data; 4 | use soroban_sdk::{testutils::Address as _, Address, BytesN, Env}; 5 | 6 | use crate::contract::{ExampleContract, ExampleContractClient}; 7 | 8 | mod contract_v2 { 9 | use crate::test::MigrationData; 10 | 11 | soroban_sdk::contractimport!(file = "../testdata/upgradeable_v2_example.wasm"); 12 | } 13 | 14 | fn install_new_wasm(e: &Env) -> BytesN<32> { 15 | e.deployer().upload_contract_wasm(contract_v2::WASM) 16 | } 17 | 18 | type MigrationData = Data; 19 | 20 | #[test] 21 | fn test_upgrade() { 22 | let env = Env::default(); 23 | env.mock_all_auths(); 24 | 25 | let admin = Address::generate(&env); 26 | // deploy v1 27 | let address = env.register(ExampleContract, (&admin,)); 28 | 29 | let client_v1 = ExampleContractClient::new(&env, &address); 30 | 31 | // install the new wasm and upgrade 32 | let new_wasm_hash = install_new_wasm(&env); 33 | client_v1.upgrade(&new_wasm_hash, &admin); 34 | 35 | // init the upgraded client and migrate 36 | let client_v2 = contract_v2::Client::new(&env, &address); 37 | client_v2.migrate(&Data { num1: 12, num2: 34 }, &admin); 38 | 39 | // ensure migrate can't be invoked again 40 | assert!(client_v2.try_migrate(&Data { num1: 12, num2: 34 }, &admin).is_err()); 41 | } 42 | -------------------------------------------------------------------------------- /examples/upgradeable/v1/src/contract.rs: -------------------------------------------------------------------------------- 1 | /// A basic contract that demonstrates the usage of the `Upgradeable` derive 2 | /// macro. It only implements `UpgradeableInternal` and the derive macro do the 3 | /// rest of the job. The goal is to upgrade this "v1" contract with the contract 4 | /// in "v2". 5 | use soroban_sdk::{ 6 | contract, contracterror, contractimpl, panic_with_error, symbol_short, Address, Env, Symbol, 7 | }; 8 | use stellar_contract_utils::upgradeable::UpgradeableInternal; 9 | use stellar_macros::Upgradeable; 10 | 11 | pub const OWNER: Symbol = symbol_short!("OWNER"); 12 | 13 | #[contracterror] 14 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 15 | #[repr(u32)] 16 | pub enum ExampleContractError { 17 | Unauthorized = 1, 18 | } 19 | 20 | #[derive(Upgradeable)] 21 | #[contract] 22 | pub struct ExampleContract; 23 | 24 | #[contractimpl] 25 | impl ExampleContract { 26 | pub fn __constructor(e: &Env, admin: Address) { 27 | e.storage().instance().set(&OWNER, &admin); 28 | } 29 | } 30 | 31 | impl UpgradeableInternal for ExampleContract { 32 | fn _require_auth(e: &Env, operator: &Address) { 33 | operator.require_auth(); 34 | let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap(); 35 | if *operator != owner { 36 | panic_with_error!(e, ExampleContractError::Unauthorized) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/tokens/src/non_fungible/utils/sequential/storage.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contracttype, panic_with_error, Env}; 2 | 3 | use crate::non_fungible::NonFungibleTokenError; 4 | 5 | #[contracttype] 6 | pub enum NFTSequentialStorageKey { 7 | TokenIdCounter, 8 | } 9 | 10 | /// Get the current token counter value to determine the next token_id. 11 | /// The returned value is the next available token_id. 12 | /// 13 | /// # Arguments 14 | /// 15 | /// * `e` - Access to the Soroban environment. 16 | pub fn next_token_id(e: &Env) -> u32 { 17 | e.storage().instance().get(&NFTSequentialStorageKey::TokenIdCounter).unwrap_or(0) 18 | } 19 | 20 | /// Return the next free token ID, then increment the counter. 21 | /// 22 | /// # Arguments 23 | /// 24 | /// * `e` - Access to the Soroban environment. 25 | /// * `amount` - The number by which the counter is incremented. 26 | /// 27 | /// # Errors 28 | /// 29 | /// * [`crate::non_fungible::NonFungibleTokenError::TokenIDsAreDepleted`] - When 30 | /// all the available `token_id`s are consumed for this smart contract. 31 | pub fn increment_token_id(e: &Env, amount: u32) -> u32 { 32 | let current_id = next_token_id(e); 33 | let Some(next_id) = current_id.checked_add(amount) else { 34 | panic_with_error!(e, NonFungibleTokenError::TokenIDsAreDepleted); 35 | }; 36 | e.storage().instance().set(&NFTSequentialStorageKey::TokenIdCounter, &next_id); 37 | current_id 38 | } 39 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/ed25519-verifier/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! # Ed25519 Verifier Contract 2 | //! 3 | //! A reusable verifier contract for Ed25519 signature verification. This 4 | //! contract can be deployed once and used by multiple smart accounts across the 5 | //! network for delegated signature verification. Provides cryptographic 6 | //! verification for Ed25519 signatures against message hashes and public keys. 7 | use soroban_sdk::{contract, contractimpl, Bytes, BytesN, Env}; 8 | use stellar_accounts::verifiers::{ed25519, Verifier}; 9 | 10 | #[contract] 11 | pub struct Ed25519VerifierContract; 12 | 13 | #[contractimpl] 14 | impl Verifier for Ed25519VerifierContract { 15 | type KeyData = BytesN<32>; 16 | type SigData = BytesN<64>; 17 | 18 | /// Verify an Ed25519 signature against a message and public key. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `signature_payload` - The message hash that was signed 23 | /// * `key_data` - The 32-byte Ed25519 public key 24 | /// * `sig_data` - The 64-byte Ed25519 signature 25 | /// 26 | /// # Returns 27 | /// 28 | /// * `true` if the signature is valid 29 | /// * `false` otherwise 30 | fn verify( 31 | e: &Env, 32 | signature_payload: Bytes, 33 | key_data: BytesN<32>, 34 | sig_data: BytesN<64>, 35 | ) -> bool { 36 | ed25519::verify(e, &signature_payload, &key_data, &sig_data) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/contract-utils/src/upgradeable/storage.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{panic_with_error, symbol_short, Env, Symbol}; 2 | 3 | use crate::upgradeable::UpgradeableError; 4 | 5 | pub const MIGRATING: Symbol = symbol_short!("MIGRATING"); 6 | 7 | /// Sets the `MIGRATING` state to `true`, enabling migration process. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `e` - The Soroban environment. 12 | pub fn enable_migration(e: &Env) { 13 | e.storage().instance().set(&MIGRATING, &true); 14 | } 15 | 16 | /// Returns `true` if completing migration is allowed. 17 | /// 18 | /// # Arguments 19 | /// 20 | /// * `e` - The Soroban environment. 21 | pub fn can_complete_migration(e: &Env) -> bool { 22 | e.storage().instance().get::<_, bool>(&MIGRATING).unwrap_or(false) 23 | } 24 | 25 | /// Sets the `MIGRATING` state to `false`, completing the migration process. 26 | /// 27 | /// # Arguments 28 | /// 29 | /// * `e` - The Soroban environment. 30 | pub fn complete_migration(e: &Env) { 31 | e.storage().instance().set(&MIGRATING, &false); 32 | } 33 | 34 | /// Ensures that completing migration is allowed, otherwise panics. 35 | /// 36 | /// # Arguments 37 | /// 38 | /// * `e` - The Soroban environment. 39 | /// 40 | /// # Errors 41 | /// 42 | /// * [`UpgradeableError::MigrationNotAllowed`] - If `MIGRATING` is `false`. 43 | pub fn ensure_can_complete_migration(e: &Env) { 44 | if !can_complete_migration(e) { 45 | panic_with_error!(e, UpgradeableError::MigrationNotAllowed) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/upgradeable/upgrader/src/contract.rs: -------------------------------------------------------------------------------- 1 | /// Helper contract to perform upgrade+migrate in a single transaction. 2 | use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Symbol, Val}; 3 | use stellar_access::ownable; 4 | use stellar_contract_utils::upgradeable::UpgradeableClient; 5 | use stellar_macros::only_owner; 6 | 7 | pub const MIGRATE: Symbol = symbol_short!("migrate"); 8 | 9 | #[contract] 10 | pub struct Upgrader; 11 | 12 | #[contractimpl] 13 | impl Upgrader { 14 | pub fn __constructor(e: &Env, owner: Address) { 15 | ownable::set_owner(e, &owner); 16 | } 17 | 18 | #[only_owner] 19 | pub fn upgrade(e: &Env, contract_address: Address, operator: Address, wasm_hash: BytesN<32>) { 20 | let contract_client = UpgradeableClient::new(e, &contract_address); 21 | 22 | contract_client.upgrade(&wasm_hash, &operator); 23 | } 24 | 25 | #[only_owner] 26 | pub fn upgrade_and_migrate( 27 | e: &Env, 28 | contract_address: Address, 29 | operator: Address, 30 | wasm_hash: BytesN<32>, 31 | migration_data: soroban_sdk::Vec, 32 | ) { 33 | let contract_client = UpgradeableClient::new(e, &contract_address); 34 | 35 | contract_client.upgrade(&wasm_hash, &operator); 36 | // The types of the arguments to the migrate function are unknown to this 37 | // contract, so we need to call it with invoke_contract. 38 | e.invoke_contract::<()>(&contract_address, &MIGRATE, migration_data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/proptest-regressions/merkle.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 5abce70720b91d83efed4599d5c38af4837571b4a929beea2dc0d1585d2e0748 # shrinks to (proof, root, leaf) = ([BytesN<32>(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)], BytesN<32>(173, 50, 40, 182, 118, 247, 211, 205, 66, 132, 165, 68, 63, 23, 241, 150, 43, 54, 228, 145, 179, 10, 64, 178, 64, 88, 73, 229, 151, 186, 95, 181), BytesN<32>(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), tamper_idx = 0 8 | cc 87617f85155d9d73f49a409083e3bc7cd3e4c3ba84f68e42bd189b939d7d50ac # shrinks to (proof, root, leaf, _index) = (Vec(), BytesN<32>(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), BytesN<32>(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 0), wrong_idx = 0 9 | cc 0a92f6c8c053f5c34a92985b0d3386ee6e85d39557d876b11910daf6742fe629 # shrinks to (proof, root, leaf, index) = (Vec(), BytesN<32>(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), BytesN<32>(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 0) 10 | -------------------------------------------------------------------------------- /packages/accounts/src/verifiers/ed25519.rs: -------------------------------------------------------------------------------- 1 | /// Contract for verifying Ed25519 digital signatures. 2 | /// 3 | /// This module provides Ed25519 signature verification functionality for 4 | /// Stellar smart contracts. Ed25519 is a high-performance public-key signature 5 | /// system that provides strong security guarantees. 6 | use soroban_sdk::{Bytes, BytesN, Env}; 7 | 8 | /// Verifies an Ed25519 digital signature. 9 | /// 10 | /// This function performs Ed25519 signature verification using the Soroban 11 | /// cryptographic primitives. It extracts the public key from the key data, 12 | /// parses the signature from XDR format, and verifies the signature against 13 | /// the provided payload. 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `e` - Access to the Soroban environment. 18 | /// * `signature_payload` - The data that was signed. 19 | /// * `public_key` - The public key (32 bytes). 20 | /// * `signature` - The signature data (64 bytes). 21 | /// 22 | /// # Returns 23 | /// 24 | /// Returns `true` if the signature is valid for the given payload and public 25 | /// key. 26 | /// 27 | /// # Panics 28 | /// 29 | /// The function will panic if the cryptographic verification fails due to an 30 | /// invalid signature, which is the expected behavior for signature verification 31 | /// in Soroban contracts. 32 | pub fn verify( 33 | e: &Env, 34 | signature_payload: &Bytes, 35 | public_key: &BytesN<32>, 36 | signature: &BytesN<64>, 37 | ) -> bool { 38 | e.crypto().ed25519_verify(public_key, signature_payload, signature); 39 | 40 | true 41 | } 42 | -------------------------------------------------------------------------------- /examples/nft-enumerable/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{testutils::Address as _, Address, Env, String}; 4 | 5 | use crate::contract::{ExampleContract, ExampleContractClient}; 6 | 7 | fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { 8 | let uri = String::from_str(e, "www.mytoken.com"); 9 | let name = String::from_str(e, "My Token"); 10 | let symbol = String::from_str(e, "TKN"); 11 | let address = e.register(ExampleContract, (uri, name, symbol, owner)); 12 | ExampleContractClient::new(e, &address) 13 | } 14 | 15 | #[test] 16 | fn enumerable_transfer_override_works() { 17 | let e = Env::default(); 18 | 19 | let owner = Address::generate(&e); 20 | 21 | let recipient = Address::generate(&e); 22 | 23 | let client = create_client(&e, &owner); 24 | 25 | e.mock_all_auths(); 26 | client.mint(&owner); 27 | client.transfer(&owner, &recipient, &0); 28 | assert_eq!(client.balance(&owner), 0); 29 | assert_eq!(client.balance(&recipient), 1); 30 | assert_eq!(client.get_owner_token_id(&recipient, &0), 0); 31 | } 32 | 33 | #[test] 34 | fn enumerable_burn_works() { 35 | let e = Env::default(); 36 | let owner = Address::generate(&e); 37 | let client = create_client(&e, &owner); 38 | e.mock_all_auths(); 39 | client.mint(&owner); 40 | client.burn(&owner, &0); 41 | assert_eq!(client.balance(&owner), 0); 42 | client.mint(&owner); 43 | assert_eq!(client.balance(&owner), 1); 44 | assert_eq!(client.get_owner_token_id(&owner, &0), 1); 45 | } 46 | -------------------------------------------------------------------------------- /examples/upgradeable/upgrader/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use contract_v2::Data; 4 | use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, TryIntoVal}; 5 | 6 | use crate::contract::{Upgrader, UpgraderClient}; 7 | 8 | mod contract_v1 { 9 | soroban_sdk::contractimport!(file = "../testdata/upgradeable_v1_example.wasm"); 10 | } 11 | 12 | mod contract_v2 { 13 | use crate::test::MigrationData; 14 | 15 | soroban_sdk::contractimport!(file = "../testdata/upgradeable_v2_example.wasm"); 16 | } 17 | 18 | fn install_new_wasm(e: &Env) -> BytesN<32> { 19 | e.deployer().upload_contract_wasm(contract_v2::WASM) 20 | } 21 | 22 | type MigrationData = Data; 23 | 24 | #[test] 25 | fn test_upgrade_with_upgrader() { 26 | let e = Env::default(); 27 | e.mock_all_auths_allowing_non_root_auth(); 28 | 29 | let admin = Address::generate(&e); 30 | let contract_id = e.register(contract_v1::WASM, (&admin,)); 31 | 32 | let upgrader = e.register(Upgrader, (&admin,)); 33 | let upgrader_client = UpgraderClient::new(&e, &upgrader); 34 | 35 | let new_wasm_hash = install_new_wasm(&e); 36 | let data = Data { num1: 12, num2: 34 }; 37 | 38 | upgrader_client.upgrade_and_migrate( 39 | &contract_id, 40 | &admin, 41 | &new_wasm_hash, 42 | &soroban_sdk::vec![&e, data.try_into_val(&e).unwrap(), admin.try_into_val(&e).unwrap()], 43 | ); 44 | 45 | let client_v2 = contract_v2::Client::new(&e, &contract_id); 46 | 47 | assert!(client_v2.try_migrate(&Data { num1: 12, num2: 34 }, &admin).is_err()); 48 | } 49 | -------------------------------------------------------------------------------- /examples/ownable/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{ 4 | testutils::{Address as _, MockAuth, MockAuthInvoke}, 5 | Address, Env, IntoVal, 6 | }; 7 | 8 | use crate::contract::{ExampleContract, ExampleContractClient}; 9 | 10 | fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { 11 | let address = e.register(ExampleContract, (owner,)); 12 | ExampleContractClient::new(e, &address) 13 | } 14 | 15 | #[test] 16 | fn owner_can_increment() { 17 | let e = Env::default(); 18 | let owner = Address::generate(&e); 19 | let client = create_client(&e, &owner); 20 | 21 | e.mock_auths(&[MockAuth { 22 | address: &owner, 23 | invoke: &MockAuthInvoke { 24 | contract: &client.address, 25 | fn_name: "increment", 26 | args: ().into_val(&e), 27 | sub_invokes: &[], 28 | }, 29 | }]); 30 | 31 | assert_eq!(client.increment(), 1); 32 | } 33 | 34 | #[test] 35 | #[should_panic(expected = "HostError: Error(Auth, InvalidAction)")] 36 | fn non_owner_cannot_increment() { 37 | let e = Env::default(); 38 | let owner = Address::generate(&e); 39 | let non_owner = Address::generate(&e); 40 | let client = create_client(&e, &owner); 41 | 42 | e.mock_auths(&[MockAuth { 43 | address: &non_owner, 44 | invoke: &MockAuthInvoke { 45 | contract: &client.address, 46 | fn_name: "increment", 47 | args: ().into_val(&e), 48 | sub_invokes: &[], 49 | }, 50 | }]); 51 | 52 | client.increment(); 53 | } 54 | -------------------------------------------------------------------------------- /examples/nft-enumerable/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Non-Fungible Enumerable Example Contract. 2 | //! 3 | //! Demonstrates an example usage of the Enumerable extension, allowing for 4 | //! enumeration of all the token IDs in the contract as well as all the token 5 | //! IDs owned by each account. 6 | 7 | use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; 8 | use stellar_macros::default_impl; 9 | use stellar_tokens::non_fungible::{ 10 | burnable::NonFungibleBurnable, 11 | enumerable::{Enumerable, NonFungibleEnumerable}, 12 | Base, NonFungibleToken, 13 | }; 14 | 15 | #[contracttype] 16 | pub enum DataKey { 17 | Owner, 18 | } 19 | 20 | #[contract] 21 | pub struct ExampleContract; 22 | 23 | #[contractimpl] 24 | impl ExampleContract { 25 | pub fn __constructor(e: &Env, uri: String, name: String, symbol: String, owner: Address) { 26 | e.storage().instance().set(&DataKey::Owner, &owner); 27 | Base::set_metadata(e, uri, name, symbol); 28 | } 29 | 30 | pub fn mint(e: &Env, to: Address) -> u32 { 31 | let owner: Address = 32 | e.storage().instance().get(&DataKey::Owner).expect("owner should be set"); 33 | owner.require_auth(); 34 | Enumerable::sequential_mint(e, &to) 35 | } 36 | } 37 | 38 | #[default_impl] 39 | #[contractimpl] 40 | impl NonFungibleToken for ExampleContract { 41 | type ContractType = Enumerable; 42 | } 43 | 44 | #[default_impl] 45 | #[contractimpl] 46 | impl NonFungibleEnumerable for ExampleContract {} 47 | 48 | #[default_impl] 49 | #[contractimpl] 50 | impl NonFungibleBurnable for ExampleContract {} 51 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/utils/sac_admin_generic/README.md: -------------------------------------------------------------------------------- 1 | An example flow when a `SACAdminGeneric` contract is set as a new administrator for a Stellar Classic Asset (SAC). 2 | 3 | ```mermaid 4 | sequenceDiagram 5 | actor Issuer 6 | actor Minter 7 | participant AdminGeneric as AdminGeneric
Contract 8 | participant AdminSigner 9 | participant SAC as Stellar
Asset
Contract 10 | actor User as Asset Holder 11 | 12 | Note over Issuer,AdminGeneric: 1. Asset Deployment 13 | Issuer->>SAC: Deploy Stellar Asset Contract 14 | Note over SAC: Issuer is the initial admin 15 | 16 | Note over Issuer,AdminGeneric: 2. AdminGeneric Deployment and Setup 17 | Issuer->>AdminGeneric: Deploy AdminGeneric with __constructor(SAC, AdminSigner, Minter) 18 | Note over AdminGeneric: Constructor stores
SAC address,
AdminSigner address,
and Minter address 19 | 20 | Note over Issuer,AdminGeneric: 3. Admin Change 21 | Issuer->>SAC: set_admin(AdminGeneric) 22 | SAC-->>Issuer: Success (AdminGeneric is now admin) 23 | 24 | Note over Minter,AdminGeneric: 4. Admin Functions
via AdminGeneric 25 | Minter->>SAC: mint(User, 1000) 26 | activate SAC 27 | critical Policies checked and signers verified 28 | SAC->>+AdminGeneric: require_auth() 29 | AdminGeneric->>AdminSigner: require_auth() 30 | AdminSigner-->>AdminGeneric: Authorized 31 | AdminGeneric-->>-SAC: Authorized
(automatically because it was the invoker) 32 | end 33 | SAC-->>User: Receive 1000 tokens 34 | SAC-->>-Minter: Success 35 | 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /packages/test-utils/default-impl-macro-test/tests/ownable.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{ 2 | contract, contractimpl, contracttype, testutils::Address as _, Address, Env, String, 3 | }; 4 | use stellar_access::ownable::{set_owner, Ownable}; 5 | use stellar_macros::{default_impl, only_owner}; 6 | use stellar_tokens::fungible::{Base, FungibleToken}; 7 | 8 | #[contracttype] 9 | pub enum DataKey { 10 | Owner, 11 | } 12 | 13 | #[contract] 14 | pub struct ExampleContract; 15 | 16 | #[contractimpl] 17 | impl ExampleContract { 18 | pub fn __constructor(e: &Env, name: String, symbol: String, owner: Address) { 19 | set_owner(e, &owner); 20 | Base::set_metadata(e, 7, name, symbol); 21 | } 22 | 23 | #[only_owner] 24 | pub fn mint(e: &Env, to: Address, amount: i128) { 25 | Base::mint(e, &to, amount); 26 | } 27 | } 28 | 29 | #[default_impl] 30 | #[contractimpl] 31 | impl FungibleToken for ExampleContract { 32 | type ContractType = Base; 33 | } 34 | 35 | #[default_impl] 36 | #[contractimpl] 37 | impl Ownable for ExampleContract {} 38 | 39 | fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { 40 | let name = String::from_str(e, "My Token"); 41 | let symbol = String::from_str(e, "TKN"); 42 | let address = e.register(ExampleContract, (name, symbol, owner)); 43 | ExampleContractClient::new(e, &address) 44 | } 45 | 46 | #[test] 47 | fn default_impl_ownable() { 48 | let e = Env::default(); 49 | let owner = Address::generate(&e); 50 | let client = create_client(&e, &owner); 51 | 52 | e.mock_all_auths(); 53 | 54 | client.mint(&owner, &100); 55 | } 56 | -------------------------------------------------------------------------------- /examples/merkle-voting/src/test.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{testutils::Address as _, xdr::ToXdr, Address, BytesN, Env, Vec}; 2 | use stellar_contract_utils::crypto::{ 3 | hashable::commutative_hash_pair, hasher::Hasher, sha256::Sha256, 4 | }; 5 | 6 | use crate::contract::{MerkleVoting, MerkleVotingClient, VoteData}; 7 | 8 | fn hash_vote(e: &Env, data: &VoteData) -> BytesN<32> { 9 | let mut hasher = Sha256::new(e); 10 | hasher.update(data.clone().to_xdr(e)); 11 | hasher.finalize() 12 | } 13 | 14 | #[test] 15 | fn test_merkle_voting() { 16 | let e = Env::default(); 17 | 18 | let voter1 = Address::generate(&e); 19 | let voter2 = Address::generate(&e); 20 | 21 | let vote1 = VoteData { index: 0, account: voter1.clone(), voting_power: 100 }; 22 | let vote2 = VoteData { index: 1, account: voter2.clone(), voting_power: 50 }; 23 | 24 | let leaf1 = hash_vote(&e, &vote1); 25 | let leaf2 = hash_vote(&e, &vote2); 26 | 27 | let root = commutative_hash_pair(&leaf1, &leaf2, Sha256::new(&e)); 28 | 29 | let contract_id = e.register(MerkleVoting, (root,)); 30 | let client = MerkleVotingClient::new(&e, &contract_id); 31 | 32 | let proof1 = Vec::from_array(&e, [leaf2.clone()]); 33 | client.vote(&vote1, &proof1, &true); 34 | 35 | let proof2 = Vec::from_array(&e, [leaf1.clone()]); 36 | client.vote(&vote2, &proof2, &false); 37 | 38 | // Verify votes were recorded 39 | assert!(client.has_voted(&0)); 40 | assert!(client.has_voted(&1)); 41 | 42 | // Check vote results 43 | let (votes_pro, votes_against) = client.get_vote_results(); 44 | assert_eq!(votes_pro, 100); 45 | assert_eq!(votes_against, 50); 46 | } 47 | -------------------------------------------------------------------------------- /examples/upgradeable/v2/src/contract.rs: -------------------------------------------------------------------------------- 1 | /// The contract in "v1" needs to be upgraded with this one. We are 2 | /// demonstrating the usage of the `UpgradeableMigratable` macro, because this 3 | /// time we want to do a migration after the upgrade. That's why we derive 4 | /// `UpgradeableMigratable`. For it to work, we implement 5 | /// `UpgradeableMigratableInternal` with the custom migration logic. 6 | use soroban_sdk::{ 7 | contract, contracterror, contracttype, panic_with_error, symbol_short, Address, Env, Symbol, 8 | }; 9 | use stellar_contract_utils::upgradeable::UpgradeableMigratableInternal; 10 | use stellar_macros::UpgradeableMigratable; 11 | 12 | pub const DATA_KEY: Symbol = symbol_short!("DATA_KEY"); 13 | pub const OWNER: Symbol = symbol_short!("OWNER"); 14 | 15 | #[contracterror] 16 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 17 | #[repr(u32)] 18 | pub enum ExampleContractError { 19 | Unauthorized = 1, 20 | } 21 | 22 | #[contracttype] 23 | pub struct Data { 24 | pub num1: u32, 25 | pub num2: u32, 26 | } 27 | 28 | #[derive(UpgradeableMigratable)] 29 | #[contract] 30 | pub struct ExampleContract; 31 | 32 | impl UpgradeableMigratableInternal for ExampleContract { 33 | type MigrationData = Data; 34 | 35 | fn _require_auth(e: &Env, operator: &Address) { 36 | operator.require_auth(); 37 | let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap(); 38 | if *operator != owner { 39 | panic_with_error!(e, ExampleContractError::Unauthorized) 40 | } 41 | } 42 | 43 | fn _migrate(e: &Env, data: &Self::MigrationData) { 44 | e.storage().instance().set(&DATA_KEY, data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/test-utils/default-impl-macro-test/tests/access_control.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{ 2 | contract, contractimpl, contracttype, testutils::Address as _, Address, Env, String, Symbol, 3 | }; 4 | use stellar_access::access_control::{set_admin, AccessControl}; 5 | use stellar_macros::{default_impl, has_role}; 6 | use stellar_tokens::fungible::{Base, FungibleToken}; 7 | 8 | #[contracttype] 9 | pub enum DataKey { 10 | Admin, 11 | } 12 | 13 | #[contract] 14 | pub struct ExampleContract; 15 | 16 | #[contractimpl] 17 | impl ExampleContract { 18 | pub fn __constructor(e: &Env, name: String, symbol: String, owner: Address) { 19 | set_admin(e, &owner); 20 | Base::set_metadata(e, 7, name, symbol); 21 | } 22 | 23 | #[has_role(caller, "minter")] 24 | pub fn mint(e: &Env, to: Address, amount: i128, caller: Address) { 25 | Base::mint(e, &to, amount); 26 | } 27 | } 28 | 29 | #[default_impl] 30 | #[contractimpl] 31 | impl FungibleToken for ExampleContract { 32 | type ContractType = Base; 33 | } 34 | 35 | #[default_impl] 36 | #[contractimpl] 37 | impl AccessControl for ExampleContract {} 38 | 39 | fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { 40 | let name = String::from_str(e, "My Token"); 41 | let symbol = String::from_str(e, "TKN"); 42 | let address = e.register(ExampleContract, (name, symbol, owner)); 43 | ExampleContractClient::new(e, &address) 44 | } 45 | 46 | #[test] 47 | fn default_impl_fungible_grant_role() { 48 | let e = Env::default(); 49 | let owner = Address::generate(&e); 50 | let client = create_client(&e, &owner); 51 | 52 | e.mock_all_auths(); 53 | 54 | client.grant_role(&owner, &Symbol::new(&e, "minter"), &owner); 55 | } 56 | -------------------------------------------------------------------------------- /packages/test-utils/default-impl-macro-test/tests/non_fungible_enumerable.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env, String}; 2 | use stellar_macros::default_impl; 3 | use stellar_tokens::non_fungible::{ 4 | enumerable::{Enumerable, NonFungibleEnumerable}, 5 | Base, NonFungibleToken, 6 | }; 7 | 8 | #[contract] 9 | pub struct ExampleContract; 10 | 11 | #[contractimpl] 12 | impl ExampleContract { 13 | pub fn __constructor(e: &Env, uri: String, name: String, symbol: String) { 14 | Base::set_metadata(e, uri, name, symbol); 15 | } 16 | 17 | pub fn mint(e: &Env, to: Address, token_id: u32) { 18 | Enumerable::non_sequential_mint(e, &to, token_id); 19 | } 20 | } 21 | 22 | #[default_impl] 23 | #[contractimpl] 24 | impl NonFungibleToken for ExampleContract { 25 | type ContractType = Enumerable; 26 | } 27 | 28 | #[default_impl] 29 | #[contractimpl] 30 | impl NonFungibleEnumerable for ExampleContract {} 31 | 32 | fn create_client<'a>(e: &Env) -> ExampleContractClient<'a> { 33 | let uri = String::from_str(e, "www.mytoken.com"); 34 | let name = String::from_str(e, "My Token"); 35 | let symbol = String::from_str(e, "TKN"); 36 | let address = e.register(ExampleContract, (uri, name, symbol)); 37 | ExampleContractClient::new(e, &address) 38 | } 39 | 40 | #[test] 41 | fn default_impl_enumerable_total_supply() { 42 | let e = Env::default(); 43 | 44 | let owner = Address::generate(&e); 45 | 46 | let recipient = Address::generate(&e); 47 | 48 | let client = create_client(&e); 49 | 50 | e.mock_all_auths(); 51 | client.mint(&owner, &10); 52 | client.transfer(&owner, &recipient, &10); 53 | assert_eq!(client.total_supply(), 1); 54 | } 55 | -------------------------------------------------------------------------------- /examples/sac-admin-wrapper/src/contract.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env}; 2 | use stellar_access::access_control::{self as access_control, AccessControl}; 3 | use stellar_macros::{default_impl, only_admin, only_role}; 4 | use stellar_tokens::fungible::{self as fungible, sac_admin_wrapper::SACAdminWrapper}; 5 | 6 | #[contract] 7 | pub struct ExampleContract; 8 | 9 | #[contractimpl] 10 | impl ExampleContract { 11 | pub fn __constructor(e: &Env, default_admin: Address, manager: Address, sac: Address) { 12 | access_control::set_admin(e, &default_admin); 13 | 14 | // create a role "manager" and grant it to `manager` 15 | access_control::grant_role_no_auth(e, &manager, &symbol_short!("manager"), &default_admin); 16 | 17 | fungible::sac_admin_wrapper::set_sac_address(e, &sac); 18 | } 19 | } 20 | 21 | #[contractimpl] 22 | impl SACAdminWrapper for ExampleContract { 23 | #[only_admin] 24 | fn set_admin(e: Env, new_admin: Address, _operator: Address) { 25 | fungible::sac_admin_wrapper::set_admin(&e, &new_admin); 26 | } 27 | 28 | #[only_role(operator, "manager")] 29 | fn set_authorized(e: Env, id: Address, authorize: bool, operator: Address) { 30 | fungible::sac_admin_wrapper::set_authorized(&e, &id, authorize); 31 | } 32 | 33 | #[only_role(operator, "manager")] 34 | fn mint(e: Env, to: Address, amount: i128, operator: Address) { 35 | fungible::sac_admin_wrapper::mint(&e, &to, amount); 36 | } 37 | 38 | #[only_role(operator, "manager")] 39 | fn clawback(e: Env, from: Address, amount: i128, operator: Address) { 40 | fungible::sac_admin_wrapper::clawback(&e, &from, amount); 41 | } 42 | } 43 | 44 | #[default_impl] 45 | #[contractimpl] 46 | impl AccessControl for ExampleContract {} 47 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/utils/sac_admin_wrapper/README.md: -------------------------------------------------------------------------------- 1 | An example flow when a `SACAdminWrapper` contract is set as a new administrator for a Stellar Classic Asset (SAC). 2 | 3 | ```mermaid 4 | sequenceDiagram 5 | actor Issuer 6 | actor Minter 7 | participant AdminWrapper as AdminWrapper
Contract 8 | participant AdminSigner 9 | participant SAC as Stellar
Asset
Contract 10 | actor User as Asset Holder 11 | 12 | Note over Issuer,AdminWrapper: 1. Asset Deployment 13 | Issuer->>SAC: Deploy Stellar Asset Contract 14 | Note over SAC: Issuer is the initial admin 15 | 16 | Note over Issuer,AdminWrapper: 2. AdminWrapper Deployment and Setup 17 | Issuer->>AdminWrapper: Deploy AdminWrapper with __constructor(SAC, AdminSigner, Minter) 18 | Note over AdminWrapper: Constructor stores
SAC address,
AdminSigner address,
and Minter address 19 | 20 | Note over Issuer,AdminWrapper: 3. Admin Change 21 | Issuer->>SAC: set_admin(AdminWrapper) 22 | SAC-->>Issuer: Success (AdminWrapper is now admin) 23 | 24 | Note over Minter,AdminWrapper: 4. Admin Functions
via AdminWrapper 25 | Minter->>AdminWrapper: mint(User, 1000) 26 | activate AdminWrapper 27 | activate AdminWrapper 28 | critical Policies checked and signers verified 29 | AdminWrapper->>AdminSigner: require_auth() 30 | AdminSigner-->>AdminWrapper: Authorized 31 | end 32 | deactivate AdminWrapper 33 | AdminWrapper->>SAC: mint(User, 1000) 34 | activate SAC 35 | SAC->>AdminWrapper: require_auth() 36 | AdminWrapper-->>SAC: Authorized
(automatically because it was the invoker) 37 | SAC-->>User: Receive 1000 tokens 38 | SAC-->>AdminWrapper: Success 39 | deactivate SAC 40 | AdminWrapper-->>Minter: Success 41 | deactivate AdminWrapper 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/nft-consecutive/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{testutils::Address as _, Address, Env, String}; 4 | 5 | use crate::contract::{ExampleContract, ExampleContractClient}; 6 | 7 | fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { 8 | let uri = String::from_str(e, "www.mytoken.com"); 9 | let name = String::from_str(e, "My Token"); 10 | let symbol = String::from_str(e, "TKN"); 11 | let address = e.register(ExampleContract, (uri, name, symbol, owner)); 12 | ExampleContractClient::new(e, &address) 13 | } 14 | 15 | #[test] 16 | fn consecutive_transfer_override_works() { 17 | let e = Env::default(); 18 | 19 | let owner = Address::generate(&e); 20 | 21 | let recipient = Address::generate(&e); 22 | 23 | let client = create_client(&e, &owner); 24 | 25 | e.mock_all_auths(); 26 | client.batch_mint(&owner, &100); 27 | client.transfer(&owner, &recipient, &10); 28 | assert_eq!(client.balance(&owner), 99); 29 | assert_eq!(client.balance(&recipient), 1); 30 | assert_eq!(client.owner_of(&10), recipient); 31 | } 32 | 33 | #[test] 34 | fn consecutive_batch_mint_works() { 35 | let e = Env::default(); 36 | let owner = Address::generate(&e); 37 | let client = create_client(&e, &owner); 38 | e.mock_all_auths(); 39 | client.batch_mint(&owner, &100); 40 | client.burn(&owner, &0); 41 | assert_eq!(client.balance(&owner), 99); 42 | client.batch_mint(&owner, &100); 43 | assert_eq!(client.owner_of(&101), owner); 44 | } 45 | 46 | #[test] 47 | fn consecutive_burn_works() { 48 | let e = Env::default(); 49 | let owner = Address::generate(&e); 50 | let client = create_client(&e, &owner); 51 | e.mock_all_auths(); 52 | client.batch_mint(&owner, &100); 53 | client.burn(&owner, &0); 54 | assert_eq!(client.balance(&owner), 99); 55 | } 56 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/hashable.rs: -------------------------------------------------------------------------------- 1 | //! Generic hashing support. 2 | 3 | use soroban_sdk::{Bytes, BytesN}; 4 | 5 | use crate::crypto::hasher::Hasher; 6 | 7 | /// A hashable type. 8 | /// 9 | /// Types implementing `Hashable` are able to be [`Hashable::hash`]ed with an 10 | /// instance of [`Hasher`]. 11 | pub trait Hashable { 12 | /// Feeds this value into the given [`Hasher`]. 13 | fn hash(&self, hasher: &mut H); 14 | } 15 | 16 | impl Hashable for BytesN<32> { 17 | #[inline] 18 | fn hash(&self, hasher: &mut H) { 19 | hasher.update(self.into()); 20 | } 21 | } 22 | 23 | impl Hashable for Bytes { 24 | #[inline] 25 | fn hash(&self, hasher: &mut H) { 26 | hasher.update(self.clone()); 27 | } 28 | } 29 | 30 | /// Hash the pair `(a, b)` with `hasher`. 31 | /// 32 | /// Returns the finalized hash output from the hasher. 33 | /// 34 | /// # Arguments 35 | /// 36 | /// * `a` - The first value to hash. 37 | /// * `b` - The second value to hash. 38 | /// * `hasher` - The hasher to use. 39 | #[inline] 40 | pub fn hash_pair(a: &H, b: &H, mut hasher: S) -> S::Output 41 | where 42 | H: Hashable + ?Sized, 43 | S: Hasher, 44 | { 45 | a.hash(&mut hasher); 46 | b.hash(&mut hasher); 47 | hasher.finalize() 48 | } 49 | 50 | /// Sort the pair `(a, b)` and hash the result with `hasher`. Frequently used 51 | /// when working with merkle proofs. 52 | /// 53 | /// Returns the finalized hash output from the hasher. 54 | /// 55 | /// # Arguments 56 | /// 57 | /// * `a` - The first value to hash. 58 | /// * `b` - The second value to hash. 59 | /// * `hasher` - The hasher to use. 60 | #[inline] 61 | pub fn commutative_hash_pair(a: &H, b: &H, hasher: S) -> S::Output 62 | where 63 | H: Hashable + PartialOrd, 64 | S: Hasher, 65 | { 66 | if a > b { 67 | hash_pair(b, a, hasher) 68 | } else { 69 | hash_pair(a, b, hasher) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/test-utils/default-impl-macro-test/tests/fungible_burnable.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env, String}; 2 | use stellar_macros::default_impl; 3 | use stellar_tokens::fungible::{burnable::FungibleBurnable, Base, FungibleToken}; 4 | 5 | #[contract] 6 | pub struct ExampleContract; 7 | 8 | #[contractimpl] 9 | impl ExampleContract { 10 | pub fn __constructor(e: &Env, name: String, symbol: String) { 11 | Base::set_metadata(e, 7, name, symbol); 12 | } 13 | 14 | pub fn mint(e: &Env, to: Address, amount: i128) { 15 | Base::mint(e, &to, amount); 16 | } 17 | } 18 | 19 | #[default_impl] 20 | #[contractimpl] 21 | impl FungibleToken for ExampleContract { 22 | type ContractType = Base; 23 | } 24 | 25 | #[default_impl] 26 | #[contractimpl] 27 | impl FungibleBurnable for ExampleContract {} 28 | 29 | fn create_client<'a>(e: &Env) -> ExampleContractClient<'a> { 30 | let name = String::from_str(e, "My Token"); 31 | let symbol = String::from_str(e, "TKN"); 32 | let address = e.register(ExampleContract, (name, symbol)); 33 | ExampleContractClient::new(e, &address) 34 | } 35 | 36 | #[test] 37 | fn default_impl_fungible_burnable_burn() { 38 | let e = Env::default(); 39 | let owner = Address::generate(&e); 40 | let client = create_client(&e); 41 | 42 | e.mock_all_auths(); 43 | client.mint(&owner, &100); 44 | client.burn(&owner, &50); 45 | assert_eq!(client.balance(&owner), 50); 46 | assert_eq!(client.total_supply(), 50); 47 | } 48 | 49 | #[test] 50 | fn default_impl_fungible_burnable_burn_from() { 51 | let e = Env::default(); 52 | let owner = Address::generate(&e); 53 | let spender = Address::generate(&e); 54 | let client = create_client(&e); 55 | 56 | e.mock_all_auths(); 57 | client.mint(&owner, &100); 58 | client.approve(&owner, &spender, &50, &1000); 59 | client.burn_from(&spender, &owner, &30); 60 | assert_eq!(client.balance(&owner), 70); 61 | assert_eq!(client.total_supply(), 70); 62 | assert_eq!(client.allowance(&owner, &spender), 20); 63 | } 64 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/account/src/test.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{ 2 | contract, contractimpl, map, testutils::Address as _, vec, Address, Bytes, Env, Val, Vec, 3 | }; 4 | use stellar_accounts::{ 5 | policies::{simple_threshold::SimpleThresholdAccountParams, Policy}, 6 | smart_account::{ContextRule, Signer}, 7 | }; 8 | 9 | use crate::contract::MultisigContract; 10 | 11 | #[contract] 12 | struct MockPolicyContract; 13 | 14 | #[contractimpl] 15 | impl Policy for MockPolicyContract { 16 | type AccountParams = Val; 17 | 18 | fn can_enforce( 19 | _e: &Env, 20 | _context: soroban_sdk::auth::Context, 21 | _authenticated_signers: Vec, 22 | _rule: ContextRule, 23 | _smart_account: Address, 24 | ) -> bool { 25 | true 26 | } 27 | 28 | fn enforce( 29 | _e: &Env, 30 | _context: soroban_sdk::auth::Context, 31 | _authenticated_signers: Vec, 32 | _rule: ContextRule, 33 | _smart_account: Address, 34 | ) { 35 | } 36 | 37 | fn install( 38 | _e: &Env, 39 | _install_params: Self::AccountParams, 40 | _rule: ContextRule, 41 | _smart_account: Address, 42 | ) { 43 | } 44 | 45 | fn uninstall(_e: &Env, _rule: ContextRule, _smart_account: Address) {} 46 | } 47 | 48 | #[test] 49 | fn create_account() { 50 | let e = Env::default(); 51 | let verifier = Address::generate(&e); 52 | let policy = e.register(MockPolicyContract, ()); 53 | let signers = vec![ 54 | &e, 55 | Signer::External( 56 | verifier.clone(), 57 | Bytes::from_array( 58 | &e, 59 | b"4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29", 60 | ), 61 | ), 62 | Signer::External( 63 | verifier.clone(), 64 | Bytes::from_array( 65 | &e, 66 | b"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 67 | ), 68 | ), 69 | ]; 70 | let policies = map![&e, (policy, SimpleThresholdAccountParams { threshold: 2 })]; 71 | e.register(MultisigContract, (signers, policies)); 72 | } 73 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/burnable/storage.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{Address, Env}; 2 | 3 | use crate::fungible::{extensions::burnable::emit_burn, Base}; 4 | 5 | impl Base { 6 | /// Destroys `amount` of tokens from `from`. Updates the total 7 | /// supply accordingly. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `e` - Access to the Soroban environment. 12 | /// * `from` - The account whose tokens are destroyed. 13 | /// * `amount` - The amount of tokens to burn. 14 | /// 15 | /// # Errors 16 | /// 17 | /// * refer to [`Base::update`] errors. 18 | /// 19 | /// # Events 20 | /// 21 | /// * topics - `["burn", from: Address]` 22 | /// * data - `[amount: i128]` 23 | /// 24 | /// # Notes 25 | /// 26 | /// Authorization for `from` is required. 27 | pub fn burn(e: &Env, from: &Address, amount: i128) { 28 | from.require_auth(); 29 | Base::update(e, Some(from), None, amount); 30 | emit_burn(e, from, amount); 31 | } 32 | 33 | /// Destroys `amount` of tokens from `from` using the allowance mechanism. 34 | /// `amount` is then deducted from `spender` allowance. 35 | /// Updates the total supply accordingly. 36 | /// 37 | /// # Arguments 38 | /// 39 | /// * `e` - Access to the Soroban environment. 40 | /// * `spender` - The address authorizing the transfer, and having its 41 | /// allowance. 42 | /// * `from` - The account whose tokens are destroyed. 43 | /// * `amount` - The amount of tokens to burn. 44 | /// 45 | /// # Errors 46 | /// 47 | /// * refer to [`Base::spend_allowance`] errors. 48 | /// * refer to [`Base::update`] errors. 49 | /// 50 | /// # Events 51 | /// 52 | /// * topics - `["burn", from: Address]` 53 | /// * data - `[amount: i128]` 54 | /// 55 | /// # Notes 56 | /// 57 | /// Authorization for `spender` is required. 58 | pub fn burn_from(e: &Env, spender: &Address, from: &Address, amount: i128) { 59 | spender.require_auth(); 60 | Base::spend_allowance(e, from, spender, amount); 61 | Base::update(e, Some(from), None, amount); 62 | emit_burn(e, from, amount); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/tokens/src/non_fungible/extensions/burnable/storage.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{Address, Env}; 2 | 3 | use crate::non_fungible::{burnable::emit_burn, Base}; 4 | 5 | impl Base { 6 | /// Destroys the token with `token_id` from `from`, ensuring ownership 7 | /// checks, and emits a `burn` event. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `e` - Access to the Soroban environment. 12 | /// * `from` - The account whose token is destroyed. 13 | /// * `token_id` - The identifier of the token to burn. 14 | /// 15 | /// # Errors 16 | /// 17 | /// * refer to [`Base::update`] errors. 18 | /// 19 | /// # Events 20 | /// 21 | /// * topics - `["burn", from: Address]` 22 | /// * data - `[token_id: u32]` 23 | /// 24 | /// # Notes 25 | /// 26 | /// Authorization for `from` is required. 27 | pub fn burn(e: &Env, from: &Address, token_id: u32) { 28 | from.require_auth(); 29 | Base::update(e, Some(from), None, token_id); 30 | emit_burn(e, from, token_id); 31 | } 32 | 33 | /// Destroys the token with `token_id` from `from`, ensuring ownership 34 | /// and approval checks, and emits a `burn` event. 35 | /// 36 | /// # Arguments 37 | /// 38 | /// * `e` - Access to the Soroban environment. 39 | /// * `spender` - The account that is allowed to burn the token on behalf of 40 | /// the owner. 41 | /// * `from` - The account whose token is destroyed. 42 | /// * `token_id` - The identifier of the token to burn. 43 | /// 44 | /// # Errors 45 | /// 46 | /// * refer to [`Base::check_spender_approval`] errors. 47 | /// * refer to [`Base::update`] errors. 48 | /// 49 | /// # Events 50 | /// 51 | /// * topics - `["burn", from: Address]` 52 | /// * data - `[token_id: u32]` 53 | /// 54 | /// # Notes 55 | /// 56 | /// Authorization for `spender` is required. 57 | pub fn burn_from(e: &Env, spender: &Address, from: &Address, token_id: u32) { 58 | spender.require_auth(); 59 | Base::check_spender_approval(e, spender, from, token_id); 60 | Base::update(e, Some(from), None, token_id); 61 | emit_burn(e, from, token_id); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/fungible-capped/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Capped Example Contract. 2 | //! 3 | //! Demonstrates an example usage of `capped` module by 4 | //! implementing a capped mint mechanism, and setting the maximum supply 5 | //! at the constructor. 6 | //! 7 | //! **IMPORTANT**: this example is for demonstration purposes, and authorization 8 | //! is not taken into consideration 9 | 10 | use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; 11 | use stellar_tokens::fungible::{ 12 | capped::{check_cap, set_cap}, 13 | Base, FungibleToken, 14 | }; 15 | 16 | #[contract] 17 | pub struct ExampleContract; 18 | 19 | #[contractimpl] 20 | impl ExampleContract { 21 | pub fn __constructor(e: &Env, cap: i128) { 22 | set_cap(e, cap); 23 | } 24 | 25 | pub fn mint(e: &Env, to: Address, amount: i128) { 26 | check_cap(e, amount); 27 | Base::mint(e, &to, amount); 28 | } 29 | } 30 | 31 | #[contractimpl] 32 | impl FungibleToken for ExampleContract { 33 | type ContractType = Base; 34 | 35 | fn total_supply(e: &Env) -> i128 { 36 | Self::ContractType::total_supply(e) 37 | } 38 | 39 | fn balance(e: &Env, account: Address) -> i128 { 40 | Self::ContractType::balance(e, &account) 41 | } 42 | 43 | fn allowance(e: &Env, owner: Address, spender: Address) -> i128 { 44 | Self::ContractType::allowance(e, &owner, &spender) 45 | } 46 | 47 | fn transfer(e: &Env, from: Address, to: MuxedAddress, amount: i128) { 48 | Self::ContractType::transfer(e, &from, &to, amount); 49 | } 50 | 51 | fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) { 52 | Self::ContractType::transfer_from(e, &spender, &from, &to, amount); 53 | } 54 | 55 | fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) { 56 | Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger); 57 | } 58 | 59 | fn decimals(e: &Env) -> u32 { 60 | Self::ContractType::decimals(e) 61 | } 62 | 63 | fn name(e: &Env) -> String { 64 | Self::ContractType::name(e) 65 | } 66 | 67 | fn symbol(e: &Env) -> String { 68 | Self::ContractType::symbol(e) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/fungible-capped/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{testutils::Address as _, token, Address, Env}; 4 | 5 | use crate::contract::{ExampleContract, ExampleContractClient}; 6 | 7 | fn create_client<'a>(e: &Env, cap: &i128) -> ExampleContractClient<'a> { 8 | let address = e.register(ExampleContract, (cap,)); 9 | ExampleContractClient::new(e, &address) 10 | } 11 | 12 | #[test] 13 | fn mint_under_cap() { 14 | let e = Env::default(); 15 | let cap = 1000; 16 | let client = create_client(&e, &cap); 17 | let user = Address::generate(&e); 18 | 19 | client.mint(&user, &500); 20 | 21 | assert_eq!(client.balance(&user), 500); 22 | assert_eq!(client.total_supply(), 500); 23 | } 24 | 25 | #[test] 26 | fn mint_exact_cap() { 27 | let e = Env::default(); 28 | let cap = 1000; 29 | let client = create_client(&e, &cap); 30 | let user = Address::generate(&e); 31 | 32 | client.mint(&user, &1000); 33 | 34 | assert_eq!(client.balance(&user), 1000); 35 | assert_eq!(client.total_supply(), 1000); 36 | } 37 | 38 | #[test] 39 | #[should_panic(expected = "Error(Contract, #106)")] 40 | fn mint_exceeds_cap() { 41 | let e = Env::default(); 42 | let cap = 1000; 43 | let client = create_client(&e, &cap); 44 | let user = Address::generate(&e); 45 | 46 | // Attempt to mint 1001 tokens (would exceed cap) 47 | client.mint(&user, &1001); // This should panic 48 | } 49 | 50 | #[test] 51 | #[should_panic(expected = "Error(Contract, #106)")] 52 | fn mint_multiple_exceeds_cap() { 53 | let e = Env::default(); 54 | let cap = 1000; 55 | let client = create_client(&e, &cap); 56 | let user = Address::generate(&e); 57 | 58 | // Mint 600 tokens first 59 | client.mint(&user, &600); 60 | 61 | assert_eq!(client.balance(&user), 600); 62 | assert_eq!(client.total_supply(), 600); 63 | 64 | // Attempt to mint 500 more tokens (would exceed cap) 65 | client.mint(&user, &500); // This should panic 66 | } 67 | 68 | #[test] 69 | fn test_token_interface() { 70 | let e = Env::default(); 71 | let cap = 1000_i128; 72 | 73 | let address = e.register(ExampleContract, (cap,)); 74 | let client = token::Client::new(&e, &address); 75 | let user = Address::generate(&e); 76 | 77 | assert_eq!(client.balance(&user), 0); 78 | } 79 | -------------------------------------------------------------------------------- /examples/fungible-blocklist/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Fungibe BlockList Example Contract. 2 | 3 | //! This contract showcases how to integrate the BlockList extension with a 4 | //! SEP-41-compliant fungible token. It includes essential features such as 5 | //! controlled token transfers by an admin who can block or unblock specific 6 | //! accounts. 7 | 8 | use soroban_sdk::{contract, contracterror, contractimpl, symbol_short, Address, Env, String}; 9 | use stellar_access::access_control::{self as access_control, AccessControl}; 10 | use stellar_macros::{default_impl, only_role}; 11 | use stellar_tokens::fungible::{ 12 | blocklist::{BlockList, FungibleBlockList}, 13 | Base, FungibleToken, 14 | }; 15 | 16 | #[contract] 17 | pub struct ExampleContract; 18 | 19 | #[contracterror] 20 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 21 | #[repr(u32)] 22 | pub enum ExampleContractError { 23 | Unauthorized = 1, 24 | } 25 | 26 | #[contractimpl] 27 | impl ExampleContract { 28 | pub fn __constructor( 29 | e: &Env, 30 | name: String, 31 | symbol: String, 32 | admin: Address, 33 | manager: Address, 34 | initial_supply: i128, 35 | ) { 36 | Base::set_metadata(e, 18, name, symbol); 37 | 38 | access_control::set_admin(e, &admin); 39 | 40 | // create a role "manager" and grant it to `manager` 41 | 42 | access_control::grant_role_no_auth(e, &manager, &symbol_short!("manager"), &admin); 43 | 44 | // Mint initial supply to the admin 45 | Base::mint(e, &admin, initial_supply); 46 | } 47 | } 48 | 49 | #[default_impl] 50 | #[contractimpl] 51 | impl FungibleToken for ExampleContract { 52 | type ContractType = BlockList; 53 | } 54 | 55 | #[contractimpl] 56 | impl FungibleBlockList for ExampleContract { 57 | fn blocked(e: &Env, account: Address) -> bool { 58 | BlockList::blocked(e, &account) 59 | } 60 | 61 | #[only_role(operator, "manager")] 62 | fn block_user(e: &Env, user: Address, operator: Address) { 63 | BlockList::block_user(e, &user) 64 | } 65 | 66 | #[only_role(operator, "manager")] 67 | fn unblock_user(e: &Env, user: Address, operator: Address) { 68 | BlockList::unblock_user(e, &user) 69 | } 70 | } 71 | 72 | #[default_impl] 73 | #[contractimpl] 74 | impl AccessControl for ExampleContract {} 75 | -------------------------------------------------------------------------------- /examples/fungible-allowlist/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Fungible AllowList Example Contract. 2 | 3 | //! This contract showcases how to integrate the AllowList extension with a 4 | //! SEP-41-compliant fungible token. It includes essential features such as 5 | //! controlled token transfers by an admin who can allow or disallow specific 6 | //! accounts. 7 | 8 | use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String}; 9 | use stellar_access::access_control::{self as access_control, AccessControl}; 10 | use stellar_macros::{default_impl, only_role}; 11 | use stellar_tokens::fungible::{ 12 | allowlist::{AllowList, FungibleAllowList}, 13 | burnable::FungibleBurnable, 14 | Base, FungibleToken, 15 | }; 16 | 17 | #[contract] 18 | pub struct ExampleContract; 19 | 20 | #[contractimpl] 21 | impl ExampleContract { 22 | pub fn __constructor( 23 | e: &Env, 24 | name: String, 25 | symbol: String, 26 | admin: Address, 27 | manager: Address, 28 | initial_supply: i128, 29 | ) { 30 | Base::set_metadata(e, 18, name, symbol); 31 | 32 | access_control::set_admin(e, &admin); 33 | 34 | // create a role "manager" and grant it to `manager` 35 | access_control::grant_role_no_auth(e, &manager, &symbol_short!("manager"), &admin); 36 | 37 | // Allow the admin to transfer tokens 38 | AllowList::allow_user(e, &admin); 39 | 40 | // Mint initial supply to the admin 41 | Base::mint(e, &admin, initial_supply); 42 | } 43 | } 44 | 45 | #[default_impl] 46 | #[contractimpl] 47 | impl FungibleToken for ExampleContract { 48 | type ContractType = AllowList; 49 | } 50 | #[contractimpl] 51 | impl FungibleAllowList for ExampleContract { 52 | fn allowed(e: &Env, account: Address) -> bool { 53 | AllowList::allowed(e, &account) 54 | } 55 | 56 | #[only_role(operator, "manager")] 57 | fn allow_user(e: &Env, user: Address, operator: Address) { 58 | AllowList::allow_user(e, &user) 59 | } 60 | 61 | #[only_role(operator, "manager")] 62 | fn disallow_user(e: &Env, user: Address, operator: Address) { 63 | AllowList::disallow_user(e, &user) 64 | } 65 | } 66 | 67 | #[default_impl] 68 | #[contractimpl] 69 | impl AccessControl for ExampleContract {} 70 | 71 | #[default_impl] 72 | #[contractimpl] 73 | impl FungibleBurnable for ExampleContract {} 74 | -------------------------------------------------------------------------------- /.github/workflows/generic.yml: -------------------------------------------------------------------------------- 1 | name: "rust workflow" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | paths-ignore: 11 | - "**.md" 12 | workflow_dispatch: 13 | 14 | # If new code is pushed to a PR branch, then cancel in progress workflows for 15 | # that PR. Ensures that we don't waste CI time, and returns results quicker. 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | env: 21 | # Not needed in CI, should make things a bit faster 22 | CARGO_INCREMENTAL: 0 23 | CARGO_TERM_COLOR: always 24 | # Remove unnecessary WASM build artifacts 25 | WASM_BUILD_CLEAN_TARGET: 1 26 | # Enable sscache 27 | RUSTC_WRAPPER: "sccache" 28 | SCCACHE_GHA_ENABLED: "true" 29 | SCCACHE_CACHE_SIZE: "50GB" 30 | 31 | jobs: 32 | clippy-fmt-test: 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | os: 37 | - ubuntu-latest 38 | 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - name: git checkout 42 | uses: actions/checkout@v4 43 | 44 | - name: Run sccache 45 | uses: mozilla-actions/sccache-action@v0.0.8 46 | 47 | - name: install rust toolchain 48 | uses: actions-rust-lang/setup-rust-toolchain@v1 49 | with: 50 | toolchain: stable, nightly 51 | components: clippy, rustfmt, llvm-tools-preview 52 | target: wasm32v1-none 53 | 54 | - name: Install cargo-llvm-cov 55 | run: cargo install cargo-llvm-cov 56 | 57 | - name: Check format 58 | run: cargo +nightly fmt --all -- --check 59 | 60 | - name: Check clippy 61 | run: cargo clippy --release --locked --all-targets -- -D warnings 62 | 63 | # TODO: re-enable this after we find a stable solution to same name functions across multiple contracts 64 | # - name: Check build 65 | # run: cargo build --target wasm32v1-none --release 66 | 67 | - name: Run tests with coverage 68 | run: cargo llvm-cov --workspace --lcov --fail-under-lines 90 --output-path lcov.info 69 | 70 | - name: Upload coverage to Codecov 71 | uses: codecov/codecov-action@v3 72 | with: 73 | files: ./lcov.info 74 | token: ${{ secrets.CODECOV_TOKEN }} 75 | slug: OpenZeppelin/stellar-contracts 76 | verbose: true 77 | -------------------------------------------------------------------------------- /packages/test-utils/default-impl-macro-test/tests/non_fungible_burnable.rs: -------------------------------------------------------------------------------- 1 | //! Non-Fungible Enumerable Example Contract. 2 | //! 3 | //! Demonstrates an example usage of the Enumerable extension, allowing for 4 | //! enumeration of all the token IDs in the contract as well as all the token 5 | //! IDs owned by each account. 6 | //! 7 | //! **IMPORTANT**: This example is for demonstration purposes, and access 8 | //! control to sensitive operations is not taken into consideration! 9 | 10 | use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env, String}; 11 | use stellar_macros::default_impl; 12 | use stellar_tokens::non_fungible::{burnable::NonFungibleBurnable, Base, NonFungibleToken}; 13 | 14 | #[contract] 15 | pub struct ExampleContract; 16 | 17 | #[contractimpl] 18 | impl ExampleContract { 19 | pub fn __constructor(e: &Env, uri: String, name: String, symbol: String) { 20 | Base::set_metadata(e, uri, name, symbol); 21 | } 22 | 23 | pub fn mint(e: &Env, to: Address, token_id: u32) { 24 | Base::mint(e, &to, token_id); 25 | } 26 | } 27 | 28 | #[default_impl] 29 | #[contractimpl] 30 | impl NonFungibleToken for ExampleContract { 31 | type ContractType = Base; 32 | } 33 | 34 | #[default_impl] 35 | #[contractimpl] 36 | impl NonFungibleBurnable for ExampleContract {} 37 | 38 | fn create_client<'a>(e: &Env) -> ExampleContractClient<'a> { 39 | let uri = String::from_str(e, "www.mytoken.com"); 40 | let name = String::from_str(e, "My Token"); 41 | let symbol = String::from_str(e, "TKN"); 42 | let address = e.register(ExampleContract, (uri, name, symbol)); 43 | ExampleContractClient::new(e, &address) 44 | } 45 | 46 | #[test] 47 | fn default_impl_non_fungible_burnable_burn() { 48 | let e = Env::default(); 49 | let owner = Address::generate(&e); 50 | let client = create_client(&e); 51 | 52 | e.mock_all_auths(); 53 | client.mint(&owner, &10); 54 | client.burn(&owner, &10); 55 | assert_eq!(client.balance(&owner), 0); 56 | } 57 | 58 | #[test] 59 | fn default_impl_non_fungible_burnable_burn_from() { 60 | let e = Env::default(); 61 | let owner = Address::generate(&e); 62 | let spender = Address::generate(&e); 63 | let client = create_client(&e); 64 | 65 | e.mock_all_auths(); 66 | client.mint(&owner, &10); 67 | client.approve(&owner, &spender, &10, &1000); 68 | client.burn_from(&spender, &owner, &10); 69 | assert_eq!(client.balance(&owner), 0); 70 | } 71 | -------------------------------------------------------------------------------- /packages/contract-utils/src/crypto/test/hashable.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use std::{format, vec::Vec}; 4 | 5 | use proptest::prelude::*; 6 | use soroban_sdk::{Bytes, Env}; 7 | 8 | use crate::crypto::{hashable::*, hasher::Hasher, keccak::Keccak256}; 9 | 10 | fn non_empty_u8_vec_strategy() -> impl Strategy> { 11 | prop::collection::vec(any::(), 1..ProptestConfig::default().max_default_size_range) 12 | } 13 | 14 | #[test] 15 | fn commutative_hash_is_order_independent() { 16 | let e = Env::default(); 17 | proptest!(|(a: Vec, b: Vec)| { 18 | let a = Bytes::from_slice(&e, &a); 19 | let b = Bytes::from_slice(&e, &b); 20 | let hash1 = commutative_hash_pair(&a, &b, Keccak256::new(&e)); 21 | let hash2 = commutative_hash_pair(&b, &a, Keccak256::new(&e)); 22 | prop_assert_eq!(hash1, hash2); 23 | }) 24 | } 25 | 26 | #[test] 27 | fn regular_hash_is_order_dependent() { 28 | let e = Env::default(); 29 | proptest!(|(a in non_empty_u8_vec_strategy(), 30 | b in non_empty_u8_vec_strategy())| { 31 | prop_assume!(a != b); 32 | let a = Bytes::from_slice(&e, &a); 33 | let b = Bytes::from_slice(&e, &b); 34 | let hash1 = hash_pair(&a, &b, Keccak256::new(&e)); 35 | let hash2 = hash_pair(&b, &a, Keccak256::new(&e)); 36 | prop_assert_ne!(hash1, hash2); 37 | }) 38 | } 39 | 40 | #[test] 41 | fn hash_pair_deterministic() { 42 | let e = Env::default(); 43 | proptest!(|(a: Vec, b: Vec)| { 44 | let a = Bytes::from_slice(&e, &a); 45 | let b = Bytes::from_slice(&e, &b); 46 | let hash1 = hash_pair(&a, &b, Keccak256::new(&e)); 47 | let hash2 = hash_pair(&a, &b, Keccak256::new(&e)); 48 | prop_assert_eq!(hash1, hash2); 49 | }) 50 | } 51 | 52 | #[test] 53 | fn commutative_hash_pair_deterministic() { 54 | let e = Env::default(); 55 | proptest!(|(a: Vec, b: Vec)| { 56 | let a = Bytes::from_slice(&e, &a); 57 | let b = Bytes::from_slice(&e, &b); 58 | let hash1 = commutative_hash_pair(&a, &b, Keccak256::new(&e)); 59 | let hash2 = commutative_hash_pair(&a, &b, Keccak256::new(&e)); 60 | prop_assert_eq!(hash1, hash2); 61 | }) 62 | } 63 | 64 | #[test] 65 | fn identical_pairs_hash() { 66 | let e = Env::default(); 67 | proptest!(|(a: Vec)| { 68 | let a = Bytes::from_slice(&e, &a); 69 | let hash1 = hash_pair(&a, &a, Keccak256::new(&e)); 70 | let hash2 = commutative_hash_pair(&a, &a, Keccak256::new(&e)); 71 | assert_eq!(hash1, hash2); 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /examples/pausable/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{testutils::Address as _, Address, Env}; 4 | 5 | use crate::contract::{ExampleContract, ExampleContractClient}; 6 | 7 | fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { 8 | let address = e.register(ExampleContract, (owner,)); 9 | ExampleContractClient::new(e, &address) 10 | } 11 | 12 | #[test] 13 | fn initial_state() { 14 | let e = Env::default(); 15 | let owner = Address::generate(&e); 16 | let client = create_client(&e, &owner); 17 | 18 | assert!(!client.paused()); 19 | assert_eq!(client.increment(), 1); 20 | } 21 | 22 | #[test] 23 | fn pause_works() { 24 | let e = Env::default(); 25 | let owner = Address::generate(&e); 26 | let client = create_client(&e, &owner); 27 | 28 | e.mock_all_auths(); 29 | client.pause(&owner); 30 | 31 | assert!(client.paused()); 32 | } 33 | 34 | #[test] 35 | #[should_panic(expected = "Error(Contract, #1)")] 36 | fn errors_pause_unauthorized() { 37 | let e = Env::default(); 38 | let owner = Address::generate(&e); 39 | let user = Address::generate(&e); 40 | let client = create_client(&e, &owner); 41 | 42 | e.mock_all_auths(); 43 | client.pause(&user); 44 | } 45 | 46 | #[test] 47 | fn unpause_works() { 48 | let e = Env::default(); 49 | let owner = Address::generate(&e); 50 | let client = create_client(&e, &owner); 51 | 52 | e.mock_all_auths(); 53 | client.pause(&owner); 54 | client.unpause(&owner); 55 | 56 | assert!(!client.paused()); 57 | } 58 | 59 | #[test] 60 | #[should_panic(expected = "Error(Contract, #1)")] 61 | fn errors_unpause_unauthorized() { 62 | let e = Env::default(); 63 | let owner = Address::generate(&e); 64 | let user = Address::generate(&e); 65 | let client = create_client(&e, &owner); 66 | 67 | e.mock_all_auths(); 68 | client.pause(&owner); 69 | client.unpause(&user); 70 | } 71 | 72 | #[test] 73 | #[should_panic(expected = "Error(Contract, #1000)")] 74 | fn errors_increment_when_paused() { 75 | let e = Env::default(); 76 | let owner = Address::generate(&e); 77 | let client = create_client(&e, &owner); 78 | 79 | e.mock_all_auths(); 80 | client.pause(&owner); 81 | client.increment(); 82 | } 83 | 84 | #[test] 85 | #[should_panic(expected = "Error(Contract, #1001)")] 86 | fn errors_emergency_reset_when_not_paused() { 87 | let e = Env::default(); 88 | let owner = Address::generate(&e); 89 | let client = create_client(&e, &owner); 90 | 91 | e.mock_all_auths(); 92 | client.emergency_reset(); 93 | } 94 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/webauthn-verifier/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! # WebAuthn Verifier Contract 2 | //! 3 | //! A reusable verifier contract for WebAuthn (passkey) signature verification. 4 | //! This contract can be deployed once and used by multiple smart accounts 5 | //! across the network for delegated signature verification. Provides 6 | //! cryptographic verification for WebAuthn signatures against message hashes 7 | //! and public keys. 8 | //! 9 | //! Unlike simpler signature schemes, WebAuthn signature data is a complex 10 | //! structure containing authenticator data, client data JSON, and the signature 11 | //! itself. The `sig_data` parameter should be XDR-encoded `WebAuthnSigData` to 12 | //! ensure proper serialization and deserialization. 13 | //! 14 | //! The `key_data` parameter is expected to contain the 65-byte uncompressed 15 | //! secp256r1 public key followed by the credential ID bytes (if any) that can 16 | //! be of a variable length. The public key is available on the client side only 17 | //! during the passkey generation and the credential ID is used to identify the 18 | //! passkey. 19 | use soroban_sdk::{contract, contractimpl, xdr::FromXdr, Bytes, BytesN, Env}; 20 | use stellar_accounts::verifiers::{ 21 | utils::extract_from_bytes, 22 | webauthn::{self, WebAuthnSigData}, 23 | Verifier, 24 | }; 25 | 26 | #[contract] 27 | pub struct WebauthnVerifierContract; 28 | 29 | #[contractimpl] 30 | impl Verifier for WebauthnVerifierContract { 31 | type KeyData = Bytes; 32 | type SigData = Bytes; 33 | 34 | /// Verify a WebAuthn signature against a message and public key. 35 | /// 36 | /// # Arguments 37 | /// 38 | /// * `signature_payload` - The message hash that was signed 39 | /// * `key_data` - Bytes containing: 40 | /// - 65-byte secp256r1 public key (uncompressed format) 41 | /// - Variable length credential ID (used on the client side) 42 | /// * `sig_data` - XDR-encoded `WebAuthnSigData` structure containing: 43 | /// - Authenticator data 44 | /// - Client data JSON 45 | /// - Signature components 46 | /// 47 | /// # Returns 48 | /// 49 | /// * `true` if the signature is valid 50 | /// * `false` otherwise 51 | fn verify( 52 | e: &Env, 53 | signature_payload: Bytes, 54 | key_data: Self::KeyData, 55 | sig_data: Self::SigData, 56 | ) -> bool { 57 | let sig_struct = 58 | WebAuthnSigData::from_xdr(e, &sig_data).expect("WebAuthnSigData with correct format"); 59 | 60 | let pub_key: BytesN<65> = 61 | extract_from_bytes(e, &key_data, 0..65).expect("65-byte public key to be extracted"); 62 | 63 | webauthn::verify(e, &signature_payload, &pub_key, &sig_struct) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/nft-access-control/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Non-Fungible with Access Control Example Contract. 2 | //! 3 | //! Demonstrates how can Access Control be utilized. 4 | 5 | use soroban_sdk::{contract, contractimpl, vec, Address, Env, String, Vec}; 6 | use stellar_access::access_control::{set_admin, AccessControl}; 7 | use stellar_macros::{default_impl, has_any_role, has_role, only_admin, only_any_role, only_role}; 8 | use stellar_tokens::non_fungible::{burnable::NonFungibleBurnable, Base, NonFungibleToken}; 9 | 10 | #[contract] 11 | pub struct ExampleContract; 12 | 13 | #[contractimpl] 14 | impl ExampleContract { 15 | pub fn __constructor(e: &Env, uri: String, name: String, symbol: String, admin: Address) { 16 | set_admin(e, &admin); 17 | Base::set_metadata(e, uri, name, symbol); 18 | } 19 | 20 | #[only_admin] 21 | pub fn admin_restricted_function(e: &Env) -> Vec { 22 | vec![&e, String::from_str(e, "seems sus")] 23 | } 24 | 25 | // we want `require_auth()` provided by the macro, since there is no 26 | // `require_auth()` in `Base::mint`. 27 | #[only_role(caller, "minter")] 28 | pub fn mint(e: &Env, to: Address, token_id: u32, caller: Address) { 29 | Base::mint(e, &to, token_id) 30 | } 31 | 32 | // allows either minter or burner role, does not enforce `require_auth` in the 33 | // macro 34 | #[has_any_role(caller, ["minter", "burner"])] 35 | pub fn multi_role_action(e: &Env, caller: Address) -> String { 36 | caller.require_auth(); 37 | String::from_str(e, "multi_role_action_success") 38 | } 39 | 40 | // allows either minter or burner role AND enforces `require_auth` in the macro 41 | #[only_any_role(caller, ["minter", "burner"])] 42 | pub fn multi_role_auth_action(e: &Env, caller: Address) -> String { 43 | String::from_str(e, "multi_role_auth_action_success") 44 | } 45 | } 46 | 47 | #[default_impl] 48 | #[contractimpl] 49 | impl NonFungibleToken for ExampleContract { 50 | type ContractType = Base; 51 | } 52 | 53 | // for this contract, the `burn*` functions are only meant to be called by 54 | // specific people with the `burner` role 55 | #[contractimpl] 56 | impl NonFungibleBurnable for ExampleContract { 57 | // we DON'T want `require_auth()` provided by the macro, since there is already 58 | // `require_auth()` in `Base::burn` 59 | #[has_role(from, "burner")] 60 | fn burn(e: &Env, from: Address, token_id: u32) { 61 | Base::burn(e, &from, token_id); 62 | } 63 | 64 | #[has_role(spender, "burner")] 65 | fn burn_from(e: &Env, spender: Address, from: Address, token_id: u32) { 66 | Base::burn_from(e, &spender, &from, token_id); 67 | } 68 | } 69 | 70 | #[default_impl] 71 | #[contractimpl] 72 | impl AccessControl for ExampleContract {} 73 | -------------------------------------------------------------------------------- /examples/fungible-merkle-airdrop/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! # Airdrop Distributor for a Fungible Token 2 | //! 3 | //! This contract implements a Merkle-proof-based distribution mechanism for 4 | //! fungible token airdrops. 5 | //! 6 | //! Participants can claim their airdrop by providing a valid Merkle proof 7 | //! corresponding to a pre-generated Merkle tree. The claim is verified against 8 | //! the Merkle root stored in the contract. 9 | //! 10 | //! Each leaf of the Merkle tree must be structured as a `Receiver` struct, 11 | //! which includes: 12 | //! 13 | //! - `index: u32` — a unique index for identifying the claim. 14 | //! - `address: Address` — the Stellar address eligible to claim. 15 | //! - `amount: i128` — the amount of tokens allocated to the address. 16 | //! 17 | //! The contract and test logic were adapted from 18 | //! [philipliu/soroban-merkle-airdrop](https://github.com/philipliu/soroban-merkle-airdrop). 19 | 20 | use soroban_sdk::{contract, contractimpl, contracttype, token, Address, BytesN, Env, Vec}; 21 | use stellar_contract_utils::{ 22 | crypto::sha256::Sha256, 23 | merkle_distributor::{IndexableLeaf, MerkleDistributor}, 24 | }; 25 | 26 | type Distributor = MerkleDistributor; 27 | 28 | #[contracttype] 29 | enum DataKey { 30 | TokenAddress, 31 | } 32 | 33 | #[contracttype] 34 | struct Receiver { 35 | pub index: u32, 36 | pub address: Address, 37 | pub amount: i128, 38 | } 39 | 40 | impl IndexableLeaf for Receiver { 41 | fn index(&self) -> u32 { 42 | self.index 43 | } 44 | } 45 | 46 | #[contract] 47 | pub struct AirdropContract; 48 | 49 | #[contractimpl] 50 | impl AirdropContract { 51 | pub fn __constructor( 52 | e: Env, 53 | root_hash: BytesN<32>, 54 | token: Address, 55 | funding_amount: i128, 56 | funding_source: Address, 57 | ) { 58 | Distributor::set_root(&e, root_hash); 59 | e.storage().instance().set(&DataKey::TokenAddress, &token); 60 | token::TokenClient::new(&e, &token).transfer( 61 | &funding_source, 62 | e.current_contract_address(), 63 | &funding_amount, 64 | ); 65 | } 66 | 67 | pub fn is_claimed(e: &Env, index: u32) -> bool { 68 | Distributor::is_claimed(e, index) 69 | } 70 | 71 | pub fn claim(e: &Env, index: u32, receiver: Address, amount: i128, proof: Vec>) { 72 | let data = Receiver { index, address: receiver.clone(), amount }; 73 | Distributor::verify_and_set_claimed(e, data, proof); 74 | 75 | let token = e.storage().instance().get::<_, Address>(&DataKey::TokenAddress).unwrap(); 76 | token::TokenClient::new(e, &token).transfer( 77 | &e.current_contract_address(), 78 | &receiver, 79 | &amount, 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/sac-admin-generic/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use ed25519_dalek::{Signer, SigningKey, SECRET_KEY_LENGTH}; 4 | use soroban_sdk::{ 5 | auth::{Context, ContractContext}, 6 | testutils::{Address as _, BytesN as _}, 7 | token::StellarAssetClient, 8 | vec, Address, BytesN, Env, IntoVal, Symbol, 9 | }; 10 | 11 | use crate::contract::{SACAdminGenericError, SacAdminExampleContract, Signature}; 12 | 13 | fn create_auth_context(e: &Env, contract: &Address, fn_name: Symbol, amount: i128) -> Context { 14 | Context::Contract(ContractContext { 15 | contract: contract.clone(), 16 | fn_name, 17 | args: ((), (), amount).into_val(e), 18 | }) 19 | } 20 | 21 | #[test] 22 | fn test_sac_generic() { 23 | let e = Env::default(); 24 | let issuer = Address::generate(&e); 25 | 26 | let secret_key_chief: [u8; SECRET_KEY_LENGTH] = [ 27 | 157, 97, 177, 157, 239, 253, 90, 96, 186, 132, 74, 244, 146, 236, 44, 196, 68, 73, 197, 28 | 105, 123, 50, 105, 25, 112, 59, 172, 3, 28, 174, 127, 96, 29 | ]; 30 | let secret_key_operator: [u8; SECRET_KEY_LENGTH] = [ 31 | 57, 7, 177, 157, 29, 253, 90, 96, 186, 132, 74, 244, 146, 236, 44, 196, 68, 73, 234, 105, 32 | 13, 50, 105, 25, 112, 59, 72, 3, 28, 174, 12, 34, 33 | ]; 34 | // Generate signing keypairs. 35 | let chief = SigningKey::from_bytes(&secret_key_chief); 36 | let operator = SigningKey::from_bytes(&secret_key_operator); 37 | 38 | // Deploy the Stellar Asset Contract 39 | let sac = e.register_stellar_asset_contract_v2(issuer.clone()); 40 | let sac_client = StellarAssetClient::new(&e, &sac.address()); 41 | 42 | // Register the account contract, passing in the two signers (public keys) to 43 | // the constructor. 44 | let new_admin = e.register( 45 | SacAdminExampleContract, 46 | ( 47 | sac.address(), 48 | BytesN::from_array(&e, chief.verifying_key().as_bytes()), 49 | BytesN::from_array(&e, operator.verifying_key().as_bytes()), 50 | 1_000_000_000i128, 51 | 0i128, 52 | ), 53 | ); 54 | 55 | let payload = BytesN::random(&e); 56 | 57 | assert_eq!( 58 | e.try_invoke_contract_check_auth::( 59 | &new_admin, 60 | &payload, 61 | Signature { 62 | public_key: BytesN::from_array(&e, &operator.verifying_key().to_bytes()), 63 | signature: BytesN::from_array( 64 | &e, 65 | &operator.sign(payload.to_array().as_slice()).to_bytes() 66 | ), 67 | } 68 | .into_val(&e), 69 | &vec![&e, create_auth_context(&e, &sac_client.address, Symbol::new(&e, "mint"), 1000)], 70 | ), 71 | Ok(()) 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /examples/nft-royalties/src/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{testutils::Address as _, Address, Env, String}; 4 | 5 | use crate::contract::{ExampleContract, ExampleContractClient}; 6 | 7 | fn create_client<'a>(e: &Env, admin: &Address, manager: &Address) -> ExampleContractClient<'a> { 8 | let uri = String::from_str(e, "https://example.com/nft/"); 9 | let name = String::from_str(e, "Royalty NFT"); 10 | let symbol = String::from_str(e, "RNFT"); 11 | let address = e.register(ExampleContract, (uri, name, symbol, admin, manager)); 12 | ExampleContractClient::new(e, &address) 13 | } 14 | 15 | #[test] 16 | fn test_default_royalty() { 17 | let e = Env::default(); 18 | let admin = Address::generate(&e); 19 | let manager = Address::generate(&e); 20 | let client = create_client(&e, &admin, &manager); 21 | 22 | e.mock_all_auths(); 23 | 24 | // Mint a token 25 | let token_id = client.mint(&admin); 26 | 27 | // Check royalty info (should use default 10%) 28 | let (receiver, amount) = client.get_royalty_info(&token_id, &1000); 29 | assert_eq!(receiver, admin); 30 | assert_eq!(amount, 100); // 10% of 1000 31 | } 32 | 33 | #[test] 34 | fn test_token_specific_royalty() { 35 | let e = Env::default(); 36 | let admin = Address::generate(&e); 37 | let manager = Address::generate(&e); 38 | let royalty_receiver = Address::generate(&e); 39 | let client = create_client(&e, &admin, &manager); 40 | 41 | e.mock_all_auths(); 42 | 43 | // Mint a token with specific royalty (5%) 44 | let token_id = client.mint_with_royalty(&admin, &royalty_receiver, &500); 45 | 46 | // Check royalty info 47 | let (receiver, amount) = client.get_royalty_info(&token_id, &2000); 48 | assert_eq!(receiver, royalty_receiver); 49 | assert_eq!(amount, 100); // 5% of 2000 50 | 51 | // Mint a regular token (should use default royalty) 52 | let regular_token_id = client.mint(&admin); 53 | 54 | // Check royalty info for regular token 55 | let (receiver, amount) = client.get_royalty_info(®ular_token_id, &2000); 56 | assert_eq!(receiver, admin); 57 | assert_eq!(amount, 200); // 10% of 2000 58 | } 59 | 60 | #[test] 61 | fn test_zero_royalty() { 62 | let e = Env::default(); 63 | let admin = Address::generate(&e); 64 | let manager = Address::generate(&e); 65 | let royalty_receiver = Address::generate(&e); 66 | let client = create_client(&e, &admin, &manager); 67 | 68 | e.mock_all_auths(); 69 | 70 | // Mint a token with zero royalty 71 | let token_id = client.mint_with_royalty(&admin, &royalty_receiver, &0); 72 | 73 | // Check royalty info 74 | let (receiver, amount) = client.get_royalty_info(&token_id, &1000); 75 | assert_eq!(receiver, royalty_receiver); 76 | assert_eq!(amount, 0); // 0% royalty 77 | } 78 | -------------------------------------------------------------------------------- /examples/pausable/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Pausable Example Contract. 2 | //! 3 | //! Demonstrates an example usage of `stellar_pausable` moddule by 4 | //! implementing an emergency stop mechanism that can be triggered only by the 5 | //! owner account. 6 | //! 7 | //! Counter can be incremented only when `unpaused` and reset only when 8 | //! `paused`. 9 | 10 | use soroban_sdk::{ 11 | contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Env, 12 | }; 13 | use stellar_contract_utils::pausable::{self as pausable, Pausable}; 14 | use stellar_macros::{when_not_paused, when_paused}; 15 | 16 | #[contracttype] 17 | pub enum DataKey { 18 | Owner, 19 | Counter, 20 | } 21 | 22 | #[contracterror] 23 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 24 | #[repr(u32)] 25 | pub enum ExampleContractError { 26 | Unauthorized = 1, 27 | } 28 | 29 | #[contract] 30 | pub struct ExampleContract; 31 | 32 | #[contractimpl] 33 | impl ExampleContract { 34 | pub fn __constructor(e: &Env, owner: Address) { 35 | e.storage().instance().set(&DataKey::Owner, &owner); 36 | e.storage().instance().set(&DataKey::Counter, &0); 37 | } 38 | 39 | #[when_not_paused] 40 | pub fn increment(e: &Env) -> i32 { 41 | let mut counter: i32 = 42 | e.storage().instance().get(&DataKey::Counter).expect("counter should be set"); 43 | 44 | counter += 1; 45 | 46 | e.storage().instance().set(&DataKey::Counter, &counter); 47 | 48 | counter 49 | } 50 | 51 | #[when_paused] 52 | pub fn emergency_reset(e: &Env) { 53 | e.storage().instance().set(&DataKey::Counter, &0); 54 | } 55 | } 56 | 57 | #[contractimpl] 58 | impl Pausable for ExampleContract { 59 | fn paused(e: &Env) -> bool { 60 | pausable::paused(e) 61 | } 62 | 63 | fn pause(e: &Env, caller: Address) { 64 | // When `ownable` module is available, 65 | // the following checks should be equivalent to: 66 | // `ownable::only_owner(&e);` 67 | caller.require_auth(); 68 | let owner: Address = 69 | e.storage().instance().get(&DataKey::Owner).expect("owner should be set"); 70 | if owner != caller { 71 | panic_with_error!(e, ExampleContractError::Unauthorized); 72 | } 73 | 74 | pausable::pause(e); 75 | } 76 | 77 | fn unpause(e: &Env, caller: Address) { 78 | // When `ownable` module is available, 79 | // the following checks should be equivalent to: 80 | // `ownable::only_owner(&e);` 81 | caller.require_auth(); 82 | let owner: Address = 83 | e.storage().instance().get(&DataKey::Owner).expect("owner should be set"); 84 | if owner != caller { 85 | panic_with_error!(e, ExampleContractError::Unauthorized); 86 | } 87 | 88 | pausable::unpause(e); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "examples/fungible-allowlist", 5 | "examples/fungible-blocklist", 6 | "examples/fungible-capped", 7 | "examples/fungible-merkle-airdrop", 8 | "examples/fee-forwarder-permissioned", 9 | "examples/fee-forwarder-permissionless", 10 | "examples/fungible-pausable", 11 | "examples/fungible-vault", 12 | "examples/merkle-voting", 13 | "examples/nft-access-control", 14 | "examples/nft-consecutive", 15 | "examples/nft-enumerable", 16 | "examples/nft-royalties", 17 | "examples/nft-sequential-minting", 18 | "examples/ownable", 19 | "examples/pausable", 20 | #"examples/rwa", 21 | "examples/sac-admin-generic", 22 | "examples/sac-admin-wrapper", 23 | "examples/multisig-smart-account/*", 24 | "examples/timelock-controller", 25 | "examples/upgradeable/*", 26 | "packages/accounts", 27 | "packages/access", 28 | "packages/contract-utils", 29 | "packages/fee-abstraction", 30 | "packages/governance", 31 | "packages/macros", 32 | "packages/test-utils/*", 33 | "packages/tokens", 34 | ] 35 | exclude = ["examples/upgradeable/testdata"] 36 | 37 | [workspace.package] 38 | authors = ["OpenZeppelin"] 39 | edition = "2021" 40 | license = "MIT" 41 | repository = "https://github.com/OpenZeppelin/stellar-contracts" 42 | documentation = "https://docs.openzeppelin.com/stellar-contracts/" 43 | keywords = ["stellar", "soroban", "smart-contracts", "standards"] 44 | categories = ["no-std", "wasm"] 45 | version = "0.5.1" 46 | 47 | [workspace.dependencies] 48 | soroban-sdk = "23.0.2" 49 | proc-macro2 = "1.0" 50 | proptest = "1" 51 | quote = "1.0" 52 | syn = { version = "2.0", features = ["full"] } 53 | soroban-test-helpers = "0.2.3" 54 | hex-literal = "1.0.0" 55 | ed25519-dalek = "2.1.1" 56 | k256 = "0.13.4" 57 | p256 = "0.13.2" 58 | serde = { version = "1", default-features = false } 59 | serde-json-core = { version = "0.6.0", default-features = false } 60 | 61 | # members 62 | stellar-access = { path = "packages/access" } 63 | stellar-accounts = { path = "packages/accounts" } 64 | stellar-contract-utils = { path = "packages/contract-utils" } 65 | stellar-event-assertion = { path = "packages/test-utils/event-assertion" } 66 | stellar-fee-abstraction = { path = "packages/fee-abstraction" } 67 | stellar-governance = { path = "packages/governance" } 68 | stellar-tokens = { path = "packages/tokens" } 69 | stellar-macros = { path = "packages/macros" } 70 | 71 | [profile.release] 72 | opt-level = "z" 73 | overflow-checks = true 74 | debug = 0 75 | strip = "symbols" 76 | debug-assertions = false 77 | panic = "abort" 78 | codegen-units = 1 79 | lto = true 80 | 81 | # For more information about this profile see https://developers.stellar.org/docs/build/smart-contracts/example-contracts/logging#cargotoml-profile 82 | [profile.release-with-logs] 83 | inherits = "release" 84 | debug-assertions = true 85 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/capped/storage.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{panic_with_error, symbol_short, Env, Symbol}; 2 | 3 | use crate::fungible::{FungibleTokenError, StorageKey}; 4 | 5 | /// Storage key 6 | pub const CAP_KEY: Symbol = symbol_short!("CAP"); 7 | 8 | /// Set the maximum supply of tokens. 9 | /// 10 | /// # Arguments 11 | /// 12 | /// * `e` - Access to the Soroban environment. 13 | /// 14 | /// # Errors 15 | /// 16 | /// * [`FungibleTokenError::InvalidCap`] - Occurs when the provided cap is 17 | /// negative. 18 | /// 19 | /// # Notes 20 | /// 21 | /// * We recommend using this function in the constructor of your smart 22 | /// contract. 23 | /// * Cap functionality is designed to be used in the `mint` function 24 | /// definition. 25 | /// * This function DOES NOT enforce that the cap must be greater than or equal 26 | /// to the current total supply. While this may deviate from common 27 | /// assumptions (e.g., treating `supply_cap >= total_supply` as an invariant), 28 | /// it allows for more flexible use-cases. For instance, a contract owner 29 | /// might decide to permanently reduce the token supply by burning tokens 30 | /// later, and setting a lower cap ahead of time effectively prevents any 31 | /// further minting until the total supply falls below the new cap. 32 | pub fn set_cap(e: &Env, cap: i128) { 33 | if cap < 0 { 34 | panic_with_error!(e, FungibleTokenError::InvalidCap); 35 | } 36 | e.storage().instance().set(&CAP_KEY, &cap); 37 | } 38 | 39 | /// Returns the maximum supply of tokens. 40 | /// 41 | /// # Arguments 42 | /// 43 | /// * `e` - Access to the Soroban environment. 44 | /// 45 | /// # Errors 46 | /// 47 | /// * [`FungibleTokenError::CapNotSet`] - Occurs when the cap has not been set. 48 | pub fn query_cap(e: &Env) -> i128 { 49 | e.storage() 50 | .instance() 51 | .get(&CAP_KEY) 52 | .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::CapNotSet)) 53 | } 54 | 55 | /// Panics if new `amount` of tokens added to the current supply will exceed the 56 | /// maximum supply. 57 | /// 58 | /// # Arguments 59 | /// 60 | /// * `e` - Access to the Soroban environment. 61 | /// * `amount` - The new amount of tokens to be added to the total supply. 62 | /// 63 | /// # Errors 64 | /// 65 | /// * refer to [`query_cap`] errors. 66 | /// * [`FungibleTokenError::MathOverflow`] - Occurs when the sum of the new 67 | /// amount and the current total supply will overflow. 68 | /// * [`FungibleTokenError::ExceededCap`] - Occurs when the new amount of tokens 69 | /// will exceed the cap. 70 | pub fn check_cap(e: &Env, amount: i128) { 71 | let cap: i128 = query_cap(e); 72 | let total_supply: i128 = e.storage().instance().get(&StorageKey::TotalSupply).unwrap_or(0); 73 | let Some(sum) = total_supply.checked_add(amount) else { 74 | panic_with_error!(e, FungibleTokenError::MathOverflow); 75 | }; 76 | if cap < sum { 77 | panic_with_error!(e, FungibleTokenError::ExceededCap); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/fee-abstraction/README.md: -------------------------------------------------------------------------------- 1 | # Fee Abstraction 2 | 3 | Utilities for implementing fee abstraction (fee forwarding) for interacting with Soroban contracts, allowing users to pay transaction fees in tokens (e.g., USDC) instead of native XLM. 4 | 5 | ## Overview 6 | 7 | Fee abstraction enables a better UX by letting users pay for transactions with tokens they already hold. Another actor, called relayer, covers the XLM network fees and is compensated in the user's chosen token. 8 | 9 | The flow involves an off-chain negotiation between the user and a relayer (quote request, fee agreement), but the actual execution happens through an intermediary contract called **FeeForwarder**. This contract enforces that the user is charged at most `max_fee_amount` (the cap they signed). The relayer determines the actual `fee_amount` at submission time based on network conditions, but can never exceed the user's authorized maximum. 10 | 11 | ```mermaid 12 | sequenceDiagram 13 | actor User 14 | actor Relayer 15 | participant FeeForwarder 16 | participant Token 17 | participant Target Contract 18 | 19 | User->>User: 1. Prepare call to Target.target_fn() 20 | User->>Relayer: 2. Request quote (fee token, expiration, target) 21 | Relayer-->>User: Quote: max_fee_amount 22 | 23 | User->>User: 3. Sign authorization for FeeForwarder.forward()
including subinvocations for:
Token.approve() and Target.target_fn() 24 | User->>Relayer: 4. Hand over signed authorization entry 25 | 26 | Relayer->>Relayer: 5. Verify params satisfy requirements
(fee amount, token, fee recipient, ...) 27 | Relayer->>Relayer: 6. Sign as source_account 28 | Relayer->>FeeForwarder: 7. Execute forward():
submit a tx and pay XLM fees 29 | 30 | FeeForwarder->>FeeForwarder: 8. Validate authorizations 31 | FeeForwarder->>Token: 9. Approve max fee amount (optional) 32 | FeeForwarder->>Token: 10. Transfer fee amount to fee recipient 33 | FeeForwarder->>Target Contract: 11. Invoke target_fn(target_args) 34 | Target Contract-->>User: Result 35 | ``` 36 | 37 | ## Features 38 | 39 | - Fee Collection Helpers (eager and lazy approval strategies) 40 | - Invoker Helper handling user-side authorizations 41 | - Optional Fee Token Allowlist 42 | - Optional Token Sweeping (fees are collected on the forwarding contract and transferred later on) 43 | - Validation Utilities 44 | 45 | ## Examples 46 | 47 | - [fee-forwarder-permissioned](../../examples/fee-forwarder-permissioned) 48 | 49 | - Only trusted executors can call `forward`. 50 | - The forwarder contract itself collects the fees, which can be swept later. 51 | 52 | - [fee-forwarder-permissionless](../../examples/fee-forwarder-permissionless) 53 | - Anyone can call `forward`; there is no executor allowlist. 54 | - The relayer (transaction submitter) receives the collected fee. 55 | 56 | Together, these examples show how to combine the auth helper 57 | (`auth_user_and_invoke`) with the shared fee collection helper (`collect_fee`) 58 | by selecting the appropriate [`FeeAbstractionApproval`] strategy (`Eager` or 59 | `Lazy`) for different use cases. 60 | -------------------------------------------------------------------------------- /examples/merkle-voting/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! # Merkle-Based Voting Contract Example 2 | //! 3 | //! This contract demonstrates how to implement a secure on-chain voting 4 | //! mechanism using Merkle proofs. 5 | //! 6 | //! Eligible voters are encoded as leaf nodes in a Merkle tree, each containing: 7 | //! 8 | //! - `index: u32` — a unique identifier for the voter 9 | //! - `account: Address` — the voter's address 10 | //! - `voting_power: i128` — the weight of their vote 11 | //! 12 | //! To vote, users submit their `VoteData` and a Merkle proof verifying 13 | //! inclusion in the tree. The contract ensures that each vote can only be cast 14 | //! once (via the index) and tallies the result based on the `approve` flag. 15 | //! 16 | //! This pattern is useful for snapshot-based governance systems or off-chain 17 | //! voter lists. 18 | use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Vec}; 19 | use stellar_contract_utils::{ 20 | crypto::sha256::Sha256, 21 | merkle_distributor::{IndexableLeaf, MerkleDistributor}, 22 | }; 23 | 24 | type Distributor = MerkleDistributor; 25 | 26 | #[contracttype] 27 | #[derive(Clone)] 28 | pub struct VoteData { 29 | pub index: u32, 30 | pub account: Address, 31 | pub voting_power: i128, 32 | } 33 | 34 | impl IndexableLeaf for VoteData { 35 | fn index(&self) -> u32 { 36 | self.index 37 | } 38 | } 39 | 40 | #[contracttype] 41 | pub enum DataKey { 42 | TotalVotesPro, 43 | TotalVotesAgainst, 44 | } 45 | 46 | #[contract] 47 | pub struct MerkleVoting; 48 | 49 | #[contractimpl] 50 | impl MerkleVoting { 51 | pub fn __constructor(e: Env, root_hash: BytesN<32>) { 52 | Distributor::set_root(&e, root_hash); 53 | e.storage().instance().set(&DataKey::TotalVotesPro, &0i128); 54 | e.storage().instance().set(&DataKey::TotalVotesAgainst, &0i128); 55 | } 56 | 57 | pub fn vote(e: &Env, vote_data: VoteData, proof: Vec>, approve: bool) { 58 | // Verify merkle proof using the MerkleDistributor 59 | Distributor::verify_and_set_claimed(e, vote_data.clone(), proof); 60 | 61 | // Update vote totals 62 | if approve { 63 | let current_pro: i128 = e.storage().instance().get(&DataKey::TotalVotesPro).unwrap(); 64 | e.storage() 65 | .instance() 66 | .set(&DataKey::TotalVotesPro, &(current_pro + vote_data.voting_power)); 67 | } else { 68 | let current_against: i128 = 69 | e.storage().instance().get(&DataKey::TotalVotesAgainst).unwrap(); 70 | e.storage() 71 | .instance() 72 | .set(&DataKey::TotalVotesAgainst, &(current_against + vote_data.voting_power)); 73 | } 74 | } 75 | 76 | pub fn has_voted(e: &Env, index: u32) -> bool { 77 | Distributor::is_claimed(e, index) 78 | } 79 | 80 | pub fn get_vote_results(e: Env) -> (i128, i128) { 81 | let votes_pro: i128 = e.storage().instance().get(&DataKey::TotalVotesPro).unwrap_or(0); 82 | let votes_against: i128 = 83 | e.storage().instance().get(&DataKey::TotalVotesAgainst).unwrap_or(0); 84 | (votes_pro, votes_against) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/contract-utils/src/pausable/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{contract, testutils::Events, vec, Env, IntoVal, Symbol}; 4 | 5 | use crate::pausable::storage::{ 6 | pause, paused, unpause, when_not_paused, when_paused, PausableStorageKey, 7 | }; 8 | 9 | #[contract] 10 | struct MockContract; 11 | 12 | #[test] 13 | fn initial_state() { 14 | let e = Env::default(); 15 | let address = e.register(MockContract, ()); 16 | 17 | e.as_contract(&address, || { 18 | assert!(!paused(&e)); 19 | }); 20 | } 21 | 22 | #[test] 23 | fn pause_works() { 24 | let e = Env::default(); 25 | let address = e.register(MockContract, ()); 26 | 27 | e.as_contract(&address, || { 28 | // Test pause 29 | pause(&e); 30 | assert!(paused(&e)); 31 | 32 | let events = e.events().all(); 33 | assert_eq!(events.len(), 1); 34 | let event = events.get(0).unwrap(); 35 | assert_eq!(event.0, address); 36 | assert_eq!(event.1, vec![&e, Symbol::new(&e, "paused").into_val(&e)]); 37 | }); 38 | } 39 | 40 | #[test] 41 | fn unpause_works() { 42 | let e = Env::default(); 43 | let address = e.register(MockContract, ()); 44 | 45 | e.as_contract(&address, || { 46 | // Manually set storage 47 | e.storage().instance().set(&PausableStorageKey::Paused, &true); 48 | 49 | // Test unpause 50 | unpause(&e); 51 | assert!(!paused(&e)); 52 | let events = e.events().all(); 53 | assert_eq!(events.len(), 1); 54 | let event = events.get(0).unwrap(); 55 | assert_eq!(event.0, address); 56 | assert_eq!(event.1, vec![&e, Symbol::new(&e, "unpaused").into_val(&e)]); 57 | }); 58 | } 59 | 60 | #[test] 61 | #[should_panic(expected = "Error(Contract, #1000)")] 62 | fn errors_pause_when_paused() { 63 | let e = Env::default(); 64 | let address = e.register(MockContract, ()); 65 | 66 | e.as_contract(&address, || { 67 | // Manually set storage 68 | e.storage().instance().set(&PausableStorageKey::Paused, &true); 69 | // Should panic when trying to pause again 70 | pause(&e); 71 | }); 72 | } 73 | 74 | #[test] 75 | #[should_panic(expected = "Error(Contract, #1001)")] 76 | fn errors_unpause_when_not_paused() { 77 | let e = Env::default(); 78 | let address = e.register(MockContract, ()); 79 | 80 | e.as_contract(&address, || { 81 | // Should panic when trying to unpause while not paused 82 | unpause(&e); 83 | }); 84 | } 85 | 86 | #[test] 87 | fn when_not_paused_works() { 88 | let e = Env::default(); 89 | let address = e.register(MockContract, ()); 90 | 91 | e.as_contract(&address, || { 92 | // Should not panic when contract is not paused 93 | when_not_paused(&e); 94 | }); 95 | } 96 | 97 | #[test] 98 | fn when_paused_works() { 99 | let e = Env::default(); 100 | let address = e.register(MockContract, ()); 101 | 102 | e.as_contract(&address, || { 103 | pause(&e); 104 | // Should not panic when contract is paused 105 | when_paused(&e); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /examples/nft-royalties/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Non-Fungible Royalties Example Contract. 2 | //! 3 | //! Demonstrates an example usage of the Royalties extension, allowing for 4 | //! setting and querying royalty information for NFTs following the ERC2981 5 | //! standard. 6 | 7 | use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String}; 8 | use stellar_access::access_control::{self as access_control, AccessControl}; 9 | use stellar_macros::{default_impl, only_admin, only_role}; 10 | use stellar_tokens::non_fungible::{royalties::NonFungibleRoyalties, Base, NonFungibleToken}; 11 | 12 | #[contract] 13 | pub struct ExampleContract; 14 | 15 | #[contractimpl] 16 | impl ExampleContract { 17 | pub fn __constructor( 18 | e: &Env, 19 | uri: String, 20 | name: String, 21 | symbol: String, 22 | admin: Address, 23 | manager: Address, 24 | ) { 25 | Base::set_metadata(e, uri, name, symbol); 26 | 27 | // Set default royalty for the entire collection (10%) 28 | Base::set_default_royalty(e, &admin, 1000); 29 | 30 | access_control::set_admin(e, &admin); 31 | 32 | // create a role "manager" and grant it to `manager` 33 | access_control::grant_role_no_auth(e, &manager, &symbol_short!("manager"), &admin); 34 | } 35 | 36 | #[only_admin] 37 | pub fn mint(e: &Env, to: Address) -> u32 { 38 | // Mint token with sequential ID 39 | Base::sequential_mint(e, &to) 40 | } 41 | 42 | #[only_admin] 43 | pub fn mint_with_royalty(e: &Env, to: Address, receiver: Address, basis_points: u32) -> u32 { 44 | // Mint token with sequential ID 45 | let token_id = Base::sequential_mint(e, &to); 46 | 47 | // Set token-specific royalty 48 | Base::set_token_royalty(e, token_id, &receiver, basis_points); 49 | 50 | token_id 51 | } 52 | 53 | pub fn get_royalty_info(e: &Env, token_id: u32, sale_price: i128) -> (Address, i128) { 54 | Base::royalty_info(e, token_id, sale_price) 55 | } 56 | } 57 | 58 | #[default_impl] 59 | #[contractimpl] 60 | impl NonFungibleToken for ExampleContract { 61 | type ContractType = Base; 62 | } 63 | 64 | #[contractimpl] 65 | impl NonFungibleRoyalties for ExampleContract { 66 | #[only_role(operator, "manager")] 67 | fn set_default_royalty(e: &Env, receiver: Address, basis_points: u32, operator: Address) { 68 | Base::set_default_royalty(e, &receiver, basis_points); 69 | } 70 | 71 | #[only_role(operator, "manager")] 72 | fn set_token_royalty( 73 | e: &Env, 74 | token_id: u32, 75 | receiver: Address, 76 | basis_points: u32, 77 | operator: Address, 78 | ) { 79 | Base::set_token_royalty(e, token_id, &receiver, basis_points); 80 | } 81 | 82 | #[only_role(operator, "manager")] 83 | fn remove_token_royalty(e: &Env, token_id: u32, operator: Address) { 84 | Base::remove_token_royalty(e, token_id); 85 | } 86 | 87 | fn royalty_info(e: &Env, token_id: u32, sale_price: i128) -> (Address, i128) { 88 | Base::royalty_info(e, token_id, sale_price) 89 | } 90 | } 91 | 92 | #[default_impl] 93 | #[contractimpl] 94 | impl AccessControl for ExampleContract {} 95 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/utils/sac_admin_generic/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Stellar Asset Contract (SAC) Admin Generic Module 2 | //! 3 | //! The Stellar Asset Contract (SAC) serves as a bridge between traditional 4 | //! Stellar network assets and the Soroban smart contract environment. 5 | //! When a classic Stellar asset is ported to Soroban, it is represented by 6 | //! a SAC - a smart contract that provides both user-facing and administrative 7 | //! functions for asset management. 8 | //! 9 | //! SACs expose standard functions for handling fungible tokens, such as 10 | //! `transfer`, `approve`, `burn`, etc. Additionally, they include 11 | //! administrative functions (`mint`, `clawback`, `set_admin`, `set_authorized`) 12 | //! that are initially restricted to the issuer (a G-account). 13 | //! 14 | //! The `set_admin` function enables transferring administrative control to a 15 | //! custom contract, allowing for more complex authorization logic. This 16 | //! flexibility opens up possibilities for implementing custom rules, such as 17 | //! role-based access control, two-step admin transfers, mint rate limits, and 18 | //! upgradeability. 19 | //! 20 | //! When implementing a SAC Admin smart contract, there are two main approaches: 21 | //! 22 | //! - **Generic Approach:** 23 | //! - The new admin contract leverages the `__check_auth` function to handle 24 | //! authentication and authorization logic. 25 | //! - This approach allows for injecting any custom authorization logic while 26 | //! maintaining a unified interface for both user-facing and admin 27 | //! functions. 28 | //! 29 | //! - **Wrapper Approach:** 30 | //! - The new admin contract acts as a middleware, defining specific entry 31 | //! points for each admin function and forwarding calls to the corresponding 32 | //! SAC functions. 33 | //! - Custom logic is applied before forwarding the call, providing a 34 | //! straightforward and modular design, though at the cost of splitting 35 | //! user-facing and admin interfaces. 36 | //! 37 | //! _Trade-offs_ 38 | //! - The generic approach maintains a single interface but requires a more 39 | //! sophisticated authorization mechanism. 40 | //! - The wrapper approach is simpler to implement and more flexible but 41 | //! requires additional entry points for each admin function. 42 | //! 43 | //! ## Security Warning 44 | //! 45 | //! When changing a SAC Admin address, the new admin address provided is not 46 | //! validated at that time. This means administration from the SAC can be lock 47 | //! down forever. The choice of the new SAC Admin address must be carefully and 48 | //! thoroughly considered. 49 | //! 50 | //! ## Module Overview 51 | //! 52 | //! This module provides helper functions for implementing **the generic 53 | //! version** for a SAC Admin contract. 54 | //! 55 | //! - An example contract that follows this approach can be found in 56 | //! "examples/sac-admin-generic". 57 | //! - An example flow when a `SACAdminGeneric` contract is set as a new 58 | //! administrator for a SAC can be found [here](./README.md); 59 | 60 | mod storage; 61 | pub use storage::{ 62 | extract_sac_contract_context, get_fn_param, get_sac_address, set_sac_address, SacFn, 63 | }; 64 | 65 | #[cfg(test)] 66 | mod test; 67 | -------------------------------------------------------------------------------- /packages/accounts/src/verifiers/utils/base64_url.rs: -------------------------------------------------------------------------------- 1 | // Ported from 2 | // * https://github.com/golang/go/blob/26b5783b72376acd0386f78295e678b9a6bff30e/src/encoding/base64/base64.go#L53-L192 3 | // * https://github.com/kalepail/passkey-kit/blob/next/contracts/smart-wallet/src/base64_url.rs 4 | // 5 | // Modifications: 6 | // * Removed logic supporting padding. 7 | // * Hardcoded the Base64 URL alphabet. 8 | // * Use a fixed length pre-allocated destination. 9 | // * Ported to Rust. 10 | // 11 | // Original Copyright notice: 12 | // 13 | // Copyright (c) 2009 The Go Authors. All rights reserved. 14 | // Redistribution and use in source and binary forms, with or without 15 | // modification, are permitted provided that the following conditions are 16 | // met: 17 | // 18 | // * Redistributions of source code must retain the above copyright 19 | // notice, this list of conditions and the following disclaimer. 20 | // * Redistributions in binary form must reproduce the above 21 | // copyright notice, this list of conditions and the following disclaimer 22 | // in the documentation and/or other materials provided with the 23 | // distribution. 24 | // * Neither the name of Google Inc. nor the names of its 25 | // contributors may be used to endorse or promote products derived from 26 | // this software without specific prior written permission. 27 | // 28 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 29 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 30 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 31 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 32 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 33 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 34 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 35 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 36 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 37 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 38 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | 40 | const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 41 | 42 | pub fn base64_url_encode(dst: &mut [u8], src: &[u8]) { 43 | let mut di: usize = 0; 44 | let mut si: usize = 0; 45 | let n = (src.len() / 3) * 3; // (.. / 3 * 3) to ensure a % 3 `n` 46 | 47 | while si < n { 48 | let val = (src[si] as usize) << 16 | (src[si + 1] as usize) << 8 | (src[si + 2] as usize); 49 | dst[di] = ALPHABET[val >> 18 & 0x3F]; 50 | dst[di + 1] = ALPHABET[val >> 12 & 0x3F]; 51 | dst[di + 2] = ALPHABET[val >> 6 & 0x3F]; 52 | dst[di + 3] = ALPHABET[val & 0x3F]; 53 | si += 3; 54 | di += 4; 55 | } 56 | 57 | let remain = src.len() - si; 58 | 59 | if remain == 0 { 60 | return; 61 | } 62 | 63 | let mut val = (src[si] as usize) << 16; 64 | 65 | if remain == 2 { 66 | val |= (src[si + 1] as usize) << 8; 67 | } 68 | 69 | dst[di] = ALPHABET[val >> 18 & 0x3F]; 70 | dst[di + 1] = ALPHABET[val >> 12 & 0x3F]; 71 | 72 | if remain == 2 { 73 | dst[di + 2] = ALPHABET[val >> 6 & 0x3F]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/burnable/mod.rs: -------------------------------------------------------------------------------- 1 | mod storage; 2 | 3 | #[cfg(test)] 4 | mod test; 5 | 6 | use soroban_sdk::{contractevent, Address, Env}; 7 | 8 | use crate::fungible::FungibleToken; 9 | 10 | /// Burnable Trait for Fungible Token 11 | /// 12 | /// The `FungibleBurnable` trait extends the `FungibleToken` trait to provide 13 | /// the capability to burn tokens. This trait is designed to be used in 14 | /// conjunction with the `FungibleToken` trait. 15 | /// 16 | /// To fully comply with the SEP-41 specification one have to implement the 17 | /// this `FungibleBurnable` trait along with the `[FungibleToken]` trait. 18 | /// SEP-41 mandates support for token burning to be considered compliant. 19 | /// 20 | /// Excluding the `burn` functionality from the `[FungibleToken]` trait 21 | /// is a deliberate design choice to accommodate flexibility and customization 22 | /// for various smart contract use cases. 23 | pub trait FungibleBurnable: FungibleToken { 24 | /// Destroys `amount` of tokens from `from`. Updates the total 25 | /// supply accordingly. 26 | /// 27 | /// # Arguments 28 | /// 29 | /// * `e` - Access to the Soroban environment. 30 | /// * `from` - The account whose tokens are destroyed. 31 | /// * `amount` - The amount of tokens to burn. 32 | /// 33 | /// # Errors 34 | /// 35 | /// * [`crate::fungible::FungibleTokenError::InsufficientBalance`] - When 36 | /// attempting to burn more tokens than `from` current balance. 37 | /// * [`crate::fungible::FungibleTokenError::LessThanZero`] - When `amount < 38 | /// 0`. 39 | /// 40 | /// # Events 41 | /// 42 | /// * topics - `["burn", from: Address]` 43 | /// * data - `[amount: i128]` 44 | fn burn(e: &Env, from: Address, amount: i128); 45 | 46 | /// Destroys `amount` of tokens from `from`. Updates the total 47 | /// supply accordingly. 48 | /// 49 | /// # Arguments 50 | /// 51 | /// * `e` - Access to the Soroban environment. 52 | /// * `spender` - The address authorized to burn the tokens. 53 | /// * `from` - The account whose tokens are destroyed. 54 | /// * `amount` - The amount of tokens to burn. 55 | /// 56 | /// # Errors 57 | /// 58 | /// * [`crate::fungible::FungibleTokenError::InsufficientBalance`] - When 59 | /// attempting to burn more tokens than `from` current balance. 60 | /// * [`crate::fungible::FungibleTokenError::InsufficientAllowance`] - When 61 | /// attempting to burn more tokens than `from` allowance. 62 | /// * [`crate::fungible::FungibleTokenError::LessThanZero`] - When `amount < 63 | /// 0`. 64 | /// 65 | /// # Events 66 | /// 67 | /// * topics - `["burn", from: Address]` 68 | /// * data - `[amount: i128]` 69 | fn burn_from(e: &Env, spender: Address, from: Address, amount: i128); 70 | } 71 | 72 | // ################## EVENTS ################## 73 | 74 | /// Event emitted when tokens are burned. 75 | #[contractevent] 76 | #[derive(Clone, Debug, Eq, PartialEq)] 77 | pub struct Burn { 78 | #[topic] 79 | pub from: Address, 80 | pub amount: i128, 81 | } 82 | 83 | /// Emits an event indicating a burn of tokens. 84 | /// 85 | /// # Arguments 86 | /// 87 | /// * `e` - Access to Soroban environment. 88 | /// * `from` - The address holding the tokens. 89 | /// * `amount` - The amount of tokens to be burned. 90 | pub fn emit_burn(e: &Env, from: &Address, amount: i128) { 91 | Burn { from: from.clone(), amount }.publish(e); 92 | } 93 | -------------------------------------------------------------------------------- /packages/macros/src/helpers.rs: -------------------------------------------------------------------------------- 1 | //! This crate is a collection of utility functions for stellar related macros. 2 | //! It is not intended to be used directly, but rather imported into other 3 | //! macros. 4 | 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath}; 8 | 9 | /// Parses the environment argument from the function signature 10 | pub fn parse_env_arg(input_fn: &ItemFn) -> TokenStream { 11 | let (env_ident, is_ref) = check_env_arg(input_fn); 12 | 13 | if is_ref { 14 | quote! { #env_ident } 15 | } else { 16 | quote! { &#env_ident } 17 | } 18 | } 19 | 20 | fn check_env_arg(input_fn: &ItemFn) -> (Ident, bool) { 21 | // Get the first argument 22 | let first_arg = input_fn.sig.inputs.first().unwrap_or_else(|| { 23 | panic!("function '{}' must have at least one argument", input_fn.sig.ident) 24 | }); 25 | 26 | // Extract the pattern and type from the argument 27 | let FnArg::Typed(PatType { pat, ty, .. }) = first_arg else { 28 | panic!("first argument of function '{}' must be a typed parameter", input_fn.sig.ident); 29 | }; 30 | 31 | // Get the identifier from the pattern 32 | let Pat::Ident(pat_ident) = &**pat else { 33 | panic!("first argument of function '{}' must be an identifier", input_fn.sig.ident); 34 | }; 35 | let ident = pat_ident.ident.clone(); 36 | 37 | // Check if the type is Env or &Env 38 | let is_ref = match &**ty { 39 | Type::Reference(type_ref) => { 40 | let Type::Path(path) = &*type_ref.elem else { 41 | panic!("first argument of function '{}' must be Env or &Env", input_fn.sig.ident); 42 | }; 43 | check_is_env(path, &input_fn.sig.ident); 44 | true 45 | } 46 | Type::Path(path) => { 47 | check_is_env(path, &input_fn.sig.ident); 48 | false 49 | } 50 | _ => panic!("first argument of function '{}' must be Env or &Env", input_fn.sig.ident), 51 | }; 52 | 53 | (ident, is_ref) 54 | } 55 | 56 | fn check_is_env(path: &TypePath, fn_name: &Ident) { 57 | let is_env = path.path.segments.last().map(|seg| seg.ident == "Env").unwrap_or(false); 58 | 59 | if !is_env { 60 | panic!("first argument of function '{fn_name}' must be Env or &Env",); 61 | } 62 | } 63 | 64 | /// Generates a function that enforces authorization for a specific role 65 | /// 66 | /// This function is used by macros like `only_owner` and `only_admin` to 67 | /// generate code that checks authorization before executing the function body. 68 | /// 69 | /// # Arguments 70 | /// 71 | /// * `input_fn` - The function to wrap with authorization check 72 | /// * `auth_check_func` - The function to be called to enforce authorization 73 | /// (e.g., `stellar_access::ownable::enforce_owner_auth`) 74 | /// 75 | /// # Returns 76 | /// 77 | /// A TokenStream containing the function with authorization check added 78 | pub fn generate_auth_check(input_fn: &ItemFn, auth_check_func: TokenStream) -> TokenStream { 79 | // Get the environment parameter 80 | let env_param = parse_env_arg(input_fn); 81 | 82 | // Extract function components 83 | let fn_attrs = &input_fn.attrs; 84 | let fn_vis = &input_fn.vis; 85 | let fn_sig = &input_fn.sig; 86 | let fn_block = &input_fn.block; 87 | 88 | // Generate the expanded function with authorization check 89 | quote! { 90 | #(#fn_attrs)* 91 | #fn_vis #fn_sig { 92 | #auth_check_func(#env_param); 93 | #fn_block 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /examples/multisig-smart-account/threshold-policy/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! # Simple Threshold Policy Contract 2 | //! 3 | //! A reusable policy contract that implements simple threshold-based 4 | //! authorization. This contract can be deployed once and used by multiple smart 5 | //! accounts, with each account defining its own threshold value for different 6 | //! context rules. Enables M-of-N multisig functionality where M signers out of 7 | //! N total signers must authorize a transaction. 8 | use soroban_sdk::{auth::Context, contract, contractimpl, Address, Env, Vec}; 9 | use stellar_accounts::{ 10 | policies::{simple_threshold, Policy}, 11 | smart_account::{ContextRule, Signer}, 12 | }; 13 | 14 | #[contract] 15 | pub struct ThresholdPolicyContract; 16 | 17 | #[contractimpl] 18 | impl Policy for ThresholdPolicyContract { 19 | type AccountParams = simple_threshold::SimpleThresholdAccountParams; 20 | 21 | /// Check if the threshold policy can be enforced. 22 | /// 23 | /// Verifies that the number of authenticated signers meets or exceeds 24 | /// the configured threshold for this context rule. 25 | /// 26 | /// # Returns 27 | /// 28 | /// * `true` if the threshold is met 29 | /// * `false` otherwise 30 | fn can_enforce( 31 | e: &Env, 32 | context: Context, 33 | authenticated_signers: Vec, 34 | context_rule: ContextRule, 35 | smart_account: Address, 36 | ) -> bool { 37 | simple_threshold::can_enforce( 38 | e, 39 | &context, 40 | &authenticated_signers, 41 | &context_rule, 42 | &smart_account, 43 | ) 44 | } 45 | 46 | /// Enforce the threshold policy. 47 | /// 48 | /// Records that authorization occurred and emits an event. 49 | /// This is called after `can_enforce` returns true. 50 | fn enforce( 51 | e: &Env, 52 | context: Context, 53 | authenticated_signers: Vec, 54 | context_rule: ContextRule, 55 | smart_account: Address, 56 | ) { 57 | simple_threshold::enforce( 58 | e, 59 | &context, 60 | &authenticated_signers, 61 | &context_rule, 62 | &smart_account, 63 | ) 64 | } 65 | 66 | /// Install the threshold policy for a smart account. 67 | /// 68 | /// Stores the threshold configuration for the given context rule. 69 | fn install( 70 | e: &Env, 71 | install_params: Self::AccountParams, 72 | context_rule: ContextRule, 73 | smart_account: Address, 74 | ) { 75 | simple_threshold::install(e, &install_params, &context_rule, &smart_account) 76 | } 77 | 78 | /// Uninstall the threshold policy for a smart account. 79 | /// 80 | /// Removes the threshold configuration for the given context rule. 81 | fn uninstall(e: &Env, context_rule: ContextRule, smart_account: Address) { 82 | simple_threshold::uninstall(e, &context_rule, &smart_account) 83 | } 84 | } 85 | 86 | #[contractimpl] 87 | impl ThresholdPolicyContract { 88 | /// Get the current threshold for a smart account 89 | pub fn get_threshold(e: &Env, context_rule_id: u32, smart_account: Address) -> u32 { 90 | simple_threshold::get_threshold(e, context_rule_id, &smart_account) 91 | } 92 | 93 | /// Set a new threshold for a smart account 94 | pub fn set_threshold( 95 | e: Env, 96 | threshold: u32, 97 | context_rule: ContextRule, 98 | smart_account: Address, 99 | ) { 100 | simple_threshold::set_threshold(&e, threshold, &context_rule, &smart_account) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/nft-consecutive/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Non-Fungible Consecutive Example Contract. 2 | //! 3 | //! Demonstrates an example usage of the Consecutive extension, enabling 4 | //! efficient batch minting in a single transaction. 5 | 6 | use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; 7 | use stellar_tokens::non_fungible::{ 8 | burnable::NonFungibleBurnable, 9 | consecutive::{Consecutive, NonFungibleConsecutive}, 10 | Base, ContractOverrides, NonFungibleToken, 11 | }; 12 | 13 | #[contracttype] 14 | pub enum DataKey { 15 | Owner, 16 | } 17 | 18 | #[contract] 19 | pub struct ExampleContract; 20 | 21 | #[contractimpl] 22 | impl ExampleContract { 23 | pub fn __constructor(e: &Env, uri: String, name: String, symbol: String, owner: Address) { 24 | e.storage().instance().set(&DataKey::Owner, &owner); 25 | Base::set_metadata(e, uri, name, symbol); 26 | } 27 | 28 | pub fn batch_mint(e: &Env, to: Address, amount: u32) -> u32 { 29 | let owner: Address = 30 | e.storage().instance().get(&DataKey::Owner).expect("owner should be set"); 31 | owner.require_auth(); 32 | Consecutive::batch_mint(e, &to, amount) 33 | } 34 | } 35 | 36 | // You don't have to provide the implementations for all the methods, 37 | // `#[default_impl]` macro does this for you. This example showcases 38 | // what is happening under the hood when you use `#[default_impl]` macro. 39 | #[contractimpl] 40 | impl NonFungibleToken for ExampleContract { 41 | type ContractType = Consecutive; 42 | 43 | fn balance(e: &Env, owner: Address) -> u32 { 44 | Self::ContractType::balance(e, &owner) 45 | } 46 | 47 | fn owner_of(e: &Env, token_id: u32) -> Address { 48 | Self::ContractType::owner_of(e, token_id) 49 | } 50 | 51 | fn transfer(e: &Env, from: Address, to: Address, token_id: u32) { 52 | Self::ContractType::transfer(e, &from, &to, token_id); 53 | } 54 | 55 | fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, token_id: u32) { 56 | Self::ContractType::transfer_from(e, &spender, &from, &to, token_id); 57 | } 58 | 59 | fn approve( 60 | e: &Env, 61 | approver: Address, 62 | approved: Address, 63 | token_id: u32, 64 | live_until_ledger: u32, 65 | ) { 66 | Self::ContractType::approve(e, &approver, &approved, token_id, live_until_ledger); 67 | } 68 | 69 | fn approve_for_all(e: &Env, owner: Address, operator: Address, live_until_ledger: u32) { 70 | Self::ContractType::approve_for_all(e, &owner, &operator, live_until_ledger); 71 | } 72 | 73 | fn get_approved(e: &Env, token_id: u32) -> Option
{ 74 | Self::ContractType::get_approved(e, token_id) 75 | } 76 | 77 | fn is_approved_for_all(e: &Env, owner: Address, operator: Address) -> bool { 78 | Self::ContractType::is_approved_for_all(e, &owner, &operator) 79 | } 80 | 81 | fn name(e: &Env) -> String { 82 | Self::ContractType::name(e) 83 | } 84 | 85 | fn symbol(e: &Env) -> String { 86 | Self::ContractType::symbol(e) 87 | } 88 | 89 | fn token_uri(e: &Env, token_id: u32) -> String { 90 | Self::ContractType::token_uri(e, token_id) 91 | } 92 | } 93 | 94 | impl NonFungibleConsecutive for ExampleContract {} 95 | 96 | #[contractimpl] 97 | impl NonFungibleBurnable for ExampleContract { 98 | fn burn(e: &Env, from: Address, token_id: u32) { 99 | Self::ContractType::burn(e, &from, token_id); 100 | } 101 | 102 | fn burn_from(e: &Env, spender: Address, from: Address, token_id: u32) { 103 | Self::ContractType::burn_from(e, &spender, &from, token_id); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/contract-utils/src/merkle_distributor/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Merkle Distributor 2 | //! 3 | //! This module implements a Merkle-based claim distribution system using Merkle 4 | //! proofs for verification. 5 | //! 6 | //! ## Implementation Notes 7 | //! 8 | //! Claims are **indexed by a `u32` index**, corresponding to the position of 9 | //! each leaf in the original Merkle tree. 10 | //! 11 | //! ### Requirements for Leaf Structure 12 | //! 13 | //! - Each node (leaf) **MUST** include an indexable field of type `u32` and 14 | //! implement the `IndexableLeaf`. 15 | //! - Aside from the `index`, the node can contain any additional fields, with 16 | //! any names and types, depending on the specific use case (e.g., `address`, 17 | //! `amount`, `token_id`, etc.). 18 | //! - When constructing the Merkle tree, ensure that the `index` values are 19 | //! unique and consecutive (or at least unique). 20 | //! 21 | //! ### Example 22 | //! 23 | //! ```ignore,rust 24 | //! use soroban_sdk::contracttype; 25 | //! use stellar_merkle_distributor::IndexableLeaf; 26 | //! 27 | //! #[contracttype] 28 | //! struct LeafData { 29 | //! pub index: u32, 30 | //! pub address: Address, 31 | //! pub amount: i128, 32 | //! } 33 | //! 34 | //! impl IndexableLeaf for LeafData { 35 | //! fn index(&self) -> u32 { 36 | //! self.index 37 | //! } 38 | //! } 39 | //! ``` 40 | //! 41 | //! This structure supports a wide variety of distribution mechanisms such as: 42 | //! 43 | //! - Token airdrops 44 | //! - NFT distributions 45 | //! - Off-chain allowlists 46 | //! - Snapshot-based voting 47 | //! - Custom claim logic involving metadata 48 | 49 | mod storage; 50 | 51 | #[cfg(test)] 52 | mod test; 53 | 54 | use core::marker::PhantomData; 55 | 56 | use soroban_sdk::{contracterror, contractevent, Bytes, Env, Val}; 57 | 58 | use crate::crypto::hasher::Hasher; 59 | pub use crate::merkle_distributor::storage::MerkleDistributorStorageKey; 60 | 61 | pub trait IndexableLeaf { 62 | fn index(&self) -> u32; 63 | } 64 | 65 | pub struct MerkleDistributor(PhantomData); 66 | 67 | // ################## ERRORS ################## 68 | 69 | #[contracterror] 70 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 71 | #[repr(u32)] 72 | pub enum MerkleDistributorError { 73 | /// The merkle root is not set. 74 | RootNotSet = 1300, 75 | /// The provided index was already claimed. 76 | IndexAlreadyClaimed = 1301, 77 | /// The proof is invalid. 78 | InvalidProof = 1302, 79 | } 80 | 81 | // ################## CONSTANTS ################## 82 | 83 | const DAY_IN_LEDGERS: u32 = 17280; 84 | pub const MERKLE_CLAIMED_EXTEND_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; 85 | pub const MERKLE_CLAIMED_TTL_THRESHOLD: u32 = MERKLE_CLAIMED_EXTEND_AMOUNT - DAY_IN_LEDGERS; 86 | 87 | // ################## EVENTS ################## 88 | 89 | /// Event emitted when the merkle root is set. 90 | #[contractevent] 91 | #[derive(Clone, Debug, Eq, PartialEq)] 92 | pub struct SetRoot { 93 | pub root: Bytes, 94 | } 95 | 96 | /// Event emitted when an index is claimed. 97 | #[contractevent] 98 | #[derive(Clone, Debug)] 99 | pub struct SetClaimed { 100 | pub index: Val, 101 | } 102 | 103 | /// Emits an event when the merkle root is set. 104 | /// 105 | /// # Arguments 106 | /// 107 | /// * `e` - The Soroban environment. 108 | /// * `root` - The merkle root. 109 | pub fn emit_set_root(e: &Env, root: Bytes) { 110 | SetRoot { root }.publish(e); 111 | } 112 | 113 | /// Emits an event when an index is claimed. 114 | /// 115 | /// # Arguments 116 | /// 117 | /// * `e` - The Soroban environment. 118 | /// * `index` - The index that was claimed. 119 | pub fn emit_set_claimed(e: &Env, index: Val) { 120 | SetClaimed { index }.publish(e); 121 | } 122 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/burnable/test.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use soroban_sdk::{contract, testutils::Address as _, Address, Env}; 4 | use stellar_event_assertion::EventAssertion; 5 | 6 | use crate::fungible::Base; 7 | 8 | #[contract] 9 | struct MockContract; 10 | 11 | #[test] 12 | fn burn_works() { 13 | let e = Env::default(); 14 | e.mock_all_auths(); 15 | let address = e.register(MockContract, ()); 16 | let account = Address::generate(&e); 17 | e.as_contract(&address, || { 18 | Base::mint(&e, &account, 100); 19 | Base::burn(&e, &account, 50); 20 | assert_eq!(Base::balance(&e, &account), 50); 21 | assert_eq!(Base::total_supply(&e), 50); 22 | 23 | let mut event_assert = EventAssertion::new(&e, address.clone()); 24 | event_assert.assert_event_count(2); 25 | event_assert.assert_fungible_mint(&account, 100); 26 | event_assert.assert_fungible_burn(&account, 50); 27 | }); 28 | } 29 | 30 | #[test] 31 | fn burn_with_allowance_works() { 32 | let e = Env::default(); 33 | e.mock_all_auths(); 34 | let address = e.register(MockContract, ()); 35 | let owner = Address::generate(&e); 36 | let spender = Address::generate(&e); 37 | e.as_contract(&address, || { 38 | Base::mint(&e, &owner, 100); 39 | Base::approve(&e, &owner, &spender, 30, 1000); 40 | Base::burn_from(&e, &spender, &owner, 30); 41 | assert_eq!(Base::balance(&e, &owner), 70); 42 | assert_eq!(Base::balance(&e, &spender), 0); 43 | assert_eq!(Base::total_supply(&e), 70); 44 | 45 | let mut event_assert = EventAssertion::new(&e, address.clone()); 46 | event_assert.assert_event_count(3); 47 | event_assert.assert_fungible_mint(&owner, 100); 48 | event_assert.assert_fungible_approve(&owner, &spender, 30, 1000); 49 | event_assert.assert_fungible_burn(&owner, 30); 50 | }); 51 | } 52 | 53 | #[test] 54 | #[should_panic(expected = "Error(Contract, #100)")] 55 | fn burn_with_insufficient_balance_panics() { 56 | let e = Env::default(); 57 | e.mock_all_auths(); 58 | let address = e.register(MockContract, ()); 59 | let account = Address::generate(&e); 60 | e.as_contract(&address, || { 61 | Base::mint(&e, &account, 100); 62 | assert_eq!(Base::balance(&e, &account), 100); 63 | assert_eq!(Base::total_supply(&e), 100); 64 | Base::burn(&e, &account, 101); 65 | }); 66 | } 67 | 68 | #[test] 69 | #[should_panic(expected = "Error(Contract, #101)")] 70 | fn burn_with_no_allowance_panics() { 71 | let e = Env::default(); 72 | e.mock_all_auths(); 73 | let address = e.register(MockContract, ()); 74 | let owner = Address::generate(&e); 75 | let spender = Address::generate(&e); 76 | e.as_contract(&address, || { 77 | Base::mint(&e, &owner, 100); 78 | assert_eq!(Base::balance(&e, &owner), 100); 79 | assert_eq!(Base::total_supply(&e), 100); 80 | Base::burn_from(&e, &spender, &owner, 50); 81 | }); 82 | } 83 | 84 | #[test] 85 | #[should_panic(expected = "Error(Contract, #101)")] 86 | fn burn_with_insufficient_allowance_panics() { 87 | let e = Env::default(); 88 | e.mock_all_auths(); 89 | let address = e.register(MockContract, ()); 90 | let owner = Address::generate(&e); 91 | let spender = Address::generate(&e); 92 | e.as_contract(&address, || { 93 | Base::mint(&e, &owner, 100); 94 | Base::approve(&e, &owner, &spender, 50, 100); 95 | assert_eq!(Base::allowance(&e, &owner, &spender), 50); 96 | assert_eq!(Base::balance(&e, &owner), 100); 97 | assert_eq!(Base::total_supply(&e), 100); 98 | Base::burn_from(&e, &spender, &owner, 60); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /examples/fungible-vault/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Tokenized Vault Example Contract. 2 | 3 | use soroban_sdk::{contract, contractimpl, Address, Env, String}; 4 | use stellar_macros::default_impl; 5 | use stellar_tokens::{ 6 | fungible::{Base, FungibleToken}, 7 | vault::{FungibleVault, Vault}, 8 | }; 9 | 10 | #[contract] 11 | pub struct ExampleContract; 12 | 13 | #[contractimpl] 14 | impl ExampleContract { 15 | pub fn __constructor( 16 | e: &Env, 17 | name: String, 18 | symbol: String, 19 | asset: Address, 20 | decimals_offset: u32, 21 | ) { 22 | // Asset and decimal offset should be configured once during initialization. 23 | Vault::set_asset(e, asset); 24 | Vault::set_decimals_offset(e, decimals_offset); 25 | // Vault overrides the decimals function by default. 26 | // Decimal offset must be set prior to metadata initialization. 27 | Base::set_metadata(e, Self::decimals(e), name, symbol); 28 | } 29 | } 30 | 31 | #[default_impl] 32 | #[contractimpl] 33 | impl FungibleToken for ExampleContract { 34 | type ContractType = Vault; 35 | 36 | // Allows override of decimals and other base functions. 37 | 38 | fn decimals(e: &Env) -> u32 { 39 | Vault::decimals(e) 40 | } 41 | } 42 | 43 | #[contractimpl] 44 | impl FungibleVault for ExampleContract { 45 | // Allows override of public vault functions. 46 | 47 | fn query_asset(e: &Env) -> Address { 48 | Vault::query_asset(e) 49 | } 50 | 51 | fn total_assets(e: &Env) -> i128 { 52 | Vault::total_assets(e) 53 | } 54 | 55 | fn convert_to_shares(e: &Env, assets: i128) -> i128 { 56 | Vault::convert_to_shares(e, assets) 57 | } 58 | 59 | fn convert_to_assets(e: &Env, shares: i128) -> i128 { 60 | Vault::convert_to_assets(e, shares) 61 | } 62 | 63 | fn max_deposit(e: &Env, receiver: Address) -> i128 { 64 | Vault::max_deposit(e, receiver) 65 | } 66 | 67 | fn preview_deposit(e: &Env, assets: i128) -> i128 { 68 | Vault::preview_deposit(e, assets) 69 | } 70 | 71 | fn max_mint(e: &Env, receiver: Address) -> i128 { 72 | Vault::max_mint(e, receiver) 73 | } 74 | 75 | fn preview_mint(e: &Env, shares: i128) -> i128 { 76 | Vault::preview_mint(e, shares) 77 | } 78 | 79 | fn max_withdraw(e: &Env, owner: Address) -> i128 { 80 | Vault::max_withdraw(e, owner) 81 | } 82 | 83 | fn preview_withdraw(e: &Env, assets: i128) -> i128 { 84 | Vault::preview_withdraw(e, assets) 85 | } 86 | 87 | fn max_redeem(e: &Env, owner: Address) -> i128 { 88 | Vault::max_redeem(e, owner) 89 | } 90 | 91 | fn preview_redeem(e: &Env, shares: i128) -> i128 { 92 | Vault::preview_redeem(e, shares) 93 | } 94 | 95 | fn deposit(e: &Env, assets: i128, receiver: Address, from: Address, operator: Address) -> i128 { 96 | operator.require_auth(); 97 | Vault::deposit(e, assets, receiver, from, operator) 98 | } 99 | 100 | fn mint(e: &Env, shares: i128, receiver: Address, from: Address, operator: Address) -> i128 { 101 | operator.require_auth(); 102 | Vault::mint(e, shares, receiver, from, operator) 103 | } 104 | 105 | fn withdraw( 106 | e: &Env, 107 | assets: i128, 108 | receiver: Address, 109 | owner: Address, 110 | operator: Address, 111 | ) -> i128 { 112 | operator.require_auth(); 113 | Vault::withdraw(e, assets, receiver, owner, operator) 114 | } 115 | 116 | fn redeem(e: &Env, shares: i128, receiver: Address, owner: Address, operator: Address) -> i128 { 117 | operator.require_auth(); 118 | Vault::redeem(e, shares, receiver, owner, operator) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/tokens/src/rwa/identity_verifier/mod.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contractclient, Address, Env}; 2 | 3 | #[cfg(test)] 4 | mod test; 5 | 6 | pub mod storage; 7 | 8 | /// # Identity Verifier Module 9 | /// 10 | /// This module provides the `IdentityVerifier` trait for verifying user 11 | /// identities in Real World Asset (RWA) tokens. Identity verification is 12 | /// critical for regulatory compliance, ensuring only verified users can 13 | /// participate in token operations by validating addresses against 14 | /// cryptographic claims from trusted authorities. 15 | /// 16 | /// ## Architecture & Implementation Approaches 17 | /// 18 | /// Identity verification systems can be implemented in various ways depending 19 | /// on regulatory and business requirements: 20 | /// 21 | /// - **Merkle Tree**: Efficient verification using merkle proofs (minimal 22 | /// storage) 23 | /// - **Zero-Knowledge**: Privacy-preserving verification (custom ZK circuits) 24 | /// - **Claim-based**: Cryptographic claims from trusted issuers (our default 25 | /// approach) 26 | /// - and other custom approaches 27 | /// 28 | /// ## Default Implementation 29 | /// 30 | /// Our suggested claim-based implementation uses two external contracts: 31 | /// 1. **Claim Topics and Issuers**: Manages trusted issuers and claim types 32 | /// 2. **Identity Registry Storage**: Maps wallet addresses to onchain 33 | /// identities 34 | /// 35 | /// Since `IdentityRegistryStorage` may not be required for all approaches 36 | /// (e.g., merkle tree or zero-knowledge implementations), it's not part of the 37 | /// trait interface. However, `storage.rs` provides the necessary functions for 38 | /// `IdentityRegistryStorage` integration. Examples are available in the RWA 39 | /// examples folder. 40 | #[contractclient(name = "IdentityVerifierClient")] 41 | pub trait IdentityVerifier { 42 | /// Verifies that the identity of an user address has the required valid 43 | /// claims. 44 | /// 45 | /// # Arguments 46 | /// 47 | /// * `e` - Access to the Soroban environment. 48 | /// * `account` - The account to verify. 49 | /// 50 | /// # Errors 51 | /// 52 | /// * [`crate::rwa::RWAError::IdentityVerificationFailed`] - When the 53 | /// identity of the account cannot be verified. 54 | fn verify_identity(e: &Env, account: &Address); 55 | 56 | /// Returns the target address for the recovery process for the old account. 57 | /// If the old account is not a target of a recovery process, `None` is 58 | /// returned. 59 | /// 60 | /// # Arguments 61 | /// 62 | /// * `e` - Access to the Soroban environment. 63 | /// * `old_account` - The address of the old account. 64 | fn recovery_target(e: &Env, old_account: &Address) -> Option
; 65 | 66 | /// Sets the identity registry contract of the token. 67 | /// This function can only be called by the operator with necessary 68 | /// privileges. RBAC checks are expected to be enforced on the 69 | /// `operator`. 70 | /// 71 | /// # Arguments 72 | /// 73 | /// * `e` - Access to the Soroban environment. 74 | /// * `claim_topics_and_issuers` - The address of the claim topics and 75 | /// issuers contract to set. 76 | /// * `operator` - The address of the operator. 77 | /// 78 | /// # Events 79 | /// 80 | /// * topics - `["claim_topics_issuers_set", claim_topics_and_issuers: 81 | /// Address]` 82 | /// * data - `[]` 83 | fn set_claim_topics_and_issuers(e: &Env, claim_topics_and_issuers: Address, operator: Address); 84 | 85 | /// Returns the Claim Topics and Issuers contract linked to the token. 86 | /// 87 | /// # Errors 88 | /// 89 | /// * [`crate::rwa::RWAError::ClaimTopicsAndIssuersNotSet`] - When the claim 90 | /// topics and issuers contract is not set. 91 | fn claim_topics_and_issuers(e: &Env) -> Address; 92 | } 93 | -------------------------------------------------------------------------------- /packages/contract-utils/src/math/i256_fixed_point.rs: -------------------------------------------------------------------------------- 1 | // Based on the Soroban fixed-point mathematics library 2 | // Original implementation: https://github.com/script3/soroban-fixed-point-math 3 | 4 | use soroban_sdk::{panic_with_error, Env, I256}; 5 | 6 | use crate::math::{SorobanFixedPoint, SorobanFixedPointError}; 7 | 8 | impl SorobanFixedPoint for I256 { 9 | fn fixed_mul_floor(&self, env: &Env, y: &I256, denominator: &I256) -> I256 { 10 | mul_div_floor(env, self, y, denominator) 11 | } 12 | 13 | fn fixed_mul_ceil(&self, env: &Env, y: &I256, denominator: &I256) -> I256 { 14 | mul_div_ceil(env, self, y, denominator) 15 | } 16 | 17 | fn checked_fixed_mul_floor(&self, env: &Env, y: &I256, denominator: &I256) -> Option { 18 | checked_mul_div_floor(env, self, y, denominator) 19 | } 20 | 21 | fn checked_fixed_mul_ceil(&self, env: &Env, y: &I256, denominator: &I256) -> Option { 22 | checked_mul_div_ceil(env, self, y, denominator) 23 | } 24 | } 25 | 26 | /// Performs floor(x * y / z) 27 | pub(crate) fn mul_div_floor(env: &Env, x: &I256, y: &I256, z: &I256) -> I256 { 28 | let zero = I256::from_i32(env, 0); 29 | let r = x.mul(y); 30 | 31 | if *z == zero { 32 | panic_with_error!(env, SorobanFixedPointError::DivisionByZero); 33 | } 34 | 35 | if r < zero || (r > zero && *z < zero) { 36 | // ceil is taken by default for a negative result 37 | let remainder = r.rem_euclid(z); 38 | let one = I256::from_i32(env, 1); 39 | r.div(z).sub(if remainder > zero { &one } else { &zero }) 40 | } else { 41 | // floor is taken by default for a positive or zero result 42 | r.div(z) 43 | } 44 | } 45 | 46 | /// Performs ceil(x * y / z) 47 | pub(crate) fn mul_div_ceil(env: &Env, x: &I256, y: &I256, z: &I256) -> I256 { 48 | let zero = I256::from_i32(env, 0); 49 | let r = x.mul(y); 50 | 51 | if *z == zero { 52 | panic_with_error!(env, SorobanFixedPointError::DivisionByZero); 53 | } 54 | 55 | if *z < zero || r <= zero { 56 | // ceil is taken by default for a negative or zero result 57 | r.div(z) 58 | } else { 59 | // floor is taken by default for a positive result 60 | let remainder = r.rem_euclid(z); 61 | let one = I256::from_i32(env, 1); 62 | r.div(z).add(if remainder > zero { &one } else { &zero }) 63 | } 64 | } 65 | 66 | /// Checked version of floor(x * y / z) 67 | pub(crate) fn checked_mul_div_floor(env: &Env, x: &I256, y: &I256, z: &I256) -> Option { 68 | let zero = I256::from_i32(env, 0); 69 | let r = x.mul(y); 70 | 71 | if *z == zero { 72 | return None; 73 | } 74 | 75 | if r < zero || (r > zero && *z < zero) { 76 | // ceil is taken by default for a negative result 77 | let remainder = r.rem_euclid(z); 78 | let one = I256::from_i32(env, 1); 79 | Some(r.div(z).sub(if remainder > zero { &one } else { &zero })) 80 | } else { 81 | // floor is taken by default for a positive or zero result 82 | Some(r.div(z)) 83 | } 84 | } 85 | 86 | /// Checked version of ceil(x * y / z) 87 | pub(crate) fn checked_mul_div_ceil(env: &Env, x: &I256, y: &I256, z: &I256) -> Option { 88 | let zero = I256::from_i32(env, 0); 89 | let r = x.mul(y); 90 | 91 | if *z == zero { 92 | return None; 93 | } 94 | 95 | if *z < zero || r <= zero { 96 | // ceil is taken by default for a negative or zero result 97 | Some(r.div(z)) 98 | } else { 99 | // floor is taken by default for a positive result 100 | let remainder = r.rem_euclid(z); 101 | let one = I256::from_i32(env, 1); 102 | Some(r.div(z).add(if remainder > zero { &one } else { &zero })) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/contract-utils/src/pausable/storage.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contracttype, panic_with_error, Env}; 2 | 3 | use crate::pausable::{emit_paused, emit_unpaused, PausableError}; 4 | 5 | /// Storage key for the pausable state 6 | #[contracttype] 7 | pub enum PausableStorageKey { 8 | /// Indicates whether the contract is in paused state. 9 | Paused, 10 | } 11 | 12 | /// Returns true if the contract is paused, and false otherwise. 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `e` - Access to Soroban environment. 17 | pub fn paused(e: &Env) -> bool { 18 | // if not paused, consider default false (unpaused) 19 | e.storage().instance().get(&PausableStorageKey::Paused).unwrap_or(false) 20 | 21 | // NOTE: We don't extend the TTL here. We don’t think utilities should 22 | // have any opinion on the TTLs, contracts usually manage TTL's themselves. 23 | // Extending the TTL in the utilities would be redundant in the most cases. 24 | } 25 | 26 | /// Triggers paused state. 27 | /// 28 | /// # Arguments 29 | /// 30 | /// * `e` - Access to Soroban environment. 31 | /// 32 | /// # Errors 33 | /// 34 | /// * refer to [`when_not_paused`] errors. 35 | /// 36 | /// # Events 37 | /// 38 | /// * topics - `["paused"]` 39 | /// * data - `[]` 40 | /// 41 | /// # Security Warning 42 | /// 43 | /// **IMPORTANT**: This function lacks authorization checks and should only 44 | /// be used in admin functions that implement their own authorization logic. 45 | /// 46 | /// Example: 47 | /// 48 | /// ```ignore,rust 49 | /// use stellar_access_control_macros::only_role; 50 | /// 51 | /// #[only_role(operator, "pauser")] // `only_role` handles authorization 52 | /// fn emergency_pause(e: &Env, operator: Address) { 53 | /// pausable::pause(e); 54 | /// } 55 | /// ``` 56 | pub fn pause(e: &Env) { 57 | when_not_paused(e); 58 | e.storage().instance().set(&PausableStorageKey::Paused, &true); 59 | emit_paused(e); 60 | } 61 | 62 | /// Triggers unpaused state. 63 | /// 64 | /// # Arguments 65 | /// 66 | /// * `e` - Access to Soroban environment. 67 | /// 68 | /// # Errors 69 | /// 70 | /// * refer to [`when_paused`] errors. 71 | /// 72 | /// # Events 73 | /// 74 | /// * topics - `["unpaused"]` 75 | /// * data - `[]` 76 | /// 77 | /// # Security Warning 78 | /// 79 | /// **IMPORTANT**: This function lacks authorization checks and should only 80 | /// be used in admin functions that implement their own authorization logic. 81 | /// 82 | /// Example: 83 | /// 84 | /// ```ignore,rust 85 | /// use stellar_access_control_macros::only_role; 86 | /// 87 | /// #[only_role(operator, "unpauser")] // `only_role` handles authorization 88 | /// fn unpause(e: &Env, operator: Address) { 89 | /// pausable::unpause(e); 90 | /// } 91 | /// ``` 92 | pub fn unpause(e: &Env) { 93 | when_paused(e); 94 | e.storage().instance().set(&PausableStorageKey::Paused, &false); 95 | emit_unpaused(e); 96 | } 97 | 98 | /// Helper to make a function callable only when the contract is NOT paused. 99 | /// 100 | /// # Arguments 101 | /// 102 | /// * `e` - Access to Soroban environment. 103 | /// 104 | /// # Errors 105 | /// 106 | /// * [`PausableError::EnforcedPause`] - Occurs when the contract is already in 107 | /// `Paused` state. 108 | pub fn when_not_paused(e: &Env) { 109 | if paused(e) { 110 | panic_with_error!(e, PausableError::EnforcedPause); 111 | } 112 | } 113 | 114 | /// Helper to make a function callable only when the contract is paused. 115 | /// 116 | /// # Arguments 117 | /// 118 | /// * `e` - Access to Soroban environment. 119 | /// 120 | /// # Errors 121 | /// 122 | /// * [`PausableError::ExpectedPause`] - Occurs when the contract is already in 123 | /// `Unpaused` state. 124 | pub fn when_paused(e: &Env) { 125 | if !paused(e) { 126 | panic_with_error!(e, PausableError::ExpectedPause); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/allowlist/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod storage; 2 | 3 | #[cfg(test)] 4 | mod test; 5 | 6 | use soroban_sdk::{contractevent, Address, Env}; 7 | pub use storage::AllowList; 8 | 9 | use crate::fungible::FungibleToken; 10 | 11 | /// AllowList Trait for Fungible Token 12 | /// 13 | /// The `FungibleAllowList` trait extends the `FungibleToken` trait to 14 | /// provide an allowlist mechanism that can be managed by an authorized account. 15 | /// This extension ensures that transfer can only take place if the sender and 16 | /// the receiver are both allowed. Note that, spender does not have to be 17 | /// allowed. 18 | /// 19 | /// This trait is designed to be used in conjunction with the `FungibleToken` 20 | /// trait. 21 | /// 22 | /// **NOTE** 23 | /// 24 | /// All setter functions, exposed in the `FungibleAllowList` trait, include an 25 | /// additional parameter `operator: Address`. This account is the one 26 | /// authorizing the invocation. Having it as a parameter grants the flexibility 27 | /// to introduce simple or complex role-based access controls. 28 | /// 29 | /// However, this parameter is omitted from the module functions, defined in 30 | /// "storage.rs", because the authorizations are to be handled in the access 31 | /// control helpers or directly implemented. 32 | pub trait FungibleAllowList: FungibleToken { 33 | /// Returns the allowed status of an account. 34 | /// 35 | /// # Arguments 36 | /// 37 | /// * `e` - Access to the Soroban environment. 38 | /// * `account` - The address to check the allowed status for. 39 | fn allowed(e: &Env, account: Address) -> bool; 40 | 41 | /// Allows a user to receive and transfer tokens. 42 | /// 43 | /// # Arguments 44 | /// 45 | /// * `e` - Access to the Soroban environment. 46 | /// * `user` - The address to allow. 47 | /// * `operator` - The address authorizing the invocation. 48 | /// 49 | /// # Events 50 | /// 51 | /// * topics - `["allow", user: Address]` 52 | /// * data - `[]` 53 | fn allow_user(e: &Env, user: Address, operator: Address); 54 | 55 | /// Disallows a user from receiving and transferring tokens. 56 | /// 57 | /// # Arguments 58 | /// 59 | /// * `e` - Access to the Soroban environment. 60 | /// * `user` - The address to disallow. 61 | /// * `operator` - The address authorizing the invocation. 62 | /// 63 | /// # Events 64 | /// 65 | /// * topics - `["disallow", user: Address]` 66 | /// * data - `[]` 67 | fn disallow_user(e: &Env, user: Address, operator: Address); 68 | } 69 | 70 | // ################## EVENTS ################## 71 | 72 | /// Event emitted when a user is allowed to transfer tokens. 73 | #[contractevent] 74 | #[derive(Clone, Debug, Eq, PartialEq)] 75 | pub struct UserAllowed { 76 | #[topic] 77 | pub user: Address, 78 | } 79 | 80 | /// Event emitted when a user is disallowed from transferring tokens. 81 | #[contractevent] 82 | #[derive(Clone, Debug, Eq, PartialEq)] 83 | pub struct UserDisallowed { 84 | #[topic] 85 | pub user: Address, 86 | } 87 | 88 | /// Emits an event when a user is allowed to transfer tokens. 89 | /// 90 | /// # Arguments 91 | /// 92 | /// * `e` - Access to Soroban environment. 93 | /// * `user` - The address that is allowed to transfer tokens. 94 | pub fn emit_user_allowed(e: &Env, user: &Address) { 95 | UserAllowed { user: user.clone() }.publish(e); 96 | } 97 | 98 | /// Emits an event when a user is disallowed from transferring tokens. 99 | /// 100 | /// # Arguments 101 | /// 102 | /// * `e` - Access to Soroban environment. 103 | /// * `user` - The address that is disallowed from transferring tokens. 104 | pub fn emit_user_disallowed(e: &Env, user: &Address) { 105 | UserDisallowed { user: user.clone() }.publish(e); 106 | } 107 | -------------------------------------------------------------------------------- /packages/access/src/role_transfer/storage.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{panic_with_error, Address, Env, IntoVal, Val}; 2 | 3 | use crate::role_transfer::RoleTransferError; 4 | 5 | /// Initiates the role transfer. If `live_until_ledger == 0`, cancels the 6 | /// pending transfer. 7 | /// 8 | /// Does not emit any events. 9 | /// 10 | /// # Arguments 11 | /// 12 | /// * `e` - Access to the Soroban environment. 13 | /// * `new` - The proposed new role holder. 14 | /// * `pending_key` - Storage key for the pending role holder. 15 | /// * `live_until_ledger` - Ledger number until which the new role holder can 16 | /// accept. A value of `0` cancels the pending transfer. If the specified 17 | /// ledger is in the past or exceeds the maximum allowed TTL extension for a 18 | /// temporary storage entry, the function will panic. 19 | /// 20 | /// # Errors 21 | /// 22 | /// * [`RoleTransferError::NoPendingTransfer`] - If trying to cancel a transfer 23 | /// that doesn't exist. 24 | /// * [`RoleTransferError::InvalidLiveUntilLedger`] - If the specified ledger is 25 | /// in the past, or exceeds the maximum allowed TTL extension for a temporary 26 | /// storage entry. 27 | /// * [`RoleTransferError::InvalidPendingAccount`] - If the specified pending 28 | /// account is not the same as the provided `new` address. 29 | /// 30 | /// # Notes 31 | /// 32 | /// * This function does not enforce authorization. Ensure that authorization is 33 | /// handled at a higher level. 34 | /// * The period during which the transfer can be accepted is implicitly 35 | /// timebound by the maximum allowed storage TTL value which is a network 36 | /// parameter, i.e. one cannot set `live_until_ledger` for a longer period. 37 | /// * There is also a default minimum TTL and if the computed period is shorter 38 | /// than it, the entry will outlive `live_until_ledger`. 39 | pub fn transfer_role(e: &Env, new: &Address, pending_key: &T, live_until_ledger: u32) 40 | where 41 | T: IntoVal, 42 | { 43 | if live_until_ledger == 0 { 44 | let Some(pending) = e.storage().temporary().get::(pending_key) else { 45 | panic_with_error!(e, RoleTransferError::NoPendingTransfer); 46 | }; 47 | if pending != *new { 48 | panic_with_error!(e, RoleTransferError::InvalidPendingAccount); 49 | } 50 | e.storage().temporary().remove(pending_key); 51 | 52 | return; 53 | } 54 | 55 | let current_ledger = e.ledger().sequence(); 56 | if live_until_ledger > e.ledger().max_live_until_ledger() || live_until_ledger < current_ledger 57 | { 58 | panic_with_error!(e, RoleTransferError::InvalidLiveUntilLedger); 59 | } 60 | 61 | let live_for = live_until_ledger - current_ledger; 62 | e.storage().temporary().set(pending_key, new); 63 | e.storage().temporary().extend_ttl(pending_key, live_for, live_for); 64 | } 65 | 66 | /// Completes the role transfer if authorization is provided by the pending role 67 | /// holder. Pending role holder is retrieved from the storage. 68 | /// 69 | /// # Arguments 70 | /// 71 | /// * `e` - Access to the Soroban environment. 72 | /// * `active_key` - Storage key for the current role holder. 73 | /// * `pending_key` - Storage key for the pending role holder. 74 | /// 75 | /// # Errors 76 | /// 77 | /// * [`RoleTransferError::NoPendingTransfer`] - If there is no pending transfer 78 | /// to accept. 79 | pub fn accept_transfer(e: &Env, active_key: &T, pending_key: &U) -> Address 80 | where 81 | T: IntoVal, 82 | U: IntoVal, 83 | { 84 | let pending = e 85 | .storage() 86 | .temporary() 87 | .get::(pending_key) 88 | .unwrap_or_else(|| panic_with_error!(e, RoleTransferError::NoPendingTransfer)); 89 | 90 | pending.require_auth(); 91 | 92 | e.storage().temporary().remove(pending_key); 93 | e.storage().instance().set(active_key, &pending); 94 | 95 | pending 96 | } 97 | -------------------------------------------------------------------------------- /packages/tokens/src/fungible/extensions/blocklist/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod storage; 2 | 3 | #[cfg(test)] 4 | mod test; 5 | 6 | use soroban_sdk::{contractevent, Address, Env}; 7 | pub use storage::BlockList; 8 | 9 | use crate::fungible::FungibleToken; 10 | 11 | /// BlockList Trait for Fungible Token 12 | /// 13 | /// The `FungibleBlockList` trait extends the `FungibleToken` trait to 14 | /// provide a blocklist mechanism that can be managed by an authorized account. 15 | /// This extension ensures that transfer can only take place if both the sender 16 | /// and the receiver are not blocked. Note that, this restriction does not 17 | /// apply to the spender. 18 | /// 19 | /// This trait is designed to be used in conjunction with the `FungibleToken` 20 | /// trait. 21 | /// 22 | /// **NOTE** 23 | /// 24 | /// All setter functions, exposed in the `FungibleBlockList` trait, include an 25 | /// additional parameter `operator: Address`. This account is the one 26 | /// authorizing the invocation. Having it as a parameter grants the flexibility 27 | /// to introduce simple or complex role-based access controls. 28 | /// 29 | /// However, this parameter is omitted from the module functions, defined in 30 | /// "storage.rs", because the authorizations are to be handled in the access 31 | /// control helpers or directly implemented. 32 | pub trait FungibleBlockList: FungibleToken { 33 | /// Returns the blocked status of an account. 34 | /// 35 | /// # Arguments 36 | /// 37 | /// * `e` - Access to the Soroban environment. 38 | /// * `account` - The address to check the blocked status for. 39 | fn blocked(e: &Env, account: Address) -> bool; 40 | 41 | /// Blocks a user from receiving and transferring tokens. 42 | /// 43 | /// # Arguments 44 | /// 45 | /// * `e` - Access to the Soroban environment. 46 | /// * `user` - The address to block. 47 | /// * `operator` - The address authorizing the invocation. 48 | /// 49 | /// # Events 50 | /// 51 | /// * topics - `["block", user: Address]` 52 | /// * data - `[]` 53 | fn block_user(e: &Env, user: Address, operator: Address); 54 | 55 | /// Unblocks a user, allowing them to receive and transfer tokens. 56 | /// 57 | /// # Arguments 58 | /// 59 | /// * `e` - Access to the Soroban environment. 60 | /// * `user` - The address to unblock. 61 | /// * `operator` - The address authorizing the invocation. 62 | /// 63 | /// # Events 64 | /// 65 | /// * topics - `["unblock", user: Address]` 66 | /// * data - `[]` 67 | fn unblock_user(e: &Env, user: Address, operator: Address); 68 | } 69 | 70 | // ################## EVENTS ################## 71 | 72 | /// Event emitted when a user is blocked from transferring tokens. 73 | #[contractevent] 74 | #[derive(Clone, Debug, Eq, PartialEq)] 75 | pub struct UserBlocked { 76 | #[topic] 77 | pub user: Address, 78 | } 79 | 80 | /// Event emitted when a user is unblocked and allowed to transfer tokens. 81 | #[contractevent] 82 | #[derive(Clone, Debug, Eq, PartialEq)] 83 | pub struct UserUnblocked { 84 | #[topic] 85 | pub user: Address, 86 | } 87 | 88 | /// Emits an event when a user is blocked from transferring tokens. 89 | /// 90 | /// # Arguments 91 | /// 92 | /// * `e` - Access to Soroban environment. 93 | /// * `user` - The address that is blocked from transferring tokens. 94 | pub fn emit_user_blocked(e: &Env, user: &Address) { 95 | UserBlocked { user: user.clone() }.publish(e); 96 | } 97 | 98 | /// Emits an event when a user is unblocked and allowed to transfer tokens 99 | /// again. 100 | /// 101 | /// # Arguments 102 | /// 103 | /// * `e` - Access to Soroban environment. 104 | /// * `user` - The address that is unblocked. 105 | pub fn emit_user_unblocked(e: &Env, user: &Address) { 106 | UserUnblocked { user: user.clone() }.publish(e); 107 | } 108 | --------------------------------------------------------------------------------