balancer / balancer-core

Balancer on the EVM

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Missing validation of function parameters

EricR opened this issue Β· comments

Description

Multiple functions within BPool.sol and BToken.sol were found to be missing adequate validation of parameters. While no instances were found to be directly exploitable, validation of all data is considered to be a best practice and can help prevent user error.

The following instances were discovered during review:

  • The function setSwapFee in BPool.sol does not validate a minimum fee, which according to the FAQ should be 0.01%.
  • Several functions in BPool.sol, such as bind, unbind, gulp, joinPool, exitPool, and others, do not validate parameters. For example, address parameters such as token, and integer parameters such poolAmountIn and poolAmountOut, should all be validated to ensure that they are acceptable values.
  • The functions approve, transfer, and transferFrom in BToken.sol do not validate that address parameters are not equal to zero.

Attack Scenario

Due to human error or a bug in a script, an affected function is called with an incorrect value (e.g., an address of zero), resulting in lost funds and/or unintended contract behavior.

Recommendation

Review all functions within the codebase and ensure that adequate data validation is consistently performed.

Echidna was used to test BToken.sol for desired ERC20 properties.

First a copy of BToken.sol was made, crytic_BToken.sol, and all BToken functions were denoted as public instead of external so that they could be easily tested by Echidna.

Then, echidna-test was run within the contracts directory (see below). Fuzzing with Echidna discovered missing address validation in transfer.

cryticERC20BTokenTransferableBurnable.sol:

import "crytic_BToken.sol"; 

contract CryticInterface{
    address internal crytic_owner = address(0x41414141);
    address internal crytic_user = address(0x42424242);
    address internal crytic_attacker = address(0x43434343);

    uint internal initialTotalSupply;
}

contract CryticTestBToken is CryticInterface, BToken {

    constructor() public {
        initialTotalSupply = 100000000;
        _totalSupply = initialTotalSupply;
        _balance[crytic_owner] = 0;
        _balance[crytic_user] = _totalSupply / 2;
        _balance[crytic_attacker] = _totalSupply / 2;
    }

    /*
    Type: Code quality
    Return: Success
    */
    function crytic_zero_always_empty_ERC20Properties() public returns(bool){
        return balanceOf(address(0x0)) == 0;
    }

    /*
    Type: Code Quality
    Return: 
    */
    function crytic_approve_overwrites() public returns (bool) {
        bool approve_return; 
        approve_return = approve(crytic_user, 10);
        require(approve_return);
        approve_return = approve(crytic_user, 20);
        require(approve_return);
        return allowance(msg.sender, crytic_user) == 20;
    }

    /*
    Type: Undetermined severity
    Return: Success
    */
    function crytic_less_than_total_ERC20Properties() public returns(bool){
        return balanceOf(msg.sender) <= totalSupply();
    }

    /*
    Type: Low severity
    Return: Success
    */
    function crytic_totalSupply_consistant_ERC20Properties() public returns(bool){
        return balanceOf(crytic_owner) + balanceOf(crytic_user) + balanceOf(crytic_attacker) <= totalSupply();
    }

    /*
    Properties: Transferable
    */

    /*
    Type: Code Quality
    Return: Fail or Throw
    */
    function crytic_revert_transfer_to_zero_ERC20PropertiesTransferable() public returns (bool) {
        if (balanceOf(msg.sender) == 0)
          revert();
        return transfer(address(0x0), balanceOf(msg.sender));
    }

    /*
    Type: Code Quality
    Return: Fail or Throw
    */
    function crytic_revert_transferFrom_to_zero_ERC20PropertiesTransferable() public returns (bool) {
        uint balance = balanceOf(msg.sender);
        bool approve_return = approve(msg.sender, balance);
        return transferFrom(msg.sender, address(0x0), balanceOf(msg.sender));
    }

    /*
    Type: ERC20 Standard
    Fire: Transfer(msg.sender, msg.sender, balanceOf(msg.sender))
    Return: Success
    */
    function crytic_self_transferFrom_ERC20PropertiesTransferable() public returns(bool){
        uint balance = balanceOf(msg.sender);
        bool approve_return = approve(msg.sender, balance);
        bool transfer_return = transferFrom(msg.sender, msg.sender, balance);
        return (balanceOf(msg.sender) == balance) && approve_return && transfer_return;
    }


    /*
    Type: ERC20 Standard
    Return: Success
    */
    function crytic_self_transferFrom_to_other_ERC20PropertiesTransferable() public returns(bool){
        uint balance = balanceOf(msg.sender);
        bool approve_return = approve(msg.sender, balance);
        bool transfer_return = transferFrom(msg.sender, crytic_owner, balance);
        return (balanceOf(msg.sender) == 0) && approve_return && transfer_return;
    }

    /*
    Type: ERC20 Standard
    Fire: Transfer(msg.sender, msg.sender, balanceOf(msg.sender))
    Return: Success
    */
    function crytic_self_transfer_ERC20PropertiesTransferable() public returns(bool){
        uint balance = balanceOf(msg.sender);
        bool transfer_return = transfer(msg.sender, balance);
        return (balanceOf(msg.sender) == balance) && transfer_return;
    }

    /*
    Type: ERC20 Standard
    Fire: Transfer(msg.sender, other, 1)
    Return: Success
    */
    function crytic_transfer_to_other_ERC20PropertiesTransferable() public returns(bool){
        uint balance = balanceOf(msg.sender);
        address other = crytic_user;
        if (other == msg.sender) {
           other = crytic_owner;
        }
        if (balance >= 1) {
           bool transfer_other = transfer(other, 1);
           return (balanceOf(msg.sender) == balance-1) && (balanceOf(other) >= 1) && transfer_other;
        }
        return true;
    }

    /*
    Type: ERC20 Standard
    Fire: Transfer(msg.sender, user, balance+1)
    Return: Fail or Throw
    */
    function crytic_revert_transfer_to_user_ERC20PropertiesTransferable() public returns(bool){
        uint balance = balanceOf(msg.sender);
        if (balance == (2 ** 256 - 1))
            return true;
        bool transfer_other = transfer(crytic_user, balance+1);
        return true;
    }

    /*
    Properties: Not Mintable, Not Burnable
    */
    /*
    Type: Undetermined severity
    Return: Success
    */
    function crytic_supply_constant_ERC20PropertiesNotMintableNotBurnable() public returns(bool){
        return initialTotalSupply == totalSupply();
    }
}

cryticERC20BTokenTransferableBurnable.yaml:

seqLen: 50
testLimit: 100000
prefix: "crytic_"
deployer: "0x41414141"
sender: ["0x42424242", "0x43434343"]
dashboard: false
$ SOLC_VERSION=0.5.11 echidna-test cryticERC20BTokenTransferableBurnable.sol CryticTestBToken --config cryticERC20BTokenTransferableBurnable.yaml
Analyzing contract: [...]/cryticERC20BTokenTransferableBurnable.sol:CryticTestBToken
crytic_totalSupply_consistant_ERC20Properties: passed! πŸŽ‰
crytic_approve_overwrites: passed! πŸŽ‰
crytic_self_transferFrom_to_other_ERC20PropertiesTransferable: passed! πŸŽ‰
crytic_zero_always_empty_ERC20Properties: failed!πŸ’₯
  Call sequence:
    transfer(0,16)

crytic_self_transfer_ERC20PropertiesTransferable: passed! πŸŽ‰
crytic_self_transferFrom_ERC20PropertiesTransferable: passed! πŸŽ‰
crytic_revert_transferFrom_to_zero_ERC20PropertiesTransferable: failed with no transactions made ⁉️
crytic_revert_transfer_to_user_ERC20PropertiesTransferable: passed! πŸŽ‰
crytic_supply_constant_ERC20PropertiesNotMintableNotBurnable: passed! πŸŽ‰
crytic_revert_transfer_to_zero_ERC20PropertiesTransferable: passed! πŸŽ‰
crytic_transfer_to_other_ERC20PropertiesTransferable: passed! πŸŽ‰
crytic_less_than_total_ERC20Properties: passed! πŸŽ‰

Seed: -2380744064300327126
  • swapFee now has no minimum
  • we will not be implementing address validation at the contract layer. banning 0x0 addresses is a very opinionated check that we disagree with