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
57 changes: 32 additions & 25 deletions contracts/revenue_split/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![no_std]

use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, Vec, token};
use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, Vec, token, Symbol};
use common::CommonError;

#[cfg(test)]
Expand Down Expand Up @@ -82,16 +82,12 @@ impl RevenueSplitContract {
Ok(())
}

/// Distributes a specific token amount from a sender to the listed recipients based on their shares.
pub fn distribute(env: Env, token: Address, from: Address, amount: i128) -> Result<(), ContractError> {
/// Distributes multiple assets from a sender to the listed recipients based on their shares.
pub fn distribute(env: Env, from: Address, assets: Vec<(Address, i128)>) -> Result<(), ContractError> {
if !env.storage().instance().has(&DataKey::Admin) {
return Err(ContractError::NotInitialized);
}

if amount <= 0 {
return Err(ContractError::InvalidAmount);
}

from.require_auth();

let shares: Vec<RecipientShare> = env
Expand All @@ -100,27 +96,38 @@ impl RevenueSplitContract {
.get(&DataKey::Recipients)
.ok_or(ContractError::NotInitialized)?;

let client = token::Client::new(&env, &token);

let mut amount_distributed = 0;

for (i, share) in shares.iter().enumerate() {
// Calculate slice of the total amount using basis points
// Formula: amount * basis_points / 10000
let recipient_amount = (amount as i128 * share.basis_points as i128) / TOTAL_BASIS_POINTS as i128;

if recipient_amount > 0 {
// To avoid precision loss dust, the last recipient takes any minor remainders.
if i as u32 == shares.len() - 1 {
let final_amount = amount - amount_distributed;
if final_amount > 0 {
client.transfer(&from, &share.destination, &final_amount);
for asset_pair in assets.iter() {
let token = asset_pair.0;
let amount = asset_pair.1;

if amount <= 0 {
return Err(ContractError::InvalidAmount);
}

let client = token::Client::new(&env, &token);

let mut amount_distributed = 0;

for (i, share) in shares.iter().enumerate() {
// Calculate slice of the total amount using basis points
// Formula: amount * basis_points / 10000
let recipient_amount = (amount as i128 * share.basis_points as i128) / TOTAL_BASIS_POINTS as i128;

if recipient_amount > 0 {
// To avoid precision loss dust, the last recipient takes any minor remainders.
if i as u32 == shares.len() - 1 {
let final_amount = amount - amount_distributed;
if final_amount > 0 {
client.transfer(&from, &share.destination, &final_amount);
}
} else {
client.transfer(&from, &share.destination, &recipient_amount);
amount_distributed += recipient_amount;
}
} else {
client.transfer(&from, &share.destination, &recipient_amount);
amount_distributed += recipient_amount;
}
}

env.events().publish((Symbol::new(&env, "distribute"), token.clone()), amount);
}

Ok(())
Expand Down
55 changes: 53 additions & 2 deletions contracts/revenue_split/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ fn test_distribution_invalid_amount_returns_error() {
let sender = Address::generate(&env);
stellar_asset_client.mint(&sender, &1000);

assert!(contract_client.try_distribute(&token_id, &sender, &0).is_err());
let assets = Vec::from_array(&env, [(token_id.clone(), 0i128)]);
assert!(contract_client.try_distribute(&sender, &assets).is_err());
assert_eq!(token_client.balance(&sender), 1000);
}

Expand Down Expand Up @@ -154,7 +155,8 @@ fn test_distribution() {
stellar_asset_client.mint(&sender, &1000);

// Distribute 1000 tokens
contract_client.distribute(&token_id, &sender, &1000);
let assets = Vec::from_array(&env, [(token_id.clone(), 1000i128)]);
contract_client.distribute(&sender, &assets);

// Verify balances
assert_eq!(token_client.balance(&sender), 0);
Expand All @@ -163,6 +165,55 @@ fn test_distribution() {
assert_eq!(token_client.balance(&recipient3), 200);
}

#[test]
fn test_distribution_multiple_assets() {
let env = Env::default();
env.mock_all_auths();

// Create tokens
let token_admin = Address::generate(&env);
let (token_id1, stellar_asset_client1, token_client1) = create_token_contract(&env, &token_admin);
let (token_id2, stellar_asset_client2, token_client2) = create_token_contract(&env, &token_admin);

// Setup revenue split contract
let contract_id = env.register(RevenueSplitContract, ());
let contract_client = RevenueSplitContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let recipient1 = Address::generate(&env);
let recipient2 = Address::generate(&env);

// 60%, 40%
let shares = Vec::from_array(&env, [
RecipientShare { destination: recipient1.clone(), basis_points: 6000 },
RecipientShare { destination: recipient2.clone(), basis_points: 4000 },
]);

contract_client.init(&admin, &shares);

// Fund a sender
let sender = Address::generate(&env);
stellar_asset_client1.mint(&sender, &1000);
stellar_asset_client2.mint(&sender, &2000);

// Distribute multiple assets
let assets = Vec::from_array(&env, [
(token_id1.clone(), 1000i128),
(token_id2.clone(), 2000i128)
]);
contract_client.distribute(&sender, &assets);

// Verify balances token 1
assert_eq!(token_client1.balance(&sender), 0);
assert_eq!(token_client1.balance(&recipient1), 600);
assert_eq!(token_client1.balance(&recipient2), 400);

// Verify balances token 2
assert_eq!(token_client2.balance(&sender), 0);
assert_eq!(token_client2.balance(&recipient1), 1200);
assert_eq!(token_client2.balance(&recipient2), 800);
}

#[test]
fn test_update_recipients() {
let env = Env::default();
Expand Down
Loading