在上篇系列文章中,我们介绍了函数修改器相关知识。这篇文章,我们来看一些应用函数修改器的最佳实践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数值对的操作。
另外有两个合约,Parent
和Orangization
。Parent
用来管理Organization
的生命周期,如创建,存储,查找,升级其实例。Orangization
则通过ProposalsLibrary
与EternalStorage
进行互操作,同时也与ITokenLedger
的一个实现类进行交互。
所以在这里每个Orangization
都将有一个EternalStorage
实例,用来读写数据。由此我们需要限制只能由创建EternalStorage
的那个Orangization
才能进行写入,其它的Orangization
及用户则不可写。
实现方式是继承Ownable
,并将所有需要保护的函数增加onlyOwner
修改器,如setUIntValue
,setStringValue
等。
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);
}
[...]
}
因为创建EternalStorage
的msg.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);
}
}
为便于理解上述的整个流程。这里说明一下
EternalStorage
和ProposalsLibrary
的关系和分别的角色。EternalStorage
相当于是一个底层的KV存储结构的数据库,完全不管逻辑。而ProposalsLibrary
是一个类似DAO层,在存储层之上的,面向业务对象,方便按对象方式使用的工具类。
注意: 因为调用了一个库函数ProposalsLibrary
来附加一些函数到EternalStorage
,所以最终有效的消息调用链是: Organization
-> ProposalsLibrary
-> EternalStorage
。由于库函数会传递msg.value
和msg.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
版权所有,转载注明出处
参考资料
处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。