function mint(uint256 _tokenId, uint256 _amount) public override {
uint256 price = raac_hp.tokenToHousePrice(_tokenId);
if(price == 0) { revert RAACNFT__HousePrice(); }
if(price > _amount) { revert RAACNFT__InsufficientFundsMint(); }
token.safeTransferFrom(msg.sender, address(this), _amount);
_safeMint(msg.sender, _tokenId);
if (_amount > price) {
uint256 refundAmount = _amount - price;
token.safeTransfer(msg.sender, refundAmount);
}
emit NFTMinted(msg.sender, _tokenId, price);
}
The problem with this, however, is that there's no mechanism that ensures that the underlying asset, or the house for which the price is being queried using the tokenId
, is actually owned by the msg.sender
. This allows anyone to mint the RAACNFT
for any house as long as the necessary funds are provided.
Malicious accounts could prevent benign users from making use of the protocol by "blocking" the NFTs that belong to them.
They have to pay the price for each instance though, meaning this would at most result in a griefing attack.
Manual review.
Below is an example of what this could look like using EIP712 Typed Data Signing:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../../interfaces/core/oracles/IRAACHousePrices.sol";
import "../../interfaces/core/tokens/IRAACNFT.sol";
contract RAACNFT is ERC721, ERC721Enumerable, Ownable, IRAACNFT {
using SafeERC20 for IERC20;
+ using ECDSA for bytes32;
IERC20 public token;
IRAACHousePrices public raac_hp;
+ address public signer; // Address authorized to sign minting permissions
uint256 public currentBatchSize = 3;
+ mapping(bytes32 => bool) public usedSignatures;
string public baseURI = "ipfs://QmZzEbTnUWs5JDzrLKQ9yGk1kvszdnwdMaVw9vNgjCFLo2/";
+ error InvalidSignature();
+ error SignatureAlreadyUsed();
+ error SignatureExpired();
+ error UnauthorizedSigner();
constructor(
address _token,
address _housePrices,
address initialOwner,
address _signer
) ERC721("RAAC NFT", "RAACNFT") Ownable(initialOwner) {
if (_token == address(0) ||
_housePrices == address(0) ||
initialOwner == address(0) ||
+ _signer == address(0)
) revert RAACNFT__InvalidAddress();
token = IERC20(_token);
raac_hp = IRAACHousePrices(_housePrices);
+ signer = _signer;
}
+ function _domainSeparator() internal view returns (bytes32) {
+ return keccak256(
+ abi.encode(
+ keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
+ keccak256(bytes("RAAC NFT")),
+ keccak256(bytes("1")),
+ block.chainid,
+ address(this)
+ )
+ );
+ }
+ function _hashTypedDataV4(
+ address owner,
+ uint256 tokenId,
+ uint256 deadline
+ ) internal view returns (bytes32) {
+ bytes32 PERMIT_TYPEHASH = keccak256(
+ "NFTMintPermit(address owner,uint256 tokenId,uint256 deadline)"
+ );
+
+ bytes32 structHash = keccak256(
+ abi.encode(
+ PERMIT_TYPEHASH,
+ owner,
+ tokenId,
+ deadline
+ )
+ );
+
+ return keccak256(
+ abi.encodePacked(
+ "\x19\x01",
+ _domainSeparator(),
+ structHash
+ )
+ );
+ }
/**
* @notice Mints a new NFT with signature verification
* @param tokenId The token ID to mint
* @param amount The amount of tokens to pay
+ * @param deadline The timestamp until which the signature is valid
+ * @param signature The EIP-712 signature authorizing the mint
*/
function mint(
uint256 tokenId,
uint256 amount,
+ uint256 deadline,
+ bytes calldata signature
) public override {
+ // Check deadline
+ if (block.timestamp > deadline) revert SignatureExpired();
+ // Verify signature
+ bytes32 digest = _hashTypedDataV4(msg.sender, tokenId, deadline);
+ if (usedSignatures[digest]) revert SignatureAlreadyUsed();
+ address recoveredSigner = digest.recover(signature);
+ if (recoveredSigner != signer) revert UnauthorizedSigner();
+ // Mark signature as used
+ usedSignatures[digest] = true;
// Verify price and amount
uint256 price = raac_hp.tokenToHousePrice(tokenId);
if (price == 0) revert RAACNFT__HousePrice();
if (price > amount) revert RAACNFT__InsufficientFundsMint();
// Transfer tokens
token.safeTransferFrom(msg.sender, address(this), amount);
// Mint NFT
_safeMint(msg.sender, tokenId);
// Refund excess payment
if (amount > price) {
uint256 refundAmount = amount - price;
token.safeTransfer(msg.sender, refundAmount);
}
emit NFTMinted(msg.sender, tokenId, price);
}
....
}