一、合约详情
继续官网合约示例的学习——安全的远程购买合约learnblockchain.cn/docs/solidi… 参考版本:0.8.17
目前来说,我们最熟悉的远程购买场景应该就是通过电商购买的场景了,就是一个很典型的买家和卖家相互独立,且需要远程进行交易过程的。这个过程中最需要解决的就是“信任”问题,因为远程,因此无法做到一手交钱一手交货。在我们熟悉的电商场景中,最初是支付宝作为一个值得信任的第三方支付担保平台把这个“信任”场景解决了,用户购买商品的钱,不是直接打款给商家,而是先转给这个第三方支付担保平台,当用户收到货品后确认收货,支付宝才会把应付款转给商家。
我们可以很自然的联想到把上述支付宝承担的角色转移给智能合约,在商家发货和用户收货中间这段时间,资金会存在智能合约里,一旦一方作假或违约,将会受到资金损失,从而促使买卖双方都遵循约定履行责任。
合约的主要设计逻辑为:
- 卖家部署合约,并传入2*value,初始化合约的状态控制变量(这里没有明文写,可能enum数组默认取第一个元素吧)
- 买家购买时,也传入2*value
- 买家确认收货时,可以得到返还的1个value
- 等到买家收到货品之后,卖家一共可以取得3个value
合约代码如下(官网示例):
// 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 }
State public state;
/// 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 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();
event SellerRefunded();
//确保 `msg.value` 是一个偶数。
//如果它是一个奇数,则它将被截断。
//通过乘法检查它不是奇数。
constructor() payable {
seller = payable(msg.sender);
value = msg.value / 2;
if ((2 * value) != msg.value)
revert ValueNotEven();
}
///中止购买并回收以太币。
///只能在合约被锁定之前由卖家调用。
function abort()
external
onlySeller
inState(State.Created)
{
emit Aborted();
state = State.Inactive;
seller.transfer(address(this).balance);
}
/// 买家确认购买。
/// 交易必须包含 `2 * value` 个以太币。
/// 以太币会被锁定,直到 confirmReceived 被调用。
function confirmPurchase()
external
inState(State.Created)
condition(msg.value == (2 * value))
payable
{
emit PurchaseConfirmed();
buyer = payable(msg.sender);
state = State.Locked;
}
/// 确认你(买家)已经收到商品。
/// 这会释放被锁定的以太币。
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);
}
}
二、用例设计
我仍将进行一组简单的测试用例设计,用来调用此合约,并验证合约功能的正确性:
- B部署合约后,此时合约状态为Created,A确认购买时传入2倍val,此时合约的状态应该为Locked,A的账户余额减少2个val。然后进行确认收货,此时合约的状态应该为Release,A的账户余额增加1个val。B再进行提款操作,此时合约的状态为Inactive,B的账户增加3个val
- B部署合约后,此时合约状态为Created,再调用abort()函数,合约状态变为Inactive
- B部署合约后,此时合约状态为Created,A确认购买时传入2倍val,此时合约的状态应该为Locked,A的账户余额减少2个val。此时B进行提款操作,提款失败返回Invalid state
- ……(可补充更多用例)
针对上述第一个用例,用hardhat写一个例子:
const { ethers } = require("hardhat");
const { expect, assert, AssertionError } = require("chai");
const hre = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { changeEtherBalance } = require("@nomicfoundation/hardhat-chai-matchers");
const { isCallTrace } = require("hardhat/internal/hardhat-network/stack-traces/message-trace");
async function beforeTest(){
const [a, b] = await ethers.getSigners();
const val=2000;
const purchase = await hre.ethers.getContractFactory("Purchase");
return{purchase,a,b,val}
}
describe("Purchase的测试用例集", function () {
it("B部署合约后,此时合约状态为Created,A确认购买时传入2倍val,此时合约的状态应该为Locked,A的账户余额减少2个val。然后进行确认收货,此时合约的状态应该为Release,A的账户余额增加1个val。B再进行提款操作,此时合约的状态为Inactive,B的账户增加3个val", async function () {
const {purchase,a,b,val}= await loadFixture(beforeTest);
const purchaseInstance=await purchase.connect(b).deploy({value:val*2});
assert.equal(await purchaseInstance.state(),0);
await expect (purchaseInstance.connect(a).confirmPurchase({value:val*2})).to.changeEtherBalance(a,-val*2);
assert.equal(await purchaseInstance.state(),1);
await expect (purchaseInstance.connect(a).confirmReceived()).to.changeEtherBalance(a,val);
assert.equal(await purchaseInstance.state(),2);
await expect (purchaseInstance.connect(b).refundSeller()).to.changeEtherBalance(b,val*3);
assert.equal(await purchaseInstance.state(),3);
});
});
三、实战经验
- 合约部署时可传入value,了解到合约部署时也是和其他任何交易一样,可以传入{value:xxx}的,比如本例中用到的:
await purchase.connect(b).deploy({value:val*2});
- 合约中状态变量的定义为:
enum State { Created, Locked, Release, Inactive }
State public state;
合约的构造函数定义为:
constructor() payable {
seller = payable(msg.sender);
value = msg.value / 2;
// state = State.Created;
if ((2 * value) != msg.value)
revert ValueNotEven();
}
在部署合约时,应该就为合约状态赋值为Created,而本例中构造函数在没有显式赋值的情况下,部署完成后合约的state也是Created。说明定义了一个enum的State,这个State的实例默认值为第一个枚举元素。
- 在对enum类型变量进行断言时,一开始误以为其值应该是枚举值的字符串,实际上它的值不是enum本身这个字符串,而是在enum数组中的位置比如,Created的值就是0,用console.log去打印State实例的值时,得到的也是它在enum数组中的位置。