diff --git a/README.md b/README.md index c6440c6b..cd5a55bd 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ The contracts layer is responsible for: ## Workspace layout * [Cargo.toml](./Cargo.toml): Rust workspace configuration -* [creator-keys](./creator-keys): first Soroban contract crate +* [creator-keys](./creator-keys): Soroban creator-key contract crate +* [referral-contract](./referral-contract): referral registration and tiered reward accounting ## Current state @@ -114,6 +115,8 @@ No additional environment variables are currently required to run the contract v For contributor test deployments and release checks, use the guide in [docs/stellar-testnet-deployment.md](./docs/stellar-testnet-deployment.md). Store and name release wasm files using [docs/deploy-artifacts.md](./docs/deploy-artifacts.md). For client and server integration expectations (read methods, events, version bumps), see [docs/contract-consumer-boundaries.md](./docs/contract-consumer-boundaries.md). For detailed return value semantics of every read-only entrypoint (units, precision, edge-case outputs), see [docs/read-only-methods.md](./docs/read-only-methods.md). For contract event naming conventions and topic format, see [docs/contract-event-conventions.md](./docs/contract-event-conventions.md). For quote-related storage key behavior and invariants, see [docs/quote-storage-keys.md](./docs/quote-storage-keys.md). For caller restrictions and the authorization model for each public function, see [docs/authorization-model.md](./docs/authorization-model.md). For the bonding curve formula, fee application, and worked price examples, see [docs/bonding-curve-formula.md](./docs/bonding-curve-formula.md). For the locked allocation parameter format, unlock ledger constraints, and supply impact, see [docs/locked-allocation-parameter-format.md](./docs/locked-allocation-parameter-format.md). For key staking storage keys and the data model, see [docs/key-staking-storage.md](./docs/key-staking-storage.md). +For referral-chain registration and multi-tier swap-fee reward accounting, see [docs/referral-reward-contract.md](./docs/referral-reward-contract.md). + ## Open source workflow * Read [CONTRIBUTING.md](./CONTRIBUTING.md) before starting work. diff --git a/docs/referral-reward-contract.md b/docs/referral-reward-contract.md new file mode 100644 index 00000000..ebc375a6 --- /dev/null +++ b/docs/referral-reward-contract.md @@ -0,0 +1,22 @@ +# Referral reward contract + +`referral-contract` records an immutable direct referrer for each wallet and +walks that relationship up to three tiers. The wallet must authorize +`register_referral` and `on_swap_fee`. + +`on_swap_fee(wallet, fee_amount)` treats `fee_amount` as the referral-fee +allocation (not the full swap amount). For each registered tier it accrues +40%, 20%, or 10% of that allocation to the referrer's balance, rounded down +to the smallest integer unit. The remaining allocation is not assigned by +this contract. `get_reward_balance(referrer)` reports the accumulated amount. + +Each tier allocation emits a `ref_dist` event whose +`ReferralRewardDistributed` payload contains `tier`, `referrer`, and `amount`. +The event and accrued balance provide a stable settlement/indexing surface. +The current entrypoint does not accept an asset address, so it does not +transfer tokens; the caller's swap/settlement flow must handle payment in the +same units as `fee_amount`. + +`get_referral_chain(wallet)` returns a Soroban vector ordered from direct +referrer outward, containing no more than three addresses. Self-referrals and +repeat registrations are rejected. diff --git a/referral-contract/src/lib.rs b/referral-contract/src/lib.rs index 78461526..7d14069f 100644 --- a/referral-contract/src/lib.rs +++ b/referral-contract/src/lib.rs @@ -1,13 +1,21 @@ #![no_std] + use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, + contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Vec, }; +pub const REFERRAL_REWARD_DISTRIBUTED_EVENT_NAME: soroban_sdk::Symbol = + symbol_short!("ref_dist"); + #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum ContractError { AlreadyInitialised = 1, + ReferralAlreadyRegistered = 2, + SelfReferral = 3, + InvalidFeeAmount = 4, + ArithmeticOverflow = 5, } #[contract] @@ -17,9 +25,19 @@ pub struct ReferralContract; #[contracttype] pub enum DataKey { FirstTradeStatus(Address), + Referral(Address), + RewardBalance(Address), Initialised, } +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct ReferralRewardDistributed { + pub tier: u32, + pub referrer: Address, + pub amount: i128, +} + #[contractimpl] impl ReferralContract { pub fn init(env: Env) -> Result<(), ContractError> { @@ -40,9 +58,107 @@ impl ReferralContract { .unwrap_or(false) } + /// Register a wallet's immutable direct referrer. + pub fn register_referral( + env: Env, + wallet: Address, + referrer: Address, + ) -> Result<(), ContractError> { + wallet.require_auth(); + + if wallet == referrer { + return Err(ContractError::SelfReferral); + } + + let key = DataKey::Referral(wallet.clone()); + if env.storage().persistent().has(&key) { + return Err(ContractError::ReferralAlreadyRegistered); + } + + env.storage().persistent().set(&key, &referrer); + Ok(()) + } + + /// Returns the direct referrer followed by up to two successive referrers. + pub fn get_referral_chain(env: Env, wallet: Address) -> Vec
{ + let mut chain = Vec::new(&env); + let mut current = wallet; + + for _ in 0..3 { + let Some(referrer) = env + .storage() + .persistent() + .get::