Core Contracts

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

Reentrancy Attack via onERC721Received Callback

Author Revealed upon completion

Summary

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 RAACNFT contract’s mint() function calls _safeMint(), which invokes onERC721Received when sending the NFT to a contract address. Because onERC721Received is an external callback, malicious contracts can use this hook to re-enter the mint() function (or other vulnerable state-changing functions). Under specific conditions, this can lead to double spends, duplicated refunds, or other unintended state modifications if the logic in mint() is not protected.

Vulnerability Details

  • Entry Point: The _safeMint() function triggers a callback (onERC721Received) on the recipient contract if it’s not an externally owned account (EOA).

  • Reentrancy: During this callback, the malicious recipient contract can re-invoke mint() (or other methods), potentially exploiting intermediate states (e.g., partially updated variables, incomplete refunds) to gain additional benefits.

  • Limited Safeguards: The code does not currently employ a nonReentrant modifier or an equivalent mechanism, leaving it open to reentrancy.

Impact

  • Double Minting or Double Refund: Attackers could attempt to mint more than once, or receive multiple refunds if the contract’s state logic is not properly updated before the callback.

  • State Manipulation: Partial updates in the mint() function might be exploited, leading to inaccurate balances or unexpected token distributions.

  • Financial & Logic Risks: Depending on the severity of the reentered logic, attackers could obtain extra NFTs, hijack or inflate refunds, or otherwise disrupt the expected behavior of the system.

Recommendations

  1. Use a Reentrancy Guard

    • Apply nonReentrant (e.g., from OpenZeppelin’s ReentrancyGuard) to the mint() function, preventing nested calls during execution.

  2. Checks-Effects-Interactions

    • Update contract state fully (e.g., finalize user balances, deduct refunds) before making external calls (_safeMint).

  3. Consider Restricting _safeMint

    • If your use case permits, consider using _mint() or restricting the NFT recipient to EOAs only. Reducing or disallowing contract recipients minimizes reentrancy vectors.

  4. Comprehensive Testing

    • Implement unit tests and fuzz testing that simulate malicious onERC721Received callbacks to confirm the fix.

With these measures, the contract can better protect itself against reentrancy attacks initiated through ERC721’s safe transfer hooks.

string public baseURI = "ipfs://QmZzEbTnUWs5JDzrLKQ9yGk1kvszdnwdMaVw9vNgjCFLo2/";
- constructor(address _token, address _housePrices, address initialOwner) ERC721("RAAC NFT", "RAACNFT") Ownable(initialOwner) {
+contract RAACNFT is ERC721, ERC721Enumerable, Ownable, ReentrancyGuard, IRAACNFT {
+ constructor(address _token, address _housePrices, address initialOwner)
+ ERC721("RAAC NFT", "RAACNFT")
+ Ownable(initialOwner)
+ ReentrancyGuard()
+ {
if (_token == address(0) || _housePrices == address(0) || initialOwner == address(0)) revert RAACNFT__InvalidAddress();
token = IERC20(_token);
raac_hp = IRAACHousePrices(_housePrices);
@@ -38,7 +40,7 @@ contract RAACNFT is ERC721, ERC721Enumerable, Ownable, IRAACNFT {
}
// Add nonReentrant to prevent reentrancy attacks via _safeMint() callback
- function mint(uint256 _tokenId, uint256 _amount) public override {
+ function mint(uint256 _tokenId, uint256 _amount) public override nonReentrant {
uint256 price = raac_hp.tokenToHousePrice(_tokenId);
if(price == 0) { revert RAACNFT__HousePrice(); }
if(price > _amount) { revert RAACNFT__InsufficientFundsMint(); }
Updates

Lead Judging Commences

inallhonesty Lead Judge 8 days ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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