├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── remappings.txt ├── script └── Counter.s.sol ├── src └── CursedStructToken.sol └── test └── CusedStructToken.t.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/* 8 | /broadcast/*/31337/ 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CURSED STRUCT TOKEN 2 | 3 | > pls dont use 4 | 5 | This token contains a struct. But not just any struct. The unholiest of structs. This struct 6 | contains all internal functions of an ERC20 contract. 7 | 8 | Tests were just straight up copy-pasta'd from 9 | [t11s' Solmate](https://github.com/transmissions11/solmate) (shout out btw) because there's not a 10 | chance in hell I write tests for this suffering abomination myself. We thank you for your 11 | contribution. 12 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | forge-std/=lib/forge-std/src/ 2 | -------------------------------------------------------------------------------- /script/Counter.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract CounterScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/CursedStructToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.16; 3 | 4 | // ------------------------------------------------------------------------------------------------- 5 | // STRUCT DEFINITION 6 | 7 | // wArNiNg: nAmInG fUnCtIoN tYpE pArAmEtErS iS dEpReCaTeD. 8 | struct CursedStruct { 9 | function() pure returns (string memory) name; 10 | 11 | function() pure returns (string memory) symbol; 12 | 13 | function() pure returns (uint8) decimals; 14 | 15 | function( 16 | uint256 // slot 17 | ) view returns (uint256) totalSupply; 18 | 19 | function( 20 | mapping(address => uint256) storage, // balances, 21 | address // account 22 | ) view returns (uint256) balanceOf; 23 | 24 | function( 25 | mapping(address => mapping(address => uint256)) storage, // allowances, 26 | address, // owner, 27 | address // spender 28 | ) view returns (uint256) allowance; 29 | 30 | function( 31 | mapping(address => uint256) storage, // balances, 32 | address, // sender, 33 | address, // receiver, 34 | uint256 // amount 35 | ) returns (bool) transfer; 36 | 37 | function( 38 | mapping(address => uint256) storage, // balances, 39 | mapping(address => mapping(address => uint256)) storage, // allowances, 40 | address, // caller, 41 | address, // sender, 42 | address, // receiver, 43 | uint256 // amount 44 | ) returns (bool) transferFrom; 45 | 46 | function( 47 | mapping(address => mapping(address => uint256)) storage, // allowances, 48 | address,// caller, 49 | address, // spender, 50 | uint256 // amount 51 | ) returns (bool) approve; 52 | } 53 | 54 | // ------------------------------------------------------------------------------------------------- 55 | // FUNCTION DEFINITIONS 56 | 57 | function _name() pure returns (string memory) { 58 | return "Cursed Struct Token"; 59 | } 60 | 61 | function _symbol() pure returns (string memory) { 62 | return "CST"; 63 | } 64 | 65 | function _decimals() pure returns (uint8) { 66 | return 18; 67 | } 68 | 69 | function _totalSupply(uint256 slot) view returns (uint256) { 70 | uint256 supply; 71 | assembly { supply := sload(slot) } 72 | return supply; 73 | } 74 | 75 | function _balanceOf( 76 | mapping(address => uint256) storage balances, 77 | address account 78 | ) view returns (uint256) { 79 | return balances[account]; 80 | } 81 | 82 | function _allowance( 83 | mapping(address => mapping(address => uint256)) storage allowances, 84 | address owner, 85 | address spender 86 | ) view returns (uint256) { 87 | return allowances[owner][spender]; 88 | } 89 | 90 | function _transfer( 91 | mapping(address => uint256) storage balances, 92 | address sender, 93 | address receiver, 94 | uint256 amount 95 | ) returns (bool) { 96 | balances[sender] -= amount; 97 | balances[receiver] += amount; 98 | return true; 99 | } 100 | 101 | function _transferFrom( 102 | mapping(address => uint256) storage balances, 103 | mapping(address => mapping(address => uint256)) storage allowances, 104 | address caller, 105 | address sender, 106 | address receiver, 107 | uint256 amount 108 | ) returns (bool) { 109 | uint256 allowed = allowances[sender][caller]; 110 | if (allowed != type(uint256).max) allowances[sender][caller] = allowed - amount; 111 | balances[sender] -= amount; 112 | balances[receiver] += amount; 113 | return true; 114 | } 115 | 116 | function _approve( 117 | mapping(address => mapping(address => uint256)) storage allowances, 118 | address caller, 119 | address spender, 120 | uint256 amount 121 | ) returns (bool) { 122 | allowances[caller][spender] = amount; 123 | return true; 124 | } 125 | 126 | // ------------------------------------------------------------------------------------------------- 127 | // CONTRACT DEFINITION 128 | 129 | contract CursedStructToken { 130 | 131 | /// @notice Logged on transfer. 132 | /// @param sender Sending address. 133 | /// @param receiver Receiving address. 134 | /// @param amount Amount transferred. 135 | event Transfer(address indexed sender, address indexed receiver, uint256 amount); 136 | 137 | /// @notice Logged on approval. 138 | /// @param owner Approving address. 139 | /// @param spender Approved address. 140 | /// @param amount Amount spender can transfer on behalf of owner. 141 | event Approval(address indexed owner, address indexed spender, uint256 amount); 142 | 143 | uint256 _totalSupplyAmount; 144 | 145 | mapping(address => uint256) _balances; 146 | 147 | mapping(address => mapping(address => uint256)) _allowances; 148 | 149 | /// @notice Returns name. 150 | /// @return Name. 151 | function name() public pure returns (string memory) { 152 | return _getCursedStruct().name(); 153 | } 154 | 155 | /// @notice Returns symbol. 156 | /// @return Symbol. 157 | function symbol() public pure returns (string memory) { 158 | return _getCursedStruct().symbol(); 159 | } 160 | 161 | /// @notice Returns decimals. 162 | /// @return Decimals. 163 | function decimals() public pure returns (uint8) { 164 | return _getCursedStruct().decimals(); 165 | } 166 | 167 | /// @notice Returns total supply. 168 | /// @return Supply. 169 | function totalSupply() public view returns (uint256) { 170 | return _getCursedStruct().totalSupply(0); 171 | } 172 | 173 | /// @notice Returns balance of an account. 174 | /// @param account Account address whose balance is queried. 175 | /// @return Balance of account. 176 | function balanceOf(address account) public view returns (uint256) { 177 | return _getCursedStruct().balanceOf(_balances, account); 178 | } 179 | 180 | /// @notice Returns allowance a spender has to transfer on behalf of an owner. 181 | /// @param owner Owner address. 182 | /// @param spender Spender address. 183 | /// @return Amount spender may transfer on behalf of the owner. 184 | function allowance(address owner, address spender) public view returns (uint256) { 185 | return _getCursedStruct().allowance(_allowances, owner, spender); 186 | } 187 | 188 | /// @notice Transfers an amount of tokens from the caller to the receiver. 189 | /// @dev Logs Transfer. Reverts on sender balance underflow AND receiver balance overflow. 190 | /// @param receiver Receiving address. 191 | /// @param amount Amount to transfer. 192 | /// @return success True if successful transfer. 193 | function transfer(address receiver, uint256 amount) public returns (bool success) { 194 | success = _getCursedStruct().transfer(_balances, msg.sender, receiver, amount); 195 | emit Transfer(msg.sender, receiver, amount); 196 | } 197 | 198 | /// @notice Transfers an amount of tokens from the sender to the receiver. Can be called by any 199 | /// other address. 200 | /// @dev Logs Transfer. Reverts on sender balance underflow, receiver balance overflow, AND 201 | /// sender's allowance for caller underflow. 202 | /// @param sender Sending address. 203 | /// @param receiver Receiving address. 204 | /// @param amount Amount to transfer. 205 | /// @return success True if successful transfer. 206 | function transferFrom( 207 | address sender, 208 | address receiver, 209 | uint256 amount 210 | ) public returns (bool success) { 211 | success = _getCursedStruct().transferFrom( 212 | _balances, 213 | _allowances, 214 | msg.sender, 215 | sender, 216 | receiver, 217 | amount 218 | ); 219 | emit Transfer(sender, receiver, amount); 220 | } 221 | 222 | /// @notice Caller approves spender to transfer up to an amount on their behalf. 223 | /// @dev Logs Approval. 224 | /// @param spender Address to which the caller is delegating an allowance. 225 | /// @param amount Amount the spender is allowed to transfer on the caller's behalf. 226 | /// @return success True if successful approval. 227 | function approve(address spender, uint256 amount) public returns (bool success) { 228 | success = _getCursedStruct().approve(_allowances, msg.sender, spender, amount); 229 | emit Approval(msg.sender, spender, amount); 230 | } 231 | 232 | /// @notice Mint an amount of tokens to a receiver. 233 | /// @dev Reverts on receiver balance overflow OR total supply overflow. 234 | /// @param receiver Receiver address. 235 | /// @param amount Amount to mint. 236 | function mint(address receiver, uint256 amount) public { 237 | _balances[receiver] += amount; 238 | _totalSupplyAmount += amount; 239 | } 240 | 241 | // Constructs the cursed struct. 242 | function _getCursedStruct() internal pure returns (CursedStruct memory cs) { 243 | cs.name = _name; 244 | cs.symbol = _symbol; 245 | cs.decimals = _decimals; 246 | cs.totalSupply = _totalSupply; 247 | cs.balanceOf = _balanceOf; 248 | cs.allowance = _allowance; 249 | cs.transfer = _transfer; 250 | cs.transferFrom = _transferFrom; 251 | cs.approve = _approve; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /test/CusedStructToken.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/CursedStructToken.sol"; 6 | 7 | contract CursedStructTokenTest is Test { 8 | CursedStructToken token; 9 | 10 | function setUp() public { 11 | token = new CursedStructToken(); 12 | } 13 | 14 | function invariantMetadata() public { 15 | assertEq(token.name(), "Cursed Struct Token"); 16 | assertEq(token.symbol(), "CST"); 17 | assertEq(token.decimals(), 18); 18 | } 19 | 20 | function testMint() public { 21 | token.mint(address(0xBEEF), 1e18); 22 | 23 | assertEq(token.totalSupply(), 1e18); 24 | assertEq(token.balanceOf(address(0xBEEF)), 1e18); 25 | } 26 | 27 | function testApprove() public { 28 | assertTrue(token.approve(address(0xBEEF), 1e18)); 29 | 30 | assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); 31 | } 32 | 33 | function testTransfer() public { 34 | token.mint(address(this), 1e18); 35 | 36 | assertTrue(token.transfer(address(0xBEEF), 1e18)); 37 | assertEq(token.totalSupply(), 1e18); 38 | 39 | assertEq(token.balanceOf(address(this)), 0); 40 | assertEq(token.balanceOf(address(0xBEEF)), 1e18); 41 | } 42 | 43 | function testTransferFrom() public { 44 | address from = address(0xABCD); 45 | 46 | token.mint(from, 1e18); 47 | 48 | vm.prank(from); 49 | token.approve(address(this), 1e18); 50 | 51 | assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); 52 | assertEq(token.totalSupply(), 1e18); 53 | 54 | assertEq(token.allowance(from, address(this)), 0); 55 | 56 | assertEq(token.balanceOf(from), 0); 57 | assertEq(token.balanceOf(address(0xBEEF)), 1e18); 58 | } 59 | 60 | function testInfiniteApproveTransferFrom() public { 61 | address from = address(0xABCD); 62 | 63 | token.mint(from, 1e18); 64 | 65 | vm.prank(from); 66 | token.approve(address(this), type(uint256).max); 67 | 68 | assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); 69 | assertEq(token.totalSupply(), 1e18); 70 | 71 | assertEq(token.allowance(from, address(this)), type(uint256).max); 72 | 73 | assertEq(token.balanceOf(from), 0); 74 | assertEq(token.balanceOf(address(0xBEEF)), 1e18); 75 | } 76 | 77 | function testFailTransferInsufficientBalance() public { 78 | token.mint(address(this), 0.9e18); 79 | vm.expectRevert(); 80 | token.transfer(address(0xBEEF), 1e18); 81 | } 82 | 83 | function testFailTransferFromInsufficientAllowance() public { 84 | address from = address(0xABCD); 85 | 86 | token.mint(from, 1e18); 87 | 88 | vm.prank(from); 89 | token.approve(address(this), 0.9e18); 90 | 91 | vm.expectRevert(); 92 | token.transferFrom(from, address(0xBEEF), 1e18); 93 | } 94 | 95 | function testFailTransferFromInsufficientBalance() public { 96 | address from = address(0xABCD); 97 | 98 | token.mint(from, 0.9e18); 99 | 100 | vm.prank(from); 101 | token.approve(address(this), 1e18); 102 | 103 | vm.expectRevert(); 104 | token.transferFrom(from, address(0xBEEF), 1e18); 105 | } 106 | 107 | function testMint(address from, uint256 amount) public { 108 | token.mint(from, amount); 109 | 110 | assertEq(token.totalSupply(), amount); 111 | assertEq(token.balanceOf(from), amount); 112 | } 113 | 114 | function testApprove(address to, uint256 amount) public { 115 | assertTrue(token.approve(to, amount)); 116 | 117 | assertEq(token.allowance(address(this), to), amount); 118 | } 119 | 120 | function testTransfer(address from, uint256 amount) public { 121 | token.mint(address(this), amount); 122 | 123 | assertTrue(token.transfer(from, amount)); 124 | assertEq(token.totalSupply(), amount); 125 | 126 | if (address(this) == from) { 127 | assertEq(token.balanceOf(address(this)), amount); 128 | } else { 129 | assertEq(token.balanceOf(address(this)), 0); 130 | assertEq(token.balanceOf(from), amount); 131 | } 132 | } 133 | 134 | function testTransferFrom( 135 | address to, 136 | uint256 approval, 137 | uint256 amount 138 | ) public { 139 | amount = bound(amount, 0, approval); 140 | 141 | address from = address(0xABCD); 142 | 143 | token.mint(from, amount); 144 | 145 | vm.prank(from); 146 | token.approve(address(this), approval); 147 | 148 | assertTrue(token.transferFrom(from, to, amount)); 149 | assertEq(token.totalSupply(), amount); 150 | 151 | uint256 app = from == address(this) || approval == type(uint256).max ? approval : approval - amount; 152 | assertEq(token.allowance(from, address(this)), app); 153 | 154 | if (from == to) { 155 | assertEq(token.balanceOf(from), amount); 156 | } else { 157 | assertEq(token.balanceOf(from), 0); 158 | assertEq(token.balanceOf(to), amount); 159 | } 160 | } 161 | 162 | function testFailTransferInsufficientBalance( 163 | address to, 164 | uint256 mintAmount, 165 | uint256 sendAmount 166 | ) public { 167 | sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); 168 | 169 | token.mint(address(this), mintAmount); 170 | vm.expectRevert(); 171 | token.transfer(to, sendAmount); 172 | } 173 | 174 | function testFailTransferFromInsufficientAllowance( 175 | address to, 176 | uint256 approval, 177 | uint256 amount 178 | ) public { 179 | amount = bound(amount, approval + 1, type(uint256).max); 180 | 181 | address from = address(0xABCD); 182 | 183 | token.mint(from, amount); 184 | 185 | vm.prank(from); 186 | token.approve(address(this), approval); 187 | 188 | vm.expectRevert(); 189 | token.transferFrom(from, to, amount); 190 | } 191 | 192 | function testFailTransferFromInsufficientBalance( 193 | address to, 194 | uint256 mintAmount, 195 | uint256 sendAmount 196 | ) public { 197 | sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); 198 | 199 | address from = address(0xABCD); 200 | 201 | token.mint(from, mintAmount); 202 | 203 | vm.prank(from); 204 | token.approve(address(this), sendAmount); 205 | 206 | vm.expectRevert(); 207 | token.transferFrom(from, to, sendAmount); 208 | } 209 | } 210 | --------------------------------------------------------------------------------