Solidity的函数修改器的一些最佳实践(二十)|入门系列

2017/5/21 posted in  Solidity入门系列

在上篇系列文章中,我们介绍了函数修改器相关知识。这篇文章,我们来看一些应用函数修改器的最佳实践1。主要是对于单管理员,多管理员,数据检验,以及防重入漏洞等相关问题,应用函数修改器的实践。

验证调用者

对于验证调用者的一种常见情景是,对于敏感操作,如更改合约所有者,销毁部署合约等,我们应该仅允许合约所有者。

pragma solidity ^0.4.0;

contract Ownable {
  address public owner = msg.sender;

  /// @notice 检查必须是合约的所有者
  modifier onlyOwner {
    if (msg.sender != owner) throw;
    _;
  }

  /// @notice 改变合约的拥有者身份
  /// @param _newOwner 新所有者的地址
  function changeOwner(address _newOwner)
  onlyOwner
  {
    if(_newOwner == 0x0) throw;
    owner = _newOwner;
  }
}

继承Ownable合约的,需要在初始化时,设置初始的owner值。使用onlyOwner修改器的函数,都因为仅接受所有者调用,将不会接受来自其它用户或合约的调用,从而实现的访问控制。

限制数据写入

原作者限制数据写入的背景是,为实现合约的升级,且减少合约更新的gas消费。因为迁移合约存储成本高昂。原作者提出的解决方案是,抽象一个数据存储合约EternalStorage,合约内提供KV数据格式存储,及基本KV数值对的操作。

另外有两个合约,ParentOrangizationParent用来管理Organization的生命周期,如创建,存储,查找,升级其实例。Orangization则通过ProposalsLibraryEternalStorage进行互操作,同时也与ITokenLedger的一个实现类进行交互。

所以在这里每个Orangization都将有一个EternalStorage实例,用来读写数据。由此我们需要限制只能由创建EternalStorage的那个Orangization才能进行写入,其它的Orangization及用户则不可写。

实现方式是继承Ownable,并将所有需要保护的函数增加onlyOwner修改器,如setUIntValuesetStringValue等。

import "Ownable.sol";

contract EternalStorage is Ownable {

    function EternalStorage(){
    }

    mapping(bytes32 => uint) UIntStorage;

    function getUIntValue(bytes32 record) constant returns (uint){
        return UIntStorage[record];
    }

    function setUIntValue(bytes32 record, uint value)
    onlyOwner
    {
        UIntStorage[record] = value;
    }

    function deleteUIntValue(bytes32 record)
    onlyOwner
    {
      delete UIntStorage[record];
    }

    mapping(bytes32 => string) StringStorage;

    function getStringValue(bytes32 record) constant returns (string){
        return StringStorage[record];
    }

    function setStringValue(bytes32 record, string value)
    onlyOwner
    {
        StringStorage[record] = value;
    }

    function deleteStringValue(bytes32 record)
    onlyOwner
    {
      delete StringStorage[record];
    }

    mapping(bytes32 => address) AddressStorage;

    function getAddressValue(bytes32 record) constant returns (address){
        return AddressStorage[record];
    }

    function setAddressValue(bytes32 record, address value)
    onlyOwner
    {
        AddressStorage[record] = value;
    }

    function deleteAddressValue(bytes32 record)
    onlyOwner
    {
      delete AddressStorage[record];
    }

    mapping(bytes32 => bytes) BytesStorage;

    function getBytesValue(bytes32 record) constant returns (bytes){
        return BytesStorage[record];
    }

    function setBytesValue(bytes32 record, bytes value)
    onlyOwner
    {
        BytesStorage[record] = value;
    }

    function deleteBytesValue(bytes32 record)
    onlyOwner
    {
      delete BytesStorage[record];
    }

    mapping(bytes32 => bytes32) Bytes32Storage;

    function getBytes32Value(bytes32 record) constant returns (bytes32){
        return Bytes32Storage[record];
    }

    function setBytes32Value(bytes32 record, bytes32 value)
    onlyOwner
    {
        Bytes32Storage[record] = value;
    }

    function deleteBytes32Value(bytes32 record)
    onlyOwner
    {
      delete Bytes32Storage[record];
    }

    mapping(bytes32 => bool) BooleanStorage;

    function getBooleanValue(bytes32 record) constant returns (bool){
        return BooleanStorage[record];
    }

    function setBooleanValue(bytes32 record, bool value)
    onlyOwner
    {
        BooleanStorage[record] = value;
    }

    function deleteBooleanValue(bytes32 record)
    onlyOwner
    {
      delete BooleanStorage[record];
    }

    mapping(bytes32 => int) IntStorage;

    function getIntValue(bytes32 record) constant returns (int){
        return IntStorage[record];
    }

    function setIntValue(bytes32 record, int value)
    onlyOwner
    {
        IntStorage[record] = value;
    }

    function deleteIntValue(bytes32 record)
    onlyOwner
    {
      delete IntStorage[record];
    }
}

为保证EternalStorage的所有者是Organization合约。这里最简单的方式是由Organization来创建存储实例,参考下面的例子:


import "ProposalsLibrary.sol";
import "EternalStorage.sol";

contract Organization
{
  using ProposalsLibrary for EternalStorage;
  EternalStorage public eternalStorage;

  function Organization(address _tokenLedger) {
    eternalStorage = new EternalStorage();
  }
  
  function addProposal(bytes32 _name)
  {
    eternalStorage.addProposal(_name);
  }
  
  [...]
}

因为创建EternalStoragemsg.sender将是Organization合约地址,最终这个地址将会成为唯一能写入的地址。

ProposalsLibray的代码如下,方便理解:

import "EternalStorage.sol";

library ProposalsLibrary {

  function getProposalCount(address _storageContract) constant returns(uint256) 
  {
    return EternalStorage(_storageContract).getUIntValue(sha3("ProposalCount"));
  } 
    
  function addProposal(address _storageContract, bytes32 _name)
  {
    var idx = getProposalCount(_storageContract);
    EternalStorage(_storageContract).setBytes32Value(sha3("proposal_name", idx), _name);
    EternalStorage(_storageContract).setUIntValue(sha3("proposal_eth", idx), 0);
    EternalStorage(_storageContract).setUIntValue(sha3("ProposalCount"), idx + 1);
  }
}

为便于理解上述的整个流程。这里说明一下EternalStorageProposalsLibrary的关系和分别的角色。EternalStorage相当于是一个底层的KV存储结构的数据库,完全不管逻辑。而ProposalsLibrary是一个类似DAO层,在存储层之上的,面向业务对象,方便按对象方式使用的工具类。

注意: 因为调用了一个库函数ProposalsLibrary来附加一些函数到EternalStorage,所以最终有效的消息调用链是: Organization -> ProposalsLibrary -> EternalStorage。由于库函数会传递msg.valuemsg.sender。最终EternalStorage得到的msg.sender将是Organization的地址,而非ProposalsLibrary的地址。

加强版

下面原作者尝试来实现场景,Parent管理Organizations的生命周期。同时为让合约更加轻便,我们也将移除Organization合约中对EternalStorage的依赖,统一交由Parent合约管理。这样Parent将创建EternalStorage的新实例,并通过changeOwner()改变其所有者为Parent新创建的组织:

mapping(bytes32 => address) public Organizations;

  function createOrganization(bytes32 key_)
  {
    var tokenLedger = new TokenLedger();
    var eternalStorage = new EternalStorage();

    var Organization = new Organization(tokenLedger, eternalStorage);
    eternalStorage.changeOwner(Organization);

    Organizations[key_] = Organization;
    OrganizationCreated(Organization, now);
  }

多管理帐户支持

由于msg.sender只能允许合约有一个所有者。但我们可以基于EternalStorage之上,建立一个管理员功能,来管理这些地址。下面是实现了这样一个管理功能的代码:

import "EternalStorage.sol";

library SecurityLibrary
{
  event AdminAdded(address _user);
  event AdminRemoved(address _user);

  // Manages records for admins stored in the format:
  // sha3('admin:', address) -> bool isUserAdmin , e.g. 0xd91cf6dac04d456edc5fcb6659dd8ddedbb26661 -> true

  function getAdminsCount(address _storageContract)
  constant returns(uint256)
  {
    return EternalStorage(_storageContract).getUIntValue(sha3("AdminsCount"));
  }

  function addAdmin(address _storageContract, address _user)
  {
    var userIsAdmin = EternalStorage(_storageContract).getBooleanValue(sha3('admin:', _user));
    if(userIsAdmin)
      throw;

    EternalStorage(_storageContract).setBooleanValue(sha3('admin:', _user), true);

    // Increment the admins count in storage
    var adminsCount = EternalStorage(_storageContract).getUIntValue(sha3("AdminsCount"));
    EternalStorage(_storageContract).setUIntValue(sha3("AdminsCount"), adminsCount + 1);

    AdminAdded(_user);
  }

  function removeAdmin(address _storageContract, address _user)
  {
    var userIsAdmin = EternalStorage(_storageContract).getBooleanValue(sha3('admin:', _user));
    if(!userIsAdmin)
      throw;

    var adminsCount = EternalStorage(_storageContract).getUIntValue(sha3("AdminsCount"));
    if (adminsCount == 1)
      throw;

    EternalStorage(_storageContract).deleteBooleanValue(sha3('admin:', _user));

    // Decrement the admins count in storage
    adminsCount-=1;
    EternalStorage(_storageContract).setUIntValue(sha3("AdminsCount"), adminsCount);

    AdminRemoved(_user);
  }

  function isUserAdmin(address _storageContract, address _user)
  constant returns (bool)
  {
    return EternalStorage(_storageContract).getBooleanValue(sha3('admin:', _user));
  }
}

上面代码中的isUserAdmin方法就可以被应用到一个修改器中。如果我们想要允许在Organization中支持多管理员,我们可以按如下实现:

import "ITokenLedger.sol";
import "ProposalsLibrary.sol";
import "SecurityLibrary.sol";

contract Organization
{
  ITokenLedger public tokenLedger;
  using ProposalsLibrary for address;
  using SecurityLibrary for address;
  address public eternalStorage;

  function Organization(address _tokenLedger, address _eternalStorage) {
    tokenLedger = ITokenLedger(_tokenLedger);
    eternalStorage = _eternalStorage;
  }

  modifier onlyAdmins {
    if (!eternalStorage.isUserAdmin(msg.sender)) throw;
    _
  }

  function addProposal(bytes32 _name)
  onlyAdmins
  {
    eternalStorage.addProposal(_name);
  }
  
  [...]
}

上面的代码中,实现的修改器onlyAdmins,通过使用if (!eternalStorage.isUserAdmin(msg.sender)) throw;来允许多个管理员。

校验数据

修改器除了可以校验数据以外,还可校验数据。下面是一些常见的需要校验的情形:

contract DataVerifiable {

  /// @notice throws if ether was sent accidentally
  modifier refundEtherSentByAccident() {
    if(msg.value > 0) throw;
    _
  }

  /// @notice throw if an address is invalid
  /// @param _target the address to check
  modifier throwIfAddressIsInvalid(address _target) {
    if(_target == 0x0) throw;
    _
  }

  /// @notice throw if the id is invalid
  /// @param _id the ID to validate
  modifier throwIfIsEmptyString(string _id) {
    if(bytes(_id).length == 0) throw;
    _
  }

  /// @notice throw if the uint is equal to zero
  /// @param _id the ID to validate
  modifier throwIfEqualToZero(uint _id) {
    if(_id == 0) throw;
    _
  }

  /// @notice throw if the id is invalid
  /// @param _id the ID to validate
  modifier throwIfIsEmptyBytes32(bytes32 _id) {
    if(_id == "") throw;
    _
  }
}

拼凑所有的到一起

通过上述的授权检查和数据校验的修改器。你可以在执行任何函数前进行检查,来尽可能的保证安全。

原作者使用他的项目中的Organization.addProposal(bytes32)做为一个例子。添加提案时,允许多个管理者中的任一个来创建提案,且创建提案过程中不允许在任何情况下携带ether,提案地址不能为空,下面是具体的代码:

import "ITokenLedger.sol";
import "ProposalsLibrary.sol";
import "SecurityLibrary.sol";
import "DataVerifiable.sol";

contract Organization is DataVerifiable
{
  ITokenLedger public tokenLedger;
  using ProposalsLibrary for address;
  using SecurityLibrary for address;
  address public eternalStorage;

  function Organization(address _tokenLedger, address _eternalStorage) {
    tokenLedger = ITokenLedger(_tokenLedger);
    eternalStorage = _eternalStorage;
  }

  modifier onlyAdmins {
    if (!eternalStorage.isUserAdmin(msg.sender)) throw;
    _
  }

  function addProposal(bytes32 _name)
  onlyAdmins
  refundEtherSentByAccident
  throwIfIsEmptyBytes32(_name)
  {
    eternalStorage.addProposal(_name);
  }
  
  [...]
}

其它修改器最佳实践:防重入漏洞

可重入问题是一个可能引发非常严重的问题,甚至直接导致著名的1.5亿融资DAO项目被盗。因为黑客在触发splitDAO时,让splitDAO产生了一个不断重入的递归调用2。但阻止重复递归调用的实现却非常简单,使用函数修改器即可简单实现:

pragma solidity ^0.4.0;

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        if(locked) throw;
        locked = true;
        _;
        locked = false;
    }

    function payout(){
      //付款逻辑
    }

    //不能重入的逻辑
    function f() noReentrancy returns (uint) {
        payout();
        //通知其它合约
        if(!msg.sender.call()) throw;
        return 7;
    }
}

上述例子中的msg.sender.call(),在向其它合约发起调用时,非常有可能会调回到当前合约,引起重复的payout()。通过引入函数修改器noReentrancy,当f()在执行中时,locked会被置为true,第二次进入将直接因为if(locked) throw;抛出异常,从而不可重入。

关于作者

专注基于以太坊(Ethereum)的相关区块链(Blockchain)技术,了解以太坊,Solidity,Truffle,web3.js。

个人博客: http://tryblockchain.org
版权所有,转载注明出处

参考资料

处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。

友情链接: 区块链技术中文社区    深入浅出区块链