3分钟Solidity: 9.9 双向支付通道

25 阅读5分钟

欢迎订阅专栏3分钟Solidity--智能合约--Web3区块链技术必学

如需获取本内容的最新版本,请参见 Cyfrin.io 的双向支付渠道(代码示例)

双向支付通道允许参与者AliceBob在链下反复转移以太币。

支付可以是双向的,Alice支付给BobBob也可以支付给Alice

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "./ECDSA.sol";

/* 
开启通道
1.  Alice和Bob共同注资一个多重签名钱包
2.  预先计算支付通道地址
3.  双方交换初始余额的签名
4.  创建可从多重签名钱包部署支付通道的交易

更新通道余额
1.  重复开启通道的步骤1至3
2.  从多重签名钱包创建一笔交易,该交易将:
    -   删除原本会部署旧支付通道的交易
    -   然后创建一笔可以部署具有新余额支付通道的交易

当Alice和Bob就最终余额达成一致时关闭通道
1.  从多重签名钱包创建一笔交易,该交易将:
    -   向Alice和Bob发送付款
    -   然后删除本可以创建支付通道的交易

当Alice和Bob对最终余额无法达成一致时关闭通道
1.  从多重签名部署支付通道
2.  调用challengeExit()以启动关闭通道的流程
3.  通道到期后,Alice和Bob可以提取资金
*/

contract BiDirectionalPaymentChannel {
    using ECDSA for bytes32;

    event ChallengeExit(address indexed sender, uint256 nonce);
    event Withdraw(address indexed to, uint256 amount);

    address payable[2] public users;
    mapping(address => bool) public isUser;

    mapping(address => uint256) public balances;

    uint256 public challengePeriod;
    uint256 public expiresAt;
    uint256 public nonce;

    modifier checkBalances(uint256[2] memory _balances) {
        require(
            address(this).balance >= _balances[0] + _balances[1],
            "合约余额必须>=用户总余额"
        );
        _;
    }

    // NOTE: deposit from multi-sig wallet
    constructor(
        address payable[2] memory _users,
        uint256[2] memory _balances,
        uint256 _expiresAt,
        uint256 _challengePeriod
    ) payable checkBalances(_balances) {
        require(_expiresAt > block.timestamp, "过期时间必须>当前时间");
        require(_challengePeriod > 0, "Challenge period must be > 0");

        for (uint256 i = 0; i < _users.length; i++) {
            address payable user = _users[i];

            require(!isUser[user], "用户必须是唯一的");
            users[i] = user;
            isUser[user] = true;

            balances[user] = _balances[i];
        }

        expiresAt = _expiresAt;
        challengePeriod = _challengePeriod;
    }

    function verify(
        bytes[2] memory _signatures,
        address _contract,
        address[2] memory _signers,
        uint256[2] memory _balances,
        uint256 _nonce
    ) public pure returns (bool) {
        for (uint256 i = 0; i < _signatures.length; i++) {
            /*
            NOTE: 签署包含本合同地址的签名,以防止对其他合约的重放攻击
            */
            bool valid = _signers[i]
                == keccak256(abi.encodePacked(_contract, _balances, _nonce))
                    .toEthSignedMessageHash().recover(_signatures[i]);

            if (!valid) {
                return false;
            }
        }

        return true;
    }

    modifier checkSignatures(
        bytes[2] memory _signatures,
        uint256[2] memory _balances,
        uint256 _nonce
    ) {
        // Note: 将存储阵列复制到内存
        address[2] memory signers;
        for (uint256 i = 0; i < users.length; i++) {
            signers[i] = users[i];
        }

        require(
            verify(_signatures, address(this), signers, _balances, _nonce),
            "无效签名"
        );

        _;
    }

    modifier onlyUser() {
        require(isUser[msg.sender], "非用户");
        _;
    }

    function challengeExit(
        uint256[2] memory _balances,
        uint256 _nonce,
        bytes[2] memory _signatures
    )
        public
        onlyUser
        checkSignatures(_signatures, _balances, _nonce)
        checkBalances(_balances)
    {
        require(block.timestamp < expiresAt, "交易过期");
        require(_nonce > nonce, "Nonce必须大于当前的nonce");

        for (uint256 i = 0; i < _balances.length; i++) {
            balances[users[i]] = _balances[i];
        }

        nonce = _nonce;
        expiresAt = block.timestamp + challengePeriod;

        emit ChallengeExit(msg.sender, nonce);
    }

    function withdraw() public onlyUser {
        require(
            block.timestamp >= expiresAt, "挑战期未过期"
        );

        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0;

        (bool sent,) = msg.sender.call{value: amount}("");
        require(sent, "发送以太币失败");

        emit Withdraw(msg.sender, amount);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/ECDSA.sol)

library ECDSA {
    enum RecoverError {
        NoError,
        InvalidSignature,
        InvalidSignatureLength,
        InvalidSignatureS,
        InvalidSignatureV
    }

    function _throwError(RecoverError error) private pure {
        if (error == RecoverError.NoError) {
            return; // 无错误:不执行任何操作
        } else if (error == RecoverError.InvalidSignature) {
            revert("ECDSA:无效签名");
        } else if (error == RecoverError.InvalidSignatureLength) {
            revert("ECDSA:签名长度无效");
        } else if (error == RecoverError.InvalidSignatureS) {
            revert("ECDSA:无效签名's'值");
        } else if (error == RecoverError.InvalidSignatureV) {
            revert("ECDSA:签名 'v' 值无效");
        }
    }

    function tryRecover(bytes32 hash, bytes memory signature)
        internal
        pure
        returns (address, RecoverError)
    {
        // 检查签名长度
        // - case 65: r,s,v signature (standard)
        // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover 接收签名参数,目前唯一获取这些参数的方式是使用汇编。
            assembly {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            return tryRecover(hash, v, r, s);
        } else if (signature.length == 64) {
            bytes32 r;
            bytes32 vs;
            // ecrecover 接收签名参数,而目前获取这些参数的唯一方法是使用汇编语言。
            assembly {
                r := mload(add(signature, 0x20))
                vs := mload(add(signature, 0x40))
            }
            return tryRecover(hash, r, vs);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength);
        }
    }

    function recover(bytes32 hash, bytes memory signature)
        internal
        pure
        returns (address)
    {
        (address recovered, RecoverError error) = tryRecover(hash, signature);
        _throwError(error);
        return recovered;
    }

    function tryRecover(bytes32 hash, bytes32 r, bytes32 vs)
        internal
        pure
        returns (address, RecoverError)
    {
        bytes32 s = vs
            & bytes32(
                0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
            );
        uint8 v = uint8((uint256(vs) >> 255) + 27);
        return tryRecover(hash, v, r, s);
    }

    function recover(bytes32 hash, bytes32 r, bytes32 vs)
        internal
        pure
        returns (address)
    {
        (address recovered, RecoverError error) = tryRecover(hash, r, vs);
        _throwError(error);
        return recovered;
    }

    function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
        internal
        pure
        returns (address, RecoverError)
    {
        // EIP-2仍然允许ecrecover()存在签名可塑性。
        // 消除这种可能性,使签名唯一。
        // 以太坊黄皮书(<https://ethereum.github.io/yellowpaper/paper.pdf>)附录F中
        // 定义了s的有效范围(301):0 < s < secp256k1n ÷ 2 + 1,
        // 以及v的有效范围(302):v ∈ {27, 28}。
        // 当前大多数库生成的签名都具有下半序的s值,从而形成唯一签名。
        //
        // 如果你的库生成可变的签名,例如s值在高位范围内,计算一个新的s值

        // 使用0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141减去s1,并将v从27翻转为28或
        // 反之亦然。如果你的库还生成v为0/1而非27/28的签名,将27加到v上以同样接受
        // 这些可变签名。
        if (
            uint256(s)
                > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
        ) {
            return (address(0), RecoverError.InvalidSignatureS);
        }
        if (v != 27 && v != 28) {
            return (address(0), RecoverError.InvalidSignatureV);
        }

        // 如果签名有效(且不可篡改),则返回签名者地址
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature);
        }

        return (signer, RecoverError.NoError);
    }

    function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
        internal
        pure
        returns (address)
    {
        (address recovered, RecoverError error) = tryRecover(hash, v, r, s);
        _throwError(error);
        return recovered;
    }

    function toEthSignedMessageHash(bytes32 hash)
        internal
        pure
        returns (bytes32)
    {
        // 32是哈希值的字节长度
        // 由上述类型签名强制执行
        return keccak256(
            abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
        );
    }
}

Remix Lite 尝试一下