diff --git a/README.md b/README.md index a6487443..232c621e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,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 @@ -115,6 +116,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 28cf3b8a..54fa12cb 100644 --- a/referral-contract/src/lib.rs +++ b/referral-contract/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] + use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Symbol, + contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Symbol, Vec, }; /// Event name emitted when a referrer's tier is upgraded. @@ -9,6 +10,9 @@ pub const REFERRAL_TIER_UPGRADED_EVENT_NAME: Symbol = symbol_short!("tier_upg"); /// Event name emitted when referral rewards are accrued. pub const REFERRAL_REWARD_ACCRUED_EVENT_NAME: Symbol = symbol_short!("ref_rwd"); +/// Event name emitted when a multi-level referral reward is distributed. +pub const REFERRAL_REWARD_DISTRIBUTED_EVENT_NAME: Symbol = symbol_short!("ref_dist"); + /// Event name emitted when referral rewards are claimed. pub const REFERRAL_REWARDS_CLAIMED_EVENT_NAME: Symbol = symbol_short!("ref_clm"); @@ -30,6 +34,7 @@ pub const DEFAULT_GOLD_THRESHOLD: i128 = 1_000_000; /// Default reward rate for Gold tier: 1,500 bps (15.00%). pub const DEFAULT_GOLD_BPS: u32 = 1_500; +// New variants must be appended at the end; discriminants are part of the ABI. #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] @@ -41,6 +46,9 @@ pub enum ContractError { NotPositiveAmount = 5, ArithmeticOverflow = 6, NoRewardsPending = 7, + SelfReferral = 8, + ReferralAlreadyRegistered = 9, + InvalidFeeAmount = 10, } /// Referral tiers in ascending rank and reward rate. @@ -93,6 +101,15 @@ pub struct ReferralRewardAccrued { pub reward_rate_bps: u32, } +/// Event data payload emitted when a referral reward is distributed to a chain level. +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct ReferralRewardDistributed { + pub tier: u32, + pub referrer: Address, + pub amount: i128, +} + /// Event data payload emitted when referral rewards are claimed. #[derive(Clone, Debug, Eq, PartialEq)] #[contracttype] @@ -105,6 +122,8 @@ pub struct ReferralRewardsClaimed { #[contracttype] pub enum DataKey { FirstTradeStatus(Address), + Referral(Address), + RewardBalance(Address), Initialised, Admin, ReferrerVolume(Address), @@ -159,9 +178,101 @@ 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::