Intro to Security First Development

Intro

This video showcased some vulnerable contracts.

Round 1

Vulnerable contract:

contract Auction {
    address highestBidder;
    uint256 highestBid;

    function bid() payable {
        require(msg.value > highestBid);

        // @audit-issue Highest bidder's `receive()` could always revert -> DoS
        highestBidder.transfer(highestBid);

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

Mitigation: Pull-over-Push pattern

contract Auction {
    address highestBidder;
    uint256 highestBid;
    mapping(address => uint256) refunds;

    function bid() payable {
        require(msg.value > highestBid);

        if (highestBidder != 0) {
            refunds[highestBidder] += highestBid;
        }

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

    function withdrawRefund() external {
        uint256 refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        msg.sender.transfer(refund);
    }
}

Round 2

Vulnerable contract:

struct payee {
    address addr;
    uint256 value;
}

Payee[] payees;

function payOut() external {
    uint256 i;
    // @audit-issue Long `payees` could exceed block gas limit
    while (i < payees.length) {
        payees[i].addr.send(payees[i].value);
        i++;
    }
}

Mitigation: Batches

struct payee {
    address addr;
    uint256 value;
}

Payee[] payees;
uint256 nextPayeeIndex;

function payOut(uint256 batchSize) external {
    uint256 i = nextPayeeIndex;
    while (i < nextPayeeIndex + batchSize) {
        payees[i].addr.send(payees[i].value);
        i++;
    }
    nextPayeeIndex = i;
}

Round 3

Vulnerable contract:

contract Bank {
    mapping(address => uint) private userBalances;

    // @audit-issue Does not follow checks-effects-interactions pattern
    function withdrawBalance() external {
        uint256 amountToWithdraw = userBalances[msg.sender];
        require(msg.sender.call{value: amountToWithdraw}(""));
        userBalances[msg.sender] = 0;
    }
}

Mitigation: Checks-Effects-Interactions pattern

contract Bank {
    mapping(address => uint) private userBalances;

    // @audit-issue Does not follow checks-effects-interactions pattern
    function withdrawBalance() external {
        uint256 amountToWithdraw = userBalances[msg.sender];
        userBalances[msg.sender] = 0;
        require(msg.sender.call{value: amountToWithdraw}(""));
    }
}

Round 4

Vulnerable:

pragma solidity ^0.7.0;

contract Token {
    mapping (address => uint256) public balanceOf;

    function transfer(address _to, uint256 _value) external {
        /* Check if sender has balance */
        // @audit-issue Does not check for the underflow case
        require(balanceOf[msg.sender] - _value >= 0);
        /* Add and subtract new balances */
        balanceOf[msg.sender] -= _value;
        // @audit-issue Could overflow
        balanceOf[_to] += _value;
    }
}

Mitigation:

pragma solidity ^0.7.0;

contract Token {
    mapping (address => uint256) public balanceOf;

    function transfer(address _to, uint256 _value) external {
        /* Check if sender has balance and for overflows/underflows */
        require(balanceOf[msg.sender] >= _value);
        require(balanceOf[_to] + _value >= balanceOf[_to]);
        /* Add and subtract new balances */
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    }
}

Round 5

Vulnerable contract:

contract TokenStore {
    uint256 public price;
    ERC20 public token;
    address public owner;

    function updatePrice(uint256 _price) {
        if (msg.sender == owner) {
            price = _price;
        }
    }

    function buy(uint256 quant) returns (uint256) {
        require(msg.value > quant * price);
        token.transfer(msg.sender. msg.value / price);
    }
}

Attacker can front-run user's buy() transaction by calling updatePrice() and pay higher gas. Attacker can set price much higher so that user receives no tokens.

Mitigation:

  • Permissions on key functions

  • Buyer specifies price

  • Timelocks

Round 6

Vulnerable contract:

contract GetRandomness {
    function random() public returns (uint256 result) {
        // randomly choose a recent blocknumber
        // @audit-issue `block.number` can't be used to generate randomness
        uint256 seed = uint256(block.blockhash(block.number));
        uint256 lookBack = uint256(seed) % 300;

        bytes32 hash = block.blockhash(lookBack);
        uint256 randomNumber = uint256(hash);

        return uint256(randomNumber % 100);
    }
}

Mitigation: Use Chainlink VRF.

Round 7

Vulnerable contract:

contract EthWrapperToken {
    ...
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    ...

    function transfer(address _to, uint256 _value) {
        /* Check if sender has balance */
        require(balanceOf[msg.sender] - _value >= 0);

        /* Add and subtract new balances */
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;

        // @audit-issue Attacker can forcefully send ethers to this contract
        require(this.balance == totalSupply);
    }
}

Mitigation: Use OpenZeppelin ERC20.

Last updated