Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 106 additions & 5 deletions contracts/payment-vault-contract/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use soroban_sdk::{Address, Env};
use crate::storage;
use soroban_sdk::{Address, Env, token};
use crate::storage::{self, Booking, BookingStatus};
use crate::error::VaultError;
use crate::events;

pub fn initialize_vault(
env: &Env,
admin: &Address,
token: &Address,
env: &Env,
admin: &Address,
token: &Address,
oracle: &Address
) -> Result<(), VaultError> {
// 1. Check if already initialized
Expand All @@ -18,5 +19,105 @@ pub fn initialize_vault(
storage::set_token(env, token);
storage::set_oracle(env, oracle);

Ok(())
}

pub fn create_booking(
env: &Env,
user: &Address,
expert: &Address,
rate: i128,
booked_duration: u64,
) -> Result<u64, VaultError> {
// Require authorization from the user creating the booking
user.require_auth();

// Validate rate
if rate <= 0 {
return Err(VaultError::InvalidAmount);
}

// Calculate total deposit
let total_deposit = rate * (booked_duration as i128);

if total_deposit <= 0 {
return Err(VaultError::InvalidAmount);
}

// Get the token contract
let token_address = storage::get_token(env);
let token_client = token::Client::new(env, &token_address);

// Transfer tokens from user to this contract
let contract_address = env.current_contract_address();
token_client.transfer(user, &contract_address, &total_deposit);

// Generate booking ID and create booking
let booking_id = storage::get_next_booking_id(env);
let booking = Booking {
id: booking_id,
expert: expert.clone(),
user: user.clone(),
rate,
total_deposit,
booked_duration,
status: BookingStatus::Pending,
};

// Save booking
storage::save_booking(env, &booking);

Ok(booking_id)
}

pub fn finalize_session(
env: &Env,
booking_id: u64,
actual_duration: u64,
) -> Result<(), VaultError> {
// 1. Require Oracle authorization
let oracle = storage::get_oracle(env);
oracle.require_auth();

// 2. Get booking and verify it exists
let booking = storage::get_booking(env, booking_id)
.ok_or(VaultError::BookingNotFound)?;

// 3. Verify booking is in Pending status
if booking.status != BookingStatus::Pending {
return Err(VaultError::BookingNotPending);
}

// 4. Calculate payments
let expert_pay = booking.rate * (actual_duration as i128);
let refund = booking.total_deposit - expert_pay;

// Ensure calculations are valid
if expert_pay < 0 || refund < 0 {
return Err(VaultError::InvalidAmount);
}
Comment on lines +91 to +98
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Missing validation: actual_duration can exceed booked_duration.

The Oracle can pass an actual_duration greater than booked_duration, which would make refund negative (line 93) and trigger InvalidAmount. While this prevents overpayment, the error is misleading.

Consider adding explicit validation to reject actual_duration > booked_duration with a clearer error, or cap actual_duration at booked_duration.

πŸ”§ Option 1: Explicit validation
+    // Ensure actual duration doesn't exceed booked duration
+    if actual_duration > booking.booked_duration {
+        return Err(VaultError::InvalidAmount);
+    }
+
     // 4. Calculate payments
     let expert_pay = booking.rate * (actual_duration as i128);
     let refund = booking.total_deposit - expert_pay;
-
-    // Ensure calculations are valid
-    if expert_pay < 0 || refund < 0 {
-        return Err(VaultError::InvalidAmount);
-    }
πŸ”§ Option 2: Cap duration
+    let capped_duration = actual_duration.min(booking.booked_duration);
+
     // 4. Calculate payments
-    let expert_pay = booking.rate * (actual_duration as i128);
+    let expert_pay = booking.rate * (capped_duration as i128);
     let refund = booking.total_deposit - expert_pay;
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 4. Calculate payments
let expert_pay = booking.rate * (actual_duration as i128);
let refund = booking.total_deposit - expert_pay;
// Ensure calculations are valid
if expert_pay < 0 || refund < 0 {
return Err(VaultError::InvalidAmount);
}
// 4. Calculate payments
let capped_duration = actual_duration.min(booking.booked_duration);
let expert_pay = booking.rate * (capped_duration as i128);
let refund = booking.total_deposit - expert_pay;
// Ensure calculations are valid
if expert_pay < 0 || refund < 0 {
return Err(VaultError::InvalidAmount);
}
πŸ€– Prompt for AI Agents
In `@contracts/payment-vault-contract/src/contract.rs` around lines 91 - 98, Add
an explicit validation before computing expert_pay/refund: check if
actual_duration > booking.booked_duration and either (a) return a clearer error
variant (e.g., VaultError::ActualDurationExceedsBooked) instead of
InvalidAmount, or (b) cap actual_duration = booking.booked_duration (use
std::cmp::min) before calculating expert_pay and refund; ensure this check is
placed prior to using actual_duration in the expert_pay and refund calculations
and update any affected error handling or tests accordingly.


// 5. Get token contract
let token_address = storage::get_token(env);
let token_client = token::Client::new(env, &token_address);
let contract_address = env.current_contract_address();

// 6. Execute transfers
// Pay expert
if expert_pay > 0 {
token_client.transfer(&contract_address, &booking.expert, &expert_pay);
}

// Refund user
if refund > 0 {
token_client.transfer(&contract_address, &booking.user, &refund);
}

// 7. Update booking status to Complete
storage::update_booking_status(env, booking_id, BookingStatus::Complete);

// 8. Emit SessionFinalized event
events::session_finalized(env, booking_id, actual_duration, expert_pay);

Ok(())
}
3 changes: 3 additions & 0 deletions contracts/payment-vault-contract/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ pub enum VaultError {
NotInitialized = 1,
AlreadyInitialized = 2,
NotAuthorized = 3,
BookingNotFound = 4,
BookingNotPending = 5,
InvalidAmount = 6,
}
6 changes: 6 additions & 0 deletions contracts/payment-vault-contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use soroban_sdk::{Env, symbol_short};

pub fn session_finalized(env: &Env, booking_id: u64, actual_duration: u64, total_cost: i128) {
let topics = (symbol_short!("finalized"), booking_id);
env.events().publish(topics, (actual_duration, total_cost));
}
29 changes: 26 additions & 3 deletions contracts/payment-vault-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

mod contract;
mod error;
mod events;
mod storage;
#[cfg(test)]
mod test;
Expand All @@ -16,11 +17,33 @@ pub struct PaymentVaultContract;
impl PaymentVaultContract {
/// Initialize the vault with the Admin, the Payment Token, and the Oracle (Backend)
pub fn init(
env: Env,
admin: Address,
token: Address,
env: Env,
admin: Address,
token: Address,
oracle: Address
) -> Result<(), VaultError> {
contract::initialize_vault(&env, &admin, &token, &oracle)
}

/// Create a booking for a consultation session
/// User deposits tokens upfront based on rate * booked_duration
pub fn create_booking(
env: Env,
user: Address,
expert: Address,
rate: i128,
booked_duration: u64,
) -> Result<u64, VaultError> {
contract::create_booking(&env, &user, &expert, rate, booked_duration)
}

/// Finalize a session (Oracle-only)
/// Calculates payments based on actual duration and processes refunds
pub fn finalize_session(
env: Env,
booking_id: u64,
actual_duration: u64,
) -> Result<(), VaultError> {
contract::finalize_session(&env, booking_id, actual_duration)
}
}
53 changes: 53 additions & 0 deletions contracts/payment-vault-contract/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ pub enum DataKey {
Admin,
Token,
Oracle,
Booking(u64), // Booking ID -> Booking
BookingCounter, // Counter for generating unique booking IDs
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BookingStatus {
Pending,
Complete,
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct Booking {
pub id: u64,
pub expert: Address,
pub user: Address,
pub rate: i128, // Payment per second
pub total_deposit: i128, // Total amount deposited by user
pub booked_duration: u64, // Booked duration in seconds
pub status: BookingStatus,
}

// --- Admin ---
Expand Down Expand Up @@ -37,4 +58,36 @@ pub fn set_oracle(env: &Env, oracle: &Address) {

pub fn get_oracle(env: &Env) -> Address {
env.storage().instance().get(&DataKey::Oracle).unwrap()
}

// --- Booking Counter ---
pub fn get_next_booking_id(env: &Env) -> u64 {
let current: u64 = env
.storage()
.instance()
.get(&DataKey::BookingCounter)
.unwrap_or(0);
let next = current + 1;
env.storage().instance().set(&DataKey::BookingCounter, &next);
next
}
Comment on lines +64 to +73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Storage type mismatch could cause booking ID reuse.

BookingCounter uses instance storage (which has TTL and can expire), while Booking records use persistent storage. If the instance storage expires and is later recreated, the counter resets to 0, potentially causing ID collisions with existing persistent bookings.

Consider using persistent storage for BookingCounter to ensure IDs remain unique across the contract's lifetime.

πŸ”§ Suggested fix
 pub fn get_next_booking_id(env: &Env) -> u64 {
     let current: u64 = env
         .storage()
-        .instance()
+        .persistent()
         .get(&DataKey::BookingCounter)
         .unwrap_or(0);
     let next = current + 1;
-    env.storage().instance().set(&DataKey::BookingCounter, &next);
+    env.storage().persistent().set(&DataKey::BookingCounter, &next);
     next
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn get_next_booking_id(env: &Env) -> u64 {
let current: u64 = env
.storage()
.instance()
.get(&DataKey::BookingCounter)
.unwrap_or(0);
let next = current + 1;
env.storage().instance().set(&DataKey::BookingCounter, &next);
next
}
pub fn get_next_booking_id(env: &Env) -> u64 {
let current: u64 = env
.storage()
.persistent()
.get(&DataKey::BookingCounter)
.unwrap_or(0);
let next = current + 1;
env.storage().persistent().set(&DataKey::BookingCounter, &next);
next
}
πŸ€– Prompt for AI Agents
In `@contracts/payment-vault-contract/src/storage.rs` around lines 64 - 73,
get_next_booking_id currently reads and writes DataKey::BookingCounter from
instance storage (env.storage().instance()), which can expire and lead to
booking ID reuse; change it to use persistent storage
(env.storage().persistent()) when getting and setting BookingCounter so the
counter survives instance TTL and prevents ID collisions with persistent Booking
records. Locate get_next_booking_id and replace the instance().get/set calls for
DataKey::BookingCounter with persistent().get/set while preserving the logic
that increments and returns next. Ensure you reference DataKey::BookingCounter,
get_next_booking_id, and the existing Booking persistent storage semantics when
making the change.


// --- Bookings ---
pub fn save_booking(env: &Env, booking: &Booking) {
env.storage()
.persistent()
.set(&DataKey::Booking(booking.id), booking);
}

pub fn get_booking(env: &Env, booking_id: u64) -> Option<Booking> {
env.storage()
.persistent()
.get(&DataKey::Booking(booking_id))
}

pub fn update_booking_status(env: &Env, booking_id: u64, status: BookingStatus) {
if let Some(mut booking) = get_booking(env, booking_id) {
booking.status = status;
save_booking(env, &booking);
}
}
Loading