本文旨在為審計人員提供帳戶抽象錢包的安全審計檢查項,並提供部分針對性的審計指南。
作者: Kong@慢霧安全團隊
前言
本文旨在為審計人員提供以 EIP4337 標準實現的帳戶抽象錢包的安全審計檢查項,並提供部分針對性的審計指南。本文假設審計人員已經對 EIP4337 帳戶抽象標準和 EIP7562 帳戶抽象驗證範圍規則標準較為熟悉,因此不再贅述這兩個標準。接下來,我們將簡單介紹 EIP4337 架構與錢包交易執行流程。
架構
交易執行
在 EIP4337 設計標準中,首先由 EOA 簽署`UserOperation` 類型的數據,並透過 RPC 提交到一個單獨的 Alt Mempools。此記憶體池獨立於以太坊記憶體池,其匯總了使用者提交的 UserOp 資料。 Bundler 會從此記憶體池中提取 UserOp 以幫助使用者執行,在執行之前將進行本機模擬,模擬失敗的`UserOp` 將被丟棄。所有的 UserOp 執行都由 Bundler 呼叫 EntryPoint 合約執行。 EntryPoint 經過一系列驗證後將呼叫使用者的 AA 錢包,執行使用者的 calldata。在此過程中,用戶需要向 Bundler 支付交易上鍊執行的手續費,或指定一個 Paymaster 進行代付。
執行詳情
以下是 Bundler 透過 EntryPoint 呼叫使用者錢包的詳細流程,稽核人員應熟悉此流程。
檢查項
慢霧安全團隊依據上述架構列出以下檢查項,建議審計人員在對每個 4337 錢包進行安全審計時都確保其通過下述檢查項:
1. 檢查是否相容所有 EVM 相容鏈
大部分 AA 錢包可能不只部署在 Ethereum 主網,因為主網在上海昇級後新增了`PUSH0` 字節碼,而 Solidity 在 0.8.20 版本及之後編譯版本都預設為上海昇級後的版本,因此其編譯後的字節碼可能不適用於所有的 EVM 相容鏈。
審計人員在審計時應該檢查合約編譯使用的 Solidity 版本,或檢查編譯後的檔案是否包含`PUSH0` 字節碼。在進行多鏈部署時,建議使用小於 0.8.20 版本的編譯器,或指定編譯版本為`paris`。
solc = "0.8.19"evm_version = "paris"
2. 檢查介面實作與回傳值是否符合 EIP4337 標準規範
EIP4337 標準規定錢包必須實現以下核心接口,其返回值`validationData` 必須包含三個值:authorizer、validUntil 和 validAfter。
function validateUserOp (PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 validationData);
同樣的,代付合約 Paymaster 也必須實現以下核心接口,其中`validatePaymasterUserOp` 的返回值`validationData` 也必須包含三個值:authorizer、validUntil 和 validAfter。
function validatePaymasterUserOp (PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) external returns (bytes memory context, uint256 validationData);
function postOp (PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) external;
當簽章驗證失敗時,authorizer 需要傳回`SIG_VALIDATION_FAILED`(即 1 值),驗證成功時,authorizer 需要傳回`SIG_VALIDATION_SUCCESS`(即 0 值),而不是`revert`。如果是其他情況導致的失敗交易都必須 revert。
3. 檢查錢包呼叫者是否可信
在 EIP4337 標準中,要求錢包或代付人實現的 validateUserOp、executeUserOp、validatePaymasterUserOp、postOp、資料執行介面等函數都應該只允許可信的 EntryPoint 進行調用,以避免錢包被未授權的使用導致資產遺失等風險。
function entryPoint() public view virtual override returns (IEntryPoint) { return _entryPoint;}
function execute(address dest, uint256 value, bytes calldata func) external { _requireFromEntryPointOrOwner(); _call(dest, value, func);}
function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external { _requireFromEntryPointOrOwner(); ...}
4. 檢查是否實現手續費支付功能
在用戶錢包中有充足原生代幣情況下,其可以無需在 EntryPoint 中質押且無需指定代付人,即可使用錢包中的代幣付款。付款費用由 EntryPoint 在呼叫 validateUserOp 函數時透過`missingAccountFunds` 參數傳入。因此稽核人員需要檢查錢包中的 validateUserOp 函數是否實作了向 EntryPoint 合約轉帳`missingAccountFunds` 金額的原生代幣的邏輯。
function validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external virtual override returns (uint256 validationData) { ... _payPrefund(missingAccountFunds);}
5. 檢查錢包創建方式
當使用者錢包尚未建立時,其可以在 UserOp 中指定工廠進行錢包建立。工廠必須使用`CREATE2` 建立錢包,以避免創建地址受到創建順序的干擾。一些用戶需要在還未完成創建之前就知道錢包地址,並向其轉移資金以支付創建費用,因此審計人員在對工廠合約進行審計時,需要確保錢包創建方式使用了`CREATE2`。
function createAccount(address owner,uint256 salt) public returns (SimpleAccount ret) { address addr = getAddress(owner, salt); uint256 codeSize = addr.code.length; if (codeSize > 0) { return SimpleAccount(payable(addr)); } ret = SimpleAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}( address(accountImplementation), abi.encodeCall(SimpleAccount.initialize, (owner)) )));}
6. 檢查重複建立相同錢包的回傳值
EIP 4337 標準要求,對於已經創建的錢包,如果傳入相同的數據,應返回同一位址。這是為了讓客戶端更容易查詢地址,而無需知道錢包是否已部署,也為了避免非預期的創建失敗或創建非預期地址的錢包。
function createAccount(address owner,uint256 salt) public returns (SimpleAccount ret) { address addr = getAddress(owner, salt); uint256 codeSize = addr.code.length; if (codeSize > 0) { return SimpleAccount(payable(addr)); } ...}
7. 檢查錢包創建時是否可被接管
在錢包創建前,用戶可能會先向預創建地址轉入手續費以供創建時支付費用,並且使得在創建同一地址時也不會交易失敗。因此,稽核人員需要檢查錢包是否可以被搶先創建,如果可以,應該檢查被搶先創建後的錢包所有權是否正確,並著重檢查是否存在未參與地址計算的初始化參數,這些參數又能否被修改。
錯誤範例程式碼(entryPoint 不參與位址運算,可被搶先建立修改為惡意的 entryPoint):
function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) public returns(address proxy){ bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index)))); bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); // solhint-disable-next-line no-inline-assembly assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); // EOA + Version tracking emit SmartAccountCreated(proxy,_defaultImpl,_owner, VERSION, _index); BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler); isAccountExist[proxy] = true;}
8. 檢查簽名有效性驗證
錢包必須支援 `validateUserOp` 介面以驗證 EntryPoint 傳入的 UserOp 的有效性,同樣的,Paymaster 也必須支援 `validatePaymasterUserOp` 介面以驗證代付資訊的有效性。因此,審計人員必須在 validateUserOp/validatePaymasterUserOp 中嚴格檢查 UserOp 中簽署的有效性以避免錢包被惡意執行,或簽名被重播。
function validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external virtual override returns (uint256 validationData) { _requireFromEntryPoint(); validationData = _validateSignature(userOp, userOpHash); _validateNonce(userOp.nonce); _payPrefund(missingAccountFunds);}
9. 檢查是否正確實施了 ERC1271
帳戶抽象錢包可能實施 EIP1271 標準以支援驗證來自合約的 1271 簽名,標準規定了透過`isValidSignature` 介面以實現具體的驗證邏輯。審計人員需要嚴格檢查 ERC1271 標準的實施是否符合規範,並確保簽章驗證邏輯安全可靠。
function isValidSignature(bytes32 _dataHash, bytes calldata _signature) public view override returns (bytes4) { // Caller should be a Safe ISafe safe = ISafe(payable(msg.sender)); bytes memory messageData = encodeMessageDataForSafe(safe, abi.encode(_dataHash)); bytes32 messageHash = keccak256(messageData); if (_signature.length == 0) { require(safe.signedMessages(messageHash) != 0, "Hash not approved"); } else { safe.checkSignatures(messageHash, _signature); } return EIP1271_MAGIC_VALUE;}
10. 檢查向 EntryPoint 的質押代幣是否可永久鎖定
帳戶抽象錢包為了支付執行費用,可能實現向 EntryPoint 質押 Native 代幣的邏輯。同樣的,為了提高聲譽,代付合約 Paymaster 與錢包工廠合約也可能實現向 EntryPoint 質押原生代幣的邏輯。 EIP7562 標準要求質押實體的最小質押時間必須是 `MIN_UNSTAKE_DELAY`(1 天),因此通常質押時間是由呼叫者自行決定或合約硬編碼決定。審計人員需要檢查是否任何人都可以傳入任意鎖定時間 `unstakeDelaySec` 進行質押,這可能導致質押代幣被永久鎖定。
function addStake(uint32 unstakeDelaySec) external payable onlyOwner { entryPoint.addStake{value: msg.value}(unstakeDelaySec);}
11. 檢查錢包是否可以不透過 EntryPoint 執行交易
EIP4337 標準要求 EntryPoint 透過直接呼叫錢包執行使用者 calldata 或呼叫 executeUserOp 函數以執行數據,並由錢包驗證呼叫者與簽署。因此,審計人員需要檢查錢包是否允許在不通過 EntryPoint 的情況下執行呼叫者的資料。如果可以,審計人員需要檢查錢包是否正確實施了權限檢查,以避免錢包被執行惡意資料。
function execute(address dest, uint256 value, bytes calldata func) external { _requireFromEntryPointOrOwner(); _call(dest, value, func);}
12. 檢查錢包是否只存取與發送者關聯的儲存字段
為了避免 Bundler 執行 UserOp,遭受 DoS,然後使得 Bundler 無法收回 gas 費成本,EIP7562 標準規定了只允許錢包訪問與其相關聯的存儲,否則 Bundler 將不會接受此 UserOp。審計人員需要檢查錢包中對儲存的存取是否符合規定。
13. 檢查執行失敗後 Paymaster 的處理邏輯是否符合預期
在 EntryPoint 中,當 innerHandleOp 執行後,EntryPoint 將透過 _postExecution 函數進行後續的手續費處理邏輯,並呼叫 Paymaster 的 postOp 函數對手續費進行處理(例如:多退少補)。而當 innerHandleOp 執行失敗時,也會透過 postExecution 進行費用處理,但此時不會觸發 Paymaster 的 postOp 函數(即使使用者指定了代付)。因此在審計 Paymaster 時,需要特別注意使用者惡意使得 innerHandleOp 和 postOp 都執行失敗的情況下,Paymaster 能否正確地處理費用,以避免 Paymaster 中的資金被耗盡。
14. 檢查模組化錢包的實作是否安全
EIP4337 錢包可能支援用戶自訂拓展錢包功能,如:運行社交恢復、合約升級、保險庫等模組。稽核人員需要檢查錢包能否安全地對模組的「增加/移除/使用」進行管理。如果使用了`DELEGATECALL` 運作模組,稽核人員應確保錢包插槽儲存的資料安全。
總結
上述基礎檢查項目是基於目前 EIP4337 的帳戶抽象標準,供審計人員對帳戶抽象錢包進行檢查。不同的錢包實現不盡相同,且目前基於 EIP4337 的實施仍處於早期階段,因此審計人員仍需要根據錢包的實際實現情況進行嚴格檢查。對於正在開發的專案來說,慢霧安全團隊建議開發者在開發過程中仔細考慮上述檢查項目。最後,文中相關連結可透過點擊閱讀原文跳轉至 GitHub 查看。
免責聲明:作為區塊鏈資訊平台,本站所發布文章僅代表作者及來賓個人觀點,與 Web3Caff 立場無關。文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。