Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 63 additions & 6 deletions contracts/assetsup/src/detokenization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<Address>>(&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(&dividend_key) {
store.remove(&dividend_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 {
Expand All @@ -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(())
Expand Down
80 changes: 80 additions & 0 deletions contracts/assetsup/src/tests/detokenization_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading