diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index 2d6528e..f04f749 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -712,6 +712,13 @@ dependencies = [ "digest", ] +[[package]] +name = "hold_back_contract" +version = "0.1.0" +dependencies = [ + "soroban-sdk 22.0.8", +] + [[package]] name = "iana-time-zone" version = "0.1.63" diff --git a/apps/contracts/README.md b/apps/contracts/README.md index 49e48dc..fa12617 100644 --- a/apps/contracts/README.md +++ b/apps/contracts/README.md @@ -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 @@ -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 diff --git a/apps/contracts/TEST_DOCUMENTATION.md b/apps/contracts/TEST_DOCUMENTATION.md index ee7667b..ede85f1 100644 --- a/apps/contracts/TEST_DOCUMENTATION.md +++ b/apps/contracts/TEST_DOCUMENTATION.md @@ -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 @@ -149,6 +150,22 @@ 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: @@ -156,7 +173,7 @@ To run all tests for all contracts: ```bash cargo test ``` - + To run tests for a specific contract: ```bash @@ -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 diff --git a/apps/contracts/contracts/auto-release-escrow-contract/src/escrow_logic.rs b/apps/contracts/contracts/auto-release-escrow-contract/src/escrow_logic.rs index d62951e..cb83b24 100644 --- a/apps/contracts/contracts/auto-release-escrow-contract/src/escrow_logic.rs +++ b/apps/contracts/contracts/auto-release-escrow-contract/src/escrow_logic.rs @@ -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, @@ -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)?; @@ -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); } diff --git a/apps/contracts/contracts/auto-release-escrow-contract/src/lib.rs b/apps/contracts/contracts/auto-release-escrow-contract/src/lib.rs index caa6e30..1c59ae4 100644 --- a/apps/contracts/contracts/auto-release-escrow-contract/src/lib.rs +++ b/apps/contracts/contracts/auto-release-escrow-contract/src/lib.rs @@ -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; @@ -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) } diff --git a/apps/contracts/contracts/auto-release-escrow-contract/src/test.rs b/apps/contracts/contracts/auto-release-escrow-contract/src/test.rs index f58e2c0..db852b0 100644 --- a/apps/contracts/contracts/auto-release-escrow-contract/src/test.rs +++ b/apps/contracts/contracts/auto-release-escrow-contract/src/test.rs @@ -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), @@ -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); } @@ -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(); @@ -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 @@ -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 @@ -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); @@ -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); @@ -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))); } @@ -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))); } @@ -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))); } @@ -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))); } diff --git a/apps/contracts/contracts/automated-auction-contract/src/auction_logic.rs b/apps/contracts/contracts/automated-auction-contract/src/auction_logic.rs index 626872a..783b0c0 100644 --- a/apps/contracts/contracts/automated-auction-contract/src/auction_logic.rs +++ b/apps/contracts/contracts/automated-auction-contract/src/auction_logic.rs @@ -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); @@ -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); } diff --git a/apps/contracts/contracts/automated-auction-contract/src/lib.rs b/apps/contracts/contracts/automated-auction-contract/src/lib.rs index 8dfdf38..094039a 100644 --- a/apps/contracts/contracts/automated-auction-contract/src/lib.rs +++ b/apps/contracts/contracts/automated-auction-contract/src/lib.rs @@ -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; @@ -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) } diff --git a/apps/contracts/contracts/automated-auction-contract/src/test.rs b/apps/contracts/contracts/automated-auction-contract/src/test.rs index c4ce592..cc45714 100644 --- a/apps/contracts/contracts/automated-auction-contract/src/test.rs +++ b/apps/contracts/contracts/automated-auction-contract/src/test.rs @@ -15,7 +15,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), @@ -90,7 +92,12 @@ fn test_create_auction() { fn test_place_bid_success() { let test = AuctionTest::setup(); let auction_id = test.contract.create_auction( - &test.seller, &"Item".into_val(&test.env), &100, &10, &3600, &test.token.address + &test.seller, + &"Item".into_val(&test.env), + &100, + &10, + &3600, + &test.token.address, ); test.contract.place_bid(&test.bidder1, &auction_id, &110); @@ -109,7 +116,12 @@ fn test_place_bid_success() { fn test_outbid_and_refund() { let test = AuctionTest::setup(); let auction_id = test.contract.create_auction( - &test.seller, &"Item".into_val(&test.env), &100, &10, &3600, &test.token.address + &test.seller, + &"Item".into_val(&test.env), + &100, + &10, + &3600, + &test.token.address, ); // Bidder 1 places a bid @@ -134,25 +146,36 @@ fn test_outbid_and_refund() { fn test_place_bid_errors() { let test = AuctionTest::setup(); let auction_id = test.contract.create_auction( - &test.seller, &"Item".into_val(&test.env), &100, &10, &10, &test.token.address + &test.seller, + &"Item".into_val(&test.env), + &100, + &10, + &10, + &test.token.address, ); // Bid too low (not meeting minimum increment) - let result_low = test.contract.try_place_bid(&test.bidder1, &auction_id, &105); + let result_low = test + .contract + .try_place_bid(&test.bidder1, &auction_id, &105); assert_eq!(result_low, Err(Ok(ContractError::BidTooLow))); // Bid exactly at minimum increment (should succeed) test.contract.place_bid(&test.bidder1, &auction_id, &110); // Bid too low (equal to current highest) - let result_equal = test.contract.try_place_bid(&test.bidder2, &auction_id, &110); + let result_equal = test + .contract + .try_place_bid(&test.bidder2, &auction_id, &110); assert_eq!(result_equal, Err(Ok(ContractError::BidTooLow))); // Expire the auction test.env.ledger().with_mut(|l| l.timestamp = 20); - + // Bid after auction ended - let result_ended = test.contract.try_place_bid(&test.bidder2, &auction_id, &120); + let result_ended = test + .contract + .try_place_bid(&test.bidder2, &auction_id, &120); assert_eq!(result_ended, Err(Ok(ContractError::AuctionHasEnded))); } @@ -160,7 +183,12 @@ fn test_place_bid_errors() { fn test_close_auction_with_winner() { let test = AuctionTest::setup(); let auction_id = test.contract.create_auction( - &test.seller, &"Item".into_val(&test.env), &100, &10, &10, &test.token.address + &test.seller, + &"Item".into_val(&test.env), + &100, + &10, + &10, + &test.token.address, ); test.contract.place_bid(&test.bidder1, &auction_id, &150); @@ -182,7 +210,12 @@ fn test_close_auction_with_winner() { fn test_close_auction_no_bids() { let test = AuctionTest::setup(); let auction_id = test.contract.create_auction( - &test.seller, &"Item".into_val(&test.env), &100, &10, &10, &test.token.address + &test.seller, + &"Item".into_val(&test.env), + &100, + &10, + &10, + &test.token.address, ); // Expire the auction @@ -203,7 +236,12 @@ fn test_close_auction_no_bids() { fn test_close_auction_not_ended() { let test = AuctionTest::setup(); let auction_id = test.contract.create_auction( - &test.seller, &"Item".into_val(&test.env), &100, &10, &3600, &test.token.address + &test.seller, + &"Item".into_val(&test.env), + &100, + &10, + &3600, + &test.token.address, ); let result = test.contract.try_close_auction(&auction_id); @@ -214,7 +252,12 @@ fn test_close_auction_not_ended() { fn test_cancel_auction() { let test = AuctionTest::setup(); let auction_id = test.contract.create_auction( - &test.seller, &"Item".into_val(&test.env), &100, &10, &3600, &test.token.address + &test.seller, + &"Item".into_val(&test.env), + &100, + &10, + &3600, + &test.token.address, ); // Seller successfully cancels @@ -224,13 +267,22 @@ fn test_cancel_auction() { // Non-seller tries to cancel let auction_id_2 = test.contract.create_auction( - &test.seller, &"Item 2".into_val(&test.env), &100, &10, &3600, &test.token.address + &test.seller, + &"Item 2".into_val(&test.env), + &100, + &10, + &3600, + &test.token.address, ); - let result_auth = test.contract.try_cancel_auction(&test.bidder1, &auction_id_2); + let result_auth = test + .contract + .try_cancel_auction(&test.bidder1, &auction_id_2); assert_eq!(result_auth, Err(Ok(ContractError::NotAuctionSeller))); // Seller tries to cancel after a bid test.contract.place_bid(&test.bidder1, &auction_id_2, &110); - let result_bids = test.contract.try_cancel_auction(&test.seller, &auction_id_2); + let result_bids = test + .contract + .try_cancel_auction(&test.seller, &auction_id_2); assert_eq!(result_bids, Err(Ok(ContractError::AuctionHasBids))); } diff --git a/apps/contracts/contracts/automatic-marketplace-fee-deduction/src/test.rs b/apps/contracts/contracts/automatic-marketplace-fee-deduction/src/test.rs index 57d7036..192fdfe 100644 --- a/apps/contracts/contracts/automatic-marketplace-fee-deduction/src/test.rs +++ b/apps/contracts/contracts/automatic-marketplace-fee-deduction/src/test.rs @@ -43,7 +43,7 @@ impl TestContext { } } - fn get_client(&self) -> MarketplaceFeeContractClient { + fn get_client(&self) -> MarketplaceFeeContractClient<'static> { MarketplaceFeeContractClient::new(&self.env, &self.contract_id) } diff --git a/apps/contracts/contracts/conditional-refund-contract/Cargo.toml b/apps/contracts/contracts/conditional-refund-contract/Cargo.toml index 80adea3..4e5767b 100644 --- a/apps/contracts/contracts/conditional-refund-contract/Cargo.toml +++ b/apps/contracts/contracts/conditional-refund-contract/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "conditional-refund-contract" version = "0.1.0" -edition = "2024" +edition = "2021" [lib] crate-type = ["cdylib"] diff --git a/apps/contracts/contracts/conditional-refund-contract/src/contract.rs b/apps/contracts/contracts/conditional-refund-contract/src/contract.rs index e6e6332..264de1c 100644 --- a/apps/contracts/contracts/conditional-refund-contract/src/contract.rs +++ b/apps/contracts/contracts/conditional-refund-contract/src/contract.rs @@ -2,7 +2,7 @@ use crate::error::ContractError; use crate::events::*; use crate::refund_storage::*; use crate::storage; -use soroban_sdk::{Address, Env, String, Vec, token}; +use soroban_sdk::{token, Address, Env, String, Vec}; pub fn create_refund_contract( env: &Env, diff --git a/apps/contracts/contracts/conditional-refund-contract/src/events.rs b/apps/contracts/contracts/conditional-refund-contract/src/events.rs index ff03f7d..11fef47 100644 --- a/apps/contracts/contracts/conditional-refund-contract/src/events.rs +++ b/apps/contracts/contracts/conditional-refund-contract/src/events.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, contracttype}; +use soroban_sdk::{contracttype, Address, Env, String}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/apps/contracts/contracts/conditional-refund-contract/src/lib.rs b/apps/contracts/contracts/conditional-refund-contract/src/lib.rs index 53c8afd..4464b09 100644 --- a/apps/contracts/contracts/conditional-refund-contract/src/lib.rs +++ b/apps/contracts/contracts/conditional-refund-contract/src/lib.rs @@ -7,7 +7,7 @@ mod refund_storage; mod storage; mod test; -use soroban_sdk::{Address, Env, String, contract, contractimpl}; +use soroban_sdk::{contract, contractimpl, Address, Env, String}; pub use contract::*; pub use error::*; diff --git a/apps/contracts/contracts/conditional-refund-contract/src/refund_storage.rs b/apps/contracts/contracts/conditional-refund-contract/src/refund_storage.rs index 481f278..8c2748e 100644 --- a/apps/contracts/contracts/conditional-refund-contract/src/refund_storage.rs +++ b/apps/contracts/contracts/conditional-refund-contract/src/refund_storage.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, Symbol, Vec, contracttype, symbol_short}; +use soroban_sdk::{contracttype, symbol_short, Address, Env, String, Symbol, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/apps/contracts/contracts/conditional-refund-contract/src/test.rs b/apps/contracts/contracts/conditional-refund-contract/src/test.rs index 31f7660..ec4aacb 100644 --- a/apps/contracts/contracts/conditional-refund-contract/src/test.rs +++ b/apps/contracts/contracts/conditional-refund-contract/src/test.rs @@ -4,7 +4,7 @@ extern crate std; use crate::error::ContractError; use crate::refund_storage::ContractStatus; use crate::{ConditionalRefundContract, ConditionalRefundContractClient}; -use soroban_sdk::{Address, Env, String, testutils::Address as _, testutils::Ledger, token}; +use soroban_sdk::{testutils::Address as _, testutils::Ledger, token, Address, Env, String}; use token::Client as TokenClient; use token::StellarAssetClient as TokenAdminClient; diff --git a/apps/contracts/contracts/escrow-arbitration-contract/Cargo.toml b/apps/contracts/contracts/escrow-arbitration-contract/Cargo.toml index 093cf1a..a77471a 100644 --- a/apps/contracts/contracts/escrow-arbitration-contract/Cargo.toml +++ b/apps/contracts/contracts/escrow-arbitration-contract/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "escrow_arbitration_contract" version = "0.1.0" -edition = "2024" +edition = "2021" [lib] crate-type = ["cdylib"] diff --git a/apps/contracts/contracts/escrow-arbitration-contract/src/contract.rs b/apps/contracts/contracts/escrow-arbitration-contract/src/contract.rs index 9b9b378..4526a8d 100644 --- a/apps/contracts/contracts/escrow-arbitration-contract/src/contract.rs +++ b/apps/contracts/contracts/escrow-arbitration-contract/src/contract.rs @@ -2,7 +2,7 @@ use crate::error::ContractError; use crate::escrow_storage; use crate::escrow_storage::*; use crate::events::*; -use soroban_sdk::{Address, Env, String, Vec, token}; +use soroban_sdk::{token, Address, Env, String, Vec}; pub fn create_escrow( env: &Env, diff --git a/apps/contracts/contracts/escrow-arbitration-contract/src/escrow_storage.rs b/apps/contracts/contracts/escrow-arbitration-contract/src/escrow_storage.rs index 839b1f1..e3bacca 100644 --- a/apps/contracts/contracts/escrow-arbitration-contract/src/escrow_storage.rs +++ b/apps/contracts/contracts/escrow-arbitration-contract/src/escrow_storage.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, Symbol, Vec, contracttype, symbol_short}; +use soroban_sdk::{contracttype, symbol_short, Address, Env, String, Symbol, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/apps/contracts/contracts/escrow-arbitration-contract/src/events.rs b/apps/contracts/contracts/escrow-arbitration-contract/src/events.rs index 4fcf25e..a44f115 100644 --- a/apps/contracts/contracts/escrow-arbitration-contract/src/events.rs +++ b/apps/contracts/contracts/escrow-arbitration-contract/src/events.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, contracttype}; +use soroban_sdk::{contracttype, Address, Env, String}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/apps/contracts/contracts/escrow-arbitration-contract/src/lib.rs b/apps/contracts/contracts/escrow-arbitration-contract/src/lib.rs index 054ab32..04f7a61 100644 --- a/apps/contracts/contracts/escrow-arbitration-contract/src/lib.rs +++ b/apps/contracts/contracts/escrow-arbitration-contract/src/lib.rs @@ -7,7 +7,7 @@ mod events; mod storage; mod test; -use soroban_sdk::{Address, Env, String, contract, contractimpl}; +use soroban_sdk::{contract, contractimpl, Address, Env, String}; pub use contract::*; pub use error::*; diff --git a/apps/contracts/contracts/escrow-arbitration-contract/src/test.rs b/apps/contracts/contracts/escrow-arbitration-contract/src/test.rs index 974daf9..4e20e9d 100644 --- a/apps/contracts/contracts/escrow-arbitration-contract/src/test.rs +++ b/apps/contracts/contracts/escrow-arbitration-contract/src/test.rs @@ -4,7 +4,7 @@ extern crate std; use crate::error::ContractError; use crate::escrow_storage::EscrowStatus; use crate::{EscrowArbitrationContract, EscrowArbitrationContractClient}; -use soroban_sdk::{Address, Env, String, testutils::Address as _, token}; +use soroban_sdk::{testutils::Address as _, token, Address, Env, String}; use token::Client as TokenClient; use token::StellarAssetClient as TokenAdminClient; diff --git a/apps/contracts/contracts/hold_back_contract/Cargo.toml b/apps/contracts/contracts/hold_back_contract/Cargo.toml new file mode 100644 index 0000000..46cd5d8 --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "hold_back_contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[features] +testutils = ["soroban-sdk/testutils"] \ No newline at end of file diff --git a/apps/contracts/contracts/hold_back_contract/Makefile b/apps/contracts/contracts/hold_back_contract/Makefile new file mode 100644 index 0000000..7788f03 --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/Makefile @@ -0,0 +1,19 @@ +-include .env + + +format: + @echo "formatting the project" + @cargo fmt + +build: + @echo "building the project" + @stellar contract build + +test: + @echo "testing the project tests" + @cargo test + + +clippy: + @echo "clip" + @cargo clippy \ No newline at end of file diff --git a/apps/contracts/contracts/hold_back_contract/README.md b/apps/contracts/contracts/hold_back_contract/README.md new file mode 100644 index 0000000..ab53479 --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/README.md @@ -0,0 +1,138 @@ +HoldBackContract +A Soroban smart contract for a Stellar-based marketplace implementing a holdback guarantee to ensure quality and manage disputes. +🌟 Features +💰 Holdback Payment System + +Configurable Holdback: Set percentage of payment held in escrow +Flexible Release: Time-based or buyer-approved release +Secure Escrow: Funds locked until conditions are met + +🤝 Buyer-Seller Protection + +Clear Terms: Transparent holdback rate and release period +Dispute Resolution: Admin-mediated refunds or releases +Mutual Safeguards: Protects both parties from non-compliance + +🔄 Transaction Processing + +Payment Tracking: Monitor transaction status and holdback +Automatic Release: Time-based release after deadline +Event Emission: Transparent logging of all actions + +🛡️ Risk Management + +Dispute Handling: Buyer-initiated disputes with admin resolution +Cancellation: Refund option for unresolved disputes +Deadline Enforcement: Strict holdback release timing + +🏗️ Architecture +File Structure +src/ +├── lib.rs # Main contract interface +├── error.rs # Error definitions +├── storage.rs # Data storage utilities +└── test.rs # Comprehensive test suite + +🚀 Getting Started +Prerequisites + +Rust 1.70+ +Soroban CLI +Stellar account with testnet tokens + +Building the Contract +# Build the contract +cargo build --target wasm32-unknown-unknown --release + +# Build with Soroban CLI +soroban contract build + +Testing +# Run the test suite +cargo test + +📖 Usage +1. Initialize Contract +contract.initialize(env, admin_address) + +2. Create Payment +let transaction_id = contract.create_payment( + env, + buyer_address, + seller_address, + token_address, + amount, + holdback_rate, + holdback_days, +); + +3. Approve Release (Buyer) +contract.approve_release(env, transaction_id, buyer_address); + +4. Initiate Dispute (Buyer) +contract.initiate_dispute(env, transaction_id, buyer_address); + +5. Resolve Dispute (Admin) +contract.resolve_dispute(env, transaction_id, refund, admin_address); + +6. Check and Release +contract.check_and_release(env, transaction_id); + +🔄 Contract Workflow + +Payment Creation: Buyer initiates payment with holdback terms +Holdback Period: Funds held until release conditions met +Release Options: Buyer approves early release or time-based release +Dispute Process: Buyer initiates dispute; admin resolves with refund or release +Completion: Transaction finalized or cancelled + +📊 Contract States + + + +Status +Description +Available Actions + + + +Held +Payment created, holdback in escrow +Approve, Dispute, Check/Release + + +HoldbackPending +Buyer approved release +Check/Release + + +Disputed +Dispute initiated +Resolve (Admin) + + +Completed +Holdback released +View Only + + +Cancelled +Funds refunded +View Only + + +🛡️ Security Features + +Authorization: require_auth() for all sensitive actions +State Validation: Strict state machine for valid transitions +Fund Protection: Escrow ensures secure holdback management +Event Logging: Transparent events for all major actions + +📚 Test Coverage +Tests cover: + +Contract initialization +Payment creation with holdback +Buyer-approved and time-based releases +Dispute initiation and resolution +Edge cases (invalid inputs, unauthorized actions, non-existent transactions) diff --git a/apps/contracts/contracts/hold_back_contract/src/entities.rs b/apps/contracts/contracts/hold_back_contract/src/entities.rs new file mode 100644 index 0000000..422cdb9 --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/src/entities.rs @@ -0,0 +1,34 @@ +use soroban_sdk::{contracttype, Address}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TransactionStatus { + Held, + HoldbackPending, + Completed, + Cancelled, + Disputed, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Transaction { + pub buyer: Address, + pub seller: Address, + pub amount: u128, + pub token: Address, + pub holdback_rate: u32, + pub holdback_amount: u128, + pub final_amount: u128, + pub release_time: u64, + pub status: TransactionStatus, +} + +#[contracttype] +#[derive(Debug, Eq, PartialEq)] +pub enum DataKey { + Transaction(u128), + TransactionCounter, + Token, + Admin, +} diff --git a/apps/contracts/contracts/hold_back_contract/src/errors.rs b/apps/contracts/contracts/hold_back_contract/src/errors.rs new file mode 100644 index 0000000..24f7465 --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/src/errors.rs @@ -0,0 +1,15 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub enum Error { + NotInitialized = 1, + InvalidAmount = 2, + InvalidHoldbackRate = 3, + InvalidBuyer = 4, + InvalidSeller = 5, + TransactionNotFound = 6, + InvalidStatus = 7, + Unauthorized = 8, + AlreadyInitialized = 9, +} diff --git a/apps/contracts/contracts/hold_back_contract/src/hold_back_contract.rs b/apps/contracts/contracts/hold_back_contract/src/hold_back_contract.rs new file mode 100644 index 0000000..e6ae1c9 --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/src/hold_back_contract.rs @@ -0,0 +1,280 @@ +use crate::entities::*; +use crate::errors::*; +use soroban_sdk::{contract, contractimpl, log, token, Address, Env}; + +pub const DAY_IN_SECONDS: u64 = 86400; + +#[contract] +pub struct HoldBackContract; + +#[contractimpl] +impl HoldBackContract { + pub fn initialize(env: Env, admin: Address) -> Result { + if env.storage().persistent().has(&DataKey::Admin) { + return Err(Error::AlreadyInitialized); + } + env.storage().persistent().set(&DataKey::Admin, &admin); + Ok(true) + } + + pub fn create_payment( + env: Env, + buyer: Address, + seller: Address, + amount: u128, + token: Address, + holdback_rate: u32, + holdback_days: u32, + ) -> Result { + buyer.require_auth(); + let admin: Address = env + .storage() + .persistent() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + + if amount == 0 { + return Err(Error::InvalidAmount); + } + if holdback_rate == 0 || holdback_rate > 100 { + return Err(Error::InvalidHoldbackRate); + } + if buyer == seller || buyer == admin || seller == admin { + return Err(Error::InvalidBuyer); + } + if buyer == token || seller == token { + return Err(Error::InvalidSeller); + } + + let holdback_amount = (amount * holdback_rate as u128) / 100; + let final_amount = amount + .checked_sub(holdback_amount) + .ok_or(Error::InvalidAmount)?; + + let token_client = token::Client::new(&env, &token); + token_client.transfer(&buyer, &env.current_contract_address(), &(amount as i128)); + + if final_amount > 0 { + token_client.transfer( + &env.current_contract_address(), + &seller, + &(final_amount as i128), + ); + } + + let transaction_id = env + .storage() + .persistent() + .get(&DataKey::TransactionCounter) + .unwrap_or(0u128) + .checked_add(1) + .ok_or(Error::InvalidAmount)?; + env.storage() + .persistent() + .set(&DataKey::TransactionCounter, &transaction_id); + + let transaction = Transaction { + buyer: buyer.clone(), + seller: seller.clone(), + amount, + token, + holdback_rate, + holdback_amount, + final_amount, + release_time: env.ledger().timestamp() + (holdback_days as u64 * DAY_IN_SECONDS), + status: TransactionStatus::Held, + }; + env.storage() + .persistent() + .set(&DataKey::Transaction(transaction_id), &transaction); + + env.events().publish( + ("transaction_created",), + (transaction_id, buyer, seller, amount, holdback_amount), + ); + + log!( + &env, + "Transaction {} created with holdback {}%", + transaction_id, + holdback_rate + ); + Ok(transaction_id) + } + + pub fn approve_release(env: Env, transaction_id: u128, buyer: Address) -> Result<(), Error> { + buyer.require_auth(); + let mut transaction: Transaction = env + .storage() + .persistent() + .get(&DataKey::Transaction(transaction_id)) + .ok_or(Error::TransactionNotFound)?; + if transaction.buyer != buyer { + return Err(Error::Unauthorized); + } + if transaction.status != TransactionStatus::Held { + return Err(Error::InvalidStatus); + } + + transaction.status = TransactionStatus::HoldbackPending; + env.storage() + .persistent() + .set(&DataKey::Transaction(transaction_id), &transaction); + + Self::release_holdback_if_due(&env, transaction_id)?; + Ok(()) + } + + pub fn initiate_dispute(env: Env, transaction_id: u128, buyer: Address) -> Result<(), Error> { + buyer.require_auth(); + let mut transaction: Transaction = env + .storage() + .persistent() + .get(&DataKey::Transaction(transaction_id)) + .ok_or(Error::TransactionNotFound)?; + if transaction.buyer != buyer { + return Err(Error::Unauthorized); + } + if transaction.status != TransactionStatus::Held + && transaction.status != TransactionStatus::HoldbackPending + { + return Err(Error::InvalidStatus); + } + + transaction.status = TransactionStatus::Disputed; + env.storage() + .persistent() + .set(&DataKey::Transaction(transaction_id), &transaction); + + env.events() + .publish(("dispute_initiated",), (transaction_id, buyer)); + Ok(()) + } + + pub fn resolve_dispute( + env: Env, + transaction_id: u128, + refund: bool, + admin: Address, + ) -> Result<(), Error> { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .persistent() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + let mut transaction: Transaction = env + .storage() + .persistent() + .get(&DataKey::Transaction(transaction_id)) + .ok_or(Error::TransactionNotFound)?; + if transaction.status != TransactionStatus::Disputed { + return Err(Error::InvalidStatus); + } + + let token_client = token::Client::new(&env, &transaction.token); + if refund { + token_client.transfer( + &env.current_contract_address(), + &transaction.buyer, + &(transaction.holdback_amount as i128), + ); + transaction.status = TransactionStatus::Cancelled; + env.events().publish( + ("holdback_refunded",), + ( + transaction_id, + transaction.buyer.clone(), + transaction.holdback_amount, + ), + ); + } else { + token_client.transfer( + &env.current_contract_address(), + &transaction.seller, + &(transaction.holdback_amount as i128), + ); + transaction.status = TransactionStatus::Completed; + env.events().publish( + ("holdback_released",), + ( + transaction_id, + transaction.seller.clone(), + transaction.holdback_amount, + ), + ); + } + env.storage() + .persistent() + .set(&DataKey::Transaction(transaction_id), &transaction); + Ok(()) + } + + pub fn check_and_release(env: Env, transaction_id: u128) -> Result<(), Error> { + let transaction: Transaction = env + .storage() + .persistent() + .get(&DataKey::Transaction(transaction_id)) + .ok_or(Error::TransactionNotFound)?; + if transaction.status != TransactionStatus::Held + && transaction.status != TransactionStatus::HoldbackPending + { + return Err(Error::InvalidStatus); + } + + Self::release_holdback_if_due(&env, transaction_id)?; + Ok(()) + } + + fn release_holdback_if_due(env: &Env, transaction_id: u128) -> Result<(), Error> { + let mut transaction: Transaction = env + .storage() + .persistent() + .get(&DataKey::Transaction(transaction_id)) + .ok_or(Error::TransactionNotFound)?; + + if transaction.status == TransactionStatus::HoldbackPending + || (transaction.status == TransactionStatus::Held + && env.ledger().timestamp() >= transaction.release_time) + { + let token_client = token::Client::new(&env, &transaction.token); + token_client.transfer( + &env.current_contract_address(), + &transaction.seller, + &(transaction.holdback_amount as i128), + ); + transaction.status = TransactionStatus::Completed; + env.storage() + .persistent() + .set(&DataKey::Transaction(transaction_id), &transaction); + + env.events().publish( + ("holdback_released",), + ( + transaction_id, + transaction.seller, + transaction.holdback_amount, + ), + ); + } + Ok(()) + } + + pub fn get_transaction(env: Env, transaction_id: u128) -> Result { + env.storage() + .persistent() + .get(&DataKey::Transaction(transaction_id)) + .ok_or(Error::TransactionNotFound) + } + + pub fn get_admin(env: Env) -> Result { + env.storage() + .persistent() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized) + } +} diff --git a/apps/contracts/contracts/hold_back_contract/src/lib.rs b/apps/contracts/contracts/hold_back_contract/src/lib.rs new file mode 100644 index 0000000..2701c26 --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] + +mod entities; +mod errors; +mod hold_back_contract; +mod tests; diff --git a/apps/contracts/contracts/hold_back_contract/src/tests.rs b/apps/contracts/contracts/hold_back_contract/src/tests.rs new file mode 100644 index 0000000..fb4066e --- /dev/null +++ b/apps/contracts/contracts/hold_back_contract/src/tests.rs @@ -0,0 +1,265 @@ +#[cfg(test)] +mod test { + + use crate::{entities::*, hold_back_contract::*}; + + use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token::{self, StellarAssetClient}, + Address, Env, + }; + + fn create_variables() -> (Env, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + // register the contract + let contract_address = env.register(HoldBackContract, {}); + let mocked_address = Address::generate(&env); + + (env, contract_address, mocked_address) + } + + fn create_token(env: &Env, admin: &Address) -> (Address, StellarAssetClient<'static>) { + let client = env.register_stellar_asset_contract_v2(admin.clone()); + ( + client.address(), + token::StellarAssetClient::new(&env, &client.address()), + ) + } + + fn create_variables_and_initialize_contract() -> (Env, HoldBackContractClient<'static>, Address) + { + let (env, contract_address, mocked_address) = create_variables(); + + let contract_instance = HoldBackContractClient::new(&env, &contract_address); + + contract_instance.initialize(&mocked_address); + + (env, contract_instance, mocked_address) + } + + fn generate_addresses(env: &Env) -> (Address, Address, Address, Address) { + ( + Address::generate(env), + Address::generate(env), + Address::generate(env), + Address::generate(env), + ) + } + //"initialize the contract" + #[test] + fn test_initialize_contract() { + let (env, contract_address, mocked_address) = create_variables(); + + let contract_instance = HoldBackContractClient::new(&env, &contract_address); + + assert_eq!(contract_instance.initialize(&mocked_address), true, "error"); + + assert_eq!(contract_instance.get_admin(), mocked_address); + } + + #[test] + fn test_create_payment() { + let (env, contract_instance, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + + let transaction_id = + contract_instance.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + let transaction = contract_instance.get_transaction(&transaction_id); + + assert_eq!(transaction.buyer, buyer); + assert_eq!(transaction.seller, seller); + assert_eq!(transaction.amount, 1000); + assert_eq!(transaction.holdback_rate, 20); + assert_eq!(transaction.holdback_amount, 200); + assert_eq!(transaction.final_amount, 800); + assert_eq!(transaction.status, TransactionStatus::Held); + assert_eq!(transaction.token, token_address); + } + + #[test] + fn test_approve_release() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + + let transaction_id = + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + + contract.approve_release(&transaction_id, &buyer); + let transaction = contract.get_transaction(&transaction_id); + assert_eq!(transaction.status, TransactionStatus::Completed); + } + + #[test] + fn test_time_based_release() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + let transaction_id = + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &1); + + env.ledger().set_timestamp(DAY_IN_SECONDS + 1); + + contract.check_and_release(&transaction_id); + let transaction = contract.get_transaction(&transaction_id); + assert_eq!(transaction.status, TransactionStatus::Completed); + } + + #[test] + fn test_dispute_and_refunded() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + + let transaction_id = + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + + contract.initiate_dispute(&transaction_id, &buyer); + contract.resolve_dispute(&transaction_id, &true, &admin); + + let transaction = contract.get_transaction(&transaction_id); + assert_eq!(transaction.status, TransactionStatus::Cancelled); + } + + #[test] + #[should_panic] + fn test_invalid_transaction() { + let (_, contract, _) = create_variables_and_initialize_contract(); + + contract.get_transaction(&999); + } + + #[test] + #[should_panic] + fn test_unauthorized_approve() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + let transaction_id = + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + + contract.approve_release(&transaction_id, &seller); + } + + #[test] + #[should_panic] + fn test_invalid_amount() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, _) = create_token(&env, &admin); + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + } + #[test] + #[should_panic] + fn test_invalid_holdback_rate() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + contract.create_payment(&buyer, &seller, &1000, &token_address, &0, &7); + } + + #[test] + #[should_panic] + fn test_unauthorized_dispute() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + let transaction_id = + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + + contract.initiate_dispute(&transaction_id, &seller); + } + + #[test] + #[should_panic] + fn test_unauthorized_resolve_dispute() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + let transaction_id = + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + + contract.initiate_dispute(&transaction_id, &buyer); + contract.resolve_dispute(&transaction_id, &true, &buyer); + } + + #[test] + #[should_panic] + fn test_approve_invalid_status() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + let transaction_id = + contract.create_payment(&buyer, &seller, &1000, &token_address, &20, &7); + + contract.initiate_dispute(&transaction_id, &buyer); + contract.approve_release(&transaction_id, &buyer); + } + + #[test] + #[should_panic] + fn test_buyer_is_seller() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, _, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + contract.create_payment(&buyer, &buyer, &1000, &token_address, &20, &7); + } + + #[test] + #[should_panic] + fn test_buyer_is_admin() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, seller, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + contract.create_payment(&admin, &seller, &1000, &token_address, &20, &7); + } + + #[test] + #[should_panic] + fn test_seller_is_token() { + let (env, contract, admin) = create_variables_and_initialize_contract(); + + let (buyer, _, _, _) = generate_addresses(&env); + + let (token_address, token_client) = create_token(&env, &admin); + token_client.mint(&buyer, &4000); + contract.create_payment(&buyer, &token_address, &1000, &token_address, &20, &7); + } +} diff --git a/apps/contracts/contracts/milestone-payment-contract/Cargo.toml b/apps/contracts/contracts/milestone-payment-contract/Cargo.toml index dcccfa7..b932c25 100644 --- a/apps/contracts/contracts/milestone-payment-contract/Cargo.toml +++ b/apps/contracts/contracts/milestone-payment-contract/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "milestone-payment" version = "0.1.0" -edition = "2024" +edition = "2021" [lib] crate-type = ["cdylib"] diff --git a/apps/contracts/contracts/milestone-payment-contract/src/contract.rs b/apps/contracts/contracts/milestone-payment-contract/src/contract.rs index 4ad6976..5767b9a 100644 --- a/apps/contracts/contracts/milestone-payment-contract/src/contract.rs +++ b/apps/contracts/contracts/milestone-payment-contract/src/contract.rs @@ -3,7 +3,7 @@ use crate::events::*; use crate::milestone_storage; use crate::milestone_storage::*; use crate::storage; -use soroban_sdk::{Address, Env, String, Vec, token}; +use soroban_sdk::{token, Address, Env, String, Vec}; // Validate milestone data fn validate_milestones( diff --git a/apps/contracts/contracts/milestone-payment-contract/src/events.rs b/apps/contracts/contracts/milestone-payment-contract/src/events.rs index 3aca364..f47593d 100644 --- a/apps/contracts/contracts/milestone-payment-contract/src/events.rs +++ b/apps/contracts/contracts/milestone-payment-contract/src/events.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, contracttype}; +use soroban_sdk::{contracttype, Address, Env, String}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/apps/contracts/contracts/milestone-payment-contract/src/lib.rs b/apps/contracts/contracts/milestone-payment-contract/src/lib.rs index 3a96af3..a47b9ce 100644 --- a/apps/contracts/contracts/milestone-payment-contract/src/lib.rs +++ b/apps/contracts/contracts/milestone-payment-contract/src/lib.rs @@ -7,7 +7,7 @@ mod milestone_storage; mod storage; mod test; -use soroban_sdk::{Address, Env, Vec, contract, contractimpl}; +use soroban_sdk::{contract, contractimpl, Address, Env, Vec}; pub use contract::*; pub use error::*; diff --git a/apps/contracts/contracts/milestone-payment-contract/src/milestone_storage.rs b/apps/contracts/contracts/milestone-payment-contract/src/milestone_storage.rs index 0341b35..c7f0590 100644 --- a/apps/contracts/contracts/milestone-payment-contract/src/milestone_storage.rs +++ b/apps/contracts/contracts/milestone-payment-contract/src/milestone_storage.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, Symbol, Vec, contracttype, symbol_short}; +use soroban_sdk::{contracttype, symbol_short, Address, Env, String, Symbol, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/apps/contracts/contracts/milestone-payment-contract/src/test.rs b/apps/contracts/contracts/milestone-payment-contract/src/test.rs index 47fb5f8..0f88043 100644 --- a/apps/contracts/contracts/milestone-payment-contract/src/test.rs +++ b/apps/contracts/contracts/milestone-payment-contract/src/test.rs @@ -4,7 +4,7 @@ extern crate std; use crate::error::ContractError; use crate::milestone_storage::{ContractStatus, MilestoneData, MilestoneStatus}; use crate::{MilestonePaymentContract, MilestonePaymentContractClient}; -use soroban_sdk::{Address, Env, String, testutils::Address as _, token, vec}; +use soroban_sdk::{testutils::Address as _, token, vec, Address, Env, String}; use token::Client as TokenClient; use token::StellarAssetClient as TokenAdminClient; diff --git a/apps/contracts/contracts/partial-payment-contract/src/deposit_logic.rs b/apps/contracts/contracts/partial-payment-contract/src/deposit_logic.rs index 79b0d47..263f85b 100644 --- a/apps/contracts/contracts/partial-payment-contract/src/deposit_logic.rs +++ b/apps/contracts/contracts/partial-payment-contract/src/deposit_logic.rs @@ -113,11 +113,7 @@ pub fn claim_payment(env: &Env, seller: Address, transaction_id: u64) -> Result< } /// Allows the buyer to get a refund if the deadline has passed. -pub fn request_refund( - env: &Env, - buyer: Address, - transaction_id: u64, -) -> Result<(), ContractError> { +pub fn request_refund(env: &Env, buyer: Address, transaction_id: u64) -> Result<(), ContractError> { buyer.require_auth(); let mut transaction = storage::get_transaction(env, transaction_id)?; @@ -128,7 +124,9 @@ pub fn request_refund( if env.ledger().timestamp() <= transaction.deadline { return Err(ContractError::DeadlineNotPassed); } - if transaction.status == TransactionStatus::Funded || transaction.status == TransactionStatus::Completed { + if transaction.status == TransactionStatus::Funded + || transaction.status == TransactionStatus::Completed + { // If fully funded, the deal is locked in. No refunds. return Err(ContractError::TransactionFullyFunded); } @@ -160,7 +158,9 @@ pub fn cancel_transaction( if transaction.buyer != canceller && transaction.seller != canceller { return Err(ContractError::NotParticipant); } - if transaction.status == TransactionStatus::Funded || transaction.status == TransactionStatus::Completed { + if transaction.status == TransactionStatus::Funded + || transaction.status == TransactionStatus::Completed + { return Err(ContractError::TransactionFullyFunded); } diff --git a/apps/contracts/contracts/partial-payment-contract/src/event.rs b/apps/contracts/contracts/partial-payment-contract/src/event.rs index 795378f..833c645 100644 --- a/apps/contracts/contracts/partial-payment-contract/src/event.rs +++ b/apps/contracts/contracts/partial-payment-contract/src/event.rs @@ -1,7 +1,13 @@ -use soroban_sdk::{symbol_short, Address, Env,}; +use soroban_sdk::{symbol_short, Address, Env}; /// Emits an event when a new transaction is started. -pub fn transaction_started(env: &Env, transaction_id: u64, buyer: &Address, seller: &Address, total_amount: i128) { +pub fn transaction_started( + env: &Env, + transaction_id: u64, + buyer: &Address, + seller: &Address, + total_amount: i128, +) { let topics = (symbol_short!("started"), buyer.clone(), seller.clone()); let data = (transaction_id, total_amount); env.events().publish(topics, data); diff --git a/apps/contracts/contracts/partial-payment-contract/src/lib.rs b/apps/contracts/contracts/partial-payment-contract/src/lib.rs index 5a65d85..7c39b17 100644 --- a/apps/contracts/contracts/partial-payment-contract/src/lib.rs +++ b/apps/contracts/contracts/partial-payment-contract/src/lib.rs @@ -9,10 +9,7 @@ mod test; use soroban_sdk::{contract, contractimpl, Address, Env}; -use crate::{ - error::ContractError, - storage::Transaction, -}; +use crate::{error::ContractError, storage::Transaction}; #[contract] pub struct PartialPaymentContract; @@ -28,14 +25,7 @@ impl PartialPaymentContract { payment_token: Address, deadline: u64, // A timestamp by which the full payment must be completed ) -> Result { - deposit_logic::start_transaction( - &env, - buyer, - seller, - total_amount, - payment_token, - deadline, - ) + deposit_logic::start_transaction(&env, buyer, seller, total_amount, payment_token, deadline) } /// Allows a buyer to make a partial deposit towards a transaction. diff --git a/apps/contracts/contracts/partial-payment-contract/src/test.rs b/apps/contracts/contracts/partial-payment-contract/src/test.rs index 766463f..f60b382 100644 --- a/apps/contracts/contracts/partial-payment-contract/src/test.rs +++ b/apps/contracts/contracts/partial-payment-contract/src/test.rs @@ -8,12 +8,13 @@ use soroban_sdk::{ }; use token::StellarAssetClient as TokenAdminClient; - 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), @@ -87,7 +88,11 @@ fn test_make_partial_and_full_deposit() { let test = DepositTest::setup(); let deadline = test.env.ledger().timestamp() + 3600; let tx_id = test.contract.start_transaction( - &test.buyer, &test.seller, &1000, &test.token.address, &deadline + &test.buyer, + &test.seller, + &1000, + &test.token.address, + &deadline, ); // First partial deposit @@ -110,16 +115,20 @@ fn test_claim_payment() { let test = DepositTest::setup(); let deadline = test.env.ledger().timestamp() + 3600; let tx_id = test.contract.start_transaction( - &test.buyer, &test.seller, &1000, &test.token.address, &deadline + &test.buyer, + &test.seller, + &1000, + &test.token.address, + &deadline, ); test.contract.make_deposit(&test.buyer, &tx_id, &1000); // Seller claims payment test.contract.claim_payment(&test.seller, &tx_id); - + let tx = test.contract.get_transaction(&tx_id); assert_eq!(tx.status, TransactionStatus::Completed); - + // Check balances assert_eq!(test.token.balance(&test.seller), 1000); assert_eq!(test.token.balance(&test.contract.address), 0); @@ -131,7 +140,11 @@ fn test_claim_payment_errors() { let test = DepositTest::setup(); let deadline = test.env.ledger().timestamp() + 3600; let tx_id = test.contract.start_transaction( - &test.buyer, &test.seller, &1000, &test.token.address, &deadline + &test.buyer, + &test.seller, + &1000, + &test.token.address, + &deadline, ); test.contract.make_deposit(&test.buyer, &tx_id, &500); @@ -149,7 +162,11 @@ fn test_request_refund_after_deadline() { let test = DepositTest::setup(); let deadline = test.env.ledger().timestamp() + 10; let tx_id = test.contract.start_transaction( - &test.buyer, &test.seller, &1000, &test.token.address, &deadline + &test.buyer, + &test.seller, + &1000, + &test.token.address, + &deadline, ); test.contract.make_deposit(&test.buyer, &tx_id, &300); @@ -172,7 +189,11 @@ fn test_request_refund_errors() { let test = DepositTest::setup(); let deadline = test.env.ledger().timestamp() + 3600; let tx_id = test.contract.start_transaction( - &test.buyer, &test.seller, &1000, &test.token.address, &deadline + &test.buyer, + &test.seller, + &1000, + &test.token.address, + &deadline, ); test.contract.make_deposit(&test.buyer, &tx_id, &300); @@ -182,7 +203,7 @@ fn test_request_refund_errors() { // Fully fund the transaction test.contract.make_deposit(&test.buyer, &tx_id, &700); - + // Advance time past the deadline test.env.ledger().with_mut(|l| l.timestamp = deadline + 10); @@ -196,7 +217,11 @@ fn test_cancel_transaction() { let test = DepositTest::setup(); let deadline = test.env.ledger().timestamp() + 3600; let tx_id = test.contract.start_transaction( - &test.buyer, &test.seller, &1000, &test.token.address, &deadline + &test.buyer, + &test.seller, + &1000, + &test.token.address, + &deadline, ); test.contract.make_deposit(&test.buyer, &tx_id, &200);