透明代理主要解决函数选择器冲突的问题。
由于函数选择器仅有4个字节,范围很小,因此两个不同的函数可能会有相同的选择器,例如下面两个函数:
// 选择器冲突的例子
contract Foo {
function burn(uint256) external {}
function collate_propagate_storage(bytes16) external {}
}
函数burn()和collate_propagate_storage()的选择器都为0x42966c68,是一样的,这种情况被称为“选择器冲突”。在这种情况下,EVM无法通过函数选择器分辨用户调用哪个函数,因此该合约无法通过编译。但是代理合约和逻辑合约是两个合约,就算他们之间存在“选择器冲突”也可以正常编译,这可能会导致很严重的安全事故。 举个例子,如果逻辑合约的a函数和代理合约的升级函数的选择器相同,那么管理人就会在调用a函数的时候,将代理合约升级成一个黑洞合约,后果不堪设想。
管理员可能会因为“函数选择器冲突”,在调用逻辑合约的函数时,误调用代理合约的可升级函数。那么限制管理员的权限,不让他调用任何逻辑合约的函数,就能解决冲突:
- 管理员变为工具人,仅能调用代理合约的可升级函数对合约升级,不能通过回调函数调用逻辑合约。
- 其它用户不能调用可升级函数,但是可以调用逻辑合约的函数。
介绍代理合约中选择器冲突(Selector Clash)的另一个解决办法:通用可升级代理(UUPS)。
将升级函数放在逻辑合约中。这样一来,如果有其它函数与升级函数存在“选择器冲突”,编译时就会报错。 如果用户A通过合约B(代理合约)去delegatecall合约C(逻辑合约),上下文仍是合约B的上下文,msg.sender仍是用户A而不是合约B。因此,UUPS合约可以将升级函数放在逻辑合约中,并检查调用者是否为管理员。
- 防止出现函数选择器冲突的问题导致无法升级。
- 相较于透明代理,更节约gas,因为在调用方法的时候没有前置判断是否为管理员。
多签钱包是一种电子钱包,特点是交易被多个私钥持有者(多签人)授权后才能执行:例如钱包由3个多签人管理,每笔交易需要至少2人签名授权。多签钱包可以防止单点故障(私钥丢失,单人作恶),更加去中心化,更加安全,被很多DAO采用。在以太坊上的多签钱包其实是智能合约,属于合约钱包。
因为多签钱包实际上是一个合约,所以实现多人签名后才能执行方法的逻辑实际上是在执行的参数中,需要包含多个签名者的签名信息。
- 设置多签人和门槛(链上):部署多签合约时,我们需要初始化多签人列表和执行门槛
- 创建交易(链下)
- 对交易进行签名且收集多签签名(链下)
- 用多签合约的执行函数,验证签名并执行交易(链上)。
/// @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()检验签名是否有效、数量是否达到执行门槛。