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
3 changes: 2 additions & 1 deletion contracts/manage_hub/src/fractionalization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ impl FractionalizationModule {
for holder in holder_keys.iter() {
let holder_address: Address = holder;
if let Some(share_count) = shares.get(holder_address.clone()) {
let voting_power = share_count
let share_count_i128: i128 = share_count;
let voting_power = share_count_i128
.checked_mul(10_000)
.ok_or(Error::TimestampOverflow)?
.checked_div(info.total_shares)
Expand Down
31 changes: 31 additions & 0 deletions contracts/manage_hub/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ mod membership_token;
mod migration;
mod pause_errors;
mod rewards;
pub mod royalty;
mod staking;
mod staking_errors;
mod subscription;
Expand Down Expand Up @@ -139,6 +140,36 @@ impl Contract {
Ok(())
}

pub fn transfer_token_with_royalty(
env: Env,
id: BytesN<32>,
new_user: Address,
payment_token: Address,
sale_price: i128,
) -> Result<(), Error> {
MembershipTokenContract::transfer_token_with_royalty(
env,
id,
new_user,
payment_token,
sale_price,
)?;
Ok(())
}

pub fn set_royalty(
env: Env,
token_id: BytesN<32>,
recipients: Vec<types::RoyaltyRecipient>,
) -> Result<(), Error> {
crate::royalty::RoyaltyModule::set_royalty(env, token_id, recipients)?;
Ok(())
}

pub fn get_royalty_info(env: Env, token_id: BytesN<32>) -> Option<types::RoyaltyInfo> {
crate::royalty::RoyaltyModule::get_royalty_info(env, token_id)
}

pub fn approve(
env: Env,
token_id: BytesN<32>,
Expand Down
27 changes: 27 additions & 0 deletions contracts/manage_hub/src/membership_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub enum DataKey {
UpgradeHistory(BytesN<32>),
/// Version snapshot for rollback, keyed by token ID and version number.
VersionSnapshot(BytesN<32>, u32),
Royalty(BytesN<32>),
}

#[contracttype]
Expand Down Expand Up @@ -202,6 +203,32 @@ impl MembershipTokenContract {
Ok(())
}

pub fn transfer_token_with_royalty(
env: Env,
id: BytesN<32>,
new_user: Address,
payment_token: Address,
sale_price: i128,
) -> Result<(), Error> {
// First do the standard transfer logic (which includes auth and ownership changes)
Self::transfer_token(env.clone(), id.clone(), new_user.clone())?;

// Then calculate and process royalties
crate::royalty::RoyaltyModule::calculate_and_pay_royalties(
&env,
&id,
&payment_token,
sale_price,
)?;

// Emit token transferred event with sale price info
env.events().publish(
(symbol_short!("tok_sale"), id, new_user),
(sale_price, env.ledger().timestamp()),
);

Ok(())
}
pub fn batch_transfer_tokens(
env: Env,
params: Vec<crate::types::BatchTransferParams>,
Expand Down
128 changes: 128 additions & 0 deletions contracts/manage_hub/src/royalty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Allow deprecated events API until migration to #[contractevent] macro
#![allow(deprecated)]

use crate::errors::Error;
use crate::membership_token::DataKey;
use crate::types::{RoyaltyConfig, RoyaltyInfo, RoyaltyRecipient};
use soroban_sdk::{symbol_short, Address, BytesN, Env, Vec};

pub struct RoyaltyModule;

impl RoyaltyModule {
/// Maximum allowed total royalty percentage (basis points: 10000 = 100%)
const MAX_ROYALTY_BPS: u32 = 10000;

/// Validates royalty configuration
fn validate_config(recipients: &Vec<RoyaltyRecipient>) -> Result<u32, Error> {
let mut total_percentage: u32 = 0;

for recipient in recipients.iter() {
total_percentage = total_percentage.saturating_add(recipient.percentage);
}

if total_percentage > Self::MAX_ROYALTY_BPS {
return Err(Error::InvalidPaymentAmount);
}

Ok(total_percentage)
}

/// Sets or updates the royalty configuration for a token
pub fn set_royalty(
env: Env,
token_id: BytesN<32>,
recipients: Vec<RoyaltyRecipient>,
) -> Result<(), Error> {
// Validation
let _ = Self::validate_config(&recipients)?;

let config = RoyaltyConfig {
token_id: token_id.clone(),
recipients: recipients.clone(),
enabled: true,
};

// Emit set event
env.events().publish(
(symbol_short!("roy_set"), token_id.clone()),
(recipients.len(), env.ledger().timestamp()),
);

env.storage()
.persistent()
.set(&DataKey::Royalty(token_id), &config);

Ok(())
}

/// Calculates required royalty payments based on sale price and emits distribution events.
pub fn calculate_and_pay_royalties(
env: &Env,
token_id: &BytesN<32>,
payment_token: &Address,
sale_price: i128,
) -> Result<i128, Error> {
if sale_price <= 0 {
return Ok(0);
}

let config: Option<RoyaltyConfig> = env
.storage()
.persistent()
.get(&DataKey::Royalty(token_id.clone()));

if let Some(cfg) = config {
if !cfg.enabled || cfg.recipients.is_empty() {
return Ok(0);
}

let mut total_royalty_amount: i128 = 0;

for recipient in cfg.recipients.iter() {
// Calculate portion using basis points (percentage * sale_price / 10000)
let amount =
(sale_price * recipient.percentage as i128) / Self::MAX_ROYALTY_BPS as i128;

if amount > 0 {
total_royalty_amount += amount;

// Note: Here we'd normally call `token::Client::new(env, payment_token).transfer(...)`
// To keep things simple and avoiding external cross-contract token integrations for the royalty distribution,
// we emit an event that off-chain indexers or wrapper contracts can use to fulfill the payment synchronously or asynchronously.
env.events().publish(
(
symbol_short!("roy_paid"),
token_id.clone(),
recipient.address.clone(),
),
(payment_token.clone(), amount, env.ledger().timestamp()),
);
}
}

return Ok(total_royalty_amount);
}

Ok(0)
}

/// Get details of royalty configuration
pub fn get_royalty_info(env: Env, token_id: BytesN<32>) -> Option<RoyaltyInfo> {
let config_opt: Option<RoyaltyConfig> =
env.storage().persistent().get(&DataKey::Royalty(token_id));

if let Some(config) = config_opt {
let mut total_percentage = 0;
for r in config.recipients.iter() {
total_percentage += r.percentage;
}

Some(RoyaltyInfo {
config,
total_percentage,
})
} else {
None
}
}
}
Loading