diff --git a/test/Base.t.sol b/test/Base.t.sol new file mode 100644 index 00000000..991c01aa --- /dev/null +++ b/test/Base.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30 <0.9.0; + +import {StdCheats} from "forge-std/StdCheats.sol"; +import {StdAssertions} from "forge-std/StdAssertions.sol"; + +import {Constants} from "./utils/Constants.sol"; +import {Defaults} from "./utils/Defaults.sol"; +import {Modifiers} from "./utils/Modifiers.sol"; +import {Users} from "./utils/Types.sol"; + +abstract contract Base_Test is Constants, Modifiers, StdAssertions, StdCheats { + /*////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + Users internal users; + + /*////////////////////////////////////////////////////////////// + TEST CONTRACTS + //////////////////////////////////////////////////////////////*/ + + Defaults internal defaults; + + /*////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////*/ + + function setUp() public virtual { + defaults = new Defaults(); + + createTestUsers(); + defaults.setUsers(users); + + setVariables(defaults, users); // set in modifier contract + + setMsgSender(users.alice); // alice default caller + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + function createUser(string memory name) internal returns (address payable user) { + user = payable(makeAddr(name)); // label implicitly created via Foundry + vm.deal({account: user, newBalance: 100 ether}); + } + + function createTestUsers() internal { + users.alice = createUser("Alice"); + users.bob = createUser("Bob"); + users.charlee = createUser("Charlee"); + + users.admin = createUser("Admin"); + users.receiver = createUser("Receiver"); + users.sender = createUser("Sender"); + } +} diff --git a/test/harnesses/token/ERC20/ERC20/ERC20BurnFacetHarness.sol b/test/harnesses/token/ERC20/ERC20/ERC20BurnFacetHarness.sol new file mode 100644 index 00000000..5f73e3a3 --- /dev/null +++ b/test/harnesses/token/ERC20/ERC20/ERC20BurnFacetHarness.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20BurnFacet} from "src/token/ERC20/ERC20/ERC20BurnFacet.sol"; + +/** + * @title ERC20BurnFacetHarness + * @notice Test harness for ERC20BurnFacet that adds initialization and minting for testing + */ +contract ERC20BurnFacetHarness is ERC20BurnFacet { + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + /** + * @notice ERC20 view helpers so tests can call the standard API + */ + function balanceOf(address _account) external view returns (uint256) { + return getStorage().balanceOf[_account]; + } + + function totalSupply() external view returns (uint256) { + return getStorage().totalSupply; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return getStorage().allowance[_owner][_spender]; + } + + /** + * @notice Minimal approve implementation for tests (writes into the same storage used by burnFrom) + */ + function approve(address _spender, uint256 _value) external returns (bool) { + require(_spender != address(0), "ERC20: approve to zero address"); + ERC20Storage storage s = getStorage(); + s.allowance[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + /** + * @notice Mint tokens to an address + * @dev Only used for testing - exposes internal mint functionality + */ + function mint(address _to, uint256 _value) external { + ERC20Storage storage s = getStorage(); + require(_to != address(0), "ERC20: mint to zero address"); + unchecked { + s.totalSupply += _value; + s.balanceOf[_to] += _value; + } + emit Transfer(address(0), _to, _value); + } +} diff --git a/test/harnesses/token/ERC20/ERC20/ERC20FacetHarness.sol b/test/harnesses/token/ERC20/ERC20/ERC20FacetHarness.sol new file mode 100644 index 00000000..3a3dbcf0 --- /dev/null +++ b/test/harnesses/token/ERC20/ERC20/ERC20FacetHarness.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20Facet} from "src/token/ERC20/ERC20/ERC20Facet.sol"; + +/** + * @title ERC20FacetHarness + * @notice Test harness for ERC20Facet that adds initialization and minting for testing + */ +contract ERC20FacetHarness is ERC20Facet { + /** + * @notice Initialize the ERC20 token storage + * @dev Only used for testing - production diamonds should initialize in constructor + */ + function initialize(string memory _name, string memory _symbol, uint8 _decimals) external { + ERC20Storage storage s = getStorage(); + s.name = _name; + s.symbol = _symbol; + s.decimals = _decimals; + } + + /** + * @notice Mint tokens to an address + * @dev Only used for testing - exposes internal mint functionality + */ + function mint(address _to, uint256 _value) external { + ERC20Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + unchecked { + s.totalSupply += _value; + s.balanceOf[_to] += _value; + } + emit Transfer(address(0), _to, _value); + } +} diff --git a/test/harnesses/token/ERC20/ERC20/ERC20Harness.sol b/test/harnesses/token/ERC20/ERC20/ERC20Harness.sol new file mode 100644 index 00000000..43622016 --- /dev/null +++ b/test/harnesses/token/ERC20/ERC20/ERC20Harness.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import "src/token/ERC20/ERC20/ERC20Mod.sol" as ERC20Mod; + +/** + * @title ERC20Harness + * @notice Test harness that exposes LibERC20's internal functions as external + * @dev Required for testing since LibERC20 only has internal functions + */ +contract ERC20Harness { + /** + * @notice Initialize the ERC20 token storage + * @dev Only used for testing + */ + function initialize(string memory _name, string memory _symbol, uint8 _decimals) external { + ERC20Mod.ERC20Storage storage s = ERC20Mod.getStorage(); + s.name = _name; + s.symbol = _symbol; + s.decimals = _decimals; + } + + /** + * @notice Exposes ERC20Mod.mint as an external function + */ + function mint(address _account, uint256 _value) external { + ERC20Mod.mint(_account, _value); + } + + /** + * @notice Exposes ERC20Mod.burn as an external function + */ + function burn(address _account, uint256 _value) external { + ERC20Mod.burn(_account, _value); + } + + /** + * @notice Exposes ERC20Mod.transferFrom as an external function + */ + function transferFrom(address _from, address _to, uint256 _value) external { + ERC20Mod.transferFrom(_from, _to, _value); + } + + /** + * @notice Exposes ERC20Mod.transfer as an external function + */ + function transfer(address _to, uint256 _value) external { + ERC20Mod.transfer(_to, _value); + } + + /** + * @notice Exposes ERC20Mod.approve as an external function + */ + function approve(address _spender, uint256 _value) external { + ERC20Mod.approve(_spender, _value); + } + + /** + * @notice Get storage values for testing + */ + function name() external view returns (string memory) { + return ERC20Mod.getStorage().name; + } + + function symbol() external view returns (string memory) { + return ERC20Mod.getStorage().symbol; + } + + function decimals() external view returns (uint8) { + return ERC20Mod.getStorage().decimals; + } + + function totalSupply() external view returns (uint256) { + return ERC20Mod.getStorage().totalSupply; + } + + function balanceOf(address _account) external view returns (uint256) { + return ERC20Mod.getStorage().balanceOf[_account]; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return ERC20Mod.getStorage().allowance[_owner][_spender]; + } +} diff --git a/test/harnesses/token/ERC20/ERC20/ERC20PermitFacetHarness.sol b/test/harnesses/token/ERC20/ERC20/ERC20PermitFacetHarness.sol new file mode 100644 index 00000000..20593c11 --- /dev/null +++ b/test/harnesses/token/ERC20/ERC20/ERC20PermitFacetHarness.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20PermitFacet} from "src/token/ERC20/ERC20Permit/ERC20PermitFacet.sol"; + +/** + * @title ERC20PermitFacetHarness + * @notice Test harness for ERC20PermitFacet that adds initialization and minting for testing + */ +contract ERC20PermitFacetHarness is ERC20PermitFacet { + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + /** + * @notice Initialize the ERC20 token storage + * @dev Only used for testing - production diamonds should initialize in constructor + */ + function initialize(string memory _name) external { + ERC20Storage storage s = getERC20Storage(); + s.name = _name; + } + + /** + * @notice Mint tokens to an address + * @dev Only used for testing - exposes internal mint functionality + */ + function mint(address _to, uint256 _value) external { + ERC20Storage storage s = getERC20Storage(); + require(_to != address(0), "ERC20: mint to zero address"); + unchecked { + s.totalSupply += _value; + s.balanceOf[_to] += _value; + } + emit Transfer(address(0), _to, _value); + } + + /** + * @notice ERC20 view helpers so tests can call the standard API + */ + function balanceOf(address _account) external view returns (uint256) { + return getERC20Storage().balanceOf[_account]; + } + + function totalSupply() external view returns (uint256) { + return getERC20Storage().totalSupply; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return getERC20Storage().allowance[_owner][_spender]; + } + + /** + * @notice Minimal approve implementation for tests + */ + function approve(address _spender, uint256 _value) external returns (bool) { + require(_spender != address(0), "ERC20: approve to zero address"); + ERC20Storage storage s = getERC20Storage(); + s.allowance[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + /** + * @notice TransferFrom implementation for tests (needed by test_Permit_ThenTransferFrom) + */ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + ERC20Storage storage s = getERC20Storage(); + require(_to != address(0), "ERC20: transfer to zero address"); + require(s.balanceOf[_from] >= _value, "ERC20: insufficient balance"); + + uint256 currentAllowance = s.allowance[_from][msg.sender]; + require(currentAllowance >= _value, "ERC20: insufficient allowance"); + + unchecked { + s.allowance[_from][msg.sender] = currentAllowance - _value; + s.balanceOf[_from] -= _value; + } + s.balanceOf[_to] += _value; + emit Transfer(_from, _to, _value); + return true; + } +} diff --git a/test/unit/concrete/token/ERC20/ERC20/metadata.t.sol b/test/unit/concrete/token/ERC20/ERC20/metadata.t.sol new file mode 100644 index 00000000..4a7629db --- /dev/null +++ b/test/unit/concrete/token/ERC20/ERC20/metadata.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {stdError} from "forge-std/StdError.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; + +import "src/token/ERC20/ERC20/ERC20Mod.sol" as ERC20Mod; + +contract Metadata_ERC20Mod_Concrete_Unit_Test is Base_Test { + ERC20Harness internal harness; + + function setUp() public override { + Base_Test.setUp(); + + harness = new ERC20Harness(); + harness.initialize(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS); + } + + function test_Name() external view { + assertEq(harness.name(), "Test Token"); + } + + function test_Symbol() external view { + assertEq(harness.symbol(), "TEST"); + } + + function test_Decimals() external view { + assertEq(harness.decimals(), 18); + } + + function test_InitialTotalSupply() external view { + assertEq(harness.totalSupply(), 0); + } +} diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol b/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol new file mode 100644 index 00000000..829efb6b --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {stdError} from "forge-std/StdError.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; + +import "src/token/ERC20/ERC20/ERC20Mod.sol" as ERC20Mod; + +contract Approve_ERC20Mod_Fuzz_Unit_Test is Base_Test { + ERC20Harness internal harness; + + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + function setUp() public override { + Base_Test.setUp(); + + harness = new ERC20Harness(); + } + + function testFuzz_ShouldRevert_SpenderIsZeroAddress(uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InvalidSpender.selector, ADDRESS_ZERO)); + harness.approve(ADDRESS_ZERO, value); + } + + function testFuzz_Approve(address spender, uint256 value) external whenSpenderNotZeroAddress { + vm.assume(spender != ADDRESS_ZERO); + + vm.expectEmit(address(harness)); + emit Approval(users.alice, spender, value); + harness.approve(spender, value); + + assertEq(harness.allowance(users.alice, spender), value); + } +} + diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/approve.tree b/test/unit/fuzz/token/ERC20/ERC20/mod/approve.tree new file mode 100644 index 00000000..44b17877 --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/approve.tree @@ -0,0 +1,6 @@ +Approve_ERC20Mod_Fuzz_Unit_Test +├── when the spender is the zero address +│ └── it should revert +└── when the spender is not the zero address + ├── it should set the spender's allowance from the caller + └── it should emit an {Approval} event \ No newline at end of file diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/burn.t.sol b/test/unit/fuzz/token/ERC20/ERC20/mod/burn.t.sol new file mode 100644 index 00000000..50b933bb --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/burn.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {stdError} from "forge-std/StdError.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; + +import "src/token/ERC20/ERC20/ERC20Mod.sol" as ERC20Mod; + +contract Burn_ERC20Mod_Fuzz_Unit_Test is Base_Test { + ERC20Harness internal harness; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + function setUp() public override { + Base_Test.setUp(); + + harness = new ERC20Harness(); + } + + function testFuzz_ShouldRevert_Account_ZeroAddress(uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InvalidSender.selector, address(0))); + harness.burn(ADDRESS_ZERO, value); + } + + function testFuzz_ShouldRevert_AccountBalanceLtBurnAmount(address account, uint256 balance, uint256 value) + external + whenAccountNotZeroAddress + { + vm.assume(account != ADDRESS_ZERO); + vm.assume(balance < MAX_UINT256); + value = bound(value, balance + 1, MAX_UINT256); + + harness.mint(account, balance); + + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InsufficientBalance.selector, account, balance, value)); + harness.burn(account, value); + } + + function testFuzz_Burn(address account, uint256 balance, uint256 value) + external + whenAccountNotZeroAddress + givenWhenAccountBalanceGEBurnAmount + { + vm.assume(account != ADDRESS_ZERO); + balance = bound(balance, 1, MAX_UINT256); + value = bound(value, 1, balance); + + harness.mint(account, balance); + + uint256 beforeTotalSupply = harness.totalSupply(); + uint256 beforeBalanceOfAccount = harness.balanceOf(account); + + vm.expectEmit(address(harness)); + emit Transfer(account, ADDRESS_ZERO, value); + harness.burn(account, value); + + assertEq(harness.totalSupply(), beforeTotalSupply - value, "totalSupply"); + assertEq(harness.balanceOf(account), beforeBalanceOfAccount - value, "balanceOf(account)"); + } +} + diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/burn.tree b/test/unit/fuzz/token/ERC20/ERC20/mod/burn.tree new file mode 100644 index 00000000..2faa2990 --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/burn.tree @@ -0,0 +1,10 @@ +Burn_ERC20Mod_Fuzz_Unit_Test +├── when the account to burn for is the zero address +│ └── it should revert +└── when the account to burn for is not the zero address + ├── when the balance of the account is less than the burn amount + │ └── it should revert + └── when the balance of the account is greater than, or equal to, the burn amount + ├── it should decrement the account's balance by the burn amount + ├── it should decrement the total supply by the burn amount + └── it should emit a {Transfer} event diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/mint.t.sol b/test/unit/fuzz/token/ERC20/ERC20/mod/mint.t.sol new file mode 100644 index 00000000..6bc10277 --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/mint.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {stdError} from "forge-std/StdError.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; + +import "src/token/ERC20/ERC20/ERC20Mod.sol" as ERC20Mod; + +contract Mint_ERC20Mod_Fuzz_Unit_Test is Base_Test { + ERC20Harness internal harness; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + function setUp() public override { + Base_Test.setUp(); + + harness = new ERC20Harness(); + } + + function testFuzz_ShouldRevert_Account_ZeroAddress(uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InvalidReceiver.selector, address(0))); + harness.mint(ADDRESS_ZERO, value); + } + + function testFuzz_ShouldRevert_TotalSupply_Overflows(address account, uint256 value) + external + whenAccountNotZeroAddress + { + vm.assume(account != ADDRESS_ZERO); + vm.assume(value > 0); + + harness.mint(users.alice, MAX_UINT256); + + vm.expectRevert(stdError.arithmeticError); + harness.mint(account, value); + } + + function testFuzz_Mint(address account, uint256 value) + external + whenAccountNotZeroAddress + givenWhenTotalSupplyNotOverflow + { + vm.assume(account != ADDRESS_ZERO); + + uint256 beforeTotalSupply = harness.totalSupply(); + uint256 beforeBalanceOfAccount = harness.balanceOf(account); + + vm.expectEmit(address(harness)); + emit Transfer(ADDRESS_ZERO, account, value); + harness.mint(account, value); + + assertEq(harness.totalSupply(), beforeTotalSupply + value, "totalSupply"); + assertEq(harness.balanceOf(account), beforeBalanceOfAccount + value, "balanceOf(account)"); + } +} + diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/mint.tree b/test/unit/fuzz/token/ERC20/ERC20/mod/mint.tree new file mode 100644 index 00000000..3045ce31 --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/mint.tree @@ -0,0 +1,10 @@ +Mint_ERC20Mod_Fuzz_Unit_Test +├── when the account to mint for is the zero address +│ └── it should revert +└── when the account to mint for is not the zero address + ├── given when the value to mint causes the total supply to overflow + │ └── it should revert + └── given when the value to mint does not cause total supply to overflow + ├── it should increment the total supply by the mint amount + ├── it should increment the account's balance by the mint amount + └── it should emit a {Transfer} event diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.t.sol b/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.t.sol new file mode 100644 index 00000000..222b94ed --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {stdError} from "forge-std/StdError.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; + +import "src/token/ERC20/ERC20/ERC20Mod.sol" as ERC20Mod; + +contract Transfer_ERC20Mod_Fuzz_Unit_Test is Base_Test { + ERC20Harness internal harness; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + function setUp() public override { + Base_Test.setUp(); + + harness = new ERC20Harness(); + } + + function testFuzz_ShouldRevert_ReceiverIsZeroAddress(uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InvalidReceiver.selector, ADDRESS_ZERO)); + harness.transfer(ADDRESS_ZERO, value); + } + + function testFuzz_ShouldRevert_CallerInsufficientBalance(address to, uint256 balance, uint256 value) + external + whenReceiverNotZeroAddress + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(balance < MAX_UINT256); + value = bound(value, balance + 1, MAX_UINT256); + + harness.mint(users.alice, balance); + + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InsufficientBalance.selector, users.alice, balance, value)); + harness.transfer(to, value); + } + + function testFuzz_Transfer(address to, uint256 balance, uint256 value) + external + whenReceiverNotZeroAddress + givenWhenSenderBalanceGETransferAmount + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != users.alice); + balance = bound(balance, 1, MAX_UINT256); + value = bound(value, 1, balance); + + harness.mint(users.alice, balance); + + uint256 beforeBalanceOfAlice = harness.balanceOf(users.alice); + uint256 beforeBalanceOfTo = harness.balanceOf(to); + + vm.expectEmit(address(harness)); + emit Transfer(users.alice, to, value); + harness.transfer(to, value); + + assertEq(harness.balanceOf(users.alice), beforeBalanceOfAlice - value, "balanceOf(users.alice)"); + assertEq(harness.balanceOf(to), beforeBalanceOfTo + value, "balanceOf(to)"); + } +} + diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.tree b/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.tree new file mode 100644 index 00000000..aae5f95c --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.tree @@ -0,0 +1,10 @@ +Transfer_ERC20Mod_Fuzz_Unit_Test +├── when the receiver address is the zero address +│ └── it should revert +└── when the receiver is not the zero address + ├── given when the balance of the sender is less than the transfer amount + │ └── it should revert + └── given when the balance of the sender is greater than, or equal to, the transfer amount + ├── it should decrement the balance of the sender by the transfer amount + ├── it should increment the balance of the receiver by the transfer amount + └── it should emit a {Transfer} event diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.t.sol b/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.t.sol new file mode 100644 index 00000000..7e68aad4 --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {stdError} from "forge-std/StdError.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; + +import "src/token/ERC20/ERC20/ERC20Mod.sol" as ERC20Mod; + +contract TransferFrom_ERC20Mod_Fuzz_Unit_Test is Base_Test { + ERC20Harness internal harness; + + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + function setUp() public override { + Base_Test.setUp(); + + harness = new ERC20Harness(); + } + + function testFuzz_ShouldRevert_SenderIsZeroAddress(address to, uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InvalidSender.selector, ADDRESS_ZERO)); + harness.transferFrom(ADDRESS_ZERO, to, value); + } + + function testFuzz_ShouldRevert_ReceiverIsZeroAddress(address from, uint256 value) + external + whenSenderNotZeroAddress + { + vm.assume(from != ADDRESS_ZERO); + + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InvalidReceiver.selector, ADDRESS_ZERO)); + harness.transferFrom(from, ADDRESS_ZERO, value); + } + + function testFuzz_ShouldRevert_SpenderAllowanceLtAmount(address from, address to, uint256 value, uint256 allowance) + external + whenSenderNotZeroAddress + whenReceiverNotZeroAddress + { + vm.assume(from != ADDRESS_ZERO); + vm.assume(to != ADDRESS_ZERO); + allowance = bound(allowance, 0, MAX_UINT256 - 1); + value = bound(value, allowance + 1, MAX_UINT256); + + setMsgSender(from); + harness.approve(users.sender, allowance); + setMsgSender(users.sender); + + vm.expectRevert( + abi.encodeWithSelector(ERC20Mod.ERC20InsufficientAllowance.selector, users.sender, allowance, value) + ); + harness.transferFrom(from, to, value); + } + + function testFuzz_ShouldRevert_SenderBalanceLtAmount( + address from, + address to, + uint256 value, + uint256 allowance, + uint256 balance + ) external whenSenderNotZeroAddress whenReceiverNotZeroAddress givenWhenSpenderAllowanceGETransferAmount { + vm.assume(from != ADDRESS_ZERO); + vm.assume(to != ADDRESS_ZERO); + + value = bound(value, 1, MAX_UINT256); + allowance = bound(allowance, value, MAX_UINT256); // allowance >= value + balance = bound(balance, 0, value - 1); // balance < value + + harness.mint(from, balance); + + setMsgSender(from); + harness.approve(users.sender, allowance); + setMsgSender(users.sender); + + vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InsufficientBalance.selector, from, balance, value)); + harness.transferFrom(from, to, value); + } + + function testFuzz_TransferFrom_InfiniteApproval(address from, address to, uint256 value, uint256 balance) + external + whenSenderNotZeroAddress + whenReceiverNotZeroAddress + givenWhenSpenderAllowanceGETransferAmount + givenWhenSenderBalanceGETransferAmount + { + vm.assume(from != ADDRESS_ZERO); + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != from); + vm.assume(users.sender != from); + + value = bound(value, 1, MAX_UINT256); + balance = bound(balance, value, MAX_UINT256); + + harness.mint(from, balance); + + setMsgSender(from); + harness.approve(users.sender, MAX_UINT256); + setMsgSender(users.sender); + + uint256 beforeBalanceOfFrom = harness.balanceOf(from); + uint256 beforeBalanceOfTo = harness.balanceOf(to); + + vm.expectEmit(address(harness)); + emit Transfer(from, to, value); + harness.transferFrom(from, to, value); + + assertEq(harness.balanceOf(from), beforeBalanceOfFrom - value, "balanceOf(from)"); + assertEq(harness.balanceOf(to), beforeBalanceOfTo + value, "balanceOf(to)"); + } + + function testFuzz_TransferFrom(address from, address to, uint256 value, uint256 allowance, uint256 balance) + external + whenSenderNotZeroAddress + whenReceiverNotZeroAddress + givenWhenSpenderAllowanceGETransferAmount + givenWhenSenderBalanceGETransferAmount + { + vm.assume(from != ADDRESS_ZERO); + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != from); + vm.assume(users.sender != from); + + value = bound(value, 1, MAX_UINT256 - 1); + allowance = bound(allowance, value, MAX_UINT256 - 1); + balance = bound(balance, value, MAX_UINT256); + + harness.mint(from, balance); + + setMsgSender(from); + harness.approve(users.sender, allowance); + setMsgSender(users.sender); + + uint256 beforeBalanceOfFrom = harness.balanceOf(from); + uint256 beforeBalanceOfTo = harness.balanceOf(to); + + vm.expectEmit(address(harness)); + emit Transfer(from, to, value); + harness.transferFrom(from, to, value); + + assertEq(harness.balanceOf(from), beforeBalanceOfFrom - value, "balanceOf(from)"); + assertEq(harness.balanceOf(to), beforeBalanceOfTo + value, "balanceOf(to)"); + assertEq(harness.allowance(from, users.sender), allowance - value, "allowance(from, users.sender)"); + } +} diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.tree b/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.tree new file mode 100644 index 00000000..e774d902 --- /dev/null +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.tree @@ -0,0 +1,22 @@ +TransferFrom_ERC20Mod_Fuzz_Unit_Test +├── when the sender is the zero address +│ └── it should revert +└── when the sender is not the zero address + ├── when the receiver is the zero address + │ └── it should revert + └── when the receiver is not the zero address + ├── given when the spender's allowance is less than the transfer amount + │ └── it should revert + └── given when the spender's allowance is greater than, or equal to, the transfer amount + ├── given when the sender's balance is less than the transfer amount + │ └── it should revert + └── given when the sender's balance is greater than, or equal to, the transfer amount + ├── given when the spender's allowance equals the maximum uint256 value + │ ├── it should decrement the sender's balance by the transfer amount + │ ├── it should increment the receiver's balance by the transfer amount + │ └── it should emit a {Transfer} event + └── given when the sender's allowance does not equal the maximum uint256 value + ├── it should decrement the spender's allowance by the transfer amount + ├── it should decrement the sender's balance by the transfer amount + ├── it should increment the receiver's balance by the transfer amount + └── it should emit a {Transfer} event diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol new file mode 100644 index 00000000..e21f6c15 --- /dev/null +++ b/test/utils/Constants.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +abstract contract Constants { + /*////////////////////////////////////////////////////////////// + GENERIC + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant MAX_UINT256 = type(uint256).max; + + address internal constant ADDRESS_ZERO = address(0); + + /*////////////////////////////////////////////////////////////// + INTERFACE ID + //////////////////////////////////////////////////////////////*/ + + bytes4 internal constant IERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 internal constant IERC20_INTERFACE_ID = 0x36372b07; + bytes4 internal constant IERC721_INTERFACE_ID = 0x80ac58cd; + bytes4 internal constant IERC1155_INTERFACE_ID = 0xd9b67a26; + bytes4 internal constant INVALID_INTERFACE_ID = 0xffffffff; + bytes4 internal constant CUSTOM_INTERFACE_ID = 0x12345678; + bytes4 internal constant ZERO_INTERFACE_ID = 0x00000000; + + /*////////////////////////////////////////////////////////////// + ROLES + //////////////////////////////////////////////////////////////*/ + + bytes32 internal constant DEFAULT_ADMIN_ROLE = 0x00; + bytes32 internal constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 internal constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 internal constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 internal constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE"); + bytes32 internal constant USER_ROLE = keccak256("USER_ROLE"); + bytes32 internal constant TRUSTED_BRIDGE_ROLE = keccak256("TRUSTED_BRIDGE_ROLE"); + + /*////////////////////////////////////////////////////////////// + TOKEN + //////////////////////////////////////////////////////////////*/ + + string public constant TOKEN_NAME = "Test Token"; + string public constant TOKEN_SYMBOL = "TEST"; + uint8 public constant TOKEN_DECIMALS = 18; + uint256 internal constant INITIAL_SUPPLY = 1_000_000e18; + + uint256 internal constant TOKEN_ID_1 = 1; + uint256 internal constant TOKEN_ID_2 = 2; + uint256 internal constant TOKEN_ID_3 = 3; + + /*////////////////////////////////////////////////////////////// + URI + //////////////////////////////////////////////////////////////*/ + + string internal constant BASE_URI = "https://example.com/api/nft/"; + string internal constant DEFAULT_URI = "https://token.uri/{id}.json"; + string internal constant TOKEN_URI = "token1.json"; + + /*////////////////////////////////////////////////////////////// + ERC-1155 RECEIVER MAGIC VALUES + //////////////////////////////////////////////////////////////*/ + + bytes4 internal constant RECEIVER_SINGLE_MAGIC_VALUE = 0xf23a6e61; // onERC1155Received + bytes4 internal constant RECEIVER_BATCH_MAGIC_VALUE = 0xbc197c81; // onERC1155BatchReceived + + /*////////////////////////////////////////////////////////////// + FEE + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant FEE_DENOMINATOR = 10_000; +} diff --git a/test/utils/Defaults.sol b/test/utils/Defaults.sol new file mode 100644 index 00000000..6aa44f6b --- /dev/null +++ b/test/utils/Defaults.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Constants} from "./Constants.sol"; +import {Users} from "./Types.sol"; + +contract Defaults is Constants { + /*////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + Users private users; + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + function setUsers(Users memory users_) public { + users = users_; + } +} diff --git a/test/utils/Modifiers.sol b/test/utils/Modifiers.sol new file mode 100644 index 00000000..e33e7492 --- /dev/null +++ b/test/utils/Modifiers.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Defaults} from "./Defaults.sol"; +import {Users} from "./Types.sol"; +import {Utils} from "./Utils.sol"; + +abstract contract Modifiers is Utils { + /*////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + Defaults private defaults; + Users private users; + + function setVariables(Defaults _defaults, Users memory _users) public { + defaults = _defaults; + users = _users; + } + + /*////////////////////////////////////////////////////////////// + ERC-20 + //////////////////////////////////////////////////////////////*/ + + modifier whenAccountNotZeroAddress() { + _; + } + + modifier whenReceiverNotZeroAddress() { + _; + } + + modifier whenSpenderNotZeroAddress() { + _; + } + + modifier whenSenderNotZeroAddress() { + _; + } + + modifier givenWhenTotalSupplyNotOverflow() { + _; + } + + modifier givenWhenAccountBalanceGEBurnAmount() { + _; + } + + modifier givenWhenSenderBalanceGETransferAmount() { + _; + } + + modifier givenWhenSpenderAllowanceGETransferAmount() { + _; + } +} diff --git a/test/utils/Types.sol b/test/utils/Types.sol new file mode 100644 index 00000000..6e29464a --- /dev/null +++ b/test/utils/Types.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +struct Users { + address payable alice; + address payable bob; + address payable charlee; + address payable admin; + address payable receiver; + address payable sender; +} diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol new file mode 100644 index 00000000..32354477 --- /dev/null +++ b/test/utils/Utils.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {CommonBase as StdBase} from "forge-std/Base.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; + +abstract contract Utils is StdBase, StdUtils { + /*////////////////////////////////////////////////////////////// + MISC + //////////////////////////////////////////////////////////////*/ + + function getBlockTimestamp() internal view returns (uint40) { + return uint40(vm.getBlockTimestamp()); + } + + function setMsgSender(address msgSender) internal { + vm.stopPrank(); + vm.startPrank(msgSender); + + vm.deal(msgSender, 1 ether); // Deal ETH to new caller. + } +}