乐闻世界logo
搜索文章和话题

服务端面试题手册

Solidity 中如何实现多签钱包(Multi-Sig Wallet)?

多签钱包(Multi-Signature Wallet)是一种需要多个私钥共同授权才能执行交易的安全机制。它在资产管理、企业治理和 DAO 组织中广泛应用。1. 多签钱包的核心概念/*多签钱包原理:- 设置多个所有者(Owners)- 设定确认阈值(Threshold):需要多少个签名才能执行交易- 例如:3/5 多签表示需要 5 个所有者中的 3 个确认应用场景:- 企业资产管理- DAO 财库- 项目方资金托管- 冷钱包安全存储*/2. 基础多签钱包实现contract MultiSigWallet { // 事件 event Deposit(address indexed sender, uint256 amount); event SubmitTransaction( address indexed owner, uint256 indexed txIndex, address indexed to, uint256 value, bytes data ); event ConfirmTransaction(address indexed owner, uint256 indexed txIndex); event RevokeConfirmation(address indexed owner, uint256 indexed txIndex); event ExecuteTransaction(address indexed owner, uint256 indexed txIndex); event OwnerAdded(address indexed owner); event OwnerRemoved(address indexed owner); event RequirementChanged(uint256 required); // 状态变量 address[] public owners; // 所有者列表 mapping(address => bool) public isOwner; // 是否是所有者 uint256 public numConfirmationsRequired; // 需要的确认数 struct Transaction { address to; // 目标地址 uint256 value; // 转账金额 bytes data; // 调用数据 bool executed; // 是否已执行 uint256 numConfirmations; // 当前确认数 } Transaction[] public transactions; // 交易索引 => 所有者 => 是否已确认 mapping(uint256 => mapping(address => bool)) public isConfirmed; // 修饰器 modifier onlyOwner() { require(isOwner[msg.sender], "Not owner"); _; } modifier txExists(uint256 _txIndex) { require(_txIndex < transactions.length, "Transaction does not exist"); _; } modifier notExecuted(uint256 _txIndex) { require(!transactions[_txIndex].executed, "Transaction already executed"); _; } modifier notConfirmed(uint256 _txIndex) { require(!isConfirmed[_txIndex][msg.sender], "Transaction already confirmed"); _; } // 构造函数 constructor(address[] memory _owners, uint256 _numConfirmationsRequired) { require(_owners.length > 0, "Owners required"); require( _numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length, "Invalid number of confirmations" ); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(owner != address(0), "Invalid owner"); require(!isOwner[owner], "Owner not unique"); isOwner[owner] = true; owners.push(owner); } numConfirmationsRequired = _numConfirmationsRequired; } // 接收 ETH receive() external payable { emit Deposit(msg.sender, msg.value); } // 提交交易 function submitTransaction( address _to, uint256 _value, bytes memory _data ) public onlyOwner { uint256 txIndex = transactions.length; transactions.push(Transaction({ to: _to, value: _value, data: _data, executed: false, numConfirmations: 0 })); emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data); } // 确认交易 function confirmTransaction(uint256 _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) { Transaction storage transaction = transactions[_txIndex]; transaction.numConfirmations += 1; isConfirmed[_txIndex][msg.sender] = true; emit ConfirmTransaction(msg.sender, _txIndex); } // 执行交易 function executeTransaction(uint256 _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) { Transaction storage transaction = transactions[_txIndex]; require( transaction.numConfirmations >= numConfirmationsRequired, "Not enough confirmations" ); transaction.executed = true; (bool success, ) = transaction.to.call{value: transaction.value}( transaction.data ); require(success, "Transaction failed"); emit ExecuteTransaction(msg.sender, _txIndex); } // 撤销确认 function revokeConfirmation(uint256 _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) { require(isConfirmed[_txIndex][msg.sender], "Transaction not confirmed"); Transaction storage transaction = transactions[_txIndex]; transaction.numConfirmations -= 1; isConfirmed[_txIndex][msg.sender] = false; emit RevokeConfirmation(msg.sender, _txIndex); } // 查询函数 function getOwners() public view returns (address[] memory) { return owners; } function getTransactionCount() public view returns (uint256) { return transactions.length; } function getTransaction(uint256 _txIndex) public view returns ( address to, uint256 value, bytes memory data, bool executed, uint256 numConfirmations ) { Transaction storage transaction = transactions[_txIndex]; return ( transaction.to, transaction.value, transaction.data, transaction.executed, transaction.numConfirmations ); }}3. 高级多签钱包(支持动态管理)contract AdvancedMultiSigWallet { // 事件 event Deposit(address indexed sender, uint256 amount, uint256 balance); event TransactionSubmitted( uint256 indexed txId, address indexed proposer, address indexed to, uint256 value, bytes data ); event TransactionConfirmed(uint256 indexed txId, address indexed owner); event TransactionRevoked(uint256 indexed txId, address indexed owner); event TransactionExecuted(uint256 indexed txId); event OwnerAdded(address indexed owner); event OwnerRemoved(address indexed owner); event RequirementChanged(uint256 oldRequired, uint256 newRequired); event PauseStatusChanged(bool paused); // 交易类型枚举 enum TransactionType { Transfer, // 转账 ContractCall, // 合约调用 AddOwner, // 添加所有者 RemoveOwner, // 移除所有者 ChangeRequirement // 修改阈值 } struct Transaction { TransactionType txType; address to; uint256 value; bytes data; bool executed; uint256 confirmations; uint256 submissionTime; uint256 executionTime; } // 状态变量 address[] public owners; mapping(address => bool) public isOwner; mapping(address => uint256) public ownerIndex; uint256 public required; // 需要的确认数 Transaction[] public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; mapping(uint256 => mapping(address => uint256)) public confirmationTime; bool public paused; uint256 public constant TIMELOCK_DURATION = 24 hours; // 时间锁 uint256 public constant MAX_OWNERS = 50; uint256 public constant MAX_TRANSACTIONS = 1000; // 修饰器 modifier onlyWallet() { require(msg.sender == address(this), "Only wallet"); _; } modifier onlyOwner() { require(isOwner[msg.sender], "Not owner"); _; } modifier ownerExists(address _owner) { require(isOwner[_owner], "Owner does not exist"); _; } modifier ownerDoesNotExist(address _owner) { require(!isOwner[_owner], "Owner exists"); _; } modifier notNull(address _address) { require(_address != address(0), "Null address"); _; } modifier validRequirement(uint256 _ownerCount, uint256 _required) { require( _required <= _ownerCount && _required != 0 && _ownerCount != 0, "Invalid requirement" ); _; } modifier whenNotPaused() { require(!paused, "Wallet is paused"); _; } // 构造函数 constructor( address[] memory _owners, uint256 _required ) validRequirement(_owners.length, _required) { require(_owners.length <= MAX_OWNERS, "Too many owners"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(!isOwner[owner] && owner != address(0), "Invalid owner"); isOwner[owner] = true; ownerIndex[owner] = owners.length; owners.push(owner); } required = _required; } // 接收 ETH receive() external payable { emit Deposit(msg.sender, msg.value, address(this).balance); } // 暂停功能 function setPaused(bool _paused) external onlyWallet { paused = _paused; emit PauseStatusChanged(_paused); } // 提交转账交易 function submitTransfer( address _to, uint256 _value ) external onlyOwner whenNotPaused returns (uint256 txId) { require(_to != address(0), "Invalid recipient"); require(_value > 0 && _value <= address(this).balance, "Invalid value"); txId = addTransaction(TransactionType.Transfer, _to, _value, ""); confirmTransaction(txId); } // 提交合约调用 function submitContractCall( address _to, uint256 _value, bytes memory _data ) external onlyOwner whenNotPaused returns (uint256 txId) { require(_to != address(0), "Invalid contract"); txId = addTransaction(TransactionType.ContractCall, _to, _value, _data); confirmTransaction(txId); } // 提交添加所有者 function submitAddOwner( address _owner ) external onlyOwner ownerDoesNotExist(_owner) notNull(_owner) whenNotPaused returns (uint256 txId) { require(owners.length < MAX_OWNERS, "Max owners reached"); txId = addTransaction( TransactionType.AddOwner, _owner, 0, "" ); confirmTransaction(txId); } // 提交移除所有者 function submitRemoveOwner( address _owner ) external onlyOwner ownerExists(_owner) whenNotPaused returns (uint256 txId) { require(owners.length > required, "Cannot remove owner"); txId = addTransaction( TransactionType.RemoveOwner, _owner, 0, "" ); confirmTransaction(txId); } // 提交修改阈值 function submitChangeRequirement( uint256 _newRequired ) external onlyOwner whenNotPaused returns (uint256 txId) { require(_newRequired > 0 && _newRequired <= owners.length, "Invalid requirement"); txId = addTransaction( TransactionType.ChangeRequirement, address(uint160(_newRequired)), 0, "" ); confirmTransaction(txId); } // 添加交易到列表 function addTransaction( TransactionType _txType, address _to, uint256 _value, bytes memory _data ) internal returns (uint256 txId) { require(transactions.length < MAX_TRANSACTIONS, "Too many transactions"); txId = transactions.length; transactions.push(Transaction({ txType: _txType, to: _to, value: _value, data: _data, executed: false, confirmations: 0, submissionTime: block.timestamp, executionTime: 0 })); emit TransactionSubmitted(txId, msg.sender, _to, _value, _data); } // 确认交易 function confirmTransaction(uint256 _txId) public onlyOwner whenNotPaused { require(_txId < transactions.length, "Invalid txId"); require(!transactions[_txId].executed, "Already executed"); require(!confirmations[_txId][msg.sender], "Already confirmed"); confirmations[_txId][msg.sender] = true; confirmationTime[_txId][msg.sender] = block.timestamp; transactions[_txId].confirmations++; emit TransactionConfirmed(_txId, msg.sender); // 自动执行 if (transactions[_txId].confirmations >= required) { executeTransaction(_txId); } } // 撤销确认 function revokeConfirmation(uint256 _txId) external onlyOwner whenNotPaused { require(_txId < transactions.length, "Invalid txId"); require(!transactions[_txId].executed, "Already executed"); require(confirmations[_txId][msg.sender], "Not confirmed"); confirmations[_txId][msg.sender] = false; transactions[_txId].confirmations--; emit TransactionRevoked(_txId, msg.sender); } // 执行交易 function executeTransaction(uint256 _txId) public onlyOwner whenNotPaused { Transaction storage txn = transactions[_txId]; require(!txn.executed, "Already executed"); require(txn.confirmations >= required, "Not enough confirmations"); require( block.timestamp >= txn.submissionTime + TIMELOCK_DURATION, "Timelock not expired" ); txn.executed = true; txn.executionTime = block.timestamp; // 根据交易类型执行 if (txn.txType == TransactionType.Transfer || txn.txType == TransactionType.ContractCall) { (bool success, ) = txn.to.call{value: txn.value}(txn.data); require(success, "External call failed"); } else if (txn.txType == TransactionType.AddOwner) { addOwnerInternal(txn.to); } else if (txn.txType == TransactionType.RemoveOwner) { removeOwnerInternal(txn.to); } else if (txn.txType == TransactionType.ChangeRequirement) { changeRequirementInternal(uint256(uint160(txn.to))); } emit TransactionExecuted(_txId); } // 内部函数:添加所有者 function addOwnerInternal(address _owner) internal { isOwner[_owner] = true; ownerIndex[_owner] = owners.length; owners.push(_owner); emit OwnerAdded(_owner); } // 内部函数:移除所有者 function removeOwnerInternal(address _owner) internal { uint256 index = ownerIndex[_owner]; address lastOwner = owners[owners.length - 1]; owners[index] = lastOwner; ownerIndex[lastOwner] = index; owners.pop(); delete isOwner[_owner]; delete ownerIndex[_owner]; emit OwnerRemoved(_owner); } // 内部函数:修改阈值 function changeRequirementInternal(uint256 _newRequired) internal { uint256 oldRequired = required; required = _newRequired; emit RequirementChanged(oldRequired, _newRequired); } // 批量确认 function confirmMultiple(uint256[] calldata _txIds) external onlyOwner whenNotPaused { for (uint i = 0; i < _txIds.length; i++) { confirmTransaction(_txIds[i]); } } // 查询函数 function getOwners() external view returns (address[] memory) { return owners; } function getTransactionCount() external view returns (uint256) { return transactions.length; } function getTransaction(uint256 _txId) external view returns (Transaction memory) { return transactions[_txId]; } function isConfirmedBy(uint256 _txId, address _owner) external view returns (bool) { return confirmations[_txId][_owner]; } function getConfirmationCount(uint256 _txId) external view returns (uint256) { return transactions[_txId].confirmations; } function getConfirmations(uint256 _txId) external view returns (address[] memory) { address[] memory confirmed = new address[](owners.length); uint256 count = 0; for (uint i = 0; i < owners.length; i++) { if (confirmations[_txId][owners[i]]) { confirmed[count] = owners[i]; count++; } } // 调整数组大小 assembly { mstore(confirmed, count) } return confirmed; } function getPendingTransactions() external view returns (uint256[] memory) { uint256[] memory pending = new uint256[](transactions.length); uint256 count = 0; for (uint i = 0; i < transactions.length; i++) { if (!transactions[i].executed) { pending[count] = i; count++; } } assembly { mstore(pending, count) } return pending; } function getBalance() external view returns (uint256) { return address(this).balance; }}4. 使用 Gnosis Safe 模式// 模拟 Gnosis Safe 的核心功能contract GnosisSafeStyle { // 常量 bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); bytes32 private constant SAFE_TX_TYPEHASH = keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"); // 状态变量 mapping(address => bool) public isOwner; address[] public owners; uint256 public threshold; uint256 public nonce; // 交易哈希 => 是否已执行 mapping(bytes32 => bool) public executed; // 交易哈希 => 签名者 => 是否已签名 mapping(bytes32 => mapping(address => bool)) public signed; // 事件 event SafeSetup(address indexed initiator, address[] owners, uint256 threshold); event ApproveHash(bytes32 indexed approvedHash, address indexed owner); event SignMsg(bytes32 indexed msgHash); event ExecutionSuccess(bytes32 indexed txHash); event ExecutionFailure(bytes32 indexed txHash); // 操作类型 enum Operation { Call, DelegateCall } constructor(address[] memory _owners, uint256 _threshold) { require(_threshold > 0 && _threshold <= _owners.length, "Invalid threshold"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(owner != address(0) && !isOwner[owner], "Invalid owner"); isOwner[owner] = true; owners.push(owner); } threshold = _threshold; emit SafeSetup(msg.sender, _owners, _threshold); } // 接收 ETH receive() external payable {} // 获取交易哈希 function getTransactionHash( address to, uint256 value, bytes calldata data, Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce ) public view returns (bytes32) { bytes32 safeTxHash = keccak256( abi.encode( SAFE_TX_TYPEHASH, to, value, keccak256(data), operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, _nonce ) ); return keccak256( abi.encodePacked( bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash ) ); } // 获取域分隔符 function domainSeparator() public view returns (bytes32) { return keccak256( abi.encode( DOMAIN_SEPARATOR_TYPEHASH, block.chainid, address(this) ) ); } // 执行交易(带签名) function execTransaction( address to, uint256 value, bytes calldata data, Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures ) public payable returns (bool success) { bytes32 txHash = getTransactionHash( to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce ); nonce++; checkSignatures(txHash, signatures); // 执行交易 if (operation == Operation.DelegateCall) { (success, ) = to.delegatecall(data); } else { (success, ) = to.call{value: value}(data); } if (success) { emit ExecutionSuccess(txHash); } else { emit ExecutionFailure(txHash); } return success; } // 检查签名 function checkSignatures(bytes32 dataHash, bytes memory signatures) public view { require(signatures.length >= threshold * 65, "Not enough signatures"); address lastOwner = address(0); for (uint i = 0; i < threshold; i++) { bytes memory signature = slice(signatures, i * 65, 65); address signer = recoverSigner(dataHash, signature); require(isOwner[signer], "Invalid signer"); require(signer > lastOwner, "Signers not ordered"); lastOwner = signer; } } // 恢复签名者 function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) internal pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); return ecrecover(_ethSignedMessageHash, v, r, s); } // 分割签名 function splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { require(sig.length == 65, "Invalid signature length"); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } if (v < 27) { v += 27; } } // 切片函数 function slice(bytes memory data, uint256 start, uint256 length) internal pure returns (bytes memory) { bytes memory result = new bytes(length); for (uint i = 0; i < length; i++) { result[i] = data[start + i]; } return result; }}5. 多签钱包的安全考虑contract MultiSigSecurity { /* 安全最佳实践: 1. 阈值设置 - 推荐 2/3、3/5 等多签配置 - 避免 1/1(单签)或 n/n(需要所有人签名) 2. 所有者管理 - 使用硬件钱包作为所有者 - 定期轮换密钥 - 地理分散存储 3. 交易限制 - 设置每日限额 - 大额交易延迟执行 - 白名单地址 4. 应急机制 - 紧急暂停功能 - 资金回收机制 - 升级能力 */}// 带安全限制的多签钱包contract SecureMultiSig { address[] public owners; mapping(address => bool) public isOwner; uint256 public required; // 安全限制 uint256 public dailyLimit; uint256 public spentToday; uint256 public lastDay; mapping(address => bool) public whitelist; bool public whitelistEnabled; modifier onlyOwner() { require(isOwner[msg.sender], "Not owner"); _; } constructor( address[] memory _owners, uint256 _required, uint256 _dailyLimit ) { // ... 初始化代码 dailyLimit = _dailyLimit; } // 检查并更新每日限额 function checkDailyLimit(uint256 _value) internal { if (block.timestamp > lastDay + 1 days) { lastDay = block.timestamp; spentToday = 0; } require( spentToday + _value <= dailyLimit, "Daily limit exceeded" ); spentToday += _value; } // 检查白名单 function checkWhitelist(address _to) internal view { if (whitelistEnabled) { require(whitelist[_to], "Address not whitelisted"); } } // 添加白名单 function addToWhitelist(address _addr) external onlyOwner { whitelist[_addr] = true; } // 移除白名单 function removeFromWhitelist(address _addr) external onlyOwner { whitelist[_addr] = false; } // 设置白名单开关 function setWhitelistEnabled(bool _enabled) external onlyOwner { whitelistEnabled = _enabled; }}6. 测试和部署建议// 使用 Hardhat 测试多签钱包const { expect } = require("chai");const { ethers } = require("hardhat");describe("MultiSigWallet", function () { let multiSig; let owners; let required = 2; beforeEach(async function () { [owner1, owner2, owner3, nonOwner] = await ethers.getSigners(); owners = [owner1.address, owner2.address, owner3.address]; const MultiSig = await ethers.getContractFactory("MultiSigWallet"); multiSig = await MultiSig.deploy(owners, required); await multiSig.deployed(); // 向多签钱包发送 ETH await owner1.sendTransaction({ to: multiSig.address, value: ethers.utils.parseEther("10") }); }); it("Should submit and confirm transaction", async function () { const to = nonOwner.address; const value = ethers.utils.parseEther("1"); // 提交交易 await multiSig.connect(owner1).submitTransaction(to, value, "0x"); // 确认交易 await multiSig.connect(owner2).confirmTransaction(0); // 验证交易已执行 const tx = await multiSig.transactions(0); expect(tx.executed).to.be.true; // 验证余额 const balance = await ethers.provider.getBalance(to); expect(balance).to.equal(value); }); it("Should require enough confirmations", async function () { const to = nonOwner.address; const value = ethers.utils.parseEther("1"); await multiSig.connect(owner1).submitTransaction(to, value, "0x"); // 只有一个确认,不应该执行 const tx = await multiSig.transactions(0); expect(tx.executed).to.be.false; });});7. 总结多签钱包是重要的安全工具:核心机制:多个所有者共同管理阈值确认机制交易生命周期管理实现要点:完整的交易流程(提交-确认-执行)权限控制事件记录查询功能安全考虑:合理的阈值设置时间锁机制交易限额应急处理生产建议:使用经过审计的库(如 Gnosis Safe)充分测试考虑使用代理模式升级实施监控和报警
阅读 0·3月6日 21:47

Solidity 中如何处理时间锁(Timelock)机制?

时间锁(Timelock)是一种安全机制,要求交易在执行前必须经过一段延迟时间。这在 DeFi 协议、治理合约和升级机制中广泛应用,为用户提供反应时间,防止恶意操作。1. 时间锁的核心概念/*时间锁原理:- 交易提交后进入队列- 必须经过最小延迟时间才能执行- 在延迟期内可以取消交易- 超过最大延迟时间后交易过期应用场景:- 协议参数变更- 合约升级- 大额资金转移- 治理提案执行- 紧急功能暂停*/2. 基础时间锁实现contract BasicTimelock { // 事件 event TransactionScheduled( bytes32 indexed txHash, address indexed target, uint256 value, bytes data, uint256 eta ); event TransactionExecuted( bytes32 indexed txHash, address indexed target, uint256 value, bytes data ); event TransactionCancelled(bytes32 indexed txHash); event DelayChanged(uint256 oldDelay, uint256 newDelay); // 状态变量 address public admin; uint256 public delay; // 最小延迟时间 uint256 public constant GRACE_PERIOD = 14 days; // 宽限期 uint256 public constant MINIMUM_DELAY = 2 days; // 最小延迟 uint256 public constant MAXIMUM_DELAY = 30 days; // 最大延迟 // 交易哈希 => 计划执行时间 mapping(bytes32 => uint256) public queuedTransactions; modifier onlyAdmin() { require(msg.sender == admin, "Timelock: caller is not admin"); _; } constructor(address _admin, uint256 _delay) { require(_delay >= MINIMUM_DELAY, "Delay must exceed minimum"); require(_delay <= MAXIMUM_DELAY, "Delay must not exceed maximum"); admin = _admin; delay = _delay; } // 接收 ETH receive() external payable {} // 设置延迟时间 function setDelay(uint256 _delay) public onlyAdmin { require(_delay >= MINIMUM_DELAY, "Delay must exceed minimum"); require(_delay <= MAXIMUM_DELAY, "Delay must not exceed maximum"); emit DelayChanged(delay, _delay); delay = _delay; } // 计算交易哈希 function hashTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public pure returns (bytes32) { return keccak256(abi.encode(target, value, signature, data, eta)); } // 计划交易 function queueTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public onlyAdmin returns (bytes32 txHash) { require( eta >= block.timestamp + delay, "Timelock: estimated execution block must satisfy delay" ); txHash = hashTransaction(target, value, signature, data, eta); require( queuedTransactions[txHash] == 0, "Timelock: transaction already queued" ); queuedTransactions[txHash] = eta; emit TransactionScheduled(txHash, target, value, data, eta); } // 取消交易 function cancelTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public onlyAdmin { bytes32 txHash = hashTransaction(target, value, signature, data, eta); require( queuedTransactions[txHash] != 0, "Timelock: transaction not queued" ); delete queuedTransactions[txHash]; emit TransactionCancelled(txHash); } // 执行交易 function executeTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public payable onlyAdmin { bytes32 txHash = hashTransaction(target, value, signature, data, eta); require( queuedTransactions[txHash] != 0, "Timelock: transaction not queued" ); require( block.timestamp >= eta, "Timelock: transaction hasn't surpassed time lock" ); require( block.timestamp <= eta + GRACE_PERIOD, "Timelock: transaction is stale" ); delete queuedTransactions[txHash]; bytes memory callData; if (bytes(signature).length == 0) { callData = data; } else { callData = abi.encodePacked( bytes4(keccak256(bytes(signature))), data ); } (bool success, ) = target.call{value: value}(callData); require(success, "Timelock: transaction execution reverted"); emit TransactionExecuted(txHash, target, value, data); }}3. 高级时间锁(支持批量操作)contract AdvancedTimelock { // 事件 event OperationScheduled( bytes32 indexed id, uint256 indexed delay, address indexed target, uint256 value, bytes data ); event OperationExecuted(bytes32 indexed id); event OperationCancelled(bytes32 indexed id); event MinDelayChange(uint256 oldDuration, uint256 newDuration); event RoleGranted(bytes32 indexed role, address indexed account); event RoleRevoked(bytes32 indexed role, address indexed account); // 角色定义 bytes32 public constant TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE"); bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE"); bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE"); // 操作状态 enum OperationState { Unset, // 未设置 Pending, // 等待中 Ready, // 可执行 Done, // 已完成 Expired // 已过期 } struct Operation { uint256 delay; // 延迟时间 uint256 scheduledAt; // 计划时间 bool executed; // 是否已执行 } // 状态变量 mapping(bytes32 => Operation) public operations; mapping(bytes32 => mapping(address => bool)) public hasRole; uint256 public minDelay; // 最小延迟 uint256 public maxDelay; // 最大延迟 uint256 public gracePeriod; // 宽限期 bytes32[] public operationIds; // 操作ID列表 modifier onlyRole(bytes32 role) { require(hasRole[role][msg.sender], "Missing role"); _; } constructor( uint256 _minDelay, address[] memory proposers, address[] memory executors ) { require(_minDelay > 0, "Min delay must be > 0"); minDelay = _minDelay; maxDelay = _minDelay * 10; gracePeriod = 14 days; // 设置管理员 _grantRole(TIMELOCK_ADMIN_ROLE, msg.sender); // 设置提议者 for (uint i = 0; i < proposers.length; i++) { _grantRole(PROPOSER_ROLE, proposers[i]); } // 设置执行者 for (uint i = 0; i < executors.length; i++) { _grantRole(EXECUTOR_ROLE, executors[i]); } // 取消者可以是任何人 _grantRole(CANCELLER_ROLE, msg.sender); } function _grantRole(bytes32 role, address account) internal { hasRole[role][account] = true; emit RoleGranted(role, account); } function grantRole(bytes32 role, address account) external onlyRole(TIMELOCK_ADMIN_ROLE) { _grantRole(role, account); } function revokeRole(bytes32 role, address account) external onlyRole(TIMELOCK_ADMIN_ROLE) { hasRole[role][account] = false; emit RoleRevoked(role, account); } // 接收 ETH receive() external payable {} // 计算操作ID function hashOperation( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt ) public pure returns (bytes32) { return keccak256(abi.encode(target, value, data, predecessor, salt)); } // 批量操作的哈希 function hashOperationBatch( address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt ) public pure returns (bytes32) { return keccak256(abi.encode(targets, values, datas, predecessor, salt)); } // 获取操作状态 function getOperationState(bytes32 id) public view returns (OperationState) { Operation memory op = operations[id]; if (op.scheduledAt == 0) { return OperationState.Unset; } else if (op.executed) { return OperationState.Done; } else if (block.timestamp < op.scheduledAt + op.delay) { return OperationState.Pending; } else if (block.timestamp > op.scheduledAt + op.delay + gracePeriod) { return OperationState.Expired; } else { return OperationState.Ready; } } // 检查是否准备好执行 function isOperationReady(bytes32 id) public view returns (bool) { return getOperationState(id) == OperationState.Ready; } // 检查是否已完成 function isOperationDone(bytes32 id) public view returns (bool) { return getOperationState(id) == OperationState.Done; } // 计划操作 function schedule( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt, uint256 delay ) public onlyRole(PROPOSER_ROLE) { bytes32 id = hashOperation(target, value, data, predecessor, salt); require( getOperationState(id) == OperationState.Unset, "Operation already scheduled" ); require(delay >= minDelay, "Delay too short"); require(delay <= maxDelay, "Delay too long"); operations[id] = Operation({ delay: delay, scheduledAt: block.timestamp, executed: false }); operationIds.push(id); emit OperationScheduled(id, delay, target, value, data); } // 批量计划 function scheduleBatch( address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt, uint256 delay ) public onlyRole(PROPOSER_ROLE) { require( targets.length == values.length && targets.length == datas.length, "Length mismatch" ); bytes32 id = hashOperationBatch(targets, values, datas, predecessor, salt); require( getOperationState(id) == OperationState.Unset, "Operation already scheduled" ); require(delay >= minDelay, "Delay too short"); operations[id] = Operation({ delay: delay, scheduledAt: block.timestamp, executed: false }); operationIds.push(id); for (uint i = 0; i < targets.length; i++) { emit OperationScheduled(id, delay, targets[i], values[i], datas[i]); } } // 取消操作 function cancel(bytes32 id) external onlyRole(CANCELLER_ROLE) { require( getOperationState(id) != OperationState.Unset, "Operation not scheduled" ); require( getOperationState(id) != OperationState.Done, "Operation already done" ); delete operations[id]; emit OperationCancelled(id); } // 执行操作 function execute( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt ) public payable onlyRole(EXECUTOR_ROLE) { bytes32 id = hashOperation(target, value, data, predecessor, salt); require( getOperationState(id) == OperationState.Ready, "Operation not ready" ); operations[id].executed = true; (bool success, ) = target.call{value: value}(data); require(success, "Execution failed"); emit OperationExecuted(id); } // 批量执行 function executeBatch( address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt ) public payable onlyRole(EXECUTOR_ROLE) { require( targets.length == values.length && targets.length == datas.length, "Length mismatch" ); bytes32 id = hashOperationBatch(targets, values, datas, predecessor, salt); require( getOperationState(id) == OperationState.Ready, "Operation not ready" ); operations[id].executed = true; for (uint i = 0; i < targets.length; i++) { (bool success, ) = targets[i].call{value: values[i]}(datas[i]); require(success, "Batch execution failed"); } emit OperationExecuted(id); } // 更新延迟 function updateDelay(uint256 newDelay) external onlyRole(TIMELOCK_ADMIN_ROLE) { require(newDelay >= minDelay, "Delay too short"); require(newDelay <= maxDelay, "Delay too long"); emit MinDelayChange(minDelay, newDelay); minDelay = newDelay; } // 获取待处理操作 function getPendingOperations() external view returns (bytes32[] memory) { uint256 count = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Pending) { count++; } } bytes32[] memory pending = new bytes32[](count); uint256 index = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Pending) { pending[index] = operationIds[i]; index++; } } return pending; } // 获取可执行操作 function getReadyOperations() external view returns (bytes32[] memory) { uint256 count = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Ready) { count++; } } bytes32[] memory ready = new bytes32[](count); uint256 index = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Ready) { ready[index] = operationIds[i]; index++; } } return ready; }}4. 时间锁在治理中的应用contract GovernanceWithTimelock { // 事件 event ProposalCreated( uint256 indexed id, address proposer, address[] targets, uint256[] values, bytes[] calldatas, uint256 eta ); event ProposalExecuted(uint256 indexed id); event ProposalCancelled(uint256 indexed id); event VoteCast( address indexed voter, uint256 indexed proposalId, bool support, uint256 votes ); // 提案结构 struct Proposal { address proposer; address[] targets; uint256[] values; bytes[] calldatas; uint256 forVotes; uint256 againstVotes; bool executed; bool canceled; uint256 eta; uint256 startBlock; uint256 endBlock; } // 状态变量 address public timelock; address public governanceToken; uint256 public votingDelay; // 投票延迟(区块数) uint256 public votingPeriod; // 投票期(区块数) uint256 public proposalThreshold; // 提案门槛 uint256 public quorumVotes; // 法定票数 uint256 public gracePeriod; // 宽限期 mapping(uint256 => Proposal) public proposals; mapping(uint256 => mapping(address => bool)) public hasVoted; uint256 public proposalCount; constructor( address _timelock, address _governanceToken, uint256 _votingDelay, uint256 _votingPeriod, uint256 _proposalThreshold, uint256 _quorumVotes ) { timelock = _timelock; governanceToken = _governanceToken; votingDelay = _votingDelay; votingPeriod = _votingPeriod; proposalThreshold = _proposalThreshold; quorumVotes = _quorumVotes; gracePeriod = 14 days; } // 创建提案 function propose( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) public returns (uint256) { require( getVotes(msg.sender, block.number - 1) >= proposalThreshold, "Below proposal threshold" ); require( targets.length == values.length && targets.length == calldatas.length, "Length mismatch" ); require(targets.length > 0, "Must provide actions"); proposalCount++; uint256 proposalId = proposalCount; Proposal storage newProposal = proposals[proposalId]; newProposal.proposer = msg.sender; newProposal.targets = targets; newProposal.values = values; newProposal.calldatas = calldatas; newProposal.startBlock = block.number + votingDelay; newProposal.endBlock = block.number + votingDelay + votingPeriod; emit ProposalCreated( proposalId, msg.sender, targets, values, calldatas, 0 ); return proposalId; } // 投票 function castVote(uint256 proposalId, bool support) external { require(state(proposalId) == ProposalState.Active, "Voting closed"); Proposal storage proposal = proposals[proposalId]; require(!hasVoted[proposalId][msg.sender], "Already voted"); uint256 votes = getVotes(msg.sender, proposal.startBlock); require(votes > 0, "No voting power"); hasVoted[proposalId][msg.sender] = true; if (support) { proposal.forVotes += votes; } else { proposal.againstVotes += votes; } emit VoteCast(msg.sender, proposalId, support, votes); } // 排队执行(通过时间锁) function queue(uint256 proposalId) external { require( state(proposalId) == ProposalState.Succeeded, "Proposal not succeeded" ); Proposal storage proposal = proposals[proposalId]; uint256 eta = block.timestamp + AdvancedTimelock(timelock).minDelay(); proposal.eta = eta; // 通过时间锁排队 for (uint i = 0; i < proposal.targets.length; i++) { AdvancedTimelock(timelock).schedule( proposal.targets[i], proposal.values[i], proposal.calldatas[i], bytes32(0), bytes32(proposalId), AdvancedTimelock(timelock).minDelay() ); } } // 执行提案 function execute(uint256 proposalId) external payable { require( state(proposalId) == ProposalState.Queued, "Proposal not queued" ); Proposal storage proposal = proposals[proposalId]; require( block.timestamp >= proposal.eta, "Timelock not passed" ); require( block.timestamp <= proposal.eta + gracePeriod, "Proposal expired" ); proposal.executed = true; // 通过时间锁执行 for (uint i = 0; i < proposal.targets.length; i++) { AdvancedTimelock(timelock).execute( proposal.targets[i], proposal.values[i], proposal.calldatas[i], bytes32(0), bytes32(proposalId) ); } emit ProposalExecuted(proposalId); } // 取消提案 function cancel(uint256 proposalId) external { require( state(proposalId) != ProposalState.Executed, "Already executed" ); Proposal storage proposal = proposals[proposalId]; require( msg.sender == proposal.proposer || getVotes(proposal.proposer, block.number - 1) < proposalThreshold, "Not authorized" ); proposal.canceled = true; // 取消时间锁中的操作 for (uint i = 0; i < proposal.targets.length; i++) { bytes32 id = keccak256(abi.encode( proposal.targets[i], proposal.values[i], proposal.calldatas[i], bytes32(0), bytes32(proposalId) )); if (AdvancedTimelock(timelock).getOperationState(id) != AdvancedTimelock.OperationState.Unset) { AdvancedTimelock(timelock).cancel(id); } } emit ProposalCancelled(proposalId); } // 提案状态枚举 enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed } // 获取提案状态 function state(uint256 proposalId) public view returns (ProposalState) { require(proposalCount >= proposalId && proposalId > 0, "Invalid id"); Proposal storage proposal = proposals[proposalId]; if (proposal.canceled) { return ProposalState.Canceled; } else if (proposal.executed) { return ProposalState.Executed; } else if (block.number <= proposal.startBlock) { return ProposalState.Pending; } else if (block.number <= proposal.endBlock) { return ProposalState.Active; } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes) { return ProposalState.Defeated; } else if (proposal.eta == 0) { return ProposalState.Succeeded; } else if (block.timestamp >= proposal.eta + gracePeriod) { return ProposalState.Expired; } else { return ProposalState.Queued; } } // 获取投票权 function getVotes(address account, uint256 blockNumber) public view returns (uint256) { // 简化实现,实际应查询治理代币 return 1000; }}5. 时间锁安全最佳实践contract TimelockSecurity { /* 安全最佳实践: 1. 延迟时间设置 - 最小延迟:2-3 天(给用户反应时间) - 最大延迟:30 天(防止无限期延迟) - 宽限期:14 天(过期后无法执行) 2. 权限分离 - 提议者:可以提交操作 - 执行者:可以执行操作 - 取消者:可以取消操作 - 管理员:可以修改参数 3. 监控和报警 - 监听所有时间锁事件 - 设置异常操作报警 - 建立应急响应机制 4. 多重验证 - 操作哈希验证 - 参数边界检查 - 状态前置检查 */}// 带紧急暂停的时间锁contract PausableTimelock { bool public paused; address public guardian; modifier whenNotPaused() { require(!paused, "Timelock paused"); _; } modifier onlyGuardian() { require(msg.sender == guardian, "Not guardian"); _; } function pause() external onlyGuardian { paused = true; } function unpause() external onlyGuardian { paused = false; } function execute( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt ) external payable whenNotPaused { // 执行逻辑... }}6. 测试示例// Hardhat 测试时间锁const { expect } = require("chai");const { ethers } = require("hardhat");describe("Timelock", function () { let timelock; let admin, proposer, executor, canceller; const minDelay = 2 * 24 * 60 * 60; // 2 days beforeEach(async function () { [admin, proposer, executor, canceller] = await ethers.getSigners(); const Timelock = await ethers.getContractFactory("AdvancedTimelock"); timelock = await Timelock.deploy( minDelay, [proposer.address], [executor.address] ); await timelock.deployed(); // 给时间锁发送 ETH await admin.sendTransaction({ to: timelock.address, value: ethers.utils.parseEther("10") }); }); it("Should schedule and execute operation after delay", async function () { const target = admin.address; const value = ethers.utils.parseEther("1"); const data = "0x"; const predecessor = ethers.constants.HashZero; const salt = ethers.utils.randomBytes(32); // 计划操作 await timelock.connect(proposer).schedule( target, value, data, predecessor, salt, minDelay ); const id = await timelock.hashOperation(target, value, data, predecessor, salt); expect(await timelock.getOperationState(id)).to.equal(1); // Pending // 时间推进 await network.provider.send("evm_increaseTime", [minDelay]); await network.provider.send("evm_mine"); expect(await timelock.getOperationState(id)).to.equal(2); // Ready // 执行操作 await timelock.connect(executor).execute(target, value, data, predecessor, salt); expect(await timelock.getOperationState(id)).to.equal(3); // Done }); it("Should not execute before delay", async function () { const target = admin.address; const value = ethers.utils.parseEther("1"); const data = "0x"; const predecessor = ethers.constants.HashZero; const salt = ethers.utils.randomBytes(32); await timelock.connect(proposer).schedule( target, value, data, predecessor, salt, minDelay ); // 尝试提前执行 await expect( timelock.connect(executor).execute(target, value, data, predecessor, salt) ).to.be.revertedWith("Operation not ready"); });});7. 总结时间锁是重要的安全机制:核心功能:延迟执行可取消过期机制权限控制应用场景:治理合约协议升级参数调整资金转移安全要点:合理的延迟时间权限分离监控报警应急机制最佳实践:使用 OpenZeppelin 的 TimelockController充分测试时间边界建立监控体系制定应急预案
阅读 0·3月6日 21:47

Solidity 中如何防止常见的智能合约安全漏洞?

智能合约安全是区块链开发的核心议题。由于合约一旦部署就难以修改,且涉及资产安全,预防漏洞比修复漏洞更重要。以下是常见安全漏洞及防护措施。1. 重入攻击(Reentrancy Attack)漏洞原理攻击者利用合约在状态更新前调用外部合约的特性,递归调用目标合约。// 存在漏洞的合约contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0); // 危险:先转账,后更新状态 (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] = 0; // 更新太晚! }}// 攻击合约contract Attacker { VulnerableBank public bank; constructor(address _bank) { bank = VulnerableBank(_bank); } receive() external payable { if (address(bank).balance >= 1 ether) { bank.withdraw(); // 递归调用 } } function attack() external payable { bank.deposit{value: 1 ether}(); bank.withdraw(); }}防护措施import "@openzeppelin/contracts/security/ReentrancyGuard.sol";contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances; bool private locked; // 互斥锁 // 方案 1:Checks-Effects-Interactions 模式 function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); // Checks balances[msg.sender] = 0; // Effects:先更新状态 (bool success, ) = msg.sender.call{value: amount}(""); // Interactions require(success, "Transfer failed"); } // 方案 2:使用 ReentrancyGuard function withdrawWithGuard() public nonReentrant { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] = 0; } // 方案 3:自定义互斥锁 modifier noReentrant() { require(!locked, "Reentrant call"); locked = true; _; locked = false; }}2. 整数溢出/下溢(Integer Overflow/Underflow)漏洞原理Solidity 0.8.0 之前,整数运算溢出不会报错,导致意外结果。// Solidity < 0.8.0 的漏洞contract VulnerableOverflow { uint8 public counter = 255; function increment() public { counter++; // 溢出为 0! } function decrement() public { counter--; // 下溢为 255! }}防护措施// 方案 1:使用 Solidity 0.8.0+contract SafeInSolidity08 { uint8 public counter = 255; function increment() public { counter++; // 自动检查溢出,溢出会 revert }}// 方案 2:使用 SafeMath 库(Solidity < 0.8.0)library SafeMath { function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "Addition overflow"); return c; } function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a, "Subtraction underflow"); return a - b; } function mul(uint256 a, uint256 b) internal pure returns (uint256) { if (a == 0) return 0; uint256 c = a * b; require(c / a == b, "Multiplication overflow"); return c; }}// 方案 3:使用 unchecked 进行显式控制contract ExplicitControl { function safeAdd(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { uint256 c = a + b; require(c >= a, "Overflow"); return c; } }}3. 访问控制漏洞漏洞原理不正确的权限检查导致未授权访问。// 存在漏洞的合约contract VulnerableAccess { address public owner; function init() public { // 危险:任何人都可以调用 owner = msg.sender; } function withdraw() public { // 危险:使用 tx.origin require(tx.origin == owner, "Not owner"); payable(msg.sender).transfer(address(this).balance); }}防护措施import "@openzeppelin/contracts/access/Ownable.sol";import "@openzeppelin/contracts/access/AccessControl.sol";// 方案 1:使用 OpenZeppelin Ownablecontract SecureWithOwnable is Ownable { constructor() { // owner 在构造函数中设置 } function secureWithdraw() public onlyOwner { payable(owner()).transfer(address(this).balance); }}// 方案 2:使用 AccessControlcontract SecureWithRoles is AccessControl { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); } function adminFunction() public onlyRole(ADMIN_ROLE) { // 只有管理员可以执行 }}// 方案 3:正确的构造函数初始化contract ProperInitialization { address public owner; bool private initialized; constructor() { owner = msg.sender; initialized = true; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); // 使用 msg.sender _; } modifier notInitialized() { require(!initialized, "Already initialized"); _; }}4. 前端运行攻击(Front-Running)漏洞原理攻击者观察内存池中的交易,以更高的 Gas 价格抢先执行。// 存在漏洞的合约contract VulnerableFrontRunning { mapping(bytes32 => uint256) public bids; function placeBid(bytes32 itemId, uint256 amount) external { // 攻击者可以看到内存池中的出价,然后出更高价 require(amount > bids[itemId], "Bid too low"); bids[itemId] = amount; }}防护措施contract SecureAgainstFrontRunning { struct Bid { bytes32 blindedBid; uint256 deposit; } mapping(address => Bid[]) public bids; mapping(bytes32 => address) public highestBidder; uint256 public highestBid; // 阶段 1:提交盲拍(哈希) function bid(bytes32 _blindedBid) external payable { bids[msg.sender].push(Bid({ blindedBid: _blindedBid, deposit: msg.value })); } // 阶段 2:揭示真实出价 function reveal( uint256[] calldata _values, bool[] calldata _fake, bytes32[] calldata _secret ) external { // 验证揭示的出价与提交的哈希匹配 // ... } // 使用 Commit-Reveal 模式 // 或使用密封拍卖}// 另一种方案:使用提交-揭示模式contract CommitRevealAuction { struct Commit { bytes32 commitHash; uint256 deposit; bool revealed; } mapping(address => Commit) public commits; uint256 public revealDeadline; function commitBid(bytes32 _commitHash) external payable { require(block.timestamp < revealDeadline, "Commit period ended"); commits[msg.sender] = Commit({ commitHash: _commitHash, deposit: msg.value, revealed: false }); } function revealBid(uint256 _bid, bytes32 _secret) external { require(block.timestamp >= revealDeadline, "Reveal not started"); Commit storage c = commits[msg.sender]; require(!c.revealed, "Already revealed"); bytes32 hash = keccak256(abi.encodePacked(_bid, _secret)); require(hash == c.commitHash, "Invalid reveal"); c.revealed = true; // 处理出价... }}5. 时间操纵攻击漏洞原理矿工可以操纵区块时间戳(在一定范围内)。// 存在漏洞的合约contract VulnerableTime { uint256 public gameEndTime; constructor() { gameEndTime = block.timestamp + 1 days; } function claimPrize() external { // 矿工可以将时间戳设置得比实际时间早 require(block.timestamp > gameEndTime, "Game not ended"); // 发放奖品... }}防护措施contract SecureTime { uint256 public gameStartBlock; uint256 public constant GAME_DURATION_BLOCKS = 7200; // 约 1 天 constructor() { gameStartBlock = block.number; } // 使用区块号代替时间戳 function claimPrize() external { require( block.number >= gameStartBlock + GAME_DURATION_BLOCKS, "Game not ended" ); // 发放奖品... } // 如果需要使用时间戳,添加缓冲 function claimPrizeWithBuffer() external { // 添加 15 分钟缓冲(约 90 个区块) require( block.timestamp >= gameEndTime + 15 minutes, "Game not ended with buffer" ); }}6. 拒绝服务攻击(DoS)漏洞原理通过耗尽 Gas 或阻止关键功能执行来攻击合约。// 存在漏洞的合约contract VulnerableDoS { address[] public investors; mapping(address => uint256) public balances; // 危险:遍历可能耗尽 Gas function distributeDividends() external { for (uint i = 0; i < investors.length; i++) { // 如果 investors 数组很大,会耗尽 Gas payable(investors[i]).transfer(balances[investors[i]]); } }}// 另一种 DoS:通过 revert 阻止转账contract VulnerableDoS2 { mapping(address => uint256) public balances; function withdraw() external { uint256 amount = balances[msg.sender]; // 如果接收者是恶意合约,会 revert 阻止所有人提款 (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] = 0; }}防护措施contract SecureAgainstDoS { mapping(address => uint256) public balances; mapping(address => bool) public withdrawn; // 方案 1:拉取模式(Pull)代替推送模式(Push) function withdraw() external { uint256 amount = balances[msg.sender]; require(amount > 0, "No balance"); require(!withdrawn[msg.sender], "Already withdrawn"); withdrawn[msg.sender] = true; balances[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } // 方案 2:分页处理 function distributeDividends(uint256 _start, uint256 _end) external { require(_end <= investors.length, "Invalid range"); require(_end - _start <= 100, "Batch too large"); // 限制批次大小 for (uint i = _start; i < _end; i++) { address investor = investors[i]; if (!paid[investor]) { paid[investor] = true; payable(investor).transfer(balances[investor]); } } } // 方案 3:使用 try-catch(Solidity 0.6.0+) function safeTransfer(address _to, uint256 _amount) internal { (bool success, ) = _to.call{value: _amount}(""); if (!success) { // 记录失败,不阻止其他转账 failedTransfers[_to] += _amount; } }}7. 随机数漏洞已在随机数专题中详细讲解,核心要点:不要使用区块哈希、时间戳等链上数据生成随机数使用 Chainlink VRF 等预言机方案使用 Commit-Reveal 模式8. 逻辑漏洞权限绕过// 危险:逻辑错误导致权限绕过contract LogicError { mapping(address => bool) public admins; function addAdmin(address _admin) external { // 错误:缺少权限检查 admins[_admin] = true; }}// 修复contract FixedLogic { mapping(address => bool) public admins; modifier onlyAdmin() { require(admins[msg.sender], "Not admin"); _; } function addAdmin(address _admin) external onlyAdmin { admins[_admin] = true; }}重入变种:跨函数重入contract CrossFunctionReentrancy { mapping(address => uint256) public balances; function transfer(address _to, uint256 _amount) external { require(balances[msg.sender] >= _amount); balances[msg.sender] -= _amount; balances[_to] += _amount; } function withdraw() external { uint256 amount = balances[msg.sender]; require(amount > 0); (bool success, ) = msg.sender.call{value: amount}(""); require(success); // 攻击者可以在 receive 中调用 transfer,然后再次 withdraw balances[msg.sender] = 0; }}// 修复:使用互斥锁contract FixedCrossFunction is ReentrancyGuard { function withdraw() external nonReentrant { // ... }}9. 安全开发最佳实践// 完整的安全合约模板import "@openzeppelin/contracts/security/ReentrancyGuard.sol";import "@openzeppelin/contracts/security/Pausable.sol";import "@openzeppelin/contracts/access/Ownable.sol";import "@openzeppelin/contracts/utils/Address.sol";contract SecureContractTemplate is ReentrancyGuard, Pausable, Ownable { using Address for address; // 1. 使用最新编译器版本 pragma solidity ^0.8.19; // 2. 使用内置溢出检查(0.8.0+) uint256 public counter; // 3. 使用 ReentrancyGuard function withdraw() external nonReentrant whenNotPaused { // ... } // 4. 使用 Pausable 应急暂停 function pause() external onlyOwner { _pause(); } // 5. 正确的访问控制 function adminFunction() external onlyOwner { // ... } // 6. 事件记录 event ActionExecuted(address indexed user, uint256 amount); // 7. 自定义错误(Gas 优化) error InsufficientBalance(uint256 requested, uint256 available); error InvalidAddress(); // 8. 输入验证 function validateInput(address _addr, uint256 _amount) internal pure { if (_addr == address(0)) revert InvalidAddress(); if (_amount == 0) revert InsufficientBalance(_amount, 0); } // 9. 使用 Checks-Effects-Interactions 模式 function secureTransfer(address _to, uint256 _amount) external { // Checks require(balances[msg.sender] >= _amount, "Insufficient balance"); // Effects balances[msg.sender] -= _amount; // Interactions (bool success, ) = _to.call{value: _amount}(""); require(success, "Transfer failed"); } // 10. 接收 ETH 的安全处理 receive() external payable { emit DepositReceived(msg.sender, msg.value); }}10. 安全审计检查清单[ ] 重入攻击防护(Checks-Effects-Interactions、ReentrancyGuard)[ ] 整数溢出检查(使用 0.8.0+ 或 SafeMath)[ ] 访问控制验证(Ownable、AccessControl)[ ] 输入验证(零地址检查、范围检查)[ ] 时间操纵防护(使用区块号代替时间戳)[ ] 随机数安全(使用 VRF)[ ] DoS 防护(拉取模式、分页处理)[ ] 事件完整性(所有状态变更触发事件)[ ] 应急机制(Pausable、升级能力)[ ] 代码审计(Slither、Mythril、Certora)11. 安全工具静态分析:Slither、Mythril、Manticore形式化验证:Certora、K Framework模糊测试:Echidna、Harvey监控:Tenderly、Forta// hardhat 配置安全工具require("@nomiclabs/hardhat-truffle5");require("hardhat-gas-reporter");require("solidity-coverage");module.exports = { solidity: { version: "0.8.19", settings: { optimizer: { enabled: true, runs: 200 } } }, gasReporter: { enabled: true },};​
阅读 0·3月6日 21:47

Solidity 智能合约中如何实现访问控制?有哪些最佳实践?

访问控制是智能合约安全的核心组成部分,确保只有授权用户才能执行特定操作。Solidity 提供了多种实现访问控制的方式。1. 基础访问控制:Only Owner 模式最简单的访问控制模式,只允许合约部署者执行敏感操作。contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor() { owner = msg.sender; emit OwnershipTransferred(address(0), msg.sender); } modifier onlyOwner() { require(msg.sender == owner, "Not the owner"); _; } function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; } // 只有 owner 可以调用的函数 function withdraw() public onlyOwner { payable(owner).transfer(address(this).balance); }}2. OpenZeppelin AccessControl:角色基础访问控制更灵活、更安全的访问控制方案,支持多角色管理。import "@openzeppelin/contracts/access/AccessControl.sol";contract RoleBasedAccess is AccessControl { // 定义角色 bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); constructor() { // 部署者获得默认 admin 角色 _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); _grantRole(MINTER_ROLE, msg.sender); } // 只有 MINTER_ROLE 可以铸造 function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { // 铸造逻辑 } // 只有 PAUSER_ROLE 可以暂停 function pause() public onlyRole(PAUSER_ROLE) { // 暂停逻辑 } // 只有 ADMIN_ROLE 可以设置参数 function setParameter(uint256 param) public onlyRole(ADMIN_ROLE) { // 设置参数逻辑 }}3. 多签访问控制对于高价值合约,使用多签钱包进行访问控制更安全。contract MultiSigControl { address[] public owners; mapping(address => bool) public isOwner; uint256 public requiredConfirmations; struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 confirmations; } Transaction[] public transactions; mapping(uint256 => mapping(address => bool)) public confirmed; modifier onlyOwner() { require(isOwner[msg.sender], "Not an owner"); _; } constructor(address[] memory _owners, uint256 _required) { require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(owner != address(0), "Invalid owner"); require(!isOwner[owner], "Owner not unique"); isOwner[owner] = true; owners.push(owner); } requiredConfirmations = _required; } function submitTransaction(address _to, uint256 _value, bytes memory _data) public onlyOwner returns (uint256) { uint256 txId = transactions.length; transactions.push(Transaction({ to: _to, value: _value, data: _data, executed: false, confirmations: 0 })); return txId; } function confirmTransaction(uint256 _txId) public onlyOwner { require(_txId < transactions.length, "Invalid tx"); require(!confirmed[_txId][msg.sender], "Already confirmed"); confirmed[_txId][msg.sender] = true; transactions[_txId].confirmations++; if (transactions[_txId].confirmations >= requiredConfirmations) { executeTransaction(_txId); } } function executeTransaction(uint256 _txId) internal { Transaction storage transaction = transactions[_txId]; require(!transaction.executed, "Already executed"); transaction.executed = true; (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction failed"); }}4. 时间锁访问控制为敏感操作添加时间延迟,给用户反应时间。contract TimelockControl { uint256 public constant DELAY = 2 days; struct PendingAction { bytes32 actionHash; uint256 executeTime; bool executed; } mapping(bytes32 => PendingAction) public pendingActions; event ActionScheduled(bytes32 indexed actionHash, uint256 executeTime); event ActionExecuted(bytes32 indexed actionHash); function scheduleAction(bytes32 actionHash) public onlyOwner { require(pendingActions[actionHash].executeTime == 0, "Already scheduled"); uint256 executeTime = block.timestamp + DELAY; pendingActions[actionHash] = PendingAction({ actionHash: actionHash, executeTime: executeTime, executed: false }); emit ActionScheduled(actionHash, executeTime); } function executeAction(bytes32 actionHash) public { PendingAction storage action = pendingActions[actionHash]; require(action.executeTime > 0, "Not scheduled"); require(block.timestamp >= action.executeTime, "Too early"); require(!action.executed, "Already executed"); action.executed = true; // 执行具体操作 emit ActionExecuted(actionHash); } function cancelAction(bytes32 actionHash) public onlyOwner { require(!pendingActions[actionHash].executed, "Already executed"); delete pendingActions[actionHash]; }}5. 基于代币的访问控制使用代币持有量来控制访问权限,常用于 DAO 治理。contract TokenBasedAccess { IERC20 public governanceToken; uint256 public minTokensRequired; constructor(address _token, uint256 _minTokens) { governanceToken = IERC20(_token); minTokensRequired = _minTokens; } modifier onlyTokenHolder() { require(governanceToken.balanceOf(msg.sender) >= minTokensRequired, "Insufficient tokens"); _; } function propose(bytes memory proposal) public onlyTokenHolder { // 提案逻辑 } function vote(uint256 proposalId, bool support) public onlyTokenHolder { // 投票逻辑 }}6. 组合访问控制模式实际项目中通常需要组合多种访问控制模式。import "@openzeppelin/contracts/access/AccessControl.sol";import "@openzeppelin/contracts/security/Pausable.sol";contract ComprehensiveAccess is AccessControl, Pausable { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE"); mapping(address => bool) public whitelist; bool public whitelistEnabled; constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); _grantRole(OPERATOR_ROLE, msg.sender); _grantRole(EMERGENCY_ROLE, msg.sender); } // 白名单检查 modifier onlyWhitelisted() { require(!whitelistEnabled || whitelist[msg.sender], "Not whitelisted"); _; } // 管理员功能:管理白名单 function addToWhitelist(address user) public onlyRole(ADMIN_ROLE) { whitelist[user] = true; } function removeFromWhitelist(address user) public onlyRole(ADMIN_ROLE) { whitelist[user] = false; } function toggleWhitelist(bool enabled) public onlyRole(ADMIN_ROLE) { whitelistEnabled = enabled; } // 操作员功能:日常操作 function processTransaction(address user, uint256 amount) public onlyRole(OPERATOR_ROLE) onlyWhitelisted whenNotPaused { // 处理交易逻辑 } // 紧急功能:暂停合约 function emergencyPause() public onlyRole(EMERGENCY_ROLE) { _pause(); } function emergencyUnpause() public onlyRole(ADMIN_ROLE) { _unpause(); } // 升级功能:只有 admin function upgrade(address newImplementation) public onlyRole(ADMIN_ROLE) { // 升级逻辑 }}最佳实践总结| 场景 | 推荐方案 | 说明 || ------ | ------------- | ----------- || 简单合约 | Ownable | 单所有者模式,简单有效 || 复杂合约 | AccessControl | 多角色管理,灵活安全 || 高价值合约 | 多签 + 时间锁 | 分散风险,增加安全缓冲 || DAO 治理 | 代币基础 | 去中心化治理 || 生产环境 | 组合模式 | 多种机制结合 |安全注意事项永远不要使用 tx.origin 进行权限检查:// 危险!modifier onlyOwner() { require(tx.origin == owner, "Not owner"); // 错误! _;}// 正确modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _;}权限转移要验证地址:function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); // 必须验证 owner = newOwner;}考虑使用 OpenZeppelin 库:经过审计的标准实现,减少安全风险。定期审计权限设置:检查是否有不必要的权限或权限过大的账户。实施最小权限原则:只授予完成工作所需的最小权限。
阅读 0·3月6日 21:46

Solidity 智能合约中如何实现重入攻击防护?

重入攻击(Reentrancy Attack)是智能合约中最常见且危害最大的安全漏洞之一。攻击者利用合约在状态更新前调用外部合约的特性,递归调用目标合约来重复提取资金。重入攻击原理// 存在漏洞的合约示例contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0); // 危险:先转账,后更新状态 (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] = 0; // 状态更新太晚 }}攻击者合约可以递归调用 withdraw(),在余额被清零前多次提取资金。防护方法 1:Checks-Effects-Interactions 模式这是最推荐的防护方式,遵循先检查、再更新状态、最后交互的顺序。contract SecureBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); // Checks balances[msg.sender] = 0; // Effects:先更新状态 (bool success, ) = msg.sender.call{value: amount}(""); // Interactions require(success, "Transfer failed"); }}防护方法 2:使用重入锁(Reentrancy Guard)OpenZeppelin 提供的 ReentrancyGuard 是最常用的解决方案。import "@openzeppelin/contracts/security/ReentrancyGuard.sol";contract ProtectedBank is ReentrancyGuard { mapping(address => uint256) public balances; function withdraw() public nonReentrant { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] = 0; }}防护方法 3:使用互斥锁(Mutex)自定义实现重入锁机制:contract MutexBank { mapping(address => uint256) public balances; bool private locked; modifier noReentrant() { require(!locked, "Reentrant call detected"); locked = true; _; locked = false; } function withdraw() public noReentrant { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] = 0; }}防护方法 4:使用 transfer 或 send虽然 transfer 和 send 有 Gas 限制(2300 Gas),可以防止重入,但不推荐使用,因为:可能与某些智能合约钱包不兼容Gas 限制可能在未来的以太坊升级中改变// 不推荐的方式function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0); balances[msg.sender] = 0; payable(msg.sender).transfer(amount); // Gas 限制 2300}最佳实践总结始终使用 Checks-Effects-Interactions 模式:这是最基础也是最重要的防护使用 OpenZeppelin 的 ReentrancyGuard:对于复杂合约,额外增加一层保护避免使用 call 进行 ETH 转账:如果必须使用,确保状态已更新代码审计:部署前进行专业的安全审计使用静态分析工具:如 Slither、Mythril 等检测重入漏洞检测重入漏洞的工具Slither:静态分析工具,可检测多种漏洞包括重入Mythril:符号执行工具,分析合约安全性Echidna:模糊测试工具,发现边界情况Certora:形式化验证工具,数学证明合约正确性
阅读 0·3月6日 21:46

Solidity 中如何处理错误和异常?自定义错误与 require、assert、revert 的区别是什么?

错误处理是智能合约开发的关键部分。Solidity 提供了多种错误处理机制,从 Solidity 0.8.4 开始引入了自定义错误,大幅优化了 Gas 消耗和错误信息的可读性。1. 错误处理机制概览/*Solidity 中的错误处理:1. require(condition, message) - 用于验证输入和外部条件 - 失败时回滚所有状态变更 - 退还剩余 Gas2. assert(condition) - 用于检查内部不变量 - 失败表示代码有 bug - 消耗所有 Gas(严重错误)3. revert(message) / revert CustomError() - 显式回滚交易 - Solidity 0.8.4+ 支持自定义错误4. try/catch - 捕获外部调用异常 - Solidity 0.6.0+ 支持*/2. require 的使用contract RequireExample { mapping(address => uint256) public balances; // 基本用法 function transfer(address _to, uint256 _amount) external { require(_to != address(0), "Invalid recipient address"); require(_amount > 0, "Amount must be greater than 0"); require(balances[msg.sender] >= _amount, "Insufficient balance"); balances[msg.sender] -= _amount; balances[_to] += _amount; } // 多个条件检查 function complexOperation( address _token, uint256 _amount, uint256 _minAmount, uint256 _deadline ) external { require(_token != address(0), "Invalid token"); require(_amount >= _minAmount, "Amount below minimum"); require(_amount <= 10000 ether, "Amount exceeds maximum"); require(block.timestamp < _deadline, "Transaction expired"); require( _amount % 100 == 0, "Amount must be multiple of 100" ); // 执行操作... } // 带错误消息的 require function withdraw(uint256 _amount) external { uint256 balance = balances[msg.sender]; require( balance >= _amount, string(abi.encodePacked( "Insufficient balance. Available: ", uint2str(balance), ", Requested: ", uint2str(_amount) )) ); balances[msg.sender] -= _amount; payable(msg.sender).transfer(_amount); } // 辅助函数:uint 转 string function uint2str(uint256 _i) internal pure returns (string memory) { if (_i == 0) return "0"; uint256 j = _i; uint256 length; while (j != 0) { length++; j /= 10; } bytes memory bstr = new bytes(length); uint256 k = length; j = _i; while (j != 0) { bstr[--k] = bytes1(uint8(48 + j % 10)); j /= 10; } return string(bstr); }}3. assert 的使用contract AssertExample { uint256 public constant MAX_SUPPLY = 1000000; uint256 public totalSupply; mapping(address => uint256) public balances; // assert 用于检查内部不变量 function mint(address _to, uint256 _amount) external { // 使用 require 检查外部输入 require(_to != address(0), "Invalid address"); require(_amount > 0, "Invalid amount"); uint256 newTotalSupply = totalSupply + _amount; require(newTotalSupply <= MAX_SUPPLY, "Exceeds max supply"); // 状态更新 totalSupply = newTotalSupply; balances[_to] += _amount; // 使用 assert 检查内部不变量 // 如果失败,说明代码有严重 bug assert(totalSupply >= balances[_to]); assert(balances[_to] >= _amount); } // 数学运算后的不变量检查 function divide(uint256 a, uint256 b) external pure returns (uint256) { require(b > 0, "Division by zero"); uint256 result = a / b; // 检查数学不变量 assert(result * b <= a); return result; } // 复杂操作后的状态检查 function complexTransfer( address _from, address _to, uint256 _amount ) external { uint256 fromBalanceBefore = balances[_from]; uint256 toBalanceBefore = balances[_to]; // 执行转账 balances[_from] -= _amount; balances[_to] += _amount; // 检查不变量 assert(balances[_from] == fromBalanceBefore - _amount); assert(balances[_to] == toBalanceBefore + _amount); assert( balances[_from] + balances[_to] == fromBalanceBefore + toBalanceBefore ); }}4. revert 的使用contract RevertExample { // 自定义错误(Solidity 0.8.4+) error InsufficientBalance(uint256 available, uint256 required); error InvalidAddress(address provided); error Unauthorized(address caller, bytes32 requiredRole); error TransferFailed(address from, address to, uint256 amount); error DeadlineExpired(uint256 deadline, uint256 currentTime); error SlippageExceeded(uint256 expected, uint256 actual); mapping(address => uint256) public balances; mapping(address => bytes32) public roles; // 使用自定义错误(Gas 优化) function transferWithCustomError( address _to, uint256 _amount ) external { if (_to == address(0)) { revert InvalidAddress(_to); } uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } balances[msg.sender] -= _amount; balances[_to] += _amount; } // 带多个参数的自定义错误 function swapTokens( address _tokenIn, address _tokenOut, uint256 _amountIn, uint256 _minAmountOut, uint256 _deadline ) external { if (block.timestamp > _deadline) { revert DeadlineExpired(_deadline, block.timestamp); } // 模拟交换计算 uint256 amountOut = _amountIn * 95 / 100; // 5% 滑点 if (amountOut < _minAmountOut) { revert SlippageExceeded(_minAmountOut, amountOut); } // 执行交换... } // 权限检查 function adminFunction() external view { bytes32 requiredRole = keccak256("ADMIN_ROLE"); if (roles[msg.sender] != requiredRole) { revert Unauthorized(msg.sender, requiredRole); } // 执行管理员操作... } // 传统 revert(字符串消息) function legacyRevert(address _to, uint256 _amount) external { if (_to == address(0)) { revert("Transfer to zero address"); } if (balances[msg.sender] < _amount) { revert("Insufficient balance"); } // 执行转账... }}5. 自定义错误的 Gas 优化contract GasComparison { // 自定义错误 error CustomError(uint256 code); // 使用 require 字符串(消耗更多 Gas) function useRequire(uint256 _value) external pure { require(_value > 0, "Value must be greater than zero"); require(_value < 1000, "Value must be less than 1000"); require(_value % 2 == 0, "Value must be even"); } // 使用自定义错误(节省 Gas) function useCustomError(uint256 _value) external pure { if (_value == 0) revert CustomError(1); if (_value >= 1000) revert CustomError(2); if (_value % 2 != 0) revert CustomError(3); } /* Gas 对比: - require 字符串:约 200-300 gas + 字符串存储成本 - 自定义错误:约 50-100 gas 在频繁调用的函数中,自定义错误可以显著节省 Gas */}// 实际应用中的自定义错误设计contract TokenWithCustomErrors { // 定义所有可能的错误 error TransferFromZeroAddress(); error TransferToZeroAddress(); error TransferAmountExceedsBalance(address sender, uint256 balance, uint256 amount); error ApproveFromZeroAddress(); error ApproveToZeroAddress(); error InsufficientAllowance(address owner, address spender, uint256 allowance, uint256 amount); error MintToZeroAddress(); error BurnFromZeroAddress(); error BurnAmountExceedsBalance(address account, uint256 balance, uint256 amount); error InvalidSender(address sender); error InvalidReceiver(address receiver); error PermitDeadlineExpired(uint256 deadline, uint256 currentTime); error InvalidPermitSignature(address signer, address owner); mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; uint256 private _totalSupply; function transfer(address _to, uint256 _amount) external returns (bool) { address owner = msg.sender; if (owner == address(0)) revert TransferFromZeroAddress(); if (_to == address(0)) revert TransferToZeroAddress(); uint256 fromBalance = _balances[owner]; if (fromBalance < _amount) { revert TransferAmountExceedsBalance(owner, fromBalance, _amount); } _balances[owner] = fromBalance - _amount; _balances[_to] += _amount; return true; } function transferFrom( address _from, address _to, uint256 _amount ) external returns (bool) { if (_from == address(0)) revert TransferFromZeroAddress(); if (_to == address(0)) revert TransferToZeroAddress(); uint256 currentAllowance = _allowances[_from][msg.sender]; if (currentAllowance < _amount) { revert InsufficientAllowance(_from, msg.sender, currentAllowance, _amount); } _allowances[_from][msg.sender] = currentAllowance - _amount; uint256 fromBalance = _balances[_from]; if (fromBalance < _amount) { revert TransferAmountExceedsBalance(_from, fromBalance, _amount); } _balances[_from] = fromBalance - _amount; _balances[_to] += _amount; return true; } function mint(address _to, uint256 _amount) external { if (_to == address(0)) revert MintToZeroAddress(); _totalSupply += _amount; _balances[_to] += _amount; } function burn(uint256 _amount) external { address account = msg.sender; if (account == address(0)) revert BurnFromZeroAddress(); uint256 accountBalance = _balances[account]; if (accountBalance < _amount) { revert BurnAmountExceedsBalance(account, accountBalance, _amount); } _balances[account] = accountBalance - _amount; _totalSupply -= _amount; }}6. try/catch 异常处理contract TryCatchExample { // 外部合约接口 interface IExternalContract { function riskyOperation(uint256 _value) external returns (uint256); function anotherFunction() external view returns (bool); } // 自定义错误 error ExternalCallFailed(address target, bytes reason); error ExternalCallReverted(address target, string reason); error ExternalCallPanic(address target, uint256 code); // 使用 try/catch 处理外部调用 function safeExternalCall( address _target, uint256 _value ) external returns (bool success, uint256 result) { try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { // 调用成功 return (true, _result); } catch Error(string memory reason) { // 捕获 revert("string") 错误 revert ExternalCallReverted(_target, reason); } catch Panic(uint256 errorCode) { // 捕获 assert 失败或内部错误 // errorCode: // 0x01: assert 失败 // 0x11: 算术溢出/下溢 // 0x12: 除以零 // 0x21: 转换为无效枚举 // 0x22: 访问错误编码的 storage 字节数组 // 0x31: 空数组 pop // 0x32: 数组越界访问 // 0x41: 内存分配过多 // 0x51: 调用未初始化的内部函数 revert ExternalCallPanic(_target, errorCode); } catch (bytes memory lowLevelData) { // 捕获自定义错误或其他低级错误 revert ExternalCallFailed(_target, lowLevelData); } } // 批量调用,部分失败不影响其他 function batchExternalCalls( address[] calldata _targets, uint256[] calldata _values ) external returns (bool[] memory successes, uint256[] memory results) { require(_targets.length == _values.length, "Length mismatch"); successes = new bool[](_targets.length); results = new uint256[](_targets.length); for (uint i = 0; i < _targets.length; i++) { try IExternalContract(_targets[i]).riskyOperation(_values[i]) returns (uint256 result) { successes[i] = true; results[i] = result; } catch { // 记录失败,继续执行其他调用 successes[i] = false; results[i] = 0; } } return (successes, results); } // 带重试的外部调用 function callWithRetry( address _target, uint256 _value, uint256 _maxAttempts ) external returns (bool success, uint256 result) { for (uint i = 0; i < _maxAttempts; i++) { try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { return (true, _result); } catch { // 重试前等待(实际应用中可能需要更复杂的逻辑) if (i == _maxAttempts - 1) { return (false, 0); } } } return (false, 0); } // 带超时检查的外部调用 function callWithTimeout( address _target, uint256 _value, uint256 _deadline ) external returns (bool success, uint256 result) { require(block.timestamp < _deadline, "Deadline passed"); try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { return (true, _result); } catch { return (false, 0); } }}7. 错误处理最佳实践contract ErrorHandlingBestPractices { /* 最佳实践总结: 1. require 使用场景: - 验证函数输入参数 - 检查外部条件 - 验证返回值 2. assert 使用场景: - 检查内部不变量 - 验证数学运算结果 - 检查不应该发生的情况 3. revert 使用场景: - 复杂的条件判断 - 需要详细错误信息 - Gas 优化(自定义错误) 4. try/catch 使用场景: - 外部合约调用 - 需要优雅处理失败 - 批量操作 */ // 错误定义 error InvalidInput(string param, string reason); error StateInvariantViolation(string invariant); error ExternalDependencyFailed(address dependency); uint256 public constant MAX_VALUE = 10000; uint256 public constant MIN_VALUE = 1; uint256 public totalValue; mapping(address => uint256) public values; // 综合示例 function complexOperation( address _externalContract, uint256 _input, address _recipient ) external { // 1. 输入验证(require) require(_externalContract != address(0), "Invalid contract"); require(_recipient != address(0), "Invalid recipient"); // 使用自定义错误进行详细验证 if (_input < MIN_VALUE || _input > MAX_VALUE) { revert InvalidInput( "_input", "Must be between MIN_VALUE and MAX_VALUE" ); } // 2. 外部调用(try/catch) uint256 externalResult; try IExternal(_externalContract).getValue() returns (uint256 value) { externalResult = value; } catch Error(string memory reason) { revert ExternalDependencyFailed(_externalContract); } catch { revert ExternalDependencyFailed(_externalContract); } // 3. 计算和状态更新 uint256 oldTotal = totalValue; uint256 newValue = _input + externalResult; values[_recipient] += newValue; totalValue += newValue; // 4. 不变量检查(assert) assert(totalValue >= values[_recipient]); assert(totalValue == oldTotal + newValue); assert(values[_recipient] >= newValue); }}interface IExternal { function getValue() external view returns (uint256);}8. 错误处理模式对比contract ErrorComparison { /* 三种错误处理方式的对比: | 特性 | require | assert | revert | |------|---------|--------|--------| | 使用场景 | 输入验证 | 内部不变量 | 复杂条件 | | Gas 退还 | 是 | 否 | 是 | | 错误信息 | 字符串 | 无 | 自定义错误 | | Gas 消耗 | 中等 | 高 | 低(自定义错误)| | 严重性 | 一般 | 严重(bug)| 一般 | 推荐使用场景: - 频繁调用的函数:使用自定义错误 - 输入验证:使用 require - 内部检查:使用 assert - 外部调用:使用 try/catch */ // 示例:根据不同场景选择错误处理方式 // 场景 1:输入验证 - 使用 require function deposit(uint256 _amount) external { require(_amount > 0, "Amount must be positive"); // ... } // 场景 2:Gas 优化 - 使用自定义错误 error InvalidAmount(uint256 provided, uint256 minimum); function optimizedDeposit(uint256 _amount) external { if (_amount < 100) { revert InvalidAmount(_amount, 100); } // ... } // 场景 3:内部不变量 - 使用 assert function internalOperation() external { uint256 before = totalSupply; // ... 复杂操作 assert(totalSupply >= before); // 不应该减少 } uint256 public totalSupply;}9. 实际项目中的错误处理// OpenZeppelin 风格的错误处理contract ProductionGradeErrors { // 定义模块化的错误 // 通用错误 error ZeroAddress(); error ZeroAmount(); error ExceedsMax(uint256 provided, uint256 max); error InsufficientBalance(uint256 available, uint256 required); error Unauthorized(address caller); error AlreadyInitialized(); error NotInitialized(); // 特定功能错误 error TransferFailed(address from, address to, uint256 amount); error SwapFailed(address tokenIn, address tokenOut, uint256 amount); error StakingPeriodNotEnded(uint256 endTime, uint256 currentTime); error CooldownPeriodActive(uint256 cooldownEnd); // 状态变量 bool private _initialized; address public owner; mapping(address => uint256) public balances; mapping(address => uint256) public stakingEndTime; uint256 public constant COOLDOWN_PERIOD = 7 days; modifier onlyOwner() { if (msg.sender != owner) revert Unauthorized(msg.sender); _; } modifier initializer() { if (_initialized) revert AlreadyInitialized(); _initialized = true; _; } // 初始化函数 function initialize(address _owner) external initializer { if (_owner == address(0)) revert ZeroAddress(); owner = _owner; } // 存款函数 function deposit(uint256 _amount) external { if (_amount == 0) revert ZeroAmount(); balances[msg.sender] += _amount; } // 取款函数 function withdraw(uint256 _amount) external { uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } // 检查冷却期 uint256 cooldownEnd = stakingEndTime[msg.sender] + COOLDOWN_PERIOD; if (block.timestamp < cooldownEnd) { revert CooldownPeriodActive(cooldownEnd); } balances[msg.sender] = balance - _amount; (bool success, ) = msg.sender.call{value: _amount}(""); if (!success) { revert TransferFailed(address(this), msg.sender, _amount); } } // 质押函数 function stake(uint256 _amount, uint256 _duration) external { if (_amount == 0) revert ZeroAmount(); uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } uint256 maxDuration = 365 days; if (_duration > maxDuration) { revert ExceedsMax(_duration, maxDuration); } balances[msg.sender] = balance - _amount; stakingEndTime[msg.sender] = block.timestamp + _duration; } // 解质押函数 function unstake() external { uint256 endTime = stakingEndTime[msg.sender]; if (endTime == 0) revert NotInitialized(); if (block.timestamp < endTime) { revert StakingPeriodNotEnded(endTime, block.timestamp); } // 执行解质押... }}10. 总结Solidity 的错误处理机制各有特点:require:用于输入验证和外部条件检查提供清晰的错误信息退还剩余 Gasassert:用于内部不变量检查失败表示代码有 bug消耗所有 Gas(严重错误)revert + 自定义错误:Solidity 0.8.4+ 推荐显著节省 Gas支持参数化错误信息try/catch:处理外部调用异常支持优雅降级批量操作必备最佳实践:频繁调用的函数使用自定义错误输入验证使用 require内部检查使用 assert外部调用使用 try/catch设计清晰、模块化的错误体系
阅读 0·3月6日 21:44

什么是跨链技术?详解原子交换、中继链和桥接方案的工作原理

跨链技术背景区块链孤岛问题:不同区块链网络相互独立,无法直接通信和交换价值,形成了"价值孤岛"。区块链孤岛:┌──────────┐ ┌──────────┐ ┌──────────┐│ 以太坊 │ │ 比特币 │ │ Solana ││ 生态 │ × │ 生态 │ × │ 生态 ││ ERC-20 │ │ BTC │ │ SPL │└──────────┘ └──────────┘ └──────────┘ ↑ ↑ ↑ 无法直接互操作,需要跨链桥跨链技术分类跨链技术├── 同构跨链(同类型区块链)│ ├── 中继链(Relay Chain)│ └── 侧链桥接│└── 异构跨链(不同类型区块链) ├── 哈希时间锁定合约(HTLC) ├── 公证人机制(Notary) ├── 侧链/中继链 └── 跨链桥(Bridge)1. 哈希时间锁定合约(HTLC - Hash Time-Locked Contracts)原理利用哈希锁和时间锁实现无需信任第三方的原子交换。原子交换流程(Alice 用 BTC 换 Bob 的 ETH):步骤1:Alice 生成随机数 S,计算哈希 H = hash(S)步骤2:Alice 创建 BTC 合约 ┌─────────────────────────────────────┐ │ 锁定 1 BTC,条件: │ │ • 知道 S 使得 hash(S) = H │ │ • 或 24小时后退回给 Alice │ └─────────────────────────────────────┘步骤3:Bob 看到 H 后,创建 ETH 合约 ┌─────────────────────────────────────┐ │ 锁定等值 ETH,条件: │ │ • 知道 S 使得 hash(S) = H │ │ • 或 12小时后退回给 Bob │ └─────────────────────────────────────┘ (时间必须短于 Alice 的合约)步骤4:Alice 用 S 解锁 Bob 的 ETH步骤5:Bob 看到 S 后,解锁 Alice 的 BTC结果:要么双方都成功交换,要么都失败(原子性)代码示例contract HTLC { address public alice; address public bob; bytes32 public hashLock; uint256 public timeout; bool public claimed; constructor(address _bob, bytes32 _hashLock, uint256 _timeout) payable { alice = msg.sender; bob = _bob; hashLock = _hashLock; timeout = _timeout; } // Bob 用原像解锁 function claim(string memory secret) public { require(msg.sender == bob, "Not bob"); require(keccak256(abi.encodePacked(secret)) == hashLock, "Invalid secret"); require(!claimed, "Already claimed"); claimed = true; payable(bob).transfer(address(this).balance); } // 超时后 Alice 退款 function refund() public { require(msg.sender == alice, "Not alice"); require(block.timestamp >= timeout, "Not timed out"); require(!claimed, "Already claimed"); payable(alice).transfer(address(this).balance); }}优点:✅ 无需信任第三方✅ 原子性保证缺点:❌ 双方需要同时在线❌ 仅支持相同哈希算法的链❌ 用户体验复杂2. 中继链(Relay Chain)原理通过专门的中继链连接多个平行链,实现跨链通信。Polkadot 架构示例: ┌───────────────┐ │ 中继链 │ │ (Relay Chain) │ │ │ │ • 共识安全 │ │ • 跨链消息路由 │ └───────┬───────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │ 平行链A │ │ 平行链B │ │ 平行链C │ │ │ │ │ │ │ │ DeFi │←───────→│ NFT │←───────→│ 游戏 │ │ 应用 │ XCMP │ 市场 │ XCMP │ 应用 │ └─────────┘ └─────────┘ └─────────┘XCMP(跨链消息传递)跨链消息流程:平行链A ──┬── 生成跨链消息 ├── 提交到中继链 ├── 中继链验证并路由 ├── 平行链B 接收消息 └── 执行相应操作代表项目:Polkadot、Cosmos(IBC 协议)优点:✅ 安全性高(共享中继链安全)✅ 可扩展性好缺点:❌ 架构复杂❌ 仅支持同构链3. 跨链桥(Cross-chain Bridge)原理通过锁定-铸造(Lock-Mint)或销毁-释放(Burn-Unlock)机制实现资产跨链。跨链桥工作流程(以太坊 → BSC):1. 锁定阶段 用户 ──发送 10 ETH──→ 以太坊桥合约 ↓ 锁定 ETH ↓ 生成锁定证明2. 中继阶段 验证者/中继者 ──监控并传递证明──→ BSC 桥合约3. 铸造阶段 BSC 桥合约 ──验证证明──→ 铸造 10 包装 ETH(WETH) ↓ 发送给用户反向流程(BSC → 以太坊):销毁 WETH ──→ 释放原始 ETH桥接方案类型| 类型 | 机制 | 代表项目 | 安全性 ||------|------|----------|--------|| 托管桥 | 中心化机构托管资产 | WBTC | ⭐⭐ || 多签桥 | 多签验证者集 | Ronin | ⭐⭐⭐ || 轻客户端桥 | 链上验证区块头 | Rainbow Bridge | ⭐⭐⭐⭐ || 乐观桥 | 欺诈证明机制 | Nomad | ⭐⭐⭐ || ZK 桥 | 零知识证明 | zkBridge | ⭐⭐⭐⭐⭐ |跨链桥安全事件| 时间 | 项目 | 损失 | 原因 ||------|------|------|------|| 2022-03 | Ronin | 6.25 亿美元 | 私钥泄露 || 2022-10 | BNB Chain | 5.7 亿美元 | 合约漏洞 || 2021-08 | Poly Network | 6.1 亿美元 | 合约漏洞(后归还)|4. LayerZero 全链互操作协议原理结合 Oracle 和 Relayer 实现无需信任假设的跨链通信。LayerZero 架构:链A LayerZero 链B┌─────────┐ ┌───────────┐ ┌─────────┐│ 应用合约 │──────────→│ 端点合约 │ │ 端点合约 │└────┬────┘ └─────┬─────┘ └────┬────┘ │ │ │ │ ┌────────────┼────────────┐ │ │ ↓ ↓ ↓ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ └───→│ Oracle │ │Relayer │ │验证合约 │←──┘ │(Chainlink)│(独立中继)│ │ │ └────────┘ └────────┘ └────────┘核心创新:Oracle 提供区块头Relayer 提供交易证明两者独立,防止合谋跨链方案对比| 方案 | 信任模型 | 速度 | 成本 | 复杂度 | 适用场景 ||------|----------|------|------|--------|----------|| HTLC | 无需信任 | 慢 | 低 | 高 | P2P 原子交换 || 中继链 | 共享安全 | 快 | 中 | 高 | 同构链生态 || 托管桥 | 中心化 | 快 | 低 | 低 | 简单资产转移 || 多签桥 | 多数诚实 | 快 | 中 | 中 | 主流资产桥接 || ZK 桥 | 密码学保证 | 快 | 高 | 高 | 高安全性需求 || LayerZero | 双独立方 | 快 | 中 | 中 | 通用消息传递 |面试要点理解区块链孤岛问题的本质掌握 HTLC 的原子交换原理了解中继链的工作机制熟悉跨链桥的锁定-铸造模式知道不同跨链方案的权衡了解跨链桥的安全风险能够分析主流跨链项目的架构
阅读 0·3月6日 21:44

如何在项目中配置 Babel 以支持 TypeScript 和 React?

配置方案1. 安装必要的依赖# 核心依赖npm install --save-dev @babel/core @babel/cli @babel/preset-env# TypeScript 支持npm install --save-dev @babel/preset-typescript# React 支持npm install --save-dev @babel/preset-react# 运行时支持(可选但推荐)npm install --save @babel/runtimenpm install --save-dev @babel/plugin-transform-runtime2. 基础配置(babel.config.js)module.exports = { presets: [ // 根据目标环境自动选择转换 ['@babel/preset-env', { targets: { browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'] }, useBuiltIns: 'usage', corejs: 3 }], // TypeScript 支持 '@babel/preset-typescript', // React 支持(包含 JSX 转换) ['@babel/preset-react', { runtime: 'automatic', // React 17+ 新 JSX 转换 development: process.env.NODE_ENV === 'development' }] ], plugins: [ // 运行时优化 ['@babel/plugin-transform-runtime', { corejs: 3, helpers: true, regenerator: true }] ]};3. 配合 Webpack 使用// webpack.config.jsmodule.exports = { module: { rules: [ { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react' ] } } } ] }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }};4. TypeScript 配置(tsconfig.json){ "compilerOptions": { "target": "ESNext", "module": "ESNext", "jsx": "preserve", // 保留 JSX,让 Babel 处理 "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, // Babel 需要 "noEmit": true // Babel 负责编译,TS 只负责类型检查 }, "include": ["src/**/*"], "exclude": ["node_modules"]}高级配置1. 环境特定配置// babel.config.jsmodule.exports = { presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], env: { development: { plugins: [ 'react-refresh/babel' // React Fast Refresh ] }, production: { plugins: [ 'transform-remove-console' // 移除 console ] }, test: { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }] ] } }};2. 装饰器支持npm install --save-dev @babel/plugin-proposal-decoratorsmodule.exports = { presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], plugins: [ ['@babel/plugin-proposal-decorators', { legacy: true }], '@babel/plugin-proposal-class-properties' ]};3. 路径别名配置// babel.config.jsmodule.exports = { presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], plugins: [ ['module-resolver', { root: ['./src'], alias: { '@': './src', '@components': './src/components', '@utils': './src/utils' } }] ]};// tsconfig.json{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@utils/*": ["src/utils/*"] } }}常见问题1. 类型检查与编译分离// package.json{ "scripts": { "type-check": "tsc --noEmit", "build": "npm run type-check && babel src --out-dir dist --extensions '.ts,.tsx'" }}2. 处理 CSS/SCSS 导入// 在 TypeScript 中声明模块declare module '*.scss' { const content: { [className: string]: string }; export default content;}3. 热更新配置// webpack.config.jsconst ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');module.exports = { module: { rules: [{ test: /\.(ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { plugins: [ process.env.NODE_ENV === 'development' && 'react-refresh/babel' ].filter(Boolean) } } }] }, plugins: [ process.env.NODE_ENV === 'development' && new ReactRefreshWebpackPlugin() ].filter(Boolean)};
阅读 0·3月6日 21:43

Zustand 与 Redux 相比有哪些优缺点?

Zustand 的优点:更简单的 API无需定义 action types、reducers、action creators代码量减少 60-70%学习曲线更低无需 Provider不需要在应用顶层包裹 Provider 组件减少了组件树的嵌套层级更容易集成到现有项目中更好的性能内置选择性订阅机制自动优化重渲染无需手动使用 useSelector 或 connect更小的体积仅约 1KB gzippedRedux Toolkit 约 12KB gzippedTypeScript 友好内置完整的 TypeScript 支持类型推断更准确Zustand 的缺点:生态系统较小中间件和扩展库相对较少社区资源不如 Redux 丰富调试工具有限虽然 Redux DevTools 可以使用,但功能不如 Redux 完善时间旅行调试支持有限大型项目经验较少在超大型企业级应用中的实践案例较少最佳实践仍在发展中团队熟悉度开发者对 Redux 更熟悉招聘和培训成本可能更高选择 Zustand 的场景:中小型项目项目规模不大,状态管理需求简单不需要复杂的状态管理架构快速原型开发需要快速搭建和迭代重视开发速度而非架构完整性性能敏感的应用需要最小化重渲染对包大小有严格要求团队偏好简洁团队成员熟悉 hooks 模式希望减少样板代码选择 Redux 的场景:大型企业级应用需要严格的状态管理架构团队规模大,需要标准化流程复杂的状态逻辑需要时间旅行调试状态更新逻辑复杂,需要规范化团队已有 Redux 经验团队成员熟悉 Redux已有相关的工具和基础设施总结:Zustand 更适合现代 React 开发,特别是中小型项目和追求简洁的场景。Redux 更适合大型企业级应用和需要严格状态管理的场景。
阅读 0·3月6日 21:43