--- ### Debugging Fuzz Sequences Alright! The moment of truth, let's run our test with: ```bash forge test --mt statefulFuzz_testInvariantBreaksHandler ``` ::image{src='/security-section-5/16-debugging-fuzz-sequences/debugging-fuzz-sequences1.png' style='width: 100%; height: auto;'} Oh no! Something went wrong. We can see `assertion violated` in the output, but there's not a lot of information. In situations like this, we should leverage the `-vvvv` flag. > `-vvvv` can be used to increase the _verbosity_ of an output, often providing additional data or insight. Let's try it again: ```bash forge test --mt statefulFuzz_testInvariantBreaksHandler -vvvv ``` A couple things stand out in this more robust output now (I've highlighted them in blue in the screenshot above). First, the error we're getting seems to be `ERC20InsufficientAllowance`. The reason seems to be that we're calling `transferFrom` on a random address. Whoops! It seems we didn't set our handler as the targetContract in our test - we only set the function selectors! Let's rectify this now. ```js function setUp() public { vm.startPrank(user); mockUSDC = new MockUSDC(); yieldERC20 = new YieldERC20(); startingAmount = yieldERC20.INITIAL_SUPPLY(); mockUSDC.mint(user, startingAmount); vm.stopPrank(); supportedTokens.push(IERC20(address(yieldERC20))); supportedTokens.push(IERC20(address(mockUSDC))); handlerStatefulFuzzCatches = new HandlerStatefulFuzzCatches(supportedTokens); handler = new Handler(handlerStatefulFuzzCatches, mockUSDC, yieldERC20, user); // HANDLER INITIALIZED bytes4[] memory selectors = new bytes4[](4); // SPECIFY SELECTORS TO FUZZ selectors[0] = handler.depositYieldERC20.selector; selectors[1] = handler.depositMockUSDC.selector; selectors[2] = handler.withdrawYieldERC20.selector; selectors[3] = handler.withdrawMockUSDC.selector; targetSelector(FuzzSelector({addr: address(handler), selectors: selectors})); // SET TARGET SELECTORS targetContract(address(handler)); // SET TARGET CONTRACT TO HANDLER } ``` Now let's try it. ::image{src='/security-section-5/16-debugging-fuzz-sequences/debugging-fuzz-sequences2.png' style='width: 100%; height: auto;'} Alright! It looks like we may have found something! We're seeing an error of `ERC20InsufficientBalance` when calling `withdrawToken` on `yieldERC20`. That's odd. Let's look at the `withdrawToken` function again. ```js function withdrawToken(IERC20 token) external requireSupportedToken(token) { uint256 currentBalance = tokenBalances[msg.sender][token]; tokenBalances[msg.sender][token] = 0; token.safeTransfer(msg.sender, currentBalance); } ``` Nothing out of the ordinary it seems, we're just calling `safeTransfer` on the token. Maybe we need to take a closer look at `YieldERC20.sol`. ```js // SPDX-License-Identifier: MIT pragma solidity 0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract YieldERC20 is ERC20 { uint256 public constant INITIAL_SUPPLY = 1_000_000e18; address public immutable owner; // We take a fee once every 10 transactions uint256 public count = 0; uint256 public constant FEE = 10; uint256 public constant USER_AMOUNT = 90; uint256 public constant PRECISION = 100; constructor() ERC20("MockYieldERC20", "MYIELD") { owner = msg.sender; _mint(msg.sender, INITIAL_SUPPLY); } /** * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding * this function. * * Every 10 transactions, we take a fee of 10% and send it to the owner. */ function _update(address from, address to, uint256 value) internal virtual override { if (to == owner) { super._update(from, to, value); } else if (count >= 10) { uint256 userAmount = value * USER_AMOUNT / PRECISION; uint256 ownerAmount = value * FEE / PRECISION; count = 0; super._update(from, to, userAmount); super._update(from, owner, ownerAmount); } else { count++; super._update(from, to, value); } } } ``` Ah ha! This `_update` function is sending a 10% fee to the owner of YieldERC20 every 10 transactions. This is why our `withdrawTokens` function was throwing an `ERC20InsufficientBalance` error - `HandlerStatefulFuzzCatches.sol` doesn't have enough `YieldERC20` to pay the fee! This is actually a fairly common situation known is a `Fee on Transfer` token and they exist in a classification of vulnerabilities known as `Weird ERC20s`. We executed handler-based stateful fuzz testing in order to pinpoint this potential problem in our contract! This should clearly demonstrate how powerful a tool this method of fuzz testing can be. Let's recap everything we've learn about fuzzing.
Uncover a broken invariant and debug the output sequence of our fuzz testing in this TSwap lesson.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Solidity Developer
Smart Contract SecurityDuration: 25min
Duration: 1h 18min
Duration: 35min
Duration: 2h 28min
Duration: 5h 03min
Duration: 5h 22min
Duration: 4h 33min
Duration: 2h 01min
Duration: 1h 40min