solidity/docs/examples/safe-remote.rst
2023-05-22 12:54:43 +01:00

150 lines
5.0 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 some compensation, e.g. Ether,
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 Ether 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 Ether 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.
.. code-block:: solidity
// 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()
external
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()
external
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()
external
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()
external
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);
}
}