Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
import {Vault} from "../src/Vault.sol";
import {Token} from "../src/Token.sol";

contract DeployScript is Script {
function run() external {
Expand All @@ -12,9 +13,11 @@ contract DeployScript is Script {

Counter counter = new Counter();
Vault vault = new Vault();
Token token = new Token("Test Token", "TST", 1_000_000 ether);

console.log("Counter deployed at:", address(counter));
console.log("Vault deployed at: ", address(vault));
console.log("Token deployed at: ", address(token));

vm.stopBroadcast();
}
Expand Down
56 changes: 56 additions & 0 deletions src/Token.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// Minimal ERC20-style token - used to practice transfer/approve fuzz tests
contract Token {
string public name;
string public symbol;
uint8 public constant decimals = 18;

uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);

constructor(string memory name_, string memory symbol_, uint256 initialSupply) {
name = name_;
symbol = symbol_;
totalSupply = initialSupply;
balanceOf[msg.sender] = initialSupply;
emit Transfer(address(0), msg.sender, initialSupply);
}

function transfer(address to, uint256 amount) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}

function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
require(allowed >= amount, "Token: insufficient allowance");
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - amount;
}
_transfer(from, to, amount);
return true;
}

function _transfer(address from, address to, uint256 amount) internal {
require(to != address(0), "Token: transfer to zero");
uint256 bal = balanceOf[from];
require(bal >= amount, "Token: insufficient balance");
unchecked {
balanceOf[from] = bal - amount;
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
}
}
94 changes: 94 additions & 0 deletions test/Token.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {Token} from "../src/Token.sol";

contract TokenTest is Test {
Token public token;
address alice = makeAddr("alice");
address bob = makeAddr("bob");

uint256 constant INITIAL = 1_000_000 ether;

function setUp() public {
token = new Token("Test", "TST", INITIAL);
// Move the entire supply to alice so tests start from a known state
token.transfer(alice, INITIAL);
}

function test_Metadata() public view {
assertEq(token.name(), "Test");
assertEq(token.symbol(), "TST");
assertEq(token.decimals(), 18);
assertEq(token.totalSupply(), INITIAL);
}

function test_Transfer() public {
vm.prank(alice);
token.transfer(bob, 100 ether);

assertEq(token.balanceOf(alice), INITIAL - 100 ether);
assertEq(token.balanceOf(bob), 100 ether);
}

function test_Transfer_RevertOnInsufficientBalance() public {
vm.expectRevert("Token: insufficient balance");
vm.prank(bob);
token.transfer(alice, 1);
}

function test_Transfer_RevertOnZeroAddress() public {
vm.expectRevert("Token: transfer to zero");
vm.prank(alice);
token.transfer(address(0), 1);
}

function test_Approve_AndTransferFrom() public {
vm.prank(alice);
token.approve(bob, 50 ether);
assertEq(token.allowance(alice, bob), 50 ether);

vm.prank(bob);
token.transferFrom(alice, bob, 30 ether);

assertEq(token.balanceOf(bob), 30 ether);
assertEq(token.allowance(alice, bob), 20 ether);
}

function test_TransferFrom_RevertOnInsufficientAllowance() public {
vm.prank(alice);
token.approve(bob, 10 ether);

vm.expectRevert("Token: insufficient allowance");
vm.prank(bob);
token.transferFrom(alice, bob, 11 ether);
}

function test_InfiniteAllowance_NotDecremented() public {
vm.prank(alice);
token.approve(bob, type(uint256).max);

vm.prank(bob);
token.transferFrom(alice, bob, 100 ether);

// Infinite allowance should be preserved across transfers
assertEq(token.allowance(alice, bob), type(uint256).max);
}

function testFuzz_Transfer_PreservesTotalSupply(uint256 amount) public {
amount = bound(amount, 0, INITIAL);
vm.prank(alice);
token.transfer(bob, amount);

assertEq(token.balanceOf(alice) + token.balanceOf(bob), INITIAL);
assertEq(token.totalSupply(), INITIAL);
}

function testFuzz_Approve(address spender, uint256 amount) public {
vm.assume(spender != address(0));
vm.prank(alice);
token.approve(spender, amount);
assertEq(token.allowance(alice, spender), amount);
}
}
Loading