/ abzu-token / src / curve.rs
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  }