How Our Ethereum Escrow Smart Contract Works

Many of you have been asking for details on how the LocalEthereum escrow smart contract works. The contract is live now, and has already been used successfully to conduct hundreds of over-the-counter ether trades.

This blog post might get pretty technical, but I’ve tried my very best to keep it light.

For those that don’t know, escrows on LocalEthereum are executed using an open source Ethereum smart contract. At no point during a trade can we touch your ether. The only time where we can step is when we get explicit permission to resolve a dispute, and even then we can only direct the ether to one of the parties of the trade.

Using LocalEthereum, an ordinary trade works like this:

  1. The buyer and seller confirm and agree on the terms of the trade.
  2. The seller places the ether into the smart contract (with one click). This provides proof-of-funds and allows for a much safer trade.
  3. The buyer makes payment directly to the seller.
  4. Either:
    1. The seller successfully confirms the payment, and releases the escrow. Trade complete!
    2. A party raises a dispute, and brings in a third-party arbitrator, giving them the keys to decrypt the messages and work with both parties to make a resolution.

The smart contract allows users to safely exchange ether with one another, and to name a trusted third-party to mediate a trade if a dispute arises. Currently, the trusted mediator is always LocalEthereum, but the contract will be adapted in the future to switch over to a reputation-based distributed arbitrator pool.

This post goes over the first version of our smart contract, which we expect to be replaced one day.

Creating and funding an escrow

Technically, escrows are not directly linked to trades on LocalEthereum, and, if we allowed it, the contract could be utilized to escrow transactions outside of LocalEthereum (perhaps for other real-world goods and services). When you first open a trade with somebody using LocalEthereum, no escrow exists on the blockchain… until the seller initiates and funds it in a single transaction.

Every escrow first requires a signed invitation from LocalEthereum, which is just to keep the contract clean. The seller can request one of these signatures from LocalEthereum’s API when they’re ready to place their ether in escrow. The temporary invitation contains a signature of the trade’s properties, including:

  1. The buyer’s address used to interact with the escrow and receive funds
  2. The seller’s address used to interact with the escrow, and receive returned ether in case of a cancellation
  3. The size of the trade in ether
  4. LocalEthereum’s percentage
  5. The payment window in seconds (except for cash trades)

Creating an escrow requires making a call to the external createEscrow function with these parameters and the signed invitation, and paying the full balance up-front. The function can be called from any address — the ether doesn’t have to be sent from the same address as the seller, and it typically isn’t.

We make funding easy by letting sellers choose to use their encrypted in-browser wallet with one-click, but there is also the option to simply copy the necessary data value to initiate the escrow from an external wallet.

The gas cost for creating an escrow is paid by the seller, and the seller can choose a gas price they are comfortable with. The createEscrow function uses approximately 69,000 gas — which costs less than US10¢ at today’s prices.

If you’re a seller, it’s important to make sure you and the buyer are on the same page before you fund the escrow, as it costs a few cents to fund an escrow even if you end up cancelling it. To avoid needlessly locking up your ether, sellers should wait until the buyer responds with agreement of the terms.

Once the initial createEscrow transaction has been confirmed by the network, the escrow exists on the blockchain and can be easily verified by anyone.

When LocalEthereum notices that the escrow has been created and funded, which doesn’t take longer than a few seconds, the trade looks like this for a seller:

Avoiding up-front miner fees with a relay system

LocalEthereum might be many people’s first interaction with Ethereum and cryptocurrencies in general, and that makes things complicated. It costs ether to interact with the blockchain in terms of gas, and somebody who doesn’t have any ether is unable to interact with smart contracts directly.

With this in mind, we’ve designed a system in which traders can interact with our smart contract for “free” using a proxy. The cost of gas is paid up-front by us to relay digital signatures which authorise instructions on a user’s behalf, with the expectation that we’ll be reimbursed at the end of the escrow.

At first glance this system may sound exploitable. What’s to stop users from racking up debt with no intention to pay? There are a few reasons why fronting gas costs is not very gameable:

  1. We choose the gas price, and we’ll keep it reasonable.
  2. We only relay what is legitimate and important.
  3. Since we’ve managed to cut our gas costs down significantly by minimising transactions and storage space, the amount that we loan for each trade is tiny.
  4. There’s a strong financial disincentive to an attack of this nature, because it would involve the attacker burning lots of ether on gas and fees. (Note: The minimum trade size is currently ~US$5.00.)
  5. Our API is rate-limited and protected by CAPTCHAs plus other bot-deterrents, and you need signed permission from the API to create an escrow.
  6. The overhead on relaying actions is reduced by sending many at a time, modifying many escrows in a single transaction. As escrow activity increases, the transaction overhead costs will continue to shrink in proportion.

Making changes to the escrow

During the course of an escrow, there are a few actions each party can take. Ordinarily, these actions are performed by relay, but the parties have the choice to make calls externally if they prefer. If LocalEthereum were to ever go offline, traders are always able to interact with their escrows directly on the Ethereum network.

Each “action” has a single-byte identifier, which are defined as constants near the beginning of the smart contract’s source code.

To invoke an action via relay, the caller simply needs to sign a Keccak hash of the concatenation of the trade ID, the action byte and a maximum gas price that the relayer is allowed to spend for the action. The gas price cap prevents an attack vector whereby LocalEthereum could hypothetically overpay for gas and cause the parties to burn more than a reasonable amount on network fees.

Releasing funds (action 0x05). The seller can release the funds to the buyer at any time during the escrow. This will end the escrow, and its balance, minus network fees and LocalEthereum’s fee, will be sent to the buyer’s address.

Cancelling as a buyer (action 0x02). The buyer can cancel the escrow at any time, returning the funds to the seller. Our fee won’t be deducted from the balance, but any unpaid network fees covered by us will be taken from the total.

Preventing the seller from cancelling (action 0x01). The buyer can halt the payment window countdown and prevent the seller from being able to cancel the escrow. This is done to indicate that the buyer has made payment, and he expects the ether to be released soon.

Once the escrow has entered this stage where the buyer has essentially locked the ether in escrow, there are three ways the escrow can end:

  1. The seller can release the funds.
  2. The buyer can cancel the trade.
  3. Either party can call in the arbitrator to resolve the escrow.

When a buyer asks us to relay this action, we don’t relay it immediately, but we do let the seller know that we have it. In fact in most successful escrows, this action is never broadcasted to the network. This is because it’s only important to relay this before the payment window has expired, and there’s no benefit to doing it earlier. Currently, our servers hold on to the relay request and only send it roughly twenty minutes before the payment window is due to expire (which is more than enough time for the transaction to safely propagate and confirm). In most successful escrows, the seller has already released the escrow by this time, and so this action becomes redundant.

By not relaying unnecessary actions immediately, this allows the traders to save on network fees. (Most successful trades invoke only createEscrow and the seller’s release action.)

Cancelling as a seller (action 0x03). The seller cannot as easily cancel as the buyer, and can only do so if the buyer allows it. The seller first needs to wait until the payment window has expired before they can withdraw their funds from escrow.

Payment windows vary by option and are subject to change — these are the current windows as of October 26, 2017:

Method Payment window
Bank transfer 2 hours
Cash deposit 2 hours
PayPal 2 hours
International wire 2 hours
AliPay 2 hours
WeChat Pay 2 hours
“Other” 2 hours
Cash (in person) The seller must request to cancel (action 0x04), and once they’ve done so the buyer has two hours to reject the request.

Escrow identifiers (which are much more than that)

Allocating storage in an Ethereum smart contract can be really expensive if you’re not careful. Of all of the operations available in the EVM, writing to fresh storage is by far the most costly operation.

Our smart contract is designed to keep a minimal storage footprint. Each escrow in the contract requires an allocation of only two 256-bit “words”, or 64 bytes in total. This is accomplished by tightly packing static properties of the trade into a “trade hash”, and then using that hash as the unique key to the escrow in the public mapping. By packing these into the hash, we avoid the need for allocating extra space for each static property.

Escrows are stored in a public variable named escrows which is a mapping of the custom Escrow struct:

mapping (bytes32 => Escrow) public escrows;

The 32-byte keys to the mapping are Keccak hash functions of the tightly-packed concatenation of:

  • Trade ID (16 bytes): Although currently always the related UUID identifier of a trade on LocalEthereum, any unique nonce would work just fine. This is to make sure that no two trades will ever collide.
  • Seller address (24 bytes): The address of the seller. (This doesn’t need to be the same address that the funds are deposited from — it can be freshly generated.)
  • Buyer address (24 bytes): The address of the buyer.
  • Value (256 bytes, unsigned): The value of the trade (in wei).
  • Fee (16 bytes, unsigned): LocalEthereum’s commission of the trade upon successful release, represented in 1/10,000ths.

Once we have all of the static properties of the trade, we can generate the trade hash with a simple Keccak-256 function in Solidity:

This is the key that anybody can use to verify the status of an escrow in progress on the blockchain. For an easy web inspector, we recommend using Etherscan’s “Read Contract” feature:

The Escrow struct

Information about each escrow is stored in an Escrow struct. This struct fits an allocation of one word; although only 161 bits of storage are needed, the EVM allocates storage only in 256-bit blocks.

struct Escrow {
  bool exists;
  uint32 sellerCanCancelAfter;
  uint128 totalGasFeesSpentByRelayer;

The struct contains these variables:

  • exists: This is simply a boolean to indicate that the escrow exists, since there is no way to differentiate unused storage from zero in Solidity. This is always true until the escrow is released or cancelled.
  • sellerCanCancelAfter: This is initially an epoch timestamp containing the date after which a seller is permitted to cancel the escrow and return the funds to themselves. There are two other special values:
    • A value of 0 indicates that the seller is not allowed to cancel at any time. The value can be set to 0 at any time at the buyer’s request to lock the funds in the escrow until the seller releases the funds or the arbitrator steps in to resolve the trade. This is typically done when buyer indicates he has made payment, and expects the funds to be released.
    • For specific payment types where payment windows don’t make as much sense (e.g. cash) this is set to 1, which is a special value to mean infinity. When sellerCanCancelAfter equals 1, the seller can’t cancel, but can initiate a request to cancel. When the seller indicates that he’d like to cancel the trade, the buyer is given adequate time to dispute the request before the seller-cancellation is permitted (currently set to two hours).
  • totalGasFeesSpentByRelayer: This is a counter of the gas accumulated by relay operations. It will be deducted from the escrow to cover network fees once it is released or cancelled.

Disputing a trade

In the case of a dispute, either party needs to give us their signed dispute token. With a signature of this dispute token, which is simply the output of sha3(Trade ID, 0x06), the arbitrator can resolve the dispute in either party’s favour.

To do this, we call the external resolveDispute function with the trade’s static properties, the signed dispute token, and the percentage of the escrow’s balance that will be sent to each party. It is impossible for us to have the disputed ether sent to anybody else.

We also expect to receive the LocalEthereum trade conversation’s “shared secret” at the same time, which gives us the ability to decipher and read encrypted message history between the buyer and seller. However, that is for another post…

**LocalEthereum has already helped users complete ~200 over-the-counter trades worth more than US$120,000 since we opened trading this week.**

Update (a few weeks later): LocalEthereum is now doing much more than that in volume and trades each day.

Questions? Contact us here.