①仅在必要的时间才使用代理模式
不是每个合约都必要升级。如上文所示,使用代理模式涉及许多风险。“可升级”的属性也会引起信任问题,因为代理管理员可以在没有得到社区同意的情况下升级合约。我们发起仅在必要时才将代理模式整合到项目中。
② 不要修改代理库
代理合约库很复杂,尤其是处置惩罚存储管理和升级机制的部门。修改中的任何错误都会影响代理和逻辑合约的工作。我们在审计过程中发现的大量与代理有关的高严肃性bug都是由不精确的代理库修改造成的。Audius事件就是一个典型的例子,展示了代理合约修改不当所带来的结果。
代理合约利用管理要点
① 初始化逻辑合约
攻击者可以接管一个未初始化的逻辑合约,并有可能借此破坏代理合约系统。因此请在摆设后初始化逻辑合约,大概在逻辑合约的构造函数中使用_disableInitializers()来主动禁用初始化。
② 确保代理管理账户的安全
一个可升级的合约系统通常必要一个“代理管理员”的特权角色来管理合约的升级。如果管理密钥走漏,攻击者可以自由地将合约升级为恶意合约,就可以窃取用户的资产。我们发起审慎管理代理管理账户的私钥,以避免任何被黑客攻击的潜在风险。可以使用多签名钱包来防止单点的密钥管理失败。
③为透明代理管理使用单独的账户
代理管理和逻辑管理应该是独立的地点,以防止丢失与逻辑实现的交互。如果代理管理和逻辑管理引用同一个地点,就不会转发任何调用来执行特权功能,从而克制管理功能的更改。
代理合约存储相干
①在代理合约中声明状态变量时要小心审慎
正如Audius黑客事件中所解释的那样,代理合约在声明本身的状态变量时必须审慎。在代理合约中以正常方式声明的状态变量会在读写数据时造成数据冲突。如果代理合约必要一个状态变量,请将该值保存在一个类似EIP1967的存储槽中,以防止在执行逻辑合约代码时发生冲突。
② 维护逻辑合约的变量声明顺序和范例
每个版本的逻辑合约都必须保持状态变量类似的顺序和范例,而且必要在现有变量的末尾添加新的状态变量。否则,委托调用会导致代理合约读取或覆盖不精确的存储值,而且旧的数据可能与新声明的变量相干联,这会给应用程序带来严肃的问题。
③ 在基础合约中包罗存储间隙
逻辑合约必要在合约代码中包罗存储间隙,以便在摆设新的逻辑实现时猜测到新的状态变量。在添加新的状态变量后,必要得当地更新间隙的巨细。
① 可升级合约只能继承自其他可升级合约
与不可升级的合约相比,可升级的合约具有不同的结构。例如,构造函数与改变代理状态不兼容,它使用initialize()函数来设置状态变量。任何继承另一个合约的合约都必要使用其继承合约的initialize()函数来分配各自的变量。当使用OpenZeppelin库或编写本身的代码时,确保可升级的合约只能继承其他的可升级合约。
② 不要在逻辑合约中实例化新合约
通过Solidity创建实例化的合约将不可升级。合约应单独摆设,并将其地点作为参数通报给可升级的逻辑合约,以实现可升级状态。
③ Parent合约初始化风险
当初始化Parent合约时,_{ContractName}_init函数将初始化其Parent合约。调用多个 _{ContractName}_init可能导致Parent合约的第二次初始化。注意__{ContractName}_init_unchained()将只初始化{ContractName}的参数,而不会调用其Parent合约的初始化器。
然而,这并不是一个值得保举的做法,因为所有的Parent合约都必要被初始化,不初始化所需的合约会导致以后的执行问题。
逻辑合约的实现
① 避免使用selfdestruct()或对不信任的合约执行delegatecall()/call()。
如果合约中存在selfdestruct()或delegatecall(),攻击者就有可能利用这些函数来破坏逻辑实现或执行自定义逻辑。开发者应验证用户的输入,不允许合约执行对不信任合约的delegatecall/calls。别的,不发起在逻辑合约中使用delegatecall(),因为在多个合约的代理链中管理存储结构会很贫苦。 写在最后