From 6bc2de69f5dee0aa455970d48d755c34216146f0 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 19:20:58 +0100 Subject: [PATCH 01/10] test: add ERC-20 tree specification --- test/trees/ERC20.tree | 76 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/trees/ERC20.tree diff --git a/test/trees/ERC20.tree b/test/trees/ERC20.tree new file mode 100644 index 00000000..a8bae69e --- /dev/null +++ b/test/trees/ERC20.tree @@ -0,0 +1,76 @@ +Approve +├── when the spender is the zero address +│ └── it should revert +└── when the spender is not the zero address + ├── it should set the caller's allowance for the spender to value + ├── it should emit an {Approval} event + └── it should return true + +Transfer +├── when the receiver is the zero address +│ └── it should revert +└── when the receiver is not the zero address + ├── 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 + ├── when the amount is zero + │ ├── it should emit a {Transfer} event + │ ├── it should not change balances + │ └── it should return true + └── when the amount is > 0 + ├── 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 + └── it should return true + +TransferFrom +├── when the sender is the zero address +│ └── it should revert +├── when the receiver is the zero address +│ └── it should revert +└── when both sender and receiver are non-zero addresses + ├── given allowance < amount + │ └── it should revert + └── given allowance >= amount + ├── given balance < amount + │ └── it should revert + └── given balance >= amount + ├── when amount is zero + │ ├── it should not change balances + │ ├── it should not decrement allowance + │ ├── it should emit a {Transfer} event + │ └── it should return true + ├── when amount > 0 and allowance is max uint256 + │ ├── it should decrement the sender's balance by the transfer amount + │ ├── it should increment the receiver's balance by the transfer amount + │ ├── it should not change allowance + │ ├── it should emit a {Transfer} event + │ └── it should return true + └── when the amount is greater than zero and allowance is finite + ├── it should decrement the 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 + └── it should return true + +Mint +├── when the account to mint to is the zero address +│ └── it should revert +└── when the account to mint to is not the zero address + ├── given the mint amount would overflow total supply + │ └── it should revert + └── given the mint amount does not overflow total supply + ├── it should increment total supply by the mint amount + ├── it should increment the account's balance by the mint amount + └── it should emit a {Transfer} event from zero address to the account + +Burn +├── when the account to burn from is the zero address +│ └── it should revert +└── when the account to burn from is not the zero address + ├── when the account balance is less than the burn amount + │ └── it should revert + └── when the account balance is greater than or equal to the burn amount + ├── it should decrement the account balance by the burn amount + ├── it should decrement total supply by the burn amount + └── it should emit a {Transfer} event to the zero address \ No newline at end of file From cab2d86ce634bd1ad4644e63c3c0021f4fc250a3 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 19:22:11 +0100 Subject: [PATCH 02/10] test: centralise ERC-20 BTT spec and drop per-suite trees --- .../fuzz/token/ERC20/ERC20/mod/approve.t.sol | 9 +++----- .../fuzz/token/ERC20/ERC20/mod/approve.tree | 6 ----- .../fuzz/token/ERC20/ERC20/mod/burn.t.sol | 11 ++++------ .../unit/fuzz/token/ERC20/ERC20/mod/burn.tree | 10 --------- .../fuzz/token/ERC20/ERC20/mod/mint.t.sol | 9 +++----- .../unit/fuzz/token/ERC20/ERC20/mod/mint.tree | 10 --------- .../fuzz/token/ERC20/ERC20/mod/transfer.t.sol | 11 ++++------ .../fuzz/token/ERC20/ERC20/mod/transfer.tree | 10 --------- .../token/ERC20/ERC20/mod/transferFrom.t.sol | 17 +++++--------- .../token/ERC20/ERC20/mod/transferFrom.tree | 22 ------------------- 10 files changed, 20 insertions(+), 95 deletions(-) delete mode 100644 test/unit/fuzz/token/ERC20/ERC20/mod/approve.tree delete mode 100644 test/unit/fuzz/token/ERC20/ERC20/mod/burn.tree delete mode 100644 test/unit/fuzz/token/ERC20/ERC20/mod/mint.tree delete mode 100644 test/unit/fuzz/token/ERC20/ERC20/mod/transfer.tree delete mode 100644 test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.tree diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol b/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol index 08659e5c..f456eb1d 100644 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol @@ -9,21 +9,19 @@ 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; +import "src/token/ERC20/ERC20/ERC20Mod.sol"; +/// @dev BTT spec: test/trees/ERC20.tree 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)); + vm.expectRevert(abi.encodeWithSelector(ERC20InvalidSpender.selector, ADDRESS_ZERO)); harness.approve(ADDRESS_ZERO, value); } @@ -37,4 +35,3 @@ contract Approve_ERC20Mod_Fuzz_Unit_Test is Base_Test { 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 deleted file mode 100644 index 44b17877..00000000 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/approve.tree +++ /dev/null @@ -1,6 +0,0 @@ -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 index fe95b3c2..589d4ca2 100644 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/burn.t.sol +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/burn.t.sol @@ -9,21 +9,19 @@ 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; +import "src/token/ERC20/ERC20/ERC20Mod.sol"; +/// @dev BTT spec: test/trees/ERC20.tree 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))); + vm.expectRevert(abi.encodeWithSelector(ERC20InvalidSender.selector, address(0))); harness.burn(ADDRESS_ZERO, value); } @@ -37,7 +35,7 @@ contract Burn_ERC20Mod_Fuzz_Unit_Test is Base_Test { harness.mint(account, balance); - vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InsufficientBalance.selector, account, balance, value)); + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, account, balance, value)); harness.burn(account, value); } @@ -63,4 +61,3 @@ contract Burn_ERC20Mod_Fuzz_Unit_Test is Base_Test { 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 deleted file mode 100644 index 2faa2990..00000000 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/burn.tree +++ /dev/null @@ -1,10 +0,0 @@ -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 index efd0c547..bb625081 100644 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/mint.t.sol +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/mint.t.sol @@ -9,21 +9,19 @@ 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; +import "src/token/ERC20/ERC20/ERC20Mod.sol"; +/// @dev BTT spec: test/trees/ERC20.tree 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))); + vm.expectRevert(abi.encodeWithSelector(ERC20InvalidReceiver.selector, address(0))); harness.mint(ADDRESS_ZERO, value); } @@ -58,4 +56,3 @@ contract Mint_ERC20Mod_Fuzz_Unit_Test is Base_Test { 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 deleted file mode 100644 index 3045ce31..00000000 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/mint.tree +++ /dev/null @@ -1,10 +0,0 @@ -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 index 6f1b7e69..7918779a 100644 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.t.sol +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.t.sol @@ -9,21 +9,19 @@ 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; +import "src/token/ERC20/ERC20/ERC20Mod.sol"; +/// @dev BTT spec: test/trees/ERC20.tree 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)); + vm.expectRevert(abi.encodeWithSelector(ERC20InvalidReceiver.selector, ADDRESS_ZERO)); harness.transfer(ADDRESS_ZERO, value); } @@ -37,7 +35,7 @@ contract Transfer_ERC20Mod_Fuzz_Unit_Test is Base_Test { harness.mint(users.alice, balance); - vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InsufficientBalance.selector, users.alice, balance, value)); + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, users.alice, balance, value)); harness.transfer(to, value); } @@ -64,4 +62,3 @@ contract Transfer_ERC20Mod_Fuzz_Unit_Test is Base_Test { 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 deleted file mode 100644 index aae5f95c..00000000 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.tree +++ /dev/null @@ -1,10 +0,0 @@ -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 index 339df5e1..2f5e9708 100644 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.t.sol +++ b/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.t.sol @@ -9,22 +9,19 @@ 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; +import "src/token/ERC20/ERC20/ERC20Mod.sol"; +/// @dev BTT spec: test/trees/ERC20.tree 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)); + vm.expectRevert(abi.encodeWithSelector(ERC20InvalidSender.selector, ADDRESS_ZERO)); harness.transferFrom(ADDRESS_ZERO, to, value); } @@ -34,7 +31,7 @@ contract TransferFrom_ERC20Mod_Fuzz_Unit_Test is Base_Test { { vm.assume(from != ADDRESS_ZERO); - vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InvalidReceiver.selector, ADDRESS_ZERO)); + vm.expectRevert(abi.encodeWithSelector(ERC20InvalidReceiver.selector, ADDRESS_ZERO)); harness.transferFrom(from, ADDRESS_ZERO, value); } @@ -52,9 +49,7 @@ contract TransferFrom_ERC20Mod_Fuzz_Unit_Test is Base_Test { harness.approve(users.sender, allowance); setMsgSender(users.sender); - vm.expectRevert( - abi.encodeWithSelector(ERC20Mod.ERC20InsufficientAllowance.selector, users.sender, allowance, value) - ); + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientAllowance.selector, users.sender, allowance, value)); harness.transferFrom(from, to, value); } @@ -78,7 +73,7 @@ contract TransferFrom_ERC20Mod_Fuzz_Unit_Test is Base_Test { harness.approve(users.sender, allowance); setMsgSender(users.sender); - vm.expectRevert(abi.encodeWithSelector(ERC20Mod.ERC20InsufficientBalance.selector, from, balance, value)); + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, from, balance, value)); harness.transferFrom(from, to, value); } diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.tree b/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.tree deleted file mode 100644 index e774d902..00000000 --- a/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.tree +++ /dev/null @@ -1,22 +0,0 @@ -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 From a2d8c35ad7053c864974134bd9d009afa1fb22f4 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 19:24:49 +0100 Subject: [PATCH 03/10] test: reorganise unit testing layout --- .../ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/approve.t.sol | 0 .../ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/burn.t.sol | 0 .../ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/mint.t.sol | 0 .../ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/transfer.t.sol | 0 .../ERC20/mod => token/ERC20/ERC20/mod/fuzz}/transferFrom.t.sol | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename test/unit/{fuzz/token/ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/approve.t.sol (100%) rename test/unit/{fuzz/token/ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/burn.t.sol (100%) rename test/unit/{fuzz/token/ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/mint.t.sol (100%) rename test/unit/{fuzz/token/ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/transfer.t.sol (100%) rename test/unit/{fuzz/token/ERC20/ERC20/mod => token/ERC20/ERC20/mod/fuzz}/transferFrom.t.sol (100%) diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol similarity index 100% rename from test/unit/fuzz/token/ERC20/ERC20/mod/approve.t.sol rename to test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/burn.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/burn.t.sol similarity index 100% rename from test/unit/fuzz/token/ERC20/ERC20/mod/burn.t.sol rename to test/unit/token/ERC20/ERC20/mod/fuzz/burn.t.sol diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/mint.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/mint.t.sol similarity index 100% rename from test/unit/fuzz/token/ERC20/ERC20/mod/mint.t.sol rename to test/unit/token/ERC20/ERC20/mod/fuzz/mint.t.sol diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/transfer.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol similarity index 100% rename from test/unit/fuzz/token/ERC20/ERC20/mod/transfer.t.sol rename to test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol diff --git a/test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol similarity index 100% rename from test/unit/fuzz/token/ERC20/ERC20/mod/transferFrom.t.sol rename to test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol From 7863233194b6f58aa393e1d104156035be2d1e7e Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 20:37:07 +0100 Subject: [PATCH 04/10] chore: align ERC20Mod return values with ERC-20 --- src/token/ERC20/ERC20/ERC20Mod.sol | 9 ++++++--- test/harnesses/token/ERC20/ERC20/ERC20Harness.sol | 12 ++++++------ test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol | 3 ++- test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol | 3 ++- .../token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol | 6 ++++-- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/token/ERC20/ERC20/ERC20Mod.sol b/src/token/ERC20/ERC20/ERC20Mod.sol index d4c43027..f8ce5335 100644 --- a/src/token/ERC20/ERC20/ERC20Mod.sol +++ b/src/token/ERC20/ERC20/ERC20Mod.sol @@ -133,7 +133,7 @@ function burn(address _account, uint256 _value) { * @param _to The address to send tokens to. * @param _value The number of tokens to transfer. */ -function transferFrom(address _from, address _to, uint256 _value) { +function transferFrom(address _from, address _to, uint256 _value) returns (bool) { ERC20TransferStorage storage s = getStorage(); if (_from == address(0)) { revert ERC20InvalidSender(address(0)); @@ -157,6 +157,7 @@ function transferFrom(address _from, address _to, uint256 _value) { } s.balanceOf[_to] += _value; emit Transfer(_from, _to, _value); + return true; } /** @@ -165,7 +166,7 @@ function transferFrom(address _from, address _to, uint256 _value) { * @param _to The address to send tokens to. * @param _value The number of tokens to transfer. */ -function transfer(address _to, uint256 _value) { +function transfer(address _to, uint256 _value) returns (bool) { ERC20TransferStorage storage s = getStorage(); if (_to == address(0)) { revert ERC20InvalidReceiver(address(0)); @@ -179,6 +180,7 @@ function transfer(address _to, uint256 _value) { } s.balanceOf[_to] += _value; emit Transfer(msg.sender, _to, _value); + return true; } /** @@ -187,11 +189,12 @@ function transfer(address _to, uint256 _value) { * @param _spender The address to approve for spending. * @param _value The amount of tokens to approve. */ -function approve(address _spender, uint256 _value) { +function approve(address _spender, uint256 _value) returns (bool) { if (_spender == address(0)) { revert ERC20InvalidSpender(address(0)); } ERC20TransferStorage storage s = getStorage(); s.allowance[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); + return true; } diff --git a/test/harnesses/token/ERC20/ERC20/ERC20Harness.sol b/test/harnesses/token/ERC20/ERC20/ERC20Harness.sol index f2d139a4..efdb9b55 100644 --- a/test/harnesses/token/ERC20/ERC20/ERC20Harness.sol +++ b/test/harnesses/token/ERC20/ERC20/ERC20Harness.sol @@ -30,22 +30,22 @@ contract ERC20Harness { /** * @notice Exposes ERC20Mod.transferFrom as an external function */ - function transferFrom(address _from, address _to, uint256 _value) external { - ERC20Mod.transferFrom(_from, _to, _value); + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + return ERC20Mod.transferFrom(_from, _to, _value); } /** * @notice Exposes ERC20Mod.transfer as an external function */ - function transfer(address _to, uint256 _value) external { - ERC20Mod.transfer(_to, _value); + function transfer(address _to, uint256 _value) external returns (bool) { + return ERC20Mod.transfer(_to, _value); } /** * @notice Exposes ERC20Mod.approve as an external function */ - function approve(address _spender, uint256 _value) external { - ERC20Mod.approve(_spender, _value); + function approve(address _spender, uint256 _value) external returns (bool) { + return ERC20Mod.approve(_spender, _value); } function totalSupply() external view returns (uint256) { diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol index f456eb1d..0e05423d 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol @@ -30,8 +30,9 @@ contract Approve_ERC20Mod_Fuzz_Unit_Test is Base_Test { vm.expectEmit(address(harness)); emit Approval(users.alice, spender, value); - harness.approve(spender, value); + bool result = harness.approve(spender, value); + assertEq(result, true, "approve failed"); assertEq(harness.allowance(users.alice, spender), value); } } diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol index 7918779a..dc59780e 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol @@ -56,8 +56,9 @@ contract Transfer_ERC20Mod_Fuzz_Unit_Test is Base_Test { vm.expectEmit(address(harness)); emit Transfer(users.alice, to, value); - harness.transfer(to, value); + bool result = harness.transfer(to, value); + assertEq(result, true, "transfer failed"); assertEq(harness.balanceOf(users.alice), beforeBalanceOfAlice - value, "balanceOf(users.alice)"); assertEq(harness.balanceOf(to), beforeBalanceOfTo + value, "balanceOf(to)"); } diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol index 2f5e9708..794b563b 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol @@ -103,8 +103,9 @@ contract TransferFrom_ERC20Mod_Fuzz_Unit_Test is Base_Test { vm.expectEmit(address(harness)); emit Transfer(from, to, value); - harness.transferFrom(from, to, value); + bool result = harness.transferFrom(from, to, value); + assertEq(result, true, "transfer failed"); assertEq(harness.balanceOf(from), beforeBalanceOfFrom - value, "balanceOf(from)"); assertEq(harness.balanceOf(to), beforeBalanceOfTo + value, "balanceOf(to)"); } @@ -136,8 +137,9 @@ contract TransferFrom_ERC20Mod_Fuzz_Unit_Test is Base_Test { vm.expectEmit(address(harness)); emit Transfer(from, to, value); - harness.transferFrom(from, to, value); + bool result = harness.transferFrom(from, to, value); + assertEq(result, true, "transfer failed"); 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)"); From 86a6abddf6faacf7e4af7970285ac608e37201d6 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 20:57:31 +0100 Subject: [PATCH 05/10] test: add ERC20TransferFacet base and approve unit tests --- .../ERC20TransferFacetBase.t.sol | 24 +++++++++++++++ .../ERC20/transfer-facet/fuzz/approve.t.sol | 30 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol create mode 100644 test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol new file mode 100644 index 00000000..fc3e5839 --- /dev/null +++ b/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30 <0.9.0; + +/* Compose + * https://compose.diamonds + */ + +import {stdError} from "forge-std/StdError.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; +import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; + +/// @dev BTT spec: test/trees/ERC20.tree +contract ERC20TransferFacet_Base_Test is Base_Test { + using ERC20StorageUtils for address; + + ERC20TransferFacet internal facet; + + function setUp() public virtual override { + Base_Test.setUp(); + facet = new ERC20TransferFacet(); + vm.label(address(facet), "ERC20TransferFacet"); + } +} diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol new file mode 100644 index 00000000..b4a59f4e --- /dev/null +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {stdError} from "forge-std/StdError.sol"; +import {ERC20TransferFacet_Base_Test} from "../ERC20TransferFacetBase.t.sol"; + +import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; + +/// @dev BTT spec: test/trees/ERC20.tree +contract Approve_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_Test { + function testFuzz_ShouldRevert_SpenderIsZeroAddress(uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20TransferFacet.ERC20InvalidSpender.selector, ADDRESS_ZERO)); + facet.approve(ADDRESS_ZERO, value); + } + + function testFuzz_Approve(address spender, uint256 value) external whenSpenderNotZeroAddress { + vm.assume(spender != ADDRESS_ZERO); + + vm.expectEmit(address(facet)); + emit ERC20TransferFacet.Approval(users.alice, spender, value); + bool result = facet.approve(spender, value); + + assertEq(result, true, "approve failed"); + assertEq(facet.allowance(users.alice, spender), value); + } +} From 16d25efab3184bd63432002eba80399cb6a54d1d Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 21:10:21 +0100 Subject: [PATCH 06/10] test: add ERC20TransferFacet transfer unit tests --- .../ERC20TransferFacetBase.t.sol | 4 -- .../ERC20/transfer-facet/fuzz/transfer.t.sol | 61 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol index fc3e5839..2c976a9f 100644 --- a/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol +++ b/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol @@ -5,15 +5,11 @@ pragma solidity >=0.8.30 <0.9.0; * https://compose.diamonds */ -import {stdError} from "forge-std/StdError.sol"; import {Base_Test} from "test/Base.t.sol"; -import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; /// @dev BTT spec: test/trees/ERC20.tree contract ERC20TransferFacet_Base_Test is Base_Test { - using ERC20StorageUtils for address; - ERC20TransferFacet internal facet; function setUp() public virtual override { diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol new file mode 100644 index 00000000..6255f68e --- /dev/null +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; + +import {ERC20TransferFacet_Base_Test} from "../ERC20TransferFacetBase.t.sol"; +import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; + +/// @dev BTT spec: test/trees/ERC20.tree +contract Transfer_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_Test { + using ERC20StorageUtils for address; + + function testFuzz_ShouldRevert_ReceiverIsZeroAddress(uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20TransferFacet.ERC20InvalidReceiver.selector, ADDRESS_ZERO)); + facet.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); + + address(facet).mint(users.alice, balance); + + vm.expectRevert( + abi.encodeWithSelector(ERC20TransferFacet.ERC20InsufficientBalance.selector, users.alice, balance, value) + ); + facet.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); + + address(facet).mint(users.alice, balance); + + uint256 beforeBalanceOfAlice = facet.balanceOf(users.alice); + uint256 beforeBalanceOfTo = facet.balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC20TransferFacet.Transfer(users.alice, to, value); + bool result = facet.transfer(to, value); + + assertEq(result, true, "transfer failed"); + assertEq(facet.balanceOf(users.alice), beforeBalanceOfAlice - value, "balanceOf(users.alice)"); + assertEq(facet.balanceOf(to), beforeBalanceOfTo + value, "balanceOf(to)"); + } +} From b42418f48f8d606a0652f8544b1acdc5be8fea60 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 21:20:06 +0100 Subject: [PATCH 07/10] test: add ERDC20TransferFacet transferFrom unit tests --- .../transfer-facet/fuzz/transferFrom.t.sol | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol new file mode 100644 index 00000000..cc0b67e4 --- /dev/null +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC20TransferFacet_Base_Test} from "../ERC20TransferFacetBase.t.sol"; +import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; + +import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; + +/// @dev BTT spec: test/trees/ERC20.tree +contract TransferFrom_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_Test { + using ERC20StorageUtils for address; + + function testFuzz_ShouldRevert_SenderIsZeroAddress(address to, uint256 value) external { + vm.expectRevert(abi.encodeWithSelector(ERC20TransferFacet.ERC20InvalidSender.selector, ADDRESS_ZERO)); + facet.transferFrom(ADDRESS_ZERO, to, value); + } + + function testFuzz_ShouldRevert_ReceiverIsZeroAddress(address from, uint256 value) + external + whenSenderNotZeroAddress + { + vm.assume(from != ADDRESS_ZERO); + + vm.expectRevert(abi.encodeWithSelector(ERC20TransferFacet.ERC20InvalidReceiver.selector, ADDRESS_ZERO)); + facet.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); + facet.approve(users.sender, allowance); + setMsgSender(users.sender); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20TransferFacet.ERC20InsufficientAllowance.selector, users.sender, allowance, value + ) + ); + facet.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 + + address(facet).mint(from, balance); + + setMsgSender(from); + facet.approve(users.sender, allowance); + setMsgSender(users.sender); + + vm.expectRevert( + abi.encodeWithSelector(ERC20TransferFacet.ERC20InsufficientBalance.selector, from, balance, value) + ); + facet.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); + + address(facet).mint(from, balance); + + setMsgSender(from); + facet.approve(users.sender, MAX_UINT256); + setMsgSender(users.sender); + + uint256 beforeBalanceOfFrom = facet.balanceOf(from); + uint256 beforeBalanceOfTo = facet.balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC20TransferFacet.Transfer(from, to, value); + bool result = facet.transferFrom(from, to, value); + + assertEq(result, true, "transfer failed"); + assertEq(facet.balanceOf(from), beforeBalanceOfFrom - value, "balanceOf(from)"); + assertEq(facet.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); + + address(facet).mint(from, balance); + + setMsgSender(from); + facet.approve(users.sender, allowance); + setMsgSender(users.sender); + + uint256 beforeBalanceOfFrom = facet.balanceOf(from); + uint256 beforeBalanceOfTo = facet.balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC20TransferFacet.Transfer(from, to, value); + bool result = facet.transferFrom(from, to, value); + + assertEq(result, true, "transfer failed"); + assertEq(facet.balanceOf(from), beforeBalanceOfFrom - value, "balanceOf(from)"); + assertEq(facet.balanceOf(to), beforeBalanceOfTo + value, "balanceOf(to)"); + assertEq(facet.allowance(from, users.sender), allowance - value, "allowance(from, users.sender)"); + } +} From 878a2a4f93cc96095e7ae2668f573df03a9e4b48 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 21:30:14 +0100 Subject: [PATCH 08/10] test: cover zero-amount branches for ERC20TransferFacet transfer paths --- .../ERC20/transfer-facet/fuzz/transfer.t.sol | 27 +++++++++++- .../transfer-facet/fuzz/transferFrom.t.sol | 41 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol index 6255f68e..b1092452 100644 --- a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol @@ -5,11 +5,11 @@ pragma solidity >=0.8.30; * https://compose.diamonds */ -import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; - import {ERC20TransferFacet_Base_Test} from "../ERC20TransferFacetBase.t.sol"; import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; +import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; + /// @dev BTT spec: test/trees/ERC20.tree contract Transfer_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_Test { using ERC20StorageUtils for address; @@ -35,6 +35,29 @@ contract Transfer_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_T facet.transfer(to, value); } + function testFuzz_ShouldReturnTrue_AmountIsZero(address to, uint256 senderBalance, uint256 receiverBalance) + external + whenReceiverNotZeroAddress + givenWhenSenderBalanceGETransferAmount + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != users.alice); + + senderBalance = bound(senderBalance, 0, MAX_UINT256 / 2); + receiverBalance = bound(receiverBalance, 0, MAX_UINT256 / 2); + + address(facet).mint(users.alice, senderBalance); + address(facet).mint(to, receiverBalance); + + vm.expectEmit(address(facet)); + emit ERC20TransferFacet.Transfer(users.alice, to, 0); + bool result = facet.transfer(to, 0); + + assertEq(result, true, "transfer failed"); + assertEq(facet.balanceOf(users.alice), senderBalance, "balanceOf(users.alice)"); + assertEq(facet.balanceOf(to), receiverBalance, "balanceOf(to)"); + } + function testFuzz_Transfer(address to, uint256 balance, uint256 value) external whenReceiverNotZeroAddress diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol index cc0b67e4..354a4abb 100644 --- a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol @@ -77,6 +77,47 @@ contract TransferFrom_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Ba facet.transferFrom(from, to, value); } + function testFuzz_ShouldReturnTrue_AmountIsZero( + address from, + address to, + uint256 allowance, + uint256 fromBalance, + uint256 toBalance + ) + external + whenSenderNotZeroAddress + whenReceiverNotZeroAddress + givenWhenSpenderAllowanceGETransferAmount + givenWhenSenderBalanceGETransferAmount + { + vm.assume(from != ADDRESS_ZERO); + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != from); + vm.assume(users.sender != from); + + allowance = bound(allowance, 0, MAX_UINT256); + fromBalance = bound(fromBalance, 0, MAX_UINT256 / 2); + toBalance = bound(toBalance, 0, MAX_UINT256 / 2); + + address(facet).mint(from, fromBalance); + address(facet).mint(to, toBalance); + + setMsgSender(from); + facet.approve(users.sender, allowance); + setMsgSender(users.sender); + + uint256 beforeAllowance = facet.allowance(from, users.sender); + + vm.expectEmit(address(facet)); + emit ERC20TransferFacet.Transfer(from, to, 0); + bool result = facet.transferFrom(from, to, 0); + + assertEq(result, true, "transferFrom failed"); + assertEq(facet.balanceOf(from), fromBalance, "balanceOf(from)"); + assertEq(facet.balanceOf(to), toBalance, "balanceOf(to)"); + assertEq(facet.allowance(from, users.sender), beforeAllowance, "allowance should be unchanged"); + } + function testFuzz_TransferFrom_InfiniteApproval(address from, address to, uint256 value, uint256 balance) external whenSenderNotZeroAddress From 9fbbf132e1a56adeb5eb7895ab5fa31c2f4267db Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 21:37:30 +0100 Subject: [PATCH 09/10] chore: add missing natspec --- src/token/ERC20/ERC20/ERC20Mod.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/token/ERC20/ERC20/ERC20Mod.sol b/src/token/ERC20/ERC20/ERC20Mod.sol index f8ce5335..dd6e4e10 100644 --- a/src/token/ERC20/ERC20/ERC20Mod.sol +++ b/src/token/ERC20/ERC20/ERC20Mod.sol @@ -132,6 +132,7 @@ function burn(address _account, uint256 _value) { * @param _from The address to send tokens from. * @param _to The address to send tokens to. * @param _value The number of tokens to transfer. + * @return True if the transfer was successful. */ function transferFrom(address _from, address _to, uint256 _value) returns (bool) { ERC20TransferStorage storage s = getStorage(); @@ -165,6 +166,7 @@ function transferFrom(address _from, address _to, uint256 _value) returns (bool) * @dev Updates balances directly without allowance mechanism. * @param _to The address to send tokens to. * @param _value The number of tokens to transfer. + * @return True if the transfer was successful. */ function transfer(address _to, uint256 _value) returns (bool) { ERC20TransferStorage storage s = getStorage(); @@ -188,6 +190,7 @@ function transfer(address _to, uint256 _value) returns (bool) { * @dev Sets the allowance for the spender. * @param _spender The address to approve for spending. * @param _value The amount of tokens to approve. + * @return True if the approval was successful. */ function approve(address _spender, uint256 _value) returns (bool) { if (_spender == address(0)) { From 78df956afb13000c89f8e88c035051a281b35c53 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 6 Jan 2026 21:41:16 +0100 Subject: [PATCH 10/10] chore: fix solidity comment style and clarify test scaffolding --- test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol | 4 +++- test/unit/token/ERC20/ERC20/mod/fuzz/burn.t.sol | 4 +++- test/unit/token/ERC20/ERC20/mod/fuzz/mint.t.sol | 4 +++- test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol | 4 +++- test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol | 4 +++- .../ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol | 1 - test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol | 4 +++- .../unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol | 4 +++- .../token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol | 4 +++- 9 files changed, 24 insertions(+), 9 deletions(-) diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol index 0e05423d..582b414a 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/approve.t.sol @@ -11,7 +11,9 @@ import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; import "src/token/ERC20/ERC20/ERC20Mod.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract Approve_ERC20Mod_Fuzz_Unit_Test is Base_Test { ERC20Harness internal harness; diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/burn.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/burn.t.sol index 589d4ca2..42b5e9d2 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/burn.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/burn.t.sol @@ -11,7 +11,9 @@ import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; import "src/token/ERC20/ERC20/ERC20Mod.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract Burn_ERC20Mod_Fuzz_Unit_Test is Base_Test { ERC20Harness internal harness; diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/mint.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/mint.t.sol index bb625081..4d58f9d4 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/mint.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/mint.t.sol @@ -11,7 +11,9 @@ import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; import "src/token/ERC20/ERC20/ERC20Mod.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract Mint_ERC20Mod_Fuzz_Unit_Test is Base_Test { ERC20Harness internal harness; diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol index dc59780e..1c111a3f 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/transfer.t.sol @@ -11,7 +11,9 @@ import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; import "src/token/ERC20/ERC20/ERC20Mod.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract Transfer_ERC20Mod_Fuzz_Unit_Test is Base_Test { ERC20Harness internal harness; diff --git a/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol b/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol index 794b563b..167a543c 100644 --- a/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol +++ b/test/unit/token/ERC20/ERC20/mod/fuzz/transferFrom.t.sol @@ -11,7 +11,9 @@ import {ERC20Harness} from "test/harnesses/token/ERC20/ERC20/ERC20Harness.sol"; import "src/token/ERC20/ERC20/ERC20Mod.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract TransferFrom_ERC20Mod_Fuzz_Unit_Test is Base_Test { ERC20Harness internal harness; diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol index 2c976a9f..1e02d64c 100644 --- a/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol +++ b/test/unit/token/ERC20/ERC20/transfer-facet/ERC20TransferFacetBase.t.sol @@ -8,7 +8,6 @@ pragma solidity >=0.8.30 <0.9.0; import {Base_Test} from "test/Base.t.sol"; import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; -/// @dev BTT spec: test/trees/ERC20.tree contract ERC20TransferFacet_Base_Test is Base_Test { ERC20TransferFacet internal facet; diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol index b4a59f4e..a0e7b7f1 100644 --- a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/approve.t.sol @@ -10,7 +10,9 @@ import {ERC20TransferFacet_Base_Test} from "../ERC20TransferFacetBase.t.sol"; import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract Approve_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_Test { function testFuzz_ShouldRevert_SpenderIsZeroAddress(uint256 value) external { vm.expectRevert(abi.encodeWithSelector(ERC20TransferFacet.ERC20InvalidSpender.selector, ADDRESS_ZERO)); diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol index b1092452..fb5f58da 100644 --- a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transfer.t.sol @@ -10,7 +10,9 @@ import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract Transfer_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_Test { using ERC20StorageUtils for address; diff --git a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol index 354a4abb..07fcc5d9 100644 --- a/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol +++ b/test/unit/token/ERC20/ERC20/transfer-facet/fuzz/transferFrom.t.sol @@ -10,7 +10,9 @@ import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; import {ERC20TransferFacet} from "src/token/ERC20/ERC20/ERC20TransferFacet.sol"; -/// @dev BTT spec: test/trees/ERC20.tree +/** + * @dev BTT spec: test/trees/ERC20.tree + */ contract TransferFrom_ERC20TransferFacet_Fuzz_Unit_Test is ERC20TransferFacet_Base_Test { using ERC20StorageUtils for address;