作者:知道創宇404區塊鏈安全研究團隊
時間:2018年11月12日
English Version: http://www.bjnorthway.com/754/
項目地址:https://github.com/knownsec/Ethereum-Smart-Contracts-Security-CheckList

在以太坊合約審計checkList中,我將以太坊合約審計中遇到的問題分為5大種,包括編碼規范問題、設計缺陷問題、編碼安全問題、編碼設計問題、編碼問題隱患。其中涵蓋了超過29種會出現以太坊智能合約審計過程中遇到的問題。幫助智能合約的開發者和安全工作者快速入門智能合約安全。

本CheckList在完成過程中參考并整理兼容了各大區塊鏈安全研究團隊的研究成果,CheckList中如有不完善/錯誤的地方也歡迎大家提issue.

由于本文的目的主要是CheckList,所以文中不會包含太詳細的漏洞/隱患信息,大部分漏洞分析在掃描報告中會有所提及。

1、編碼規范問題

(1) 編譯器版本

合約代碼中,應指定編譯器版本。建議使用最新的編譯器版本

pragma solidity ^0.4.25;

老版本的編譯器可能會導致各種已知的安全問題,例如http://www.bjnorthway.com/631/#44-dividenddistributor

v0.4.23更新了一個編譯器漏洞,在這個版本中如果同時使用了兩種構造函數,即

contract a {
    function a() public{
        ...
    }
    constructor() public{
        ...
    }
}

會忽略其中的一個構造函數,該問題只影響v0.4.22

v0.4.25修復了下面提到的未初始化存儲指針問題。

https://etherscan.io/solcbuginfo

(2) 構造函數書寫問題

對應不同編譯器版本應使用正確的構造函數,否則可能導致合約所有者變更

在小于0.4.22版本的solidify編譯器語法要求中,合約構造函數必須和合約名字相等, 名字受到大小寫影響。如:

contract Owned {
    function Owned() public{
    }

在0.4.22版本以后,引入了constructor關鍵字作為構造函數聲明,但不需要function

contract Owned {
    constructor() public {
    }

如果沒有按照對應的寫法,構造函數就會被編譯成一個普通函數,可以被任意人調用,會導致owner權限被竊取等更嚴重的后果。

(3) 返回標準

遵循ERC20規范,要求transfer、approve函數應返回bool值,需要添加返回值代碼

function transfer(address _to, uint256 _value) public returns (bool success)

而transferFrom返回結果應該和transfer返回結果一致。

(4) 事件標準

遵循ERC20規范,要求transfer、approve函數觸發相應的事件

function approve(address _spender, uint256 _value) public returns (bool success){
    allowance[msg.sender][_spender] = _value;
    emit Approval(msg.sender, _spender, _value)
    return true

(5) 假充值問題

轉賬函數中,對余額以及轉賬金額的判斷,需要使用require函數拋出錯誤,否則會錯誤的判斷為交易成功

function transfer(address _to, uint256 _value) returns (bool success) {
    if (balances[msg.sender] >= _value && _value > 0) {
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value);
        return true;
    } else { return false; }
}

上述代碼可能會導致假充值。

正確代碼如下:

function transfer(address _to, uint256 _amount)  public returns (bool success) {
    require(_to != address(0));
    require(_amount <= balances[msg.sender]);

    balances[msg.sender] = balances[msg.sender].sub(_amount);
    balances[_to] = balances[_to].add(_amount);
    emit Transfer(msg.sender, _to, _amount);
    return true;
}

2、設計缺陷問題

(1) approve授權函數條件競爭

approve函數中應避免條件競爭。在修改allowance前,應先修改為0,再修改為_value。

這個漏洞的起因是由于底層礦工協議中為了鼓勵礦工挖礦,礦工可以自己決定打包什么交易,為了收益更大,礦工一般會選擇打包gas price更大的交易,而不會依賴交易順序的前后。

通過置0的方式,可以在一定程度上緩解條件競爭中產生的危害,合約管理人可以通過檢查日志來判斷是否有條件競爭情況的發生,這種修復方式更大的意義在于,提醒使用approve函數的用戶,該函數的操作在一定程度上是不可逆的。

function approve(address _spender, uint256 _value) public returns (bool success){
    allowance[msg.sender][_spender] = _value;
    return true

上述代碼就有可能導致條件競爭。

應在approve中加入

require((_value == 0) || (allowance[msg.sender][_spender] == 0));

將allowance先改為0再改為對應數字

(2) 循環Dos問題

[1] 循環消耗問題

在合約中,不推薦使用太大次的循環

在以太坊中,每一筆交易都會消耗一定量的gas,而實際消耗量是由交易的復雜度決定的,循環次數越大,交易的復雜度越高,當超過允許的最大gas消耗量時,會導致交易失敗。

真實世界事件

Simoleon (SIM)

Pandemica

[2] 循環安全問題

合約中,應盡量避免循環次數受到用戶控制,攻擊者可能會使用過大的循環來完成Dos攻擊

當用戶需要同時向多個賬戶轉賬,我們需要對目標賬戶列表遍歷轉賬,就有可能導致Dos攻擊。

function Distribute(address[] _addresses, uint256[] _values) payable returns(bool){
    for (uint i = 0; i < _addresses.length; i++) {
        transfer(_addresses[i], _values[i]);
    }
    return true;
}

遇到上述情況是,推薦使用withdrawFunds來讓用戶取回自己的代幣,而不是發送給對應賬戶,可以在一定程序上減少危害。

上述代碼如果控制函數調用,那么就可以構造巨大循環消耗gas,造成Dos問題

3、編碼安全問題

(1) 溢出問題

[1] 算術溢出

在調用加減乘除時,應使用safeMath庫來替代,否則容易導致算數上下溢,造成不可避免的損失

pragma solidity ^0.4.18;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  function Token(uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0); //可以通過下溢來繞過判斷
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public constant returns (uint balance) {
    return balances[_owner];
  }
}

balances[msg.sender] - _value >= 0可以通過下溢來繞過判斷。

通常的修復方式都是使用openzeppelin-safeMath,但也可以通過對不同變量的判斷來限制,但很難對乘法和指數做什么限制。

正確的寫法如下:

function transfer(address _to, uint256 _amount)  public returns (bool success) {
    require(_to != address(0));
    require(_amount <= balances[msg.sender]);

    balances[msg.sender] = balances[msg.sender].sub(_amount);
    balances[_to] = balances[_to].add(_amount);
    emit Transfer(msg.sender, _to, _amount);
    return true;
}

真實世界事件

Hexagon

SMT/BEC

[2] 鑄幣燒幣溢出問題

鑄幣函數中,應對totalSupply設置上限,避免因為算術溢出等漏洞導致惡意鑄幣增發

function TokenERC20(
    uint256 initialSupply,
    string tokenName,
    string tokenSymbol
) public {
    totalSupply = initialSupply * 10 ** uint256(decimals);  
    balanceOf[msg.sender] = totalSupply;                
    name = tokenName;                                   
    symbol = tokenSymbol;                               
}

上述代碼中就未對totalSupply做限制,可能導致指數算數上溢。

正確寫法如下:

contract OPL {
    // Public variables
    string public name;
    string public symbol;
    uint8 public decimals = 18; // 18 decimals
    bool public adminVer = false;
    address public owner;
    uint256 public totalSupply;
    function OPL() public {
        totalSupply = 210000000 * 10 ** uint256(decimals);      
        ...                                 
}

真實世界事件

(2) 重入漏洞

智能合約中避免使用call來交易,避免重入漏洞

在智能合約中提供了call、send、transfer三種方式來交易以太坊,其中call最大的區別就是沒有限制gas,而其他兩種在gas不夠的情況下都會報out of gas。

重入漏洞有幾大特征。 1、使用了call函數作為轉賬函數 2、沒有限制call函數的gas 3、扣余額在轉賬之后 4、call時加入了()來執行fallback函數

function withdraw(uint _amount) {
    require(balances[msg.sender] >= _amount);
    msg.sender.call.value(_amount)();
    balances[msg.sender] -= _amount;
}

上述代碼就是一個簡單的重入漏洞的demo。通過重入注入轉賬,將大量合約代幣遞歸轉賬而出。

對于可能存在的重入問題,盡可能的使用transfer函數完成轉賬,或者限制call執行的gas,都可以有效的減少該問題的危害。

contract EtherStore {

    // initialise the mutex
    bool reEntrancyMutex = false;
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(!reEntrancyMutex);
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
        // set the reEntrancy mutex before the external call
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);
        // release the mutex after the external call
        reEntrancyMutex = false; 
    }
 }

上述代碼是一種用互斥鎖來避免遞歸防護方式。

真實世界事件

The Dao

(3) call注入

call函數調用時,應該做嚴格的權限控制,或直接寫死call調用的函數

在EVM的設計中,如果call的參數data是0xdeadbeef(假設的一個函數名) + 0x0000000000.....01,這樣的話就是調用函數

call注入可能導致代幣竊取,權限繞過,通過call注入可以調用私有函數,甚至部分高權限函數。

addr.call(data);             
addr.delegatecall(data); 
addr.callcode(data);     

如delegatecall,在合約內必須調用其它合約時,可以使用關鍵字library,這樣可以確保合約是無狀態而且不可自毀的。通過強制設置合約為無狀態可以一定程度上緩解儲存環境的復雜性,防止攻擊者通過修改狀態來攻擊合約。

真實世界事件

call注入

(4) 權限控制

合約中不同函數應設置合理的權限

檢查合約中各函數是否正確使用了public、private等關鍵詞進行可見性修飾,檢查合約是否正確定義并使用了modifier對關鍵函數進行訪問限制,避免越權導致的問題。

function initContract() public {
    owner = msg.sender;
}

上述代碼作為初始函數不應該為public。

真實世界事件

Parity Multi-sig bug 1

Parity Multi-sig bug 2

Rubixi

(5) 重放攻擊

合約中如果涉及委托管理的需求,應注意驗證的不可復用性,避免重放攻擊

在資產管理體系中,常有委托管理的情況,委托人將資產給受托人管理,委托人支付一定的費用給受托人。這個業務場景在智能合約中也比較普遍。

這里舉例子為transferProxy函數,該函數用于當user1轉token給user3,但沒有eth來支付gasprice,所以委托user2代理支付,通過調用transferProxy來完成。

function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,
    uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){

    if(balances[_from] < _fee + _value 
        || _fee > _fee + _value) revert();

    uint256 nonce = nonces[_from];
    bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));
    if(_from != ecrecover(h,_v,_r,_s)) revert();

    if(balances[_to] + _value < balances[_to]
        || balances[msg.sender] + _fee < balances[msg.sender]) revert();
    balances[_to] += _value;
    emit Transfer(_from, _to, _value);

    balances[msg.sender] += _fee;
    emit Transfer(_from, msg.sender, _fee);

    balances[_from] -= _value + _fee;
    nonces[_from] = nonce + 1;
    return true;
}

這個函數的問題在于nonce值是可以預判的,其他變量不變的情況下,可以進行重放攻擊,多次轉賬。

漏洞來自于Defcon2018演講議題

Replay Attacks on Ethereum Smart Contracts

Replay Attacks on Ethereum Smart Contracts pdf

4、編碼設計問題

(1) 地址初始化問題

涉及到地址的函數中,建議加入require(_to!=address(0))驗證,有效避免用戶誤操作或未知錯誤導致的不必要的損失

由于EVM在編譯合約代碼時初始化的地址為0,如果開發者在代碼中初始化了某個address變量,但未賦予初值,或用戶在發起某種操作時,誤操作未賦予address變量,但在下面的代碼中需要對這個變量做處理,就可能導致不必要的安全風險。

這樣的檢查可以以最簡單的方式避免未知錯誤、短地址攻擊等問題的發生。

(2) 判斷函數問題

及到條件判斷的地方,使用require函數而不是assert函數,因為assert會導致剩余的gas全部消耗掉,而他們在其他方面的表現都是一致的

值得注意的是,assert存在強制一致性,對于固定變量的檢查來說,assert可以用于避免一些未知的問題,因為他會強制終止合約并使其無效化,在一些固定條件下,assert更適用。

(3) 余額判斷問題

不要假設合約創建時余額為0,可以強制轉賬

謹慎編寫用于檢查賬戶余額的不變量,因為攻擊者可以強制發送wei到任何賬戶,即使fallback函數throw也不行。

攻擊者可以用1wei來創建合約,然后調用selfdestruct(victimAddress)來銷毀,這樣余額就會強制轉移給目標,而且目標合約沒有代碼執行,無法阻止。

值得注意的是,在打包過程中,攻擊者可以通過條件競爭在合約創建前轉賬,這樣在合約創建時余額就不為0.

(4) 轉賬函數問題

在完成交易時,默認情況下推薦使用transfer而不是send完成交易

當transfer或者send函數的目標是合約時,會調用合約的fallback函數,但fallback函數執行失敗時。

transfer會拋出錯誤并自動回滾,而send會返回false,所以在使用send時需要判斷返回類型,否則可能會導致轉賬失敗但余額減少的情況。

function withdraw(uint256 _amount) public {
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] -= _amount;
    etherLeft -= _amount;
    msg.sender.send(_amount);  
}

上面給出的代碼中使用 send() 函數進行轉賬,因為這里沒有驗證 send() 返回值,如果msg.sender 為合約賬戶 fallback() 調用失敗,則 send() 返回false,最終導致賬戶余額減少了,錢卻沒有拿到。

(5) 代碼外部調用設計問題

對于外部合約優先使用pull而不是push

在進行外部調用時,總會有意無意的失敗,為了避免發生未知的損失,應該經可能的把對外的操作改為用戶自己來取。 錯誤樣例:

contract auction {
    address highestBidder;
    uint highestBid;

    function bid() payable {
        if (msg.value < highestBid) throw;

        if (highestBidder != 0) {
            if (!highestBidder.send(highestBid)) { // 可能會發生錯誤
                throw;
            }
        }

       highestBidder = msg.sender;
       highestBid = msg.value;
    }
}

當需要向某一方轉賬時,將轉賬改為定義withdraw函數,讓用戶自己來執行合約將余額取出,這樣可以最大程度的避免未知的損失。

范例代碼:

contract auction {
    address highestBidder;
    uint highestBid;
    mapping(address => uint) refunds;

    function bid() payable external {
        if (msg.value < highestBid) throw;

        if (highestBidder != 0) {
            refunds[highestBidder] += highestBid; // 記錄在refunds中
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        if (!msg.sender.send(refund)) {
            refunds[msg.sender] = refund; // 如果轉賬錯誤還可以挽回
        }
    }
}

(6) 錯誤處理

合約中涉及到call等在address底層操作的方法時,做好合理的錯誤處理

address.call()
address.callcode()
address.delegatecall()
address.send()

這類操作如果遇到錯誤并不會拋出異常,而是會返回false并繼續執行。

function withdraw(uint256 _amount) public {
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] -= _amount;
    etherLeft -= _amount;
    msg.sender.send(_amount);  
}

上述代碼沒有校驗send的返回值,如果msg.sender是合約賬戶,fallback調用失敗時,send返回false。

所以當使用上述方法時,需要對返回值做檢查并做錯誤處理。

if(!someAddress.send(55)) {
    // Some failure code
}

http://www.bjnorthway.com/607/#4-unchecked-return-values-for-low-level-calls

值得注意的一點是,作為EVM設計的一部分,下面這些函數如果調用的合約不存在,將會返回True

call、delegatecall、callcode、staticcall

在調用這類函數之前,需要對地址的有效性做檢查。

(7) 弱隨機數問題

智能合約上隨機數生成方式需要更多考量

Fomo3D合約在空投獎勵的隨機數生成中就引入了block信息作為隨機數種子生成的參數,導致隨機數種子只受到合約地址影響,無法做到完全隨機。

function airdrop()
    private 
    view 
    returns(bool)
{
    uint256 seed = uint256(keccak256(abi.encodePacked(

        (block.timestamp).add
        (block.difficulty).add
        ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
        (block.gaslimit).add
        ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
        (block.number)

    )));
    if((seed - ((seed / 1000) * 1000)) < airDropTracker_)
        return(true);
    else
        return(false);
}

上述這段代碼直接導致了Fomo3d薅羊毛事件的誕生。真實世界損失巨大,超過數千eth。

所以在合約中關于這樣的應用時,考慮更合適的生成方式和合理的利用順序非常重要。

這里提供一個比較合理的隨機數生成方式hash-commit-reveal,即玩家提交行動計劃,然后行動計劃hash后提交給后端,后端生成相應的hash值,然后生成對應的隨機數reveal,返回對應隨機數commit。這樣,服務端拿不到行動計劃,客戶端也拿不到隨機數。

有一個很棒的實現代碼是dice2win的隨機數生成代碼。

hash-commit-reveal最大的問題在于服務端會在用戶提交之后短暫的獲得整個過程中的所有數據,如果惡意進行選擇中止攻擊,也在一定程度上破壞了公平性。詳細分析見智能合約游戲之殤——Dice2win安全分析

當然hash-commit在一些簡單場景下也是不錯的實現方式。即玩家提交行動計劃的hash,然后生成隨機數,然后提交行動計劃。

真實世界事件

Fomo3d薅羊毛

Last Winner

(8) 變量覆蓋問題

在合約中避免array變量key可以被控制

map[uint256(msg.sender)+x] = blockNum;

在EVM中數組和其他類型不同,因為數組時動態大小的,所以數組類型的數據計算方式為

address(map_data) = sha3(key)+offset

其中key就是map變量定義的位置,也就是1,offset就是數組中的偏移,比如map[2],offset就是2.

map[2]的地址就是sha3(1)+2,假設map[2]=2333,則storage[sha3(1)+2]=2333

這樣一來就出現問題了,由于offset我們可控,我們就可以向storage的任意地址寫值。

這就可能覆蓋storage的任意地址的值,影響代碼本身的邏輯,導致進一步更嚴重的問題。

詳細的原理可以看

5、編碼問題隱患

(1) 語法特性問題

在智能合約中小心整數除法的向下取整問題

在智能合約中,所有的整數除法都會向下取整到最接近的整數,當我們需要更高的精度時,我們需要使用乘數來加大這個數字。

該問題如果在代碼中顯式出現,編譯器會提出問題警告,無法繼續編譯,但如果隱式出現,將會采取向下取整的處理方式。

錯誤樣例

uint x = 5 / 2; // 2

正確代碼

uint multiplier = 10;
uint x = (5 * multiplier) / 2;

(2) 數據私密問題

注意鏈上的所有數據都是公開的

在合約中,所有的數據包括私有變量都是公開的,不可以將任何有私密性的數據儲存在鏈上。

(3) 數據可靠性

合約中不應該讓時間戳參與到代碼中,容易受到礦工的干擾,應使用block.height等不變的數據

uint someVariable = now + 1;

if (now % 2 == 0) { // now可能被礦工控制

}

(4) gas消耗優化

對于某些不涉及狀態變化的函數和變量可以加constant來避免gas的消耗

contract EUXLinkToken is ERC20 {
    using SafeMath for uint256;
    address owner = msg.sender;

    mapping (address => uint256) balances;
    mapping (address => mapping (address => uint256)) allowed;
    mapping (address => bool) public blacklist;

    string public constant name = "xx";
    string public constant symbol = "xxx";
    uint public constant decimals = 8;
    uint256 public totalSupply = 1000000000e8;
    uint256 public totalDistributed = 200000000e8;
    uint256 public totalPurchase = 200000000e8;
    uint256 public totalRemaining = totalSupply.sub(totalDistributed).sub(totalPurchase);

    uint256 public value = 5000e8;
    uint256 public purchaseCardinal = 5000000e8;

    uint256 public minPurchase = 0.001e18;
    uint256 public maxPurchase = 10e18;

(5) 合約用戶

合約中,應盡量考慮交易目標為合約時的情況,避免因此產生的各種惡意利用

contract Auction{
    address public currentLeader;
    uint256 public hidghestBid;

    function bid() public payable {
        require(msg.value > highestBid);
        require(currentLeader.send(highestBid));
        currentLeader = msg.sender;
        highestBid = currentLeader;
    }
}

上述合約就是一個典型的沒有考慮合約為用戶時的情況,這是一個簡單的競拍爭奪王位的代碼。當交易ether大于合約內的highestBid,當前用戶就會成為合約當前的"王",他的交易額也會成為新的highestBid。

contract Attack {
    function () { revert(); }

    function Attack(address _target) payable {
        _target.call.value(msg.value)(bytes4(keccak256("bid()")));
    }
}

但當新的用戶試圖成為新的“王”時,當代碼執行到require(currentLeader.send(highestBid));時,合約中的fallback函數會觸發,如果攻擊者在fallback函數中加入revert()函數,那么交易就會返回false,即永遠無法完成交易,那么當前合約就會一直成為合約當前的"王"。

(6) 日志記錄

關鍵事件應有Event記錄,為了便于運維監控,除了轉賬,授權等函數以外,其他操作也需要加入詳細的事件記錄,如轉移管理員權限、其他特殊的主功能

function transferOwnership(address newOwner) onlyOwner public {
    owner = newOwner;
    emit OwnershipTransferred(owner, newowner);
    }

(7) 回調函數

合約中定義Fallback函數,并使Fallback函數盡可能的簡單

Fallback會在合約執行發生問題時調用(如沒有匹配的函數時),而且當調用send或者transfer函數時,只有2300gas 用于失敗后fallback函數執行,2300 gas只允許執行一組字節碼指令,需要謹慎編寫,以免gas不夠用。

部分樣例:

function() payable { LogDepositReceived(msg.sender); }

function() public payable{ revert();};

(8) Owner權限問題

避免owner權限過大

部分合約owner權限過大,owner可以隨意操作合約內各種數據,包括修改規則,任意轉賬,任意鑄幣燒幣,一旦發生安全問題,可能會導致嚴重的結果。

關于owner權限問題,應該遵循幾個要求: 1、合約創造后,任何人不能改變合約規則,包括規則參數大小等 2、只允許owner從合約中提取余額

(9) 用戶鑒權問題

合約中不要使用tx.origin做鑒權

tx.origin代表最初始的地址,如果用戶a通過合約b調用了合約c,對于合約c來說,tx.origin就是用戶a,而msg.sender才是合約b,對于鑒權來說,這是十分危險的,這代表著可能導致的釣魚攻擊。

下面是一個范例:

pragma solidity >0.4.24;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
    address owner;

    constructor() public {
        owner = msg.sender;
    }

    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

我們可以構造攻擊合約

pragma solidity >0.4.24;

interface TxUserWallet {
    function transferTo(address dest, uint amount) external;
}

contract TxAttackWallet {
    address owner;

    constructor() public {
        owner = msg.sender;
    }

    function() external {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}

當用戶被欺騙調用攻擊合約,則會直接繞過鑒權而轉賬成功,這里應使用msg.sender來做權限判斷。

https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin

(10) 條件競爭問題

合約中盡量避免對交易順序的依賴

在智能合約中,經常容易出現對交易順序的依賴,如占山為王規則、或最后一個贏家規則。都是對交易順序有比較強的依賴的設計規則,但以太坊本身的底層規則是基于礦工利益最大法則,在一定程度的極限情況下,只要攻擊者付出足夠的代價,他就可以一定程度控制交易的順序。開發者應避免這個問題。

真實世界事件

Fomo3d事件

(11) 未初始化的儲存指針

避免在函數中初始化struct變量

在solidity中允許一個特殊的數據結構為struct結構體,而函數內的局部變量默認使用storage或memory儲存。

而存在storage(存儲器)和memory(內存)是兩個不同的概念,solidity允許指針指向一個未初始化的引用,而未初始化的局部stroage會導致變量指向其他儲存變量,導致變量覆蓋,甚至其他更嚴重的后果。

pragma solidity ^0.4.0;

contract Test {

        address public owner;
        address public a;

        struct Seed {
                address x;
                uint256 y;
        }

        function Test() {
                owner = msg.sender;
                a = 0x1111111111111111111111111111111111111111;
        }

        function fake_foo(uint256 n) public {
                Seed s;
                s.x = msg.sender;
                s.y = n;
        }
}

上面代碼編譯后,s.x和s.y會錯誤的指向ownner和a。

攻擊者在執行fake_foo之后,會將owner修改為自己。

上述問題在最新版的0.4.25版本被修復。

以太坊合約審計checkList審計系列報告

REF


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/741/