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
7 changes: 7 additions & 0 deletions apps/contracts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ contracts/
├── mutual-cancellation-contract/ # Cooperative transaction cancellation
├── staggered-payment-contract/ # Time-based payment scheduling
└── timelock-contract/ # Time-locked token deposits
└── hold_back_contract/ # Hold a portion of the payment temporarily
```

## Contract Descriptions
Expand Down Expand Up @@ -51,6 +52,10 @@ Implements time-based payment scheduling with verification steps, suitable for s
### Timelock Contract
Creates time-locked token deposits that cannot be withdrawn until a specified time has elapsed, with optional clawback mechanisms.

### Holdback COntract
This contract ensures that a portion of the payment is held back temporarily after the transaction is completed, serving as a guarantee to incentivize quality and reduce potential disputes. The holdback amount is released only after a predefined period or condition, such as buyer approval or the absence of disputes.


## Getting Started

### Prerequisites
Expand Down
20 changes: 19 additions & 1 deletion apps/contracts/TEST_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This document provides an overview of the test coverage for each smart contract
8. [Mutual Cancellation Contract](#mutual-cancellation-contract)
9. [Staggered Payment Contract](#staggered-payment-contract)
10. [Timelock Contract](#timelock-contract)
11. [Holdback Contract](#holdback-contract)

## Conditional Refund Contract

Expand Down Expand Up @@ -149,14 +150,30 @@ The timelock contract tests verify the functionality of time-locked token deposi
- Authorization checks
- Event emission

## Holdback Contract

The holdback contract tests verify the functionality of a holdback guarantee mechanism for secure marketplace transactions.

### Test Coverage

- Contract initialization
- Creating payments with holdback
- Buyer-approved holdback release
- Time-based holdback release
- Dispute initiation and resolution (refund and release scenarios)
- Handling invalid inputs (amount, holdback rate)
- Unauthorized access prevention
- Invalid state transitions
- Edge cases (buyer as seller/admin, non-existent transactions)

## Running the Tests

To run all tests for all contracts:

```bash
cargo test
```

To run tests for a specific contract:

```bash
Expand All @@ -183,3 +200,4 @@ cargo test -p conditional_refund_contract
| Mutual Cancellation | 0 | 0 | 0 | None |
| Staggered Payment | 6 | 6 | 0 | Medium |
| Timelock | 17 | 17 | 0 | High |
| Holdback | 15 | 15 | 0 | High
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ pub fn set_admin(env: &Env, admin: Address, new_admin: Address) -> Result<(), Co
Ok(())
}


/// Creates a new escrow agreement and immediately locks the buyer's funds.
pub fn create_escrow(
env: &Env,
Expand Down Expand Up @@ -90,7 +89,6 @@ pub fn confirm_receipt(env: &Env, buyer: Address, escrow_id: u64) -> Result<(),
Ok(())
}


/// Releases funds to the seller if the release time has passed OR the buyer has confirmed.
pub fn release_funds(env: &Env, escrow_id: u64) -> Result<(), ContractError> {
let mut escrow = storage::get_escrow(env, escrow_id)?;
Expand All @@ -99,7 +97,8 @@ pub fn release_funds(env: &Env, escrow_id: u64) -> Result<(), ContractError> {
return Err(ContractError::EscrowNotActive);
}

let can_release = env.ledger().timestamp() >= escrow.release_timestamp || escrow.buyer_confirmed;
let can_release =
env.ledger().timestamp() >= escrow.release_timestamp || escrow.buyer_confirmed;
if !can_release {
return Err(ContractError::ReleaseTimeNotPassed);
}
Expand Down
13 changes: 3 additions & 10 deletions apps/contracts/contracts/auto-release-escrow-contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
#![no_std]

mod error;
mod event;
mod escrow_logic;
mod event;
mod storage;
#[cfg(test)]
mod test;

use soroban_sdk::{contract, contractimpl, Address, Env, String};

use crate::{
error::ContractError,
storage::Escrow,
};
use crate::{error::ContractError, storage::Escrow};

#[contract]
pub struct AutoReleaseEscrowContract;
Expand Down Expand Up @@ -50,11 +47,7 @@ impl AutoReleaseEscrowContract {

/// Allows the buyer to confirm they have received the goods/service,
/// enabling an early release of funds.
pub fn confirm_receipt(
env: Env,
buyer: Address,
escrow_id: u64,
) -> Result<(), ContractError> {
pub fn confirm_receipt(env: Env, buyer: Address, escrow_id: u64) -> Result<(), ContractError> {
escrow_logic::confirm_receipt(&env, buyer, escrow_id)
}

Expand Down
98 changes: 74 additions & 24 deletions apps/contracts/contracts/auto-release-escrow-contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ fn create_token_contract<'a>(
env: &Env,
admin: &Address,
) -> (token::Client<'a>, TokenAdminClient<'a>) {
let token_address = env.register_stellar_asset_contract_v2(admin.clone()).address();
let token_address = env
.register_stellar_asset_contract_v2(admin.clone())
.address();
(
token::Client::new(env, &token_address),
TokenAdminClient::new(env, &token_address),
Expand Down Expand Up @@ -82,12 +84,18 @@ fn test_set_admin() {

// Verify the new admin can perform admin actions, like resolving a dispute
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &100, &test.token.address, &(test.env.ledger().timestamp() + 100)
&test.buyer,
&test.seller,
&100,
&test.token.address,
&(test.env.ledger().timestamp() + 100),
);
test.contract.dispute_escrow(&test.buyer, &escrow_id, &"reason".into_val(&test.env));
test.contract
.dispute_escrow(&test.buyer, &escrow_id, &"reason".into_val(&test.env));

// The new admin should now be able to resolve it
test.contract.resolve_dispute_and_refund(&new_admin, &escrow_id);
test.contract
.resolve_dispute_and_refund(&new_admin, &escrow_id);
let escrow = test.contract.get_escrow(&escrow_id);
assert_eq!(escrow.status, EscrowStatus::Refunded);
}
Expand All @@ -102,7 +110,6 @@ fn test_set_admin_unauthorized() {
assert_eq!(result, Err(Ok(ContractError::NotAdmin)));
}


#[test]
fn test_create_escrow_and_fund_locking() {
let test = EscrowTest::setup();
Expand Down Expand Up @@ -135,7 +142,11 @@ fn test_release_funds_after_time_elapses() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 10;
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);

// Advance time past the release timestamp
Expand All @@ -156,7 +167,11 @@ fn test_confirm_receipt_and_early_release() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 3600; // 1 hour
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);

// Buyer confirms receipt
Expand All @@ -178,19 +193,25 @@ fn test_dispute_and_admin_refund() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 3600;
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);

// Buyer disputes the escrow
let reason = String::from_str(&test.env, "Item not as described");
test.contract.dispute_escrow(&test.buyer, &escrow_id, &reason);
test.contract
.dispute_escrow(&test.buyer, &escrow_id, &reason);

let escrow_after_dispute = test.contract.get_escrow(&escrow_id);
assert_eq!(escrow_after_dispute.status, EscrowStatus::Disputed);
assert_eq!(escrow_after_dispute.dispute_reason, Some(reason));

// Admin resolves the dispute and refunds the buyer
test.contract.resolve_dispute_and_refund(&test.admin, &escrow_id);
test.contract
.resolve_dispute_and_refund(&test.admin, &escrow_id);

let escrow_after_refund = test.contract.get_escrow(&escrow_id);
assert_eq!(escrow_after_refund.status, EscrowStatus::Refunded);
Expand All @@ -205,7 +226,11 @@ fn test_release_fails_before_time() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 3600;
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);

let result = test.contract.try_release_funds(&escrow_id);
Expand All @@ -217,10 +242,15 @@ fn test_release_fails_if_disputed() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 3600;
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);
test.contract.dispute_escrow(&test.buyer, &escrow_id, &"reason".into_val(&test.env));

test.contract
.dispute_escrow(&test.buyer, &escrow_id, &"reason".into_val(&test.env));

let result = test.contract.try_release_funds(&escrow_id);
assert_eq!(result, Err(Ok(ContractError::EscrowNotActive)));
}
Expand All @@ -230,10 +260,16 @@ fn test_dispute_fails_if_not_buyer() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 3600;
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);

let result = test.contract.try_dispute_escrow(&test.seller, &escrow_id, &"reason".into_val(&test.env));
let result =
test.contract
.try_dispute_escrow(&test.seller, &escrow_id, &"reason".into_val(&test.env));
assert_eq!(result, Err(Ok(ContractError::NotBuyer)));
}

Expand All @@ -242,11 +278,18 @@ fn test_dispute_fails_if_already_disputed() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 3600;
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);
test.contract.dispute_escrow(&test.buyer, &escrow_id, &"reason1".into_val(&test.env));

let result = test.contract.try_dispute_escrow(&test.buyer, &escrow_id, &"reason2".into_val(&test.env));
test.contract
.dispute_escrow(&test.buyer, &escrow_id, &"reason1".into_val(&test.env));

let result =
test.contract
.try_dispute_escrow(&test.buyer, &escrow_id, &"reason2".into_val(&test.env));
assert_eq!(result, Err(Ok(ContractError::EscrowAlreadyDisputed)));
}

Expand All @@ -255,11 +298,18 @@ fn test_resolve_dispute_fails_if_not_admin() {
let test = EscrowTest::setup();
let release_timestamp = test.env.ledger().timestamp() + 3600;
let escrow_id = test.contract.create_escrow(
&test.buyer, &test.seller, &1000, &test.token.address, &release_timestamp
&test.buyer,
&test.seller,
&1000,
&test.token.address,
&release_timestamp,
);
test.contract.dispute_escrow(&test.buyer, &escrow_id, &"reason".into_val(&test.env));
test.contract
.dispute_escrow(&test.buyer, &escrow_id, &"reason".into_val(&test.env));

// Seller (not admin) tries to resolve
let result = test.contract.try_resolve_dispute_and_refund(&test.seller, &escrow_id);
let result = test
.contract
.try_resolve_dispute_and_refund(&test.seller, &escrow_id);
assert_eq!(result, Err(Ok(ContractError::NotAdmin)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub fn place_bid(
if bid_amount < auction.highest_bid + auction.min_bid_increment {
return Err(ContractError::BidTooLow);
}

// If there was a previous bidder, refund their bid.
if let Some(previous_bidder) = auction.highest_bidder {
let token_client = token::Client::new(env, &auction.payment_token);
Expand Down Expand Up @@ -139,7 +139,7 @@ pub fn cancel_auction(env: &Env, seller: Address, auction_id: u64) -> Result<(),
if auction.status == AuctionStatus::Active {
return Err(ContractError::AuctionHasBids);
}

if auction.status == AuctionStatus::Closed {
return Err(ContractError::AuctionHasEnded);
}
Expand Down
11 changes: 2 additions & 9 deletions apps/contracts/contracts/automated-auction-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ mod test;

use soroban_sdk::{contract, contractimpl, Address, Env, String};

use crate::{
error::ContractError,
storage::Auction,
};
use crate::{error::ContractError, storage::Auction};

#[contract]
pub struct AutomatedAuctionContract;
Expand Down Expand Up @@ -56,11 +53,7 @@ impl AutomatedAuctionContract {
}

/// Allows the seller to cancel an auction before any bids have been placed.
pub fn cancel_auction(
env: Env,
seller: Address,
auction_id: u64,
) -> Result<(), ContractError> {
pub fn cancel_auction(env: Env, seller: Address, auction_id: u64) -> Result<(), ContractError> {
auction_logic::cancel_auction(&env, seller, auction_id)
}

Expand Down
Loading
Loading