diff --git a/contracts/assetsup/src/detokenization.rs b/contracts/assetsup/src/detokenization.rs index dc16c43..c6f2078 100644 --- a/contracts/assetsup/src/detokenization.rs +++ b/contracts/assetsup/src/detokenization.rs @@ -40,12 +40,13 @@ pub fn propose_detokenization(env: &Env, asset_id: u64, proposer: Address) -> Re } /// Execute detokenization if vote passed +/// This will remove all tokens from circulation and clear tokenization records pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Result<(), Error> { let store = env.storage().persistent(); // Verify asset is tokenized let key = TokenDataKey::TokenizedAsset(asset_id); - let _: TokenizedAsset = store.get(&key).ok_or(Error::AssetNotTokenized)?; + let tokenized_asset: TokenizedAsset = store.get(&key).ok_or(Error::AssetNotTokenized)?; // Check if proposal is active let proposal_key = TokenDataKey::DetokenizationProposal(asset_id); @@ -64,6 +65,65 @@ pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Res return Err(Error::DetokenizationNotApproved); } + // Save total supply for event before clearing + let total_supply = tokenized_asset.total_supply; + + // Clear all votes BEFORE removing TokenizedAsset (voting module needs it) + voting::clear_proposal_votes(env, asset_id, proposal_id)?; + + // Get list of all token holders before clearing + let holders_list_key = TokenDataKey::TokenHoldersList(asset_id); + let holders = store.get::<_, soroban_sdk::Vec
>(&holders_list_key) + .ok_or(Error::AssetNotTokenized)?; + + // Remove all token holder records + for holder in holders.iter() { + let holder_key = TokenDataKey::TokenHolder(asset_id, holder.clone()); + if store.has(&holder_key) { + store.remove(&holder_key); + } + + // Remove any token locks + let lock_key = TokenDataKey::TokenLockedUntil(asset_id, holder.clone()); + if store.has(&lock_key) { + store.remove(&lock_key); + } + + // Remove unclaimed dividends + let dividend_key = TokenDataKey::UnclaimedDividend(asset_id, holder); + if store.has(÷nd_key) { + store.remove(÷nd_key); + } + } + + // Remove token holders list + if store.has(&holders_list_key) { + store.remove(&holders_list_key); + } + + // Remove transfer restrictions + let restriction_key = TokenDataKey::TransferRestriction(asset_id); + if store.has(&restriction_key) { + store.remove(&restriction_key); + } + + // Remove whitelist + let whitelist_key = TokenDataKey::Whitelist(asset_id); + if store.has(&whitelist_key) { + store.remove(&whitelist_key); + } + + // Remove token metadata + let metadata_key = TokenDataKey::TokenMetadata(asset_id); + if store.has(&metadata_key) { + store.remove(&metadata_key); + } + + // Remove the tokenized asset record (this eliminates all tokens from circulation) + if store.has(&key) { + store.remove(&key); + } + // Update proposal to executed let timestamp = env.ledger().timestamp(); let executed_proposal = DetokenizationProposal::Executed(ExecutedProposal { @@ -72,13 +132,10 @@ pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Res }); store.set(&proposal_key, &executed_proposal); - // Clear all votes - voting::clear_proposal_votes(env, asset_id, proposal_id)?; - - // Emit event: (asset_id, proposal_id) + // Emit event: (asset_id, proposal_id, total_supply_removed) env.events().publish( ("detokenization", "asset_detokenized"), - (asset_id, proposal_id), + (asset_id, proposal_id, total_supply), ); Ok(()) diff --git a/contracts/assetsup/src/tests/detokenization_new.rs b/contracts/assetsup/src/tests/detokenization_new.rs index ac98fa1..ba7a0b9 100644 --- a/contracts/assetsup/src/tests/detokenization_new.rs +++ b/contracts/assetsup/src/tests/detokenization_new.rs @@ -188,3 +188,83 @@ fn test_detokenization_majority_threshold() { assert!(first_execute_err); assert!(second_execute_ok); } + +#[test] +fn test_token_elimination_on_execution() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let tokenizer = Address::generate(&env); + let holder2 = Address::generate(&env); + let proposer = Address::generate(&env); + let asset_id = 1000u64; + + let (before_exists, after_exists, balance_cleared, holders_cleared) = + env.as_contract(&contract_id, || { + setup_tokenized_asset(&env, asset_id, &tokenizer); + + // Transfer some tokens to create multiple holders + tokenization::transfer_tokens(&env, asset_id, tokenizer.clone(), holder2.clone(), 300) + .unwrap(); + + // Verify asset exists before detokenization + let before_exists = tokenization::get_tokenized_asset(&env, asset_id).is_ok(); + + // Propose detokenization + let proposal_id = + detokenization::propose_detokenization(&env, asset_id, proposer.clone()).unwrap(); + + // Both holders vote (100%) + voting::cast_vote(&env, asset_id, proposal_id, tokenizer.clone()).unwrap(); + voting::cast_vote(&env, asset_id, proposal_id, holder2.clone()).unwrap(); + + // Execute detokenization + detokenization::execute_detokenization(&env, asset_id, proposal_id).unwrap(); + + // Verify tokens are removed from circulation + let after_exists = tokenization::get_tokenized_asset(&env, asset_id).is_ok(); + + // Verify balances are cleared + let balance1 = tokenization::get_token_balance(&env, asset_id, tokenizer.clone()); + let balance2 = tokenization::get_token_balance(&env, asset_id, holder2.clone()); + let balance_cleared = balance1.unwrap_or(0) == 0 && balance2.unwrap_or(0) == 0; + + // Verify holders list is cleared + let holders_result = tokenization::get_token_holders(&env, asset_id); + let holders_cleared = holders_result.is_err(); + + (before_exists, after_exists, balance_cleared, holders_cleared) + }); + + // Assert asset existed before + assert!(before_exists); + // Assert asset no longer exists after detokenization + assert!(!after_exists); + // Assert balances are cleared + assert!(balance_cleared); + // Assert holders list is cleared + assert!(holders_cleared); +} + +#[test] +fn test_cannot_propose_after_execution() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let tokenizer = Address::generate(&env); + let proposer = Address::generate(&env); + let asset_id = 1000u64; + + let second_proposal_err = env.as_contract(&contract_id, || { + setup_tokenized_asset(&env, asset_id, &tokenizer); + + // Propose and execute detokenization + let proposal_id = + detokenization::propose_detokenization(&env, asset_id, proposer.clone()).unwrap(); + voting::cast_vote(&env, asset_id, proposal_id, tokenizer.clone()).unwrap(); + detokenization::execute_detokenization(&env, asset_id, proposal_id).unwrap(); + + // Try to propose again after execution - should fail because asset is not tokenized + detokenization::propose_detokenization(&env, asset_id, proposer.clone()).is_err() + }); + + assert!(second_proposal_err); +}