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
86 changes: 86 additions & 0 deletions contracts/assetsup/src/dividends.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use soroban_sdk::{contractimpl, Address, BigInt, Env};

use crate::types::{OwnershipRecord, TokenizedAsset};

pub struct DividendContract;

#[contractimpl]
impl DividendContract {
/// Distribute dividends proportionally to all holders
/// amount: total dividend to distribute
pub fn distribute_dividend(env: Env, asset_id: u64, amount: BigInt) {
// Get tokenized asset
let tokenized_asset: TokenizedAsset = env
.storage()
.get((b"asset", asset_id))
.expect("Asset not found")
.unwrap();

// Iterate all ownership records (minimal V1: assume keys stored separately)
// This assumes we have a helper to enumerate owners; otherwise we can store owners list
let owners: Vec<Address> = env
.storage()
.get((b"owners_list", asset_id))
.unwrap_or(Some(Vec::new()))
.unwrap();

for owner in owners.iter() {
let mut ownership: OwnershipRecord = env
.storage()
.get((b"ownership", asset_id, owner))
.unwrap()
.unwrap();

// proportion = owner balance / total supply
let proportion = &ownership.balance * &amount / &tokenized_asset.total_supply;

// Update unclaimed dividend
ownership.unclaimed_dividends =
&ownership.unclaimed_dividends + &proportion;

env.storage()
.set((b"ownership", asset_id, owner), &ownership);
}
}

/// Cast vote on an asset proposal
/// proposal_id is a u64 identifier for the proposal
pub fn cast_vote(env: Env, asset_id: u64, proposal_id: u64, voter: Address) {
// Get ownership
let ownership: OwnershipRecord = env
.storage()
.get((b"ownership", asset_id, &voter))
.unwrap()
.unwrap();

// Minimal threshold: voter must have >0 tokens
if ownership.balance <= BigInt::from_i128(&env, 0) {
panic!("Not enough tokens to vote");
}

// Check if voter already voted
let mut votes: Vec<Address> = env
.storage()
.get((b"votes", asset_id, proposal_id))
.unwrap_or(Some(Vec::new()))
.unwrap();

if votes.contains(&voter) {
panic!("Voter already voted");
}

votes.push(voter.clone());
env.storage().set((b"votes", asset_id, proposal_id), &votes);

// Store voting power weighted by token balance
let mut tally: BigInt = env
.storage()
.get((b"vote_tally", asset_id, proposal_id))
.unwrap_or(Some(BigInt::from_i128(&env, 0)))
.unwrap();

tally = tally + &ownership.balance;
env.storage()
.set((b"vote_tally", asset_id, proposal_id), &tally);
}
}
210 changes: 123 additions & 87 deletions contracts/assetsup/src/tests/tokenize.rs
Original file line number Diff line number Diff line change
@@ -1,95 +1,131 @@
#![cfg(test)]

extern crate std;

use soroban_sdk::{Address, BytesN, Env, String, testutils::Address as _};

use crate::{
asset::Asset,
types::{AssetStatus, AssetType},
};

use super::initialize::setup_test_environment;
#[test]
fn test_mint_tokens_v1() {
let env = Env::default();
let tokenizer = Address::random(&env);

let asset_id = 200u64;
let symbol = "AST200".to_string();
let total_supply = BigInt::from_i128(&env, 500);
let decimals = 2u32;
let name = "Mint Test Asset".to_string();
let description = "Testing minting".to_string();
let asset_type = AssetType::Digital;

// Tokenize asset first
let tokenized_asset = TokenizeContract::tokenize(
env.clone(),
asset_id,
symbol,
total_supply.clone(),
decimals,
name,
description,
asset_type,
tokenizer.clone(),
);

// Mint additional tokens
let mint_amount = BigInt::from_i128(&env, 200);
let updated_asset = TokenizeContract::mint_tokens(env.clone(), asset_id, mint_amount.clone(), tokenizer.clone());

// Verify total supply increased
assert_eq!(updated_asset.total_supply, &total_supply + &mint_amount);

// Verify tokenizer's ownership updated
let ownership: OwnershipRecord = env.storage().get((b"ownership", asset_id, &tokenizer)).unwrap().unwrap();
assert_eq!(ownership.balance, &total_supply + &mint_amount);
}

fn make_bytes32(env: &Env, seed: u32) -> BytesN<32> {
let mut arr = [0u8; 32];
for (i, item) in arr.iter_mut().enumerate() {
*item = ((seed as usize + i) % 256) as u8;
}
BytesN::from_array(env, &arr)
#[test]
fn test_burn_tokens_v1() {
let env = Env::default();
let tokenizer = Address::random(&env);

let asset_id = 300u64;
let symbol = "AST300".to_string();
let total_supply = BigInt::from_i128(&env, 1000);
let decimals = 2u32;
let name = "Burn Test Asset".to_string();
let description = "Testing burning".to_string();
let asset_type = AssetType::Digital;

// Tokenize asset first
let tokenized_asset = TokenizeContract::tokenize(
env.clone(),
asset_id,
symbol,
total_supply.clone(),
decimals,
name,
description,
asset_type,
tokenizer.clone(),
);

// Burn some tokens
let burn_amount = BigInt::from_i128(&env, 400);
let updated_asset = TokenizeContract::burn_tokens(env.clone(), asset_id, burn_amount.clone(), tokenizer.clone());

// Verify total supply decreased
assert_eq!(updated_asset.total_supply, &total_supply - &burn_amount);

// Verify tokenizer's ownership updated
let ownership: OwnershipRecord = env.storage().get((b"ownership", asset_id, &tokenizer)).unwrap().unwrap();
assert_eq!(ownership.balance, &total_supply - &burn_amount);
}

#[test]
fn test_tokenize_asset_success() {
let (env, client, admin) = setup_test_environment();
// initialize admin
client.initialize(&admin);

// prepare an asset and register
let owner = Address::generate(&env);
let id = make_bytes32(&env, 11);
let initial_token = make_bytes32(&env, 12);
let branch_id = make_bytes32(&env, 99);

let asset = Asset {
id: id.clone(),
name: String::from_str(&env, "Server X"),
asset_type: AssetType::Digital,
category: String::from_str(&env, "Compute"),
branch_id: branch_id.clone(),
department_id: 7,
status: AssetStatus::Active,
purchase_date: 1_725_000_100,
purchase_cost: 1_000_000,
current_value: 900_000,
warranty_expiry: 1_826_000_000,
stellar_token_id: initial_token,
owner: owner.clone(),
};

client.register_asset(&asset);

// new token id to set
let new_token = make_bytes32(&env, 13);

// admin-only: with mocked auth, this will succeed
let res = client.try_tokenize_asset(&id, &new_token);
assert!(res.is_ok());

// verify updated
let got = client.get_asset(&id);
assert_eq!(got.stellar_token_id, new_token);
#[should_panic(expected = "Unauthorized: only tokenizer can mint")]
fn test_mint_unauthorized() {
let env = Env::default();
let tokenizer = Address::random(&env);
let attacker = Address::random(&env);

let asset_id = 400u64;
let total_supply = BigInt::from_i128(&env, 500);

// Tokenize asset first
TokenizeContract::tokenize(
env.clone(),
asset_id,
"AST400".to_string(),
total_supply.clone(),
2,
"Unauthorized Mint".to_string(),
"Test".to_string(),
AssetType::Digital,
tokenizer.clone(),
);

// Attempt to mint from unauthorized address
let mint_amount = BigInt::from_i128(&env, 100);
TokenizeContract::mint_tokens(env, asset_id, mint_amount, attacker);
}

#[test]
#[should_panic(expected = "Error(Contract, #2)")]
fn test_tokenize_asset_without_admin_initialized() {
let (env, client, _admin) = setup_test_environment();

// prepare an asset and register
let owner = Address::generate(&env);
let id = make_bytes32(&env, 21);
let token = make_bytes32(&env, 22);
let branch_id = make_bytes32(&env, 1);

let asset = Asset {
id: id.clone(),
name: String::from_str(&env, "Router Y"),
asset_type: AssetType::Physical,
category: String::from_str(&env, "Network"),
branch_id: branch_id.clone(),
department_id: 2,
status: AssetStatus::Active,
purchase_date: 1_700_000_001,
purchase_cost: 50_000,
current_value: 45_000,
warranty_expiry: 1_760_000_000,
stellar_token_id: make_bytes32(&env, 23),
owner,
};

client.register_asset(&asset);

// calling tokenize without initialize should panic with AdminNotFound (Error #2)
client.tokenize_asset(&id, &token);
#[should_panic(expected = "Unauthorized: only tokenizer can burn")]
fn test_burn_unauthorized() {
let env = Env::default();
let tokenizer = Address::random(&env);
let attacker = Address::random(&env);

let asset_id = 500u64;
let total_supply = BigInt::from_i128(&env, 500);

// Tokenize asset first
TokenizeContract::tokenize(
env.clone(),
asset_id,
"AST500".to_string(),
total_supply.clone(),
2,
"Unauthorized Burn".to_string(),
"Test".to_string(),
AssetType::Digital,
tokenizer.clone(),
);

// Attempt to burn from unauthorized address
let burn_amount = BigInt::from_i128(&env, 100);
TokenizeContract::burn_tokens(env, asset_id, burn_amount, attacker);
}
Loading
Loading