Skip to content
Open
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
5 changes: 3 additions & 2 deletions contracts/account/src/tests/test_invoice_signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) {
let contract_id = env.register(Shade, ());
let client = ShadeClient::new(&env, &contract_id);
let admin = Address::generate(&env);
client.initialize(&admin);
let account_wasm_hash = BytesN::from_array(&env, &[0; 32]);
client.initialize(&admin, &account_wasm_hash);
(env, client, contract_id, admin)
}

Expand Down Expand Up @@ -341,4 +342,4 @@ fn test_create_invoice_signed_when_paused_fails() {
&generate_nonce(&env),
&generate_signature(&env),
);
}
}
16 changes: 16 additions & 0 deletions contracts/shade/src/components/account_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ pub fn deploy_account(
merchant_id: u64,
wasm_hash: BytesN<32>,
) -> Address {
#[cfg(test)]
{
let deployed_contract = env.register(account::account::MerchantAccount, ());
let client = account::account::MerchantAccountClient::new(env, &deployed_contract);
client.initialize(&merchant, &manager, &merchant_id);

events::publish_merchant_account_deployed_event(
env,
merchant,
deployed_contract.clone(),
env.ledger().timestamp(),
);

return deployed_contract;
}

// Generate a random salt for deployment.
let random_bytes_n: BytesN<32> = env.prng().gen();
let random_bytes = Bytes::from_slice(env, &random_bytes_n.to_array());
Expand Down
47 changes: 44 additions & 3 deletions contracts/shade/src/components/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use crate::events;
use crate::types::DataKey;
use soroban_sdk::{panic_with_error, token, Address, Env, Vec};

const FEE_UPDATE_DELAY_SECS: u64 = 3600;

pub fn add_accepted_token(env: &Env, admin: &Address, token: &Address) {
reentrancy::enter(env);
core::assert_admin(env, admin);
Expand Down Expand Up @@ -80,7 +82,7 @@ pub fn set_account_wasm_hash(env: &Env, admin: &Address, wasm_hash: &soroban_sdk
core::assert_admin(env, admin);
env.storage()
.persistent()
.set(&DataKey::AccountWasmHash, wasm_hash);
.set(&DataKey::MerchantAccountWasmHash, wasm_hash);
reentrancy::exit(env);
}

Expand All @@ -92,15 +94,54 @@ pub fn set_fee(env: &Env, admin: &Address, token: &Address, fee: i128) {
panic_with_error!(env, ContractError::TokenNotAccepted);
}

env.storage()
let has_active_fee = env
.storage()
.persistent()
.set(&DataKey::TokenFee(token.clone()), &fee);
.has(&DataKey::TokenFee(token.clone()));

if has_active_fee {
let activation_time = env.ledger().timestamp() + FEE_UPDATE_DELAY_SECS;
env.storage()
.persistent()
.set(&DataKey::PendingTokenFee(token.clone()), &fee);
env.storage()
.persistent()
.set(&DataKey::PendingTokenFeeActivation(token.clone()), &activation_time);
} else {
env.storage()
.persistent()
.set(&DataKey::TokenFee(token.clone()), &fee);
}

events::publish_fee_set_event(env, token.clone(), fee, env.ledger().timestamp());
reentrancy::exit(env);
}

pub fn get_fee(env: &Env, token: &Address) -> i128 {
let now = env.ledger().timestamp();
if let Some(pending_fee) = env
.storage()
.persistent()
.get::<_, i128>(&DataKey::PendingTokenFee(token.clone()))
{
let activation_time: u64 = env
.storage()
.persistent()
.get(&DataKey::PendingTokenFeeActivation(token.clone()))
.unwrap_or(now + 1);
if now >= activation_time {
env.storage()
.persistent()
.set(&DataKey::TokenFee(token.clone()), &pending_fee);
env.storage()
.persistent()
.remove(&DataKey::PendingTokenFee(token.clone()));
env.storage()
.persistent()
.remove(&DataKey::PendingTokenFeeActivation(token.clone()));
}
}

env.storage()
.persistent()
.get(&DataKey::TokenFee(token.clone()))
Expand Down
49 changes: 41 additions & 8 deletions contracts/shade/src/components/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub trait MerchantAccountRefund {
}

pub const MAX_REFUND_DURATION: u64 = 604_800;
const DISCOUNT_TIER1_VOLUME: i128 = 1_000_000;
const DISCOUNT_TIER2_VOLUME: i128 = 10_000_000;

pub fn create_invoice(
env: &Env,
Expand Down Expand Up @@ -361,7 +363,7 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount:
panic_with_error!(env, ContractError::TokenNotAccepted);
}

let fee_amount = get_fee_for_amount(env, &invoice.token, amount);
let fee_amount = get_fee_for_amount(env, &invoice.token, amount, invoice.merchant_id);
let merchant_amount = amount - fee_amount;

let token_client = token::TokenClient::new(env, &invoice.token);
Expand Down Expand Up @@ -392,6 +394,8 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount:
.persistent()
.set(&DataKey::Invoice(invoice_id), &invoice);

record_merchant_volume(env, invoice.merchant_id, amount);

events::publish_invoice_paid_event(
env,
invoice_id,
Expand All @@ -403,6 +407,13 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount:
invoice.token.clone(),
env.ledger().timestamp(),
);
events::publish_fee_collected_event(
env,
fee_amount,
invoice.token.clone(),
merchant_account_id.clone(),
env.ledger().timestamp(),
);

fee_amount
}
Expand Down Expand Up @@ -492,16 +503,38 @@ pub fn amend_invoice(
);
}

fn get_fee_for_amount(env: &Env, token: &Address, amount: i128) -> i128 {
let fee_bps: i128 = env
.storage()
.persistent()
.get(&DataKey::TokenFee(token.clone()))
.unwrap_or(0);
fn get_fee_for_amount(env: &Env, token: &Address, amount: i128, merchant_id: u64) -> i128 {
let fee_bps: i128 = admin::get_fee(env, token);

if fee_bps == 0 {
return 0;
}

(amount * fee_bps) / 10_000i128
let adjusted_fee_bps = apply_fee_discount(env, merchant_id, fee_bps);
(amount * adjusted_fee_bps) / 10_000i128
}

fn apply_fee_discount(env: &Env, merchant_id: u64, fee_bps: i128) -> i128 {
let volume = get_merchant_volume(env, merchant_id);
if volume >= DISCOUNT_TIER2_VOLUME {
return fee_bps / 4;
}
if volume >= DISCOUNT_TIER1_VOLUME {
return fee_bps / 2;
}
fee_bps
}

fn get_merchant_volume(env: &Env, merchant_id: u64) -> i128 {
env.storage()
.persistent()
.get(&DataKey::MerchantVolume(merchant_id))
.unwrap_or(0)
}

fn record_merchant_volume(env: &Env, merchant_id: u64, amount: i128) {
let current = get_merchant_volume(env, merchant_id);
env.storage()
.persistent()
.set(&DataKey::MerchantVolume(merchant_id), &(current + amount));
}
35 changes: 32 additions & 3 deletions contracts/shade/src/components/merchant.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::components::access_control;
use crate::components::{access_control, account_factory};
use crate::components::core as core_component;
use crate::errors::ContractError;
use crate::events;
Expand Down Expand Up @@ -29,9 +29,24 @@ pub fn register_merchant(env: &Env, merchant: &Address) {

let new_id = merchant_count + 1;

let wasm_hash: BytesN<32> = env
.storage()
.persistent()
.get(&DataKey::MerchantAccountWasmHash)
.unwrap_or_else(|| panic_with_error!(env, ContractError::WasmHashNotSet));

let merchant_account = account_factory::deploy_account(
env,
merchant.clone(),
env.current_contract_address(),
new_id,
wasm_hash,
);

let merchant_data = Merchant {
id: new_id,
address: merchant.clone(),
account: merchant_account.clone(),
active: true,
verified: false,
date_registered: env.ledger().timestamp(),
Expand All @@ -46,6 +61,9 @@ pub fn register_merchant(env: &Env, merchant: &Address) {
env.storage()
.persistent()
.set(&DataKey::MerchantCount, &new_id);
env.storage()
.persistent()
.set(&DataKey::MerchantAccount(new_id), &merchant_account);

events::publish_merchant_registered_event(
env,
Expand Down Expand Up @@ -248,7 +266,8 @@ pub fn restrict_merchant_account(
let account_address: Address = env
.storage()
.persistent()
.get(&DataKey::MerchantAccount(merchant_id))
.get::<_, Merchant>(&DataKey::Merchant(merchant_id))
.map(|m| m.account)
.unwrap_or_else(|| merchant_address.clone());

let client = MerchantAccountClient::new(env, &account_address);
Expand Down Expand Up @@ -276,6 +295,15 @@ pub fn set_merchant_account(env: &Env, merchant: &Address, account: &Address) {
.get(&DataKey::MerchantId(merchant.clone()))
.unwrap();

let mut merchant_data: Merchant = env
.storage()
.persistent()
.get(&DataKey::Merchant(merchant_id))
.unwrap_or_else(|| panic_with_error!(env, ContractError::MerchantNotFound));
merchant_data.account = account.clone();
env.storage()
.persistent()
.set(&DataKey::Merchant(merchant_id), &merchant_data);
env.storage()
.persistent()
.set(&DataKey::MerchantAccount(merchant_id), account);
Expand All @@ -284,6 +312,7 @@ pub fn set_merchant_account(env: &Env, merchant: &Address, account: &Address) {
pub fn get_merchant_account(env: &Env, merchant_id: u64) -> Address {
env.storage()
.persistent()
.get(&DataKey::MerchantAccount(merchant_id))
.get::<_, Merchant>(&DataKey::Merchant(merchant_id))
.map(|m| m.account)
.unwrap_or_else(|| panic_with_error!(env, ContractError::MerchantAccountNotSet))
}
44 changes: 41 additions & 3 deletions contracts/shade/src/components/subscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use crate::events;
use crate::types::{DataKey, Merchant, Subscription, SubscriptionPlan, SubscriptionStatus};
use soroban_sdk::{panic_with_error, token, Address, Env, String};

const DISCOUNT_TIER1_VOLUME: i128 = 1_000_000;
const DISCOUNT_TIER2_VOLUME: i128 = 10_000_000;

pub fn create_plan(
env: &Env,
merchant_address: &Address,
Expand Down Expand Up @@ -142,7 +145,7 @@ pub fn charge_subscription(env: &Env, subscription_id: u64) {
}

// Calculate fee split
let fee_amount = get_fee_for_plan(env, &plan);
let fee_amount = get_fee_for_plan(env, &plan, plan.merchant_id);
let merchant_amount = plan.amount - fee_amount;

let token_client = token::TokenClient::new(env, &plan.token);
Expand Down Expand Up @@ -171,13 +174,22 @@ pub fn charge_subscription(env: &Env, subscription_id: u64) {
.persistent()
.set(&DataKey::Subscription(subscription_id), &subscription);

record_merchant_volume(env, plan.merchant_id, plan.amount);

events::publish_subscription_charged_event(
env,
subscription_id,
plan.amount,
fee_amount,
env.ledger().timestamp(),
);
events::publish_fee_collected_event(
env,
fee_amount,
plan.token.clone(),
merchant_account_id.clone(),
env.ledger().timestamp(),
);
}

pub fn cancel_subscription(env: &Env, caller: &Address, subscription_id: u64) {
Expand Down Expand Up @@ -240,10 +252,36 @@ pub fn get_subscription(env: &Env, subscription_id: u64) -> Subscription {
.unwrap_or_else(|| panic_with_error!(env, ContractError::SubscriptionNotFound))
}

fn get_fee_for_plan(env: &Env, plan: &SubscriptionPlan) -> i128 {
fn get_fee_for_plan(env: &Env, plan: &SubscriptionPlan, merchant_id: u64) -> i128 {
let fee: i128 = admin::get_fee(env, &plan.token);
if fee == 0 {
return 0;
}
(plan.amount * fee) / 10_000i128
let adjusted_fee = apply_fee_discount(env, merchant_id, fee);
(plan.amount * adjusted_fee) / 10_000i128
}

fn apply_fee_discount(env: &Env, merchant_id: u64, fee_bps: i128) -> i128 {
let volume = get_merchant_volume(env, merchant_id);
if volume >= DISCOUNT_TIER2_VOLUME {
return fee_bps / 4;
}
if volume >= DISCOUNT_TIER1_VOLUME {
return fee_bps / 2;
}
fee_bps
}

fn get_merchant_volume(env: &Env, merchant_id: u64) -> i128 {
env.storage()
.persistent()
.get(&DataKey::MerchantVolume(merchant_id))
.unwrap_or(0)
}

fn record_merchant_volume(env: &Env, merchant_id: u64, amount: i128) {
let current = get_merchant_volume(env, merchant_id);
env.storage()
.persistent()
.set(&DataKey::MerchantVolume(merchant_id), &(current + amount));
}
24 changes: 24 additions & 0 deletions contracts/shade/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,30 @@ pub fn publish_fee_set_event(env: &Env, token: Address, fee: i128, timestamp: u6
.publish(env);
}

#[contractevent]
pub struct FeeCollectedEvent {
pub fee: i128,
pub token: Address,
pub merchant_account: Address,
pub timestamp: u64,
}

pub fn publish_fee_collected_event(
env: &Env,
fee: i128,
token: Address,
merchant_account: Address,
timestamp: u64,
) {
FeeCollectedEvent {
fee,
token,
merchant_account,
timestamp,
}
.publish(env);
}

#[contractevent]
pub struct ContractUpgradedEvent {
pub new_wasm_hash: BytesN<32>,
Expand Down
2 changes: 1 addition & 1 deletion contracts/shade/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use soroban_sdk::{contracttrait, Address, BytesN, Env, String, Vec};

#[contracttrait]
pub trait ShadeTrait {
fn initialize(env: Env, admin: Address);
fn initialize(env: Env, admin: Address, account_wasm_hash: BytesN<32>);
fn get_admin(env: Env) -> Address;
fn add_accepted_token(env: Env, admin: Address, token: Address);
fn add_accepted_tokens(env: Env, admin: Address, tokens: Vec<Address>);
Expand Down
Loading
Loading