近期有若干代币都发生了被盗的事件。被盗的原因有的是自身的管理问题,有的是合作伙伴的管理问题。但是被盗之后如何处理,是个需要解决的客观存在。
一个办法是,部署新的合约。在新的合约里,1. 把所有持币者的余额妥善设置,通常是选定一个时间点/块高度,将老合约里的数据作为快照保存下来,以便将之同步到新合约(显然,盗币者/黑客的余额要么保留在新合约的大池子里,要么划拨到一个特殊的账户地址下);2. 把盗币者/黑客的地址列入黑名单(显然,合约需要增加这种机制);3. 与相关交易所进行接洽,使其更新代币合约地址为新的。
看上去不算复杂,而且最后一步其实并非技术性工作。然而在实际的操作中,难点很可能是第一时间都想象不到的:得到持币者列表。出于能耗/资源方面的考虑,合约实现时,通常会将用户余额存储到一个 map 数据结构内,key 为 address,value 为余额。听上去信息倒是完整,只可惜这个 map 数据类型,在 Solidity 的实现中(事实上在 EVM 层面也一样)是不可遍历的。
如果按照踏实的路线,那恐怕就是从链上的区块数据进行整理了。但也不是一点巧劲也借不上。etherscan 站点上会有 holder 的披露,其数量有限制,但对于有些代币来说恐怕已经足够使用。
那还有没有其他的更新/升级智能合约的方式呢?随着智能合约应用的广泛和深入,许多人对于智能合约初期的“一旦部署、无法更改”的这一看似最大的优点也产生了不同的看法。没有什么的完美的,也没有什么是不朽的,修订永远是需要的。智能合约无论怎么说,也是程序,对于程序,那个“永远的中间层”似乎更接近不朽。所以,智能合约可以使用代理合约的模式,来包裹实现,呈现出可以更改和升级的特征。这个代理合约,对于程序员来理解的话,可以认为是 C 语言里面的指针。
很显然,随着进一步的发展,业务模块仍将朝更明确、更独立的方向发展。管理员也好,预言机也好,业务的真实实现也好,都有可能成为可插拔的模块。
附,两年前的一篇文章,作者:Trail of Bits Blog。
如何实现智能合约的迁移
虽然相比其他互联网技术,智能合约等区块链技术相对安全,但是并非绝对安全,即使是零漏洞的合约也有可能被窃取的私钥劫持。先前的 Bancor 和 KICKICO 黑客事件表明:攻击者可以损害智能合约钱包。在这些攻击中,即使合约具备可升级性机制,也可能无法修复已部署的智能合约。**唯一的解决办法是重新部署并正确初始化新的合约实例,以便为用户恢复功能。**
因此,所有智能合约开发者必须在合约设计阶段整合一个迁移程序。此外,企业必须做好在合约损害事件发生时实施迁移的准备。
迁移过程有两个步骤:
1、恢复要迁移的数据
2、将数据写入新合约
第一步:数据恢复
你需要从区块链的某个特定区块中读取数据。要想从损害事件(黑客攻击或故障)中恢复数据,你需要在事件发生之前使用这个区块,或者过滤攻击者的操作。
如果可以的话,请暂停合约。这对于用户来说更加透明公平,并能阻止攻击者盯上那些对迁移不知情的用户。
数据恢复的具体操作取决于你的数据结构。
对于简单类型的公共变量(public variables,例如 uint 或 address)来说,通过它们的 getter 来检索特定值就可以了。而对于私有变量(private variables),你可以依赖事件,也可以计算变量的内存偏移量,然后使用 getStorageAt [4] 函数检索它的值。
由于元素的数量是已知的,因此数组也很容易恢复。
至于映射(mappings)的话,情况有点复杂。由于键(Keys)在映射过程中不会被存储,所以你需要将它们进行恢复才能访问对应的值(Values)。为了简化链下追踪的过程,建议在值被存储在映射中时触发事件(emit events)。
在 ERC20 代币合约中,可以通过追踪代币的 Transfer 事件的地址来获取代币持有者列表。这个过程很难。
对此,有两个帮助方案:第一,扫描区块链并自行检索持有者;第二,依靠以太坊区块链的公开 Google BigTable 存档。
如果不熟悉 web3 API 而无法从区块链中提取信息,那么可以使用 ethereum-etl,其提供了一系列脚本来简化数据提取的过程。
如果没有已经完成同步的区块链,还可以使用 Google BigQuery API。图 1 展示了如何通过 BigQuery 来收集某个特定代币的所有地址:
1 2 3 4 5 |
SELECT from_address FROM `bigquery-public-data.ethereum_blockchain.token_transfers` AS token_transfers WHERE token_transfers.token_address=0x41424344 Union DISTINCT SELECT to_address FROM `bigquery-public-data.ethereum_blockchain.token_transfers` AS token_transfers WHERE token transfers.token address=0x41424344 |
图 1:利用 Google BigQuery 来恢复那些与在 0x41424344 这个地址中的代币相关联的 Transfer 事件的所有地址
BigQuery 提供对区块号的访问,因此可以将查询结果调整为返回特定区块的交易。
一旦恢复了所有代币持有者的地址,就可以离线查询 balanceOf 函数以恢复与每个持有者相关的余额,同时过滤余额为零的帐户。
第二步:数据写入
完成数据收集后,需要开启新合约。
对于简单变量,可以通过合约的构造函数来设置相应的值。
如果你的数据无法保存在单笔交易中,那么情况会有点复杂,成本也会略高。每笔交易都包含在某个区块中,该区块限制了其交易可以使用的 gas 总量(即所谓的 GasLimit)。如果某笔交易的 gas 成本接近或超过此限制,那么矿工将不会将其打包进该区块内。因此,如果想要迁移大量数据,那么必须将数据迁移拆分成多笔交易。
这类情况的解决方案是:在合约中添加初始化状态,只有合约拥有者才能更改状态变量,并且用户无法执行任何操作。
对于 ERC20 代币,上述过程将需要以下步骤:
1、在初始化状态下部署合约;
2、迁移余额;
3、将合约的状态移至生产状态。
初始化状态可以通过使用 OpenZeppelin 提供的 Pausable 功能和指示初始化状态的布尔值(boolean)来实现。
为了降低成本,我们可以使用 batchTransfer(批量传输)函数(该函数允许在单笔交易中设置多个帐户)来实现余额的迁移:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * @dev Initiate the account of destinations[i] with values[i].The function must only be called before * any transfer of tokens (duringInitialization). The caller must check that destinations are unique addresses. * For a large number of destinations, separate the balances initialization in different calls to batchTransfer. * @param destinations List of addresses to set the values * @param values List of values to set function batchTransfer(address[] destinations, uint256[] values) duringInitialization onlyOwner external { require(destinations.length == values.length); uint256 length = destinations.length; uint i; for(i=0; i<length; i++) { balances[destinations[i]] = values[i]; emit Transfer(0x0, destinations[i], values[i]); } } |
图 2: batchTransfer 函数示例
建议
在合约部署之前做好迁移程序的功课。
使用事件(events)来提高数据追踪的效率。
如果想要部署可升级合约,那么必须准备好迁移程序,因为你的密钥可能会受到损害,或者你的合约可能会受到错误且不可逆转的操纵。