Skip to content

Commit

Permalink
Added tests for SimpleMint with and without signature
Browse files Browse the repository at this point in the history
  • Loading branch information
luloxi committed Sep 16, 2024
1 parent 46bb2dc commit bdaaf42
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 51 deletions.
31 changes: 23 additions & 8 deletions packages/foundry/contracts/SimpleMint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { SimpleMintNFT } from "./SimpleMintNFT.sol";
* @notice A contract to enable artists uploading and signing their art, and someone else paying for the gas to mint it on the blockchain
*/
contract SimpleMint is EIP712 {
// mapping(address => uint256) public artistNonces;
address[] public collections;

event CollectionStarted(
address indexed nft,
address artist,
Expand All @@ -20,39 +21,53 @@ contract SimpleMint is EIP712 {
string tokenURI
);

bytes32 public constant TYPEHASH = keccak256(
"startCollection(string name,string symbol,string tokenURI,address artist)"
);
bytes32 public constant TYPEHASH =
keccak256("startCollection(string,string,string,address)");

constructor() EIP712("SimpleMint", "1.0.0") { }

// Add a nonce to prevent collections being started more than once
// Function to start a collection by the artist
function startCollection(
string memory _name,
string memory _symbol,
string memory _tokenURI,
address _artist
) public returns (address) {
SimpleMintNFT nft = new SimpleMintNFT(_name, _symbol, _tokenURI, _artist);
collections.push(address(nft));
emit CollectionStarted(address(nft), _artist, _name, _symbol, _tokenURI);
return address(nft);
}

// Function to start a collection by a third party using a signature from the artist
function startCollectionBySig(
string memory _name,
string memory _symbol,
string memory _tokenURI,
address _artist,
bytes memory signature
) external {
) external returns (address) {
// 1. Create a hash of the input data using the provided type hash
bytes32 structHash =
keccak256(abi.encode(TYPEHASH, _name, _symbol, _tokenURI, _artist));
bytes32 hash = _hashTypedDataV4(structHash);

// 2. Recover the artist's address from the signature
address signer = ECDSA.recover(hash, signature);
require(signer == _artist, "Invalid signature");
startCollection(_name, _symbol, _tokenURI, _artist);

// 3. Verify that the signer matches the provided artist address
require(
signer == _artist, "Invalid signature: signer does not match artist"
);

// 4. Start the collection using the verified artist's address
address collectionInstanceAddress =
startCollection(_name, _symbol, _tokenURI, _artist);

return collectionInstanceAddress;
}

// Helper function to get the message hash for signing
function getMessageHash(
string memory _name,
string memory _symbol,
Expand Down
98 changes: 55 additions & 43 deletions packages/foundry/contracts/SimpleMintNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,57 +9,69 @@ import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract SimpleMintNFT is ERC721, ERC721Enumerable, ERC721URIStorage {
uint256 private _tokenIds; // Replaces the Counters.Counter
string public collectionTokenURI;
address public artist;
uint256 private _tokenIds; // Replaces the Counters.Counter
string public collectionTokenURI;
address public artist;

constructor(string memory _name, string memory _symbol, string memory _tokenURI, address _artist)
ERC721(_name, _symbol)
{
collectionTokenURI = _tokenURI;
artist = _artist;
}
constructor(
string memory _name,
string memory _symbol,
string memory _tokenURI,
address _artist
) ERC721(_name, _symbol) {
collectionTokenURI = _tokenURI;
artist = _artist;
}

// Mint an NFT
function mintItem() public returns (bool) {
_tokenIds++; // Increment the token ID manually
uint256 id = _tokenIds;
// Mint an NFT
function mintItem() public returns (bool) {
_tokenIds++; // Increment the token ID manually
uint256 id = _tokenIds;

_mint(msg.sender, id);
_setTokenURI(id, collectionTokenURI);
_mint(msg.sender, id);
_setTokenURI(id, collectionTokenURI);

return true;
}
return true;
}

function tokenURI(uint256 /* tokenId */ ) public view override(ERC721, ERC721URIStorage) returns (string memory) {
// return super.tokenURI(tokenId);
return string(abi.encodePacked("https://ipfs.io/ipfs/", collectionTokenURI));
}
function tokenURI(
uint256 /* tokenId */
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
// return super.tokenURI(tokenId);
// This should be the IPFS URI when contract is live
return string(abi.encodePacked("https://ipfs.io/ipfs/", collectionTokenURI));
// return collectionTokenURI;
}

function tokenIdCounter() public view returns (uint256) {
return _tokenIds;
}
function tokenIdCounter() public view returns (uint256) {
return _tokenIds;
}

// The following functions are overrides required by Solidity.
// The following functions are overrides required by Solidity.

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function supportsInterface(
bytes4 interfaceId
)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}

function _update(address to, uint256 tokenId, address auth)
internal
override(ERC721, ERC721Enumerable)
returns (address)
{
return super._update(to, tokenId, auth);
}
function _update(
address to,
uint256 tokenId,
address auth
) internal override(ERC721, ERC721Enumerable) returns (address) {
return super._update(to, tokenId, auth);
}

function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) {
super._increaseBalance(account, value);
}
function _increaseBalance(
address account,
uint128 value
) internal override(ERC721, ERC721Enumerable) {
super._increaseBalance(account, value);
}
}
96 changes: 96 additions & 0 deletions packages/foundry/test/SimpleMintTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { Test, console } from "forge-std/Test.sol";
import { SimpleMint } from "../contracts/SimpleMint.sol";
import { SimpleMintNFT } from "../contracts/SimpleMintNFT.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

contract SimpleMintTest is Test {
SimpleMint public simpleMint;
SimpleMintNFT public simpleMintNFT;

// NFT building helpers
string NAME = "Song Number Two";
string SYMBOL = "SONG2";
string TOKEN_URI = "QmTokenUri";

// Variables for testing the EIP-712 signature
address ARTIST_ADDRESS;
uint256 PRIVATE_KEY;
address gasPayer;

function setUp() public {
// Address that will pay for the gas of the signer
gasPayer = makeAddr("gasPayer");
// Generate address and private key for signing
(ARTIST_ADDRESS, PRIVATE_KEY) = makeAddrAndKey("artist");
// Deploy the SimpleMint contract
simpleMint = new SimpleMint();
}

function testStartCollection() public {
// Start a collection
address newCollection =
simpleMint.startCollection(NAME, SYMBOL, TOKEN_URI, ARTIST_ADDRESS);
// Check that the collection was started
SimpleMintNFT newCollectionInstance = SimpleMintNFT(newCollection);
string memory completeTokenURI =
string(abi.encodePacked("https://ipfs.io/ipfs/", TOKEN_URI));

assertEq(newCollectionInstance.name(), NAME);
assertEq(newCollectionInstance.symbol(), SYMBOL);
assertEq(newCollectionInstance.tokenURI(0), completeTokenURI);
assertEq(newCollectionInstance.artist(), ARTIST_ADDRESS);
}

function testArtistCanSignCollectionAndSomeoneElsePaysTheGas() public {
vm.startPrank(ARTIST_ADDRESS);
// Sign the message containing the collection metadata with the artist's private key
(uint8 v, bytes32 r, bytes32 s) = signMessage();
vm.stopPrank();

// Prank the gas payer
vm.prank(gasPayer);
bytes memory signature = abi.encodePacked(r, s, v);
address newCollection = simpleMint.startCollectionBySig(
NAME, SYMBOL, TOKEN_URI, ARTIST_ADDRESS, signature
);

// Assert the collection has been correctly initialized
SimpleMintNFT newCollectionInstance = SimpleMintNFT(newCollection);
string memory completeTokenURI =
string(abi.encodePacked("https://ipfs.io/ipfs/", TOKEN_URI));

assertEq(newCollectionInstance.name(), NAME);
assertEq(newCollectionInstance.symbol(), SYMBOL);
assertEq(newCollectionInstance.tokenURI(0), completeTokenURI);
assertEq(newCollectionInstance.artist(), ARTIST_ADDRESS);
}

function signMessage() internal view returns (uint8 v, bytes32 r, bytes32 s) {
// Reproduce the domain separator from the contract
bytes32 domainSeparator = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes("SimpleMint")),
keccak256(bytes("1.0.0")),
block.chainid, // Ensure this matches the chain ID in the test
address(simpleMint) // The contract's address
)
);

// Reproduce the struct hash from the contract
bytes32 structHash = keccak256(
abi.encode(simpleMint.TYPEHASH(), NAME, SYMBOL, TOKEN_URI, ARTIST_ADDRESS)
);

bytes32 hashedMessage =
keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));

(v, r, s) = vm.sign(PRIVATE_KEY, hashedMessage);
}
}

0 comments on commit bdaaf42

Please sign in to comment.