/ ledger / offramp / src / lib.rs
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  }