diff --git a/contracts/Forwarder.sol b/contracts/Forwarder.sol deleted file mode 100644 index e6a6ae9f9..000000000 --- a/contracts/Forwarder.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -import "@openzeppelin/contracts/metatx/MinimalForwarder.sol"; - -/* - * @dev Minimal forwarder for GSNv2 - */ -contract Forwarder is MinimalForwarder { - // solhint-disable-next-line no-empty-blocks - constructor() MinimalForwarder() {} -} diff --git a/contracts/base/ERC1155LazyMint.sol b/contracts/base/ERC1155LazyMint.sol index ace35bb15..feeb7dcc4 100644 --- a/contracts/base/ERC1155LazyMint.sol +++ b/contracts/base/ERC1155LazyMint.sol @@ -9,6 +9,7 @@ import "../extension/Ownable.sol"; import "../extension/Royalty.sol"; import "../extension/BatchMintMetadata.sol"; import "../extension/LazyMint.sol"; +import "../extension/interface/IClaimableERC1155.sol"; import "../lib/TWStrings.sol"; @@ -44,7 +45,16 @@ import "../lib/TWStrings.sol"; * */ -contract ERC1155LazyMint is ERC1155, ContractMetadata, Ownable, Royalty, Multicall, BatchMintMetadata, LazyMint { +contract ERC1155LazyMint is + ERC1155, + ContractMetadata, + Ownable, + Royalty, + Multicall, + BatchMintMetadata, + LazyMint, + IClaimableERC1155 +{ using TWStrings for uint256; /*////////////////////////////////////////////////////////////// @@ -106,6 +116,7 @@ contract ERC1155LazyMint is ERC1155, ContractMetadata, Ownable, Royalty, Multica require(_tokenId < nextTokenIdToMint(), "invalid id"); _mint(_receiver, _tokenId, _quantity, ""); + emit TokensClaimed(msg.sender, _receiver, _tokenId, _quantity); } /** diff --git a/contracts/base/ERC721LazyMint.sol b/contracts/base/ERC721LazyMint.sol index fa224f603..59cde1c00 100644 --- a/contracts/base/ERC721LazyMint.sol +++ b/contracts/base/ERC721LazyMint.sol @@ -9,6 +9,7 @@ import "../extension/Ownable.sol"; import "../extension/Royalty.sol"; import "../extension/BatchMintMetadata.sol"; import "../extension/LazyMint.sol"; +import "../extension/interface/IClaimableERC721.sol"; import "../lib/TWStrings.sol"; @@ -38,7 +39,16 @@ import "../lib/TWStrings.sol"; * without paying the gas cost for actually minting the NFTs. */ -contract ERC721LazyMint is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, BatchMintMetadata, LazyMint { +contract ERC721LazyMint is + ERC721A, + ContractMetadata, + Multicall, + Ownable, + Royalty, + BatchMintMetadata, + LazyMint, + IClaimableERC721 +{ using TWStrings for uint256; /*////////////////////////////////////////////////////////////// @@ -99,9 +109,10 @@ contract ERC721LazyMint is ERC721A, ContractMetadata, Multicall, Ownable, Royalt */ function claim(address _receiver, uint256 _quantity) public payable virtual { verifyClaim(msg.sender, _quantity); // add your claim verification logic by overriding this function - + uint256 startTokenId = _currentIndex; require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "Not enough lazy minted tokens."); _safeMint(_receiver, _quantity, ""); + emit TokensClaimed(msg.sender, _receiver, startTokenId, _quantity); } /** diff --git a/contracts/extension/interface/IClaimableERC1155.sol b/contracts/extension/interface/IClaimableERC1155.sol new file mode 100644 index 000000000..a622ab778 --- /dev/null +++ b/contracts/extension/interface/IClaimableERC1155.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IClaimableERC1155 { + /// @dev Emitted when tokens are claimed + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed tokenId, + uint256 quantityClaimed + ); + + /** + * @notice Lets an address claim multiple lazy minted NFTs at once to a recipient. + * Contract creators should override this function to create custom logic for claiming, + * for e.g. price collection, allowlist, max quantity, etc. + * + * @dev The logic in the `verifyClaim` function determines whether the caller is authorized to mint NFTs. + * + * @param _receiver The recipient of the tokens to mint. + * @param _tokenId The tokenId of the lazy minted NFT to mint. + * @param _quantity The number of tokens to mint. + */ + function claim( + address _receiver, + uint256 _tokenId, + uint256 _quantity + ) external payable; + + /** + * @notice Override this function to add logic for claim verification, based on conditions + * such as allowlist, price, max quantity etc. + * + * @dev Checks a request to claim NFTs against a custom condition. + * + * @param _claimer Caller of the claim function. + * @param _tokenId The tokenId of the lazy minted NFT to mint. + * @param _quantity The number of NFTs being claimed. + */ + function verifyClaim( + address _claimer, + uint256 _tokenId, + uint256 _quantity + ) external view; +} diff --git a/contracts/extension/interface/IClaimableERC721.sol b/contracts/extension/interface/IClaimableERC721.sol new file mode 100644 index 000000000..546da08b2 --- /dev/null +++ b/contracts/extension/interface/IClaimableERC721.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IClaimableERC721 { + /// @dev Emitted when tokens are claimed + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed startTokenId, + uint256 quantityClaimed + ); + + /** + * @notice Lets an address claim multiple lazy minted NFTs at once to a recipient. + * Contract creators should override this function to create custom logic for claiming, + * for e.g. price collection, allowlist, max quantity, etc. + * + * @dev The logic in the `verifyClaim` function determines whether the caller is authorized to mint NFTs. + * + * @param _receiver The recipient of the NFT to mint. + * @param _quantity The number of NFTs to mint. + */ + function claim(address _receiver, uint256 _quantity) external payable; + + /** + * @notice Override this function to add logic for claim verification, based on conditions + * such as allowlist, price, max quantity etc. + * + * @dev Checks a request to claim NFTs against a custom condition. + * + * @param _claimer Caller of the claim function. + * @param _quantity The number of NFTs being claimed. + */ + function verifyClaim(address _claimer, uint256 _quantity) external view; +} diff --git a/contracts/forwarder/Forwarder.sol b/contracts/forwarder/Forwarder.sol new file mode 100644 index 000000000..6a4f2bd0b --- /dev/null +++ b/contracts/forwarder/Forwarder.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; + +/* + * @dev Minimal forwarder for GSNv2 + */ +contract Forwarder is EIP712 { + using ECDSA for bytes32; + + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + } + + bytes32 private constant TYPEHASH = + keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)"); + + mapping(address => uint256) private _nonces; + + constructor() EIP712("GSNv2 Forwarder", "0.0.1") {} + + function getNonce(address from) public view returns (uint256) { + return _nonces[from]; + } + + function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { + address signer = _hashTypedDataV4( + keccak256(abi.encode(TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) + ).recover(signature); + + return _nonces[req.from] == req.nonce && signer == req.from; + } + + function execute(ForwardRequest calldata req, bytes calldata signature) + public + payable + returns (bool, bytes memory) + { + require(verify(req, signature), "MinimalForwarder: signature does not match request"); + _nonces[req.from] = req.nonce + 1; + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = req.to.call{ gas: req.gas, value: req.value }( + abi.encodePacked(req.data, req.from) + ); + + if (!success) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert("Transaction reverted silently"); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + // Check gas: https://ronan.eth.link/blog/ethereum-gas-dangers/ + assert(gasleft() > req.gas / 63); + return (success, result); + } +} diff --git a/contracts/forwarder/ForwarderChainlessDomain.sol b/contracts/forwarder/ForwarderChainlessDomain.sol new file mode 100644 index 000000000..75f69a4a2 --- /dev/null +++ b/contracts/forwarder/ForwarderChainlessDomain.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (metatx/MinimalForwarder.sol) + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../openzeppelin-presets/cryptography/EIP712ChainlessDomain.sol"; + +/** + * @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}. + */ +contract ForwarderChainlessDomain is EIP712ChainlessDomain { + using ECDSA for bytes32; + + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + uint256 chainid; + } + + bytes32 private constant _TYPEHASH = + keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 chainid)" + ); + + mapping(address => uint256) private _nonces; + + constructor() EIP712ChainlessDomain("GSNv2 Forwarder", "0.0.1") {} + + function getNonce(address from) public view returns (uint256) { + return _nonces[from]; + } + + function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { + address signer = _hashTypedDataV4( + keccak256( + abi.encode( + _TYPEHASH, + req.from, + req.to, + req.value, + req.gas, + req.nonce, + keccak256(req.data), + block.chainid + ) + ) + ).recover(signature); + return _nonces[req.from] == req.nonce && signer == req.from; + } + + function execute(ForwardRequest calldata req, bytes calldata signature) + public + payable + returns (bool, bytes memory) + { + // require(req.chainid == block.chainid, "MinimalForwarder: invalid chainId"); + require(verify(req, signature), "MinimalForwarder: signature does not match request"); + _nonces[req.from] = req.nonce + 1; + + (bool success, bytes memory returndata) = req.to.call{ gas: req.gas, value: req.value }( + abi.encodePacked(req.data, req.from) + ); + + // Validate that the relayer has sent enough gas for the call. + // See https://ronan.eth.link/blog/ethereum-gas-dangers/ + if (gasleft() <= req.gas / 63) { + // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since + // neither revert or assert consume all gas since Solidity 0.8.0 + // https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require + assembly { + invalid() + } + } + + return (success, returndata); + } +} diff --git a/contracts/forwarder/ForwarderConsumer.sol b/contracts/forwarder/ForwarderConsumer.sol new file mode 100644 index 000000000..a8c9985d8 --- /dev/null +++ b/contracts/forwarder/ForwarderConsumer.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; + +contract ForwarderConsumer is ERC2771Context { + address public caller; + + constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {} + + function setCaller() external { + caller = _msgSender(); + } +} diff --git a/contracts/ForwarderEOAOnly.sol b/contracts/forwarder/ForwarderEOAOnly.sol similarity index 79% rename from contracts/ForwarderEOAOnly.sol rename to contracts/forwarder/ForwarderEOAOnly.sol index 6a4b26f1d..59c8d8027 100644 --- a/contracts/ForwarderEOAOnly.sol +++ b/contracts/forwarder/ForwarderEOAOnly.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "./openzeppelin-presets/metatx/MinimalForwarderEOAOnly.sol"; +import "../openzeppelin-presets/metatx/MinimalForwarderEOAOnly.sol"; /* * @dev Minimal forwarder for GSNv2 diff --git a/contracts/openzeppelin-presets/cryptography/EIP712ChainlessDomain.sol b/contracts/openzeppelin-presets/cryptography/EIP712ChainlessDomain.sol new file mode 100644 index 000000000..f5be7dd7c --- /dev/null +++ b/contracts/openzeppelin-presets/cryptography/EIP712ChainlessDomain.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/cryptography/draft-EIP712.sol) + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, + * thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding + * they need in their contracts using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * _Available since v3.4._ + */ +abstract contract EIP712ChainlessDomain { + /* solhint-disable var-name-mixedcase */ + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + address private immutable _CACHED_THIS; + + bytes32 private immutable _HASHED_NAME; + bytes32 private immutable _HASHED_VERSION; + bytes32 private immutable _TYPE_HASH; + + /* solhint-enable var-name-mixedcase */ + + /** + * @dev Initializes the domain separator and parameter caches. + * + * The meaning of `name` and `version` is specified in + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: + * + * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. + * - `version`: the current major version of the signing domain. + * + * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart + * contract upgrade]. + */ + constructor(string memory name, string memory version) { + bytes32 hashedName = keccak256(bytes(name)); + bytes32 hashedVersion = keccak256(bytes(version)); + bytes32 typeHash = keccak256("EIP712Domain(string name,string version,address verifyingContract)"); + _HASHED_NAME = hashedName; + _HASHED_VERSION = hashedVersion; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion); + _CACHED_THIS = address(this); + _TYPE_HASH = typeHash; + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _CACHED_THIS) { + return _CACHED_DOMAIN_SEPARATOR; + } else { + return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); + } + } + + function _buildDomainSeparator( + bytes32 typeHash, + bytes32 nameHash, + bytes32 versionHash + ) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, versionHash, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); + } +} diff --git a/contracts/package.json b/contracts/package.json index cc5088d2b..cfc13f20e 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@thirdweb-dev/contracts", "description": "Collection of smart contracts deployable via the thirdweb SDK, dashboard and CLI", - "version": "3.1.6-1", + "version": "3.1.6", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/docs/EIP712ChainlessDomain.md b/docs/EIP712ChainlessDomain.md new file mode 100644 index 000000000..4cf54a996 --- /dev/null +++ b/docs/EIP712ChainlessDomain.md @@ -0,0 +1,12 @@ +# EIP712ChainlessDomain + + + + + + + +*https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in their contracts using a combination of `abi.encode` and `keccak256`. This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA ({_hashTypedDataV4}). The implementation of the domain separator was designed to be as efficient as possible while still properly updating the chain id to protect against replay attacks on an eventual fork of the chain. NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. _Available since v3.4._* + + + diff --git a/docs/ERC1155DelayedReveal.md b/docs/ERC1155DelayedReveal.md index fa673cdf6..dc5b88b65 100644 --- a/docs/ERC1155DelayedReveal.md +++ b/docs/ERC1155DelayedReveal.md @@ -797,6 +797,25 @@ event TokenURIRevealed(uint256 indexed index, string revealedURI) | index `indexed` | uint256 | undefined | | revealedURI | string | undefined | +### TokensClaimed + +```solidity +event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed tokenId, uint256 quantityClaimed) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| claimer `indexed` | address | undefined | +| receiver `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | +| quantityClaimed | uint256 | undefined | + ### TokensLazyMinted ```solidity diff --git a/docs/ERC1155LazyMint.md b/docs/ERC1155LazyMint.md index b7adeda92..8bd615d13 100644 --- a/docs/ERC1155LazyMint.md +++ b/docs/ERC1155LazyMint.md @@ -667,6 +667,25 @@ event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, | royaltyRecipient `indexed` | address | undefined | | royaltyBps | uint256 | undefined | +### TokensClaimed + +```solidity +event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed tokenId, uint256 quantityClaimed) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| claimer `indexed` | address | undefined | +| receiver `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | +| quantityClaimed | uint256 | undefined | + ### TokensLazyMinted ```solidity diff --git a/docs/ERC721DelayedReveal.md b/docs/ERC721DelayedReveal.md index 38ffed5c7..8da60102b 100644 --- a/docs/ERC721DelayedReveal.md +++ b/docs/ERC721DelayedReveal.md @@ -857,6 +857,25 @@ event TokenURIRevealed(uint256 indexed index, string revealedURI) | index `indexed` | uint256 | undefined | | revealedURI | string | undefined | +### TokensClaimed + +```solidity +event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed startTokenId, uint256 quantityClaimed) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| claimer `indexed` | address | undefined | +| receiver `indexed` | address | undefined | +| startTokenId `indexed` | uint256 | undefined | +| quantityClaimed | uint256 | undefined | + ### TokensLazyMinted ```solidity diff --git a/docs/ERC721LazyMint.md b/docs/ERC721LazyMint.md index 34a5158ff..1ca3a173c 100644 --- a/docs/ERC721LazyMint.md +++ b/docs/ERC721LazyMint.md @@ -727,6 +727,25 @@ event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, | royaltyRecipient `indexed` | address | undefined | | royaltyBps | uint256 | undefined | +### TokensClaimed + +```solidity +event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed startTokenId, uint256 quantityClaimed) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| claimer `indexed` | address | undefined | +| receiver `indexed` | address | undefined | +| startTokenId `indexed` | uint256 | undefined | +| quantityClaimed | uint256 | undefined | + ### TokensLazyMinted ```solidity diff --git a/docs/Forwarder.md b/docs/Forwarder.md index c0488f9b8..f6fb0860f 100644 --- a/docs/Forwarder.md +++ b/docs/Forwarder.md @@ -13,7 +13,7 @@ ### execute ```solidity -function execute(MinimalForwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes) +function execute(Forwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes) ``` @@ -24,7 +24,7 @@ function execute(MinimalForwarder.ForwardRequest req, bytes signature) external | Name | Type | Description | |---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined | +| req | Forwarder.ForwardRequest | undefined | | signature | bytes | undefined | #### Returns @@ -59,7 +59,7 @@ function getNonce(address from) external view returns (uint256) ### verify ```solidity -function verify(MinimalForwarder.ForwardRequest req, bytes signature) external view returns (bool) +function verify(Forwarder.ForwardRequest req, bytes signature) external view returns (bool) ``` @@ -70,7 +70,7 @@ function verify(MinimalForwarder.ForwardRequest req, bytes signature) external v | Name | Type | Description | |---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined | +| req | Forwarder.ForwardRequest | undefined | | signature | bytes | undefined | #### Returns diff --git a/docs/MinimalForwarder.md b/docs/ForwarderChainlessDomain.md similarity index 69% rename from docs/MinimalForwarder.md rename to docs/ForwarderChainlessDomain.md index b37c70303..09d1d173a 100644 --- a/docs/MinimalForwarder.md +++ b/docs/ForwarderChainlessDomain.md @@ -1,4 +1,4 @@ -# MinimalForwarder +# ForwarderChainlessDomain @@ -13,7 +13,7 @@ ### execute ```solidity -function execute(MinimalForwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes) +function execute(ForwarderChainlessDomain.ForwardRequest req, bytes signature) external payable returns (bool, bytes) ``` @@ -24,7 +24,7 @@ function execute(MinimalForwarder.ForwardRequest req, bytes signature) external | Name | Type | Description | |---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined | +| req | ForwarderChainlessDomain.ForwardRequest | undefined | | signature | bytes | undefined | #### Returns @@ -59,7 +59,7 @@ function getNonce(address from) external view returns (uint256) ### verify ```solidity -function verify(MinimalForwarder.ForwardRequest req, bytes signature) external view returns (bool) +function verify(ForwarderChainlessDomain.ForwardRequest req, bytes signature) external view returns (bool) ``` @@ -70,7 +70,7 @@ function verify(MinimalForwarder.ForwardRequest req, bytes signature) external v | Name | Type | Description | |---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined | +| req | ForwarderChainlessDomain.ForwardRequest | undefined | | signature | bytes | undefined | #### Returns diff --git a/docs/ForwarderConsumer.md b/docs/ForwarderConsumer.md new file mode 100644 index 000000000..db575554e --- /dev/null +++ b/docs/ForwarderConsumer.md @@ -0,0 +1,65 @@ +# ForwarderConsumer + + + + + + + + + +## Methods + +### caller + +```solidity +function caller() external view returns (address) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +### isTrustedForwarder + +```solidity +function isTrustedForwarder(address forwarder) external view returns (bool) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| forwarder | address | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + +### setCaller + +```solidity +function setCaller() external nonpayable +``` + + + + + + + + + diff --git a/docs/IClaimableERC1155.md b/docs/IClaimableERC1155.md new file mode 100644 index 000000000..fc6c93379 --- /dev/null +++ b/docs/IClaimableERC1155.md @@ -0,0 +1,73 @@ +# IClaimableERC1155 + + + + + + + + + +## Methods + +### claim + +```solidity +function claim(address _receiver, uint256 _tokenId, uint256 _quantity) external payable +``` + +Lets an address claim multiple lazy minted NFTs at once to a recipient. Contract creators should override this function to create custom logic for claiming, for e.g. price collection, allowlist, max quantity, etc. + +*The logic in the `verifyClaim` function determines whether the caller is authorized to mint NFTs.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _receiver | address | The recipient of the tokens to mint. | +| _tokenId | uint256 | The tokenId of the lazy minted NFT to mint. | +| _quantity | uint256 | The number of tokens to mint. | + +### verifyClaim + +```solidity +function verifyClaim(address _claimer, uint256 _tokenId, uint256 _quantity) external view +``` + +Override this function to add logic for claim verification, based on conditions such as allowlist, price, max quantity etc. + +*Checks a request to claim NFTs against a custom condition.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _claimer | address | Caller of the claim function. | +| _tokenId | uint256 | The tokenId of the lazy minted NFT to mint. | +| _quantity | uint256 | The number of NFTs being claimed. | + + + +## Events + +### TokensClaimed + +```solidity +event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed tokenId, uint256 quantityClaimed) +``` + + + +*Emitted when tokens are claimed* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| claimer `indexed` | address | undefined | +| receiver `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | +| quantityClaimed | uint256 | undefined | + + + diff --git a/docs/IClaimableERC721.md b/docs/IClaimableERC721.md new file mode 100644 index 000000000..b7a7f6d65 --- /dev/null +++ b/docs/IClaimableERC721.md @@ -0,0 +1,71 @@ +# IClaimableERC721 + + + + + + + + + +## Methods + +### claim + +```solidity +function claim(address _receiver, uint256 _quantity) external payable +``` + +Lets an address claim multiple lazy minted NFTs at once to a recipient. Contract creators should override this function to create custom logic for claiming, for e.g. price collection, allowlist, max quantity, etc. + +*The logic in the `verifyClaim` function determines whether the caller is authorized to mint NFTs.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _receiver | address | The recipient of the NFT to mint. | +| _quantity | uint256 | The number of NFTs to mint. | + +### verifyClaim + +```solidity +function verifyClaim(address _claimer, uint256 _quantity) external view +``` + +Override this function to add logic for claim verification, based on conditions such as allowlist, price, max quantity etc. + +*Checks a request to claim NFTs against a custom condition.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _claimer | address | Caller of the claim function. | +| _quantity | uint256 | The number of NFTs being claimed. | + + + +## Events + +### TokensClaimed + +```solidity +event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed startTokenId, uint256 quantityClaimed) +``` + + + +*Emitted when tokens are claimed* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| claimer `indexed` | address | undefined | +| receiver `indexed` | address | undefined | +| startTokenId `indexed` | uint256 | undefined | +| quantityClaimed | uint256 | undefined | + + + diff --git a/src/test/Forwarder.t.sol b/src/test/Forwarder.t.sol new file mode 100644 index 000000000..575ad2735 --- /dev/null +++ b/src/test/Forwarder.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/forwarder/Forwarder.sol"; +import "contracts/forwarder/ForwarderConsumer.sol"; + +import "./utils/BaseTest.sol"; + +contract ForwarderTest is BaseTest { + ForwarderConsumer public consumer; + + uint256 public userPKey = 1020; + address public user; + address public relayer = address(0x4567); + + bytes32 internal typehashForwardRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + user = vm.addr(userPKey); + consumer = new ForwarderConsumer(forwarder); + + typehashForwardRequest = keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)" + ); + nameHash = keccak256(bytes("GSNv2 Forwarder")); + versionHash = keccak256(bytes("0.0.1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, forwarder)); + + vm.label(user, "End user"); + vm.label(forwarder, "Forwarder"); + vm.label(relayer, "Relayer"); + vm.label(address(consumer), "Consumer"); + } + + /*/////////////////////////////////////////////////////////////// + Regular `Forwarder`: chainId in typehash + //////////////////////////////////////////////////////////////*/ + + function signForwarderRequest(Forwarder.ForwardRequest memory forwardRequest, uint256 privateKey) + internal + returns (bytes memory) + { + bytes memory encodedRequest = abi.encode( + typehashForwardRequest, + forwardRequest.from, + forwardRequest.to, + forwardRequest.value, + forwardRequest.gas, + forwardRequest.nonce, + keccak256(forwardRequest.data) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + bytes memory signature = abi.encodePacked(r, s, v); + + return signature; + } + + function test_state_forwarder() public { + Forwarder.ForwardRequest memory forwardRequest; + + forwardRequest.from = user; + forwardRequest.to = address(consumer); + forwardRequest.value = 0; + forwardRequest.gas = 100_000; + forwardRequest.nonce = Forwarder(forwarder).getNonce(user); + forwardRequest.data = abi.encodeCall(ForwarderConsumer.setCaller, ()); + + bytes memory signature = signForwarderRequest(forwardRequest, userPKey); + vm.prank(relayer); + Forwarder(forwarder).execute(forwardRequest, signature); + + assertEq(consumer.caller(), user); + } +} diff --git a/src/test/ForwarderChainlessDomain.t.sol b/src/test/ForwarderChainlessDomain.t.sol new file mode 100644 index 000000000..e7b131a6f --- /dev/null +++ b/src/test/ForwarderChainlessDomain.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/forwarder/ForwarderConsumer.sol"; +import "contracts/forwarder/ForwarderChainlessDomain.sol"; + +import "./utils/BaseTest.sol"; + +contract ForwarderChainlessDomainTest is BaseTest { + address public forwarderChainlessDomain; + ForwarderConsumer public consumer; + + uint256 public userPKey = 1020; + address public user; + address public relayer = address(0x4567); + + bytes32 internal typehashForwardRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + user = vm.addr(userPKey); + consumer = new ForwarderConsumer(forwarder); + + forwarderChainlessDomain = address(new ForwarderChainlessDomain()); + consumer = new ForwarderConsumer(forwarderChainlessDomain); + + typehashForwardRequest = keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 chainid)" + ); + nameHash = keccak256(bytes("GSNv2 Forwarder")); + versionHash = keccak256(bytes("0.0.1")); + typehashEip712 = keccak256("EIP712Domain(string name,string version,address verifyingContract)"); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, forwarderChainlessDomain)); + + vm.label(user, "End user"); + vm.label(forwarder, "Forwarder"); + vm.label(relayer, "Relayer"); + vm.label(address(consumer), "Consumer"); + } + + /*/////////////////////////////////////////////////////////////// + Updated `Forwarder`: chainId in ForwardRequest, not typehash. + //////////////////////////////////////////////////////////////*/ + + function signForwarderRequest(ForwarderChainlessDomain.ForwardRequest memory forwardRequest, uint256 privateKey) + internal + returns (bytes memory) + { + bytes memory encodedRequest = abi.encode( + typehashForwardRequest, + forwardRequest.from, + forwardRequest.to, + forwardRequest.value, + forwardRequest.gas, + forwardRequest.nonce, + keccak256(forwardRequest.data), + forwardRequest.chainid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + bytes memory signature = abi.encodePacked(r, s, v); + + return signature; + } + + function test_state_forwarderChainlessDomain() public { + ForwarderChainlessDomain.ForwardRequest memory forwardRequest; + + forwardRequest.from = user; + forwardRequest.to = address(consumer); + forwardRequest.value = 0; + forwardRequest.gas = 100_000; + forwardRequest.nonce = ForwarderChainlessDomain(forwarderChainlessDomain).getNonce(user); + forwardRequest.data = abi.encodeCall(ForwarderConsumer.setCaller, ()); + forwardRequest.chainid = block.chainid; + + bytes memory signature = signForwarderRequest(forwardRequest, userPKey); + vm.prank(relayer); + ForwarderChainlessDomain(forwarderChainlessDomain).execute(forwardRequest, signature); + + assertEq(consumer.caller(), user); + } +} diff --git a/src/test/sdk/base/BaseUtilTest.sol b/src/test/sdk/base/BaseUtilTest.sol index 52ff6f845..b99f2ec93 100644 --- a/src/test/sdk/base/BaseUtilTest.sol +++ b/src/test/sdk/base/BaseUtilTest.sol @@ -8,7 +8,7 @@ import "../../mocks/WETH9.sol"; import "../../mocks/MockERC20.sol"; import "../../mocks/MockERC721.sol"; import "../../mocks/MockERC1155.sol"; -import "contracts/Forwarder.sol"; +import "contracts/forwarder/Forwarder.sol"; import "contracts/lib/TWStrings.sol"; import "contracts/mock/Mock.sol"; diff --git a/src/test/utils/BaseTest.sol b/src/test/utils/BaseTest.sol index c7ca91147..17a4703ac 100644 --- a/src/test/utils/BaseTest.sol +++ b/src/test/utils/BaseTest.sol @@ -9,7 +9,7 @@ import "../mocks/WETH9.sol"; import "../mocks/MockERC20.sol"; import "../mocks/MockERC721.sol"; import "../mocks/MockERC1155.sol"; -import { Forwarder } from "contracts/Forwarder.sol"; +import "contracts/forwarder/Forwarder.sol"; import "contracts/TWRegistry.sol"; import "contracts/TWFactory.sol"; import { Multiwrap } from "contracts/multiwrap/Multiwrap.sol";