From c3e0d54be44a3ff1ae23efb0ceac76d7df5487e6 Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Sat, 17 May 2025 16:07:39 +0300 Subject: [PATCH] additional tests added and extra mocks added Signed-off-by: Uncle Fatso --- src/mock/SigUtils.sol | 57 +++++++ test/UniswapV2ERC20.t.sol | 79 ++++++++++ test/UniswapV2Factory.t.sol | 13 ++ test/UniswapV2Pair.t.sol | 1 - test/UniswapV2Router.t.sol | 69 ++++++++ test/tokens/Allowance.t.sol | 80 ++++++++++ test/tokens/Permit.t.sol | 306 ++++++++++++++++++++++++++++++++++++ test/tokens/Transfer.t.sol | 78 +++++++++ 8 files changed, 682 insertions(+), 1 deletion(-) create mode 100644 src/mock/SigUtils.sol create mode 100644 test/UniswapV2ERC20.t.sol create mode 100644 test/tokens/Allowance.t.sol create mode 100644 test/tokens/Permit.t.sol create mode 100644 test/tokens/Transfer.t.sol diff --git a/src/mock/SigUtils.sol b/src/mock/SigUtils.sol new file mode 100644 index 0000000..7569a8c --- /dev/null +++ b/src/mock/SigUtils.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.20; + +contract SigUtils { + bytes32 internal DOMAIN_SEPARATOR; + + constructor(bytes32 _DOMAIN_SEPARATOR) { + DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR; + } + + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + struct Permit { + address owner; + address spender; + uint256 value; + uint256 nonce; + uint256 deadline; + } + + // computes the hash of a permit + function getStructHash(Permit memory _permit) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encode( + PERMIT_TYPEHASH, + _permit.owner, + _permit.spender, + _permit.value, + _permit.nonce, + _permit.deadline + ) + ); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function getTypedDataHash(Permit memory _permit) + public + view + returns (bytes32) + { + return + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + getStructHash(_permit) + ) + ); + } +} diff --git a/test/UniswapV2ERC20.t.sol b/test/UniswapV2ERC20.t.sol new file mode 100644 index 0000000..0cfb336 --- /dev/null +++ b/test/UniswapV2ERC20.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import "../src/UniswapV2ERC20.sol"; + +import "./tokens/Permit.t.sol"; +import "./tokens/Allowance.t.sol"; +import "./tokens/Transfer.t.sol"; + +contract UniswapV2ERC20Mintable is UniswapV2ERC20 { + function mint(address to, uint256 value) public { + _mint(to, value); + } +} + +contract UniswapV2ERC20Test is Test, ERC20PermitTest, ERC20AllowanceTest, ERC20TransferTest { + UniswapV2ERC20Mintable uni; + + address constant alice = 0x0000000000000000000000000000000000000001; + address constant bob = 0x0000000000000000000000000000000000000002; + uint256 constant amount = 1e23; + uint256 constant maxAmount = type(uint256).max; + + function setUp() public { + uni = new UniswapV2ERC20Mintable(); + initializePermit(address(uni), amount, maxAmount); + initializeAllowance(alice, bob, address(uni), amount, maxAmount, amount); + initializeTransfer(alice, bob, address(uni), amount, 0); + } + + function testNameIsCorrect() public view { + assertEq(uni.name(), "Uniswap V2"); + } + + function testSymbolIsCorrect() public view { + assertEq(uni.symbol(), "UNI-V2"); + } + + function testNumberOfDecimalslIsCorrect() public view { + assertEq(uni.decimals(), 18); + } + + function testInitialSupplyIsZero() public view { + assertEq(uni.totalSupply(), 0); + } + + function testDomainSeparatorIsCorrect() public view { + assertEq(uni.DOMAIN_SEPARATOR(), keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(uni.name())), + keccak256(bytes("1")), + block.chainid, + address(uni) + ) + )); + } + + function testPermitTypeHashIsCorrect() public view { + assertEq( + uni.PERMIT_TYPEHASH(), + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + ); + } + + function _mintTransferTokens(address who, uint256 value) internal override { + uni.mint(who, value); + } + + function _mintAllowanceTokens(address who, uint256 value) internal override { + uni.mint(who, value); + } + + function _mintPermitTokens(address who, uint256 value) internal override { + uni.mint(who, value); + } +} diff --git a/test/UniswapV2Factory.t.sol b/test/UniswapV2Factory.t.sol index 2c4ac07..612cf43 100644 --- a/test/UniswapV2Factory.t.sol +++ b/test/UniswapV2Factory.t.sol @@ -38,6 +38,19 @@ contract UniswapV2FactoryTest is Test { assertEq(UniswapV2Pair(tokenPair).token1(), _token1); } + function testCreatePairReverse() public { + address tokenPair = factory.createPair(address(token1), address(token0)); + (address _token0, address _token1) = UniswapV2Library.sortTokens( + address(token0), + address(token1) + ); + + assertEq(factory.allPairsLength(), 1); + assertEq(factory.getPair(address(token0), address(token1)), tokenPair); + assertEq(UniswapV2Pair(tokenPair).token0(), _token0); + assertEq(UniswapV2Pair(tokenPair).token1(), _token1); + } + function testCreatePairMultipleTokens() public { address tokenPair0 = factory.createPair(address(token0), address(token1)); address tokenPair1 = factory.createPair(address(token2), address(token3)); diff --git a/test/UniswapV2Pair.t.sol b/test/UniswapV2Pair.t.sol index bb373a8..c12d3b9 100644 --- a/test/UniswapV2Pair.t.sol +++ b/test/UniswapV2Pair.t.sol @@ -45,7 +45,6 @@ contract TestUniswapV2Pair is Test { factory = new UniswapV2Factory(address(0)); address tokenAddress = factory.createPair(address(token0), address(token1)); pair = UniswapV2Pair(tokenAddress); - // pair.initialize(address(token0), address(token1)); token0.mint(address(this), 10 ether); token1.mint(address(this), 10 ether); diff --git a/test/UniswapV2Router.t.sol b/test/UniswapV2Router.t.sol index 6a6ff46..2770014 100644 --- a/test/UniswapV2Router.t.sol +++ b/test/UniswapV2Router.t.sol @@ -22,6 +22,9 @@ contract TestUniswapV2Router is Test { WETH9 public weth; + fallback() external payable {} + receive() external payable {} + function setUp() public { weth = new WETH9(); factory = new UniswapV2Factory(address(0)); @@ -34,6 +37,34 @@ contract TestUniswapV2Router is Test { token1.mint(address(this), 10 ether); } + function testAddLiquidityEthPairFor() public { + token0.approve(address(router), 10 ether); + + (address _token0, address _token1) = UniswapV2Library.sortTokens( + address(token0), + address(weth) + ); + address pair = UniswapV2Library.pairFor( + address(factory), + _token0, + _token1 + ); + + (, , uint256 liquidity) = router.addLiquidityETH{value: 1 ether}( + address(token0), + 1 ether, + 1 ether, + 1 ether, + address(this), + block.timestamp + 1 + ); + + assertEq(liquidity, 1 ether - UniswapV2Pair(pair).MINIMUM_LIQUIDITY()); + assertEq(factory.getPair(address(token0), address(weth)), pair); + assertEq(weth.balanceOf(address(pair)), 1 ether); + assertEq(token0.balanceOf(address(pair)), 1 ether); + } + function testAddLiquidityPairFor() public { token0.approve(address(router), 10 ether); token1.approve(address(router), 10 ether); @@ -252,6 +283,44 @@ contract TestUniswapV2Router is Test { ); } + function testRemoveLiquidityEth() public { + token0.approve(address(router), 10 ether); + weth.approve(address(router), 10 ether); + + (uint256 amount0, uint256 amount1, uint256 liquidity) = router.addLiquidityETH{value: 1 ether}( + address(token0), + 1 ether, + 1 ether, + 1 ether, + address(this), + block.timestamp + 1 + ); + + uint256 prevAmount0 = token0.balanceOf(address(this)); + uint256 prevAmount1 = address(this).balance; + + address pair = factory.getPair(address(token0), address(weth)); + assertEq(IERC20(pair).balanceOf(address(this)), liquidity); + IERC20(pair).approve(address(router), liquidity); + + router.removeLiquidityETH( + address(token0), + liquidity, + 0, + 0, + address(this), + block.timestamp + 1 + ); + UniswapV2Pair(pair).skim(address(this)); + + assertEq(IERC20(pair).balanceOf(address(this)), 0); + assertEq(token0.balanceOf(address(this)), prevAmount0 + amount0 - UniswapV2Pair(pair).MINIMUM_LIQUIDITY()); + assertEq(address(this).balance, prevAmount1 + amount1 - UniswapV2Pair(pair).MINIMUM_LIQUIDITY()); + + assertEq(token0.balanceOf(pair), UniswapV2Pair(pair).MINIMUM_LIQUIDITY()); + assertEq(weth.balanceOf(address(pair)), UniswapV2Pair(pair).MINIMUM_LIQUIDITY()); + } + function testRemoveLiquidity() public { token0.approve(address(router), 1 ether); token1.approve(address(router), 1 ether); diff --git a/test/tokens/Allowance.t.sol b/test/tokens/Allowance.t.sol new file mode 100644 index 0000000..32d39a9 --- /dev/null +++ b/test/tokens/Allowance.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.20; + +import {Test} from "forge-std/Test.sol"; +import "@openzeppelin-contracts/token/ERC20/ERC20.sol"; + +abstract contract ERC20AllowanceTest is Test { + ERC20 tokenAllowance; + uint256 amountAllowance; + uint256 maxAmountAllowance; + uint256 maxRealAmountAllowance; + + address aliceAllowance; + address bobAllowance; + + function initializeAllowance( + address alice, + address bob, + address token, + uint256 amount, + uint256 maxAmount, + uint256 maxRealAmount + ) public { + tokenAllowance = ERC20(token); + amountAllowance = amount; + maxAmountAllowance = maxAmount; + maxRealAmountAllowance = maxRealAmount; + aliceAllowance = alice; + bobAllowance = bob; + } + + function test_allowance_couldApproveFunds() public { + _mintAllowanceTokens(aliceAllowance, amountAllowance); + assertEq(tokenAllowance.allowance(aliceAllowance, bobAllowance), 0); + vm.prank(aliceAllowance); + tokenAllowance.approve(bobAllowance, amountAllowance); + assertEq(tokenAllowance.allowance(aliceAllowance, bobAllowance), amountAllowance); + } + + function test_allowance_transferFromDecreaseAllowance() public { + _mintAllowanceTokens(aliceAllowance, amountAllowance); + vm.prank(aliceAllowance); + tokenAllowance.approve(bobAllowance, amountAllowance); + assertEq(tokenAllowance.balanceOf(aliceAllowance), amountAllowance); + assertEq(tokenAllowance.balanceOf(bobAllowance), 0); + assertEq(tokenAllowance.allowance(aliceAllowance, bobAllowance), amountAllowance); + vm.prank(bobAllowance); + bool success = tokenAllowance.transferFrom(aliceAllowance, bobAllowance, amountAllowance); + assertEq(success, true); + assertEq(tokenAllowance.balanceOf(aliceAllowance), 0); + assertEq(tokenAllowance.balanceOf(bobAllowance), amountAllowance); + assertEq(tokenAllowance.allowance(aliceAllowance, bobAllowance), 0); + } + + function test_allowance_couldNotTransferFromIfNotEnoughFunds() public { + _mintAllowanceTokens(aliceAllowance, amountAllowance); + vm.prank(aliceAllowance); + tokenAllowance.approve(bobAllowance, maxAmountAllowance); + assertEq(tokenAllowance.balanceOf(aliceAllowance), amountAllowance); + assertEq(tokenAllowance.balanceOf(bobAllowance), 0); + assertEq(tokenAllowance.allowance(aliceAllowance, bobAllowance), maxAmountAllowance); + vm.expectRevert(); + vm.prank(bobAllowance); + tokenAllowance.transferFrom(aliceAllowance, bobAllowance, maxAmountAllowance); + } + + function test_allowance_couldNotTransferFromIfNotEnoughAllowance() public { + _mintAllowanceTokens(aliceAllowance, maxRealAmountAllowance); + vm.prank(aliceAllowance); + tokenAllowance.approve(bobAllowance, amountAllowance); + assertEq(tokenAllowance.balanceOf(aliceAllowance), maxRealAmountAllowance); + assertEq(tokenAllowance.balanceOf(bobAllowance), 0); + assertEq(tokenAllowance.allowance(aliceAllowance, bobAllowance), amountAllowance); + vm.expectRevert(); + vm.prank(bobAllowance); + tokenAllowance.transferFrom(aliceAllowance, bobAllowance, maxAmountAllowance); + } + + function _mintAllowanceTokens(address who, uint256 value) internal virtual; +} diff --git a/test/tokens/Permit.t.sol b/test/tokens/Permit.t.sol new file mode 100644 index 0000000..f2aca3f --- /dev/null +++ b/test/tokens/Permit.t.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.20; + +import {Test} from "forge-std/Test.sol"; +import "../../src/mock/SigUtils.sol"; +import "@openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol"; + +abstract contract ERC20PermitTest is Test { + SigUtils sigUtils; + ERC20Permit permitToken; + + address owner; + address spender; + uint256 permitAmount; + uint256 maxPermitAmount; + + uint256 constant ownerPrivateKey = 0xA11CE; + uint256 constant spenderPrivateKey = 0xB0B; + + function initializePermit( + address token, + uint256 amount, + uint256 maxAmount + ) public { + permitAmount = amount; + maxPermitAmount = maxAmount; + permitToken = ERC20Permit(token); + sigUtils = new SigUtils(permitToken.DOMAIN_SEPARATOR()); + + owner = vm.addr(ownerPrivateKey); + spender = vm.addr(spenderPrivateKey); + } + + function test_permit_initialNonceIsZero() public view { + assertEq(permitToken.nonces(owner), 0); + assertEq(permitToken.nonces(spender), 0); + } + + function test_permit_acceptsOwnerSignature() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: permitAmount, + nonce: 0, + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + + assertEq(permitToken.allowance(owner, spender), permitAmount); + assertEq(permitToken.nonces(owner), 1); + } + + function test_permit_expiredPermit() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: permitAmount, + nonce: permitToken.nonces(owner), + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + vm.warp(1 days + 1 seconds); // fast forward one second past the deadline + + vm.expectRevert(); + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + } + + function test_permit_invalidSigner() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: permitAmount, + nonce: permitToken.nonces(owner), + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(spenderPrivateKey, digest); // spender signs owner's approval + + vm.expectRevert(); + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + } + + function test_permit_invalidNonce() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: permitAmount, + nonce: 1, // owner nonce stored on-chain is 0 + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + vm.expectRevert(); + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + } + + function test_permit_signatureReplay() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: permitAmount, + nonce: 0, + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + + vm.expectRevert(); + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + } + + function test_permit_transferFromLimitedPermit() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: permitAmount, + nonce: 0, + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + + vm.prank(spender); + permitToken.transferFrom(owner, spender, permitAmount); + + assertEq(permitToken.balanceOf(owner), 0); + assertEq(permitToken.balanceOf(spender), permitAmount); + assertEq(permitToken.allowance(owner, spender), 0); + } + + function test_permit_transferFromMaxPermit() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: maxPermitAmount, + nonce: 0, + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + + vm.prank(spender); + permitToken.transferFrom(owner, spender, permitAmount); + + assertEq(permitToken.balanceOf(owner), 0); + assertEq(permitToken.balanceOf(spender), permitAmount); + assertEq(permitToken.allowance(owner, spender), maxPermitAmount); + } + + function test_permit_invalidAllowance() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: 1, + nonce: 0, + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + + vm.expectRevert(); + vm.prank(spender); + permitToken.transferFrom(owner, spender, permitAmount); + } + + function test_permit_ivnalidBalance() public { + _mintPermitTokens(owner, permitAmount); + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: owner, + spender: spender, + value: 2, + nonce: 0, + deadline: 1 days + }); + + bytes32 digest = sigUtils.getTypedDataHash(permit); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + permitToken.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + v, + r, + s + ); + + vm.expectRevert(); + vm.prank(spender); + permitToken.transferFrom(owner, spender, permitAmount); + } + + function _mintPermitTokens(address who, uint256 permitAmount) internal virtual; +} diff --git a/test/tokens/Transfer.t.sol b/test/tokens/Transfer.t.sol new file mode 100644 index 0000000..995fa1e --- /dev/null +++ b/test/tokens/Transfer.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.20; + +import {Test} from "forge-std/Test.sol"; +import "@openzeppelin-contracts/token/ERC20/ERC20.sol"; + +abstract contract ERC20TransferTest is Test { + ERC20 tokenTransfer; + uint256 amountTransfer; + uint256 totalSupplyTransfer; + + address aliceTransfer; + address bobTransfer; + + function initializeTransfer( + address alice, + address bob, + address token, + uint256 amount, + uint256 totalSupply + ) public { + tokenTransfer = ERC20(token); + amountTransfer = amount; + totalSupplyTransfer = totalSupply; + aliceTransfer = alice; + bobTransfer = bob; + } + + function test_transfer_tokenTransfers() public { + _mintTransferTokens(aliceTransfer, amountTransfer); + assertEq(tokenTransfer.balanceOf(aliceTransfer), amountTransfer); + assertEq(tokenTransfer.balanceOf(bobTransfer), 0); + vm.prank(aliceTransfer); + bool success = tokenTransfer.transfer(bobTransfer, amountTransfer); + assertEq(success, true); + assertEq(tokenTransfer.balanceOf(bobTransfer), amountTransfer); + assertEq(tokenTransfer.balanceOf(aliceTransfer), 0); + } + + function test_transfer_transferFuzzing(uint64 fuzzingTransferAmount) public { + if (totalSupplyTransfer == 0) vm.assume(fuzzingTransferAmount > 0); + else vm.assume(fuzzingTransferAmount > 0 && fuzzingTransferAmount < totalSupplyTransfer); + _mintTransferTokens(aliceTransfer, fuzzingTransferAmount); + assertEq(tokenTransfer.balanceOf(aliceTransfer), fuzzingTransferAmount); + assertEq(tokenTransfer.balanceOf(bobTransfer), 0); + vm.prank(aliceTransfer); + bool success = tokenTransfer.transfer(bobTransfer, fuzzingTransferAmount); + assertEq(success, true); + assertEq(tokenTransfer.balanceOf(bobTransfer), fuzzingTransferAmount); + assertEq(tokenTransfer.balanceOf(aliceTransfer), 0); + } + + function test_transfer_couldNotTransferMoreThanAvailable() public { + _mintTransferTokens(aliceTransfer, amountTransfer); + vm.expectRevert(); + vm.prank(aliceTransfer); + tokenTransfer.transfer(bobTransfer, type(uint256).max); + } + + function test_transfer_doesNotChangeTotalSupply() public { + assertEq(tokenTransfer.totalSupply(), totalSupplyTransfer); + _mintTransferTokens(aliceTransfer, amountTransfer); + if (totalSupplyTransfer == 0) { + assertEq(tokenTransfer.totalSupply(), amountTransfer); + vm.prank(aliceTransfer); + tokenTransfer.transfer(bobTransfer, amountTransfer); + assertEq(tokenTransfer.totalSupply(), amountTransfer); + } else { + assertEq(tokenTransfer.totalSupply(), totalSupplyTransfer); + vm.prank(aliceTransfer); + tokenTransfer.transfer(bobTransfer, amountTransfer); + assertEq(tokenTransfer.totalSupply(), totalSupplyTransfer); + } + + } + + function _mintTransferTokens(address who, uint256 value) internal virtual; +}