diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 5c98c00..651f682 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -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 { @@ -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(); } diff --git a/src/Token.sol b/src/Token.sol new file mode 100644 index 0000000..b5413da --- /dev/null +++ b/src/Token.sol @@ -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); + } +} diff --git a/test/Token.t.sol b/test/Token.t.sol new file mode 100644 index 0000000..d2263f0 --- /dev/null +++ b/test/Token.t.sol @@ -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); + } +}