智能合约之支付(payment)合约解读

payment

Posted by 土猪 on August 23, 2020

最近一股冷空气从南极吹到了墨尔本,整个周末,狂风暴雨,没完没了,狂风中还隐隐约约听到龙吟之声,暴雨还导致家里一扇窗户都漏水了,紧急联系了builder,他说下周来修,这个四级封锁,每天200多人感染的时候叫人过来干活,心中怯怯焉。现在反正也出不去,没啥事情好做,在油管上把鬼吹灯系列都看完了,在孩子的要求下,购买了netfix,最后连电影都看腻了,整个人觉得无比惆怅,只好来研究下智能合约,聊以度日。

image.png

我要分析的是‘openzeppelin-solidity’这个库里面的’payment’文件夹下的内容,这里聚集了几乎所有跟众筹中支付相关的项目文件。 先看看‘PaymentSplitter.sol‘这个合约,这个合约非常适合分赃,比如,几个朋友一起搞ICO众筹资金,收款账号不是一个以太坊的外部账户(就是钱包里通过私钥控制的账户),而是一个基于PaymentSplitter合约的智能合约,通过这个合约把收到的以太坊再次分配给各个外部账户,由于每个人贡献不一样,所以分钱多少也不一样,通过这个合约中的’share’来设定。

这个合约在初始化时候,就有一个分赃账户的地址和份额的设定:

constructor (address[] memory payees, uint256[] memory shares) public payable {
        // solhint-disable-next-line max-line-length
        require(payees.length == shares.length, "PaymentSplitter: payees and shares length mismatch");
        require(payees.length > 0, "PaymentSplitter: no payees");

        for (uint256 i = 0; i < payees.length; i++) {
            _addPayee(payees[i], shares[i]);
        }
    }

当然,如果有更多人参与项目中来,并且希望成为‘股东之一来分钱,这个合约还有个方法:

function _addPayee(address account, uint256 shares_) private {
        require(account != address(0), "PaymentSplitter: account is the zero address");
        require(shares_ > 0, "PaymentSplitter: shares are 0");
        require(_shares[account] == 0, "PaymentSplitter: account already has shares");

        _payees.push(account);
        _shares[account] = shares_;
        _totalShares = _totalShares.add(shares_);
        emit PayeeAdded(account, shares_);
    }

我们注意到,这个方法是一个private的,意味着不是外部可以直接调用的,那么就需要继承这个合约的继承者来调用,当然,调用者要通过modifier来满足各种规则的限制,比如必须是合约所有者,或者几个人都同意才行,你想加入,可不是随便能加入的。

再看看这个合约中另外一个最重要的函数—release,这是个需要外部调用的函数,在收到以太后,大伙分赃的时候,需要自己主动调用这个函数,才能把钱分到自己手里,以太坊的智能合约,在钱的问题上,一般都采用pull,而是push的方法,意思是不会在收款函数中主动调用分钱函数,把钱分到股东手里,而是要自己去调用收钱函数,这主要是为了防止我之前提起过的重入攻击(re-entry),如果你在这里主动发钱,对方收款账户很有可能是一个智能合约地址,坏人就可以通过智能合约,再次调用你的发钱函数,或者block在那里,让你难受,你就需要额外小心。所以不如这样,你要钱,自己来收。

function release(address payable account) public {
        require(_shares[account] > 0, "PaymentSplitter: account has no shares");

        uint256 totalReceived = address(this).balance.add(_totalReleased);
        uint256 payment = totalReceived.mul(_shares[account]).div(_totalShares).sub(_released[account]);

        require(payment != 0, "PaymentSplitter: account is not due payment");

        _released[account] = _released[account].add(payment);
        _totalReleased = _totalReleased.add(payment);

        account.transfer(payment);
        emit PaymentReleased(account, payment);
    }

在钱的问题上,还有个需求,就是需要个地方把钱托管起来,以后想要就能拿出来,于是有了’Escrow.sol‘ 托管协议,它继承了ownership文件夹下的’Secondary.sol’合约,我看了一下,这个合约的主要好处就是可以随便转移合约所有权。

function transferPrimary(address recipient) public onlyPrimary {
        require(recipient != address(0), "Secondary: new primary is the zero address");
        _primary = recipient;
        emit PrimaryTransferred(_primary);
    }
}

托管嘛,简而言之,就是解决两个问题,一个是存钱,一个是取钱,请看存钱函数:

function deposit(address payee) public onlyPrimary payable {
        uint256 amount = msg.value;
        _deposits[payee] = _deposits[payee].add(amount);

        emit Deposited(payee, amount);
    }

其实它就是对‘谁’存了多少钱做了一个记录而已,真正收到的钱(就是以太)是自动被这个合约锁定了,在这个合约账户名下。

再看看取函数:

function withdraw(address payable payee) public onlyPrimary {
        uint256 payment = _deposits[payee];

        _deposits[payee] = 0;

        payee.transfer(payment);

        emit Withdrawn(payee, payment);
    }

看到这里,我不禁奇了,收款的fallback函数呢?这个需要在继承者那里或者调用这个合约的合约那里的fallback实现,如果外面有以太被转移给这个合约,我们在fallback里调用这个deposit函数就可以记录什么人送来了多少钱。同时我们还看到,withdraw和deposit都是需要宿主权限才能做的事情,所以,我们要么象最后说的PullPayment合约那样,创建一个Escrow合约的实例,要么继承这个Escrow合约,那么就自动有了宿主的权限。

这时候我突然想起了我那个“去中心化支付宝或者paypal”,完全可以使用这个合约来处理,在合约中放个时间变量,然后在withdraw里放个对时间的判断,就可以实现定时取款的功能了。以太坊的智能合约不会主动在timeout的时候做些事情,必须被动调动函数,在函数里判断是否到期可以执行接下来的步骤。

看完了Escrow托管合约,再看看继承了它的另外一个合约:ConditionalEscrow,顾名思义,就是满足一定条件的托管协议,在满足一定条件下取款,这个主要是这个函数来做的:

function withdrawalAllowed(address payee) public view returns (bool);

但是我们看到,这个函数并没有任何具体的实现,所以这就为继承这个合约的合约提供了无比巨大的想象空间。请看,它真的是啥也没做,就提供个接口而已:

function withdraw(address payable payee) public {
        require(withdrawalAllowed(payee), "ConditionalEscrow: payee is not allowed to withdraw");
        super.withdraw(payee);
    }

这个合约就这么一点,扫过就算了,看托管合约文件夹下最后一个合约:RefundEscrow,这个玩意估计应用很多,特别在ICO众筹中,有了这么个合约,就能保证国家一声令下,圈钱的人能相应号召,把钱还给投资人,避免了坐牢的危险;还可以在众筹失败后,良心发现,主动把钱还给投资人。

这个合约很有意思,在一开始定义了个枚举变量:

enum State { Active, Refunding, Closed }

这就决定了在托管的不同状态,可以做不同事情,托管协议生效,就是初始化状态“Active”,在此状态下,可以存钱,可以关闭托管,托管结束就是“Close”,或者把状态改成“Refunding”;在“Close”状态下,受益人才可以把钱取出来:

function beneficiaryWithdraw() public {
        require(_state == State.Closed, "RefundEscrow: beneficiary can only withdraw while closed");
        _beneficiary.transfer(address(this).balance);
    }

等等,说好的可以refund呢?咋在这里看不到一个refund的函数呢?开始我也纳闷,这也太黑了吧,但是仔细一看,它实现了这个函数:

function withdrawalAllowed(address) public view returns (bool) {
        return _state == State.Refunding;
    }

因为合约RefundEscrow 继承了合约ConditionalEscrow,所以也就继承了这个函数,那么调用这个函数,投资人就可以把钱取回去啦:

function withdraw(address payable payee) public {
        require(withdrawalAllowed(payee), "ConditionalEscrow: payee is not allowed to withdraw");
        super.withdraw(payee);
    }

前提是合约的状态被置成”Refunding”:

 function enableRefunds() public onlyPrimary {
        require(_state == State.Active, "RefundEscrow: can only enable refunds while active");
        _state = State.Refunding;
        emit RefundsEnabled();
    }

最后来看看这个合约:PullPayment.sol,这个合约就是创建了一个Escrow的合约实例,然后包装一下它的函数:

 Escrow private _escrow;

    constructor () internal {
        _escrow = new Escrow();
    }

别的没啥特别好说的,就是这个内部函数,结合Escrow的那个被调用函数,这个调用方式真是solidity独特的:

function _asyncTransfer(address dest, uint256 amount) internal {
        _escrow.deposit.value(amount)(dest);
    }
function deposit(address payee) public onlyPrimary payable {
        uint256 amount = msg.value;
        _deposits[payee] = _deposits[payee].add(amount);

        emit Deposited(payee, amount);
    }

看完和理解这些合约后,以后关于支付方面的智能合约,可以不用自己从头造轮子了,完全可以继承或者生成以上这些合约。比如如果搞去中心化支付宝,支付一块就可以继承“ConditionalEscrow”这个合约,重写“withdrawalAllowed“这个函数,增加一个时间的变量,说明什么时间到期,每次调用取钱函数时候,都判断一下当前时间跟设置那个时间,这样就解决第三方资金托管和到期支付的功能了。

相关文章阅读:


支付宝打赏

您的打赏是对我最大的鼓励!