Safe Remote Purchases¶
Warning
This is example code for learning purposes. Do not use in production without thorough review and testing.
In this example, we have an escrow contract implementing a system for a trustless
transaction between a buyer and a seller. In this system, a seller posts an item
for sale and makes a deposit to the contract of twice the item’s value. At
this moment, the contract has a balance of 2 * value. The seller can reclaim
the deposit and close the sale as long as a buyer has not yet made a purchase.
If a buyer is interested in making a purchase, they would make a payment and
submit an equal amount for deposit (totaling 2 * value) into the contract
and locking the contract from further modification. At this moment, the contract
has a balance of 4 * value and the seller would send the item to buyer. Upon
the buyer’s receipt of the item, the buyer will mark the item as received in the
contract, thereby returning the buyer’s deposit (not payment), releasing the
remaining funds to the seller, and completing the transaction.
There are certainly other ways of designing a secure escrow system with less overhead for both the buyer and seller, but for the purpose of this example, we want to explore one way how an escrow system can be implemented trustlessly.
Let’s go!
1#pragma version >0.3.10
2
3# Safe Remote Purchase
4# Originally from
5# https://github.com/ethereum/solidity/blob/develop/docs/solidity-by-example.rst
6# Ported to vyper and optimized.
7
8# Rundown of the transaction:
9# 1. Seller posts item for sale and posts safety deposit of double the item value.
10# Balance is 2*value.
11# (1.1. Seller can reclaim deposit and close the sale as long as nothing was purchased.)
12# 2. Buyer purchases item (value) plus posts an additional safety deposit (Item value).
13# Balance is 4*value.
14# 3. Seller ships item.
15# 4. Buyer confirms receiving the item. Buyer's deposit (value) is returned.
16# Seller's deposit (2*value) + items value is returned. Balance is 0.
17
18value: public(uint256) #Value of the item
19seller: public(address)
20buyer: public(address)
21unlocked: public(bool)
22ended: public(bool)
23finalized: public(bool)
24
25@deploy
26@payable
27def __init__():
28 assert (msg.value % 2) == 0
29 assert msg.value > 0
30 self.value = msg.value // 2 # The seller initializes the contract by
31 # posting a safety deposit of 2*value of the item up for sale.
32 self.seller = msg.sender
33 self.unlocked = True
34
35@external
36def abort():
37 assert not self.finalized
38 assert self.unlocked #Is the contract still refundable?
39 assert msg.sender == self.seller # Only the seller can refund
40 # his deposit before any buyer purchases the item.
41 self.finalized = True
42 assert self.balance > 0 and self.balance == 2 * self.value
43 send(self.seller, self.balance)
44
45@external
46@payable
47def purchase():
48 assert not self.finalized
49 assert self.unlocked # Is the contract still open (is the item still up
50 # for sale)?
51 assert msg.value == (2 * self.value) # Is the deposit the correct value?
52 self.buyer = msg.sender
53 self.unlocked = False
54
55@external
56def received():
57 # 1. Conditions
58 assert not self.finalized
59 assert not self.unlocked # Is the item already purchased and pending
60 # confirmation from the buyer?
61 assert msg.sender == self.buyer
62 assert not self.ended
63
64 # 2. Effects
65 self.ended = True
66 self.finalized = True
67
68 # 3. Interaction
69 send(self.buyer, self.value) # Return the buyer's deposit (=value) to the buyer.
70 assert self.balance == 3 * self.value
71 send(self.seller, self.balance) # Return the seller's deposit (=2*value) and the
72 # purchase price (=value) to the seller.
This is also a moderately short contract, however a little more complex in logic. Let’s break down this contract bit by bit.
18value: public(uint256) #Value of the item
19seller: public(address)
20buyer: public(address)
21unlocked: public(bool)
22ended: public(bool)
23finalized: public(bool)
Like the other contracts, we begin by declaring our global variables public with
their respective data types. Remember that the public function allows the
variables to be readable by an external caller, but not writeable.
25@deploy
26@payable
27def __init__():
28 assert (msg.value % 2) == 0
29 assert msg.value > 0
30 self.value = msg.value // 2 # The seller initializes the contract by
31 # posting a safety deposit of 2*value of the item up for sale.
32 self.seller = msg.sender
33 self.unlocked = True
With a @payable decorator on the constructor, the contract creator will be
required to make an initial deposit equal to twice the item’s value to
initialize the contract, which will be later returned. This is in addition to
the gas fees needed to deploy the contract on the blockchain, which is not
returned. We assert that the deposit is divisible by 2 to ensure that the
seller deposited a valid amount. The constructor stores the item’s value
in the contract variable self.value and saves the contract creator into
self.seller. The contract variable self.unlocked is initialized to
True.
35@external
36def abort():
37 assert not self.finalized
38 assert self.unlocked #Is the contract still refundable?
39 assert msg.sender == self.seller # Only the seller can refund
40 # his deposit before any buyer purchases the item.
41 self.finalized = True
42 assert self.balance > 0 and self.balance == 2 * self.value
43 send(self.seller, self.balance)
The abort() method is a method only callable by the seller and while the
contract is still unlocked—meaning it is callable only prior to any buyer
making a purchase. As we will see in the purchase() method that when
a buyer calls the purchase() method and sends a valid amount to the contract,
the contract will be locked and the seller will no longer be able to call
abort().
When the seller calls abort() and if the assert statements pass, the
contract sends the balance back to the seller, effectively canceling the sale.
45@external
46@payable
47def purchase():
48 assert not self.finalized
49 assert self.unlocked # Is the contract still open (is the item still up
50 # for sale)?
51 assert msg.value == (2 * self.value) # Is the deposit the correct value?
52 self.buyer = msg.sender
53 self.unlocked = False
Like the constructor, the purchase() method has a @payable decorator,
meaning it can be called with a payment. For the buyer to make a valid
purchase, we must first assert that the contract’s unlocked property is
True and that the amount sent is equal to twice the item’s value. We then
set the buyer to the msg.sender and lock the contract. At this point, the
contract has a balance equal to 4 times the item value and the seller must
send the item to the buyer.
55@external
56def received():
57 # 1. Conditions
58 assert not self.finalized
59 assert not self.unlocked # Is the item already purchased and pending
60 # confirmation from the buyer?
61 assert msg.sender == self.buyer
62 assert not self.ended
63
64 # 2. Effects
65 self.ended = True
66 self.finalized = True
67
68 # 3. Interaction
69 send(self.buyer, self.value) # Return the buyer's deposit (=value) to the buyer.
70 assert self.balance == 3 * self.value
71 send(self.seller, self.balance) # Return the seller's deposit (=2*value) and the
72 # purchase price (=value) to the seller.
Finally, upon the buyer’s receipt of the item, the buyer can confirm their
receipt by calling the received() method to distribute the funds as
intended—where the seller receives 3/4 of the contract balance and the buyer
receives 1/4.
By calling received(), we begin by checking that the contract is indeed
locked, ensuring that a buyer had previously paid. We also ensure that this
method is only callable by the buyer. If these two assert statements pass,
we refund the buyer their initial deposit and send the seller the remaining
funds, completing the transaction.