Learn Solidity (3) — Safe Remote Purchase

Solidity学习笔记(3) — Safe Remote Purchase

作者:孔令坤,转载请注明出处

今天用拍卖合约(Safe Remote Purchase)进行Smart Contract编程的学习。

今天的这一段代码,有一些比较tricky的地方,所以我的笔记也会比较详细。

附上官方的学习代码链接:https://solidity.readthedocs.io/en/latest/solidity-by-example.html#safe-remote-purchase

另外发现了一个不错的大纲型文档:https://medium.com/@mattcondon/getting-up-to-speed-on-ethereum-63ed28821bbe

1.功能简介

这个合约作为一个第三方的平台,用于远程购买一个商品时暂存买方付的钱,然后在买方确认交易后将钱付给商家。整体而言这个合约相当于我们用淘宝网购时的淘宝平台,用户购买时将钱先付给淘宝平台,然后商家发货。用户在收到商品后在淘宝上确认付款,这时淘宝会把钱打给商户。

在使用这个合约时,卖方需要先自己创建(create)合约。整个流程与下图类似:

2.代码简介

初始函数:首先,我们看建立合约的初始函数,根据函数中的payable,说明这里发起协议的seller需要在transaction里面放价格为商品两倍的保证金。这里是整个合约最tricky的地方,即seller放了商品价格两倍的保证金。这样有两个目的,其一可以防止黑心商家,发起协议后不给用户发货,用户在确认后白白损失金额。其二可以让买方积极的去确认收到商品了,因为买方也要交商品价格两倍的保证金,如果不确认收到商品,他们将白白损失钱财。

1
2
3
4
5
6
7
8
enum State { Created, Locked, Inactive }
State public state;
// 这一段用require代码确认seller的value是偶数
function Purchase() public payable {
seller = msg.sender;
value = msg.value / 2;
require((2 * value) == msg.value);
}

其实这么写有浪费gas的问题。因为如果不是偶数,之前的语句已经执行过了,require导致函数回退时将会消耗更多的gas。所以我觉得这一段可以改成:

1
2
3
4
5
function Purchase() public payable {
seller = msg.sender;
require((msg.value % 2) == 0);
value = msg.value / 2;
}

另外神奇的是,在创建合约的函数里面,合约并没有写state = State.Created;然而在下面的abort()函数中,inState(State.Created)的判定竟然通过了。经过代码调试,我发现在State public state;这句话的时候,虽然没有直接赋值,但是系统默认state的值为空,在做==判定的时候,默认state = 0了。总之之后编程需要多多小心。PS. 我这里在调试的时候,加了一个enum State { Created, Locked, Inactive }然后创建了一个2eth的合约。于是我永久的丢掉了2eth。

终止合约:在终止函数里,我想提一下transfersend函数的区别。两者都是像地址传递钱,区别在于如果transfer函数运行失败,系统会throw一个错误,而send函数则会返回一个false的布尔值。

1
2
3
4
5
6
7
8
9
10
function abort()
public
onlySeller
inState(State.Created)
{
Aborted();
state = State.Inactive;
// 这里把seller的保证金还给seller,里面应该是2V - gas fee
seller.transfer(this.balance);
}

确认支付: 这个函数由buyer在seller发起的协议基础上访问,会在协议里存两倍商品价格的钱,存完后会将Contract的状态标记为锁定, i,e., Locked。至于为何要付两倍的钱,我认为可能是因为这个协议担心用户会懒得确认到货,所以这么做来使得用户自己能够被

1
2
3
4
5
6
7
8
9
10
function confirmPurchase()
public
inState(State.Created)
condition(msg.value == (2 * value))
payable
{
PurchaseConfirmed();
buyer = msg.sender;
state = State.Locked;
}

确认收货: 这个函数由buyer在seller发起的协议基础上访问,buyer确认收到商品,这样之后协议会把钱退回给buyer和seller。需要注意的是,这里官方代码认为所有的交易费用应该由seller承担。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function confirmReceived()
public
onlyBuyer
inState(State.Locked)
{
ItemReceived();
// It is important to change the state first because
// otherwise, the contracts called using `send` below
// can call in again here.
state = State.Inactive;
// 返回用户多余的押金,一倍商品价格
buyer.transfer(value);
// 把剩下的 3V - gas fee的钱给商家。
seller.transfer(this.balance);
}

3.一些思考

在看代码的过程中,我发现这个合约有一个无法解决的问题。

假如有一些恶意的用户想搞垮商家,于是买了商品后就是不确认,这样用户通过损失等价于一个商品价格的费用(因为用户其实收到了商品),就可以坑掉商家三倍商品价格的钱(商家发出了商品)。

其实这个问题的本质源于smart contract无法监管商品的运转,即它无法发现假如钱被锁死在了合约里面,是谁干了坏事。所以我觉得就remote purchace这个问题而言,比较理想的状况还是得有一个买卖双方都信的过的第三方机构介入其中。buyer先把钱寄给这个第三方机构,然后由第三方机构作为仲裁方,决定把钱最终交给buyer还是seller。