With the release of version 2.1.0 of Echidna, our fuzzing tool for Ethereum smart contracts, we’ve introduced new features for direct retrieval of on-chain data, such as contract code and storage slot values. This data can be used to fuzz deployed contracts in their on-chain state or to test how new code integrates with existing contracts.
Echidna now has the capability to recreate real-world hacks by fuzzing contract interfaces and on-chain code. In this blog post, we’ll demonstrate how the 2022 Stax Finance hack was reproduced using only Echidna to find and exploit the vulnerability. This incident involved a missing validation check in the StaxLPStaking
contract, which led to the theft of 321,154 xLP tokens, worth approximately $2.3 million at the time of the attack.
Echidna’s “optimization mode” will automatically discover transaction sequences that maximize or minimize the outcome of a custom function. In this case, we’ll simply ask it to maximize an attacker’s balance and let it do the rest of the work.
Recreating the Stax Finance exploit
To reproduce the Stax Finance exploit using Echidna, we need:
- A contract to be fuzzed by Echidna that wraps the target Stax contract and related contracts (figure 1)
- An Echidna configuration file that contains the block number from before the attack took place and an RPC provider to get on-chain information (figure 2)
Figure 1 shows a simplified version of the fuzzing contract contract, and figure 2 shows the configuration file. You can find the full contract and configuration file here.
contract StaxExploit { IStaxLP StaxLP = IStaxLP(0xBcB8b7FC9197fEDa75C101fA69d3211b5a30dCD9); IStaxLPStaking StaxLPStaking = IStaxLPStaking(0xd2869042E12a3506100af1D192b5b04D65137941); ... constructor() { // Using HEVM to set the block.number and block.timestamp hevm.warp(1665493703); hevm.roll(15725066); // setting up initial balances ... } function getBalance() internal returns (uint256) { return StaxLP.balanceOf(address(this)); } function stake(uint256 _amount) public { _amount = (_amount % getBalance()) + 1; StaxLPStaking.stake(_amount); } // Other functions wrappers ... function migrateStake( address oldStaking, uint256 amount ) public { StaxLPStaking.migrateStake(oldStaking, amount); } function migrateWithdraw( address staker, uint256 amount ) public { StaxLPStaking.migrateWithdraw(staker, amount); } fallback() external payable {} // The optimization function function echidna_optimize_extracted_profit() public returns (int256) { return (int256(StaxLP.balanceOf(address(this))) - int256(initialAmount)); } }
In the fuzzing contract, we added a function called echidna_optimize_extracted_profit()
, allowing Echidna to monitor the profit for the current transaction sequence and identify the most profitable one.
testMode: optimization testLimit: 1000000 corpusDir: corpus-stax rpcUrl: https://.../ rpcBlock: 15725066
As shown in the configuration file, we set Echidna to run in optimization mode to maximize the profit function.
Next, we ran Echidna on the fuzzing contract using the command in figure 3.
$ echidna ./StaxExploit.sol --contract StaxExploit --config echidna-config.yaml
Echidna’s optimizer generates random sequences of function calls with varying arguments, calculating the return value of the echidna_optimize_extracted_profit()
function for each sequence. At the end of the run, it discards any unnecessary or reverting calls from the sequence of transactions, leaving only those calls that maximize the profit.
Thus, with our fuzzing contract and the profit function, Echidna can swiftly discover the correct sequence of transactions to reproduce the hack, without needing prior knowledge of the actual contract exploit.
Nitty-gritty details
Now that we’ve given a high-level overview of how Echidna can recreate the exploit, let’s dive into some technical details for readers interested in trying this out on their own.
To set up the fuzzing contract, we used Slither’s code generation utilities. This let us get the target contract’s interface and deployment address, in addition to other necessary interfaces and addresses (e.g., ERC-20 tokens, other contracts, and user-defined data types), from Etherscan. We also created wrappers for Echidna to call the contract functions, and we added our echidna_optimize_extracted_profit()
function.
We took advantage of Echidna’s ability to use hevm cheat codes for manipulating the execution environment. This involved setting the block number and block timestamp to a point in time just prior to the actual exploit. To streamline the use of hevm cheat codes, we used helpers from our properties
repository and imported the HEVM.sol
helper.
In setting up the configuration file, we configured testMode
to optimization
. We also assigned the RPC provider and block number (indicated by rpcUrl
and rpcBlock
parameters, respectively) for Echidna to fetch the on-chain information. To prevent an indefinite runtime in case Echidna doesn’t find the exploit, we set an upper limit of one million test runs through the testLimit
parameter. The resulting corpus was stored in the corpus-stax
directory, as specified in the corpusDir
parameter.
Limitations and challenges
While Echidna is a powerful tool, it’s not without limitations and challenges:
- Echidna might not find all vulnerabilities. Since fuzz testing can’t guarantee complete coverage, it’s crucial to augment Echidna with other security testing methods like static analysis, formal verification, and even unit testing (e.g., 100% branch coverage, testing for edge cases, positive and negative tests, etc.), for a comprehensive analysis.
- Complex contracts may require more time. Depending on the complexity of the smart contract, it might take Echidna longer to discover vulnerabilities.
- Fetching contracts and slots from the network can be slow. API rate limits can hinder the process of acquiring on-chain information for contracts using numerous storage slots. There are ongoing discussions on how to mitigate this issue.
- Customization may be needed. In certain cases, you may need to tailor Echidna’s configuration or test harnesses to suit your specific use case.
To overcome these challenges, follow best practices such as combining Echidna with other security testing tools, thoroughly understanding your smart contract’s functionality, and consulting security experts as necessary.
Echidna improves contract security
The introduction of new features in Echidna, such as on-chain contract retrieval, data fetching, and multicore fuzzing, opens up new ways of improving the security of your code in real-world scenarios. Adding fuzz tests into your project improves the security of your code by covering edge cases that may be overlooked by unit or integration tests.
For more guidance on using Echidna, including detailed documentation and practical examples, visit our “Building Secure Contracts” website. If you prefer visual learning, check out our informative Echidna live streams available on YouTube.
Download Echidna today and start exploring all of its features. Visit our official repository for the latest release and installation instructions. We encourage you to reproduce this exploit to get familiar with the new on-chain fuzzing feature and to gain insights on how it can help make your contracts more secure.
原文始发于Guillermo Larregay and Elvis Skozdopolj:Fuzzing on-chain contracts with Echidna