Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Invalid

Minting of `RAACNFT` is vulnerable to front running attacks

Summary

The mint() function in the RAACNFT contract allows anyone to mint an NFT with any tokenId, as long as enough funds are sent to the contract.
This is a problem, because the NFT should be only minted by users that actually own the underlying real estate.

Vulnerability Details

The RAAC protocol allows users to mint RAACNFTs, which represent ownership of real estate. The idea is that an owner of a house can mint such an NFT with a given tokenId, and based on that tokenId, the protocol will find the price for the underlying asset so it can ask for the necessary collateral amount, which then needs to be transferred by the owner.

Notice how the mint() function first checks the price of the house using the house price oracle, and then ensures that the sender provides the necessary funds to create the NFT for the given tokenId:

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(); }
// transfer erc20 from user to contract - requires pre-approval from user
token.safeTransferFrom(msg.sender, address(this), _amount);
// mint tokenId to user
_safeMint(msg.sender, _tokenId);
// If user approved more than necessary, refund the difference
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.

Impact

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.

Tools Used

Manual review.

Recommendations

The protocol should ensure that the msg.sender of the mint() function is actually the account that should receive the NFT for a given tokenId.
This can be done by requiring a signature from a trusted account (like the owner), which contains the msg.sender address and the tokenId in the hash material. The protocol can then verify that the msg.sender is indeed the right owner of the NFT with the given tokenId.

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);
}
....
}

Relevant Links

Updates

Lead Judging Commences

inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.