External calls
External calls reentrancy can be executed by the availability of an external call to an attacker controlled contract. External calls allow for the callee to execute arbitrary code. The existence of an external call may not always be obvious, so it's important to be aware of any way in which an external call may be executed in your smart contracts.
ETH transfers
When Ether is transferred to a contract address, it will trigger the receive
or fallback
function, as implemented in the contract. An attacker can write any arbitrary logic into the fallback
method, such that anytime the contract receives a transfer, that logic is executed.
safeMint
One example of a hard to spot external call is OpenZeppelin's ERC721._safeMint
& ERC721._safeTransfer
functions.
/**
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
*/
function _safeMint(
address to,
uint256 tokenId,
bytes memory _data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, _data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
The function is titled _safeMint
because it prevents tokens from being unintentionally minted to a smart contract by checking first whether that contract has implemented ERC721Receiver, i.e. marking itself as a willing recipient of NFTs. This all seems fine, but _checkOnERC721Received
is an external call to the receiving contract, allowing arbitrary execution.