From 294fd6d062e67fd712c16a5283ba11ca6bbd6d36 Mon Sep 17 00:00:00 2001 From: Olorunshogo Moses BAMTEFA Date: Mon, 2 Mar 2026 11:25:08 +0100 Subject: [PATCH 1/4] feat: foundry base app --- .gitignore | 3 + assignments/foundry-tests/README.md | 66 +++++++++++++++++++ assignments/foundry-tests/foundry.toml | 6 ++ .../foundry-tests/script/Counter.s.sol | 19 ++++++ assignments/foundry-tests/src/Counter.sol | 14 ++++ assignments/foundry-tests/test/Counter.t.sol | 24 +++++++ 6 files changed, 132 insertions(+) create mode 100644 assignments/foundry-tests/README.md create mode 100644 assignments/foundry-tests/foundry.toml create mode 100644 assignments/foundry-tests/script/Counter.s.sol create mode 100644 assignments/foundry-tests/src/Counter.sol create mode 100644 assignments/foundry-tests/test/Counter.t.sol diff --git a/.gitignore b/.gitignore index 60f73e90..9164a523 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ node_modules artifacts/ cache/ typechain-types/ +lib/ +cache/ +out/ # Logs *.log diff --git a/assignments/foundry-tests/README.md b/assignments/foundry-tests/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/assignments/foundry-tests/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/assignments/foundry-tests/foundry.toml b/assignments/foundry-tests/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/assignments/foundry-tests/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/assignments/foundry-tests/script/Counter.s.sol b/assignments/foundry-tests/script/Counter.s.sol new file mode 100644 index 00000000..f01d69c3 --- /dev/null +++ b/assignments/foundry-tests/script/Counter.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} diff --git a/assignments/foundry-tests/src/Counter.sol b/assignments/foundry-tests/src/Counter.sol new file mode 100644 index 00000000..aded7997 --- /dev/null +++ b/assignments/foundry-tests/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/assignments/foundry-tests/test/Counter.t.sol b/assignments/foundry-tests/test/Counter.t.sol new file mode 100644 index 00000000..48319108 --- /dev/null +++ b/assignments/foundry-tests/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} From 77e94435c2fa7f030b2a33db9779ca77f713408c Mon Sep 17 00:00:00 2001 From: Olorunshogo Moses BAMTEFA Date: Mon, 2 Mar 2026 11:32:05 +0100 Subject: [PATCH 2/4] chore: add contracts and tests --- .../foundry-tests/script/Counter.s.sol | 19 -- assignments/foundry-tests/src/Counter.sol | 14 -- assignments/foundry-tests/src/CounterV3.sol | 62 +++++ .../foundry-tests/src/TimelockVault.sol | 152 ++++++++++++ assignments/foundry-tests/test/Counter.t.sol | 24 -- .../foundry-tests/test/CounterV3.t.sol | 227 ++++++++++++++++++ .../foundry-tests/test/TimelockVault.t.sol | 202 ++++++++++++++++ 7 files changed, 643 insertions(+), 57 deletions(-) delete mode 100644 assignments/foundry-tests/script/Counter.s.sol delete mode 100644 assignments/foundry-tests/src/Counter.sol create mode 100644 assignments/foundry-tests/src/CounterV3.sol create mode 100644 assignments/foundry-tests/src/TimelockVault.sol delete mode 100644 assignments/foundry-tests/test/Counter.t.sol create mode 100644 assignments/foundry-tests/test/CounterV3.t.sol create mode 100644 assignments/foundry-tests/test/TimelockVault.t.sol diff --git a/assignments/foundry-tests/script/Counter.s.sol b/assignments/foundry-tests/script/Counter.s.sol deleted file mode 100644 index f01d69c3..00000000 --- a/assignments/foundry-tests/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/assignments/foundry-tests/src/Counter.sol b/assignments/foundry-tests/src/Counter.sol deleted file mode 100644 index aded7997..00000000 --- a/assignments/foundry-tests/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/assignments/foundry-tests/src/CounterV3.sol b/assignments/foundry-tests/src/CounterV3.sol new file mode 100644 index 00000000..ba39c4bd --- /dev/null +++ b/assignments/foundry-tests/src/CounterV3.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract CounterV3 { + uint public x; + address public owner; + + event Increment(uint by); + event Decrement(uint by); + + mapping(address => bool) public authorized; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not the owner"); + _; + } + + modifier onlyAuthorized() { + require(msg.sender == owner || authorized[msg.sender] == true, "Not authorized"); + _; + } + + function inc() public onlyAuthorized { + x++; + emit Increment(1); + } + + function incBy(uint by) public onlyAuthorized { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } + + // Decrease by 1 + function dec() public onlyAuthorized { + require(x > 0, "Counter cannot go below 0."); + x -= 1; + emit Decrement(1); + } + + // Decrement by a specific amount + function decBy(uint amount) public onlyAuthorized { + require(amount > 0, "Amount must be greater than 0."); + require(x >= amount, "Counter cannot go below 0."); + x -= amount; + emit Decrement(amount); + } + + function grantAccess(address user) external onlyOwner { + authorized[user] = true; + } + + function revokeAccess(address user) external onlyOwner { + // Revoke access can never be owner + // Check for address zero modifier - always have the address zero modifier + authorized[user] = false; + } +} diff --git a/assignments/foundry-tests/src/TimelockVault.sol b/assignments/foundry-tests/src/TimelockVault.sol new file mode 100644 index 00000000..dbde6baf --- /dev/null +++ b/assignments/foundry-tests/src/TimelockVault.sol @@ -0,0 +1,152 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract TimelockVault { + struct Vault { + uint balance; + uint unlockTime; + bool active; + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint vaultId, uint amount, uint unlockTime); + event Withdrawn(address indexed user, uint vaultId, uint amount); + + function deposit(uint _unlockTime) external payable returns (uint) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + // Create new vault + vaults[msg.sender].push(Vault({balance: msg.value, unlockTime: _unlockTime, active: true})); + + uint vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + + function withdraw(uint _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint amount = userVault.balance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.active = false; + + (bool success, ) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint) { + uint totalWithdrawn = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint i = 0; i < userVaults.length; i++) { + if ( + userVaults[i].active && + userVaults[i].balance > 0 && + block.timestamp >= userVaults[i].unlockTime + ) { + uint amount = userVaults[i].balance; + userVaults[i].balance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + (bool success, ) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function getVaultCount(address _user) external view returns (uint) { + return vaults[_user].length; + } + + function getVault( + address _user, + uint _vaultId + ) external view returns (uint balance, uint unlockTime, bool active, bool isUnlocked) { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults( + address _user + ) + external + view + returns (uint[] memory activeVaults, uint[] memory balances, uint[] memory unlockTimes) + { + Vault[] storage userVaults = vaults[_user]; + // Count active vaults + uint activeCount = 0; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint[](activeCount); + balances = new uint[](activeCount); + unlockTimes = new uint[](activeCount); + + // Populate arrays + uint index = 0; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint total) { + Vault[] storage userVaults = vaults[_user]; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint i = 0; i < userVaults.length; i++) { + if ( + userVaults[i].active && + userVaults[i].balance > 0 && + block.timestamp >= userVaults[i].unlockTime + ) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/assignments/foundry-tests/test/Counter.t.sol b/assignments/foundry-tests/test/Counter.t.sol deleted file mode 100644 index 48319108..00000000 --- a/assignments/foundry-tests/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/assignments/foundry-tests/test/CounterV3.t.sol b/assignments/foundry-tests/test/CounterV3.t.sol new file mode 100644 index 00000000..7396b60a --- /dev/null +++ b/assignments/foundry-tests/test/CounterV3.t.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Test} from "../lib/forge-std/src/Test.sol"; +import {CounterV3} from "../src/CounterV3.sol"; +import {console} from "../lib/forge-std/src/console.sol"; + +contract CounterV3Test is Test { + CounterV3 public counterV3; + + uint public x; + + address public owner; + address public addr1; + address public addr2; + + // === Setup function upon to deploy the contract + function setUp() public { + counterV3 = new CounterV3(); + + x = counterV3.x(); + + owner = counterV3.owner(); + addr1 = makeAddr("addr1"); + addr2 = makeAddr("addr2"); + + console.log(" "); + + // console.log("The value of x is: ", x); + // console.log("The contract owner is: ", owner); + // console.log("addr1 is: ", addr1); + // console.log("addr2 is: ", addr2); + } + + // === Default Values + function testDefaultValues() public view { + console.log("Test 1: testDefaultValues"); + console.log(" "); + + assertEq(counterV3.owner(), owner); + console.log("The real contract owner is: ", owner); + + assertEq(counterV3.x(), 0); + console.log("The value of x in testDefaultValues is: ", x); + } + + // === Increments + function testOwnerCanIncrement() public { + console.log("Test 2: testOwnerCanIncrement"); + console.log(" "); + + counterV3.inc(); + console.log("The value of x in testOwnerCanIncrement is: ", counterV3.x()); + assertEq(counterV3.x(), 1); + } + + function testAuthorizedAddrCanIncrement() public { + console.log("Test 3: testAuthorizedAddrCanIncrement"); + console.log(" "); + + counterV3.grantAccess(addr1); + + vm.prank(addr1); + counterV3.inc(); + console.log("The value of x in testAuthorizedAddrCanIncrement is: ", counterV3.x()); + + assertEq(counterV3.x(), 1); + } + + function testUnauthorizedUserCannotIncrement() public { + console.log("Test 4: testUnauthorizedUserCannotIncrement"); + console.log(" "); + + vm.prank(addr1); + vm.expectRevert("Not authorized"); + counterV3.inc(); + console.log("The value of x in testUnauthorizedAddrCanIncrement is: ", counterV3.x()); + } + + function testIncBy() public { + console.log("Test 5: testIncBy"); + console.log(" "); + + counterV3.incBy(5); + console.log("The value of x in testIncBy is: ", counterV3.x()); + assertEq(counterV3.x(), 5); + } + + function testIncByRevertsIfZero() public { + console.log("Test 6: testIncByRevertsIfZero"); + console.log(" "); + + vm.expectRevert("incBy: increment should be positive"); + counterV3.incBy(0); + console.log("The value of x in testIncByRevertsIfZero is: ", counterV3.x()); + } + + // === Decrements + function testOwnerCanDecrement() public { + console.log("Test 7: testOwnerCanDecrement"); + console.log(" "); + + counterV3.incBy(6); + console.log("The value of x in testOwnerCanDecrement is: ", counterV3.x()); + + counterV3.dec(); + console.log("The value of x in testOwnerCanDecrement is: ", counterV3.x()); + + assertEq(counterV3.x(), 5); + } + + function testsDecRevertsIfCounterIsZero() public { + console.log("Test 8: testsDecRevertsIfCounterIsZero"); + console.log(" "); + + vm.expectRevert("Counter cannot go below 0."); + counterV3.dec(); + console.log("The value of x in testsDecRevertsIfCounterIsZero is: ", counterV3.x()); + } + + function testDecByRevertsIfAmountIsZero() public { + console.log("Test 10: testDecByRevertsIfAmountIsZero"); + console.log(" "); + + vm.expectRevert("Amount must be greater than 0."); + counterV3.decBy(0); + console.log("The value of x in ttestDecByRevertsIfAmountIsZero is: ", counterV3.x()); + } + + function testDecBy() public { + console.log("Test 10: testDecBy"); + console.log(" "); + + counterV3.incBy(10); + console.log("The value of x in testsDecBy is: ", counterV3.x()); + counterV3.decBy(4); + console.log("The value of x in testsDecBy is: ", counterV3.x()); + + assertEq(counterV3.x(), 6); + } + + function testDecByRevertsIfUnderflow() public { + console.log("Test 11: testDecByRevertsIfUnderflow"); + console.log(" "); + + counterV3.incBy(3); + console.log("The value of x in testDecByRevertsIfUnderflow is: ", counterV3.x()); + counterV3.decBy(4); + console.log("The value of x in testDecByRevertsIfUnderflow is: ", counterV3.x()); + } + + // === Modifiers and Control + function testOwnerCanGrantAccess() public { + console.log("Test 12: testOwnerCanGrantAccess"); + console.log(" "); + + counterV3.grantAccess(addr1); + console.log("Does addr1 have access: ", counterV3.authorized(addr1)); + assertTrue(counterV3.authorized(addr1)); + } + + function testOwnerCanRevokeAccess() public { + console.log("Test 12: testOwnerCanRevokeAccess"); + console.log(" "); + + counterV3.grantAccess(addr1); + counterV3.revokeAccess(addr1); + console.log("Does addr1 have access: ", counterV3.authorized(addr1)); + + assertFalse(counterV3.authorized(addr1)); + } + + function testOnlyOwnerCanGrantAccess() public { + console.log("Test 13: testOnlyOwnerCanGrantAccess"); + console.log(" "); + + vm.prank(addr1); + vm.expectRevert("Not the owner"); + counterV3.grantAccess(addr2); + console.log("Does addr2 have access: ", counterV3.authorized(addr2)); + } + + function testRevokedUserCannotIncrement() public { + console.log("Test 14: testOwnerCanGrantAccess"); + console.log(" "); + + counterV3.grantAccess(addr1); + + vm.prank(addr1); + console.log("The value of x before inc() in testRevokedUserCannotIncrement is: ", counterV3.x()); + counterV3.inc(); + console.log("The value of x after inc() in testRevokedUserCannotIncrement is: ", counterV3.x()); + + counterV3.revokeAccess(addr1); + + vm.prank(addr1); + vm.expectRevert("Not authorized"); + counterV3.inc(); + console.log("The value of x after inc() in testRevokedUserCannotIncrement is: ", counterV3.x()); + } + + // === Events + function testIncrementEmitsEvent() public { + console.log("Test 15: testIncrementEmitsEvent"); + console.log(" "); + + vm.expectEmit(true, false, false, true); + emit CounterV3.Increment(1); + + counterV3.inc(); + console.log("The value of x in testIncrementEmitsEvent is: ", counterV3.x()); + } + + function testDecrementEmitsEvent() public { + console.log("Test 16: testDecrementEmitsEvent"); + console.log(" "); + + counterV3.inc(); + console.log("The value of x after inc() in testDecrementEmitsEvent is: ", counterV3.x()); + + vm.expectEmit(true, false, false, true); + emit CounterV3.Increment(1); + + counterV3.dec(); + console.log("The value of x after dec() in testDecrementEmitsEvent is: ", counterV3.x()); + } +} diff --git a/assignments/foundry-tests/test/TimelockVault.t.sol b/assignments/foundry-tests/test/TimelockVault.t.sol new file mode 100644 index 00000000..375bbe95 --- /dev/null +++ b/assignments/foundry-tests/test/TimelockVault.t.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Test} from "../lib/forge-std/src/Test.sol"; +import {TimelockVault} from "../src/TimelockVault.sol"; +import {console} from "../lib/forge-std/src/console.sol"; + +contract TimelockVaultTest is Test { + TimelockVault public timelockVault; + + address public user = makeAddr("moses"); + address public otherUser = makeAddr("otherUser"); + + uint256 constant ONE_ETHER = 1 ether; + uint256 constant ONE_DAY = 1 days; + uint256 constant ONE_WEEK = 7 days; + + uint256 public futureUnlockTime; + + // === Setup function to deploy the contract and beforeEach + function setUp() public { + timelockVault = new TimelockVault(); + + vm.deal(user, 100 ether); + vm.deal(otherUser, 100 ether); + + futureUnlockTime = block.timestamp + ONE_WEEK; + console.log("The futureUnlockTime is: ", futureUnlockTime); + } + + // === Default Values + function testDefaultValuesOfStateVariables() public view { + console.log("Test 1: testDefaultValues"); + console.log(" "); + + uint vaultCount = timelockVault.getVaultCount(user); + console.log("Vault count is:", vaultCount); + assertEq(vaultCount, 0); + + uint totalBalance = timelockVault.getTotalBalance(user); + console.log("Total balance is:", totalBalance); + assertEq(totalBalance, 0); + + uint unlockedBalance = timelockVault.getUnlockedBalance(user); + console.log("Unlocked balance:", unlockedBalance); + assertEq(unlockedBalance, 0); + } + + // === Deposit Values + function testDepositRevertsWhenAmountIsZero() public { + vm.prank(user); + vm.expectRevert("Invalid vault ID"); + timelockVault.withdraw(0); + } + + function testDepositCreatesVaultWithCorrectUnlockTime() public { + vm.prank(user); + + uint vaultId = timelockVault.deposit{value: 1 ether}(futureUnlockTime); + (uint balance, uint storedUnlockTime, bool active, bool isUnlocked) = timelockVault.getVault( + user, + vaultId + ); + + assertEq(balance, 1 ether); + assertEq(storedUnlockTime, futureUnlockTime); + assertTrue(active); + assertFalse(isUnlocked); + } + + function testDepositRevertsWhenUnlockTimeInPast() public { + vm.prank(user); + vm.expectRevert("Unlock time must be in the future"); + timelockVault.deposit{value: 0.5 ether}(block.timestamp - 1); + } + + function testDepositCreatesVaultAndEmitsEvent() public { + vm.prank(user); + + vm.expectEmit(true, true, false, true); + emit TimelockVault.Deposited(user, 0, ONE_ETHER, futureUnlockTime); + + uint vaultId = timelockVault.deposit{value: ONE_ETHER}(futureUnlockTime); + + assertEq(vaultId, 0); + + (uint bal, uint unlock, bool active, bool isUnlocked) = timelockVault.getVault(user, 0); + assertEq(bal, ONE_ETHER); + assertEq(unlock, futureUnlockTime); + assertTrue(active); + assertFalse(isUnlocked); + + assertEq(timelockVault.getVaultCount(user), 1); + assertEq(timelockVault.getTotalBalance(user), ONE_ETHER); + assertEq(timelockVault.getUnlockedBalance(user), 0); + } + + function testMultipleDepositsIncreaseVaultCount() public { + vm.startPrank(user); + + timelockVault.deposit{value: 1 ether}(futureUnlockTime); + timelockVault.deposit{value: 2 ether}(futureUnlockTime + ONE_DAY); + timelockVault.deposit{value: 3 ether}(futureUnlockTime + ONE_WEEK); + + vm.stopPrank(); + + assertEq(timelockVault.getVaultCount(user), 3); + assertEq(timelockVault.getTotalBalance(user), 6 ether); + } + + // === Withdraw Single Vault + function testWithdrawRevertsInvalidVaultId() public { + vm.prank(user); + vm.expectRevert("Invalid vault ID"); + timelockVault.withdraw(0); + } + + function testWithdrawRevertsWhenStillLocked() public { + vm.prank(user); + uint id = timelockVault.deposit{value: ONE_ETHER}(futureUnlockTime); + + vm.prank(user); + vm.expectRevert("Funds are still locked"); + timelockVault.withdraw(id); + } + + function testWithdrawRevertsWhenAlreadyWithdrawn() public { + vm.prank(user); + uint id = timelockVault.deposit{value: ONE_ETHER}(futureUnlockTime); + + vm.warp(futureUnlockTime + 1); + + vm.prank(user); + timelockVault.withdraw(id); + + vm.prank(user); + vm.expectRevert("Vault is not active"); + timelockVault.withdraw(id); + } + + // === Withdraw All + function testWithdrawAllRevertsWhenNoUnlockedFunds() public { + vm.prank(user); + timelockVault.deposit{value: 1 ether}(futureUnlockTime + ONE_DAY); + + vm.expectRevert("No unlocked funds available"); + timelockVault.withdrawAll(); + } + + function testWithdrawAllSendsOnlyUnlockedVaults() public { + vm.startPrank(user); + timelockVault.deposit{value: 4 ether}(futureUnlockTime); + timelockVault.deposit{value: 5 ether}(futureUnlockTime + ONE_WEEK); + vm.stopPrank(); + + vm.warp(futureUnlockTime + 1); + + uint before = user.balance; + + vm.expectEmit(true, true, false, true); + emit TimelockVault.Withdrawn(user, 0, 4 ether); + + uint withdrawn = timelockVault.withdrawAll(); + + assertEq(withdrawn, 4 ether); + assertEq(user.balance - before, 4 ether); + assertEq(timelockVault.getUnlockedBalance(user), 0); + assertEq(timelockVault.getTotalBalance(user), 5 ether); + } + + // === View Functions + function testGetActiveVaultsReturnsCorrectData() public { + vm.startPrank(user); + timelockVault.deposit{value: 2 ether}(futureUnlockTime + ONE_DAY); + timelockVault.deposit{value: 3 ether}(futureUnlockTime); + + vm.stopPrank(); + + vm.warp(futureUnlockTime + 1); + + (uint[] memory ids, uint[] memory bals, uint[] memory unlocks) = timelockVault.getActiveVaults(user); + + assertEq(ids.length, 2); + console.log("Ids.length: ", ids.length); + assertEq(ids[0], 0); + console.log("Ids.length: ", ids[0]); + assertEq(bals[0], 2 ether); + assertEq(unlocks[0], futureUnlockTime + ONE_DAY); + } + + function testOtherUserCannotSeeOrWithdrawAnything() public { + vm.prank(user); + timelockVault.deposit{value: ONE_ETHER}(futureUnlockTime); + + vm.prank(otherUser); + assertEq(timelockVault.getVaultCount(otherUser), 0); + assertEq(timelockVault.getTotalBalance(otherUser), 0); + + vm.expectRevert("Invalid vault ID"); + timelockVault.withdraw(0); + } +} From 25cb7f5d185a2fe55d64bf68ed064fe6079fb8fc Mon Sep 17 00:00:00 2001 From: Olorunshogo Moses BAMTEFA Date: Mon, 2 Mar 2026 12:46:49 +0100 Subject: [PATCH 3/4] fix: full assignment submissions --- assignments/foundry-tests/test/CounterV3.t.sol | 2 +- assignments/foundry-tests/test/TimelockVault.t.sol | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assignments/foundry-tests/test/CounterV3.t.sol b/assignments/foundry-tests/test/CounterV3.t.sol index 7396b60a..6336ead7 100644 --- a/assignments/foundry-tests/test/CounterV3.t.sol +++ b/assignments/foundry-tests/test/CounterV3.t.sol @@ -219,7 +219,7 @@ contract CounterV3Test is Test { console.log("The value of x after inc() in testDecrementEmitsEvent is: ", counterV3.x()); vm.expectEmit(true, false, false, true); - emit CounterV3.Increment(1); + emit CounterV3.Decrement(1); counterV3.dec(); console.log("The value of x after dec() in testDecrementEmitsEvent is: ", counterV3.x()); diff --git a/assignments/foundry-tests/test/TimelockVault.t.sol b/assignments/foundry-tests/test/TimelockVault.t.sol index 375bbe95..e525c4bd 100644 --- a/assignments/foundry-tests/test/TimelockVault.t.sol +++ b/assignments/foundry-tests/test/TimelockVault.t.sol @@ -160,6 +160,7 @@ contract TimelockVaultTest is Test { vm.expectEmit(true, true, false, true); emit TimelockVault.Withdrawn(user, 0, 4 ether); + vm.prank(user); uint withdrawn = timelockVault.withdrawAll(); assertEq(withdrawn, 4 ether); From 7b5080e6abde1a5822f032d21c29e6564a9c8043 Mon Sep 17 00:00:00 2001 From: Olorunshogo Moses BAMTEFA Date: Mon, 2 Mar 2026 13:27:20 +0100 Subject: [PATCH 4/4] add documentation findings --- assignments/foundry-tests/README.md | 320 +++++++++++++++++++++++++++- 1 file changed, 317 insertions(+), 3 deletions(-) diff --git a/assignments/foundry-tests/README.md b/assignments/foundry-tests/README.md index 8817d6ab..60c92a25 100644 --- a/assignments/foundry-tests/README.md +++ b/assignments/foundry-tests/README.md @@ -1,6 +1,9 @@ -## Foundry +# Foundry +A Comprehensive Guide to Forge and Cast -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +## Introduction +Foundry is a blazing fast, portable, and modular toolkit for Ethereum application development written in Rust. It is designed for smart contract developers who want speed, flexibility, and powerful command line tools. Foundry consists of: @@ -11,7 +14,273 @@ Foundry consists of: ## Documentation -https://book.getfoundry.sh/ +[Foundry Documentation](https://book.getfoundry.sh/) + +This document focuses on Forge and Cast, while also explaining how they fit into the complete Foundry ecosystem. +--- + +## What is Forge +Forge is the core development tool in Foundry. It allows developers to build, test, debug, deploy, and verify smart contracts. Forge replaces the need for large JavaScript based frameworks and provides a clean and fast workflow written in Rust. + +With Forge, you can: +- Build smart contracts +- Run automated tests +- Format Solidity code +- Measure gas usage +- Deploy contracts +- Manage dependencies + +Forge works directly from the command line and uses a project structure similar to other Ethereum development tools. + +--- + +## Installing and Using Forge + +Once Foundry is installed, Forge commands are available in your terminal. + +### Build Contracts + +To compile your smart contracts, run: + +```shell +$ forge build +``` + +This command compiles all contracts in the src directory and checks for errors. +--- + + +### Run Tests + +Testing is one of the most powerful features of Forge. + +```shell +$ forge test +``` +Forge supports Solidity based testing. Tests are fast and can include fuzz testing for more advanced coverage. +--- + +### Format Code + +To format Solidity files automatically: + +```shell +$ forge fmt +``` +This ensures consistent code style across your project. +--- + +### Format Code + +To measure and track gas usage: + +```shell +$ forge snapshot +``` +This helps developers optimize smart contracts and compare gas costs between versions. +--- + +### Deploy Contracts + +You can deploy contracts using Forge scripts: + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +This command runs a deployment script using a specified RPC URL and private key. +--- + + +### Help Command + +For help with Forge: +```shell +$ forge --help +``` +--- + + + +## What is Cast + +Cast is a command line tool for interacting with Ethereum smart contracts and blockchain data. It acts like a Swiss army knife for Ethereum development. + +With Cast, you can: + +- Call contract functions +- Send transactions +- Read blockchain data +- Convert between data formats +- Query balances +- Interact with deployed contracts + +Cast is especially useful for debugging, scripting, and interacting with live networks. +--- + +### Using Cast + +Basic usage: + +```shell +$ cast +``` + +To see all available options: + +```shell +$ cast --help +``` + +Cast can be used to: + +- Query an address balance +- Call a contract function +- Send a signed transaction +- Convert between hex and decimal +- Check block information + +It works with any Ethereum compatible network by providing an RPC URL. +--- + +## Anvil + +Anvil is a local Ethereum node for development and testing. + +It allows you to: + +- Run a local blockchain +- Fork mainnet for testing +- Create test accounts with pre funded balances +- Simulate transactions + +Start Anvil with: + +```shell +$ anvil +``` +```shell +For help: +``` + +```shell +$ anvil --help +``` + +Anvil integrates seamlessly with Forge and Cast, making local testing simple and fast. + +--- + + +## Chisel + +Chisel is a Solidity REPL that allows rapid prototyping. It lets developers experiment with Solidity code directly in the terminal without deploying contracts. +--- + +## Remappings + +Remappings allow you to customize import paths in Solidity projects. They are configured in the foundry.toml file. + +Example configuration: + +```foundry.toml +[profile.default] +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/", +] +``` + + +This allows you to import OpenZeppelin contracts using clean import paths instead of long relative paths. +--- + + +## Soldeer + +Soldeer is a Solidity native package manager included with Foundry. It provides an alternative to git submodules and offers versioned dependencies with simpler management. + +Soldeer features: + +- Versioned dependencies +- Package registry support +- Automatic remapping generation + +### Initialize Soldeer + +```solidity +$ forge soldeer init +``` + +This creates a `soldeer.toml` configuration file. + +Example configuration: + +```solidity +[soldeer] +remappings_generate = true +remappings_regenerate = false +remappings_version = true +remappings_prefix = "@" +remappings_location = "config" + +[dependencies] +"@openzeppelin-contracts" = "5.0.0" +"@solmate" = "6.7.0" +``` + +This setup allows your project to use specific versions of external Solidity libraries in a clean and organized way. + +--- + +## Complete Foundry Tool Overview + +forge +Build, test, debug, deploy, and verify smart contracts + +cast +Interact with contracts, send transactions, and query blockchain data + +anvil +Run a local Ethereum node with forking support + +chisel +Solidity REPL for rapid experimentation + +--- + +## Why Use Foundry + +- Foundry is fast because it is written in Rust +- It has minimal dependencies +- It provides native Solidity testing +- It integrates local and live network workflows +- It simplifies dependency management +- It offers powerful command line tooling + +Forge and Cast together provide a complete smart contract development and interaction workflow. + +- Forge handles building and testing. +- Cast handles communication with deployed contracts. +- Anvil provides a local blockchain. +- Chisel allows quick experimentation. + +--- + +## Conclusion + +Foundry is a modern toolkit for Ethereum smart contract development. Forge allows developers to build, test, and deploy contracts efficiently. Cast enables powerful interaction with blockchain networks. Combined with Anvil and Chisel, Foundry provides a complete, high performance development environment. + +With its speed, simplicity, and modular design, Foundry is an excellent choice for Ethereum developers who want full control over their workflow. + +This document covered all core commands, tools, configuration options, and package management features necessary to understand and use Forge and Cast effectively. + + + + + + + + ## Usage @@ -63,4 +332,49 @@ $ cast $ forge --help $ anvil --help $ cast --help + + +# Foundry + + +## +`forge`: Build, test, debug, deploy, and verify smart contracts +`cast`: Interact with contracts, send transactions, and query chain data +`anvil`: Run a local Ethereum node with forking capabilities Reference +`chisel`: Solidity REPL for rapid prototyping + +### Remappings +Customize import paths with remappings in `foundry.toml`: + +```foundry.toml +[profile.default] +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/", +] +``` + +### Soldeer +Soldeer is a Solidity-native package manager that provides an alternative to git submodules. It offers versioned dependencies, a package registry, and simpler dependency management. + +#### Installation +Soldeer comes bundled with Foundry. Initialize it in your project: + +```solidity +$ forge soldeer init +``` +This creates a `soldeer.toml` configuration file. + +```solidity +[soldeer] +remappings_generate = true +remappings_regenerate = false +remappings_version = true +remappings_prefix = "@" +remappings_location = "config" + +[dependencies] +"@openzeppelin-contracts" = "5.0.0" +"@solmate" = "6.7.0" +``` + ```