crytic / echidna

Ethereum smart contract fuzzer
GNU Affero General Public License v3.0
2.65k stars 346 forks source link

Have multiple workers to speed up fetching slots #1260

Closed viper7882 closed 1 month ago

viper7882 commented 1 month ago

The desired features

  1. Have fetch slot to leverage multiple workers to parallelly retrieve the slots
  2. Cache the slots once they are retrieved

Hi admin,

Echidna is spending at least an hour's time alone to perform fetch slot for onchain fuzzing. See the figure below: GebUniswapV3KeeperFlashProxyETH_Long_Fetching_Slot and eventually turn out to be 1 hour 24 minutes for ~3200 slots: GebUniswapV3KeeperFlashProxyETH_Long_Fetching_Slot_running

It seems like Echidna Fuzzer does not leverage on multiple workers to fetch the slots and causing the inefficiency because I've experimented with or without workers, the time to fetch slot is identical.

Let's have a simple test contract to illustrate. The challenge faced by onchain fuzzing the test contract below is that every time I modify the test contract, I'll have to wait for 1 hour and 24 minutes before the fuzzing begins as the fetch slot does not seem to be cached. This is greatly impacting productivity of onchain fuzzing.

Here's one example to reproduce this 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 victim_contract_address = address(victim_contract_object);

    address public initial_victim_auctionHouse_owner = victim_contract_object.auctionHouse();
    address public initial_victim_caller_owner = victim_contract_object.caller();

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

    fallback() external {}

    receive() external payable {}

    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();
    }

    function echidna_optimize__victim_caller_owner() public returns (bool) {
        // Condition of vulnerability
        return initial_victim_caller_owner != victim_contract_object.caller();
    }
}

Command:

echidna test.sol --contract test --config echidna-config.yaml
ggrieco-tob commented 1 month ago

Have fetch slot to leverage multiple workers to parallelly retrieve the slots

We have not implemented this because it will quickly hit the fetching limits for most of public RPCs

Cache the slots once they are retrieved

This is already implemented, but you don't see it very easily because what I explained in #1261.

viper7882 commented 1 month ago

Thanks @ggrieco-tob. Understand concern over RPC fetching limit and we are good for the cache implementation. Let me close the issue

ggrieco-tob commented 1 month ago

No problem, please keep reporting RPC related issue if you have them, we want to make sure this can be used for real-world testing. I'm also considering writing a small tutorial in building-secure-contracts to help users.