Terminal.skills
Use Cases/Build and Deploy DeFi Smart Contracts with Foundry

Build and Deploy DeFi Smart Contracts with Foundry

Build a DeFi yield vault using Foundry for development and testing, Viem for frontend integration, and OpenZeppelin for battle-tested contract primitives — with fuzz testing, fork testing against mainnet, and gas-optimized deployment to Ethereum.

Blockchain & Web3#solidity#ethereum#smart-contracts#testing#deployment
Works with:claude-codeopenai-codexgemini-clicursor
$

Sana is a Solidity developer building a yield aggregator vault — users deposit USDC, the vault deploys capital across lending protocols (Aave, Compound), and rebalances for optimal yield. The contract will hold millions in user funds, so every line must be tested rigorously: fuzz testing for edge cases, fork testing against mainnet state, and formal invariant testing.

Step 1: Smart Contract Development with Foundry

solidity
// src/YieldVault.sol — ERC-4626 yield aggregator
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IPool} from "@aave/v3-core/contracts/interfaces/IPool.sol";

contract YieldVault is ERC4626, Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    IPool public immutable aavePool;
    IERC20 public immutable aToken;        // Aave interest-bearing token

    uint256 public constant MAX_DEPOSIT = 10_000_000e6;  // $10M cap
    uint256 public totalStrategyDeposits;

    event Rebalanced(uint256 aaveAmount, uint256 timestamp);
    event EmergencyWithdraw(address indexed caller, uint256 amount);

    constructor(
        IERC20 usdc_,
        IPool aavePool_,
        IERC20 aToken_
    )
        ERC4626(usdc_)
        ERC20("Yield Vault USDC", "yvUSDC")
        Ownable(msg.sender)
    {
        aavePool = aavePool_;
        aToken = aToken_;
        usdc_.approve(address(aavePool_), type(uint256).max);
    }

    function totalAssets() public view override returns (uint256) {
        // Idle assets + deployed assets (with earned interest)
        return IERC20(asset()).balanceOf(address(this)) + aToken.balanceOf(address(this));
    }

    function deposit(uint256 assets, address receiver)
        public override nonReentrant returns (uint256)
    {
        require(totalAssets() + assets <= MAX_DEPOSIT, "Deposit cap reached");
        return super.deposit(assets, receiver);
    }

    /// @notice Deploy idle assets to Aave for yield
    function rebalance() external onlyOwner {
        uint256 idle = IERC20(asset()).balanceOf(address(this));
        if (idle > 0) {
            aavePool.supply(asset(), idle, address(this), 0);
            totalStrategyDeposits += idle;
        }
        emit Rebalanced(idle, block.timestamp);
    }

    /// @notice Emergency: withdraw all from strategies
    function emergencyWithdraw() external onlyOwner {
        uint256 aaveBalance = aToken.balanceOf(address(this));
        if (aaveBalance > 0) {
            aavePool.withdraw(asset(), aaveBalance, address(this));
        }
        totalStrategyDeposits = 0;
        emit EmergencyWithdraw(msg.sender, aaveBalance);
    }
}

Step 2: Comprehensive Testing

solidity
// test/YieldVault.t.sol — Fuzz + Fork + Invariant testing
pragma solidity ^0.8.20;

import {Test, console} from "forge-std/Test.sol";
import {YieldVault} from "../src/YieldVault.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IPool} from "@aave/v3-core/contracts/interfaces/IPool.sol";

contract YieldVaultTest is Test {
    YieldVault vault;

    // Mainnet addresses (used in fork tests)
    address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address constant AAVE_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
    address constant aUSDC = 0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c;

    address alice = makeAddr("alice");
    address bob = makeAddr("bob");

    function setUp() public {
        // Fork mainnet for realistic testing
        vm.createSelectFork("mainnet", 19_000_000);

        vault = new YieldVault(
            IERC20(USDC),
            IPool(AAVE_POOL),
            IERC20(aUSDC)
        );

        // Fund test users with real USDC (via whale impersonation)
        address whale = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341;
        vm.startPrank(whale);
        IERC20(USDC).transfer(alice, 100_000e6);
        IERC20(USDC).transfer(bob, 50_000e6);
        vm.stopPrank();
    }

    function test_DepositAndWithdraw() public {
        vm.startPrank(alice);
        IERC20(USDC).approve(address(vault), 10_000e6);
        uint256 shares = vault.deposit(10_000e6, alice);
        assertGt(shares, 0);

        // Withdraw
        uint256 assets = vault.redeem(shares, alice, alice);
        assertEq(assets, 10_000e6);       // No yield yet, 1:1
        vm.stopPrank();
    }

    function test_RebalanceToAave() public {
        // Alice deposits
        vm.startPrank(alice);
        IERC20(USDC).approve(address(vault), 50_000e6);
        vault.deposit(50_000e6, alice);
        vm.stopPrank();

        // Owner rebalances to Aave
        vault.rebalance();

        // Vault now has aUSDC instead of USDC
        assertGt(IERC20(aUSDC).balanceOf(address(vault)), 0);

        // Fast forward 30 days — yield accrues
        vm.warp(block.timestamp + 30 days);

        // Total assets should be more than deposited (interest earned)
        assertGt(vault.totalAssets(), 50_000e6);
    }

    // Fuzz test: any deposit amount should work within bounds
    function testFuzz_Deposit(uint256 amount) public {
        amount = bound(amount, 1e6, 10_000_000e6);  // $1 to $10M

        // Ensure alice has enough
        deal(USDC, alice, amount);

        vm.startPrank(alice);
        IERC20(USDC).approve(address(vault), amount);
        uint256 shares = vault.deposit(amount, alice);
        vm.stopPrank();

        assertGt(shares, 0);
        assertEq(vault.totalAssets(), amount);
    }

    function test_DepositCapEnforced() public {
        deal(USDC, alice, 20_000_000e6);
        vm.startPrank(alice);
        IERC20(USDC).approve(address(vault), 20_000_000e6);
        vm.expectRevert("Deposit cap reached");
        vault.deposit(10_000_001e6, alice);
        vm.stopPrank();
    }
}

Step 3: Frontend Integration with Viem

typescript
// src/lib/vault.ts — Type-safe contract interactions
import { createPublicClient, createWalletClient, http, parseUnits, formatUnits } from "viem";
import { mainnet } from "viem/chains";
import { vaultAbi } from "./abis/vault";

const VAULT_ADDRESS = "0x..." as const;
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const;

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(process.env.NEXT_PUBLIC_RPC_URL),
});

export async function getVaultStats() {
  const [totalAssets, totalSupply, maxDeposit] = await publicClient.multicall({
    contracts: [
      { address: VAULT_ADDRESS, abi: vaultAbi, functionName: "totalAssets" },
      { address: VAULT_ADDRESS, abi: vaultAbi, functionName: "totalSupply" },
      { address: VAULT_ADDRESS, abi: vaultAbi, functionName: "MAX_DEPOSIT" },
    ],
  });

  const sharePrice = totalSupply.result! > 0n
    ? Number(totalAssets.result!) / Number(totalSupply.result!)
    : 1;

  return {
    tvl: formatUnits(totalAssets.result!, 6),
    sharePrice: sharePrice.toFixed(6),
    utilizationPercent: (Number(totalAssets.result!) / Number(maxDeposit.result!) * 100).toFixed(1),
  };
}

Results

The vault launches on mainnet after 3 weeks of development. Foundry's testing suite gives Sana confidence that the contract handles edge cases correctly.

  • Test coverage: 98% line coverage, 47 test cases (unit + fuzz + fork)
  • Fuzz runs: 10,000 fuzz iterations found 0 issues after fixing 2 edge cases in development
  • Fork testing: Verified against real Aave v3 mainnet state — deposit/withdraw/rebalance all work
  • Gas optimization: deposit() costs 89,000 gas (within target of <100K)
  • Audit readiness: Clean Slither report, all high/medium findings addressed
  • TVL after 30 days: $2.1M deposited, 4.8% APY delivered to users