Skip to content

Commit fba0f1f

Browse files
Josue19-08claude
andcommitted
feat(contract): complete detokenization flow with token elimination
Enhance execute_detokenization() to fully eliminate tokens from circulation and clear all tokenization records: - Remove all token holder balances - Clear token locks for all holders - Remove unclaimed dividends - Delete token holders list - Remove transfer restrictions and whitelist - Clear token metadata - Remove TokenizedAsset record (eliminates all tokens) - Clear voting records before removal Add comprehensive tests: - test_token_elimination_on_execution: Verifies complete token removal - test_cannot_propose_after_execution: Ensures no re-proposals after execution Fixes #473 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent ee223c4 commit fba0f1f

2 files changed

Lines changed: 143 additions & 6 deletions

File tree

contracts/assetsup/src/detokenization.rs

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ pub fn propose_detokenization(env: &Env, asset_id: u64, proposer: Address) -> Re
4040
}
4141

4242
/// Execute detokenization if vote passed
43+
/// This will remove all tokens from circulation and clear tokenization records
4344
pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Result<(), Error> {
4445
let store = env.storage().persistent();
4546

4647
// Verify asset is tokenized
4748
let key = TokenDataKey::TokenizedAsset(asset_id);
48-
let _: TokenizedAsset = store.get(&key).ok_or(Error::AssetNotTokenized)?;
49+
let tokenized_asset: TokenizedAsset = store.get(&key).ok_or(Error::AssetNotTokenized)?;
4950

5051
// Check if proposal is active
5152
let proposal_key = TokenDataKey::DetokenizationProposal(asset_id);
@@ -64,6 +65,65 @@ pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Res
6465
return Err(Error::DetokenizationNotApproved);
6566
}
6667

68+
// Save total supply for event before clearing
69+
let total_supply = tokenized_asset.total_supply;
70+
71+
// Clear all votes BEFORE removing TokenizedAsset (voting module needs it)
72+
voting::clear_proposal_votes(env, asset_id, proposal_id)?;
73+
74+
// Get list of all token holders before clearing
75+
let holders_list_key = TokenDataKey::TokenHoldersList(asset_id);
76+
let holders = store.get::<_, soroban_sdk::Vec<Address>>(&holders_list_key)
77+
.ok_or(Error::AssetNotTokenized)?;
78+
79+
// Remove all token holder records
80+
for holder in holders.iter() {
81+
let holder_key = TokenDataKey::TokenHolder(asset_id, holder.clone());
82+
if store.has(&holder_key) {
83+
store.remove(&holder_key);
84+
}
85+
86+
// Remove any token locks
87+
let lock_key = TokenDataKey::TokenLockedUntil(asset_id, holder.clone());
88+
if store.has(&lock_key) {
89+
store.remove(&lock_key);
90+
}
91+
92+
// Remove unclaimed dividends
93+
let dividend_key = TokenDataKey::UnclaimedDividend(asset_id, holder);
94+
if store.has(&dividend_key) {
95+
store.remove(&dividend_key);
96+
}
97+
}
98+
99+
// Remove token holders list
100+
if store.has(&holders_list_key) {
101+
store.remove(&holders_list_key);
102+
}
103+
104+
// Remove transfer restrictions
105+
let restriction_key = TokenDataKey::TransferRestriction(asset_id);
106+
if store.has(&restriction_key) {
107+
store.remove(&restriction_key);
108+
}
109+
110+
// Remove whitelist
111+
let whitelist_key = TokenDataKey::Whitelist(asset_id);
112+
if store.has(&whitelist_key) {
113+
store.remove(&whitelist_key);
114+
}
115+
116+
// Remove token metadata
117+
let metadata_key = TokenDataKey::TokenMetadata(asset_id);
118+
if store.has(&metadata_key) {
119+
store.remove(&metadata_key);
120+
}
121+
122+
// Remove the tokenized asset record (this eliminates all tokens from circulation)
123+
if store.has(&key) {
124+
store.remove(&key);
125+
}
126+
67127
// Update proposal to executed
68128
let timestamp = env.ledger().timestamp();
69129
let executed_proposal = DetokenizationProposal::Executed(ExecutedProposal {
@@ -72,13 +132,10 @@ pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Res
72132
});
73133
store.set(&proposal_key, &executed_proposal);
74134

75-
// Clear all votes
76-
voting::clear_proposal_votes(env, asset_id, proposal_id)?;
77-
78-
// Emit event: (asset_id, proposal_id)
135+
// Emit event: (asset_id, proposal_id, total_supply_removed)
79136
env.events().publish(
80137
("detokenization", "asset_detokenized"),
81-
(asset_id, proposal_id),
138+
(asset_id, proposal_id, total_supply),
82139
);
83140

84141
Ok(())

contracts/assetsup/src/tests/detokenization_new.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,83 @@ fn test_detokenization_majority_threshold() {
188188
assert!(first_execute_err);
189189
assert!(second_execute_ok);
190190
}
191+
192+
#[test]
193+
fn test_token_elimination_on_execution() {
194+
let env = Env::default();
195+
let contract_id = env.register(AssetUpContract, ());
196+
let tokenizer = Address::generate(&env);
197+
let holder2 = Address::generate(&env);
198+
let proposer = Address::generate(&env);
199+
let asset_id = 1000u64;
200+
201+
let (before_exists, after_exists, balance_cleared, holders_cleared) =
202+
env.as_contract(&contract_id, || {
203+
setup_tokenized_asset(&env, asset_id, &tokenizer);
204+
205+
// Transfer some tokens to create multiple holders
206+
tokenization::transfer_tokens(&env, asset_id, tokenizer.clone(), holder2.clone(), 300)
207+
.unwrap();
208+
209+
// Verify asset exists before detokenization
210+
let before_exists = tokenization::get_tokenized_asset(&env, asset_id).is_ok();
211+
212+
// Propose detokenization
213+
let proposal_id =
214+
detokenization::propose_detokenization(&env, asset_id, proposer.clone()).unwrap();
215+
216+
// Both holders vote (100%)
217+
voting::cast_vote(&env, asset_id, proposal_id, tokenizer.clone()).unwrap();
218+
voting::cast_vote(&env, asset_id, proposal_id, holder2.clone()).unwrap();
219+
220+
// Execute detokenization
221+
detokenization::execute_detokenization(&env, asset_id, proposal_id).unwrap();
222+
223+
// Verify tokens are removed from circulation
224+
let after_exists = tokenization::get_tokenized_asset(&env, asset_id).is_ok();
225+
226+
// Verify balances are cleared
227+
let balance1 = tokenization::get_token_balance(&env, asset_id, tokenizer.clone());
228+
let balance2 = tokenization::get_token_balance(&env, asset_id, holder2.clone());
229+
let balance_cleared = balance1.unwrap_or(0) == 0 && balance2.unwrap_or(0) == 0;
230+
231+
// Verify holders list is cleared
232+
let holders_result = tokenization::get_token_holders(&env, asset_id);
233+
let holders_cleared = holders_result.is_err();
234+
235+
(before_exists, after_exists, balance_cleared, holders_cleared)
236+
});
237+
238+
// Assert asset existed before
239+
assert!(before_exists);
240+
// Assert asset no longer exists after detokenization
241+
assert!(!after_exists);
242+
// Assert balances are cleared
243+
assert!(balance_cleared);
244+
// Assert holders list is cleared
245+
assert!(holders_cleared);
246+
}
247+
248+
#[test]
249+
fn test_cannot_propose_after_execution() {
250+
let env = Env::default();
251+
let contract_id = env.register(AssetUpContract, ());
252+
let tokenizer = Address::generate(&env);
253+
let proposer = Address::generate(&env);
254+
let asset_id = 1000u64;
255+
256+
let second_proposal_err = env.as_contract(&contract_id, || {
257+
setup_tokenized_asset(&env, asset_id, &tokenizer);
258+
259+
// Propose and execute detokenization
260+
let proposal_id =
261+
detokenization::propose_detokenization(&env, asset_id, proposer.clone()).unwrap();
262+
voting::cast_vote(&env, asset_id, proposal_id, tokenizer.clone()).unwrap();
263+
detokenization::execute_detokenization(&env, asset_id, proposal_id).unwrap();
264+
265+
// Try to propose again after execution - should fail because asset is not tokenized
266+
detokenization::propose_detokenization(&env, asset_id, proposer.clone()).is_err()
267+
});
268+
269+
assert!(second_proposal_err);
270+
}

0 commit comments

Comments
 (0)