crytic / echidna

Ethereum smart contract fuzzer
https://secure-contracts.com/program-analysis/echidna/index.html
GNU Affero General Public License v3.0
2.72k stars 362 forks source link

[Bug-Candidate]: A simple contract took onchain fuzzer extremely long time to run due to zero calls per second #1261

Closed viper7882 closed 4 months ago

viper7882 commented 4 months ago

Describe the issue:

Hi admin,

Besides Long time fetching slots reported here, for unknown reason it took extraordinary time to run onchain fuzzing on the even simpler test contract. Essentially there is only one uint256[] memory function and optimization function. The calls/s is always 0. I'm uncertain if zero calls per second is related to larger than normal number of slots fetched for this contract. As I have no control over number of slots fetched, it is best for you to investigate.

I didn't see similar issue on other contracts with uint256[] memory function. Hopefully you could shed some light on what's going on in the fuzzer for this simple contract.

Code example to reproduce the issue:

echidna-config.yaml

contractAddr: '0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84'
rpcBlock: 19305256
rpcUrl: https://eth.drpc.org/
seed: 6368
shrinkLimit: 3000
solcArgs: via-ir
testDestruction: true
testLimit: 100000
testMode: optimization
workers: 12

Hevm.sol

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface IHevm {
    // Set block.timestamp to newTimestamp
    function warp(uint256 newTimestamp) external;

    // Set block.number to newNumber
    function roll(uint256 newNumber) external;

    // Loads a storage slot from an address
    function load(address where, bytes32 slot) external returns (bytes32);

    // Stores a value to an address' storage slot
    function store(address where, bytes32 slot, bytes32 value) external;

    // Signs data (privateKey, digest) => (r, v, s)
    function sign(
        uint256 privateKey,
        bytes32 digest
    ) external returns (uint8 r, bytes32 v, bytes32 s);

    // Gets address for a given private key
    function addr(uint256 privateKey) external returns (address addr);

    // Performs a foreign function call via terminal
    function ffi(
        string[] calldata inputs
    ) external returns (bytes memory result);

    // Performs the next smart contract call with specified `msg.sender`
    function prank(address newSender) external;
}

IHevm constant cheats = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

test.sol

pragma solidity ^0.8.0;

// Shared library
import "./Hevm.sol";

contract DSTest {
    event log                    (string);
    event logs                   (bytes);

    event log_address            (address);
    event log_bytes32            (bytes32);
    event log_int                (int);
    event log_uint               (uint);
    event log_bytes              (bytes);
    event log_string             (string);

    event log_named_address      (string key, address val);
    event log_named_bytes32      (string key, bytes32 val);
    event log_named_decimal_int  (string key, int val, uint decimals);
    event log_named_decimal_uint (string key, uint val, uint decimals);
    event log_named_int          (string key, int val);
    event log_named_uint         (string key, uint val);
    event log_named_bytes        (string key, bytes val);
    event log_named_string       (string key, string val);

    bool public IS_TEST = true;
    bool private _failed;

    address constant HEVM_ADDRESS =
        address(bytes20(uint160(uint256(keccak256('hevm cheat code')))));

    modifier mayRevert() { _; }
    modifier testopts(string memory) { _; }

    function failed() public returns (bool) {
        if (_failed) {
            return _failed;
        } else {
            bool globalFailed = false;
            if (hasHEVMContext()) {
                (, bytes memory retdata) = HEVM_ADDRESS.call(
                    abi.encodePacked(
                        bytes4(keccak256("load(address,bytes32)")),
                        abi.encode(HEVM_ADDRESS, bytes32("failed"))
                    )
                );
                globalFailed = abi.decode(retdata, (bool));
            }
            return globalFailed;
        }
    }

    function fail() internal virtual {
        if (hasHEVMContext()) {
            (bool status, ) = HEVM_ADDRESS.call(
                abi.encodePacked(
                    bytes4(keccak256("store(address,bytes32,bytes32)")),
                    abi.encode(HEVM_ADDRESS, bytes32("failed"), bytes32(uint256(0x01)))
                )
            );
            status; // Silence compiler warnings
        }
        _failed = true;
    }

    function hasHEVMContext() internal view returns (bool) {
        uint256 hevmCodeSize = 0;
        assembly {
            hevmCodeSize := extcodesize(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)
        }
        return hevmCodeSize > 0;
    }

    modifier logs_gas() {
        uint startGas = gasleft();
        _;
        uint endGas = gasleft();
        emit log_named_uint("gas", startGas - endGas);
    }
}

interface IGebUniswapV3KeeperFlashProxyETH {
    receive() external payable;

    function bid(uint256 auctionId, uint256 amount) external;

    function liquidateAndSettleSAFE(address safe) external returns (uint256 auction);

    function multipleBid(uint256[] memory auctionIds, uint256[] memory amounts) external;

    function settleAuction(uint256 auctionId) external;

    function settleAuction(uint256[] memory auctionIds) external;

    function uniswapV3SwapCallback(int256 _amount0, int256 _amount1, bytes memory _data) external;

    function ONE() external view returns (uint256 unknown_ret_var_1);

    function ZERO() external view returns (uint256 unknown_ret_var_1);

    function auctionHouse() external view returns (address unknown_ret_var_1);

    function caller() external view returns (address unknown_ret_var_1);

    function coin() external view returns (address unknown_ret_var_1);

    function coinJoin() external view returns (address unknown_ret_var_1);

    function collateralType() external view returns (bytes32 unknown_ret_var_1);

    function ethJoin() external view returns (address unknown_ret_var_1);

    function liquidationEngine() external view returns (address unknown_ret_var_1);

    function safeEngine() external view returns (address unknown_ret_var_1);

    function uniswapPair() external view returns (address unknown_ret_var_1);

    function weth() external view returns (address unknown_ret_var_1);
}

contract test is DSTest {
    IGebUniswapV3KeeperFlashProxyETH constant GebUniswapv3KeeperFlashProxyETH =
        IGebUniswapV3KeeperFlashProxyETH(payable(0xcDCE3aF4ef75bC89601A2E785172c6B9f65a0aAc));
    IGebUniswapV3KeeperFlashProxyETH victim_contract_object = GebUniswapv3KeeperFlashProxyETH;
    address public initial_victim_auctionHouse_owner = victim_contract_object.auctionHouse();

    constructor() payable {
        // WARNING: Value must be in sync with echidna-config.yaml
        // Block Number
        cheats.roll(19305256);
        // Block Timestamp
        cheats.warp(1708872023);
    }

    function victim_settleAuction(uint256[] memory auctionIds) public {
        victim_contract_object.settleAuction(auctionIds);
    }

    function echidna_optimize__victim_auctionHouse_owner() public returns (bool) {
        // Condition of vulnerability
        return initial_victim_auctionHouse_owner != victim_contract_object.auctionHouse();
    }
}

Command:

echidna test.sol --contract test --config echidna-config.yaml

Version:

Echidna 2.2.2 0.10.1

Relevant log output:

Observation: While the Calls/s remains at 0, the number of Fetched Slots are keep increasing as time passing by. GebUniswapV3KeeperFlashProxyETH_zero_calls_per_seconds_extremely_long_fuzzing_time

ggrieco-tob commented 4 months ago

This is expected, as the settleAuction will fetch a lot of slots in each sequence:

    /// @notice Settle auctions
    /// @param auctionIds IDs of the auctions to be settled
    function settleAuction(uint[] memory auctionIds) public {
        (uint[] memory ids, uint[] memory bidAmounts, uint totalAmount) = _getOpenAuctionsBidSizes(auctionIds);
        require(totalAmount > ZERO, "GebUniswapV3KeeperFlashProxyETH/all-auctions-already-settled");

        bytes memory callbackData = abi.encodeWithSelector(this.multipleBid.selector, ids, bidAmounts);

        _startSwap(totalAmount, callbackData);
    }

It seems the correct way to run this will be to either use a local node or optimize the fuzzing to select valid auction numbers for the list.

viper7882 commented 4 months ago

I see, thank you @ggrieco-tob for your help. Closing this issue