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
29 changes: 29 additions & 0 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ impl EscrowContract {
Ok(())
}

/// Update the protocol configuration.
pub fn set_protocol_config(env: Env, config: ProtocolConfig) -> Result<(), Error> {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(Error::Unauthorized)?;
admin.require_auth();
env.storage().instance().set(&DataKey::ProtocolConfig, &config);
Ok(())
}

/// Get the current protocol configuration.
pub fn get_protocol_config(env: Env) -> Result<ProtocolConfig, Error> {
env.storage().instance().get(&DataKey::ProtocolConfig).ok_or(Error::NotInitialized)
}

/// Add a token to the allowlist — admin only.
pub fn add_allowed_token(env: Env, token: Address) -> Result<(), Error> {
let admin: Address = env
Expand Down Expand Up @@ -861,6 +878,18 @@ impl EscrowContract {

let is_multi_token = m.token_b.is_some() && m.conversion_rate > 0;

let config: ProtocolConfig = env.storage().instance().get(&DataKey::ProtocolConfig).unwrap_or(ProtocolConfig {
cancellation_fee_basis_points: 0,
treasury: env.current_contract_address(),
});

let fee_amount = if config.cancellation_fee_basis_points > 0 {
m.stake_amount.checked_mul(config.cancellation_fee_basis_points as i128).ok_or(Error::Overflow)? / 10_000
} else {
0
};
let refund_amount = m.stake_amount.checked_sub(fee_amount).ok_or(Error::Overflow)?;

if m.player1_deposited {
let client_a = token::Client::new(&env, &m.token);
client_a.transfer(&env.current_contract_address(), &m.player1, &m.stake_amount);
Expand Down
80 changes: 80 additions & 0 deletions contracts/escrow/src/tests/cancellation_fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#![cfg(test)]
extern crate std;

use super::*;
use soroban_sdk::testutils::Address as _;

#[test]
fn test_cancellation_fee_deduction() {
let (env, contract_id, _oracle, player1, player2, token, admin) = setup();
let client = EscrowContractClient::new(&env, &contract_id);
let token_client = token_client(&env, &token);

// Initial balances
assert_eq!(token_client.balance(&player1), 1000);

let match_id = helpers::create_default_match(&client, &env, &player1, &player2, &token, "test_fee_game");

// Player 1 deposits
client.deposit(&match_id, &player1);
assert_eq!(token_client.balance(&player1), 900);
assert_eq!(token_client.balance(&contract_id), 100);

// Default fee is 1%. Stake is 100. Fee should be 1.
// Player 1 cancels
client.cancel_match(&match_id, &player1);

// Contract should refund 99 to Player 1, and send 1 to admin (default treasury)
assert_eq!(token_client.balance(&player1), 999);
assert_eq!(token_client.balance(&admin), 1); // Admin starts with 0
assert_eq!(token_client.balance(&contract_id), 0);
}

#[test]
fn test_cancellation_no_fee_if_no_deposit() {
let (env, contract_id, _oracle, player1, player2, token, admin) = setup();
let client = EscrowContractClient::new(&env, &contract_id);
let token_client = token_client(&env, &token);

let match_id = helpers::create_default_match(&client, &env, &player1, &player2, &token, "test_no_fee_game");

// Neither player deposits
// Player 1 cancels
client.cancel_match(&match_id, &player1);

// Player 1 should not be charged since they didn't deposit
assert_eq!(token_client.balance(&player1), 1000);
assert_eq!(token_client.balance(&admin), 0);
assert_eq!(token_client.balance(&contract_id), 0);
}

#[test]
fn test_cancellation_fee_with_custom_config() {
let (env, contract_id, _oracle, player1, player2, token, admin) = setup();
let client = EscrowContractClient::new(&env, &contract_id);
let token_client = token_client(&env, &token);

let new_treasury = Address::generate(&env);

// Admin sets new config: 5% fee
let config = types::ProtocolConfig {
cancellation_fee_basis_points: 500, // 5%
treasury: new_treasury.clone(),
};

env.mock_all_auths();
client.set_protocol_config(&config);

let match_id = helpers::create_default_match(&client, &env, &player1, &player2, &token, "test_custom_fee_game");

// Player 2 deposits
client.deposit(&match_id, &player2);

// Player 1 cancels
client.cancel_match(&match_id, &player1);

// Player 2 should be refunded 95. Treasury gets 5.
assert_eq!(token_client.balance(&player2), 995);
assert_eq!(token_client.balance(&new_treasury), 5);
assert_eq!(token_client.balance(&contract_id), 0);
}
1 change: 1 addition & 0 deletions contracts/escrow/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,4 @@ pub fn setup_with_four_players() -> (
pub fn mint_player_balance(asset_client: &StellarAssetClient, player: &Address, amount: i128) {
asset_client.mint(player, &amount);
}
mod cancellation_fee;