1/5
## Setting Up Foundry Tests for Your Rebase Token This lesson walks through the initial setup for testing a `RebaseToken` and its associated `Vault` contract using the Foundry framework. We'll create the test file, configure the testing environment within the `setUp` function (including deploying contracts and handling common type-casting issues), and outline the structure for our first specific test focused on linear interest accrual. ## Creating the Test File and Initial Imports First, we need to create our test file. In Foundry, test files typically reside in the `test/` directory and end with `.t.sol`. Let's create `test/RebaseToken.t.sol`. Inside this file, we start with the standard boilerplate: an SPDX license identifier and the Solidity pragma version. We then import the necessary components: 1. `Test` and `console` from `forge-std`: Foundry's standard library for testing utilities and logging. 2. `RebaseToken`: The rebase token contract we want to test. 3. `Vault`: The vault contract that interacts with the rebase token (e.g., for deposits/withdrawals). 4. `IRibaseToken`: The interface for our rebase token, which is likely required by the `Vault`'s constructor or other functions for type compatibility. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // Ensure this matches or is compatible with your contracts import {Test, console} from "forge-std/Test.sol"; import {RebaseToken} from "../src/RebaseToken.sol"; import {Vault} from "../src/Vault.sol"; import {IRibaseToken} from "../src/interfaces/IRibaseToken.sol"; contract RebaseTokenTest is Test { // Test contract implementation follows } ``` ## Configuring the Test Environment with `setUp` Foundry uses a special function called `setUp()` which runs before each test function (`test*`, `testFuzz*`). This is ideal for deploying contracts and setting up initial state that's common across multiple tests. **1. State Variables:** Inside our `RebaseTokenTest` contract, we declare state variables to hold instances of our deployed contracts and addresses for simulated users. We use Foundry's `makeAddr` cheatcode to generate consistent, named addresses for testing purposes. ```solidity contract RebaseTokenTest is Test { RebaseToken private rebaseToken; Vault private vault; address public owner = makeAddr("owner"); address public user = makeAddr("user"); // setUp function definition comes next } ``` **2. The `setUp` Function:** Now, let's define the `setUp` function. We'll perform several key actions here: * **Set `msg.sender`:** Use `vm.startPrank(owner)` so that subsequent actions (like contract deployment and role assignments) are performed by the `owner` address. * **Deploy Contracts:** Instantiate `RebaseToken` and `Vault`. * **Handle Type Casting for Deployment:** When deploying the `Vault`, its constructor likely expects an argument of type `IRibaseToken`. However, our `rebaseToken` variable is of type `RebaseToken`. Solidity requires explicit casting, and sometimes an intermediate cast to `address` is necessary. * **Grant Roles:** If the `Vault` needs permission to mint or burn `RebaseToken`s (a common pattern), we need to grant the appropriate role from the `RebaseToken` contract to the `Vault` contract's address. This also often requires careful type casting. * **Simulate Initial State:** We can fund the `Vault` contract with some ETH using a low-level call to simulate accumulated rewards available for potential redemption tests later. * **Reset `msg.sender`:** Use `vm.stopPrank()` to stop impersonating the `owner`. ```solidity function setUp() public { vm.startPrank(owner); // Actions from here until stopPrank() are by 'owner' // Deploy the RebaseToken first rebaseToken = new RebaseToken(); // Deploy the Vault, passing the RebaseToken address. // Crucial Fix 1: Cast rebaseToken to address, then to IRibaseToken. // Direct casting (IRibaseToken(rebaseToken)) often fails. vault = new Vault(IRibaseToken(address(rebaseToken))); // Grant the Vault permission to mint/burn tokens. // Assumes RebaseToken has an AccessControl role like MINT_AND_BURN_ROLE. // Crucial Fix 2: Pass the Vault's address, not the contract instance. // Direct passing (rebaseToken.grantMintAndBurnRole(vault)) fails if the function expects 'address'. rebaseToken.grantMintAndBurnRole(address(vault)); // Fund the vault with 1 ETH to simulate rewards available for redemption. // Use a low-level call and cast the target address to 'payable'. (bool success,) = payable(address(vault)).call{value: 1 ether}(""); // In tests, an explicit require(success) might be omitted as failure would revert anyway. // require(success, "Vault reward funding failed"); vm.stopPrank(); // Stop acting as 'owner' } ``` **Type Casting Issues Explained:** * **Vault Constructor:** Trying `new Vault(IRibaseToken(rebaseToken))` can result in a `TypeError: Explicit type conversion not allowed from "contract RebaseToken" to "contract IRibaseToken"`. The required path is `ContractType -> address -> InterfaceType`. * **Role Granting:** Trying `rebaseToken.grantMintAndBurnRole(vault)` can result in a `TypeError` if the function expects an `address` parameter, not a `Vault` contract type. You must explicitly cast `address(vault)`. ### Addressing a Minor Compiler Warning During development or compilation, you might encounter minor issues like NatSpec comment errors. For example, a compiler warning like `Error (5856): NatSpec: Return variable name missing` indicates a `@return` comment lacks the variable name. * *Incorrect:* `@return The interest accumulated.` * *Correct:* `@return interestAccumulated The interest accumulated.` (Assuming `interestAccumulated` is the name of the return variable). Fixing these ensures cleaner code and better documentation generation. ## Planning the First Test: `testDepositLinear` With the setup complete, we can start writing individual tests. A key feature of this rebase token seems to be linear interest accrual. Let's outline a fuzz test (`testDepositLinear`) to verify this property. Fuzz tests automatically run with various random inputs, helping uncover edge cases. **Test Goal:** Verify that the amount of rebase tokens received as interest is directly proportional to the time elapsed. If we check the balance increase over two equal time intervals, the increase should be the same for both intervals. **Initial Test Setup:** 1. **Function Signature:** Define a public function starting with `testFuzz` or just `test` (Foundry treats test functions with parameters as fuzz tests) that accepts input parameters. We'll fuzz the deposit `amount`. 2. **Bound Inputs:** Use `vm.bound` (or `bound` directly as `Test` is inherited) to constrain the fuzzed `amount` to a reasonable range. This prevents extremely small or large values that might cause overflows or be unrealistic. We set a minimum (e.g., `1e5` wei) and a maximum (e.g., `type(uint96).max` if amounts are related to `uint96`). 3. **Set Caller Context:** Use `vm.startPrank(user)` to simulate the actions being performed by our regular `user`. 4. **Provide Funds:** Use `vm.deal(user, amount)` to give the `user` address the necessary ETH for the deposit (or potentially a fixed amount if the test logic requires it). ```solidity function testDepositLinear(uint256 amount) public { // Constrain the fuzzed amount to a sensible range (e.g., non-zero, below a practical limit) // bound is often preferred over vm.assume for simple range checks. amount = bound(amount, 1e5, type(uint96).max); // Example bounds // All actions within this test are performed by 'user' vm.startPrank(user); // Give the user ETH to potentially use for deposits // Note: The actual deposit might use a fixed value (e.g., 1 ether) vm.deal(user, amount); // --- Test Logic Outline (To be implemented) --- // 1. User deposits a fixed amount of ETH (e.g., 1 ether) into the Vault. // vault.deposit{value: 1 ether}(); // Example call // 2. Record the user's initial rebase token balance. // uint256 initialBalance = rebaseToken.balanceOf(user); // 3. Advance time using vm.warp(). // uint256 timeInterval = 1 days; // Example interval // vm.warp(block.timestamp + timeInterval); // 4. Record the balance after the first time warp. // uint256 balanceAfterWarp1 = rebaseToken.balanceOf(user); // 5. Advance time again by the *same* interval. // vm.warp(block.timestamp + timeInterval); // 6. Record the balance after the second time warp. // uint256 balanceAfterWarp2 = rebaseToken.balanceOf(user); // 7. Calculate interest accrued in each interval. // uint256 interest1 = balanceAfterWarp1 - initialBalance; // uint256 interest2 = balanceAfterWarp2 - balanceAfterWarp1; // 8. Assert that the interest accrued is equal (or very close, allowing for minor precision differences). // assertEq(interest1, interest2, "Interest accrual is not linear"); // --- End Test Logic Outline --- vm.stopPrank(); // Stop acting as 'user' } ``` This setup provides a solid foundation. We have deployed our contracts, handled necessary permissions and type casts, funded the system appropriately, and outlined the logic for our first crucial test case using Foundry's powerful cheatcodes for state manipulation (`startPrank`, `deal`, `warp`). The next step would be to fully implement the logic within `testDepositLinear`.
A foundational setup guide to Setting Up Foundry Tests for Your Rebase Token - Learn to create the initial test file and configure the Foundry environment using the `setUp` function, including contract deployment and resolving common type-casting issues. Outline the structure for testing linear interest accrual in a rebase token system.
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)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on April 4, 2025
Solidity Developer
Advanced FoundryDuration: 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)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on April 4, 2025
Testimonials
Read what our students have to say about this course.
Chainlink
Chainlink
Gustavo Gonzalez
Solutions Engineer at OpenZeppelin
Francesco Andreoli
Lead Devrel at Metamask
Albert Hu
DeForm Founding Engineer
Radek
Senior Developer Advocate at Ceramic
Boidushya
WalletConnect
Idris
Developer Relations Engineer at Axelar