Skip to main content

远程购买

pragma solidity ^0.4.22;

contract Purchase {
uint public value;
address public seller;
address public buyer;
enum State { Created, Locked, Inactive }
State public state;

//确保 `msg.value` 是一个偶数。
//如果它是一个奇数,则它将被截断。
//通过乘法检查它不是奇数。
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();

///中止购买并回收以太币。
///只能在合约被锁定之前由卖家调用。
function abort()
public
onlySeller
inState(State.Created)
{
emit Aborted();
state = State.Inactive;
seller.transfer(address(this).balance);
}

/// 买家确认购买。
/// 交易必须包含 `2 * value` 个以太币。
/// 以太币会被锁定,直到 confirmReceived 被调用。
function confirmPurchase()
public
inState(State.Created)
condition(msg.value == (2 * value))
payable
{
emit PurchaseConfirmed();
buyer = msg.sender;
state = State.Locked;
}

/// 确认你(买家)已经收到商品。
/// 这会释放被锁定的以太币。
function confirmReceived()
public
onlyBuyer
inState(State.Locked)
{
emit ItemReceived();
// 首先修改状态很重要,否则的话,由 `transfer` 所调用的合约可以回调进这里(再次接收以太币)。
state = State.Inactive;

// 注意: 这实际上允许买方和卖方阻止退款 - 应该使用取回模式。
buyer.transfer(value);
seller.transfer(address(this).balance);
}
}

这个智能合约是一个用于远程购买和销售的合约,其中涉及的主要参与者是卖家和买家。合约通过枚举State来跟踪交易的不同阶段,包括创建(Created)、锁定(Locked)和非激活(Inactive)状态。合约确保在每一个状态只能执行特定的操作,并且通过事件通知外界关键的合约活动。以下是合约的关键部分及其功能的解释:

  1. 构造函数(constructor)

    • 在部署合约时被调用,设置卖家地址为消息发送者(msg.sender)。
    • 合约需要发送偶数数量的以太币,因为value被设置为发送金额的一半。如果发送了奇数数量的以太币,会因为require语句而失败。
  2. 修饰符(Modifiers)

    • condition:确保特定的条件为真。
    • onlyBuyer:只允许买家执行。
    • onlySeller:只允许卖家执行。
    • inState:确保合约处于特定的状态。
  3. 事件(Events)

    • Aborted:当交易被卖家中止时触发。
    • PurchaseConfirmed:当买家确认购买时触发。
    • ItemReceived:当买家确认收到商品时触发。
  4. 函数

    • abort:只能在合约创建状态且由卖家调用,允许卖家中止交易并取回所有以太币。
    • confirmPurchase:在合约创建状态下,买家可以通过发送2 * value的以太币来确认购买,合约状态转变为锁定状态。
    • confirmReceived:只能在合约锁定状态且由买家调用,买家确认收到商品,释放锁定的以太币,合约状态转变为非激活状态。买家和卖家分别收到他们应得的金额。

confirmReceived函数中,代码首先更新状态变量state,这是一个遵循"条件-影响-交互"模式的例子。通过先修改状态,即使在执行transfer调用的过程中合约被重新调用,也不会导致资金被错误地再次发送。然而,注释中提到了一个潜在的问题,即如果买家或卖家在transfer调用中抛出异常,可能会阻止资金的退款。这是因为在 Solidity 的早期版本中(如 0.4.22),transfer方法会在发送失败时抛出异常,导致整个交易被回滚。

为了避免这个问题,通常建议使用"取回模式"(也称为"提取模式"或"取款模式"),即不直接在合约函数中发送以太币,而是允许买家和卖家通过调用另一个函数来提取他们的资金。这可以防止因为一个参与者的拒绝接受转账而导致整个交易失败的情况。

改进

以下是修改后的完整智能合约代码,使用取回模式来处理买家和卖家的资金:

pragma solidity ^0.4.22;

contract Purchase {
uint public value;
address public seller;
address public buyer;
enum State { Created, Locked, Inactive }
State public state;

// 使用映射来记录买家和卖家的退款金额
mapping(address => uint) public refunds;

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();

/// 中止购买并回收以太币。
/// 只能在合约被锁定之前由卖家调用。
function abort() public onlySeller inState(State.Created) {
emit Aborted();
state = State.Inactive;
refunds[seller] += address(this).balance;
}

/// 买家确认购买。
/// 交易必须包含 `2 * value` 个以太币。
function confirmPurchase() public inState(State.Created) condition(msg.value == (2 * value)) payable {
emit PurchaseConfirmed();
buyer = msg.sender;
state = State.Locked;
}

/// 确认你(买家)已经收到商品。
/// 这会允许买家和卖家取回他们的以太币。
function confirmReceived() public onlyBuyer inState(State.Locked) {
emit ItemReceived();
state = State.Inactive;
refunds[buyer] += value;
refunds[seller] += (address(this).balance - value);
}

/// 提取退款,买家和卖家都可以调用
function withdrawRefund() public {
uint refundAmount = refunds[msg.sender];
require(refundAmount > 0, "No refund available.");

refunds[msg.sender] = 0;
msg.sender.transfer(refundAmount);
}
}

这个合约现在使用取回模式来处理资金。在abortconfirmReceived函数中,资金不再被直接发送,而是记录在refunds映射中。买家和卖家可以通过调用withdrawRefund函数来提取他们的资金。这种模式增加了合约的安全性,因为它避免了直接在交易函数中发送以太币可能引发的问题,比如重入攻击。