diff --git a/README.md b/README.md index 5ed21775..e4d0e840 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ math ├─ Trigonometry — "Basic trigonometry functions where inputs and outputs are integers" tokens ├─ ERC20 — "Modern and gas efficient ERC20 + EIP-2612 implementation" -├─ ERC721 — TODO — "Modern, minimalist, and gas efficient ERC721 implementation" +├─ ERC721 — "Modern, minimalist, and gas efficient ERC721 implementation" ├─ ERC1155 — "Minimalist and gas efficient standard ERC1155 implementation" ├─ ERC4626 — TODO - "Minimal ERC4626 tokenized Vault implementation" utils diff --git a/foundry.toml b/foundry.toml index ee776ae8..28edca6c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,22 +4,26 @@ libs = ['lib'] ffi = true fuzz_runs = 1000 fs_permissions = [ - { access = "read", path = "./test/data-structures/mocks/InstantiatedArrays.huff" }, - { access = "read", path = "./test/data-structures/mocks/InstantiatedHashmap.huff" }, + { access = "read", path = "./test/data-structures/mocks/ArrayWrappers.huff" }, + { access = "read", path = "./test/data-structures/mocks/HashmapWrappers.huff" }, + { access = "read", path = "./test/math/mocks/FixedPointMathWrappers.huff" }, { access = "read", path = "./test/math/mocks/SafeMathWrappers.huff" }, { access = "read", path = "./test/math/mocks/MathWrappers.huff" }, { access = "read", path = "./test/math/mocks/TrigonometryWrappers.huff" }, + { access = "read", path = "./test/tokens/mocks/ERC1155Wrappers.huff" }, - { access = "read", path = "./test/tokens/mocks/MockERC20.huff" }, - { access = "read", path = "./test/tokens/mocks/ERC20Mintable.huff" }, + { access = "read", path = "./test/tokens/mocks/ERC20Wrappers.huff" }, + { access = "read", path = "./test/tokens/mocks/ERC721Wrappers.huff" }, + { access = "read", path = "./test/tokens/mocks/ERC20MintableWrappers.huff" }, + { access = "read", path = "./test/utils/mocks/BitPackLibWrappers.huff" }, - { access = "read", path = "./test/utils/mocks/ErrorsMock.huff" }, + { access = "read", path = "./test/utils/mocks/ErrorWrappers.huff" }, { access = "read", path = "./test/utils/mocks/JumpTableUtilWrappers.huff" }, { access = "read", path = "./test/utils/mocks/MerkleProofLibWrappers.huff" }, { access = "read", path = "./test/utils/mocks/MulticallableWrappers.huff" }, - { access = "read", path = "./test/utils/mocks/ReentrancyGuardMock.huff" }, - { access = "read", path = "./test/utils/mocks/LibBitWrapper.huff" }, + { access = "read", path = "./test/utils/mocks/ReentrancyGuardWrappers.huff" }, + { access = "read", path = "./test/utils/mocks/LibBitWrappers.huff" }, ] remappings = [ "forge-std/=lib/forge-std/src/", diff --git a/lib/forge-std b/lib/forge-std index 82e6f537..1556165a 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 82e6f5376c15341629ca23098e0b32d303f44f02 +Subproject commit 1556165a8f3ecfa5620152f77d3a6e5de30d0ff8 diff --git a/lib/foundry-huff b/lib/foundry-huff index c22e6f72..eb9ea316 160000 --- a/lib/foundry-huff +++ b/lib/foundry-huff @@ -1 +1 @@ -Subproject commit c22e6f72a1af4e0585228da9b317bd8e7a6e9b49 +Subproject commit eb9ea316389e8bd580f92908b8454399401d0316 diff --git a/src/data-structures/Arrays.huff b/src/data-structures/Arrays.huff index a0fa366e..b1671230 100644 --- a/src/data-structures/Arrays.huff +++ b/src/data-structures/Arrays.huff @@ -1,3 +1,8 @@ +/// @title Arrays +/// @author exp-table +/// @notice Array utility library for Solidity contracts +/// @notice Adapted from + // Sets an array in storage from calldata. // Note that since no assumptions is made regarding the context in which // this function is called, the position of the encoded array in the calldata diff --git a/src/data-structures/Hashmap.huff b/src/data-structures/Hashmap.huff index 95ba6483..7eb105f4 100644 --- a/src/data-structures/Hashmap.huff +++ b/src/data-structures/Hashmap.huff @@ -1,5 +1,5 @@ /// @title HashMap -/// @author Andreas Bigger +/// @author asnared /// @notice A Minimal HashMap based off // Given a piece of data (ie an address), hash it, generating the storage slot for a hashmap. diff --git a/src/math/Math.huff b/src/math/Math.huff index 92ae71bb..f626c71b 100644 --- a/src/math/Math.huff +++ b/src/math/Math.huff @@ -1,6 +1,7 @@ /// @title SafeMath -/// @author https://github.com/kadenzipfel -/// @notice Math module based off https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol +/// @author kadenzipfel +/// @notice Math module over Solidity's arithmetic operations +/// @notice Adapted from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol #define function sqrt(uint256) pure returns(uint256) #define function max(uint256, uint256) pure returns(uint256) diff --git a/src/math/SafeMath.huff b/src/math/SafeMath.huff index c66e7d6d..47fd1094 100644 --- a/src/math/SafeMath.huff +++ b/src/math/SafeMath.huff @@ -1,6 +1,7 @@ /// @title SafeMath -/// @author https://github.com/kadenzipfel -/// @notice SafeMath module based off https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol +/// @author kadenzipfel +/// @notice Math module over Solidity's arithmetic operations with safety checks +/// @notice Adapted from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol // Interface #define function safeAdd(uint256,uint256) pure returns (uint256) diff --git a/src/tokens/ERC1155.huff b/src/tokens/ERC1155.huff index 1339d49a..57e62b8a 100644 --- a/src/tokens/ERC1155.huff +++ b/src/tokens/ERC1155.huff @@ -8,20 +8,19 @@ #include "../utils/ERC1155Receiver.huff" #include "../utils/CommonErrors.huff" -// Stateful Function Interface +// Function Interface #define function mint(address,uint256,uint256,bytes) nonpayable returns () -#define function batchMint(address,uint256[],uint256[],bytes) nonpayable returns () - #define function burn(address,uint256,uint256) nonpayable returns () + +#define function batchMint(address,uint256[],uint256[],bytes) nonpayable returns () #define function batchBurn(address,uint256[],uint256[]) nonpayable returns () +#define function isApprovedForAll(address,address) view returns (bool) +#define function setApprovalForAll(address,bool) nonpayable returns () + #define function safeTransferFrom(address,address,uint256,uint256,bytes) nonpayable returns () #define function safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) nonpayable returns() -#define function setApprovalForAll(address,bool) nonpayable returns () -// Accessor Function Interface -#define function getApproved(uint256) view returns (address) -#define function isApprovedForAll(address,address) view returns (bool) #define function balanceOf(address,uint256) view returns (uint256) #define function balanceOfBatch(address[],uint256[]) view returns (uint256[]) @@ -542,8 +541,8 @@ 0x80 // [&ret] // Get lengths of dynamic arrays, duplicate in prepare to compare - 0x24 calldataload 0x4 add dup1 // [&ids, &ids, &owners, &owners, &ret] - 0x04 calldataload 0x4 add dup1 // [&ids, &ids, &ret] + 0x24 calldataload 0x4 add dup1 // [&ids, &ids, &ret] + 0x04 calldataload 0x4 add dup1 // [&ids, &ids, &owners, &owners, &ret] // compare lengths swap2 // [&owners, &ids, &ids, &owners, &ret] diff --git a/src/tokens/ERC20.huff b/src/tokens/ERC20.huff index 6d7c674f..037ee495 100644 --- a/src/tokens/ERC20.huff +++ b/src/tokens/ERC20.huff @@ -4,10 +4,11 @@ /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. /// @notice Modified from Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) +// Imports +#include "../utils/Errors.huff" +#include "../utils/NonPayable.huff" #include "../data-structures/Hashmap.huff" -// TODO: Add revert strings (and related tests) - // Interface #define function allowance(address,address) view returns (uint256) #define function approve(address,uint256) nonpayable returns () // these returns sb updated @@ -398,12 +399,14 @@ expired: 0x5045524D49545F444541444C494E455F45585049524544000000000000000000 // ["PERMIT_DEADLINE_EXPIRED"] 0x17 // [23 (length), "PERMIT_DEADLINE_EXPIRED"] - REVERT_WITH_REASON(0x00) + 0x00 // [0, 23 (length), "PERMIT_DEADLINE_EXPIRED"] + REQUIRE() invalidSigner: 0x494E56414C49445F5349474E4552000000000000000000000000000000000000 // ["INVALID_SIGNER"] 0x0e // [14 (length), "INVALID_SIGNER"] - REVERT_WITH_REASON(0x00) + 0x00 // [0, 14 (length), "INVALID_SIGNER"] + REQUIRE() } /// @notice Takes an address off the stack, returns the current nonce for that address onto the stack. @@ -530,30 +533,6 @@ log3 // [] } -/// @notice Reverts if the call has a non-zero value. -#define macro NON_PAYABLE() = takes (0) returns (0) { - callvalue iszero // [msg.value == 0] - novalue jumpi // [] - - // Revert with a "NON_PAYABLE" message - 0xb4e4f4e5f50415941424c4500000000000000000000000000000000000000000 // ["NON_PAYABLE"] - 0x1b // [11 (length), "NON_PAYABLE"] - REVERT_WITH_REASON(0x00) // [] - - novalue: -} - -/// @notice Revert with a string reason -#define macro REVERT_WITH_REASON(mem_ptr) = takes (2) returns(0) { - // NOTE: String must be < 32 bytes - // [len, "str"] - [ERROR_SIG] mstore // [len, "str"] 0x00 error sig - 0x20 0x04 add mstore // [len, "str"] 0x00 error sig 0x04 offset - 0x24 add mstore // ["str"] 0x00 error sig 0x04 offset 0x24 len - 0x44 add mstore // [] 0x00 error sig 0x04 offset 0x24 len 0x44: "string" - 0x64 0x00 revert -} - // Function Dispatching #define macro MAIN_ERC20() = takes (1) returns (1) { diff --git a/src/tokens/ERC721.huff b/src/tokens/ERC721.huff new file mode 100644 index 00000000..0860cf69 --- /dev/null +++ b/src/tokens/ERC721.huff @@ -0,0 +1,520 @@ +/// @title ERC721 +/// @author asnared +/// @author kadenzipfel +/// @notice Modern and heavily gas golfed ERC-721 implementation +/// @notice Adapted from Solmate https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol + +// Imports +#include "../utils/CommonErrors.huff" +#include "../utils/NonPayable.huff" +#include "../data-structures/Hashmap.huff" + +// Interface +#define function name() nonpayable returns (string) +#define function symbol() nonpayable returns (string) +#define function tokenURI(uint256) nonpayable returns (string) + +#define function mint(address, uint256) payable returns () +#define function burn(uint256) nonpayable returns () + +#define function transfer(address,uint256) nonpayable returns () +#define function transferFrom(address,address,uint256) nonpayable returns () +#define function safeTransferFrom(address,address,uint256) nonpayable returns () +#define function safeTransferFrom(address,address,uint256,bytes) nonpayable returns () + +#define function approve(address,uint256) nonpayable returns () +#define function setApprovalForAll(address,bool) nonpayable returns () + +#define function getApproved(uint256) view returns (address) +#define function isApprovedForAll(address,address) view returns (bool) +#define function ownerOf(uint256) view returns (address) +#define function balanceOf(address) view returns (uint256) +#define function supportsInterface(bytes4) view returns (bool) + +// Events +#define event Transfer(address,address,uint256) +#define event Approval(address,address,uint256) +#define event ApprovalForAll(address,address,bool) + +#define event LogWord(address) + +// Storage Slots +#define constant OWNER_LOCATION = FREE_STORAGE_POINTER() // ownerOf +#define constant BALANCE_LOCATION = FREE_STORAGE_POINTER() // balanceOf +#define constant SINGLE_APPROVAL_LOCATION = FREE_STORAGE_POINTER() // getApproved + +// Metadata +// META_NAME = "Token" +#define constant META_NAME = 0x546f6b656e000000000000000000000000000000000000000000000000000000 +#define constant META_NAME_LENGTH = 0x05 + +// META_SYMBOL = "TKN" +#define constant META_SYMBOL = 0x544B4E0000000000000000000000000000000000000000000000000000000000 +#define constant META_SYMBOL_LENGTH = 0x03 + + +/// >>>>>>>>>>>>>>>>>>>>> VIEW FUNCTIONS <<<<<<<<<<<<<<<<<<<<<< /// + +/// @notice Name +/// @notice Returns the token name string +#define macro NAME() = takes (0) returns (0) { + NON_PAYABLE() // [] + 0x20 0x00 mstore // [] + [META_NAME_LENGTH] 0x20 mstore // [] + [META_NAME] 0x40 mstore // [] + 0x60 0x00 return // [] +} + +/// @notice Symbol +/// @notice Returns the symbol of the token +#define macro SYMBOL() = takes (0) returns (0) { + NON_PAYABLE() // [] + 0x20 0x00 mstore // [] + [META_SYMBOL_LENGTH] 0x20 mstore // [] + [META_SYMBOL] 0x40 mstore // [] + 0x60 0x00 return // [] +} + +/// @notice Balance Of +/// @notice Returns the balance of the given address +#define macro BALANCE_OF() = takes (0) returns (0) { + NON_PAYABLE() // [] + 0x04 calldataload // [account] + [BALANCE_LOCATION] LOAD_ELEMENT_FROM_KEYS(0x00) // [balance] + 0x00 mstore // [] + 0x20 0x00 return // [] +} + +/// @notice Owner Of +/// @notice Returns the owner of the given token id +#define macro OWNER_OF() = takes (0) returns (0) { + 0x04 calldataload // [tokenId] + [OWNER_LOCATION] LOAD_ELEMENT_FROM_KEYS(0x00) // [owner] + 0x00 mstore // [] + 0x20 0x00 return // [] +} + +/// @notice Is Approved For All +/// @notice Returns whether the given operator is approved for all tokens of the given owner +#define macro IS_APPROVED_FOR_ALL() = takes (0) returns (0) { + 0x24 calldataload // [to] + 0x04 calldataload // [from, to] + LOAD_ELEMENT_FROM_KEYS(0x00) // [value] + 0x00 mstore // [] + 0x20 0x00 return // [] +} + +/// @notice Get Approved +/// @notice Returns the approved address for the given token id +#define macro GET_APPROVED() = takes (0) returns (0) { + 0x04 calldataload // [tokenId] + [SINGLE_APPROVAL_LOCATION] // [approval_slot, tokenId] + LOAD_ELEMENT_FROM_KEYS(0x00) // [spender] + 0x00 mstore // [] + 0x20 0x00 return // [] +} + +/// @notice Token URI +#define macro TOKEN_URI() = takes (0) returns (0) { + 0x20 0x00 mstore + 0x00 0x20 mstore + 0x40 0x00 return +} + +#define macro SUPPORTS_INTERFACE() = takes (0) returns (0) { + // grab interfaceId + 0x04 calldataload // [interfaceId] + 0xe0 + shr + + // check if erc165 interfaceId + dup1 // [interfaceId, interfaceId] + 0x01ffc9a7 eq // [is_erc165, interfaceId] + is_interface jumpi + + // check if erc721 interfaceId + dup1 // [interfaceId, interfaceId] + 0x80ac58cd eq // [is_erc721, interfaceId] + is_interface jumpi + + // check if erc721Metadata interfaceId + 0x5b5e139f eq // [is_erc721Metadata] + is_interface jumpi + + 0x00 mstore // [] + 0x20 0x00 return // [] + + is_interface: + pop // [] + 0x01 0x00 mstore // [] + 0x20 0x00 return // [] +} + +/// >>>>>>>>>>>>>>>>>>>>> INTERNAL FUNCTIONS <<<<<<<<<<<<<<<<<<<<<< /// + +/// @notice Mint +/// @notice Mints a new token +/// @dev The Mint function is payable +#define macro _MINT() = takes (2) returns (0) { + // Input stack: // [to, tokenId] + + // Check that the recipient is valid + dup1 iszero invalid_recipient jumpi // [to, tokenId] + + // Create the minting params + 0x00 // [from (0x00), to, tokenId] + dup3 // [tokenId, from (0x00), to, tokenId] + + // Check token ownership + [OWNER_LOCATION] LOAD_ELEMENT_FROM_KEYS(0x00) // [owner, from (0x00), to, tokenId] + iszero iszero unauthorized jumpi + + // Give tokens to the recipient. + TRANSFER_GIVE_TO() // [from (0x00), to, tokenId] + + // Emit the transfer event. + __EVENT_HASH(Transfer) // [sig, from (0x00), to, tokenId] + 0x00 0x00 // [0, 0, sig, from (0x00), to, tokenId] + log4 // [] + + stop + + invalid_recipient: + INVALID_RECIPIENT(0x00) + + unauthorized: + ALREADY_MINTED(0x00) +} + +/// @notice Burn +/// @notice Burns the token with the given id +#define macro _BURN() = takes (1) returns (0) { + // Input stack: // [tokenId] + NON_PAYABLE() // [tokenId] + + dup1 // [tokenId, tokenId] + [OWNER_LOCATION] LOAD_ELEMENT_FROM_KEYS(0x00) // [owner, tokenId] + + // Check that the recipient is valid + dup1 iszero // [owner == 0, owner, tokenId] + not_minted jumpi // [owner, tokenId] + + // Create the burning params + 0x00 swap1 // [owner, to (0x00), tokenId] + + // Reduce the balance of owner by 1 + 0x01 dup2 // [owner, 1, owner, to, tokenId] + [BALANCE_LOCATION] LOAD_ELEMENT_FROM_KEYS(0x00) // [balance, 1, owner, to, tokenId] + sub dup2 // [owner, balance-1, owner, to, tokenId] + [BALANCE_LOCATION] + STORE_ELEMENT_FROM_KEYS(0x00) // [owner, to, tokenId] + + // Set the owner of the token to 0x00 + 0x00 dup4 [OWNER_LOCATION] // [slot, owner, 0x00, owner, to, tokenId] + STORE_ELEMENT_FROM_KEYS(0x00) // [owner, to, tokenId] + + // Set the approval of the token to 0x00 for the owner + 0x00 dup4 [SINGLE_APPROVAL_LOCATION] // [slot, owner, 0x00, owner, to, tokenId] + STORE_ELEMENT_FROM_KEYS(0x00) // [owner, to, tokenId] + + // Emit the transfer event. + __EVENT_HASH(Transfer) // [sig, owner, to (0x00), tokenId] + 0x00 0x00 // [0, 0, sig, owner, to (0x00), tokenId] + log4 // [] + + stop + + not_minted: + NOT_MINTED(0x00) +} + +/// >>>>>>>>>>>>>>>>>>>>> EXTERNAL FUNCTIONS <<<<<<<<<<<<<<<<<<<<<< /// + +/// @notice Approve +/// @notice Approves a spender for a specific token +#define macro APPROVE() = takes (0) returns (0) { + // get owner + 0x24 calldataload dup1 // [tokenId, tokenId] + [OWNER_LOCATION] + LOAD_ELEMENT_FROM_KEYS(0x00) // [owner, tokenId] + dup1 caller // [msg.sender, owner, owner, tokenId] + eq // [is_sender_owner, owner, tokenId] + + // check if approved for all + caller dup3 // [owner, msg.sender, is_sender_owner, owner, tokenId] + LOAD_ELEMENT_FROM_KEYS(0x00) // [is_approved_for_all, is_sender_owner, owner, tokenId]] + or cont jumpi // [owner, tokenId] + not_authorized jump + cont: + + // store approval + 0x04 calldataload dup1 dup4 // [tokenId, spender, spender, owner, tokenId] + [SINGLE_APPROVAL_LOCATION] + STORE_ELEMENT_FROM_KEYS(0x00) // [spender, owner, tokenId] + swap1 // [owner, spender, tokenId] + + // emit the approval event + __EVENT_HASH(Approval) // [sig, owner, spender, tokenId] + 0x00 0x00 // [0, 0, sig, owner, spender, tokenId] + log4 // [] + + stop + + not_authorized: + UNAUTHORIZED(0x00) +} + +/// @notice Set Approval For All +/// @notice Sets an operator as approved for all tokens of the caller +#define macro SET_APPROVAL_FOR_ALL() = takes (0) returns (0) { + // Store the operator as approved for all + 0x24 calldataload // [approved] + 0x04 calldataload // [operator, approved] + caller // [msg.sender, operator, approved] + STORE_ELEMENT_FROM_KEYS(0x00) // [] + + // Emit the ApprovalForAll event + 0x24 calldataload // [approved] + 0x04 calldataload // [operator, approved] + caller // [msg.sender, operator, approved] + __EVENT_HASH(ApprovalForAll) // [sig, owner, operator] + 0x00 0x00 // [0, 32, sig, owner, operator] + log4 // [] + + // Stop execution + stop +} + +/// @notice Transfer From +/// @notice Transfers a token from one address to another +#define macro TRANSFER_FROM() = takes (0) returns (0) { + // Setup the stack for the transfer function. + 0x44 calldataload // [tokenId] + 0x24 calldataload // [to, tokenId] + 0x04 calldataload // [from, to, tokenId] + + TRANSFER_TAKE_FROM() // [from, to, tokenId] + TRANSFER_GIVE_TO() // [from, to, tokenId] + + // Emit the transfer event. + __EVENT_HASH(Transfer) // [sig,from, to, tokenId] + 0x20 0x00 // [0, 0, sig, from, to, tokenId] + log4 // [] + + // Stop execution + stop +} + +/// @notice Safe Transfer From +#define macro SAFE_TRANSFER_FROM() = takes (0) returns (0) { + // Setup the stack for the transfer function. + 0x44 calldataload // [tokenId] + 0x24 calldataload // [to, tokenId] + 0x04 calldataload // [from, to, tokenId] + + TRANSFER_TAKE_FROM() // [from, to, tokenId] + TRANSFER_GIVE_TO() // [from, to, tokenId] + + // Emit the transfer event. + __EVENT_HASH(Transfer) // [sig, from, to, tokenId] + 0x00 0x00 // [0, 0, sig, from, to, tokenId] + log4 // [] + + // Make sure we can transfer to the recipient + 0x24 calldataload // [to] + dup1 extcodesize // [to.code.length, to] + iszero safe jumpi // [to] + + // onERC721Received Selector + // 0xf0b9e5ba + 0x150b7a02 dup1 // [onERC721Received, onERC721Received, to] + 0xE0 shl // [onERC721Received_shifted, onERC721Received, to] + + // Store the left-shifted selector for call + 0x20 mstore // [onERC721Received, to] + + // Store the msg.sender as the first arg + caller 0x24 mstore // [onERC721Received, to] + + // Store from as the second arg + 0x04 calldataload // [from, onERC721Received, to] + 0x44 mstore // [onERC721Received, to] + + // Id is the third arg + 0x44 calldataload // [tokenId, onERC721Received, to] + 0x64 mstore // [onERC721Received, to] + + // Blank bytes array as 4th arg (no data) + 0x80 0x84 mstore + 0x00 0xA4 mstore + + // Call address(to).onERC721Received(msg.sender, from, tokenId, "") + 0x20 // [retSize, onERC721Received, to] + 0x00 // [retOffset, retSize, onERC721Received, to] + 0xA4 // [argSize, retOffset, retSize, onERC721Received, to] + dup3 // [argOffset, argSize, retOffset, retSize, onERC721Received, to] + dup3 // [value, argOffset, argSize, retOffset, retSize, onERC721Received, to] + dup7 // [to, value, argOffset, argSize, retOffset, retSize, onERC721Received, to] + gas // [gas, to, value, argOffset, argSize, retOffset, retSize, onERC721Received, to] + call // [success, onERC721Received, to] + + // Revert if call isn't successful + cont jumpi // [onERC721Received, to] + 0x00 dup1 revert + cont: + + // Compare the return data to the onERC721Received selector + 0x00 mload 0xE0 shr // [response, onERC721Received, to] + eq safe jumpi // [to] + + // Revert if the return data is not accepted + UNSAFE_RECIPIENT(0x00) + + safe: + stop +} + +#define macro SAFE_TRANSFER_FROM_WITH_DATA() = takes (0) returns (0) { + // Setup the stack for the transfer function. + 0x44 calldataload // [tokenId] + 0x24 calldataload // [to, tokenId] + 0x04 calldataload // [from, to, tokenId] + + TRANSFER_TAKE_FROM() // [from, to, tokenId] + TRANSFER_GIVE_TO() // [from, to, tokenId] + + // Emit the transfer event. + __EVENT_HASH(Transfer) // [sig, from, to, tokenId] + 0x00 0x00 // [0, 0, sig, from, to, tokenId] + log4 // [] + + // Make sure we can transfer to the recipient + 0x24 calldataload // [to] + dup1 extcodesize // [to.code.length, to] + iszero safe jumpi // [to] + + // onERC721Received Selector + // 0xf0b9e5ba + 0x150b7a02 dup1 // [onERC721Received, onERC721Received, to] + 0xE0 shl // [onERC721Received_shifted, onERC721Received, to] + + // Store the left-shifted selector for call + 0x20 mstore // [onERC721Received, to] + + // Store the msg.sender as the first arg + caller 0x24 mstore // [onERC721Received, to] + + // Store from as the second arg + 0x04 calldataload // [from, onERC721Received, to] + 0x44 mstore // [onERC721Received, to] + + // Id is the third arg + 0x44 calldataload // [tokenId, onERC721Received, to] + 0x64 mstore // [onERC721Received, to] + + 0x84 calldataload // [len(data), onERC721Received, to] + 0x05 shl // [len(data) * 0x20, onERC721Received, to] + 0x40 add // [len(data) * 0x20 + 0x40, onERC721Received, to] + dup1 // [len(data) * 0x20 + 0x40, len(data) * 0x20 + 0x40, onERC721received, to] + 0x64 // [0x64, len(data) * 0x20 + 0x40, len(data) * 0x20 + 0x40, onERC721received, to] + 0x84 // [0x20, 0x64, len(data) * 0x20 + 0x40, len(data) * 0x20 + 0x40, onERC721received, to] + calldatacopy // [len(bytes), onERC721received, to] + + // Call address(to).onERC721Received(msg.sender, from, tokenId, bytes) + 0x20 // [retSize, len(bytes), onERC721Received, to] + 0x00 // [retOffset, retSize, len(bytes), onERC721Received, to] + swap1 swap2 // [len(bytes), retOffset, retSize, onERC721Received, to] + 0x64 add // [argSize, retOffset, retSize, onERC721Received, to] + dup3 // [argOffset, argSize, retOffset, retSize, len(bytes), onERC721Received, to] + dup3 // [value, argOffset, argSize, retOffset, retSize, len(bytes), onERC721Received, to] + dup7 // [to, value, argOffset, argSize, retOffset, retSize, len(bytes), onERC721Received, to] + gas // [gas, to, value, argOffset, argSize, retOffset, retSize, len(bytes), onERC721Received, to] + call // [success, len(bytes), onERC721Received, to] + + // Revert if call isn't successful + cont jumpi // [len(bytes), onERC721Received, to] + 0x00 dup1 revert + cont: + + // Compare the return data to the onERC721Received selector + 0x00 mload 0xE0 shr // [response, onERC721Received, to] + eq safe jumpi // [to] + + // Revert if the return data is not accepted + UNSAFE_RECIPIENT(0x00) + + safe: + stop +} + +/// >>>>>>>>>>>>>>>>>>>>> INTERNAL HELPERS <<<<<<<<<<<<<<<<<<<<<< /// + +/// @notice Internal Macro to update Transfer from accounting +#define macro TRANSFER_TAKE_FROM() = takes (3) returns (3) { + // Input stack: [from, to, tokenId] + + // If from !== ownerOf[tokenId] revert with "WRONG_FROM" + dup1 dup4 // [tokenId, from, from, to, tokenId] + [OWNER_LOCATION] LOAD_ELEMENT_FROM_KEYS(0x00) // [owner, from, from, to, tokenId] + eq cont jumpi // [from, to, tokenId] + WRONG_FROM(0x00) + cont: + + // If to === address(0) revert with "INVALID_RECIPIENT" + dup2 iszero iszero continue jumpi // [from, to, tokenId] + INVALID_RECIPIENT(0x00) + continue: + + // Check if msg.sender == from + dup1 caller eq // [msg.sender == from, from, to, tokenId] + is_authorized jumpi // [from, to, tokenId] + + // Check if approved for all + caller dup2 // [from, msg.sender, from, to, tokenId] + LOAD_ELEMENT_FROM_KEYS(0x00) // [is_approved_for_all, from, to, tokenId] + is_authorized jumpi // [from, to, tokenId] + + // Check if approved for tokenId + dup3 // [tokenId, from, to, tokenId] + [SINGLE_APPROVAL_LOCATION] // [SINGLE_APPROVAL_LOCATION, tokenId, from, to, tokenId] + LOAD_ELEMENT_FROM_KEYS(0x00) // [address_approved_for_tokenId, from, to, tokenId] + caller eq is_authorized jumpi // [from, to, tokenId] + + // If msg.sender != from && !isApprovedForAll[from][msg.sender] && msg.sender != getApproved[id], + UNAUTHORIZED(0x00) + + is_authorized: + + // Update balance of from + 0x01 dup2 // [from, 1, from, to, tokenId] + [BALANCE_LOCATION] LOAD_ELEMENT_FROM_KEYS(0x00) // [balance, 1, from, to, tokenId] + sub dup2 // [from, balance-1, from, to, tokenId] + [BALANCE_LOCATION] + STORE_ELEMENT_FROM_KEYS(0x00) // [from, to, tokenId] +} + +/// @notice Internal Macro to update Transfer to accounting +#define macro TRANSFER_GIVE_TO() = takes (3) returns (3) { + // retrieve balance + // input stack: // [from, to, tokenId] + dup2 // [to, from, to, tokenId] + [BALANCE_LOCATION] // [balance_slot, to, from, to, tokenId] + LOAD_ELEMENT_FROM_KEYS(0x00) // [balance, from, to, tokenId] + 0x01 add // [balance+1, from, to, tokenId] + + // update balance + dup3 // [to, balance+1, from, to, tokenId] + [BALANCE_LOCATION] // [balance_slot, to, balance+1, from, to, tokenId] + STORE_ELEMENT_FROM_KEYS(0x00) // [from, to, tokenId] + + // update ownerOf + dup2 // [to, from, to, tokenId] + dup4 // [tokenId, to, from, to, tokenId] + [OWNER_LOCATION] // [owner_slot, tokenId, to, from, to, tokenId] + STORE_ELEMENT_FROM_KEYS(0x00) // [from, to, tokenId] + + // update approval + 0x00 dup4 // [tokenId, address(0), from, to, tokenId] + [SINGLE_APPROVAL_LOCATION] // [approval_slot, tokenId, address(0), from, to, tokenId] + STORE_ELEMENT_FROM_KEYS(0x00) // [from, to, tokenId] +} diff --git a/src/tokens/MockERC20.huff b/src/tokens/MockERC20.huff deleted file mode 100644 index 45d026ed..00000000 --- a/src/tokens/MockERC20.huff +++ /dev/null @@ -1,23 +0,0 @@ - -// !! WARNING !! -// This code is meant to be appended to src/tokens/ERC20.huff -// Thus, it does not import any other files - -#include "./ERC20.huff" - -// Constructor -// #define macro CONSTRUCTOR() { -// // Construct the ERC20 -// CONSTRUCTOR_ERC20() - -// // Mint the sender a supply -// caller // [to] -// 0x100 // [value, to] -// _MINT() // [] -// } - -// Main Entrypoint -#define macro MAIN() = { - 0x00 calldataload 0xE0 shr // [func sig] - MAIN_ERC20() -} diff --git a/src/utils/BitPackLib.huff b/src/utils/BitPackLib.huff index 966b851e..18fdfa4f 100644 --- a/src/utils/BitPackLib.huff +++ b/src/utils/BitPackLib.huff @@ -1,5 +1,5 @@ /// @title BitPackLib -/// @author https://github.com/kadenzipfel +/// @author kadenzipfel /// @notice Efficient bit packing library #define constant MAX = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/src/utils/CommonErrors.huff b/src/utils/CommonErrors.huff index acdf4871..ce4ceced 100644 --- a/src/utils/CommonErrors.huff +++ b/src/utils/CommonErrors.huff @@ -6,21 +6,33 @@ #include "./Errors.huff" // Error Constants -#define constant NOT_AUTHORIZED_ERROR = 0xe4e4f545f415554484f52495a4544000000000000000000000000000000000000 +#define constant NOT_AUTHORIZED_ERROR = 0x4e4f545f415554484f52495a4544000000000000000000000000000000000000 #define constant NOT_AUTHORIZED_LENGTH = 0x0e -#define constant LENGTH_MISMATCH_ERROR = 0xf4c454e4754485f4d49534d415443480000000000000000000000000000000000 +#define constant WRONG_FROM_ERROR = 0x57524f4e475f46524f4d00000000000000000000000000000000000000000000 +#define constant WRONG_FROM_LENGTH = 0x0a + +#define constant LENGTH_MISMATCH_ERROR = 0x4c454e4754485f4d49534d415443480000000000000000000000000000000000 #define constant LENGTH_MISMATCH_LENGTH = 0x0f #define constant UNSAFE_RECIPIENT_ERROR = 0x554e534146455f524543495049454e5400000000000000000000000000000000 #define constant UNSAFE_RECIPIENT_LENGTH = 0x10 -#define constant REENTRANCY_ERROR = 0xa5245454e5452414e435900000000000000000000000000000000000000000000 +#define constant REENTRANCY_ERROR = 0x5245454e5452414e435900000000000000000000000000000000000000000000 #define constant REENTRANCY_LENGTH = 0x0a -#define constant DISPATCH_ERROR_MESSAGE = 0xe44495350415443485f4552524f52000000000000000000000000000000000000 +#define constant DISPATCH_ERROR_MESSAGE = 0x44495350415443485f4552524f52000000000000000000000000000000000000 #define constant DISPATCH_LENGTH = 0x0e +#define constant INVALID_RECIPIENT_ERROR = 0x494e56414c49445f524543495049454e54000000000000000000000000000000 +#define constant INVALID_RECIPIENT_LENGTH = 0x11 + +#define constant ALREADY_MINTED_ERROR = 0x414c52454144595f4d494e544544000000000000000000000000000000000000 +#define constant ALREADY_MINTED_LENGTH = 0x0e + +#define constant NOT_MINTED_ERROR = 0x4e4f545f4d494e54454400000000000000000000000000000000000000000000 +#define constant NOT_MINTED_LENGTH = 0x0a + /// @notice Reverts with an "NOT_AUTHORIZED" message if the condition is false #define macro UNAUTHORIZED(condition) = takes (0) returns (0) { [NOT_AUTHORIZED_ERROR] // ["NOT_AUTHORIZED"] @@ -29,6 +41,14 @@ REQUIRE() // [] } +/// @notice Reverts with an "WRONG_FROM" message if the condition is false +#define macro WRONG_FROM(condition) = takes (0) returns (0) { + [WRONG_FROM_ERROR] // ["WRONG_FROM"] + [WRONG_FROM_LENGTH] // [10 (length), "WRONG_FROM"] + // [condition, 10 (length), "WRONG_FROM"] + REQUIRE() // [] +} + /// @notice Reverts with an "LENGTH_MISMATCH" message if condition is false #define macro LENGTH_MISMATCH(condition) = takes (0) returns (0) { [LENGTH_MISMATCH_ERROR] // ["LENGTH_MISMATCH"] @@ -45,6 +65,30 @@ REQUIRE() // [] } +/// @notice Reverts with an "ALREADY_MINTED" message if condition is false +#define macro ALREADY_MINTED(condition) = takes (0) returns (0) { + [ALREADY_MINTED_ERROR] // ["ALREADY_MINTED"] + [ALREADY_MINTED_LENGTH] // [14 (length), "ALREADY_MINTED"] + // [condition, 14 (length), "ALREADY_MINTED"] + REQUIRE() // [] +} + +/// @notice Reverts with an "NOT_MINTED" message if condition is false +#define macro NOT_MINTED(condition) = takes (0) returns (0) { + [NOT_MINTED_ERROR] // ["NOT_MINTED"] + [NOT_MINTED_LENGTH] // [10 (length), "NOT_MINTED"] + // [condition, 10 (length), "NOT_MINTED"] + REQUIRE() // [] +} + +/// @notice Reverts with a "INVALID_RECIPIENT" message if condition is false +#define macro INVALID_RECIPIENT(condition) = takes (0) returns (0) { + [INVALID_RECIPIENT_ERROR] // ["INVALID_RECIPIENT"] + [INVALID_RECIPIENT_LENGTH] // [17 (length), "INVALID_RECIPIENT"] + // [condition, 17 (length), "INVALID_RECIPIENT"] + REQUIRE() // [] +} + /// @notice Reverts with a "REENTRANCY" message if condition is false #define macro REENTRANCY(condition) = takes (0) returns (0) { [REENTRANCY_ERROR] // ["REENTRANCY"] diff --git a/src/utils/NonPayable.huff b/src/utils/NonPayable.huff new file mode 100644 index 00000000..a958ab04 --- /dev/null +++ b/src/utils/NonPayable.huff @@ -0,0 +1,18 @@ +/// @title Non Payable +/// @author asnared +/// @notice Simple macro to revert if a call has a value + +#include "./Errors.huff" + +// "NON_PAYABLE" Revert Message String +#define constant NON_PAYABLE_ERROR = 0xb4e4f4e5f50415941424c45000000000000000000000000000000000000000000 +#define constant NON_PAYABLE_LENGTH = 0x0b + +/// @notice Reverts if the call has a non-zero value +/// @notice Reverts with message "NON_PAYABLE" +#define macro NON_PAYABLE() = takes (0) returns (0) { + [NON_PAYABLE_ERROR] // ["NON_PAYABLE"] + [NON_PAYABLE_LENGTH] // [11 (length), "NON_PAYABLE"] + callvalue iszero // [msg.value == 0, 11 (length), "NON_PAYABLE"] + REQUIRE() // [] +} \ No newline at end of file diff --git a/test/data-structures/Arrays.t.sol b/test/data-structures/Arrays.t.sol index ff639260..a7206e24 100644 --- a/test/data-structures/Arrays.t.sol +++ b/test/data-structures/Arrays.t.sol @@ -19,7 +19,7 @@ contract ArraysTest is Test { function setUp() public { // Read instantiable arrays from file string memory instantiable_code = vm.readFile( - "test/data-structures/mocks/InstantiatedArrays.huff" + "test/data-structures/mocks/ArrayWrappers.huff" ); // Create an Instantiable Arrays diff --git a/test/data-structures/Bytes.t.sol b/test/data-structures/Bytes.t.sol index e69de29b..03c5472d 100644 --- a/test/data-structures/Bytes.t.sol +++ b/test/data-structures/Bytes.t.sol @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + diff --git a/test/data-structures/Hashmap.t.sol b/test/data-structures/Hashmap.t.sol index da6f3d81..927594ac 100644 --- a/test/data-structures/Hashmap.t.sol +++ b/test/data-structures/Hashmap.t.sol @@ -17,7 +17,7 @@ contract HashmapTest is Test { function setUp() public { // Read instantiable hashmap from file - string memory instantiable_code = vm.readFile("test/data-structures/mocks/InstantiatedHashmap.huff"); + string memory instantiable_code = vm.readFile("test/data-structures/mocks/HashmapWrappers.huff"); // Create an Instantiable Hashmap HuffConfig config = HuffDeployer.config().with_code(instantiable_code); diff --git a/test/data-structures/mocks/InstantiatedArrays.huff b/test/data-structures/mocks/ArrayWrappers.huff similarity index 100% rename from test/data-structures/mocks/InstantiatedArrays.huff rename to test/data-structures/mocks/ArrayWrappers.huff diff --git a/test/data-structures/mocks/InstantiatedHashmap.huff b/test/data-structures/mocks/HashmapWrappers.huff similarity index 100% rename from test/data-structures/mocks/InstantiatedHashmap.huff rename to test/data-structures/mocks/HashmapWrappers.huff diff --git a/test/lib/HuffTest.sol b/test/lib/HuffTest.sol deleted file mode 100644 index 8c6318c3..00000000 --- a/test/lib/HuffTest.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import "forge-std/Test.sol"; -import "foundry-huff/HuffDeployer.sol"; - -contract HuffTest is Test { - /// @dev The name of the contract. - string internal name; - - /// @dev Create a new HuffTest contract. - constructor(string memory _name) { - name = _name; - } - - /// @dev Deploy a new contract. - function deploy() internal returns (address) { - return HuffDeployer.deploy(name); - } -} \ No newline at end of file diff --git a/test/tokens/ERC1155.t.sol b/test/tokens/ERC1155.t.sol index 93f86ccb..0d148948 100644 --- a/test/tokens/ERC1155.t.sol +++ b/test/tokens/ERC1155.t.sol @@ -1225,8 +1225,8 @@ contract ERC1155Test is Test, ERC1155Recipient, FuzzingUtils{ uint256 minLength = min3(tos.length, ids.length, amounts.length); for (uint256 i = 0; i < minLength; i++) { - vm.assume(tos[i] != address(0)); - vm.assume(tos[i] != address(this)); + tos[i] = tos[i] == address(0) ? address(0xBEEF) : tos[i]; + tos[i] = tos[i] == address(this) ? address(0xBEEF) : tos[i]; } address[] memory normalizedTos = new address[](minLength); @@ -1234,7 +1234,7 @@ contract ERC1155Test is Test, ERC1155Recipient, FuzzingUtils{ for (uint256 i = 0; i < minLength; i++) { uint256 id = ids[i]; - address to = tos[i] == address(0) ? address(0xBEEF) : tos[i]; + address to = tos[i]; uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[to][id]; diff --git a/test/tokens/ERC20.t.sol b/test/tokens/ERC20.t.sol index cfac1b6c..fe883480 100644 --- a/test/tokens/ERC20.t.sol +++ b/test/tokens/ERC20.t.sol @@ -48,8 +48,8 @@ contract ERC20Test is Test { function setUp() public { // Read our wrappers - string memory mintable_wrapper = vm.readFile("test/tokens/mocks/ERC20Mintable.huff"); - string memory mock_wrapper = vm.readFile("test/tokens/mocks/MockERC20.huff"); + string memory mintable_wrapper = vm.readFile("test/tokens/mocks/ERC20MintableWrappers.huff"); + string memory mock_wrapper = vm.readFile("test/tokens/mocks/ERC20Wrappers.huff"); // Deploy the Mintable ERC20 vm.startPrank(deployer); diff --git a/test/tokens/ERC721.t.sol b/test/tokens/ERC721.t.sol index 6341d520..19e178db 100644 --- a/test/tokens/ERC721.t.sol +++ b/test/tokens/ERC721.t.sol @@ -1,2 +1,277 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; + +import "forge-std/Test.sol"; +import "foundry-huff/HuffDeployer.sol"; + +import { ERC721TokenReceiver } from "solmate/tokens/ERC721.sol"; + +contract ERC721Recipient is ERC721TokenReceiver { + address public operator; + address public from; + uint256 public id; + bytes public data; + + function onERC721Received( + address _operator, + address _from, + uint256 _id, + bytes calldata _data + ) public virtual override returns (bytes4) { + operator = _operator; + from = _from; + id = _id; + data = _data; + + return ERC721TokenReceiver.onERC721Received.selector; + } +} + +interface IERC721 { + function name() external returns (string memory); + function symbol() external returns (string memory); + function tokenURI(uint256) external returns (string memory); + + function mint(address, uint256) payable external; + function burn(uint256) external; + + function transfer(address, uint256) external; + function transferFrom(address, address, uint256) external; + function safeTransferFrom(address, address, uint256) external; + function safeTransferFrom(address, address, uint256, bytes calldata) external; + + function approve(address, uint256) external; + function setApprovalForAll(address, bool) external; + + function getApproved(uint256) external returns (address); + function isApprovedForAll(address, address) external returns (bool); + function ownerOf(uint256) external returns (address); + function balanceOf(address) external returns (uint256); + function supportsInterface(bytes4) external returns (bool); +} + +contract ERC721Test is Test { + IERC721 token; + + function setUp() public { + // Deploy the ERC721 + string memory wrapper_code = vm.readFile("test/tokens/mocks/ERC721Wrappers.huff"); + token = IERC721(HuffDeployer.config().with_code(wrapper_code).deploy("tokens/ERC721")); + } + + /// @notice Test the ERC721 Metadata + function testMetadata() public { + assertEq(keccak256(abi.encode(token.name())), keccak256(abi.encode("Token"))); + assertEq(keccak256(abi.encode(token.symbol())), keccak256(abi.encode("TKN"))); + assertEq(keccak256(abi.encode(token.tokenURI(1))), keccak256(abi.encode(""))); + } + + function testMint() public { + vm.expectRevert(bytes("INVALID_RECIPIENT")); + token.mint(address(0x0), 1337); + + // We can still mint the token + token.mint(address(0xBEEF), 1337); + + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.ownerOf(1337), address(0xBEEF)); + + // Minting the same token twice should fail + vm.expectRevert(bytes("ALREADY_MINTED")); + token.mint(address(0xBEEF), 1337); + } + + function testBurn() public { + // We can't burn a token we don't have + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0)); + vm.expectRevert(bytes("NOT_MINTED")); + token.burn(1337); + + // Mint a token + token.mint(address(this), 1337); + assertEq(token.balanceOf(address(this)), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(this)); + + // Set approval and then check that it is zeroed out + token.approve(address(0xBEEF), 1337); + assertEq(token.getApproved(1337), address(0xBEEF)); + + // Now we should be able to burn the minted token + token.burn(1337); + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0)); + + vm.expectRevert(bytes("NOT_MINTED")); + token.burn(1337); + } + + function testApprove() public { + token.mint(address(this), 1337); + token.approve(address(0xBEEF), 1337); + assertEq(token.getApproved(1337), address(0xBEEF)); + + token.approve(address(0xCAFE), 1337); + assertEq(token.getApproved(1337), address(0xCAFE)); + } + + function testApproveBurn() public { + token.mint(address(this), 1337); + token.approve(address(0xBEEF), 1337); + token.burn(1337); + + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.getApproved(1337), address(0)); + } + + function testApproveAll() public { + assertFalse(token.isApprovedForAll(address(this), address(0xBEEF))); + token.setApprovalForAll(address(0xBEEF), true); + assertTrue(token.isApprovedForAll(address(this), address(0xBEEF))); + token.setApprovalForAll(address(0xBEEF), false); + assertFalse(token.isApprovedForAll(address(this), address(0xBEEF))); + } + + /// @notice Tests Transfer From + /// @notice sbf is coming to steal your tokens + function testTransferFrom(address sbf) public { + address from = address(0xABCD); + vm.assume(sbf != from); + vm.assume(sbf != address(0)); + + // Mint the from a token + token.mint(from, 1337); + + + // Non owner cannot transfer + vm.expectRevert(bytes("WRONG_FROM")); + token.transferFrom(sbf, address(this), 1337); + + // sbf can't steal our tokens + vm.startPrank(sbf); + vm.expectRevert(bytes("NOT_AUTHORIZED")); + token.transferFrom(from, sbf, 1337); + vm.stopPrank(); + + // Prank from + vm.startPrank(from); + + // Cannot transfer to 0 + vm.expectRevert(bytes("INVALID_RECIPIENT")); + token.transferFrom(from, address(0), 1337); + + // The owner can transfer it! + token.transferFrom(from, address(0xBEEF), 1337); + vm.stopPrank(); + + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(from), 0); + } + + /// @notice Tests Transferring from yourself + function testTransferFromSelf(address minter) public { + vm.assume(minter != address(0)); + + // Mint a token + token.mint(minter, 1337); + + // Transfer from self + vm.prank(minter); + token.transferFrom(minter, address(0xBEEF), 1337); + + // Check that it worked + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(this)), 0); + } + + function testTransferFromApproveAll(address from) public { + vm.assume(from != address(0)); + vm.assume(from != address(0xBEEF)); + + // Mint a token + token.mint(from, 1337); + + // Give this contract approval to transfer + vm.prank(from); + token.setApprovalForAll(address(this), true); + + // The approved address can transfer + token.transferFrom(from, address(0xBEEF), 1337); + + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(from), 0); + } + + function testSafeTransferFromToEOA(address from) public { + vm.assume(from != address(0)); + vm.assume(from != address(0xBEEF)); + + token.mint(from, 1337); + + vm.prank(from); + token.setApprovalForAll(address(this), true); + + token.safeTransferFrom(from, address(0xBEEF), 1337); + + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(from), 0); + } + + function testSafeTransferFromToERC721Recipient(address from) public { + vm.assume(from != address(0)); + + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(from, 1337); + + vm.prank(from); + token.setApprovalForAll(address(this), true); + + token.safeTransferFrom(from, address(recipient), 1337); + + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(from), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), from); + assertEq(recipient.id(), 1337); + assertEq(keccak256(abi.encode(recipient.data())), keccak256(abi.encode(""))); + } + + function testSafeTransferFromToERC721RecipientWithData(address from) public { + vm.assume(from != address(0)); + + ERC721Recipient recipient = new ERC721Recipient(); + vm.assume(from != address(recipient)); + + token.mint(from, 1337); + + vm.prank(from); + token.setApprovalForAll(address(this), true); + + token.safeTransferFrom(from, address(recipient), 1337, "testing 123"); + + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(from), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), from); + assertEq(recipient.id(), 1337); + assertEq(keccak256(abi.encode(recipient.data())), keccak256(abi.encode("testing 123"))); + } +} diff --git a/test/tokens/mocks/ERC1155Wrappers.huff b/test/tokens/mocks/ERC1155Wrappers.huff index 4eaec38f..488b71b0 100644 --- a/test/tokens/mocks/ERC1155Wrappers.huff +++ b/test/tokens/mocks/ERC1155Wrappers.huff @@ -3,48 +3,56 @@ 0x00 calldataload 0xE0 shr // [function selector on stack] dup1 __FUNC_SIG(mint) eq mint jumpi + dup1 __FUNC_SIG(burn) eq burn jumpi + dup1 __FUNC_SIG(batchMint) eq batchMint jumpi + dup1 __FUNC_SIG(batchBurn) eq batchBurn jumpi + dup1 __FUNC_SIG(safeTransferFrom) eq safeTransferFrom jumpi dup1 __FUNC_SIG(safeBatchTransferFrom) eq batchSafeTransferFrom jumpi + + dup1 __FUNC_SIG(isApprovedForAll) eq isApprovedForAll jumpi dup1 __FUNC_SIG(setApprovalForAll) eq setApprovalForAll jumpi - dup1 __FUNC_SIG(burn) eq burn jumpi - dup1 __FUNC_SIG(batchBurn) eq batchBurn jumpi + dup1 __FUNC_SIG(balanceOf) eq balanceOf jumpi - dup1 __FUNC_SIG(isApprovedForAll) eq isApprovedForAll jumpi + dup1 __FUNC_SIG(balanceOfBatch) eq balanceOfBatch jumpi + dup1 __FUNC_SIG(name) eq name jumpi dup1 __FUNC_SIG(symbol) eq symbol jumpi dup1 __FUNC_SIG(supportsInterface) eq supportsInterface jumpi - dup1 __FUNC_SIG(symbol) eq symbol jumpi - dup1 __FUNC_SIG(getName) eq name jumpi - dup1 __FUNC_SIG(balanceOfBatch) eq balanceOfBatch jumpi // No fallback function 0x00 dup1 revert mint: MINT() - safeTransferFrom: - SAFE_TRANSFER_FROM() - batchSafeTransferFrom: - BATCH_SAFE_TRANSFER_FROM() burn: BURN() + batchMint: BATCH_MINT() - setApprovalForAll: - SET_APPROVAL_FOR_ALL() batchBurn: BATCH_BURN() - supportsInterface: - SUPPORTS_INTERFACE() + + safeTransferFrom: + SAFE_TRANSFER_FROM() + batchSafeTransferFrom: + BATCH_SAFE_TRANSFER_FROM() + + isApprovedForAll: + IS_APPROVED_FOR_ALL() + setApprovalForAll: + SET_APPROVAL_FOR_ALL() + balanceOf: BALANCE_OF() balanceOfBatch: BALANCE_OF_BATCH() - isApprovedForAll: - IS_APPROVED_FOR_ALL() + name: NAME() symbol: SYMBOL() + supportsInterface: + SUPPORTS_INTERFACE() } diff --git a/test/tokens/mocks/ERC20Mintable.huff b/test/tokens/mocks/ERC20MintableWrappers.huff similarity index 95% rename from test/tokens/mocks/ERC20Mintable.huff rename to test/tokens/mocks/ERC20MintableWrappers.huff index 5b688cab..a4fc549d 100644 --- a/test/tokens/mocks/ERC20Mintable.huff +++ b/test/tokens/mocks/ERC20MintableWrappers.huff @@ -2,7 +2,7 @@ #define function mint(address, uint256) nonpayable returns () #define function burn(address, uint256) nonpayable returns () -#define macro BURN() = takes(0) returns (0) { +#define macro BURN() = takes (0) returns (0) { NON_PAYABLE() // Setup the stack for the burn function. 0x04 calldataload // [from] @@ -15,7 +15,7 @@ stop // [] } -#define macro MINT() = takes(0) returns (0) { +#define macro MINT() = takes (0) returns (0) { NON_PAYABLE() // Setup the stack for the mint function. diff --git a/test/tokens/mocks/ERC20Wrappers.huff b/test/tokens/mocks/ERC20Wrappers.huff new file mode 100644 index 00000000..08d3027f --- /dev/null +++ b/test/tokens/mocks/ERC20Wrappers.huff @@ -0,0 +1,5 @@ + +#define macro MAIN() = { + 0x00 calldataload 0xE0 shr // [func sig] + MAIN_ERC20() +} diff --git a/test/tokens/mocks/ERC721Wrappers.huff b/test/tokens/mocks/ERC721Wrappers.huff new file mode 100644 index 00000000..61c43d99 --- /dev/null +++ b/test/tokens/mocks/ERC721Wrappers.huff @@ -0,0 +1,81 @@ + +#define function mint(address, uint256) payable returns () +#define function burn(uint256) nonpayable returns () + +#define macro MINT() = takes (0) returns (0) { + 0x24 calldataload // [tokenId] + 0x04 calldataload // [to, tokenId] + _MINT() +} + +#define macro BURN() = takes (0) returns (0) { + 0x04 calldataload // [tokenId] + _BURN() +} + +// Function Dispatch +#define macro MAIN() = takes (0) returns (0) { + 0x00 calldataload 0xE0 shr // [sig] + + // Mint and Burning Functions + dup1 __FUNC_SIG(mint) eq mint_jump jumpi + dup1 __FUNC_SIG(burn) eq burn_jump jumpi + + dup1 __FUNC_SIG(approve) eq approve jumpi + dup1 __FUNC_SIG(setApprovalForAll) eq setApprovalForAll jumpi + + dup1 __FUNC_SIG(transferFrom) eq transferFrom jumpi + dup1 __FUNC_SIG(safeTransferFrom) eq safeTransferFrom jumpi + + dup1 __FUNC_SIG(name) eq name jumpi + dup1 __FUNC_SIG(symbol) eq symbol jumpi + dup1 __FUNC_SIG(tokenURI) eq tokenURI jumpi + dup1 __FUNC_SIG(supportsInterface) eq supportsInterface jumpi + + dup1 __FUNC_SIG(getApproved) eq getApproved jumpi + dup1 __FUNC_SIG(isApprovedForAll) eq isApprovedForAll jumpi + + dup1 __FUNC_SIG(balanceOf) eq balanceOf jumpi + dup1 __FUNC_SIG(ownerOf) eq ownerOf jumpi + + dup1 __FUNC_SIG("safeTransferFrom(address,address,uint256,bytes)") eq safeTransferFromData jumpi + + // Revert on failed dispatch + 0x00 dup1 revert + + mint_jump: + MINT() + burn_jump: + BURN() + + approve: + APPROVE() + setApprovalForAll: + SET_APPROVAL_FOR_ALL() + + transferFrom: + TRANSFER_FROM() + safeTransferFrom: + SAFE_TRANSFER_FROM() + safeTransferFromData: + SAFE_TRANSFER_FROM_WITH_DATA() + + name: + NAME() + symbol: + SYMBOL() + tokenURI: + TOKEN_URI() + supportsInterface: + SUPPORTS_INTERFACE() + + getApproved: + GET_APPROVED() + isApprovedForAll: + IS_APPROVED_FOR_ALL() + + balanceOf: + BALANCE_OF() + ownerOf: + OWNER_OF() +} \ No newline at end of file diff --git a/test/tokens/mocks/MockERC20.huff b/test/tokens/mocks/MockERC20.huff deleted file mode 100644 index c620f27b..00000000 --- a/test/tokens/mocks/MockERC20.huff +++ /dev/null @@ -1,21 +0,0 @@ - -// !! WARNING !! -// This code is meant to be appended to src/tokens/ERC20.huff -// Thus, it does not import any other files - -// Constructor -// #define macro CONSTRUCTOR() { -// // Construct the ERC20 -// CONSTRUCTOR_ERC20() - -// // Mint the sender a supply -// caller // [to] -// 0x100 // [value, to] -// _MINT() // [] -// } - -// Main Entrypoint -#define macro MAIN() = { - 0x00 calldataload 0xE0 shr // [func sig] - MAIN_ERC20() -} diff --git a/test/utils/Errors.t.sol b/test/utils/Errors.t.sol index 2af78a1c..a87311a8 100644 --- a/test/utils/Errors.t.sol +++ b/test/utils/Errors.t.sol @@ -51,7 +51,7 @@ contract ErrorsTest is Test { IErrorsMock eLib; function setUp() public { - string memory wrapper_code = vm.readFile("test/utils/mocks/ErrorsMock.huff"); + string memory wrapper_code = vm.readFile("test/utils/mocks/ErrorWrappers.huff"); eLib = IErrorsMock(HuffDeployer.deploy_with_code("utils/Errors", wrapper_code)); } diff --git a/test/utils/LibBit.t.sol b/test/utils/LibBit.t.sol index be71caa2..c1520d1a 100644 --- a/test/utils/LibBit.t.sol +++ b/test/utils/LibBit.t.sol @@ -16,7 +16,7 @@ contract LibBitTest is Test { function setUp() public { /// @notice deploy a new instance of IJumpTableUtil by /// passing in the address of the deployed Huff contract - string memory wrapper_code = vm.readFile("test/utils/mocks/LibBitWrapper.huff"); + string memory wrapper_code = vm.readFile("test/utils/mocks/LibBitWrappers.huff"); lib = ILibBit(HuffDeployer.deploy_with_code("utils/LibBit", wrapper_code)); } diff --git a/test/utils/ReentrancyGuard.t.sol b/test/utils/ReentrancyGuard.t.sol index 67aa9c71..e5ca2641 100644 --- a/test/utils/ReentrancyGuard.t.sol +++ b/test/utils/ReentrancyGuard.t.sol @@ -15,7 +15,7 @@ contract ReentranctGuardTest is Test { IGuard guard; function setUp() public { - string memory wrapper_code = vm.readFile("test/utils/mocks/ReentrancyGuardMock.huff"); + string memory wrapper_code = vm.readFile("test/utils/mocks/ReentrancyGuardWrappers.huff"); guard = IGuard(HuffDeployer.deploy_with_code("utils/ReentrancyGuard", wrapper_code)); } @@ -45,8 +45,8 @@ contract ReentranctGuardTest is Test { state = guard.state(); assertEq(state, 2); - // vm.expectRevert(bytes4("")); - // guard.lock(); + vm.expectRevert(bytes("REENTRANCY")); + guard.lock(); } } diff --git a/test/utils/mocks/ErrorsMock.huff b/test/utils/mocks/ErrorWrappers.huff similarity index 100% rename from test/utils/mocks/ErrorsMock.huff rename to test/utils/mocks/ErrorWrappers.huff diff --git a/test/utils/mocks/LibBitWrapper.huff b/test/utils/mocks/LibBitWrappers.huff similarity index 100% rename from test/utils/mocks/LibBitWrapper.huff rename to test/utils/mocks/LibBitWrappers.huff diff --git a/test/utils/mocks/MerkleProofLibWrappers.huff b/test/utils/mocks/MerkleProofLibWrappers.huff index 6f5264c7..57e41acb 100644 --- a/test/utils/mocks/MerkleProofLibWrappers.huff +++ b/test/utils/mocks/MerkleProofLibWrappers.huff @@ -17,4 +17,4 @@ verifyProof: VERIFY_PROOF_WRAPPER() -} \ No newline at end of file +} diff --git a/test/utils/mocks/ReentrancyGuardMock.huff b/test/utils/mocks/ReentrancyGuardWrappers.huff similarity index 100% rename from test/utils/mocks/ReentrancyGuardMock.huff rename to test/utils/mocks/ReentrancyGuardWrappers.huff