lib.rs
1 // Copyright (c) 2025 ADnet Contributors 2 // This file is part of the AlphaVM library. 3 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at: 7 8 // http://www.apache.org/licenses/LICENSE-2.0 9 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 //! # Off-Ramp System (F-D40-46) 17 //! 18 //! Implements the off-ramp system for ALPHA redemption through Governors. 19 //! 20 //! ## Overview 21 //! 22 //! The off-ramp system allows users to redeem ALPHA tokens through their 23 //! Central Bank Governor. Users must have a valid KYC ID registered with 24 //! the Governor to initiate redemptions. 25 //! 26 //! ## Process Flow (F-D40-43) 27 //! 28 //! 1. **Pre-Verification (F-D44a)**: Client verifies KYC ID is valid 29 //! 2. **Request Submission (F-D40)**: User submits off-ramp request 30 //! 3. **Escrow (F-D41)**: ALPHA is locked in escrow 31 //! 4. **Processing (F-D42)**: Governor processes the redemption off-chain 32 //! 5. **Completion (F-D43)**: Request marked complete, ALPHA kept or burned 33 //! 34 //! ## Rejection Rules 35 //! 36 //! Requests can only be rejected for: 37 //! - Malformed transaction 38 //! - ID status changed (became invalid during processing) 39 //! 40 //! AML concerns are handled off-chain and do NOT result in on-chain rejection. 41 42 #![forbid(unsafe_code)] 43 44 use alphavm_console::{ 45 network::Network, 46 types::{Address, Field}, 47 }; 48 use alphavm_ledger_governor::{GidId, GovernorIdentity}; 49 50 use anyhow::{Result, ensure}; 51 use serde::{Deserialize, Serialize}; 52 use std::collections::HashMap; 53 54 // ============================================================================ 55 // Constants 56 // ============================================================================ 57 58 /// Minimum off-ramp amount (100 ALPHA) 59 pub const MIN_OFFRAMP_AMOUNT: u64 = 100_000_000; 60 61 /// Maximum pending requests per user 62 pub const MAX_PENDING_PER_USER: usize = 5; 63 64 /// Request timeout in blocks (~24 hours at 5 second blocks) 65 pub const REQUEST_TIMEOUT_BLOCKS: u64 = 17_280; 66 67 // ============================================================================ 68 // Off-Ramp Status (F-D41-43) 69 // ============================================================================ 70 71 /// Status of an off-ramp request 72 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 73 pub enum OffRampStatus { 74 /// Request submitted, awaiting escrow 75 Pending, 76 /// ALPHA escrowed, awaiting Governor processing 77 Escrowed, 78 /// Governor is processing the redemption off-chain 79 Processing, 80 /// Completed successfully 81 Completed(CompletionAction), 82 /// Request was rejected 83 Rejected(RejectionReason), 84 /// Request timed out 85 TimedOut, 86 } 87 88 impl Default for OffRampStatus { 89 fn default() -> Self { 90 Self::Pending 91 } 92 } 93 94 /// What happened to the ALPHA after completion (F-D43) 95 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 96 pub enum CompletionAction { 97 /// Governor kept the ALPHA (most common) 98 Kept, 99 /// Governor burned the ALPHA (reduces outstanding balance) 100 Burned, 101 } 102 103 /// Reasons for rejection (F-D43) 104 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 105 pub enum RejectionReason { 106 /// Transaction was malformed or invalid 107 MalformedTransaction, 108 /// KYC ID became invalid during processing 109 IdStatusChanged, 110 } 111 112 // ============================================================================ 113 // Off-Ramp Request (F-D40) 114 // ============================================================================ 115 116 /// An off-ramp request from a user 117 #[derive(Clone, Debug, Serialize, Deserialize)] 118 #[serde(bound = "")] 119 pub struct OffRampRequest<N: Network> { 120 /// Unique request ID 121 id: Field<N>, 122 /// User requesting redemption 123 user: Address<N>, 124 /// Amount of ALPHA to redeem (in microcredits) 125 amount: u64, 126 /// Target Governor for redemption 127 governor_id: GidId<N>, 128 /// KYC ID code (must be valid in Governor's list) 129 kyc_id: [u8; 32], 130 /// Block height when request was created 131 created_at: u64, 132 /// Current status 133 status: OffRampStatus, 134 /// Block height when status last changed 135 status_updated_at: u64, 136 /// Processing notes (for Governor use) 137 notes: Option<String>, 138 } 139 140 impl<N: Network> OffRampRequest<N> { 141 /// Create a new off-ramp request 142 /// 143 /// # Arguments 144 /// * `id` - Unique request identifier 145 /// * `user` - Address of the requesting user 146 /// * `amount` - Amount of ALPHA to redeem 147 /// * `governor_id` - Target Governor for redemption 148 /// * `kyc_id` - KYC ID code (must be valid) 149 /// * `current_block` - Current block height 150 /// 151 /// # Errors 152 /// Returns an error if amount is below minimum 153 pub fn new( 154 id: Field<N>, 155 user: Address<N>, 156 amount: u64, 157 governor_id: GidId<N>, 158 kyc_id: [u8; 32], 159 current_block: u64, 160 ) -> Result<Self> { 161 ensure!(amount >= MIN_OFFRAMP_AMOUNT, "Amount {amount} below minimum {MIN_OFFRAMP_AMOUNT}"); 162 163 Ok(Self { 164 id, 165 user, 166 amount, 167 governor_id, 168 kyc_id, 169 created_at: current_block, 170 status: OffRampStatus::Pending, 171 status_updated_at: current_block, 172 notes: None, 173 }) 174 } 175 176 // Getters 177 pub fn id(&self) -> &Field<N> { 178 &self.id 179 } 180 181 pub fn user(&self) -> &Address<N> { 182 &self.user 183 } 184 185 pub fn amount(&self) -> u64 { 186 self.amount 187 } 188 189 pub fn governor_id(&self) -> &GidId<N> { 190 &self.governor_id 191 } 192 193 pub fn kyc_id(&self) -> &[u8; 32] { 194 &self.kyc_id 195 } 196 197 pub fn created_at(&self) -> u64 { 198 self.created_at 199 } 200 201 pub fn status(&self) -> OffRampStatus { 202 self.status 203 } 204 205 pub fn status_updated_at(&self) -> u64 { 206 self.status_updated_at 207 } 208 209 pub fn notes(&self) -> Option<&str> { 210 self.notes.as_deref() 211 } 212 213 /// Check if the request is still pending (not finalized) 214 pub fn is_pending(&self) -> bool { 215 matches!(self.status, OffRampStatus::Pending | OffRampStatus::Escrowed | OffRampStatus::Processing) 216 } 217 218 /// Check if the request has been finalized 219 pub fn is_finalized(&self) -> bool { 220 matches!(self.status, OffRampStatus::Completed(_) | OffRampStatus::Rejected(_) | OffRampStatus::TimedOut) 221 } 222 223 /// Check if the request has timed out 224 pub fn is_timed_out(&self, current_block: u64) -> bool { 225 current_block > self.created_at + REQUEST_TIMEOUT_BLOCKS 226 } 227 228 // Status transitions 229 230 /// Move to escrowed status (ALPHA locked) 231 pub fn escrow(&mut self, current_block: u64) -> Result<()> { 232 ensure!(self.status == OffRampStatus::Pending, "Can only escrow pending requests"); 233 self.status = OffRampStatus::Escrowed; 234 self.status_updated_at = current_block; 235 Ok(()) 236 } 237 238 /// Move to processing status (Governor working on it) 239 pub fn start_processing(&mut self, current_block: u64) -> Result<()> { 240 ensure!(self.status == OffRampStatus::Escrowed, "Can only process escrowed requests"); 241 self.status = OffRampStatus::Processing; 242 self.status_updated_at = current_block; 243 Ok(()) 244 } 245 246 /// Complete the request (F-D43) 247 pub fn complete(&mut self, action: CompletionAction, current_block: u64) -> Result<()> { 248 ensure!(self.status == OffRampStatus::Processing, "Can only complete processing requests"); 249 self.status = OffRampStatus::Completed(action); 250 self.status_updated_at = current_block; 251 Ok(()) 252 } 253 254 /// Reject the request (F-D43) 255 pub fn reject(&mut self, reason: RejectionReason, current_block: u64) -> Result<()> { 256 ensure!(self.is_pending(), "Can only reject pending requests"); 257 self.status = OffRampStatus::Rejected(reason); 258 self.status_updated_at = current_block; 259 Ok(()) 260 } 261 262 /// Mark as timed out 263 pub fn timeout(&mut self, current_block: u64) -> Result<()> { 264 ensure!(self.is_pending(), "Can only timeout pending requests"); 265 self.status = OffRampStatus::TimedOut; 266 self.status_updated_at = current_block; 267 Ok(()) 268 } 269 270 /// Add processing notes 271 pub fn set_notes(&mut self, notes: String) { 272 self.notes = Some(notes); 273 } 274 } 275 276 // ============================================================================ 277 // Off-Ramp Processor 278 // ============================================================================ 279 280 /// Processes off-ramp requests for a Governor 281 #[derive(Clone, Debug)] 282 pub struct OffRampProcessor<N: Network> { 283 /// Governor ID this processor handles 284 governor_id: GidId<N>, 285 /// Pending requests (by request ID) 286 pending_requests: HashMap<Field<N>, OffRampRequest<N>>, 287 /// Completed requests (for audit trail) 288 completed_requests: Vec<OffRampRequest<N>>, 289 /// Count of requests per user (for rate limiting) 290 user_request_counts: HashMap<Address<N>, usize>, 291 /// Total amount currently in escrow 292 total_escrowed: u64, 293 /// Total amount successfully redeemed 294 total_redeemed: u64, 295 } 296 297 impl<N: Network> OffRampProcessor<N> { 298 /// Create a new off-ramp processor for a Governor 299 pub fn new(governor_id: GidId<N>) -> Self { 300 Self { 301 governor_id, 302 pending_requests: HashMap::new(), 303 completed_requests: Vec::new(), 304 user_request_counts: HashMap::new(), 305 total_escrowed: 0, 306 total_redeemed: 0, 307 } 308 } 309 310 /// Get the Governor ID 311 pub fn governor_id(&self) -> &GidId<N> { 312 &self.governor_id 313 } 314 315 /// Get pending request count 316 pub fn pending_count(&self) -> usize { 317 self.pending_requests.len() 318 } 319 320 /// Get total amount in escrow 321 pub fn total_escrowed(&self) -> u64 { 322 self.total_escrowed 323 } 324 325 /// Get total amount redeemed 326 pub fn total_redeemed(&self) -> u64 { 327 self.total_redeemed 328 } 329 330 /// Submit a new off-ramp request (F-D40) 331 /// 332 /// # Pre-conditions 333 /// - KYC ID must be valid (caller should check first via F-D44a) 334 /// - User must not exceed MAX_PENDING_PER_USER 335 /// - Amount must be >= MIN_OFFRAMP_AMOUNT 336 pub fn submit_request(&mut self, request: OffRampRequest<N>, governor: &GovernorIdentity<N>) -> Result<()> { 337 // Verify KYC ID is valid (F-D44a) 338 ensure!(governor.is_valid_kyc_id(request.kyc_id()), "KYC ID is not valid for this Governor"); 339 340 // Check user rate limit 341 let user_count = self.user_request_counts.get(request.user()).copied().unwrap_or(0); 342 ensure!(user_count < MAX_PENDING_PER_USER, "User has too many pending requests (max {MAX_PENDING_PER_USER})"); 343 344 // Check for duplicate ID 345 ensure!(!self.pending_requests.contains_key(request.id()), "Request ID already exists"); 346 347 // Add request 348 let user = *request.user(); 349 self.pending_requests.insert(*request.id(), request); 350 *self.user_request_counts.entry(user).or_insert(0) += 1; 351 352 Ok(()) 353 } 354 355 /// Escrow ALPHA for a request (F-D41) 356 pub fn escrow_request(&mut self, request_id: &Field<N>, current_block: u64) -> Result<u64> { 357 let request = self.pending_requests.get_mut(request_id).ok_or_else(|| anyhow::anyhow!("Request not found"))?; 358 359 request.escrow(current_block)?; 360 self.total_escrowed += request.amount(); 361 362 Ok(request.amount()) 363 } 364 365 /// Start processing a request (F-D42) 366 pub fn start_processing(&mut self, request_id: &Field<N>, current_block: u64) -> Result<()> { 367 let request = self.pending_requests.get_mut(request_id).ok_or_else(|| anyhow::anyhow!("Request not found"))?; 368 369 request.start_processing(current_block)?; 370 Ok(()) 371 } 372 373 /// Complete a request (F-D43) 374 pub fn complete_request( 375 &mut self, 376 request_id: &Field<N>, 377 action: CompletionAction, 378 current_block: u64, 379 ) -> Result<u64> { 380 let mut request = 381 self.pending_requests.remove(request_id).ok_or_else(|| anyhow::anyhow!("Request not found"))?; 382 383 let amount = request.amount(); 384 request.complete(action, current_block)?; 385 386 // Update counters 387 self.total_escrowed = self.total_escrowed.saturating_sub(amount); 388 self.total_redeemed += amount; 389 390 // Update user count 391 if let Some(count) = self.user_request_counts.get_mut(request.user()) { 392 *count = count.saturating_sub(1); 393 } 394 395 // Archive for audit 396 self.completed_requests.push(request); 397 398 Ok(amount) 399 } 400 401 /// Reject a request (F-D43) 402 pub fn reject_request( 403 &mut self, 404 request_id: &Field<N>, 405 reason: RejectionReason, 406 current_block: u64, 407 ) -> Result<u64> { 408 let mut request = 409 self.pending_requests.remove(request_id).ok_or_else(|| anyhow::anyhow!("Request not found"))?; 410 411 let amount = request.amount(); 412 let was_escrowed = matches!(request.status(), OffRampStatus::Escrowed | OffRampStatus::Processing); 413 414 request.reject(reason, current_block)?; 415 416 // Return escrowed funds 417 if was_escrowed { 418 self.total_escrowed = self.total_escrowed.saturating_sub(amount); 419 } 420 421 // Update user count 422 if let Some(count) = self.user_request_counts.get_mut(request.user()) { 423 *count = count.saturating_sub(1); 424 } 425 426 // Archive for audit 427 self.completed_requests.push(request); 428 429 Ok(if was_escrowed { amount } else { 0 }) 430 } 431 432 /// Process timeouts for all pending requests 433 pub fn process_timeouts(&mut self, current_block: u64) -> Vec<Field<N>> { 434 let mut timed_out = Vec::new(); 435 436 for (id, request) in &self.pending_requests { 437 if request.is_timed_out(current_block) { 438 timed_out.push(*id); 439 } 440 } 441 442 for id in &timed_out { 443 if let Some(mut request) = self.pending_requests.remove(id) { 444 let was_escrowed = matches!(request.status(), OffRampStatus::Escrowed | OffRampStatus::Processing); 445 446 let _ = request.timeout(current_block); 447 448 if was_escrowed { 449 self.total_escrowed = self.total_escrowed.saturating_sub(request.amount()); 450 } 451 452 if let Some(count) = self.user_request_counts.get_mut(request.user()) { 453 *count = count.saturating_sub(1); 454 } 455 456 self.completed_requests.push(request); 457 } 458 } 459 460 timed_out 461 } 462 463 /// Get a pending request by ID 464 pub fn get_request(&self, request_id: &Field<N>) -> Option<&OffRampRequest<N>> { 465 self.pending_requests.get(request_id) 466 } 467 468 /// Get all pending requests for a user 469 pub fn get_user_requests(&self, user: &Address<N>) -> Vec<&OffRampRequest<N>> { 470 self.pending_requests.values().filter(|r| r.user() == user).collect() 471 } 472 473 /// Get completed requests (for audit) 474 pub fn completed_requests(&self) -> &[OffRampRequest<N>] { 475 &self.completed_requests 476 } 477 } 478 479 // ============================================================================ 480 // Pre-Submission Verification (F-D44a) 481 // ============================================================================ 482 483 /// Verify a KYC ID is valid before submitting an off-ramp request (F-D44a) 484 /// 485 /// This is a client-side check that should be performed before submitting 486 /// to the chain. If this check passes, the redemption should never be 487 /// rejected (unless the ID status changes during processing). 488 /// 489 /// # Arguments 490 /// * `governor` - The target Governor for redemption 491 /// * `kyc_id` - The KYC ID to verify 492 /// 493 /// # Returns 494 /// `true` if the KYC ID is valid and the user can submit an off-ramp request 495 pub fn verify_kyc_id_valid<N: Network>(governor: &GovernorIdentity<N>, kyc_id: &[u8; 32]) -> bool { 496 governor.is_valid_kyc_id(kyc_id) 497 } 498 499 /// Get the associated address for a KYC ID 500 /// 501 /// # Arguments 502 /// * `governor` - The target Governor 503 /// * `kyc_id` - The KYC ID to look up 504 /// 505 /// # Returns 506 /// The on-chain address associated with this KYC ID, if valid 507 pub fn get_kyc_address<'a, N: Network>(governor: &'a GovernorIdentity<N>, kyc_id: &[u8; 32]) -> Option<&'a Address<N>> { 508 governor.get_kyc_address(kyc_id) 509 } 510 511 // ============================================================================ 512 // Tests 513 // ============================================================================ 514 515 #[cfg(test)] 516 mod tests { 517 use super::*; 518 use alphavm_console::{network::MainnetV0, types::Field}; 519 520 type CurrentNetwork = MainnetV0; 521 522 fn create_test_governor() -> GovernorIdentity<CurrentNetwork> { 523 use alphavm_console::{prelude::Double, types::Group}; 524 use alphavm_ledger_governor::bls::BlsPublicKey; 525 526 // Create minimal GID for testing with unique signers 527 // Use double_and_add pattern to create unique points 528 let registrar = Address::zero(); 529 let generator: Group<CurrentNetwork> = Group::generator(); 530 let mut signers = Vec::with_capacity(8); 531 let mut current: Group<CurrentNetwork> = generator; 532 for _ in 0..8 { 533 signers.push(BlsPublicKey::new(current)); 534 current = current.double(); // Each point is double the previous 535 } 536 537 GovernorIdentity::new(registrar, 5, signers, 1_000_000_000, 1, 0).unwrap() 538 } 539 540 #[test] 541 fn test_request_creation() { 542 let request = OffRampRequest::<CurrentNetwork>::new( 543 Field::from_u64(1), 544 Address::zero(), 545 MIN_OFFRAMP_AMOUNT, 546 GidId::new(Field::from_u64(100)), 547 [1u8; 32], 548 1000, 549 ) 550 .unwrap(); 551 552 assert_eq!(request.amount(), MIN_OFFRAMP_AMOUNT); 553 assert_eq!(request.status(), OffRampStatus::Pending); 554 assert!(request.is_pending()); 555 assert!(!request.is_finalized()); 556 } 557 558 #[test] 559 fn test_request_minimum_amount() { 560 let result = OffRampRequest::<CurrentNetwork>::new( 561 Field::from_u64(1), 562 Address::zero(), 563 MIN_OFFRAMP_AMOUNT - 1, 564 GidId::new(Field::from_u64(100)), 565 [1u8; 32], 566 1000, 567 ); 568 569 assert!(result.is_err()); 570 } 571 572 #[test] 573 fn test_request_status_transitions() { 574 let mut request = OffRampRequest::<CurrentNetwork>::new( 575 Field::from_u64(1), 576 Address::zero(), 577 MIN_OFFRAMP_AMOUNT, 578 GidId::new(Field::from_u64(100)), 579 [1u8; 32], 580 1000, 581 ) 582 .unwrap(); 583 584 // Pending -> Escrowed 585 request.escrow(1001).unwrap(); 586 assert_eq!(request.status(), OffRampStatus::Escrowed); 587 588 // Escrowed -> Processing 589 request.start_processing(1002).unwrap(); 590 assert_eq!(request.status(), OffRampStatus::Processing); 591 592 // Processing -> Completed 593 request.complete(CompletionAction::Kept, 1003).unwrap(); 594 assert_eq!(request.status(), OffRampStatus::Completed(CompletionAction::Kept)); 595 assert!(request.is_finalized()); 596 } 597 598 #[test] 599 fn test_request_rejection() { 600 let mut request = OffRampRequest::<CurrentNetwork>::new( 601 Field::from_u64(1), 602 Address::zero(), 603 MIN_OFFRAMP_AMOUNT, 604 GidId::new(Field::from_u64(100)), 605 [1u8; 32], 606 1000, 607 ) 608 .unwrap(); 609 610 request.reject(RejectionReason::IdStatusChanged, 1001).unwrap(); 611 assert_eq!(request.status(), OffRampStatus::Rejected(RejectionReason::IdStatusChanged)); 612 assert!(request.is_finalized()); 613 } 614 615 #[test] 616 fn test_request_timeout() { 617 let request = OffRampRequest::<CurrentNetwork>::new( 618 Field::from_u64(1), 619 Address::zero(), 620 MIN_OFFRAMP_AMOUNT, 621 GidId::new(Field::from_u64(100)), 622 [1u8; 32], 623 1000, 624 ) 625 .unwrap(); 626 627 assert!(!request.is_timed_out(1000 + REQUEST_TIMEOUT_BLOCKS)); 628 assert!(request.is_timed_out(1000 + REQUEST_TIMEOUT_BLOCKS + 1)); 629 } 630 631 #[test] 632 fn test_processor_creation() { 633 let processor = OffRampProcessor::<CurrentNetwork>::new(GidId::new(Field::from_u64(100))); 634 635 assert_eq!(processor.pending_count(), 0); 636 assert_eq!(processor.total_escrowed(), 0); 637 assert_eq!(processor.total_redeemed(), 0); 638 } 639 640 #[test] 641 fn test_kyc_verification() { 642 let mut governor = create_test_governor(); 643 644 // No KYC IDs initially 645 assert!(!verify_kyc_id_valid(&governor, &[1u8; 32])); 646 647 // Add a KYC ID 648 governor.add_kyc_id([1u8; 32], Address::zero(), 1).unwrap(); 649 650 // Now it should be valid 651 assert!(verify_kyc_id_valid(&governor, &[1u8; 32])); 652 assert!(!verify_kyc_id_valid(&governor, &[2u8; 32])); 653 654 // Remove it 655 governor.remove_kyc_id(&[1u8; 32]).unwrap(); 656 assert!(!verify_kyc_id_valid(&governor, &[1u8; 32])); 657 } 658 659 #[test] 660 fn test_processor_submit_and_complete() { 661 let mut governor = create_test_governor(); 662 governor.add_kyc_id([1u8; 32], Address::zero(), 1).unwrap(); 663 664 let mut processor = OffRampProcessor::new(governor.id().clone()); 665 666 let request = OffRampRequest::new( 667 Field::from_u64(1), 668 Address::zero(), 669 MIN_OFFRAMP_AMOUNT, 670 governor.id().clone(), 671 [1u8; 32], 672 1000, 673 ) 674 .unwrap(); 675 676 // Submit 677 processor.submit_request(request, &governor).unwrap(); 678 assert_eq!(processor.pending_count(), 1); 679 680 // Escrow 681 let amount = processor.escrow_request(&Field::from_u64(1), 1001).unwrap(); 682 assert_eq!(amount, MIN_OFFRAMP_AMOUNT); 683 assert_eq!(processor.total_escrowed(), MIN_OFFRAMP_AMOUNT); 684 685 // Process 686 processor.start_processing(&Field::from_u64(1), 1002).unwrap(); 687 688 // Complete 689 let redeemed = processor.complete_request(&Field::from_u64(1), CompletionAction::Kept, 1003).unwrap(); 690 assert_eq!(redeemed, MIN_OFFRAMP_AMOUNT); 691 assert_eq!(processor.pending_count(), 0); 692 assert_eq!(processor.total_escrowed(), 0); 693 assert_eq!(processor.total_redeemed(), MIN_OFFRAMP_AMOUNT); 694 } 695 }