Smart Escrow Implementation for Haggling System

Code as Law Ethereum Solidity
Avatar
EtherOptimist Core Developer
Joined: Jan 2023 • Posts: 387 4.8k

Hey BayouBid community,

We're making progress on the Smart Escrow implementation for our haggling system, and I wanted to share the current design and get your feedback. As discussed in the Code as Law principles, our goal is to make the haggling process fully transparent and enforceable by smart contracts rather than platform policies.

Here's the current draft of the HaggleEscrow smart contract that will handle offer submissions, counter-offers, and final transactions:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; /** * @title BayouBid HaggleEscrow * @notice Implements a trustless escrow for haggling on marketplace items * @dev This contract manages the full lifecycle of a haggle transaction */ contract HaggleEscrow { enum OfferStatus { Pending, Accepted, CounterOffered, Expired, Completed, Refunded } struct Offer { address buyer; address seller; uint256 itemId; uint256 offerAmount; uint256 counterOfferAmount; uint256 expiryTime; OfferStatus status; bool isEscrowFunded; } // Mapping from offerId to Offer mapping(uint256 => Offer) public offers; uint256 private nextOfferId = 1; // Platform fee percentage (0.5%) uint256 private constant PLATFORM_FEE_BPS = 50; // 50 basis points = 0.5% address private platformWallet; // Events event OfferSubmitted(uint256 indexed offerId, address indexed buyer, address indexed seller, uint256 itemId, uint256 amount, uint256 expiryTime); event OfferAccepted(uint256 indexed offerId, address indexed seller); event CounterOfferMade(uint256 indexed offerId, uint256 counterOfferAmount, uint256 newExpiryTime); event OfferCompleted(uint256 indexed offerId, uint256 finalAmount); event OfferRefunded(uint256 indexed offerId, string reason); event OfferExpired(uint256 indexed offerId); constructor(address _platformWallet) { platformWallet = _platformWallet; } /** * @notice Submit a new offer with funds in escrow * @param _seller The address of the item seller * @param _itemId The unique identifier of the item * @param _expiryTimeInHours Number of hours until the offer expires */ function submitOffer(address _seller, uint256 _itemId, uint256 _expiryTimeInHours) external payable returns (uint256) { require(_seller != address(0), "Invalid seller address"); require(_seller != msg.sender, "Cannot make offer on your own item"); require(msg.value > 0, "Offer amount must be greater than 0"); require(_expiryTimeInHours > 0, "Expiry time must be greater than 0"); uint256 expiryTime = block.timestamp + (_expiryTimeInHours * 1 hours); uint256 offerId = nextOfferId++; offers[offerId] = Offer({ buyer: msg.sender, seller: _seller, itemId: _itemId, offerAmount: msg.value, counterOfferAmount: 0, expiryTime: expiryTime, status: OfferStatus.Pending, isEscrowFunded: true }); emit OfferSubmitted(offerId, msg.sender, _seller, _itemId, msg.value, expiryTime); return offerId; } /** * @notice Accept an offer and release funds to the seller * @param _offerId The ID of the offer to accept */ function acceptOffer(uint256 _offerId) external { Offer storage offer = offers[_offerId]; require(msg.sender == offer.seller, "Only seller can accept"); require(offer.status == OfferStatus.Pending, "Offer is not pending"); require(block.timestamp < offer.expiryTime, "Offer has expired"); require(offer.isEscrowFunded, "Escrow is not funded"); offer.status = OfferStatus.Accepted; // Calculate platform fee uint256 platformFee = (offer.offerAmount * PLATFORM_FEE_BPS) / 10000; uint256 sellerAmount = offer.offerAmount - platformFee; // Transfer funds (bool platformTransferSuccess, ) = platformWallet.call{value: platformFee}(""); require(platformTransferSuccess, "Platform fee transfer failed"); (bool sellerTransferSuccess, ) = offer.seller.call{value: sellerAmount}(""); require(sellerTransferSuccess, "Seller transfer failed"); offer.status = OfferStatus.Completed; offer.isEscrowFunded = false; emit OfferAccepted(_offerId, msg.sender); emit OfferCompleted(_offerId, offer.offerAmount); } /** * @notice Make a counter offer to the buyer * @param _offerId The ID of the offer to counter * @param _counterOfferAmount The counter offer amount * @param _additionalExpiryHours Additional hours to extend the expiry */ function makeCounterOffer( uint256 _offerId, uint256 _counterOfferAmount, uint256 _additionalExpiryHours ) external { Offer storage offer = offers[_offerId]; require(msg.sender == offer.seller, "Only seller can counter"); require(offer.status == OfferStatus.Pending, "Offer is not pending"); require(block.timestamp < offer.expiryTime, "Offer has expired"); require(_counterOfferAmount > offer.offerAmount, "Counter must be higher than offer"); offer.counterOfferAmount = _counterOfferAmount; offer.expiryTime = block.timestamp + (_additionalExpiryHours * 1 hours); offer.status = OfferStatus.CounterOffered; emit CounterOfferMade(_offerId, _counterOfferAmount, offer.expiryTime); } /** * @notice Accept a counter offer by paying the difference * @param _offerId The ID of the counter offer to accept */ function acceptCounterOffer(uint256 _offerId) external payable { Offer storage offer = offers[_offerId]; require(msg.sender == offer.buyer, "Only buyer can accept counter"); require(offer.status == OfferStatus.CounterOffered, "No counter offer exists"); require(block.timestamp < offer.expiryTime, "Counter offer has expired"); uint256 additionalAmount = offer.counterOfferAmount - offer.offerAmount; require(msg.value == additionalAmount, "Must pay exact difference amount"); // Update offer amount to the counter offer amount offer.offerAmount = offer.counterOfferAmount; offer.status = OfferStatus.Accepted; // Calculate platform fee uint256 platformFee = (offer.offerAmount * PLATFORM_FEE_BPS) / 10000; uint256 sellerAmount = offer.offerAmount - platformFee; // Transfer funds (bool platformTransferSuccess, ) = platformWallet.call{value: platformFee}(""); require(platformTransferSuccess, "Platform fee transfer failed"); (bool sellerTransferSuccess, ) = offer.seller.call{value: sellerAmount}(""); require(sellerTransferSuccess, "Seller transfer failed"); offer.status = OfferStatus.Completed; offer.isEscrowFunded = false; emit OfferCompleted(_offerId, offer.offerAmount); } /** * @notice Expire an offer and refund the buyer * @param _offerId The ID of the offer to expire */ function expireOffer(uint256 _offerId) external { Offer storage offer = offers[_offerId]; require(offer.status == OfferStatus.Pending || offer.status == OfferStatus.CounterOffered, "Offer must be pending or countered"); require(block.timestamp >= offer.expiryTime, "Offer has not expired yet"); require(offer.isEscrowFunded, "Escrow is not funded"); offer.status = OfferStatus.Expired; offer.isEscrowFunded = false; // Refund buyer (bool success, ) = offer.buyer.call{value: offer.offerAmount}(""); require(success, "Refund failed"); emit OfferExpired(_offerId); emit OfferRefunded(_offerId, "Offer expired"); } /** * @notice Cancel a pending offer (only buyer can cancel) * @param _offerId The ID of the offer to cancel */ function cancelOffer(uint256 _offerId) external { Offer storage offer = offers[_offerId]; require(msg.sender == offer.buyer, "Only buyer can cancel"); require(offer.status == OfferStatus.Pending, "Offer must be pending"); require(offer.isEscrowFunded, "Escrow is not funded"); offer.status = OfferStatus.Refunded; offer.isEscrowFunded = false; // Refund buyer (bool success, ) = offer.buyer.call{value: offer.offerAmount}(""); require(success, "Refund failed"); emit OfferRefunded(_offerId, "Offer cancelled by buyer"); } /** * @notice Get offer details * @param _offerId The ID of the offer to query */ function getOfferDetails(uint256 _offerId) external view returns ( address buyer, address seller, uint256 itemId, uint256 offerAmount, uint256 counterOfferAmount, uint256 expiryTime, OfferStatus status, bool isEscrowFunded ) { Offer storage offer = offers[_offerId]; return ( offer.buyer, offer.seller, offer.itemId, offer.offerAmount, offer.counterOfferAmount, offer.expiryTime, offer.status, offer.isEscrowFunded ); } }

The contract implements a transparent escrow system that:

  • Securely holds buyer funds during the haggling process
  • Supports counter-offers from sellers
  • Facilitates automatic payment upon acceptance of an offer
  • Ensures automatic refunds when offers expire
  • Takes a small platform fee (0.5%) to sustain the BayouBid ecosystem

We plan to integrate this with the haggling UI we've implemented on the product pages. The idea is that when a buyer makes an offer, the smart contract will be invoked, and the UI will track the status of the offer.

What I'm particularly interested in is how we can make this process feel seamless even for users who aren't familiar with blockchain. What are your thoughts on:

  1. Should we add an "Inspection Period" to allow buyers to verify item condition before finalizing payment?
  2. Should counter-offers be limited in number or percentage increase?
  3. Are there any edge cases we should address?

Looking forward to your feedback and suggestions!

Avatar
DefiDeveloper Moderator
Joined: Mar 2024 • Posts: 142 2.3k

This is looking promising, EtherOptimist! I have a few thoughts on your implementation:

Should we add an "Inspection Period" to allow buyers to verify item condition before finalizing payment?

Absolutely, I think an inspection period is essential. However, it introduces a complexity - how do we resolve disputes if the buyer claims the item doesn't match the description? Here's what I'd add to your contract:

/** * @notice Update the contract to include an inspection period */ enum DeliveryStatus { Pending, Shipped, Delivered, Accepted, Disputed } struct Delivery { DeliveryStatus status; uint256 deliveryTimestamp; uint256 inspectionEndTimestamp; string disputeReason; } // Add to Offer struct // Delivery delivery; function markAsDelivered(uint256 _offerId) external { Offer storage offer = offers[_offerId]; require(msg.sender == offer.seller, "Only seller can mark as delivered"); require(offer.status == OfferStatus.Accepted, "Offer must be accepted"); offer.delivery.status = DeliveryStatus.Delivered; offer.delivery.deliveryTimestamp = block.timestamp; offer.delivery.inspectionEndTimestamp = block.timestamp + (48 hours); // 48 hour inspection period emit ItemDelivered(_offerId, block.timestamp, offer.delivery.inspectionEndTimestamp); } function acceptDelivery(uint256 _offerId) external { Offer storage offer = offers[_offerId]; require(msg.sender == offer.buyer, "Only buyer can accept delivery"); require(offer.delivery.status == DeliveryStatus.Delivered, "Item must be marked as delivered"); require(block.timestamp <= offer.delivery.inspectionEndTimestamp, "Inspection period expired"); offer.delivery.status = DeliveryStatus.Accepted; // Release funds to seller (similar to acceptOffer logic) // ... emit DeliveryAccepted(_offerId); } function disputeDelivery(uint256 _offerId, string calldata _reason) external { Offer storage offer = offers[_offerId]; require(msg.sender == offer.buyer, "Only buyer can dispute"); require(offer.delivery.status == DeliveryStatus.Delivered, "Item must be marked as delivered"); require(block.timestamp <= offer.delivery.inspectionEndTimestamp, "Inspection period expired"); offer.delivery.status = DeliveryStatus.Disputed; offer.delivery.disputeReason = _reason; emit DeliveryDisputed(_offerId, _reason); // This would trigger the dispute resolution process // We'd need to implement arbitration logic }
Should counter-offers be limited in number or percentage increase?

I think limiting counter-offers is a good idea to prevent the haggling from going on too long. Maybe allow max 3 counter-offers and cap the increase at 30% of the original offer. Also, we should consider the UX flow when a counter-offer is made - we need good notifications.

As for edge cases, I think we need to handle:

  • Transaction failures due to gas price spikes
  • What happens if a seller goes inactive during the haggling process?
  • What if an item becomes unavailable after an offer is made?

Also, for users unfamiliar with blockchain, we should consider implementing a meta-transaction layer so they don't need to deal with gas fees directly. Have you considered using EIP-2771 for this?

Avatar
BlockchainLawyer Legal Advisor
Joined: Sep 2023 • Posts: 75 3.6k

I want to address some legal considerations with this implementation. While "Code as Law" is our guiding principle, we still need to ensure compliance with consumer protection regulations across jurisdictions.

For the inspection period that DefiDeveloper suggested, I'd recommend enhancing it with a few additional features:

// Add to the contract to handle regulatory requirements // ESCROW TIMELOCK PARAMETERS uint256 private constant MIN_INSPECTION_PERIOD = 24 hours; // Minimum by consumer law in most regions uint256 private constant MAX_DISPUTE_WINDOW = 14 days; // Maximum time after delivery for disputes // Additional metadata for regulatory compliance struct ComplianceData { string itemDescription; string itemCondition; bytes32 imageHash; // IPFS hash of the item image for verification bool hasWarranty; uint256 warrantyPeriod; } // Add function to validate disputes based on evidence function submitEvidence(uint256 _offerId, string calldata _evidenceDescription, bytes32 _evidenceHash) external { // Only allow during active disputes // Store evidence on-chain for transparency // This creates an immutable record for arbitration } // Required for certain jurisdictions to allow cancellation within cooling-off period function mandatoryCoolingOff(uint256 _offerId) external { Offer storage offer = offers[_offerId]; require(msg.sender == offer.buyer, "Only buyer can invoke cooling-off"); // Check if within cooling-off period (varies by jurisdiction, typically 14 days) require(block.timestamp <= offer.acceptanceTimestamp + (14 days), "Cooling-off period expired"); // Implement refund logic with potential restocking fee as allowed by law // ... } // Function to generate a legally-binding receipt function generateReceipt(uint256 _offerId) external view returns (string memory) { // Generate a detailed receipt with all transaction details // Include timestamps and all relevant metadata // This is crucial for tax and consumer protection purposes }

A few key legal considerations:

  1. GDPR Compliance: We need to ensure that any personal data stored on-chain is compliant with privacy regulations. Consider storing only hashed or encrypted data on-chain with the actual data in a secure off-chain database.
  2. Consumer Rights: Many jurisdictions require cooling-off periods for online purchases. We should make these rights programmable in our smart contract.
  3. Dispute Resolution: While we aim for trustless transactions, we need a clear arbitration mechanism that complies with legal standards, perhaps using a decentralized court system like Kleros or Aragon Court.
  4. Tax Documentation: The contract should generate proper receipts for tax purposes in both the buyer's and seller's jurisdictions.

I've been working on a "Regulatory Compatibility Layer" that sits between our smart contracts and the user interface. This layer translates legal requirements into code constraints without compromising the core "Code as Law" principles. I can share more details about this in a separate thread if there's interest.

Avatar
EtherOptimist Core Developer
Joined: Jan 2023 • Posts: 387 4.8k

Thanks for the amazing feedback, everyone! Based on your suggestions, I've updated the contract design and would like to share our implementation roadmap:

Phase 1: Core Haggling (Target: April 2025)
  • Implement basic offer/counter-offer flow
  • Integrate with the UI components we've already built
  • Add gas abstraction layer for non-technical users
Phase 2: Escrow & Inspection (Target: May 2025)
  • Implement inspection period as DefiDeveloper suggested
  • Add delivery tracking integration
  • Create dispute resolution mechanism
Phase 3: Regulatory Compliance (Target: June 2025)
  • Implement BlockchainLawyer's Regulatory Compatibility Layer
  • Add jurisdiction-specific rules with configurable parameters
  • Create compliant documentation generation

I've also decided to add a limit on counter-offers (maximum 3) and cap the increase at 30% over the original offer, as suggested.

For transparency, I've pushed these updated contracts to our GitHub repo: bayoubid/smart-contracts/haggle-escrow, and I'd appreciate if you could review the changes.

We've already started implementing the UI integration on the product pages. The haggling interface you'll see on the item view page will soon connect directly to these contracts!

I'm really excited about making the "Code as Law" principles tangible for our users. By making the haggling system completely transparent and enforced by smart contracts rather than platform policies, we're taking a huge step toward our vision of a truly peer-to-peer marketplace.

Post a Reply