学校期末web作业,要实现一个简单的网上购书平台,写到一半的时候发现支付这一块可以借助智能合约来完成。虽然接触过一点区块链的知识,但还没有动手写过智能合约,于是匆忙学习了一下Solidity和Smart Contract的基础,开始编写我的第一个智能合约,顺便记录一下学习历程。
先看下官网给的一个例子 https://solidity.readthedocs.io/en/develop/solidity-by-example.html#safe-remote-purchase
pragma solidity >=0.4.22 <0.6.0;
contract Purchase {
uint public value;
address payable public seller;
address payable public buyer;
enum State { Created, Locked, Inactive }
State public state;
// 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() public payable {
seller = msg.sender;
value = msg.value / 2;
require((2 * value) == msg.value, "Value has to be even.");
}
modifier condition(bool _condition) {
require(_condition);
_;
}
modifier onlyBuyer() {
require(
msg.sender == buyer,
"Only buyer can call this."
);
_;
}
modifier onlySeller() {
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
modifier inState(State _state) {
require(
state == _state,
"Invalid state."
);
_;
}
event Aborted();
event PurchaseConfirmed();
event ItemReceived();
/// 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;
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 = 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.Inactive;
// NOTE: This actually allows both the buyer and the seller to
// block the refund - the withdraw pattern should be used.
buyer.transfer(value);
seller.transfer(address(this).balance);
}
}
合约存在的问题
首先卖家创建合约并交两倍原价的押金,此时合约状态为Created,卖家可以执行abort取消贩卖退回押金。买家支付两倍原价进行购买,其中一倍作押金。确认收货后,各自退回押金,商品的钱交给卖家。
合约原理很简单,但存在一个很大的问题,一份合约只能用于一次交易,如果一个商品库存有100个,卖家就要去部署100次这样的合约,不仅麻烦还难于管理。
其次,合约试图用押金来约束买家和卖家的行为,但实际上,如果买家故意不确认收货,卖家会比买家更亏,也就是说没有做到平衡。比如商品价格为p,卖家支付2p的押金,发货后等于暂时失去了3p的资金;买家支付2p的押金,得到了价格为p的商品,他等于只失去了价值为p的资金。这时买家相比卖家是赚的,他有理由恶意拒绝确认。
主要解决思路
问题的解决参考了下面两篇文章(要翻墙)
https://medium.com/coinmonks/creating-smart-contracts-with-smart-contract-d54e21d26e00
https://medium.com/coinmonks/escrow-service-as-a-smart-contract-the-business-logic-5b678ebe1955
- 利用合约来部署合约。一类商品可以产生多个订单,卖家只需要部署每一类商品的合约;买家通过调用商品合约来创建订单合约,订单合约的地址记录在商品合约中。
- 增加发货和收货期限(通过记录时间戳实现)来约束买家和卖家;
(以下的代码参考原文章的代码,修改了很大一部分)
pragma solidity^0.4.22;
// 商品合约
contract Product {
bool public enable; // 当前商品是否有效
address public sellerAddr; // 卖家的地址
uint public price; // 商品的单价
uint public commitTime; // 卖家承诺发货时间
address[] public orders; // 所有订单的地址
address public lastOrder; // 最新订单的地址
modifier onlySeller() {
require(
sellerAddr == msg.sender,
"卖家账号地址错误!"
);
_;
}
constructor(bool _enable, uint _price, uint _commitTime)
public
{
require(
_price != 0 && _commitTime != 0,
"参数信息不完整"
);
sellerAddr = msg.sender;
enable = _enable;
price = _price;
commitTime = _commitTime;
}
function setEnable(bool _enable)
public
onlySeller
{
enable = _enable;
}
function setPrice(uint _price)
public
onlySeller
{
price = _price;
}
function setCommitTime(uint _commitTime)
public
onlySeller
{
commitTime = _commitTime;
}
function getOrderCount()
public
view
returns(uint _count)
{
return orders.length;
}
// 买家创建订单,输入购买数量和两倍原价的msg.value
event NewOrder(address _orderAddr);
function newOrder(uint amount)
public
payable
{
require(
enable == true,
"产品当前无法下单"
);
require(
amount != 0,
"购买数量不能为0"
);
Order order = (new Order).value(msg.value)(msg.sender, sellerAddr, price * amount, commitTime);
orders.push(order);
lastOrder = order;
emit NewOrder(order);
}
}
// 订单合约
contract Order {
// 已支付,已接单,已发货,已签收,已收货,已失效
enum Status {
Paid, Taken, Shipped, Signed, Received, Inactive
}
address public buyerAddr; // 买家的地址
address public sellerAddr; // 卖家的地址
uint public price; // 商品的总价
uint public commitTime; // 卖家承诺发货时间
Status public status; // 订单的状态
uint public createTime; // 订单的创建(支付)时间
uint public signTime; // 订单的签收时间
string public trackingNumber; // 订单的物流号
uint8 public score; // 订单的评分
string public assession; // 订单的评价
modifier inStatus(Status _status) {
require(
status == _status,
"订单的状态不可操作"
);
_;
}
modifier onlyBuyer() {
require(
buyerAddr == msg.sender,
"买家账号地址错误!"
);
_;
}
modifier onlySeller() {
require(
sellerAddr == msg.sender,
"卖家账号地址错误!"
);
_;
}
function getBalance()
public
view
returns(uint _balance)
{
return address(this).balance;
}
constructor(address _buyerAddr, address _sellerAddr, uint _price, uint _commitTime)
public
payable
{
require(
msg.value == _price * 2,
"买家需要额外支付与商品价格等价的金额作为押金"
);
buyerAddr = _buyerAddr;
sellerAddr = _sellerAddr;
price = _price;
commitTime = _commitTime;
status = Status.Paid;
createTime = now;
}
// 卖家接单之前用户可以取消购买
function abort()
public
onlyBuyer
payable
inStatus(Status.Paid)
{
status = Status.Inactive;
buyerAddr.transfer(price * 2);
}
// 卖家选择接单或拒绝接单
function take(bool _takeOrder)
public
onlySeller
payable
inStatus(Status.Paid)
{
if (_takeOrder) {
require(
msg.value == price,
"卖家需要支付与商品价格等价的金额作为押金"
);
status = Status.Taken;
}
else {
status = Status.Inactive;
buyerAddr.transfer(price * 2);
}
}
// 买家检查卖家是否没按时发货,如果是则退还买家钱,同时作为惩罚,卖家的押金被锁定在合约里了
function checkLate()
public
onlyBuyer
inStatus(Status.Taken)
{
require(
now - createTime > commitTime,
"尚未到卖家发货截止期限"
);
status = Status.Inactive;
buyerAddr.transfer(price * 2);
}
// 卖家输入运单号,确认发货
function ship(string _trackingNumber)
public
onlySeller
payable
inStatus(Status.Taken)
{
// todo: 检查运单号是否真实存在
status = Status.Shipped;
trackingNumber = _trackingNumber;
sellerAddr.transfer(price); // 卖家发货后退回押金
}
// 确认签收
function sign()
public
inStatus(Status.Shipped)
{
// todo: 通过运单号查询物流信息,判断是否签收并获取签收时间
status = Status.Signed;
// signTime = getSignTime();
signTime = now; // 测试用
}
// 买家确认收货
function receive()
public
onlyBuyer
payable
inStatus(Status.Signed)
{
status = Status.Received;
buyerAddr.transfer(price);
sellerAddr.transfer(price);
}
// 过了10天买家没确认收货,则卖家可以自己确认
function confirmBySeller()
public
onlySeller
payable
inStatus(Status.Signed)
{
require(
now - signTime > 10 days,
"卖家超过10天才可以确认"
);
status = Status.Received;
buyerAddr.transfer(price);
sellerAddr.transfer(price);
}
// 买家对订单进行评价
function assess(uint8 _score, string _assession)
public
onlyBuyer
inStatus(Status.Received)
{
require(
_score >= 1 && _score <= 5,
"评分只能是1~5之间的整数"
);
score = _score;
assession = _assession;
status = Status.Inactive;
}
}
合约整体流程
- 首先卖家部署商品合约,声明自己的账户地址、商品的价格、承诺的发货时间。
- 买家通过商品合约部署订单合约(注意订单合约的msg.sender是商品合约的地址)支付商品价格p+押金p,买家等待卖家接单,期间可以取消购买,退还2p。
- 卖家如果接单,需要交付价值为p的押金,同时要在自己承诺的发货时间内发货。如果拒绝接单,则合约将钱退还买家。
- 如果卖家没能在承诺时间发货,买家可以调用checkLate,退回押金,同时作为惩罚,卖家的押金将无法取回(可能有方法取回?)。
- 卖家确认发货时要填写运单(物流)号,为了验证这个订单号,需要物流公司接入以太坊并提供接口,这里就不模拟了,直接跳过验证。同样,签收的过程也偷懒省略了。
- 接着买家确认收货,为了防止恶意不确认或买家忘记确认,签收10天后卖家可以自己确认。
- 最后买家对订单进行评价。合约没有实现退货等功能。
代码在Remix上可以测试,在前端调用合约时用web3.js。
最后,我自己也是刚入门智能合约,这个代码的逻辑上和功能上肯定还存在许多漏洞,所以仅供参考。