From 66a8687251b28dc3bb48bfba4df00d9bb1a8b74d Mon Sep 17 00:00:00 2001 From: Eugenio Date: Tue, 12 Dec 2023 09:32:38 -0300 Subject: [PATCH 1/2] Vanilla ERC20 without EIP-2612 (PERMIT) This "Vanilla" ERC20 contract version exclude the EIP-2612 (PERMIT) feature, thereby reducing the contract's byte size and lowering deployment costs. --- src/test/ERC20Vainilla.t.sol | 300 +++++++++++++++++++++ src/test/utils/mocks/MockERC20Vainilla.sol | 20 ++ src/tokens/ERC20Vainilla.sol | 126 +++++++++ 3 files changed, 446 insertions(+) create mode 100644 src/test/ERC20Vainilla.t.sol create mode 100644 src/test/utils/mocks/MockERC20Vainilla.sol create mode 100644 src/tokens/ERC20Vainilla.sol diff --git a/src/test/ERC20Vainilla.t.sol b/src/test/ERC20Vainilla.t.sol new file mode 100644 index 00000000..771c4535 --- /dev/null +++ b/src/test/ERC20Vainilla.t.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.15; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {DSInvariantTest} from "./utils/DSInvariantTest.sol"; + +import {MockERC20Vainilla} from "./utils/mocks/MockERC20Vainilla.sol"; + +contract ERC20VainillaTest is DSTestPlus { + MockERC20Vainilla token; + + function setUp() public { + token = new MockERC20Vainilla("Token", "TKN", 18); + } + + function invariantMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + assertEq(token.decimals(), 18); + } + + function testMint() public { + token.mint(address(0xBEEF), 1e18); + + assertEq(token.totalSupply(), 1e18); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1e18); + token.burn(address(0xBEEF), 0.9e18); + + assertEq(token.totalSupply(), 1e18 - 0.9e18); + assertEq(token.balanceOf(address(0xBEEF)), 0.1e18); + } + + function testApprove() public { + assertTrue(token.approve(address(0xBEEF), 1e18)); + + assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); + } + + function testTransfer() public { + token.mint(address(this), 1e18); + + assertTrue(token.transfer(address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + hevm.prank(from); + token.approve(address(this), 1e18); + + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), 0); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testInfiniteApproveTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + hevm.prank(from); + token.approve(address(this), type(uint256).max); + + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), type(uint256).max); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testFailTransferInsufficientBalance() public { + token.mint(address(this), 0.9e18); + token.transfer(address(0xBEEF), 1e18); + } + + function testFailTransferFromInsufficientAllowance() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + hevm.prank(from); + token.approve(address(this), 0.9e18); + + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testFailTransferFromInsufficientBalance() public { + address from = address(0xABCD); + + token.mint(from, 0.9e18); + + hevm.prank(from); + token.approve(address(this), 1e18); + + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testMetadata( + string calldata name, + string calldata symbol, + uint8 decimals + ) public { + MockERC20Vainilla tkn = new MockERC20Vainilla(name, symbol, decimals); + assertEq(tkn.name(), name); + assertEq(tkn.symbol(), symbol); + assertEq(tkn.decimals(), decimals); + } + + function testMint(address from, uint256 amount) public { + token.mint(from, amount); + + assertEq(token.totalSupply(), amount); + assertEq(token.balanceOf(from), amount); + } + + function testBurn( + address from, + uint256 mintAmount, + uint256 burnAmount + ) public { + burnAmount = bound(burnAmount, 0, mintAmount); + + token.mint(from, mintAmount); + token.burn(from, burnAmount); + + assertEq(token.totalSupply(), mintAmount - burnAmount); + assertEq(token.balanceOf(from), mintAmount - burnAmount); + } + + function testApprove(address to, uint256 amount) public { + assertTrue(token.approve(to, amount)); + + assertEq(token.allowance(address(this), to), amount); + } + + function testTransfer(address from, uint256 amount) public { + token.mint(address(this), amount); + + assertTrue(token.transfer(from, amount)); + assertEq(token.totalSupply(), amount); + + if (address(this) == from) { + assertEq(token.balanceOf(address(this)), amount); + } else { + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(from), amount); + } + } + + function testTransferFrom( + address to, + uint256 approval, + uint256 amount + ) public { + amount = bound(amount, 0, approval); + + address from = address(0xABCD); + + token.mint(from, amount); + + hevm.prank(from); + token.approve(address(this), approval); + + assertTrue(token.transferFrom(from, to, amount)); + assertEq(token.totalSupply(), amount); + + uint256 app = from == address(this) || approval == type(uint256).max ? approval : approval - amount; + assertEq(token.allowance(from, address(this)), app); + + if (from == to) { + assertEq(token.balanceOf(from), amount); + } else { + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testFailBurnInsufficientBalance( + address to, + uint256 mintAmount, + uint256 burnAmount + ) public { + burnAmount = bound(burnAmount, mintAmount + 1, type(uint256).max); + + token.mint(to, mintAmount); + token.burn(to, burnAmount); + } + + function testFailTransferInsufficientBalance( + address to, + uint256 mintAmount, + uint256 sendAmount + ) public { + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(this), mintAmount); + token.transfer(to, sendAmount); + } + + function testFailTransferFromInsufficientAllowance( + address to, + uint256 approval, + uint256 amount + ) public { + amount = bound(amount, approval + 1, type(uint256).max); + + address from = address(0xABCD); + + token.mint(from, amount); + + hevm.prank(from); + token.approve(address(this), approval); + + token.transferFrom(from, to, amount); + } + + function testFailTransferFromInsufficientBalance( + address to, + uint256 mintAmount, + uint256 sendAmount + ) public { + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); + + address from = address(0xABCD); + + token.mint(from, mintAmount); + + hevm.prank(from); + token.approve(address(this), sendAmount); + + token.transferFrom(from, to, sendAmount); + } +} + +contract ERC20VainillaInvariants is DSTestPlus, DSInvariantTest { + VainillaBalanceSum balanceSum; + MockERC20Vainilla token; + + function setUp() public { + token = new MockERC20Vainilla("Token", "TKN", 18); + balanceSum = new VainillaBalanceSum(token); + + addTargetContract(address(balanceSum)); + } + + function invariantBalanceSum() public { + assertEq(token.totalSupply(), balanceSum.sum()); + } +} + +contract VainillaBalanceSum { + MockERC20Vainilla token; + uint256 public sum; + + constructor(MockERC20Vainilla _token) { + token = _token; + } + + function mint(address from, uint256 amount) public { + token.mint(from, amount); + sum += amount; + } + + function burn(address from, uint256 amount) public { + token.burn(from, amount); + sum -= amount; + } + + function approve(address to, uint256 amount) public { + token.approve(to, amount); + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public { + token.transferFrom(from, to, amount); + } + + function transfer(address to, uint256 amount) public { + token.transfer(to, amount); + } +} diff --git a/src/test/utils/mocks/MockERC20Vainilla.sol b/src/test/utils/mocks/MockERC20Vainilla.sol new file mode 100644 index 00000000..62b719ef --- /dev/null +++ b/src/test/utils/mocks/MockERC20Vainilla.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC20Vainilla} from "../../../tokens/ERC20Vainilla.sol"; + +contract MockERC20Vainilla is ERC20Vainilla { + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals + ) ERC20Vainilla(_name, _symbol, _decimals) {} + + function mint(address to, uint256 value) public virtual { + _mint(to, value); + } + + function burn(address from, uint256 value) public virtual { + _burn(from, value); + } +} diff --git a/src/tokens/ERC20Vainilla.sol b/src/tokens/ERC20Vainilla.sol new file mode 100644 index 00000000..03809393 --- /dev/null +++ b/src/tokens/ERC20Vainilla.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Modern and gas efficient ERC20 implementation. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20ERC20Vainilla.sol) +/// @author Modified from (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20ERC20Vainilla.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. +abstract contract ERC20Vainilla { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /*////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + uint8 public immutable decimals; + + /*////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals + ) { + name = _name; + symbol = _symbol; + decimals = _decimals; + } + + /*////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 amount) public virtual returns (bool) { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) public virtual returns (bool) { + balanceOf[msg.sender] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual returns (bool) { + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } +} From 326936a44a232b6b8c9bb626b566555617f28764 Mon Sep 17 00:00:00 2001 From: Eugenio Date: Tue, 12 Dec 2023 09:34:34 -0300 Subject: [PATCH 2/2] run `forge snapshot` --- .gas-snapshot | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.gas-snapshot b/.gas-snapshot index 13e2410a..91ac770e 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -147,6 +147,27 @@ ERC20Test:testTransfer() (gas: 60272) ERC20Test:testTransfer(address,uint256) (runs: 256, μ: 57996, ~: 60484) ERC20Test:testTransferFrom() (gas: 83777) ERC20Test:testTransferFrom(address,uint256,uint256) (runs: 256, μ: 86993, ~: 92841) +ERC20VainillaInvariants:invariantBalanceSum() (runs: 256, calls: 3840, reverts: 2369) +ERC20VainillaTest:invariantMetadata() (runs: 256, calls: 3840, reverts: 2350) +ERC20VainillaTest:testApprove() (gas: 31058) +ERC20VainillaTest:testApprove(address,uint256) (runs: 256, μ: 30020, ~: 31264) +ERC20VainillaTest:testBurn() (gas: 56881) +ERC20VainillaTest:testBurn(address,uint256,uint256) (runs: 256, μ: 57770, ~: 59540) +ERC20VainillaTest:testFailBurnInsufficientBalance(address,uint256,uint256) (runs: 256, μ: 52031, ~: 55432) +ERC20VainillaTest:testFailTransferFromInsufficientAllowance() (gas: 80816) +ERC20VainillaTest:testFailTransferFromInsufficientAllowance(address,uint256,uint256) (runs: 256, μ: 79484, ~: 83355) +ERC20VainillaTest:testFailTransferFromInsufficientBalance() (gas: 81314) +ERC20VainillaTest:testFailTransferFromInsufficientBalance(address,uint256,uint256) (runs: 256, μ: 79287, ~: 83832) +ERC20VainillaTest:testFailTransferInsufficientBalance() (gas: 52718) +ERC20VainillaTest:testFailTransferInsufficientBalance(address,uint256,uint256) (runs: 256, μ: 51855, ~: 55250) +ERC20VainillaTest:testInfiniteApproveTransferFrom() (gas: 89659) +ERC20VainillaTest:testMetadata(string,string,uint8) (runs: 256, μ: 551063, ~: 545180) +ERC20VainillaTest:testMint() (gas: 53635) +ERC20VainillaTest:testMint(address,uint256) (runs: 256, μ: 51331, ~: 53819) +ERC20VainillaTest:testTransfer() (gas: 60205) +ERC20VainillaTest:testTransfer(address,uint256) (runs: 256, μ: 57913, ~: 60401) +ERC20VainillaTest:testTransferFrom() (gas: 83724) +ERC20VainillaTest:testTransferFrom(address,uint256,uint256) (runs: 256, μ: 86938, ~: 92758) ERC4626Test:invariantMetadata() (runs: 256, calls: 3840, reverts: 2883) ERC4626Test:testFailDepositWithNoApproval() (gas: 13369) ERC4626Test:testFailDepositWithNotEnoughApproval() (gas: 87005)