Build an Escrow¶
Create a trustless escrow contract for secure transactions.
Prerequisites¶
- SolScript installed
- Understanding of state machines
- Completed previous tutorials
What We'll Build¶
An escrow system with: - Deposit funds - Release to recipient - Refund to depositor - Arbiter for disputes - Time-based expiry
Step 1: State Machine¶
contract Escrow {
// Escrow states
enum State {
Created, // Initial state
Funded, // Buyer deposited funds
Released, // Funds released to seller
Refunded, // Funds returned to buyer
Disputed, // Under arbiter review
Resolved // Dispute resolved
}
// Escrow details
struct EscrowDetails {
address buyer;
address seller;
address arbiter;
uint256 amount;
uint256 deadline;
State state;
string description;
}
// State
mapping(uint256 => EscrowDetails) public escrows;
uint256 public escrowCount;
uint256 public arbiterFee = 100; // 1% in basis points
// Events
event EscrowCreated(uint256 indexed id, address buyer, address seller, uint256 amount);
event EscrowFunded(uint256 indexed id);
event EscrowReleased(uint256 indexed id);
event EscrowRefunded(uint256 indexed id);
event DisputeRaised(uint256 indexed id, address by);
event DisputeResolved(uint256 indexed id, bool releasedToSeller);
// Errors
error InvalidState(State current, State required);
error NotAuthorized();
error InsufficientFunds();
error DeadlineNotReached();
error DeadlinePassed();
}
Step 2: Modifiers¶
modifier inState(uint256 escrowId, State required) {
if (escrows[escrowId].state != required) {
revert InvalidState(escrows[escrowId].state, required);
}
_;
}
modifier onlyBuyer(uint256 escrowId) {
if (msg.sender != escrows[escrowId].buyer) revert NotAuthorized();
_;
}
modifier onlySeller(uint256 escrowId) {
if (msg.sender != escrows[escrowId].seller) revert NotAuthorized();
_;
}
modifier onlyArbiter(uint256 escrowId) {
if (msg.sender != escrows[escrowId].arbiter) revert NotAuthorized();
_;
}
modifier onlyParty(uint256 escrowId) {
EscrowDetails storage e = escrows[escrowId];
if (msg.sender != e.buyer && msg.sender != e.seller) {
revert NotAuthorized();
}
_;
}
Step 3: Create Escrow¶
function createEscrow(
address seller,
address arbiter,
uint256 deadline,
string memory description
) public payable returns (uint256) {
require(seller != address(0), "Invalid seller");
require(arbiter != address(0), "Invalid arbiter");
require(msg.value > 0, "Must send funds");
require(deadline > block.timestamp, "Deadline must be future");
escrowCount++;
escrows[escrowCount] = EscrowDetails({
buyer: msg.sender,
seller: seller,
arbiter: arbiter,
amount: msg.value,
deadline: deadline,
state: State.Funded,
description: description
});
emit EscrowCreated(escrowCount, msg.sender, seller, msg.value);
emit EscrowFunded(escrowCount);
return escrowCount;
}
Step 4: Release Funds¶
Buyer releases funds to seller:
function release(uint256 escrowId)
public
onlyBuyer(escrowId)
inState(escrowId, State.Funded)
{
EscrowDetails storage e = escrows[escrowId];
e.state = State.Released;
// Transfer funds to seller
// (Solana transfer implementation)
emit EscrowReleased(escrowId);
}
Step 5: Request Refund¶
Seller agrees to refund or deadline passed:
function refund(uint256 escrowId)
public
inState(escrowId, State.Funded)
{
EscrowDetails storage e = escrows[escrowId];
// Seller can voluntarily refund
if (msg.sender == e.seller) {
e.state = State.Refunded;
// Transfer funds back to buyer
emit EscrowRefunded(escrowId);
return;
}
// Buyer can claim refund after deadline
if (msg.sender == e.buyer) {
if (block.timestamp < e.deadline) {
revert DeadlineNotReached();
}
e.state = State.Refunded;
// Transfer funds back to buyer
emit EscrowRefunded(escrowId);
return;
}
revert NotAuthorized();
}
Step 6: Dispute Handling¶
function raiseDispute(uint256 escrowId)
public
onlyParty(escrowId)
inState(escrowId, State.Funded)
{
escrows[escrowId].state = State.Disputed;
emit DisputeRaised(escrowId, msg.sender);
}
function resolveDispute(uint256 escrowId, bool releaseToSeller)
public
onlyArbiter(escrowId)
inState(escrowId, State.Disputed)
{
EscrowDetails storage e = escrows[escrowId];
e.state = State.Resolved;
// Calculate arbiter fee
uint256 fee = (e.amount * arbiterFee) / 10000;
uint256 remaining = e.amount - fee;
if (releaseToSeller) {
// Transfer remaining to seller
// Transfer fee to arbiter
} else {
// Transfer remaining to buyer
// Transfer fee to arbiter
}
emit DisputeResolved(escrowId, releaseToSeller);
}
Step 7: View Functions¶
function getEscrow(uint256 escrowId) public view returns (
address buyer,
address seller,
address arbiter,
uint256 amount,
uint256 deadline,
State state,
string memory description
) {
EscrowDetails storage e = escrows[escrowId];
return (
e.buyer,
e.seller,
e.arbiter,
e.amount,
e.deadline,
e.state,
e.description
);
}
function isExpired(uint256 escrowId) public view returns (bool) {
return block.timestamp >= escrows[escrowId].deadline;
}
function getTimeRemaining(uint256 escrowId) public view returns (uint256) {
EscrowDetails storage e = escrows[escrowId];
if (block.timestamp >= e.deadline) return 0;
return e.deadline - block.timestamp;
}
Complete Contract¶
contract Escrow {
enum State { Created, Funded, Released, Refunded, Disputed, Resolved }
struct EscrowDetails {
address buyer;
address seller;
address arbiter;
uint256 amount;
uint256 deadline;
State state;
string description;
}
mapping(uint256 => EscrowDetails) public escrows;
uint256 public escrowCount;
uint256 public arbiterFee = 100;
event EscrowCreated(uint256 indexed id, address buyer, address seller, uint256 amount);
event EscrowFunded(uint256 indexed id);
event EscrowReleased(uint256 indexed id);
event EscrowRefunded(uint256 indexed id);
event DisputeRaised(uint256 indexed id, address by);
event DisputeResolved(uint256 indexed id, bool releasedToSeller);
error InvalidState(State current, State required);
error NotAuthorized();
error DeadlineNotReached();
modifier inState(uint256 escrowId, State required) {
if (escrows[escrowId].state != required) {
revert InvalidState(escrows[escrowId].state, required);
}
_;
}
modifier onlyBuyer(uint256 escrowId) {
if (msg.sender != escrows[escrowId].buyer) revert NotAuthorized();
_;
}
modifier onlySeller(uint256 escrowId) {
if (msg.sender != escrows[escrowId].seller) revert NotAuthorized();
_;
}
modifier onlyArbiter(uint256 escrowId) {
if (msg.sender != escrows[escrowId].arbiter) revert NotAuthorized();
_;
}
modifier onlyParty(uint256 escrowId) {
EscrowDetails storage e = escrows[escrowId];
if (msg.sender != e.buyer && msg.sender != e.seller) revert NotAuthorized();
_;
}
function createEscrow(
address seller,
address arbiter,
uint256 deadline,
string memory description
) public payable returns (uint256) {
require(seller != address(0), "Invalid seller");
require(arbiter != address(0), "Invalid arbiter");
require(msg.value > 0, "Must send funds");
require(deadline > block.timestamp, "Invalid deadline");
escrowCount++;
escrows[escrowCount] = EscrowDetails({
buyer: msg.sender,
seller: seller,
arbiter: arbiter,
amount: msg.value,
deadline: deadline,
state: State.Funded,
description: description
});
emit EscrowCreated(escrowCount, msg.sender, seller, msg.value);
emit EscrowFunded(escrowCount);
return escrowCount;
}
function release(uint256 escrowId) public onlyBuyer(escrowId) inState(escrowId, State.Funded) {
escrows[escrowId].state = State.Released;
emit EscrowReleased(escrowId);
}
function refund(uint256 escrowId) public inState(escrowId, State.Funded) {
EscrowDetails storage e = escrows[escrowId];
if (msg.sender == e.seller) {
e.state = State.Refunded;
emit EscrowRefunded(escrowId);
return;
}
if (msg.sender == e.buyer) {
if (block.timestamp < e.deadline) revert DeadlineNotReached();
e.state = State.Refunded;
emit EscrowRefunded(escrowId);
return;
}
revert NotAuthorized();
}
function raiseDispute(uint256 escrowId) public onlyParty(escrowId) inState(escrowId, State.Funded) {
escrows[escrowId].state = State.Disputed;
emit DisputeRaised(escrowId, msg.sender);
}
function resolveDispute(uint256 escrowId, bool releaseToSeller) public onlyArbiter(escrowId) inState(escrowId, State.Disputed) {
escrows[escrowId].state = State.Resolved;
emit DisputeResolved(escrowId, releaseToSeller);
}
function getEscrow(uint256 escrowId) public view returns (
address buyer, address seller, address arbiter,
uint256 amount, uint256 deadline, State state, string memory description
) {
EscrowDetails storage e = escrows[escrowId];
return (e.buyer, e.seller, e.arbiter, e.amount, e.deadline, e.state, e.description);
}
}
Build and Deploy¶
Usage Flow¶
- Buyer creates escrow with payment
- Seller delivers goods/services
- Buyer releases funds OR
- Seller refunds OR deadline passes
- If dispute: Arbiter resolves