Web3 安全系列:誤轉至其他鏈上的資金,還能救回來嗎?
在加密世界中,一次點擊失誤就可能引發一場「數碼災難」。最常見的噩夢之一,莫過於將資產發送到了錯誤的區塊鏈上。比如,本來想給以太坊 Sepolia 測試網上的地址發送 ETH,結果不小心發到了以太坊主網上的地址。這種情況下,還能從以太坊主網上將誤轉的資金取回來嗎?能否找回資產,關鍵在於收款地址的類型。本文將根據不同的情況進行分析。
1. 情境一:收款地址是 EOA
EOA(Externally Owned Account)就是我們常說的、由私鑰或助記詞直接控制的普通錢包地址。
取回資產前提條件:
- 你將資產轉到了一個 EOA 地址。
- 你擁有這個目標 EOA 地址的私鑰或助記詞。(通常是你自己的另一個錢包地址,或者是朋友的地址,且他願意配合。)
- 目標鏈是 EVM 兼容鏈。
取回資產方法:
收款 EOA 地址的私鑰持有者直接在目標鏈上提取資金即可。
2. 情境二:收款地址是合約
這是最令人絕望的情境之一。由於智能合約的地址不是由私鑰生成的,因此沒有任何人擁有智能合約的私鑰,就無法像控制 EOA 那樣去控制這個合約。並且如果該合約沒有預先編寫處理「錯誤轉入資產」的救援函數,那麼誤轉資金可能被永久鎖定在合約中,誰也無法取出。
然而在某些情況下,其實也是有一線生機的。接下來,我們會構建一個將 ETH 鎖在以太坊主網的情境,然後介紹如何將資金救出來。
2.1. 情境說明
該情境概括來說,即用戶本來想調用 Sepolia 測試網的合約,將 ETH 轉入合約來鑄造代幣,然而發起交易時,錯誤地連接到了主網,結果導致 ETH 被鎖定在了主網的合約中。具體的情境建構過程如下:

- 在以太坊 Sepolia 測試網上,項目方(EOA)部署了實現合約,假設該合約的主要功能是用戶存入 ETH,來鑄造相應的 AToken,大致代碼如
mintTokens函數所示。假設部署的地址為 A。需要注意的是,A 中不存在能直接提取 ETH 的函數。
function mintTokens() external payable {
require(msg.value > 0, "no ETH send here");
uint256 amount = msg.value / 1e18;
_mint(msg.sender, amount);
}- 在以太坊 Sepolia 測試網上,項目方(EOA)部署了工廠合約,該合約的功能是根據提供的實現合約地址以及 salt,以最小代理合約(Clones)的方式,部署指向實現合約的代理合約(如函數
deployProxyByImplementation所示)。假設部署的地址為 B。假設此處我們透過調用deployProxyByImplementation函數,以實現合約 A 地址作為 _implementation 傳入,部署了指向 A 的代理合約,地址為 C。
function deployProxyByImplementation(
address _implementation,
bytes32 _salt
) public override returns (address deployedProxy) {
bytes32 salthash = keccak256(abi.encodePacked(_msgSender(), _salt));
deployedProxy = Clones.cloneDeterministic(_implementation, salthash);
}- 用戶想在 Sepolia 測試網上透過轉入 ETH 來鑄造 AToken,於是用戶向代理合約 C 地址發起了調用,正常情況下,代理合約 C 會進一步調用到實現合約 A 的
mintTokens函數,來完成用戶的操作。然而用戶在調用時,錯誤地連接到了以太坊主網。於是,用戶直接將 ETH 轉入到了以太坊主網上的 C 地址上。此時以太坊主網 C 地址上,並未部署任何合約,也沒有人擁有該 C 地址的私鑰,因此用戶的錢,暫時被鎖定在主網的 C 地址上了。
2.2. 關鍵知識點
在介紹具體救援方案之前,先介紹一下救援需要的基本知識點。
2.1. create & create2
create 和 create2 是 Solidity 中常見的兩種部署合約的方式。
- create 部署合約時,合約地址由交易發起者的地址和該帳戶的交易次數(nonce)共同決定,與合約的內容無關。
- create2 部署合約時,合約地址的計算不再依賴於交易發起者的 nonce,而是與以下四個參數有關。
- 0xff
- 創建新合約的合約地址(address)
- 作為參數的混淆值(salt)
- 待創建合約的創建字節碼(init_code)
2.2. 最小代理合約(Clones)
最小代理合約,也常被稱為克隆合約(Clones),核心思想是用極低的成本(Gas)部署一個代理合約,該代理合約指向指定的實現合約。在 Clones 合約中,可以透過 create 或者 create2 的方式部署代理合約,比如透過 cloneDeterministic 函數部署代理合約,就是採用 create2 的方式進行部署。
在 cloneDeterministic 函數中,創建出的代理合約的字節碼非常簡短,格式為:0x363d3d373d3d3d363d73<實現合約地址>5af43d82803e903d91602b57fd5bf3,直接將實現合約的地址硬編碼到了字節碼中,並將調用到該代理合約的調用都 delegatecall 到該實現合約。
從 cloneDeterministic 函數看出,其採用了 create2 的方式創建代理合約,創建出的代理合約的地址與合約創建者地址、salt、實現合約的地址、固定的一串字節碼有關,其與實現合約的字節碼無關。
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
return cloneDeterministic(implementation, salt, 0);
}
function cloneDeterministic(
address implementation,
bytes32 salt,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
assembly ("memory-safe") {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(value, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}2.3. 救援方案
接下來介紹如何救援用戶在主網 C 地址上的 ETH。主要思路是,在以太坊主網 C 地址上,部署上合約程式碼,接管主網 C 地址,將 ETH 提取出來。具體的操作步驟如下:

- 在主網部署與測試網上相同地址 B 的工廠合約。之所以需要相同的工廠合約地址,是因為在後續調用
cloneDeterministic部署代理合約時,代理合約的地址計算與工廠合約地址有關。透過查看 Sepolia 測試網上部署工廠合約的交易,獲取到這筆交易中部署者(項目方地址)的 nonce,在主網上,將項目方(EOA)地址的 nonce 推進到部署工廠合約前的 nonce,然後在主網上部署工廠合約,由於部署者的地址以及 nonce 均與測試網上部署交易相同,因此在主網上部署的工廠合約地址也為 B。 - 在主網部署與測試網相同地址 A 的實現合約。在#最小代理合約(Clones)#部分提到,透過 Clones 合約的
cloneDeterministic函數部署代理合約,其計算出的代理合約地址,與入參 salt、實現合約地址有關,與實現合約的字節碼無關。因此,我們只需要將一個合約部署在地址 A 上即可,合約的具體內容並不影響代理合約地址的計算。那麼我們可以直接在地址 A 上部署一個具備提取 ETH 功能的合約,程式碼如下所示。
在測試網上,實現合約 A 是由項目方地址(EOA)部署的,因此同樣的,實現合約 A 的地址只與交易發起者及其 nonce 有關,因此,觀察測試網上部署實現合約 A 的交易,找到相關 nonce,將主網上項目方地址(EOA)推進到指定的 nonce,然後部署實現合約 A 即可。
contract Withdraw {
address constant private owner = xxx;
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
function withdraw(address receiver) external onlyOwner {
payable(receiver).transfer(address(this).balance);
}
}- 在主網上部署與測試網相同地址 C 的代理合約。觀察測試網上部署代理合約 C 的交易,獲取到 salt 資訊,調用工廠合約 B 的
deployProxyByImplementation函數,將實現合約 A 的地址、salt 作為參數傳入,即可在主網上的地址 C 上部署上代理合約。 - 調用主網代理合約 C 進行取款。項目方地址(EOA)調用代理合約 C 的 withdraw 函數,並指定資金接收者,成功取出代理合約 C 中被凍結的 ETH,然後還給相關的用戶。
2.4. 總結
從上述救援方案可以看出,資金能被救出來的情況,需要同時具備很多條件,比如合約部署者在目標鏈上的相關 nonce 未被使用、困住資金的合約上具備取款函數或者可以透過各種方式部署上取款的函數(合約可升級或使用 Clones 這種代理等)等。
因此,大家在交易時,一定要萬分小心,仔細核對發起的每一筆交易,與合約進行交互之前,可以使用 ZAN 提供的 AI SCAN 漏洞掃描工具,檢測合約的安全性。如果不小心出現資金被鎖住的情況,也不要慌張,可以聯繫 ZAN 的合約安全審計團隊嘗試幫您進行資金救援。
關於 ZAN
ZAN 是螞蟻數科旗下 Web3 科技品牌,致力於 Web3 應用優化--降低成本、增強安全和提升效能,圍繞 Web3 應用全生命週期,提供可靠、穩定安全、客製化的產品和服務。依托 AntChain OpenLabs 的 TrustBase 開源開放技術體系,ZAN 擁有 Web3 領域獨特的優勢和創新能力,為 Web3 社群的區塊鏈應用開發、企業和開發者的 Web3 應用提供了全面的技術產品和服務,其中包括節點服務(ZAN Node Service)、zk 加速(ZAN PowerZebra)、身分驗證eKYC(ZAN Identity)以及智慧合約稽核(ZAN Smart Contract Review)等。