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:
- Should we add an "Inspection Period" to allow buyers to verify item condition before finalizing payment?
- Should counter-offers be limited in number or percentage increase?
- Are there any edge cases we should address?
Looking forward to your feedback and suggestions!