Manual review, Foundry.
Before fixing this issue, first you need to make a design choice on how NFT minting (i.e. tokenId
distribution) is supposed to be handled. Any of the 3 scenarios is a viable option if then the logic for assiging tokenURI
s to tokenId
s is appropriately modified. However, only SCENARIO 3 applies separation of concerns, so consider implementing that one as follows:
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
// AccountAbstraction Imports
import {IAccount} from "accountabstraction/contracts/interfaces/IAccount.sol";
import {IEntryPoint} from "accountabstraction/contracts/interfaces/IEntryPoint.sol";
import {UserOperationLib} from "accountabstraction/contracts/core/UserOperationLib.sol";
import {SIG_VALIDATION_FAILED, SIG_VALIDATION_SUCCESS} from "accountabstraction/contracts/core/Helpers.sol";
import {PackedUserOperation} from "accountabstraction/contracts/interfaces/PackedUserOperation.sol";
// OZ Imports
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
- import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+ import {MondrianNFT} from "./MondrianNFT.sol";
/**
* Our abstract art account abstraction... hehe
*/
- contract MondrianWallet is Ownable, ERC721, IAccount {
+ contract MondrianWallet is Ownable, IAccount {
using UserOperationLib for PackedUserOperation;
error MondrianWallet__NotFromEntryPoint();
error MondrianWallet__NotFromEntryPointOrOwner();
- error MondrainWallet__InvalidTokenId();
- /*//////////////////////////////////////////////////////////////
- NFT URIS
- //////////////////////////////////////////////////////////////*/
- string constant ART_ONE = "ar://jMRC4pksxwYIgi6vIBsMKXh3Sq0dfFFghSEqrchd_nQ";
- string constant ART_TWO = "ar://8NI8_fZSi2JyiqSTkIBDVWRGmHCwqHT0qn4QwF9hnPU";
- string constant ART_THREE = "ar://AVwp_mWsxZO7yZ6Sf3nrsoJhVnJppN02-cbXbFpdOME";
- string constant ART_FOUR = "ar://n17SzjtRkcbHWzcPnm0UU6w1Af5N1p0LAcRUMNP-LiM";
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
IEntryPoint private immutable i_entryPoint;
+ MondiranNFT private immutable i_mondrianNFT;
/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/
modifier requireFromEntryPoint() {
if (msg.sender != address(i_entryPoint)) {
revert MondrianWallet__NotFromEntryPoint();
}
_;
}
modifier requireFromEntryPointOrOwner() {
if (msg.sender != address(i_entryPoint) && msg.sender != owner()) {
revert MondrianWallet__NotFromEntryPointOrOwner();
}
_;
}
/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
- constructor(address entryPoint) Ownable(msg.sender) ERC721("MondrianWallet", "MW") {
+ constructor(address entryPoint, address _mondrianNftAddress) Ownable(msg.sender) {
i_entryPoint = IEntryPoint(entryPoint);
+ i_mondrianNFT = MondrianNFT(_mondrianNftAddress);
+ i_mondrianNFT.mint(address(this));
}
receive() external payable {}
/*//////////////////////////////////////////////////////////////
FUNCTIONS - EXTERNAL
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IAccount
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
external
virtual
override
requireFromEntryPoint
returns (uint256 validationData)
{
validationData = _validateSignature(userOp, userOpHash);
_validateNonce(userOp.nonce);
_payPrefund(missingAccountFunds);
}
/**
* execute a transaction (called directly from owner, or by entryPoint)
* @param dest destination address to call
* @param value the value to pass in this call
* @param func the calldata to pass in this call
*/
function execute(address dest, uint256 value, bytes calldata func) external requireFromEntryPointOrOwner {
(bool success, bytes memory result) = dest.call{value: value}(func);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
/*//////////////////////////////////////////////////////////////
FUNCTIONS - INTERNAL
//////////////////////////////////////////////////////////////*/
/**
* Validate the signature is valid for this message.
* @param userOp - Validate the userOp.signature field.
* @param userOpHash - Convenient field: the hash of the request, to check the signature against.
* (also hashes the entrypoint and chain id)
* @return validationData - Signature and time-range of this operation.
* <20-byte> aggregatorOrSigFail - 0 for valid signature, 1 to mark signature failure,
* otherwise, an address of an aggregator contract.
* <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite"
* <6-byte> validAfter - first timestamp this operation is valid
* If the account doesn't use time-range, it is enough to return
* SIG_VALIDATION_FAILED value (1) for signature failure.
* Note that the validation code cannot use block.timestamp (or block.number) directly.
*/
function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
internal
pure
returns (uint256 validationData)
{
bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash);
ECDSA.recover(hash, userOp.signature);
return SIG_VALIDATION_SUCCESS;
}
/**
* Validate the nonce of the UserOperation.
* This method may validate the nonce requirement of this account.
* e.g.
* To limit the nonce to use sequenced UserOps only (no "out of order" UserOps):
* `require(nonce < type(uint64).max)`
* For a hypothetical account that *requires* the nonce to be out-of-order:
* `require(nonce & type(uint64).max == 0)`
*
* The actual nonce uniqueness is managed by the EntryPoint, and thus no other
* action is needed by the account itself.
*
* @param nonce to validate
*
* solhint-disable-next-line no-empty-blocks
*/
function _validateNonce(uint256 nonce) internal view virtual {}
/**
* Sends to the entrypoint (msg.sender) the missing funds for this transaction.
* SubClass MAY override this method for better funds management
* (e.g. send to the entryPoint more than the minimum required, so that in future transactions
* it will not be required to send again).
* @param missingAccountFunds - The minimum value this method should send the entrypoint.
* This value MAY be zero, in case there is enough deposit,
* or the userOp has a paymaster.
*/
function _payPrefund(uint256 missingAccountFunds) internal virtual {
if (missingAccountFunds != 0) {
(bool success,) = payable(msg.sender).call{value: missingAccountFunds, gas: type(uint256).max}("");
(success);
//ignore failure (its EntryPoint's job to verify, not account.)
}
}
/*//////////////////////////////////////////////////////////////
VIEW AND PURE
//////////////////////////////////////////////////////////////*/
- function tokenURI(uint256 tokenId) public view override returns (string memory) {
- if (ownerOf(tokenId) == address(0)) {
- revert MondrainWallet__InvalidTokenId();
- }
- uint256 modNumber = tokenId % 10;
- if (modNumber == 0) {
- return ART_ONE;
- } else if (modNumber == 1) {
- return ART_TWO;
- } else if (modNumber == 2) {
- return ART_THREE;
- } else {
- return ART_FOUR;
- }
- }
function getEntryPoint() public view returns (IEntryPoint) {
return i_entryPoint;
}
/**
* Return the account nonce.
* This method returns the next sequential nonce.
* For a nonce of a specific key, use `entrypoint.getNonce(account, key)`
*/
function getNonce() public view virtual returns (uint256) {
return i_entryPoint.getNonce(address(this), 0);
}
/**
* check current account deposit in the entryPoint
*/
function getDeposit() public view returns (uint256) {
return i_entryPoint.balanceOf(address(this));
}
/**
* deposit more funds for this account in the entryPoint
*/
function addDeposit() public payable {
i_entryPoint.depositTo{value: msg.value}(address(this));
}
}
+ // SPDX-License-Identifier: MIT
+ pragma solidity 0.8.24;
+ import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
+ import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
+ import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+ import "@openzeppelin/contracts/access/Ownable.sol";
+ import "./MondrianWallet.sol";
+ contract MondrianNFT is ERC721, Ownable, VRFConsumerBaseV2 {
+ uint256 public tokenCounter;
+ mapping(uint256 => string) private _tokenURIs;
+ VRFCoordinatorV2Interface private vrfCoordinator;
+ bytes32 private keyHash;
+ uint64 private subscriptionId;
+ uint32 private callbackGasLimit;
+ uint16 private requestConfirmations;
+ uint32 private numWords;
+ // Store the hash of the MondrianWallet runtime bytecode
+ bytes32 public constant MONDRIAN_WALLET_CODEHASH = keccak256(type(MondrianWallet).runtimeCode);
+ string constant ART_ONE = "ar://jMRC4pksxwYIgi6vIBsMKXh3Sq0dfFFghSEqrchd_nQ";
+ string constant ART_TWO = "ar://8NI8_fZSi2JyiqSTkIBDVWRGmHCwqHT0qn4QwF9hnPU";
+ string constant ART_THREE = "ar://AVwp_mWsxZO7yZ6Sf3nrsoJhVnJppN02-cbXbFpdOME";
+ string constant ART_FOUR = "ar://n17SzjtRkcbHWzcPnm0UU6w1Af5N1p0LAcRUMNP-LiM";
+ string[] private artURIs = [ART_ONE, ART_TWO, ART_THREE, ART_FOUR];
+ uint256 private randomResult;
+ event RandomnessRequested(uint256 requestId);
+ constructor(
+ address _vrfCoordinator,
+ bytes32 _keyHash,
+ uint64 _subscriptionId,
+ uint32 _callbackGasLimit,
+ uint16 _requestConfirmations,
+ uint32 _numWords
+ )
+ ERC721("MondrianNFT", "MNFT")
+ VRFConsumerBaseV2(_vrfCoordinator)
+ {
+ vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator);
+ keyHash = _keyHash;
+ subscriptionId = _subscriptionId;
+ callbackGasLimit = _callbackGasLimit;
+ requestConfirmations = _requestConfirmations;
+ numWords = _numWords;
+ }
+ function mint(address to) external {
+ require(isMondrianWallet(msg.sender), "Not authorized to mint");
+
+ uint256 tokenId = tokenCounter;
+ tokenCounter++;
+ // Request randomness for tokenURI assignment
+ uint256 requestId = vrfCoordinator.requestRandomWords(
+ keyHash,
+ subscriptionId,
+ requestConfirmations,
+ callbackGasLimit,
+ numWords
+ );
+ emit RandomnessRequested(requestId);
+ // Mint the NFT
+ _safeMint(to, tokenId);
+ }
+ function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
+ uint256 randomIndex = randomWords[0] % artURIs.length;
+ _tokenURIs[tokenCounter - 1] = artURIs[randomIndex];
+ }
+ function tokenURI(uint256 tokenId) public view override returns (string memory) {
+ require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
+ return _tokenURIs[tokenId];
+ }
+ function isMondrianWallet(address account) internal view returns (bool) {
+ return keccak256(account.code) == MONDRIAN_WALLET_CODEHASH;
+ }
+}