diff --git a/soroban/example-compliant-token/src/lib.rs b/soroban/example-compliant-token/src/lib.rs index c052532c..8308800f 100644 --- a/soroban/example-compliant-token/src/lib.rs +++ b/soroban/example-compliant-token/src/lib.rs @@ -20,6 +20,7 @@ const BALANCE: Symbol = symbol_short!("balance"); pub enum TokenError { InsufficientBalance = 1, InvalidAmount = 2, + RegistryNotConfigured = 3, } /// A minimal token contract that requires Predicate attestation for transfers. @@ -36,29 +37,47 @@ impl CompliantTokenContract { /// /// # Arguments /// * `admin` - Token admin who can mint - /// * `registry` - Address of the deployed PredicateRegistry contract + /// * `registry` - Address of the deployed PredicateRegistry contract, or `None` to + /// deploy without one (e.g. on a network with no registry yet). Attach later with + /// [`set_registry`]. Until a registry is set, `register_policy` and `transfer` fail. /// * `policy_id` - Policy identifier (e.g. "x-a1b2c3d4e5f6g7h8") /// * `network` - Stellar network passphrase (e.g. "Test SDF Network ; September 2015") pub fn __constructor( e: &Env, admin: Address, - registry: Address, + registry: Option
, policy_id: String, network: String, ) { e.storage().instance().set(&ADMIN, &admin); - e.storage().instance().set(®ISTRY, ®istry); + if let Some(registry) = registry { + e.storage().instance().set(®ISTRY, ®istry); + } e.storage().instance().set(&POLICY, &policy_id); e.storage().instance().set(&NETWORK, &network); } + /// Attach (or replace) the Predicate Registry this token uses. Admin only. + /// Enables `register_policy` and compliant `transfer`s on a token that was + /// deployed without a registry. + pub fn set_registry(e: &Env, registry: Address) { + let admin: Address = e.storage().instance().get(&ADMIN).unwrap(); + admin.require_auth(); + e.storage().instance().set(®ISTRY, ®istry); + } + /// Register this contract's policy with the Predicate Registry. /// Call this once after deployment. The admin must authorize. - pub fn register_policy(e: &Env) { + /// Fails with `RegistryNotConfigured` if no registry has been set. + pub fn register_policy(e: &Env) -> Result<(), TokenError> { let admin: Address = e.storage().instance().get(&ADMIN).unwrap(); admin.require_auth(); - let registry: Address = e.storage().instance().get(®ISTRY).unwrap(); + let registry: Address = e + .storage() + .instance() + .get(®ISTRY) + .ok_or(TokenError::RegistryNotConfigured)?; let policy_id: String = e.storage().instance().get(&POLICY).unwrap(); let args: soroban_sdk::Vec = soroban_sdk::vec![ @@ -67,6 +86,7 @@ impl CompliantTokenContract { soroban_sdk::IntoVal::into_val(&policy_id, e), ]; e.invoke_contract::<()>(®istry, &Symbol::new(e, "set_policy_id"), args); + Ok(()) } /// Mint tokens to an address. Admin only, no attestation required. @@ -97,6 +117,13 @@ impl CompliantTokenContract { return Err(TokenError::InvalidAmount); } + // Fail safe before moving any tokens: a compliant transfer requires a registry. + let registry: Address = e + .storage() + .instance() + .get(®ISTRY) + .ok_or(TokenError::RegistryNotConfigured)?; + // Check balance let from_key = (BALANCE, from.clone()); let balance: i128 = e.storage().persistent().get(&from_key).unwrap_or(0); @@ -105,7 +132,6 @@ impl CompliantTokenContract { } // --- Predicate compliance check --- - let registry: Address = e.storage().instance().get(®ISTRY).unwrap(); let policy: String = e.storage().instance().get(&POLICY).unwrap(); let network: String = e.storage().instance().get(&NETWORK).unwrap(); @@ -150,9 +176,9 @@ impl CompliantTokenContract { e.storage().persistent().get(&key).unwrap_or(0) } - /// Query the registry address. - pub fn registry(e: &Env) -> Address { - e.storage().instance().get(®ISTRY).unwrap() + /// Query the registry address, or `None` if no registry has been set yet. + pub fn registry(e: &Env) -> Option
{ + e.storage().instance().get(®ISTRY) } /// Query the policy ID. @@ -208,7 +234,7 @@ mod test { CompliantTokenContract, ( admin.clone(), - registry_addr.clone(), + Some(registry_addr.clone()), policy_id.clone(), network.clone(), ), @@ -284,7 +310,7 @@ mod test { CompliantTokenContract, ( admin.clone(), - registry_addr.clone(), + Some(registry_addr.clone()), policy_id.clone(), network.clone(), ), @@ -308,4 +334,66 @@ mod test { token.transfer(&alice, &bob, &100, &attestation); } + + /// Deploy with no registry: token carries the policy id, mint/balance work, + /// but register_policy and transfer fail safe until a registry is attached. + #[test] + fn test_no_registry_placeholder() { + let e = Env::default(); + e.mock_all_auths(); + + let network = String::from_str(&e, "Test SDF Network ; September 2015"); + let policy_id = String::from_str(&e, "x-example-policy"); + + let admin = Address::generate(&e); + + // Deploy with NO registry + let token_addr = e.register( + CompliantTokenContract, + ( + admin.clone(), + None::
, + policy_id.clone(), + network.clone(), + ), + ); + let token = CompliantTokenContractClient::new(&e, &token_addr); + + // registry() is None, but the policy id is still stored + assert_eq!(token.registry(), None); + assert_eq!(token.policy_id(), policy_id); + + // mint + balance work without a registry + let alice = Address::generate(&e); + let bob = Address::generate(&e); + token.mint(&alice, &1000); + assert_eq!(token.balance(&alice), 1000); + + // register_policy fails safe: no registry to call + assert_eq!( + token.try_register_policy(), + Err(Ok(TokenError::RegistryNotConfigured)) + ); + + // transfer fails safe BEFORE moving any tokens + let attestation = Attestation { + uuid: String::from_str(&e, "placeholder"), + expiration: e.ledger().timestamp() + 600, + attester: BytesN::from_array(&e, &[0u8; 32]), + signature: BytesN::from_array(&e, &[0u8; 64]), + }; + assert_eq!( + token.try_transfer(&alice, &bob, &100, &attestation), + Err(Ok(TokenError::RegistryNotConfigured)) + ); + assert_eq!(token.balance(&alice), 1000); + assert_eq!(token.balance(&bob), 0); + + // Attach a real registry later, then register_policy succeeds + let registry_owner = Address::generate(&e); + let registry_addr = e.register(PredicateRegistryContract, (registry_owner.clone(),)); + token.set_registry(®istry_addr); + assert_eq!(token.registry(), Some(registry_addr.clone())); + token.register_policy(); + } }