mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
150 lines
4.9 KiB
ReStructuredText
150 lines
4.9 KiB
ReStructuredText
.. index:: purchase, remote purchase, escrow
|
|
|
|
********************
|
|
Safe Remote Purchase
|
|
********************
|
|
|
|
Purchasing goods remotely currently requires multiple parties that need to trust each other.
|
|
The simplest configuration involves a seller and a buyer. The buyer would like to receive
|
|
an item from the seller and the seller would like to get money (or an equivalent)
|
|
in return. The problematic part is the shipment here: There is no way to determine for
|
|
sure that the item arrived at the buyer.
|
|
|
|
There are multiple ways to solve this problem, but all fall short in one or the other way.
|
|
In the following example, both parties have to put twice the value of the item into the
|
|
contract as escrow. As soon as this happened, the money will stay locked inside
|
|
the contract until the buyer confirms that they received the item. After that,
|
|
the buyer is returned the value (half of their deposit) and the seller gets three
|
|
times the value (their deposit plus the value). The idea behind
|
|
this is that both parties have an incentive to resolve the situation or otherwise
|
|
their money is locked forever.
|
|
|
|
This contract of course does not solve the problem, but gives an overview of how
|
|
you can use state machine-like constructs inside a contract.
|
|
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity ^0.8.4;
|
|
contract Purchase {
|
|
uint public value;
|
|
address payable public seller;
|
|
address payable public buyer;
|
|
|
|
enum State { Created, Locked, Release, Inactive }
|
|
// The state variable has a default value of the first member, `State.created`
|
|
State public state;
|
|
|
|
modifier condition(bool _condition) {
|
|
require(_condition);
|
|
_;
|
|
}
|
|
|
|
/// Only the buyer can call this function.
|
|
error OnlyBuyer();
|
|
/// Only the seller can call this function.
|
|
error OnlySeller();
|
|
/// The function cannot be called at the current state.
|
|
error InvalidState();
|
|
/// The provided value has to be even.
|
|
error ValueNotEven();
|
|
|
|
modifier onlyBuyer() {
|
|
if (msg.sender != buyer)
|
|
revert OnlyBuyer();
|
|
_;
|
|
}
|
|
|
|
modifier onlySeller() {
|
|
if (msg.sender != seller)
|
|
revert OnlySeller();
|
|
_;
|
|
}
|
|
|
|
modifier inState(State _state) {
|
|
if (state != _state)
|
|
revert InvalidState();
|
|
_;
|
|
}
|
|
|
|
event Aborted();
|
|
event PurchaseConfirmed();
|
|
event ItemReceived();
|
|
event SellerRefunded();
|
|
|
|
// Ensure that `msg.value` is an even number.
|
|
// Division will truncate if it is an odd number.
|
|
// Check via multiplication that it wasn't an odd number.
|
|
constructor() payable {
|
|
seller = payable(msg.sender);
|
|
value = msg.value / 2;
|
|
if ((2 * value) != msg.value)
|
|
revert ValueNotEven();
|
|
}
|
|
|
|
/// Abort the purchase and reclaim the ether.
|
|
/// Can only be called by the seller before
|
|
/// the contract is locked.
|
|
function abort()
|
|
public
|
|
onlySeller
|
|
inState(State.Created)
|
|
{
|
|
emit Aborted();
|
|
state = State.Inactive;
|
|
// We use transfer here directly. It is
|
|
// reentrancy-safe, because it is the
|
|
// last call in this function and we
|
|
// already changed the state.
|
|
seller.transfer(address(this).balance);
|
|
}
|
|
|
|
/// Confirm the purchase as buyer.
|
|
/// Transaction has to include `2 * value` ether.
|
|
/// The ether will be locked until confirmReceived
|
|
/// is called.
|
|
function confirmPurchase()
|
|
public
|
|
inState(State.Created)
|
|
condition(msg.value == (2 * value))
|
|
payable
|
|
{
|
|
emit PurchaseConfirmed();
|
|
buyer = payable(msg.sender);
|
|
state = State.Locked;
|
|
}
|
|
|
|
/// Confirm that you (the buyer) received the item.
|
|
/// This will release the locked ether.
|
|
function confirmReceived()
|
|
public
|
|
onlyBuyer
|
|
inState(State.Locked)
|
|
{
|
|
emit ItemReceived();
|
|
// It is important to change the state first because
|
|
// otherwise, the contracts called using `send` below
|
|
// can call in again here.
|
|
state = State.Release;
|
|
|
|
buyer.transfer(value);
|
|
}
|
|
|
|
/// This function refunds the seller, i.e.
|
|
/// pays back the locked funds of the seller.
|
|
function refundSeller()
|
|
public
|
|
onlySeller
|
|
inState(State.Release)
|
|
{
|
|
emit SellerRefunded();
|
|
// It is important to change the state first because
|
|
// otherwise, the contracts called using `send` below
|
|
// can call in again here.
|
|
state = State.Inactive;
|
|
|
|
seller.transfer(3 * value);
|
|
}
|
|
}
|