欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

智能合约可能出现的安全问题(二)

程序员文章站 2022-05-01 23:06:12
...

开发理念:

  1. 对可能的错误有所准备。
  2. 谨慎发布智能合约
  3. 保持智能合约的简洁。
  4. 保持更新。
  5. 清楚区块链的特性。

1.对可能的错误有所准备

任何有意义的智能合约或多或少都存在错误。因此你的代码必须能够正确的处理出现的bug和漏洞。始终保证以下规则:

  • 当智能合约出现错误时使智能合约挂起

断路器(暂停合约功能)

由于断路器在满足一定条件时将会停止执行,如果发现错误时可以使用断路器。例如,如果发现错误,大多数操作可能会在合约中被挂起,这是唯一的操作就是撤销。你可以授权给任何你受信任的一方,提供给他们触发断路器的能力,或者设计一个在满足某些条件时自动触发某个断路器的程序规则。

例如:

bool private stopped = false;
address private owner;

modifier isAdmin() {
    if(msg.sender != owner) {
        throw;
    }
    _;
}

function toggleContractActive() isAdmin public
{
    // You can add an additional modifier that restricts stopping a contract to be based on another action, such as a vote of users
    stopped = !stopped;
}

modifier stopInEmergency { if (!stopped) _; }
modifier onlyInEmergency { if (stopped) _; }

function deposit() stopInEmergency public
{
    // some code
}

function withdraw() onlyInEmergency public
{
    // some code
}

  • 防范可能的资金风险:限制(转账)速率、最大(转账)额度

速度碰撞(延迟合约动作)

速度碰撞使动作变慢,所以如果发生了恶意操作便有时间恢复。例如,The DAO 从发起分割DAO请求到真正执行动作需要27天。这样保证了资金在此期间被锁定在合约里,增加了系统的可恢复性。在DAO攻击事件中,虽然在速度碰撞给定的时间段内没有有效的措施可以采取,但结合我们其他的技术,它们是非常有效的。

例如:

struct RequestedWithdrawal {
    uint amount;
    uint time;
}

mapping (address => uint) private balances;
mapping (address => RequestedWithdrawal) private requestedWithdrawals;
uint constant withdrawalWaitPeriod = 28 days; // 4 weeks

function requestWithdrawal() public {
    if (balances[msg.sender] > 0) {
        uint amountToWithdraw = balances[msg.sender];
        balances[msg.sender] = 0; // for simplicity, we withdraw everything;
        // presumably, the deposit function prevents new deposits when withdrawals are in progress

        requestedWithdrawals[msg.sender] = RequestedWithdrawal({
            amount: amountToWithdraw,
            time: now
        });
    }
}

function withdraw() public {
    if(requestedWithdrawals[msg.sender].amount > 0 && now > requestedWithdrawals[msg.sender].time + withdrawalWaitPeriod) {
        uint amountToWithdraw = requestedWithdrawals[msg.sender].amount;
        requestedWithdrawals[msg.sender].amount = 0;

        if(!msg.sender.send(amountToWithdraw)) {
            throw;
        }
    }
}

速率限制

速率限制暂停或需要批准进行实质性更改。 例如,只允许存款人在一段时间内提取总存款的一定数量或百分比(例如,1天内最多100个ether) - 该时间段内的额外提款可能会失败或需要某种特别批准。 或者将速率限制做在合约级别,合约期限内只能发出发送一定数量的代币。

contract CircuitBreaker {
  struct Transfer { uint amount; address to; uint releaseBlock; bool released; bool stopped; }
  Transfer[] public transfers;

  address public curator;
  address public authorizedSender;
  uint public period;
  uint public limit;

  uint public currentPeriodEnd;
  uint public currentPeriodAmount;

  event PendingTransfer(uint id, uint amount, address to, uint releaseBlock);

  function CircuitBreaker(address _curator, address _authorizedSender, uint _period, uint _limit) {
    curator = _curator;
    period = _period;
    limit = _limit;
    authorizedSender = _authorizedSender;
    currentPeriodEnd = block.number + period;
  }

  function transfer(uint amount, address to) {
    if (msg.sender == authorizedSender) {
      updatePeriod();

      if (currentPeriodAmount + amount > limit) {
        uint releaseBlock = block.number + period;
        PendingTransfer(transfers.length, amount, to, releaseBlock);
        transfers.push(Transfer(amount, to, releaseBlock, false, false));
      } else {
        currentPeriodAmount += amount;
        transfers.push(Transfer(amount, to, block.number, true, false));
        if(!to.send(amount)) throw;
      }
    }
  } 
  
  function updatePeriod() {
    if (currentPeriodEnd < block.number) {
      currentPeriodEnd = block.number + period;
      currentPeriodAmount = 0;
    }
  }

  function releasePendingTransfer(uint id) {
    Transfer transfer = transfers[id];
    if (transfer.releaseBlock <= block.number && !transfer.released && !transfer.stopped) {
      transfer.released = true;
      if(!transfer.to.send(transfer.amount)) throw;
    }
  }
  
  function stopTransfer(uint id) {
    if (msg.sender == curator) {
      transfers[id].stopped = true;
    }
  }
}
  • 有效的途径来进行bug修复和功能提升

如果代码中发现了错误或者需要对某些部分做改进都需要更改代码。在以太坊上发现一个错误却没有办法处理他们是太多意义的。

关于如何在以太坊上设计一个合约升级系统是一个正处于积极研究的领域,在这篇文章当中我们没法覆盖所有复杂的领域。然而,这里有两个通用的基本方法。最简单的是专门设计一个注册合约,在注册合约中保存最新版合约的地址。对于合约使用者来说更能实现无缝衔接的方法是设计一个合约,使用它转发调用请求和数据到最新版的合约。

无论采用何种技术,组件之间都要进行模块化和良好的分离,由此代码的更改才不会破坏原有的功能,造成孤儿数据,或者带来巨大的成本。 尤其是将复杂的逻辑与数据存储分开,这样你在使用更改后的功能时不必重新创建所有数据。

当需要多方参与决定升级代码的方式也是至关重要的。根据你的合约,升级代码可能会需要通过单个或多个受信任方参与投票决定。如果这个过程会持续很长时间,你就必须要考虑是否要换成一种更加高效的方式以防止遭受到攻击,例如紧急停止或断路器

例 1:使用注册合约存储合约的最新版本

在这个例子中,调用没有被转发,因此用户必须每次在交互之前都先获取最新的合约地址。

contract SomeRegister {
    address backendContract;
    address[] previousBackends;
    address owner;

    function SomeRegister() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        if (msg.sender != owner) {
            throw;
        }
        _;
    }

    function changeBackend(address newBackend) public
    onlyOwner()
    returns (bool)
    {
        if(newBackend != backendContract) {
            previousBackends.push(backendContract);
            backendContract = newBackend;
            return true;
        }

        return false;
    }
}

这种方法有两个主要的缺点:

1、用户必须始终查找当前合约地址,否则任何未执行此操作的人都可能会使用旧版本的合约 2、在你替换了合约后你需要仔细考虑如何处理原合约中的数据

另外一种方法是设计一个用来转发调用请求和数据到最新版的合约:

例2: 使用DELEGATECALL 转发数据和调用

contract Relay {
    address public currentVersion;
    address public owner;

    modifier onlyOwner() {
        if (msg.sender != owner) {
            throw;
        }
        _;
    }

    function Relay(address initAddr) {
        currentVersion = initAddr;
        owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
    }

    function changeContract(address newVersion) public
    onlyOwner()
    {
        currentVersion = newVersion;
    }

    function() {
        if(!currentVersion.delegatecall(msg.data)) throw;
    }
}

这种方法避免了先前的问题,但也有自己的问题。它使得你必须在合约里小心的存储数据。如果新的合约和先前的合约有不同的存储层,你的数据可能会被破坏。另外,这个例子中的模式没法从函数里返回值,只负责转发它们,由此限制了它的适用性。(这里有一个更复杂的实现 想通过内联汇编和返回大小的注册表来解决这个问题)

无论你的方法如何,重要的是要有一些方法来升级你的合约,否则当被发现不可避免的错误时合约将没法使用。

2. 谨慎发布智能合约

尽量在正式发布智能合约之前修复bug。对智能合约进行彻底的测试,并在任何新的攻击手法被发现后及时的测试

在将大量资金放入合约之前,合约应当进行大量的长时间的测试。

至少应该:

  • 拥有100%测试覆盖率的完整测试套件(或接近它)
  • 在自己的testnet上部署
  • 在公共测试网上部署大量测试和错误奖励
  • 彻底的测试应该允许各种玩家与合约进行大规模互动
  • 在主网上部署beta版以限制风险总额

自动弃用

在合约测试期间,你可以在一段时间后强制执行自动弃用以阻止任何操作继续进行。例如,alpha版本的合约工作几周,然后自动关闭所有除最终退出操作的操作。

modifier isActive() {
    if (block.number > SOME_BLOCK_NUMBER) {
        throw;
    }
    _;
}

function deposit() public
isActive() {
    // some code
}

function withdraw() public {
    // some code
}

限制每个用户/合约的Ether数量

在早期阶段,你可以限制任何用户(或整个合约)的Ether数量 - 以降低风险。

3. 保持智能合约的简洁

复杂只会增加出错的风险。

  • 确保智能合约逻辑简洁
  • 确保合约和函数模块化
  • 使用已经被广泛使用的合约或工具(比如,不要自己写一个随机数生成器)
  • 条件允许的话,清晰明了比性能更重要
  • 只在你系统的去中心化部分使用区块链

4. 保持更新

  • 在任何新的漏洞被发现时检查你的智能合约
  • 尽可能快的将使用到的库或者工具更新到最新
  • 使用最新的安全技术

5. 清楚区块链的特性

尽管你先前所拥有的编程经验同样适用于以太坊开发,但这里仍然有些陷阱你需要留意:

  • 特别小心针对外部合约的调用,因为你可能执行的是一段恶意代码然后更改控制流程
  • 清楚你的public function是公开的,意味着可以被恶意调用。(在以太坊上)你的private data也是对他人可见的
  • 清楚gas的花费和区块的gas limit
    参考文章