5/5
## Setting Up Your Foundry Test Environment for Rebase Tokens To begin testing your Solidity smart contracts, particularly a `RebaseToken` and `Vault` system, we'll use the Foundry testing framework. Foundry tests are written in Solidity files that conventionally end with `.t.sol`. First, create your test file, for instance, `test/RebaseToken.t.sol`. Every Solidity file should start with an SPDX license identifier and the pragma directive specifying the compiler version: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; ``` Next, import the necessary components. For our rebase token tests, we need: * `Test` and `console` from `forge-std/Test.sol`: These provide Foundry's core testing utilities and a console logging feature. * `RebaseToken` and `Vault`: The smart contracts we intend to test, imported from their respective locations (e.g., `../src/RebaseToken.sol`). * `IRebaseToken`: An interface for our `RebaseToken`. This is crucial because our `Vault` contract's constructor will expect an `IRebaseToken` type. Your import block will look something like this: ```solidity import {Test, console} from "forge-std/Test.sol"; import {RebaseToken} from "../src/RebaseToken.sol"; import {Vault} from "../src/Vault.sol"; import {IRebaseToken} from "../src/interfaces/IRebaseToken.sol"; ``` ## Defining Your Test Contract and Initial State Foundry tests are structured within a contract that inherits from the imported `Test` contract. Inside this test contract, we'll declare state variables to hold instances of our deployed contracts and define standard addresses for actors like an owner and a user. ```solidity contract RebaseTokenTest is Test { RebaseToken private rebaseToken; Vault private vault; address public owner = makeAddr("owner"); address public user = makeAddr("user"); // Setup function and test functions will follow } ``` Here, `rebaseToken` and `vault` will store the deployed contract instances. We use Foundry's `makeAddr(string)` cheatcode to create deterministic addresses labeled "owner" and "user". This is useful for consistently setting up test scenarios. ## The `setup` Function: Deploying Contracts and Initial Configuration Foundry provides a special function named `setup`, which is executed before each test function (any function prefixed with `test...`). This is the ideal place to deploy your contracts and perform any initial configurations required for your tests. Our `setup` function will handle: 1. Deploying the `RebaseToken`. 2. Deploying the `Vault`, ensuring correct type casting for its constructor argument. 3. Granting the `Vault` contract the necessary permissions to mint and burn `RebaseToken`s. 4. Simulating initial collateral or rewards by sending ETH to the `Vault`. ```solidity function setup() public { // Impersonate the 'owner' address for deployments and role granting vm.startPrank(owner); rebaseToken = new RebaseToken(); // Deploy Vault: requires IRebaseToken. // Direct casting (IRebaseToken(rebaseToken)) is invalid. // Correct way: cast rebaseToken to address, then to IRebaseToken. vault = new Vault(IRebaseToken(address(rebaseToken))); // Grant the MINT_AND_BURN_ROLE to the Vault contract. // The grantMintAndBurnRole function expects an address. rebaseToken.grantMintAndBurnRole(address(vault)); // Send 1 ETH to the Vault to simulate initial funds. // The target address must be cast to 'payable'. (bool success, ) = payable(address(vault)).call{value: 1 ether}(""); // It's good practice to handle the success flag, though omitted for brevity here. // Stop impersonating the 'owner' vm.stopPrank(); } ``` **Key Points on Type Casting and Interactions:** * **Interface Casting:** When the `Vault` constructor expects an `IRebaseToken` and you have a `RebaseToken` instance, you must first cast the instance to its `address` and then wrap it with the interface: `IRebaseToken(address(rebaseToken))`. * **Address Casting:** When a function (like `grantMintAndBurnRole`) expects an `address` and you have a contract instance (`vault`), cast it using `address(vault)`. * **Payable Casting for Sending ETH:** To send ETH using a low-level `.call`, the recipient address (e.g., `address(vault)`) must be cast to `payable`. * **Pranking:** `vm.startPrank(address)` makes all subsequent contract calls originate from the specified address. `vm.stopPrank()` reverts to the default test contract address as the caller. This is essential for testing access control. ## Writing Your First Fuzz Test: Verifying Linear Interest Accrual With the setup complete, we can start writing test cases. We'll begin by testing if the `RebaseToken` accrues interest linearly over time after a user deposits. This test will also introduce **fuzz testing**. Fuzz testing involves calling a function with a wide range of automatically generated inputs to discover edge cases or unexpected behavior. In Foundry, you enable fuzzing by adding parameters to your test function. ```solidity // Test if interest accrues linearly after a deposit. // 'amount' will be a fuzzed input. function testDepositLinear(uint256 amount) public { // Constrain the fuzzed 'amount' to a practical range. // Min: 0.00001 ETH (1e5 wei), Max: type(uint96).max to avoid overflows. amount = bound(amount, 1e5, type(uint96).max); // 1. User deposits 'amount' ETH vm.startPrank(user); // Actions performed as 'user' vm.deal(user, amount); // Give 'user' the 'amount' of ETH to deposit // TODO: Implement deposit logic: // vault.deposit{value: amount}(); // Example // 2. TODO: Check initial rebase token balance for 'user' // uint256 initialBalance = rebaseToken.balanceOf(user); // 3. TODO: Warp time forward and check balance again // uint256 timeDelta = 1 days; // Example // vm.warp(block.timestamp + timeDelta); // uint256 balanceAfterFirstWarp = rebaseToken.balanceOf(user); // uint256 interestFirstPeriod = balanceAfterFirstWarp - initialBalance; // 4. TODO: Warp time forward by the same amount and check balance again // vm.warp(block.timestamp + timeDelta); // Warp by another 'timeDelta' // uint256 balanceAfterSecondWarp = rebaseToken.balanceOf(user); // uint256 interestSecondPeriod = balanceAfterSecondWarp - balanceAfterFirstWarp; // TODO: Assert that interestFirstPeriod == interestSecondPeriod for linear accrual. // assertEq(interestFirstPeriod, interestSecondPeriod, "Interest accrual is not linear"); vm.stopPrank(); // Stop impersonating 'user' } ``` **Explanation of Fuzzing Setup:** * **Fuzzed Parameter:** `uint256 amount` in the function signature tells Foundry to run this test multiple times with different random values for `amount`. * **`bound(variable, min, max)`:** This Foundry cheatcode constrains the fuzzed `amount` to a meaningful range (between 100,000 wei and `uint96.max`). This is more effective than `vm.assume` for setting input boundaries as it guides the fuzzer. * **User Actions:** * `vm.startPrank(user)`: Simulates the `user` performing the deposit. * `vm.deal(user, amount)`: A cheatcode to give the `user` address the specified `amount` of ETH, ensuring they have funds for the fuzzed deposit. * **Test Logic Outline:** The comments outline the steps: deposit, check balance, advance time (using `vm.warp`), check balance again, advance time by the same interval, and finally, assert that the interest accrued in both periods is equal for linear growth. ## Key Foundry Cheatcodes and Best Practices Encountered Throughout this initial setup, we've utilized several powerful Foundry cheatcodes and encountered important Solidity practices: **Foundry Cheatcodes Used:** * `vm.startPrank(address)`: Execute subsequent calls as if originating from `address`. * `vm.stopPrank()`: Revert `msg.sender` to the test contract's address. * `makeAddr(string)`: Creates a deterministic, labeled address for testing. * `vm.deal(address, amount)`: Sets the ETH balance of `address` to `amount`. * `bound(variable, min, max)`: Constrains a fuzzed input `variable` to the range `[min, max]`. * Low-level `.call{value: ethAmount}("")`: A way to send ETH to an address, especially to trigger `receive` or `fallback` functions. **Important Solidity and Testing Practices:** * **Type Casting for Interoperability:** * When a contract instance needs to be passed as an interface type: `InterfaceType(address(contractInstance))`. * When a contract instance needs to be passed as an address: `address(contractInstance)`. * When sending ETH via low-level calls, the target address must be cast to `payable`: `payable(address)`. * **Handling Low-Level Call Returns:** Functions like `.call`, `.send`, and `.transfer` return a boolean indicating success. It's crucial to handle this return value, at least by assigning it to a variable to avoid compiler warnings. In production code, you should explicitly check this boolean. ```solidity (bool success, bytes memory data) = target.call{value: amount}(""); require(success, "ETH transfer failed"); ``` * **Levels of Testing:** While this tutorial combines unit and fuzz testing for efficiency, remember that comprehensive testing often involves: * **Unit Tests:** Testing individual functions in isolation. * **Fuzz Tests:** Testing functions with a wide range of inputs. * **Integration Tests:** Testing how multiple contracts interact, or how contracts interact with off-chain scripts. By following these setup steps and understanding these core concepts, you're well on your way to writing robust tests for your `RebaseToken` system using Foundry. The next steps will involve completing the `testDepositLinear` logic and adding more test cases for other functionalities.
A practical walkthrough to Foundry Test Environment Setup for Rebase Tokens - Configure your Foundry test environment to test `RebaseToken` and `Vault` contracts, focusing on the `setup` function for deployments, type casting, and role assignments using `vm.prank`. You'll also begin writing a fuzz test for linear interest, utilizing `bound` for inputs and `vm.deal` for user balances.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on June 10, 2025
Duration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 6h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 1h 10min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on June 10, 2025