Skip to content

Latest commit

 

History

History
151 lines (136 loc) · 7.06 KB

File metadata and controls

151 lines (136 loc) · 7.06 KB

四十八课 透明代理

透明代理主要解决函数选择器冲突的问题。

选择器冲突的原理

由于函数选择器仅有4个字节,范围很小,因此两个不同的函数可能会有相同的选择器,例如下面两个函数:

    // 选择器冲突的例子
    contract Foo {
        function burn(uint256) external {}
        function collate_propagate_storage(bytes16) external {}
    }

函数burn()和collate_propagate_storage()的选择器都为0x42966c68,是一样的,这种情况被称为“选择器冲突”。在这种情况下,EVM无法通过函数选择器分辨用户调用哪个函数,因此该合约无法通过编译。但是代理合约和逻辑合约是两个合约,就算他们之间存在“选择器冲突”也可以正常编译,这可能会导致很严重的安全事故。 举个例子,如果逻辑合约的a函数和代理合约的升级函数的选择器相同,那么管理人就会在调用a函数的时候,将代理合约升级成一个黑洞合约,后果不堪设想。

透明代理原理

管理员可能会因为“函数选择器冲突”,在调用逻辑合约的函数时,误调用代理合约的可升级函数。那么限制管理员的权限,不让他调用任何逻辑合约的函数,就能解决冲突:

  1. 管理员变为工具人,仅能调用代理合约的可升级函数对合约升级,不能通过回调函数调用逻辑合约。
  2. 其它用户不能调用可升级函数,但是可以调用逻辑合约的函数。

四十九课 通用可升级代理

介绍代理合约中选择器冲突(Selector Clash)的另一个解决办法:通用可升级代理(UUPS)。

UUPS原理

将升级函数放在逻辑合约中。这样一来,如果有其它函数与升级函数存在“选择器冲突”,编译时就会报错。 如果用户A通过合约B(代理合约)去delegatecall合约C(逻辑合约),上下文仍是合约B的上下文,msg.sender仍是用户A而不是合约B。因此,UUPS合约可以将升级函数放在逻辑合约中,并检查调用者是否为管理员。

特点

  1. 防止出现函数选择器冲突的问题导致无法升级。
  2. 相较于透明代理,更节约gas,因为在调用方法的时候没有前置判断是否为管理员。

五十课 多签钱包

多签钱包是一种电子钱包,特点是交易被多个私钥持有者(多签人)授权后才能执行:例如钱包由3个多签人管理,每笔交易需要至少2人签名授权。多签钱包可以防止单点故障(私钥丢失,单人作恶),更加去中心化,更加安全,被很多DAO采用。在以太坊上的多签钱包其实是智能合约,属于合约钱包。

多签钱包的实现原理

因为多签钱包实际上是一个合约,所以实现多人签名后才能执行方法的逻辑实际上是在执行的参数中,需要包含多个签名者的签名信息。

  1. 设置多签人和门槛(链上):部署多签合约时,我们需要初始化多签人列表和执行门槛
  2. 创建交易(链下)
  3. 对交易进行签名且收集多签签名(链下)
  4. 用多签合约的执行函数,验证签名并执行交易(链上)。
/// @dev 在收集足够的多签签名后,执行交易
/// @param to 目标合约地址
/// @param value msg.value,支付的以太坊
/// @param data calldata
/// @param signatures 打包的签名,对应的多签地址由小到达,方便检查。 ({bytes32 r}{bytes32 s}{uint8 v}) (第一个多签的签名, 第二个多签的签名 ... )
function execTransaction(
    address to,
    uint256 value,
    bytes memory data,
    bytes memory signatures
) public payable virtual returns (bool success) {
    // 编码交易数据,计算哈希
    bytes32 txHash = encodeTransactionData(to, value, data, nonce, block.chainid);
    nonce++;  // 增加nonce
    checkSignatures(txHash, signatures); // 检查签名
    // 利用call执行交易,并获取交易结果
    (success, ) = to.call{value: value}(data);
    require(success , "WTF5004");
    if (success) emit ExecutionSuccess(txHash);
    else emit ExecutionFailure(txHash);
}

/**
 * @dev 检查签名和交易数据是否对应。如果是无效签名,交易会revert
 * @param dataHash 交易数据哈希
 * @param signatures 几个多签签名打包在一起
 */
function checkSignatures(
    bytes32 dataHash,
    bytes memory signatures
) public view {
    // 读取多签执行门槛
    uint256 _threshold = threshold;
    require(_threshold > 0, "WTF5005");

    // 检查签名长度足够长
    require(signatures.length >= _threshold * 65, "WTF5006");

    // 通过一个循环,检查收集的签名是否有效
    // 大概思路:
    // 1. 用ecdsa先验证签名是否有效
    // 2. 利用 currentOwner > lastOwner 确定签名来自不同多签(多签地址递增)
    // 3. 利用 isOwner[currentOwner] 确定签名者为多签持有人
    address lastOwner = address(0); 
    address currentOwner;
    uint8 v;
    bytes32 r;
    bytes32 s;
    uint256 i;
    for (i = 0; i < _threshold; i++) {
        (v, r, s) = signatureSplit(signatures, i);
        // 利用ecrecover检查签名是否有效
        currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v, r, s);
        require(currentOwner > lastOwner && isOwner[currentOwner], "WTF5007");
        lastOwner = currentOwner;
    }
}

/// 将单个签名从打包的签名分离出来
/// @param signatures 打包签名
/// @param pos 要读取的多签index.
function signatureSplit(bytes memory signatures, uint256 pos)
    internal
    pure
    returns (
        uint8 v,
        bytes32 r,
        bytes32 s
    )
{
    // 签名的格式:{bytes32 r}{bytes32 s}{uint8 v}
    assembly {
        let signaturePos := mul(0x41, pos)
        r := mload(add(signatures, add(signaturePos, 0x20)))
        s := mload(add(signatures, add(signaturePos, 0x40)))
        v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
    }
}

/// @dev 编码交易数据
/// @param to 目标合约地址
/// @param value msg.value,支付的以太坊
/// @param data calldata
/// @param _nonce 交易的nonce.
/// @param chainid 链id
/// @return 交易哈希bytes.
function encodeTransactionData(
    address to,
    uint256 value,
    bytes memory data,
    uint256 _nonce,
    uint256 chainid
) public pure returns (bytes32) {
    bytes32 safeTxHash =
        keccak256(
            abi.encode(
                to,
                value,
                keccak256(data),
                _nonce,
                chainid
            )
        );
    return safeTxHash;
}

在收集足够的多签签名后,验证签名并执行交易。传入的参数为目标地址to,发送的以太坊数额value,数据data,以及打包签名signatures。打包签名就是将收集的多签人对交易哈希的签名,按多签持有人地址从小到大顺序,打包到一个[bytes]数据中。这一步调用了encodeTransactionData()编码交易,调用了checkSignatures()检验签名是否有效、数量是否达到执行门槛。