curve.rs
1 //! Time-limited bonding curve for Year 1 price discovery. 2 //! 3 //! # Anti-Ponzi Design 4 //! 5 //! The bonding curve is a **temporary bootstrap mechanism**: 6 //! - Active only during Year 1 7 //! - Foundation acts as transparent market-maker 8 //! - Curve formula is public and immutable 9 //! - No profit motive (proceeds fund operations) 10 //! - Sunsets completely after Year 1 11 //! 12 //! After Year 1: pure peer-to-peer market, no Foundation involvement. 13 14 use crate::{Balance, Error, Result}; 15 use chrono::{DateTime, Duration, Utc}; 16 use serde::{Deserialize, Serialize}; 17 18 /// Curve shape for price calculation. 19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 20 #[serde(rename_all = "snake_case")] 21 pub enum CurveShape { 22 /// Price increases linearly with supply sold. 23 /// Simple and predictable. 24 Linear, 25 /// Price increases with sqrt of supply sold. 26 /// More gradual initial increase. 27 SquareRoot, 28 } 29 30 /// Configuration for the bonding curve. 31 #[derive(Debug, Clone, Serialize, Deserialize)] 32 pub struct CurveConfig { 33 /// Curve shape function. 34 pub shape: CurveShape, 35 /// Starting price in drops per ABZU (at 0% sold). 36 /// e.g., $0.0001 worth of ABZU = 1 drop starting price. 37 pub base_price_drops: u128, 38 /// Maximum price multiplier at 100% sold. 39 pub max_multiplier: u64, 40 /// When the curve was activated. 41 pub activated_at: DateTime<Utc>, 42 /// When the curve sunsets (Year 1 end). 43 pub sunset_at: DateTime<Utc>, 44 /// Total supply available through curve (500B = Service Reserve). 45 pub total_supply: Balance, 46 } 47 48 impl Default for CurveConfig { 49 fn default() -> Self { 50 let now = Utc::now(); 51 Self { 52 shape: CurveShape::SquareRoot, 53 // 1 drop = 0.000000000000000001 ABZU 54 // Base price: 1000 drops (0.000000000000001 ABZU) 55 base_price_drops: 1_000, 56 max_multiplier: 100, // 100x at full supply 57 activated_at: now, 58 sunset_at: now + Duration::days(365), 59 total_supply: Balance::from_abzu(500_000_000_000), // 500B 60 } 61 } 62 } 63 64 /// The bonding curve implementation. 65 /// 66 /// # Mechanics 67 /// 68 /// ```text 69 /// Year 1 (Bootstrap): 70 /// ┌─────────────────────────────────────────────────────────┐ 71 /// │ Foundation acts as transparent market-maker │ 72 /// │ │ 73 /// │ Price = f(supply_sold) │ 74 /// │ Starting: 1 ABZU = 1000 drops base │ 75 /// │ Curve: sqrt (more gradual) │ 76 /// │ │ 77 /// │ Buy: User → Curve → ABZU │ 78 /// │ Sell: ABZU → Curve → User (at curve price) │ 79 /// └─────────────────────────────────────────────────────────┘ 80 /// ``` 81 #[derive(Debug, Clone, Serialize, Deserialize)] 82 pub struct BondingCurve { 83 config: CurveConfig, 84 /// Amount sold so far. 85 sold: Balance, 86 /// Foundation reserve (proceeds from sales). 87 reserve_drops: u128, 88 } 89 90 impl BondingCurve { 91 /// Create a new bonding curve with given config. 92 pub fn new(config: CurveConfig) -> Self { 93 Self { 94 config, 95 sold: Balance::ZERO, 96 reserve_drops: 0, 97 } 98 } 99 100 /// Create with default Year 1 configuration. 101 pub fn year_one() -> Self { 102 Self::new(CurveConfig::default()) 103 } 104 105 /// Check if the curve is still active. 106 pub fn is_active(&self) -> bool { 107 let now = Utc::now(); 108 now >= self.config.activated_at && now < self.config.sunset_at 109 } 110 111 /// Get current price in drops per ABZU. 112 pub fn current_price(&self) -> u128 { 113 self.price_at_supply(self.sold) 114 } 115 116 /// Calculate price at a given sold supply. 117 fn price_at_supply(&self, sold: Balance) -> u128 { 118 let total = self.config.total_supply.drops(); 119 let sold_drops = sold.drops(); 120 121 if total == 0 { 122 return self.config.base_price_drops; 123 } 124 125 // Progress as fraction (0.0 to 1.0) 126 let progress = (sold_drops as f64) / (total as f64); 127 128 let multiplier = match self.config.shape { 129 CurveShape::Linear => { 130 // Linear: 1 + (max-1) * progress 131 1.0 + (self.config.max_multiplier as f64 - 1.0) * progress 132 } 133 CurveShape::SquareRoot => { 134 // Sqrt: 1 + (max-1) * sqrt(progress) 135 // More gradual initial increase 136 1.0 + (self.config.max_multiplier as f64 - 1.0) * progress.sqrt() 137 } 138 }; 139 140 (self.config.base_price_drops as f64 * multiplier) as u128 141 } 142 143 /// Calculate cost to buy a given amount of ABZU. 144 /// 145 /// Uses integral approximation for accuracy with curves. 146 pub fn buy_cost(&self, amount: Balance) -> Result<Balance> { 147 if !self.is_active() { 148 return Err(Error::CurveSunset); 149 } 150 151 let remaining = self.config.total_supply.checked_sub(self.sold)?; 152 if amount > remaining { 153 return Err(Error::InsufficientSupply); 154 } 155 156 // Simpson's rule integration for better accuracy 157 let steps = 100u128; 158 let step_size = amount.drops() / steps; 159 160 if step_size == 0 { 161 // Small purchase, use current price 162 let cost = (amount.drops() as u128) * self.current_price(); 163 return Ok(Balance::from_drops(cost)); 164 } 165 166 let mut total_cost: u128 = 0; 167 let start = self.sold; 168 169 for i in 0..steps { 170 let supply_at_step = Balance::from_drops(start.drops() + i * step_size); 171 let price = self.price_at_supply(supply_at_step); 172 total_cost = total_cost.saturating_add(step_size * price); 173 } 174 175 Ok(Balance::from_drops(total_cost)) 176 } 177 178 /// Execute a buy, returning the actual amount bought. 179 pub fn buy(&mut self, payment_drops: u128) -> Result<Balance> { 180 if !self.is_active() { 181 return Err(Error::CurveSunset); 182 } 183 184 // Binary search for amount we can buy with given payment 185 let mut low = 0u128; 186 let remaining = self.config.total_supply.checked_sub(self.sold)?; 187 let mut high = remaining.drops(); 188 189 while low < high { 190 let mid = low + (high - low + 1) / 2; 191 let test_amount = Balance::from_drops(mid); 192 193 match self.buy_cost(test_amount) { 194 Ok(cost) if cost.drops() <= payment_drops => low = mid, 195 _ => high = mid - 1, 196 } 197 } 198 199 if low == 0 { 200 return Err(Error::InsufficientFunds); 201 } 202 203 let amount = Balance::from_drops(low); 204 let cost = self.buy_cost(amount)?; 205 206 self.sold = self.sold.checked_add(amount)?; 207 self.reserve_drops = self.reserve_drops.saturating_add(cost.drops()); 208 209 Ok(amount) 210 } 211 212 /// Calculate proceeds from selling back ABZU. 213 /// 214 /// Sells at current curve price (may be lower than purchase price). 215 pub fn sell_proceeds(&self, amount: Balance) -> Result<Balance> { 216 if !self.is_active() { 217 return Err(Error::CurveSunset); 218 } 219 220 if amount > self.sold { 221 return Err(Error::InsufficientSupply); 222 } 223 224 // Sell at current price (simpler than reverse integration) 225 let price = self.current_price(); 226 let proceeds = (amount.drops() as u128) * price; 227 228 if proceeds > self.reserve_drops { 229 return Err(Error::InsufficientReserve); 230 } 231 232 Ok(Balance::from_drops(proceeds)) 233 } 234 235 /// Execute a sell, returning the proceeds in drops. 236 pub fn sell(&mut self, amount: Balance) -> Result<Balance> { 237 let proceeds = self.sell_proceeds(amount)?; 238 239 self.sold = self.sold.checked_sub(amount)?; 240 self.reserve_drops = self.reserve_drops.saturating_sub(proceeds.drops()); 241 242 Ok(proceeds) 243 } 244 245 /// Get current state. 246 pub fn state(&self) -> CurveState { 247 CurveState { 248 is_active: self.is_active(), 249 sold: self.sold, 250 remaining: self.config.total_supply.checked_sub(self.sold).unwrap_or(Balance::ZERO), 251 current_price: self.current_price(), 252 reserve_drops: self.reserve_drops, 253 sunset_at: self.config.sunset_at, 254 } 255 } 256 } 257 258 /// Current state of the bonding curve. 259 #[derive(Debug, Clone, Serialize, Deserialize)] 260 pub struct CurveState { 261 pub is_active: bool, 262 pub sold: Balance, 263 pub remaining: Balance, 264 pub current_price: u128, 265 pub reserve_drops: u128, 266 pub sunset_at: DateTime<Utc>, 267 } 268 269 #[cfg(test)] 270 mod tests { 271 use super::*; 272 273 #[test] 274 fn test_curve_active() { 275 let curve = BondingCurve::year_one(); 276 assert!(curve.is_active()); 277 } 278 279 #[test] 280 fn test_price_increases() { 281 // Use a small-scale curve for testing 282 let mut curve = BondingCurve::new(CurveConfig { 283 total_supply: Balance::from_abzu(1_000_000), // 1M instead of 500B 284 ..CurveConfig::default() 285 }); 286 let price_start = curve.current_price(); 287 288 // Buy 10% of supply 289 let payment = 100_000_000_000_000_000_000u128; // 100 ABZU worth at base price 290 let bought = curve.buy(payment).unwrap(); 291 assert!(bought.drops() > 0, "Should buy something"); 292 293 let price_after = curve.current_price(); 294 assert!(price_after > price_start, "Price should increase after buying"); 295 } 296 297 #[test] 298 fn test_sell_returns_proceeds() { 299 let mut curve = BondingCurve::year_one(); 300 301 // Buy first 302 let bought = curve.buy(1_000_000_000_000).unwrap(); 303 304 // Sell half 305 let half = Balance::from_drops(bought.drops() / 2); 306 let proceeds = curve.sell(half).unwrap(); 307 308 assert!(proceeds.drops() > 0); 309 } 310 311 #[test] 312 fn test_curve_sunset() { 313 let mut config = CurveConfig::default(); 314 config.sunset_at = Utc::now() - Duration::hours(1); // Already sunset 315 316 let curve = BondingCurve::new(config); 317 assert!(!curve.is_active()); 318 319 let result = curve.buy_cost(Balance::from_abzu(1)); 320 assert!(matches!(result, Err(Error::CurveSunset))); 321 } 322 323 #[test] 324 fn test_sqrt_vs_linear() { 325 let linear = BondingCurve::new(CurveConfig { 326 shape: CurveShape::Linear, 327 ..CurveConfig::default() 328 }); 329 330 let sqrt = BondingCurve::new(CurveConfig { 331 shape: CurveShape::SquareRoot, 332 ..CurveConfig::default() 333 }); 334 335 // At low supply, sqrt should have higher price (faster initial rise) 336 let test_supply = Balance::from_abzu(1_000_000_000); // 1B 337 let linear_price = linear.price_at_supply(test_supply); 338 let sqrt_price = sqrt.price_at_supply(test_supply); 339 340 // Sqrt curve rises faster initially 341 assert!(sqrt_price > linear_price); 342 } 343 }