From b63f1b2cb3a6257f3c74c05c8c64b37d4954c772 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 8 Oct 2023 21:10:36 -0400 Subject: [PATCH 01/41] test: add initial tests --- src/Will4USNFT.sol | 21 ++++++++++++--- test/Will4USNFTTest.t.sol | 55 +++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 4b67fc6..abb532b 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -41,6 +41,9 @@ contract Will4USNFT is ERC721URIStorage, Ownable { */ event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); event TokenMetadataUpdated(address indexed sender, uint256 indexed tokenId, string tokenURI); + event CampaignMemberAdded(address indexed member); + event CampaignMemberRemoved(address indexed member); + event ClassAdded(uint256 indexed classId, string metadata); /** * Modifiers ************ @@ -68,10 +71,14 @@ contract Will4USNFT is ERC721URIStorage, Ownable { function addCampaignMember(address _member) external onlyOwner { campaignMembers[_member] = true; + + emit CampaignMemberAdded(_member); } function removeCampaignMember(address _member) external onlyOwner { campaignMembers[_member] = false; + + emit CampaignMemberRemoved(_member); } /** @@ -83,20 +90,26 @@ contract Will4USNFT is ERC721URIStorage, Ownable { returns (uint256) { uint256 tokenId = _mintCampaingnItem(_recipient, _tokenURI, _classId); + classes[_classId].minted = classes[_classId].minted++; emit ItemAwarded(tokenId, _recipient, _classId); return tokenId; } - function addClass(string memory _name, string memory _description, string memory _imagePointer, uint256 _supply) - external - onlyCampaingnMember(msg.sender) - { + function addClass( + string memory _name, + string memory _description, + string memory _imagePointer, + string memory _metadata, + uint256 _supply + ) external onlyCampaingnMember(msg.sender) { uint256 id = ++classIds; classes[id] = Class(id, _supply, 0, _name, _description, _imagePointer, "https://a_new_pointer_to_json_object.io"); + + emit ClassAdded(id, _metadata); } function updateTokenMetadata(uint256 _tokenId, string memory _tokenURI) external onlyCampaingnMember(msg.sender) { diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index ef68852..0b63d8c 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -10,39 +10,66 @@ contract Will4USNFTTest is Test { address deployerAddress; event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); + event TokenMetadataUpdated(address indexed sender, uint256 indexed tokenId, string tokenURI); + event CampaignMemberAdded(address indexed member); + event CampaignMemberRemoved(address indexed member); + event ClassAdded(uint256 indexed classId, string metadata); function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); nftContract = new Will4USNFT(deployerAddress); + + vm.prank(deployerAddress); + nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); } function test_awardCampaignItem() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); - emit ItemAwarded(1, makeAddr("recipient1"), 1); + emit ItemAwarded(2, makeAddr("recipient1"), 1); uint256 tokenId = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); vm.stopPrank(); - assertEq(tokenId, 1, "Token Id should be 1"); + assertEq(tokenId, 2, "Token Id should be 1"); } - function test_UpdateMetadata() public {} - - function test_addCampaignMember(address _member) public {} + function test_updateTokenMetadata() public { + vm.startPrank(deployerAddress); + vm.expectEmit(true, true, true, true); + emit TokenMetadataUpdated(deployerAddress, 1, "https://placeholder.com/1"); + nftContract.updateTokenMetadata(1, "https://placeholder.com/1"); - function test_removeCampaignMember(address _member) public {} - - function test_awardCampaignItem(address _recipient, string memory _tokenURI, uint256 _classId) public {} + vm.stopPrank(); + assertEq(nftContract.tokenURI(1), "https://placeholder.com/1", "Token URI should be https://placeholder.com/1"); + } - function test_addClass(string memory _name, string memory _description, string memory _imagePointer, uint256 _supply) public {} + function test_addCampaignMember() public { + vm.startPrank(deployerAddress); + vm.expectEmit(true, true, true, true); + emit CampaignMemberAdded(makeAddr("member1")); + nftContract.addCampaignMember(makeAddr("member1")); - function test_updateTokenMetadata(uint256 _tokenId, string memory _tokenURI) public {} + vm.stopPrank(); + assertEq(nftContract.campaignMembers(makeAddr("member1")), true, "Member should be added"); + } - function test_getClassById(uint256 _id) public {} + function test_removeCampaignMember() public { + vm.startPrank(deployerAddress); + vm.expectEmit(true, true, true, true); + emit CampaignMemberRemoved(makeAddr("member1")); + nftContract.removeCampaignMember(makeAddr("member1")); - function test__mintCampaingnItem(address _recipient, string memory _tokenURI, uint256 _classId) public {} + vm.stopPrank(); + assertEq(nftContract.campaignMembers(makeAddr("member1")), false, "Member should be removed"); + } - function test_supportsInterface(bytes4 interfaceId) public {} + function test_addClass() public { + vm.startPrank(deployerAddress); + vm.expectEmit(true, true, true, true); + emit ClassAdded(1, "https://a_new_pointer_to_json_object.io"); + nftContract.addClass("name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7); - function test_tokenURI(uint256 tokenId) public {} + vm.stopPrank(); + assertEq(nftContract.getClassById(1).name, "name", "Class name should be name"); + } } From baf8419df14237dde3995fba699b9ba9448f77a4 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 8 Oct 2023 21:38:03 -0400 Subject: [PATCH 02/41] test: some updates/cleanup --- src/Will4USNFT.sol | 9 ++++----- test/Will4USNFTTest.t.sol | 24 +++++++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index abb532b..b2f8bfd 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -13,8 +13,8 @@ contract Will4USNFT is ERC721URIStorage, Ownable { /** * State Variables ******** */ - uint256 private _tokenIds; - uint256 public classIds; + uint256 private _tokenIds = 1; + uint256 public classIds = 1; uint256 public mintCounter; @@ -90,7 +90,6 @@ contract Will4USNFT is ERC721URIStorage, Ownable { returns (uint256) { uint256 tokenId = _mintCampaingnItem(_recipient, _tokenURI, _classId); - classes[_classId].minted = classes[_classId].minted++; emit ItemAwarded(tokenId, _recipient, _classId); @@ -104,7 +103,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { string memory _metadata, uint256 _supply ) external onlyCampaingnMember(msg.sender) { - uint256 id = ++classIds; + uint256 id = classIds++; classes[id] = Class(id, _supply, 0, _name, _description, _imagePointer, "https://a_new_pointer_to_json_object.io"); @@ -138,7 +137,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { internal returns (uint256) { - uint256 tokenId = ++_tokenIds; + uint256 tokenId = _tokenIds++; mintCounter++; // update the class minted count diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 0b63d8c..1c9cfaa 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -19,18 +19,28 @@ contract Will4USNFTTest is Test { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); nftContract = new Will4USNFT(deployerAddress); - vm.prank(deployerAddress); + vm.startPrank(deployerAddress); + nftContract.addCampaignMember(deployerAddress); + nftContract.addClass("name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7); nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + vm.stopPrank(); } function test_awardCampaignItem() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); emit ItemAwarded(2, makeAddr("recipient1"), 1); - uint256 tokenId = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); - vm.stopPrank(); + uint256 tokenId1 = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + + // mint a second token - assertEq(tokenId, 2, "Token Id should be 1"); + vm.expectEmit(true, true, true, true); + emit ItemAwarded(3, makeAddr("recipient1"), 1); + uint256 tokenId2 = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + + vm.stopPrank(); + assertEq(tokenId1, 2, "Token Id should be 2"); + assertEq(tokenId2, 3, "Token Id should be 3"); } function test_updateTokenMetadata() public { @@ -66,10 +76,10 @@ contract Will4USNFTTest is Test { function test_addClass() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); - emit ClassAdded(1, "https://a_new_pointer_to_json_object.io"); - nftContract.addClass("name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7); + emit ClassAdded(2, "https://a_new_pointer_to_json_object.io"); + nftContract.addClass("name2", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7); vm.stopPrank(); - assertEq(nftContract.getClassById(1).name, "name", "Class name should be name"); + assertEq(nftContract.getClassById(2).name, "name2", "Class name should be name"); } } From bcc3ed6a8044948af905f1d251d6351df07029e4 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 9 Oct 2023 18:08:34 -0400 Subject: [PATCH 03/41] chore: add some updates --- src/Will4USNFT.sol | 135 ++++++++++++++++++++++++++++++++++++-- test/Will4USNFTTest.t.sol | 53 ++++++++++++++- 2 files changed, 181 insertions(+), 7 deletions(-) diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index b2f8bfd..892267a 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -15,11 +15,11 @@ contract Will4USNFT is ERC721URIStorage, Ownable { */ uint256 private _tokenIds = 1; uint256 public classIds = 1; - - uint256 public mintCounter; + uint256 public MAX_MINTABLE_PER_CLASS; mapping(uint256 => Class) public classes; mapping(address => bool) public campaignMembers; + mapping(address => mapping(uint256 => uint256)) public mintedPerClass; struct Class { uint256 id; @@ -35,6 +35,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * Errors ************ */ error InvalidTokenId(uint256 tokenId); + error MaxMintablePerClassReached(uint256 classId, uint256 maxMintable); /** * Events ************ @@ -49,6 +50,11 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * Modifiers ************ */ + /** + * @notice Checks if the sender is a campaign member + * @dev This modifier is used to check if the sender is a campaign member + * @param sender The sender address + */ modifier onlyCampaingnMember(address sender) { require(campaignMembers[sender], "Only campaign members can call this function"); _; @@ -61,6 +67,8 @@ contract Will4USNFT is ERC721URIStorage, Ownable { // add the owner to the campaign members campaignMembers[owner] = true; + MAX_MINTABLE_PER_CLASS = 5; + // set the owner address _transferOwnership(owner); } @@ -69,12 +77,22 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * External Functions ***** */ + /** + * @notice Adds a campaign member + * @dev This function is only callable by the owner + * @param _member The member to add + */ function addCampaignMember(address _member) external onlyOwner { campaignMembers[_member] = true; emit CampaignMemberAdded(_member); } + /** + * @notice Removes a campaign member + * @dev This function is only callable by the owner + * @param _member The member to remove + */ function removeCampaignMember(address _member) external onlyOwner { campaignMembers[_member] = false; @@ -83,19 +101,65 @@ contract Will4USNFT is ERC721URIStorage, Ownable { /** * @notice Awards campaign nft to supporter + * @dev This function is only callable by campaign members + * @param _recipient The recipient of the item + * @param _tokenURI The token uri + * @param _classId The class ID */ function awardCampaignItem(address _recipient, string memory _tokenURI, uint256 _classId) external onlyCampaingnMember(msg.sender) returns (uint256) { + if (mintedPerClass[_recipient][_classId] > MAX_MINTABLE_PER_CLASS) { + revert MaxMintablePerClassReached(_classId, MAX_MINTABLE_PER_CLASS); + } + uint256 tokenId = _mintCampaingnItem(_recipient, _tokenURI, _classId); + mintedPerClass[_recipient][_classId]++; emit ItemAwarded(tokenId, _recipient, _classId); return tokenId; } + /** + * @notice Awards campaign nft to a batch of supporters + * @dev This function is only callable by campaign members + * @param _recipients The recipients of the item + * @param _tokenURIs The token uris + * @param _classIds The class IDs + */ + function batchAwardCampaignItem( + address[] memory _recipients, + string[] memory _tokenURIs, + uint256[] memory _classIds + ) external onlyCampaingnMember(msg.sender) returns (uint256[] memory) { + uint256[] memory tokenIds = new uint256[](_recipients.length); + + for (uint256 i = 0; i < _recipients.length; i++) { + if (mintedPerClass[_recipients[i]][_classIds[i]] > MAX_MINTABLE_PER_CLASS) { + revert("You have reached the max mintable for this class"); + } + + tokenIds[i] = _mintCampaingnItem(_recipients[i], _tokenURIs[i], _classIds[i]); + mintedPerClass[_recipients[i]][_classIds[i]]++; + + emit ItemAwarded(tokenIds[i], _recipients[i], _classIds[i]); + } + + return tokenIds; + } + + /** + * @notice Adds a new class to the campaign for issuance + * @dev This function is only callable by campaign members + * @param _name The name of the class + * @param _description The description of the class + * @param _imagePointer The image pointer for the class + * @param _metadata The metadata pointer for the class + * @param _supply The total supply of the class + */ function addClass( string memory _name, string memory _description, @@ -105,12 +169,17 @@ contract Will4USNFT is ERC721URIStorage, Ownable { ) external onlyCampaingnMember(msg.sender) { uint256 id = classIds++; - classes[id] = - Class(id, _supply, 0, _name, _description, _imagePointer, "https://a_new_pointer_to_json_object.io"); + classes[id] = Class(id, _supply, 0, _name, _description, _imagePointer, _metadata); emit ClassAdded(id, _metadata); } + /** + * @notice Updates the token metadata + * @dev This function is only callable by campaign members + * @param _tokenId The token ID to update + * @param _tokenURI The new token uri + */ function updateTokenMetadata(uint256 _tokenId, string memory _tokenURI) external onlyCampaingnMember(msg.sender) { if (super.ownerOf(_tokenId) != address(0)) { _setTokenURI(_tokenId, _tokenURI); @@ -121,27 +190,81 @@ contract Will4USNFT is ERC721URIStorage, Ownable { } } + /** + * @notice Sets the class token supply + * @dev This function is only callable by campaign members + * @param _classId The class ID + * @param _supply The new supply + */ + function setClassTokenSupply(uint256 _classId, uint256 _supply) external onlyCampaingnMember(msg.sender) { + classes[_classId].supply = _supply; + } + + /** + * @notice Sets the max mintable per wallet + * @dev This function is only callable by campaign members + * @param _maxMintable The new max mintable + */ + function setMaxMintablePerClass(uint256 _maxMintable) external onlyCampaingnMember(msg.sender) { + MAX_MINTABLE_PER_CLASS = _maxMintable; + } + /** * View Functions ****** */ + /** + * @notice Returns the class + * @param _id The class ID + */ function getClassById(uint256 _id) external view returns (Class memory) { return classes[_id]; } + /** + * @notice Returns the total supply for a class + * @param _classId The class ID + */ + function getTotalSupplyForClass(uint256 _classId) external view returns (uint256) { + return classes[_classId].supply; + } + + /** + * @notice Returns the total supply for all classes + */ + function getTotalSupplyForAllClasses() external view returns (uint256) { + uint256 totalSupply = 0; + + for (uint256 i = 1; i < classIds; i++) { + totalSupply += classes[i].supply; + } + + return totalSupply; + } + /** * Internal Functions ****** */ + // NOTE: not sure if we can use baseURI for this since each class will have a different baseURI essentially + // function _baseURI() internal pure override returns (string memory) { + // return "https://gateway.pinata.cloud/ipfs/hash/classId/tokenId"; + // } + + /** + * @notice Mints a new campaign item + * @param _recipient The recipient of the item + * @param _tokenURI The token uri + * @param _classId The class ID + */ function _mintCampaingnItem(address _recipient, string memory _tokenURI, uint256 _classId) internal returns (uint256) { uint256 tokenId = _tokenIds++; - mintCounter++; // update the class minted count - classes[_classId].minted = classes[_classId].minted++; + classes[_classId].minted++; _safeMint(_recipient, tokenId); _setTokenURI(tokenId, _tokenURI); diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 1c9cfaa..7c0ff62 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -33,7 +33,6 @@ contract Will4USNFTTest is Test { uint256 tokenId1 = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); // mint a second token - vm.expectEmit(true, true, true, true); emit ItemAwarded(3, makeAddr("recipient1"), 1); uint256 tokenId2 = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); @@ -43,6 +42,36 @@ contract Will4USNFTTest is Test { assertEq(tokenId2, 3, "Token Id should be 3"); } + function test_revert_awardCampaignItem_MAX_MINTABLE_PER_CLASS() public { + vm.startPrank(deployerAddress); + nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + vm.expectRevert(); + nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + + vm.stopPrank(); + } + + function test_batchAwardCampaignItem() public { + vm.startPrank(deployerAddress); + address[] memory recipients = new address[](2); + recipients[0] = makeAddr("recipient1"); + recipients[1] = makeAddr("recipient2"); + string[] memory tokenURIs = new string[](2); + tokenURIs[0] = "https://placeholder.com/1"; + tokenURIs[1] = "https://placeholder.com/2"; + uint256[] memory classIds = new uint256[](2); + classIds[0] = 1; + classIds[1] = 1; + + nftContract.batchAwardCampaignItem(recipients, tokenURIs, classIds); + + vm.stopPrank(); + } + function test_updateTokenMetadata() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); @@ -82,4 +111,26 @@ contract Will4USNFTTest is Test { vm.stopPrank(); assertEq(nftContract.getClassById(2).name, "name2", "Class name should be name"); } + + function test_getTotalSupplyForClass() public { + assertEq(nftContract.getTotalSupplyForClass(1), 1e7, "Total supply should be 1e7"); + } + + function test_setClassTokenSupply() public { + vm.prank(deployerAddress); + nftContract.setClassTokenSupply(1, 1e10); + + assertEq(nftContract.getClassById(1).supply, 1e10, "Total supply should be 1e10"); + } + + function test_getTotalSupplyForAllClasses() public { + assertEq(nftContract.getTotalSupplyForAllClasses(), 1e7, "Total supply should be 1e7"); + } + + function test_setMaxMintablePerClass() public { + vm.prank(deployerAddress); + nftContract.setMaxMintablePerClass(10); + + assertEq(nftContract.MAX_MINTABLE_PER_CLASS(), 10, "Total supply should be 1e10"); + } } From 0f72d7b3b9ceea4a327b1e9bdcf0c4bcb7ed9bfe Mon Sep 17 00:00:00 2001 From: 0xKurt Date: Tue, 10 Oct 2023 12:23:55 +0200 Subject: [PATCH 04/41] various improvements various improvements --- script/Will4USNFT.s.sol | 2 +- src/Will4USNFT.sol | 76 +++++++++++++++++++++++++++------------ test/Will4USNFTTest.t.sol | 6 ++-- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/script/Will4USNFT.s.sol b/script/Will4USNFT.s.sol index 68b8598..1e07f49 100644 --- a/script/Will4USNFT.s.sol +++ b/script/Will4USNFT.s.sol @@ -17,7 +17,7 @@ contract Will4USNFTScript is Script { address deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); vm.startBroadcast(deployerPrivateKey); - Will4USNFT nftContract = new Will4USNFT(deployerAddress); + Will4USNFT nftContract = new Will4USNFT(deployerAddress, 5); nftContract.awardCampaignItem(deployerAddress, "https://placeholder.com/1", 1); diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 892267a..6c08d86 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.20; import {ERC721URIStorage} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; -import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; @@ -13,9 +12,10 @@ contract Will4USNFT is ERC721URIStorage, Ownable { /** * State Variables ******** */ - uint256 private _tokenIds = 1; - uint256 public classIds = 1; - uint256 public MAX_MINTABLE_PER_CLASS; + uint256 private _tokenIds; + uint256 public classIds; + uint256 public totalClassesSupply; + uint256 public maxMintablePerClass; mapping(uint256 => Class) public classes; mapping(address => bool) public campaignMembers; @@ -35,7 +35,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * Errors ************ */ error InvalidTokenId(uint256 tokenId); - error MaxMintablePerClassReached(uint256 classId, uint256 maxMintable); + error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); /** * Events ************ @@ -45,6 +45,8 @@ contract Will4USNFT is ERC721URIStorage, Ownable { event CampaignMemberAdded(address indexed member); event CampaignMemberRemoved(address indexed member); event ClassAdded(uint256 indexed classId, string metadata); + event UpdatedClassTokenSupply(uint256 indexed classId, uint256 supply); + event UpdatedMaxMintablePerClass(uint256 maxMintable); /** * Modifiers ************ @@ -63,11 +65,14 @@ contract Will4USNFT is ERC721URIStorage, Ownable { /** * Constructor ********* */ - constructor(address owner) ERC721("Will 4 US NFT Collection", "WILL4USNFT") Ownable(owner) { + constructor(address owner, uint256 _maxMintablePerClass) + ERC721("Will 4 US NFT Collection", "WILL4USNFT") + Ownable(owner) + { // add the owner to the campaign members - campaignMembers[owner] = true; + _addCampaignMember(owner); - MAX_MINTABLE_PER_CLASS = 5; + maxMintablePerClass = _maxMintablePerClass; // set the owner address _transferOwnership(owner); @@ -83,6 +88,15 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param _member The member to add */ function addCampaignMember(address _member) external onlyOwner { + _addCampaignMember(_member); + } + + /** + * @notice Adds a campaign member + * @dev This function is internal + * @param _member The member to add + */ + function _addCampaignMember(address _member) internal { campaignMembers[_member] = true; emit CampaignMemberAdded(_member); @@ -111,8 +125,8 @@ contract Will4USNFT is ERC721URIStorage, Ownable { onlyCampaingnMember(msg.sender) returns (uint256) { - if (mintedPerClass[_recipient][_classId] > MAX_MINTABLE_PER_CLASS) { - revert MaxMintablePerClassReached(_classId, MAX_MINTABLE_PER_CLASS); + if (mintedPerClass[_recipient][_classId] > maxMintablePerClass) { + revert MaxMintablePerClassReached(_recipient, _classId, maxMintablePerClass); } uint256 tokenId = _mintCampaingnItem(_recipient, _tokenURI, _classId); @@ -135,10 +149,11 @@ contract Will4USNFT is ERC721URIStorage, Ownable { string[] memory _tokenURIs, uint256[] memory _classIds ) external onlyCampaingnMember(msg.sender) returns (uint256[] memory) { - uint256[] memory tokenIds = new uint256[](_recipients.length); + uint256 length = _recipients.length; + uint256[] memory tokenIds = new uint256[](length); - for (uint256 i = 0; i < _recipients.length; i++) { - if (mintedPerClass[_recipients[i]][_classIds[i]] > MAX_MINTABLE_PER_CLASS) { + for (uint256 i = 0; i < length;) { + if (mintedPerClass[_recipients[i]][_classIds[i]] > maxMintablePerClass) { revert("You have reached the max mintable for this class"); } @@ -146,6 +161,10 @@ contract Will4USNFT is ERC721URIStorage, Ownable { mintedPerClass[_recipients[i]][_classIds[i]]++; emit ItemAwarded(tokenIds[i], _recipients[i], _classIds[i]); + + unchecked { + ++i; + } } return tokenIds; @@ -167,7 +186,8 @@ contract Will4USNFT is ERC721URIStorage, Ownable { string memory _metadata, uint256 _supply ) external onlyCampaingnMember(msg.sender) { - uint256 id = classIds++; + uint256 id = ++classIds; + totalClassesSupply += _supply; classes[id] = Class(id, _supply, 0, _name, _description, _imagePointer, _metadata); @@ -197,7 +217,22 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param _supply The new supply */ function setClassTokenSupply(uint256 _classId, uint256 _supply) external onlyCampaingnMember(msg.sender) { + uint256 currentSupply = classes[_classId].supply; + uint256 minted = classes[_classId].minted; + + if (_supply < currentSupply) { + // if the new supply is less than the current supply, we need to check if the new supply is less than the minted + // if it is, then we need to revert + if (_supply < minted) { + revert("The new supply cannot be less than the minted"); + } + } + + // update the total supply + totalClassesSupply = totalClassesSupply - currentSupply + _supply; + classes[_classId].supply = _supply; + emit UpdatedClassTokenSupply(_classId, _supply); } /** @@ -206,7 +241,8 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param _maxMintable The new max mintable */ function setMaxMintablePerClass(uint256 _maxMintable) external onlyCampaingnMember(msg.sender) { - MAX_MINTABLE_PER_CLASS = _maxMintable; + maxMintablePerClass = _maxMintable; + emit UpdatedMaxMintablePerClass(_maxMintable); } /** @@ -233,13 +269,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @notice Returns the total supply for all classes */ function getTotalSupplyForAllClasses() external view returns (uint256) { - uint256 totalSupply = 0; - - for (uint256 i = 1; i < classIds; i++) { - totalSupply += classes[i].supply; - } - - return totalSupply; + return totalClassesSupply; } /** @@ -261,7 +291,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { internal returns (uint256) { - uint256 tokenId = _tokenIds++; + uint256 tokenId = ++_tokenIds; // update the class minted count classes[_classId].minted++; diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 7c0ff62..8a01a69 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -17,7 +17,7 @@ contract Will4USNFTTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); - nftContract = new Will4USNFT(deployerAddress); + nftContract = new Will4USNFT(deployerAddress, 5); vm.startPrank(deployerAddress); nftContract.addCampaignMember(deployerAddress); @@ -42,7 +42,7 @@ contract Will4USNFTTest is Test { assertEq(tokenId2, 3, "Token Id should be 3"); } - function test_revert_awardCampaignItem_MAX_MINTABLE_PER_CLASS() public { + function test_revert_awardCampaignItem_maxMintablePerClass() public { vm.startPrank(deployerAddress); nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); @@ -131,6 +131,6 @@ contract Will4USNFTTest is Test { vm.prank(deployerAddress); nftContract.setMaxMintablePerClass(10); - assertEq(nftContract.MAX_MINTABLE_PER_CLASS(), 10, "Total supply should be 1e10"); + assertEq(nftContract.maxMintablePerClass(), 10, "Total supply should be 1e10"); } } From 620804390b294df538da632a9aca04ab0b4a66eb Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Tue, 10 Oct 2023 22:47:48 -0400 Subject: [PATCH 05/41] chore: update baseURI --- src/Will4USNFT.sol | 37 +++++++++++++++++++++++++++---------- test/Will4USNFTTest.t.sol | 17 +++++++++++++---- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 6c08d86..4ac132b 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -4,14 +4,17 @@ pragma solidity 0.8.20; import {ERC721URIStorage} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; /// @notice This contract is the Main NFT contract for the Will 4 US Campaign /// @dev This contract is used to mint NFTs for the Will 4 US Campaign /// @author @codenamejason contract Will4USNFT is ERC721URIStorage, Ownable { + using Strings for uint256; /** * State Variables ******** */ + uint256 private _tokenIds; uint256 public classIds; uint256 public totalClassesSupply; @@ -41,7 +44,9 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * Events ************ */ event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); - event TokenMetadataUpdated(address indexed sender, uint256 indexed tokenId, string tokenURI); + event TokenMetadataUpdated( + address indexed sender, uint256 indexed classId, uint256 indexed tokenId, string tokenURI + ); event CampaignMemberAdded(address indexed member); event CampaignMemberRemoved(address indexed member); event ClassAdded(uint256 indexed classId, string metadata); @@ -200,11 +205,15 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param _tokenId The token ID to update * @param _tokenURI The new token uri */ - function updateTokenMetadata(uint256 _tokenId, string memory _tokenURI) external onlyCampaingnMember(msg.sender) { + function updateTokenMetadata(uint256 _classId, uint256 _tokenId, string memory _tokenURI) + external + onlyCampaingnMember(msg.sender) + { if (super.ownerOf(_tokenId) != address(0)) { - _setTokenURI(_tokenId, _tokenURI); + string memory uri = getTokenURI(_classId, _tokenId); + _setTokenURI(_tokenId, uri); - emit TokenMetadataUpdated(msg.sender, _tokenId, _tokenURI); + emit TokenMetadataUpdated(msg.sender, _classId, _tokenId, _tokenURI); } else { revert InvalidTokenId(_tokenId); } @@ -230,8 +239,8 @@ contract Will4USNFT is ERC721URIStorage, Ownable { // update the total supply totalClassesSupply = totalClassesSupply - currentSupply + _supply; - classes[_classId].supply = _supply; + emit UpdatedClassTokenSupply(_classId, _supply); } @@ -242,6 +251,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { */ function setMaxMintablePerClass(uint256 _maxMintable) external onlyCampaingnMember(msg.sender) { maxMintablePerClass = _maxMintable; + emit UpdatedMaxMintablePerClass(_maxMintable); } @@ -272,15 +282,22 @@ contract Will4USNFT is ERC721URIStorage, Ownable { return totalClassesSupply; } + function _baseURI() internal pure override returns (string memory) { + // TODO: 🚨 update this when production ready 🚨 + return string.concat("https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/"); + } + + function getTokenURI(uint256 _classId, uint256 _tokenId) public pure returns (string memory) { + string memory classId = Strings.toString(_classId); + string memory tokenId = Strings.toString(_tokenId); + + return string.concat(classId, "/", tokenId, ".json"); + } + /** * Internal Functions ****** */ - // NOTE: not sure if we can use baseURI for this since each class will have a different baseURI essentially - // function _baseURI() internal pure override returns (string memory) { - // return "https://gateway.pinata.cloud/ipfs/hash/classId/tokenId"; - // } - /** * @notice Mints a new campaign item * @param _recipient The recipient of the item diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 8a01a69..b4bd37d 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -10,7 +10,9 @@ contract Will4USNFTTest is Test { address deployerAddress; event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); - event TokenMetadataUpdated(address indexed sender, uint256 indexed tokenId, string tokenURI); + event TokenMetadataUpdated( + address indexed sender, uint256 indexed classId, uint256 indexed tokenId, string tokenURI + ); event CampaignMemberAdded(address indexed member); event CampaignMemberRemoved(address indexed member); event ClassAdded(uint256 indexed classId, string metadata); @@ -75,11 +77,18 @@ contract Will4USNFTTest is Test { function test_updateTokenMetadata() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); - emit TokenMetadataUpdated(deployerAddress, 1, "https://placeholder.com/1"); - nftContract.updateTokenMetadata(1, "https://placeholder.com/1"); + emit TokenMetadataUpdated( + deployerAddress, 1, 1, "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/" + ); + nftContract.updateTokenMetadata( + 1, 1, "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/" + ); vm.stopPrank(); - assertEq(nftContract.tokenURI(1), "https://placeholder.com/1", "Token URI should be https://placeholder.com/1"); + assertEq( + nftContract.tokenURI(1), + "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/1/1.json" + ); } function test_addCampaignMember() public { From 4a497421683842c7380a0ee06bb246fe7aad42ed Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Wed, 11 Oct 2023 00:34:40 -0400 Subject: [PATCH 06/41] forge install: chainlink-brownie-contracts 1.2.0 --- .gitmodules | 3 +++ lib/chainlink-brownie-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/chainlink-brownie-contracts diff --git a/.gitmodules b/.gitmodules index 690924b..0978a1f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/chainlink-brownie-contracts"] + path = lib/chainlink-brownie-contracts + url = https://github.com/smartcontractkit/chainlink-brownie-contracts diff --git a/lib/chainlink-brownie-contracts b/lib/chainlink-brownie-contracts new file mode 160000 index 0000000..27f3fc5 --- /dev/null +++ b/lib/chainlink-brownie-contracts @@ -0,0 +1 @@ +Subproject commit 27f3fc59f8edc5084a2d15926cce4e4eb15ba011 From 72c5afd9cd741a68eb710a66acffaa2073af44a2 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Wed, 11 Oct 2023 00:38:52 -0400 Subject: [PATCH 07/41] chore: add chainlink --- foundry.toml | 1 + remappings.txt | 1 + src/PersonaFunctionConsumer.sol | 92 +++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 src/PersonaFunctionConsumer.sol diff --git a/foundry.toml b/foundry.toml index 25b918f..920f209 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,6 @@ src = "src" out = "out" libs = ["lib"] +fs_permissions = [{ access = "read", path = "lib/foundry-chainlink-toolkit/out"}] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/remappings.txt b/remappings.txt index f810763..06d76a0 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,3 +3,4 @@ ds-test/=lib/forge-std/lib/ds-test/src/ erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ +@chainlink/=lib/chainlink-brownie-contracts/ diff --git a/src/PersonaFunctionConsumer.sol b/src/PersonaFunctionConsumer.sol new file mode 100644 index 0000000..3bf3f25 --- /dev/null +++ b/src/PersonaFunctionConsumer.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + +import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol"; +import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; +import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ +contract PersonaFunctionConsumer is FunctionsClient, ConfirmedOwner { + using FunctionsRequest for FunctionsRequest.Request; + + bytes32 public s_lastRequestId; + bytes public s_lastResponse; + bytes public s_lastError; + + error UnexpectedRequestID(bytes32 requestId); + + event Response(bytes32 indexed requestId, bytes response, bytes err); + + constructor(address router) FunctionsClient(router) ConfirmedOwner(msg.sender) {} + + /** + * @notice Send a simple request + * @param source JavaScript source code + * @param encryptedSecretsUrls Encrypted URLs where to fetch user secrets + * @param donHostedSecretsSlotID Don hosted secrets slotId + * @param donHostedSecretsVersion Don hosted secrets version + * @param args List of arguments accessible from within the source code + * @param bytesArgs Array of bytes arguments, represented as hex strings + * @param subscriptionId Billing ID + */ + function sendRequest( + string memory source, + bytes memory encryptedSecretsUrls, + uint8 donHostedSecretsSlotID, + uint64 donHostedSecretsVersion, + string[] memory args, + bytes[] memory bytesArgs, + uint64 subscriptionId, + uint32 gasLimit, + bytes32 jobId + ) external onlyOwner returns (bytes32 requestId) { + FunctionsRequest.Request memory req; + req.initializeRequestForInlineJavaScript(source); + if (encryptedSecretsUrls.length > 0) { + req.addSecretsReference(encryptedSecretsUrls); + } else if (donHostedSecretsVersion > 0) { + req.addDONHostedSecrets(donHostedSecretsSlotID, donHostedSecretsVersion); + } + if (args.length > 0) req.setArgs(args); + if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs); + s_lastRequestId = _sendRequest(req.encodeCBOR(), subscriptionId, gasLimit, jobId); + return s_lastRequestId; + } + + /** + * @notice Send a pre-encoded CBOR request + * @param request CBOR-encoded request data + * @param subscriptionId Billing ID + * @param gasLimit The maximum amount of gas the request can consume + * @param jobId ID of the job to be invoked + * @return requestId The ID of the sent request + */ + function sendRequestCBOR(bytes memory request, uint64 subscriptionId, uint32 gasLimit, bytes32 jobId) + external + onlyOwner + returns (bytes32 requestId) + { + s_lastRequestId = _sendRequest(request, subscriptionId, gasLimit, jobId); + return s_lastRequestId; + } + + /** + * @notice Store latest result/error + * @param requestId The request ID, returned by sendRequest() + * @param response Aggregated response from the user code + * @param err Aggregated error from the user code or from the execution pipeline + * Either response or error parameter will be set, but never both + */ + function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { + if (s_lastRequestId != requestId) { + revert UnexpectedRequestID(requestId); + } + s_lastResponse = response; + s_lastError = err; + emit Response(requestId, s_lastResponse, s_lastError); + } +} From 2cde7792e90c2bbaac93264938f44b77290b9a02 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Wed, 11 Oct 2023 00:39:53 -0400 Subject: [PATCH 08/41] forge install: foundry-chainlink-toolkit --- .gitmodules | 3 +++ lib/foundry-chainlink-toolkit | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/foundry-chainlink-toolkit diff --git a/.gitmodules b/.gitmodules index 0978a1f..38ee39e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/chainlink-brownie-contracts"] path = lib/chainlink-brownie-contracts url = https://github.com/smartcontractkit/chainlink-brownie-contracts +[submodule "lib/foundry-chainlink-toolkit"] + path = lib/foundry-chainlink-toolkit + url = https://github.com/smartcontractkit/foundry-chainlink-toolkit diff --git a/lib/foundry-chainlink-toolkit b/lib/foundry-chainlink-toolkit new file mode 160000 index 0000000..fdb16da --- /dev/null +++ b/lib/foundry-chainlink-toolkit @@ -0,0 +1 @@ +Subproject commit fdb16da12bac9d6c06425f5506bd10114f953ce8 From d4f28eb5b7e6cb31dd4fcee8d8e1b593bca91e8d Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Wed, 11 Oct 2023 01:11:54 -0400 Subject: [PATCH 09/41] chore: add api consumer contract --- remappings.txt | 2 +- src/PersonaAPIConsumer.sol | 92 +++++++++++++++++++++++++++++++++ src/PersonaFunctionConsumer.sol | 92 --------------------------------- test/PersonaAPIConsumer.t.sol | 10 ++++ 4 files changed, 103 insertions(+), 93 deletions(-) create mode 100644 src/PersonaAPIConsumer.sol delete mode 100644 src/PersonaFunctionConsumer.sol create mode 100644 test/PersonaAPIConsumer.t.sol diff --git a/remappings.txt b/remappings.txt index 06d76a0..de624bf 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,4 @@ ds-test/=lib/forge-std/lib/ds-test/src/ erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ -@chainlink/=lib/chainlink-brownie-contracts/ +@chainlink/=lib//foundry-chainlink-toolkit/lib/chainlink-brownie-contracts/contracts/ diff --git a/src/PersonaAPIConsumer.sol b/src/PersonaAPIConsumer.sol new file mode 100644 index 0000000..021a1d8 --- /dev/null +++ b/src/PersonaAPIConsumer.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import "@chainlink/src/v0.8/ChainlinkClient.sol"; +import "@chainlink/src/v0.8/ConfirmedOwner.sol"; + +/** + * @title The PersonaAPIConsumer contract + * @notice An API Consumer contract that makes GET requests to obtain KYC data + */ +contract PersonaAPIConsumer is ChainlinkClient, ConfirmedOwner { + using Chainlink for Chainlink.Request; + + bytes32 private jobId; + uint256 private fee; + + mapping(string => bool) public isKYCApproved; + + event DataFullfilled(bytes32 requestId, bool isKYCApproved); + + /** + * @notice Initialize the link token and target oracle + * + * Sepolia Testnet details: + * Link Token: 0x779877A7B0D9E8603169DdbD7836e478b4624789 + * Oracle: 0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD (Chainlink DevRel) + * jobId: ca98366cc7314957b8c012c72f05aeeb + * + */ + constructor() ConfirmedOwner(msg.sender) { + setChainlinkToken(0x779877A7B0D9E8603169DdbD7836e478b4624789); + setChainlinkOracle(0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD); + jobId = "ca98366cc7314957b8c012c72f05aeeb"; + fee = (1 * LINK_DIVISIBILITY) / 10; // 0,1 * 10**18 (Varies by network and job) + } + /** + * @notice Creates a Chainlink request to retrieve API response and update the mapping + * + * @return requestId - ID of the request + */ + + function requestKYCData() public returns (bytes32 requestId) { + Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector); + + // Set the URL to perform the GET request on + request.add("get", "set url here"); + + // Set the path to find the desired data in the API response, where the response format is: + // {"RAW": + // {"ETH": + // {"USD": + // { + // "VOLUME24HOUR": xxx.xxx, + // } + // } + // } + // } + // Chainlink node versions prior to 1.0.0 supported this format + // request.add("path", "RAW.ETH.USD.VOLUME24HOUR"); + request.add("path", ""); + + // Sends the request + return sendChainlinkRequest(request, fee); + } + + /** + * @notice Receives the response in the form of uint256 + * + * @param _requestId - id of the request + * @param _isKYCApproved - response; requested KYC data for donors + */ + // function fulfill(bytes32 _requestId, bool _isKYCApproved) public recordChainlinkFulfillment(_requestId) { + // isKYCApproved[""] = _isKYCApproved; + // emit DataFullfilled(_isKYCApproved); + // } + + /** + * Receive the response in the form of uint256 + */ + function fulfill(bytes32 _requestId, bool _isKYCApproved) public recordChainlinkFulfillment(_requestId) { + emit DataFullfilled(_requestId, _isKYCApproved); + isKYCApproved[""] = _isKYCApproved; + } + + /** + * Allow withdraw of Link tokens from the contract + */ + function withdrawLink() public onlyOwner { + LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress()); + require(link.transfer(msg.sender, link.balanceOf(address(this))), "Unable to transfer"); + } +} diff --git a/src/PersonaFunctionConsumer.sol b/src/PersonaFunctionConsumer.sol deleted file mode 100644 index 3bf3f25..0000000 --- a/src/PersonaFunctionConsumer.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.20; - -import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol"; -import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; -import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol"; - -/** - * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. - * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. - * DO NOT USE THIS CODE IN PRODUCTION. - */ -contract PersonaFunctionConsumer is FunctionsClient, ConfirmedOwner { - using FunctionsRequest for FunctionsRequest.Request; - - bytes32 public s_lastRequestId; - bytes public s_lastResponse; - bytes public s_lastError; - - error UnexpectedRequestID(bytes32 requestId); - - event Response(bytes32 indexed requestId, bytes response, bytes err); - - constructor(address router) FunctionsClient(router) ConfirmedOwner(msg.sender) {} - - /** - * @notice Send a simple request - * @param source JavaScript source code - * @param encryptedSecretsUrls Encrypted URLs where to fetch user secrets - * @param donHostedSecretsSlotID Don hosted secrets slotId - * @param donHostedSecretsVersion Don hosted secrets version - * @param args List of arguments accessible from within the source code - * @param bytesArgs Array of bytes arguments, represented as hex strings - * @param subscriptionId Billing ID - */ - function sendRequest( - string memory source, - bytes memory encryptedSecretsUrls, - uint8 donHostedSecretsSlotID, - uint64 donHostedSecretsVersion, - string[] memory args, - bytes[] memory bytesArgs, - uint64 subscriptionId, - uint32 gasLimit, - bytes32 jobId - ) external onlyOwner returns (bytes32 requestId) { - FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - if (encryptedSecretsUrls.length > 0) { - req.addSecretsReference(encryptedSecretsUrls); - } else if (donHostedSecretsVersion > 0) { - req.addDONHostedSecrets(donHostedSecretsSlotID, donHostedSecretsVersion); - } - if (args.length > 0) req.setArgs(args); - if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs); - s_lastRequestId = _sendRequest(req.encodeCBOR(), subscriptionId, gasLimit, jobId); - return s_lastRequestId; - } - - /** - * @notice Send a pre-encoded CBOR request - * @param request CBOR-encoded request data - * @param subscriptionId Billing ID - * @param gasLimit The maximum amount of gas the request can consume - * @param jobId ID of the job to be invoked - * @return requestId The ID of the sent request - */ - function sendRequestCBOR(bytes memory request, uint64 subscriptionId, uint32 gasLimit, bytes32 jobId) - external - onlyOwner - returns (bytes32 requestId) - { - s_lastRequestId = _sendRequest(request, subscriptionId, gasLimit, jobId); - return s_lastRequestId; - } - - /** - * @notice Store latest result/error - * @param requestId The request ID, returned by sendRequest() - * @param response Aggregated response from the user code - * @param err Aggregated error from the user code or from the execution pipeline - * Either response or error parameter will be set, but never both - */ - function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { - if (s_lastRequestId != requestId) { - revert UnexpectedRequestID(requestId); - } - s_lastResponse = response; - s_lastError = err; - emit Response(requestId, s_lastResponse, s_lastError); - } -} diff --git a/test/PersonaAPIConsumer.t.sol b/test/PersonaAPIConsumer.t.sol new file mode 100644 index 0000000..3ca200d --- /dev/null +++ b/test/PersonaAPIConsumer.t.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import {Test, console2} from "forge-std/Test.sol"; +import {Will4USNFT} from "../src/Will4USNFT.sol"; + +/// @notice This contract is used to test the Will4USNFT contract +contract PersonaAPIConsumer is Test { + function setUp() public {} +} From edc06becc50265d2a9f6700d37635fc97cec99c2 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Wed, 11 Oct 2023 16:00:38 -0400 Subject: [PATCH 10/41] chore: update nft contract and deploy --- script/Will4USNFT.s.sol | 2 +- src/PersonaAPIConsumer.sol | 16 ++-------- src/Will4USNFT.sol | 61 ++++++++++++++++++++++++++++---------- test/Will4USNFTTest.t.sol | 28 ++++++++--------- 4 files changed, 62 insertions(+), 45 deletions(-) diff --git a/script/Will4USNFT.s.sol b/script/Will4USNFT.s.sol index 1e07f49..1997cd4 100644 --- a/script/Will4USNFT.s.sol +++ b/script/Will4USNFT.s.sol @@ -19,7 +19,7 @@ contract Will4USNFTScript is Script { Will4USNFT nftContract = new Will4USNFT(deployerAddress, 5); - nftContract.awardCampaignItem(deployerAddress, "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(deployerAddress, 1); vm.stopBroadcast(); } diff --git a/src/PersonaAPIConsumer.sol b/src/PersonaAPIConsumer.sol index 021a1d8..9c98763 100644 --- a/src/PersonaAPIConsumer.sol +++ b/src/PersonaAPIConsumer.sol @@ -14,7 +14,7 @@ contract PersonaAPIConsumer is ChainlinkClient, ConfirmedOwner { bytes32 private jobId; uint256 private fee; - mapping(string => bool) public isKYCApproved; + mapping(address => bool) public isKYCApproved; event DataFullfilled(bytes32 requestId, bool isKYCApproved); @@ -63,23 +63,13 @@ contract PersonaAPIConsumer is ChainlinkClient, ConfirmedOwner { return sendChainlinkRequest(request, fee); } - /** - * @notice Receives the response in the form of uint256 - * - * @param _requestId - id of the request - * @param _isKYCApproved - response; requested KYC data for donors - */ - // function fulfill(bytes32 _requestId, bool _isKYCApproved) public recordChainlinkFulfillment(_requestId) { - // isKYCApproved[""] = _isKYCApproved; - // emit DataFullfilled(_isKYCApproved); - // } - /** * Receive the response in the form of uint256 */ function fulfill(bytes32 _requestId, bool _isKYCApproved) public recordChainlinkFulfillment(_requestId) { + isKYCApproved[address(0)] = _isKYCApproved; + emit DataFullfilled(_requestId, _isKYCApproved); - isKYCApproved[""] = _isKYCApproved; } /** diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 4ac132b..b3a9a93 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -122,10 +122,9 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @notice Awards campaign nft to supporter * @dev This function is only callable by campaign members * @param _recipient The recipient of the item - * @param _tokenURI The token uri * @param _classId The class ID */ - function awardCampaignItem(address _recipient, string memory _tokenURI, uint256 _classId) + function awardCampaignItem(address _recipient, uint256 _classId) external onlyCampaingnMember(msg.sender) returns (uint256) @@ -134,7 +133,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { revert MaxMintablePerClassReached(_recipient, _classId, maxMintablePerClass); } - uint256 tokenId = _mintCampaingnItem(_recipient, _tokenURI, _classId); + uint256 tokenId = _mintCampaingnItem(_recipient, _classId); mintedPerClass[_recipient][_classId]++; emit ItemAwarded(tokenId, _recipient, _classId); @@ -146,12 +145,10 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @notice Awards campaign nft to a batch of supporters * @dev This function is only callable by campaign members * @param _recipients The recipients of the item - * @param _tokenURIs The token uris * @param _classIds The class IDs */ function batchAwardCampaignItem( address[] memory _recipients, - string[] memory _tokenURIs, uint256[] memory _classIds ) external onlyCampaingnMember(msg.sender) returns (uint256[] memory) { uint256 length = _recipients.length; @@ -162,7 +159,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { revert("You have reached the max mintable for this class"); } - tokenIds[i] = _mintCampaingnItem(_recipients[i], _tokenURIs[i], _classIds[i]); + tokenIds[i] = _mintCampaingnItem(_recipients[i], _classIds[i]); mintedPerClass[_recipients[i]][_classIds[i]]++; emit ItemAwarded(tokenIds[i], _recipients[i], _classIds[i]); @@ -199,21 +196,38 @@ contract Will4USNFT is ERC721URIStorage, Ownable { emit ClassAdded(id, _metadata); } + /** + * @notice Returns all classes + */ + function getAllClasses() public view returns (Class[] memory) { + Class[] memory _classes = new Class[](classIds); + + for (uint256 i = 0; i < classIds; i++) { + _classes[i] = classes[i + 1]; + } + + return _classes; + } + /** * @notice Updates the token metadata - * @dev This function is only callable by campaign members + * @dev This function is only callable by campaign members - only use if you really need to * @param _tokenId The token ID to update - * @param _tokenURI The new token uri + * @param _classId The class ID + * @param _newTokenURI The new token URI 🚨 must be a pointer to a json object 🚨 + * @return The new token URI */ - function updateTokenMetadata(uint256 _classId, uint256 _tokenId, string memory _tokenURI) + function updateTokenMetadata(uint256 _classId, uint256 _tokenId, string memory _newTokenURI) external - onlyCampaingnMember(msg.sender) + onlyOwner + returns (string memory) { if (super.ownerOf(_tokenId) != address(0)) { - string memory uri = getTokenURI(_classId, _tokenId); - _setTokenURI(_tokenId, uri); + _setTokenURI(_tokenId, _newTokenURI); - emit TokenMetadataUpdated(msg.sender, _classId, _tokenId, _tokenURI); + emit TokenMetadataUpdated(msg.sender, _classId, _tokenId, tokenURI(_tokenId)); + + return tokenURI(_tokenId); } else { revert InvalidTokenId(_tokenId); } @@ -282,11 +296,19 @@ contract Will4USNFT is ERC721URIStorage, Ownable { return totalClassesSupply; } + /** + * @notice Returns `_baseURI` for the `tokenURI` + */ function _baseURI() internal pure override returns (string memory) { // TODO: 🚨 update this when production ready 🚨 return string.concat("https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/"); } + /** + * @notice Returns the `tokenURI` + * @param _classId The class ID + * @param _tokenId The token ID + */ function getTokenURI(uint256 _classId, uint256 _tokenId) public pure returns (string memory) { string memory classId = Strings.toString(_classId); string memory tokenId = Strings.toString(_tokenId); @@ -294,6 +316,14 @@ contract Will4USNFT is ERC721URIStorage, Ownable { return string.concat(classId, "/", tokenId, ".json"); } + /** + * @notice Returns the owner of the token + * @param _tokenId The token ID + */ + function getOwnerOfToken(uint256 _tokenId) external view returns (address) { + return super.ownerOf(_tokenId); + } + /** * Internal Functions ****** */ @@ -301,10 +331,9 @@ contract Will4USNFT is ERC721URIStorage, Ownable { /** * @notice Mints a new campaign item * @param _recipient The recipient of the item - * @param _tokenURI The token uri * @param _classId The class ID */ - function _mintCampaingnItem(address _recipient, string memory _tokenURI, uint256 _classId) + function _mintCampaingnItem(address _recipient, uint256 _classId) internal returns (uint256) { @@ -314,7 +343,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { classes[_classId].minted++; _safeMint(_recipient, tokenId); - _setTokenURI(tokenId, _tokenURI); + _setTokenURI(tokenId, getTokenURI(_classId, tokenId)); return tokenId; } diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index b4bd37d..3ef0cff 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -24,7 +24,7 @@ contract Will4USNFTTest is Test { vm.startPrank(deployerAddress); nftContract.addCampaignMember(deployerAddress); nftContract.addClass("name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7); - nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); vm.stopPrank(); } @@ -32,12 +32,12 @@ contract Will4USNFTTest is Test { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); emit ItemAwarded(2, makeAddr("recipient1"), 1); - uint256 tokenId1 = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + uint256 tokenId1 = nftContract.awardCampaignItem(makeAddr("recipient1"), 1); // mint a second token vm.expectEmit(true, true, true, true); emit ItemAwarded(3, makeAddr("recipient1"), 1); - uint256 tokenId2 = nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + uint256 tokenId2 = nftContract.awardCampaignItem(makeAddr("recipient1"), 1); vm.stopPrank(); assertEq(tokenId1, 2, "Token Id should be 2"); @@ -46,13 +46,13 @@ contract Will4USNFTTest is Test { function test_revert_awardCampaignItem_maxMintablePerClass() public { vm.startPrank(deployerAddress); - nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); - nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); - nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); - nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); - nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); vm.expectRevert(); - nftContract.awardCampaignItem(makeAddr("recipient1"), "https://placeholder.com/1", 1); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); vm.stopPrank(); } @@ -63,13 +63,11 @@ contract Will4USNFTTest is Test { recipients[0] = makeAddr("recipient1"); recipients[1] = makeAddr("recipient2"); string[] memory tokenURIs = new string[](2); - tokenURIs[0] = "https://placeholder.com/1"; - tokenURIs[1] = "https://placeholder.com/2"; uint256[] memory classIds = new uint256[](2); classIds[0] = 1; classIds[1] = 1; - nftContract.batchAwardCampaignItem(recipients, tokenURIs, classIds); + nftContract.batchAwardCampaignItem(recipients, classIds); vm.stopPrank(); } @@ -78,16 +76,16 @@ contract Will4USNFTTest is Test { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); emit TokenMetadataUpdated( - deployerAddress, 1, 1, "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/" + deployerAddress, 1, 1, "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/2/1.json" ); nftContract.updateTokenMetadata( - 1, 1, "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/" + 1, 1, "2/1.json" ); vm.stopPrank(); assertEq( nftContract.tokenURI(1), - "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/1/1.json" + "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/2/1.json" ); } From 3807928d24b476a8320e20e46f6c5ddaff594df0 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Thu, 12 Oct 2023 00:22:08 -0400 Subject: [PATCH 11/41] chore: updaet lib/modules/deps --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 1 + .gitmodules | 9 +++------ lib/chainlink-brownie-contracts | 1 - lib/foundry-chainlink-toolkit | 1 - remappings.txt | 2 +- 6 files changed, 5 insertions(+), 9 deletions(-) create mode 100644 .DS_Store delete mode 160000 lib/chainlink-brownie-contracts delete mode 160000 lib/foundry-chainlink-toolkit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..93b0aa4066561ee7464f1eb27e3d60678e7b88be GIT binary patch literal 6148 zcmeHKL2uJA6nit%R649nw z=VLmfmh--xpF{HXsJiUr4u?roCfq9 z*n`4x(?iAZTP=pS5RU}BkS6%0h|no^ZjR3<-oNXk5l6M|y9MSL{wX|HxGIu$ZVTLk z#?2lD&$rHMW86fa9QBdaJnKBTS-T{m$zU_9z&};s4 Date: Thu, 12 Oct 2023 00:35:04 -0400 Subject: [PATCH 12/41] forge install: chainlink v2.5.0 --- .gitmodules | 2 +- lib/chainlink | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 lib/chainlink diff --git a/.gitmodules b/.gitmodules index c3196be..69058bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,5 +5,5 @@ path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/chainlink"] - path = packages/foundry/lib/chainlink + path = lib/chainlink url = https://github.com/smartcontractkit/chainlink diff --git a/lib/chainlink b/lib/chainlink new file mode 160000 index 0000000..b96cb80 --- /dev/null +++ b/lib/chainlink @@ -0,0 +1 @@ +Subproject commit b96cb806e6ea648799e31a71fb1803607d79cde4 From 3356a81a999b321897f45db3fa7ee7dd0ee888ed Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Thu, 12 Oct 2023 00:40:56 -0400 Subject: [PATCH 13/41] chore: dep updates --- .gitignore | 1 - src/PersonaAPIConsumer.sol | 2 +- src/PersonaAPIFunction.sol | 92 ++++++++++++++++++++++++++++++++++++++ src/Will4USNFT.sol | 14 +++--- test/Will4USNFTTest.t.sol | 9 ++-- 5 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 src/PersonaAPIFunction.sol diff --git a/.gitignore b/.gitignore index 025d37c..d362fb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # Compiler files cache/ out/ -lib/ # Ignores broadcast logs !/broadcast diff --git a/src/PersonaAPIConsumer.sol b/src/PersonaAPIConsumer.sol index 9c98763..4e148e8 100644 --- a/src/PersonaAPIConsumer.sol +++ b/src/PersonaAPIConsumer.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.20; import "@chainlink/src/v0.8/ChainlinkClient.sol"; -import "@chainlink/src/v0.8/ConfirmedOwner.sol"; +import "@chainlink/src/v0.8/shared/access/ConfirmedOwner.sol"; /** * @title The PersonaAPIConsumer contract diff --git a/src/PersonaAPIFunction.sol b/src/PersonaAPIFunction.sol new file mode 100644 index 0000000..47b2eec --- /dev/null +++ b/src/PersonaAPIFunction.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {FunctionsClient} from "@chainlink/src/v0.8/functions/dev/1_0_0/FunctionsClient.sol"; +import {ConfirmedOwner} from "@chainlink/src/v0.8/shared/access/ConfirmedOwner.sol"; +import {FunctionsRequest} from "@chainlink/src/v0.8/functions/dev/1_0_0/libraries/FunctionsRequest.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ +contract FunctionsConsumerExample is FunctionsClient, ConfirmedOwner { + using FunctionsRequest for FunctionsRequest.Request; + + bytes32 public s_lastRequestId; + bytes public s_lastResponse; + bytes public s_lastError; + + error UnexpectedRequestID(bytes32 requestId); + + event Response(bytes32 indexed requestId, bytes response, bytes err); + + constructor(address router) FunctionsClient(router) ConfirmedOwner(msg.sender) {} + + /** + * @notice Send a simple request + * @param source JavaScript source code + * @param encryptedSecretsUrls Encrypted URLs where to fetch user secrets + * @param donHostedSecretsSlotID Don hosted secrets slotId + * @param donHostedSecretsVersion Don hosted secrets version + * @param args List of arguments accessible from within the source code + * @param bytesArgs Array of bytes arguments, represented as hex strings + * @param subscriptionId Billing ID + */ + function sendRequest( + string memory source, + bytes memory encryptedSecretsUrls, + uint8 donHostedSecretsSlotID, + uint64 donHostedSecretsVersion, + string[] memory args, + bytes[] memory bytesArgs, + uint64 subscriptionId, + uint32 gasLimit, + bytes32 jobId + ) external onlyOwner returns (bytes32 requestId) { + FunctionsRequest.Request memory req; + req.initializeRequestForInlineJavaScript(source); + if (encryptedSecretsUrls.length > 0) { + req.addSecretsReference(encryptedSecretsUrls); + } else if (donHostedSecretsVersion > 0) { + req.addDONHostedSecrets(donHostedSecretsSlotID, donHostedSecretsVersion); + } + if (args.length > 0) req.setArgs(args); + if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs); + s_lastRequestId = _sendRequest(req.encodeCBOR(), subscriptionId, gasLimit, jobId); + return s_lastRequestId; + } + + /** + * @notice Send a pre-encoded CBOR request + * @param request CBOR-encoded request data + * @param subscriptionId Billing ID + * @param gasLimit The maximum amount of gas the request can consume + * @param jobId ID of the job to be invoked + * @return requestId The ID of the sent request + */ + function sendRequestCBOR(bytes memory request, uint64 subscriptionId, uint32 gasLimit, bytes32 jobId) + external + onlyOwner + returns (bytes32 requestId) + { + s_lastRequestId = _sendRequest(request, subscriptionId, gasLimit, jobId); + return s_lastRequestId; + } + + /** + * @notice Store latest result/error + * @param requestId The request ID, returned by sendRequest() + * @param response Aggregated response from the user code + * @param err Aggregated error from the user code or from the execution pipeline + * Either response or error parameter will be set, but never both + */ + function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { + if (s_lastRequestId != requestId) { + revert UnexpectedRequestID(requestId); + } + s_lastResponse = response; + s_lastError = err; + emit Response(requestId, s_lastResponse, s_lastError); + } +} diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index b3a9a93..2e04f82 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -147,10 +147,11 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param _recipients The recipients of the item * @param _classIds The class IDs */ - function batchAwardCampaignItem( - address[] memory _recipients, - uint256[] memory _classIds - ) external onlyCampaingnMember(msg.sender) returns (uint256[] memory) { + function batchAwardCampaignItem(address[] memory _recipients, uint256[] memory _classIds) + external + onlyCampaingnMember(msg.sender) + returns (uint256[] memory) + { uint256 length = _recipients.length; uint256[] memory tokenIds = new uint256[](length); @@ -333,10 +334,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param _recipient The recipient of the item * @param _classId The class ID */ - function _mintCampaingnItem(address _recipient, uint256 _classId) - internal - returns (uint256) - { + function _mintCampaingnItem(address _recipient, uint256 _classId) internal returns (uint256) { uint256 tokenId = ++_tokenIds; // update the class minted count diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 3ef0cff..2f41792 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -76,11 +76,12 @@ contract Will4USNFTTest is Test { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); emit TokenMetadataUpdated( - deployerAddress, 1, 1, "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/2/1.json" - ); - nftContract.updateTokenMetadata( - 1, 1, "2/1.json" + deployerAddress, + 1, + 1, + "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/2/1.json" ); + nftContract.updateTokenMetadata(1, 1, "2/1.json"); vm.stopPrank(); assertEq( From f94a6882f0ac5a12936319d5739ba8d7f6975fa8 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Thu, 12 Oct 2023 00:43:09 -0400 Subject: [PATCH 14/41] chore: rename --- src/{PersonaAPIFunction.sol => FunctionConsumer.sol} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{PersonaAPIFunction.sol => FunctionConsumer.sol} (98%) diff --git a/src/PersonaAPIFunction.sol b/src/FunctionConsumer.sol similarity index 98% rename from src/PersonaAPIFunction.sol rename to src/FunctionConsumer.sol index 47b2eec..487eb48 100644 --- a/src/PersonaAPIFunction.sol +++ b/src/FunctionConsumer.sol @@ -10,7 +10,7 @@ import {FunctionsRequest} from "@chainlink/src/v0.8/functions/dev/1_0_0/librarie * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. * DO NOT USE THIS CODE IN PRODUCTION. */ -contract FunctionsConsumerExample is FunctionsClient, ConfirmedOwner { +contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { using FunctionsRequest for FunctionsRequest.Request; bytes32 public s_lastRequestId; From fea8486d03a3633ad32ff3868e807a3cff8fb9c0 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 15 Oct 2023 21:38:32 -0400 Subject: [PATCH 15/41] chore: update tests/add redeem --- DEPLOYMENTS.md | 5 ++ foundry.toml | 34 ++++++++++- script/Will4USNFT.s.sol | 11 ++-- src/FunctionConsumer.sol | 25 ++++---- src/PersonaAPIConsumer.sol | 8 ++- src/Will4USNFT.sol | 80 ++++++++++++++++++------- test/PersonaAPIConsumer.t.sol | 6 +- test/Will4USNFTDeployTest.t.sol | 34 +++++++++++ test/Will4USNFTTest.t.sol | 103 +++++++++++++++++++++++++++++--- 9 files changed, 259 insertions(+), 47 deletions(-) create mode 100644 DEPLOYMENTS.md create mode 100644 test/Will4USNFTDeployTest.t.sol diff --git a/DEPLOYMENTS.md b/DEPLOYMENTS.md new file mode 100644 index 0000000..af6e0ee --- /dev/null +++ b/DEPLOYMENTS.md @@ -0,0 +1,5 @@ +# Will4Us NFT Deployments + +## Arbitrum Mainnet + +| Contract | Address | \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 920f209..e8534b0 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,38 @@ src = "src" out = "out" libs = ["lib"] -fs_permissions = [{ access = "read", path = "lib/foundry-chainlink-toolkit/out"}] +broadcast = "broadcast" + +[rpc_endpoints] +arbitrumGoerli = "${ARBITRUM_GOERLI_RPC_URL}" + +[etherscan] +arbitrumGoerli = { key = "${ARBITRUM_API_KEY}" } + + +[fuzz] +runs = 256 +max_test_rejects = 65536 +seed = '0x3e8' +dictionary_weight = 40 +include_storage = true +include_push_bytes = true + +[invariant] +runs = 256 +depth = 15 +fail_on_revert = false +call_override = false +dictionary_weight = 80 +include_storage = true +include_push_bytes = true +shrink_sequence = true + +[fmt] +line_length = 100 +tab_width = 4 +bracket_spacing = true + +# Remappings in remappings.txt # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/script/Will4USNFT.s.sol b/script/Will4USNFT.s.sol index 1997cd4..da6aba3 100644 --- a/script/Will4USNFT.s.sol +++ b/script/Will4USNFT.s.sol @@ -1,25 +1,28 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.20; -import {Script} from "forge-std/Script.sol"; +import { Script } from "forge-std/Script.sol"; +// import { Test, console2 } from "forge-std/Test.sol"; -import {Will4USNFT} from "../src/Will4USNFT.sol"; +import { Will4USNFT } from "../src/Will4USNFT.sol"; /// @notice This script is used to deploy the Will4USNFT contract /// @dev Use this to run /// 'source .env' if you are using a .env file for your rpc-url /// 'forge script script/Will4USNFT.s.sol:Will4USNFTScript --rpc-url $GOERLI_RPC_URL --broadcast --verify -vvvv' contract Will4USNFTScript is Script { - function setUp() public {} + function setUp() public { } function run() public { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); address deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); + // string memory url = vm.rpcUrl("arbitrumGoerli"); + // assertEq(url, "https://arb-goerli.g.alchemy.com/v2/RqTiyvS7OspxaAQUQupKKCTjmf94JL-I"); vm.startBroadcast(deployerPrivateKey); Will4USNFT nftContract = new Will4USNFT(deployerAddress, 5); - nftContract.awardCampaignItem(deployerAddress, 1); + // nftContract.awardCampaignItem(deployerAddress, 1); vm.stopBroadcast(); } diff --git a/src/FunctionConsumer.sol b/src/FunctionConsumer.sol index 487eb48..50d6fc2 100644 --- a/src/FunctionConsumer.sol +++ b/src/FunctionConsumer.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; -import {FunctionsClient} from "@chainlink/src/v0.8/functions/dev/1_0_0/FunctionsClient.sol"; -import {ConfirmedOwner} from "@chainlink/src/v0.8/shared/access/ConfirmedOwner.sol"; -import {FunctionsRequest} from "@chainlink/src/v0.8/functions/dev/1_0_0/libraries/FunctionsRequest.sol"; +import { FunctionsClient } from "@chainlink/src/v0.8/functions/dev/1_0_0/FunctionsClient.sol"; +import { ConfirmedOwner } from "@chainlink/src/v0.8/shared/access/ConfirmedOwner.sol"; +import { FunctionsRequest } from + "@chainlink/src/v0.8/functions/dev/1_0_0/libraries/FunctionsRequest.sol"; /** * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. @@ -21,7 +22,7 @@ contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { event Response(bytes32 indexed requestId, bytes response, bytes err); - constructor(address router) FunctionsClient(router) ConfirmedOwner(msg.sender) {} + constructor(address router) FunctionsClient(router) ConfirmedOwner(msg.sender) { } /** * @notice Send a simple request @@ -65,11 +66,12 @@ contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { * @param jobId ID of the job to be invoked * @return requestId The ID of the sent request */ - function sendRequestCBOR(bytes memory request, uint64 subscriptionId, uint32 gasLimit, bytes32 jobId) - external - onlyOwner - returns (bytes32 requestId) - { + function sendRequestCBOR( + bytes memory request, + uint64 subscriptionId, + uint32 gasLimit, + bytes32 jobId + ) external onlyOwner returns (bytes32 requestId) { s_lastRequestId = _sendRequest(request, subscriptionId, gasLimit, jobId); return s_lastRequestId; } @@ -81,7 +83,10 @@ contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { * @param err Aggregated error from the user code or from the execution pipeline * Either response or error parameter will be set, but never both */ - function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { + function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) + internal + override + { if (s_lastRequestId != requestId) { revert UnexpectedRequestID(requestId); } diff --git a/src/PersonaAPIConsumer.sol b/src/PersonaAPIConsumer.sol index 4e148e8..865fe32 100644 --- a/src/PersonaAPIConsumer.sol +++ b/src/PersonaAPIConsumer.sol @@ -40,7 +40,8 @@ contract PersonaAPIConsumer is ChainlinkClient, ConfirmedOwner { */ function requestKYCData() public returns (bytes32 requestId) { - Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector); + Chainlink.Request memory request = + buildChainlinkRequest(jobId, address(this), this.fulfill.selector); // Set the URL to perform the GET request on request.add("get", "set url here"); @@ -66,7 +67,10 @@ contract PersonaAPIConsumer is ChainlinkClient, ConfirmedOwner { /** * Receive the response in the form of uint256 */ - function fulfill(bytes32 _requestId, bool _isKYCApproved) public recordChainlinkFulfillment(_requestId) { + function fulfill(bytes32 _requestId, bool _isKYCApproved) + public + recordChainlinkFulfillment(_requestId) + { isKYCApproved[address(0)] = _isKYCApproved; emit DataFullfilled(_requestId, _isKYCApproved); diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 2e04f82..b3ce459 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.20; -import {ERC721URIStorage} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; -import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; -import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import { ERC721URIStorage } from + "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol"; /// @notice This contract is the Main NFT contract for the Will 4 US Campaign /// @dev This contract is used to mint NFTs for the Will 4 US Campaign @@ -23,6 +24,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { mapping(uint256 => Class) public classes; mapping(address => bool) public campaignMembers; mapping(address => mapping(uint256 => uint256)) public mintedPerClass; + mapping(address => mapping(uint256 => bool)) public redeemed; struct Class { uint256 id; @@ -39,6 +41,9 @@ contract Will4USNFT is ERC721URIStorage, Ownable { */ error InvalidTokenId(uint256 tokenId); error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); + error AlreadyRedeemed(address redeemer, uint256 tokenId); + error Unauthorized(address sender); + error NewSupplyTooLow(uint256 minted, uint256 supply); /** * Events ************ @@ -52,6 +57,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { event ClassAdded(uint256 indexed classId, string metadata); event UpdatedClassTokenSupply(uint256 indexed classId, uint256 supply); event UpdatedMaxMintablePerClass(uint256 maxMintable); + event Redeemed(address indexed redeemer, uint256 indexed tokenId, uint256 indexed classId); /** * Modifiers ************ @@ -63,7 +69,9 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param sender The sender address */ modifier onlyCampaingnMember(address sender) { - require(campaignMembers[sender], "Only campaign members can call this function"); + if (!campaignMembers[sender]) { + revert Unauthorized(sender); + } _; } @@ -173,6 +181,26 @@ contract Will4USNFT is ERC721URIStorage, Ownable { return tokenIds; } + /** + * @notice Redeems a campaign item + * @dev This function is only callable by campaign members + * @param _tokenId The token ID + * @param _redeemer The owner/redeemer of the token + */ + function redeem(uint256 _tokenId, address _redeemer) external onlyCampaingnMember(msg.sender) { + if (super.ownerOf(_tokenId) == address(0)) { + revert InvalidTokenId(_tokenId); + } + + if (redeemed[_redeemer][_tokenId]) { + revert AlreadyRedeemed(_redeemer, _tokenId); + } + + redeemed[_redeemer][_tokenId] = true; + + emit Redeemed(_redeemer, _tokenId, classes[_tokenId].id); + } + /** * @notice Adds a new class to the campaign for issuance * @dev This function is only callable by campaign members @@ -240,7 +268,10 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @param _classId The class ID * @param _supply The new supply */ - function setClassTokenSupply(uint256 _classId, uint256 _supply) external onlyCampaingnMember(msg.sender) { + function setClassTokenSupply(uint256 _classId, uint256 _supply) + external + onlyCampaingnMember(msg.sender) + { uint256 currentSupply = classes[_classId].supply; uint256 minted = classes[_classId].minted; @@ -248,7 +279,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { // if the new supply is less than the current supply, we need to check if the new supply is less than the minted // if it is, then we need to revert if (_supply < minted) { - revert("The new supply cannot be less than the minted"); + revert NewSupplyTooLow(minted, _supply); } } @@ -264,7 +295,10 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @dev This function is only callable by campaign members * @param _maxMintable The new max mintable */ - function setMaxMintablePerClass(uint256 _maxMintable) external onlyCampaingnMember(msg.sender) { + function setMaxMintablePerClass(uint256 _maxMintable) + external + onlyCampaingnMember(msg.sender) + { maxMintablePerClass = _maxMintable; emit UpdatedMaxMintablePerClass(_maxMintable); @@ -274,14 +308,6 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * View Functions ****** */ - /** - * @notice Returns the class - * @param _id The class ID - */ - function getClassById(uint256 _id) external view returns (Class memory) { - return classes[_id]; - } - /** * @notice Returns the total supply for a class * @param _classId The class ID @@ -302,7 +328,9 @@ contract Will4USNFT is ERC721URIStorage, Ownable { */ function _baseURI() internal pure override returns (string memory) { // TODO: 🚨 update this when production ready 🚨 - return string.concat("https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/"); + return string.concat( + "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/" + ); } /** @@ -350,9 +378,21 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * Overrides */ - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721URIStorage) returns (bool) {} - - function tokenURI(uint256 tokenId) public view virtual override(ERC721URIStorage) returns (string memory) { + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721URIStorage) + returns (bool) + { } + + function tokenURI(uint256 tokenId) + public + view + virtual + override(ERC721URIStorage) + returns (string memory) + { return super.tokenURI(tokenId); } } diff --git a/test/PersonaAPIConsumer.t.sol b/test/PersonaAPIConsumer.t.sol index 3ca200d..65305df 100644 --- a/test/PersonaAPIConsumer.t.sol +++ b/test/PersonaAPIConsumer.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.20; -import {Test, console2} from "forge-std/Test.sol"; -import {Will4USNFT} from "../src/Will4USNFT.sol"; +import { Test, console2 } from "forge-std/Test.sol"; +import { Will4USNFT } from "../src/Will4USNFT.sol"; /// @notice This contract is used to test the Will4USNFT contract contract PersonaAPIConsumer is Test { - function setUp() public {} + function setUp() public { } } diff --git a/test/Will4USNFTDeployTest.t.sol b/test/Will4USNFTDeployTest.t.sol new file mode 100644 index 0000000..cd708eb --- /dev/null +++ b/test/Will4USNFTDeployTest.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import { Test, console2 } from "forge-std/Test.sol"; +import { Will4USNFT } from "../src/Will4USNFT.sol"; + +/// @notice This contract is used to test the Will4USNFT contract +contract Will4USNFTDeployTest is Test { + Will4USNFT public nftContract; + address deployerAddress; + + event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); + event TokenMetadataUpdated( + address indexed sender, uint256 indexed classId, uint256 indexed tokenId, string tokenURI + ); + event CampaignMemberAdded(address indexed member); + event CampaignMemberRemoved(address indexed member); + event ClassAdded(uint256 indexed classId, string metadata); + + function setUp() public { + deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); + nftContract = new Will4USNFT(deployerAddress, 5); + + vm.startPrank(deployerAddress); + nftContract.addCampaignMember(deployerAddress); + nftContract.addClass( + "name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7 + ); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); + vm.stopPrank(); + } + + // todo: test that the contract is deployed with the correct parameters +} diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 2f41792..63d5266 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -1,14 +1,20 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.20; -import {Test, console2} from "forge-std/Test.sol"; -import {Will4USNFT} from "../src/Will4USNFT.sol"; +import { Test, console2, StdUtils } from "forge-std/Test.sol"; +import { Will4USNFT } from "../src/Will4USNFT.sol"; /// @notice This contract is used to test the Will4USNFT contract contract Will4USNFTTest is Test { Will4USNFT public nftContract; address deployerAddress; + error InvalidTokenId(uint256 tokenId); + error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); + error AlreadyRedeemed(address redeemer, uint256 tokenId); + error Unauthorized(address sender); + error NewSupplyTooLow(uint256 minted, uint256 supply); + event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); event TokenMetadataUpdated( address indexed sender, uint256 indexed classId, uint256 indexed tokenId, string tokenURI @@ -16,6 +22,7 @@ contract Will4USNFTTest is Test { event CampaignMemberAdded(address indexed member); event CampaignMemberRemoved(address indexed member); event ClassAdded(uint256 indexed classId, string metadata); + event Redeemed(address indexed redeemer, uint256 indexed tokenId, uint256 indexed classId); function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); @@ -23,7 +30,9 @@ contract Will4USNFTTest is Test { vm.startPrank(deployerAddress); nftContract.addCampaignMember(deployerAddress); - nftContract.addClass("name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7); + nftContract.addClass( + "name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7 + ); nftContract.awardCampaignItem(makeAddr("recipient1"), 1); vm.stopPrank(); } @@ -104,20 +113,66 @@ contract Will4USNFTTest is Test { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); emit CampaignMemberRemoved(makeAddr("member1")); + nftContract.removeCampaignMember(makeAddr("member1")); + vm.stopPrank(); + assertEq( + nftContract.campaignMembers(makeAddr("member1")), false, "Member should be removed" + ); + } + + function test_redeem() public { + vm.startPrank(deployerAddress); + vm.expectEmit(true, true, true, true); + emit Redeemed(deployerAddress, 1, 1); + + nftContract.redeem(1, deployerAddress); + } + + function test_revert_redeem_AlreadyRedeemed() public { + vm.startPrank(deployerAddress); + nftContract.redeem(1, deployerAddress); + vm.expectRevert(); + + nftContract.redeem(1, deployerAddress); + vm.stopPrank(); + } + + function test_revert_redeem_Unauthorized() public { + vm.startPrank(makeAddr("chad")); + vm.expectRevert(); + nftContract.redeem(1, deployerAddress); vm.stopPrank(); - assertEq(nftContract.campaignMembers(makeAddr("member1")), false, "Member should be removed"); } function test_addClass() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); emit ClassAdded(2, "https://a_new_pointer_to_json_object.io"); - nftContract.addClass("name2", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7); + nftContract.addClass( + "name2", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7 + ); vm.stopPrank(); - assertEq(nftContract.getClassById(2).name, "name2", "Class name should be name"); + ( + uint256 id, + uint256 supply, + uint256 minted, + string memory name, + string memory description, + string memory imagePointer, + string memory metadataPointer + ) = nftContract.classes(2); + assertEq(name, "name2", "Class name should be name"); + } + + function test_revert_addClass_Unauthorized() public { + vm.prank(makeAddr("chad")); + vm.expectRevert(); + nftContract.addClass( + "name2", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7 + ); } function test_getTotalSupplyForClass() public { @@ -128,7 +183,35 @@ contract Will4USNFTTest is Test { vm.prank(deployerAddress); nftContract.setClassTokenSupply(1, 1e10); - assertEq(nftContract.getClassById(1).supply, 1e10, "Total supply should be 1e10"); + ( + uint256 id, + uint256 supply, + uint256 minted, + string memory name, + string memory description, + string memory imagePointer, + string memory metadataPointer + ) = nftContract.classes(1); + assertEq(supply, 1e10, "Total supply should be 1e10"); + } + + function test_revert_setClassTokenSupply_NewSupplyTooLow() public { + vm.startPrank(deployerAddress); + nftContract.setClassTokenSupply(1, 10); + nftContract.awardCampaignItem(makeAddr("recipient1"), 1); + nftContract.awardCampaignItem(makeAddr("recipient2"), 1); + nftContract.awardCampaignItem(makeAddr("recipient3"), 1); + nftContract.awardCampaignItem(makeAddr("recipient4"), 1); + nftContract.awardCampaignItem(makeAddr("recipient5"), 1); + vm.expectRevert(); + nftContract.setClassTokenSupply(1, 5); + vm.stopPrank(); + } + + function test_revert_setClassTokenSupply_Unauthorized() public { + vm.prank(makeAddr("chad")); + vm.expectRevert(); + nftContract.setClassTokenSupply(1, 1e10); } function test_getTotalSupplyForAllClasses() public { @@ -141,4 +224,10 @@ contract Will4USNFTTest is Test { assertEq(nftContract.maxMintablePerClass(), 10, "Total supply should be 1e10"); } + + function test_revert_setMaxMintablePerClass_Unauthorized() public { + vm.prank(makeAddr("chad")); + vm.expectRevert(); + nftContract.setMaxMintablePerClass(10); + } } From 3154ca316504508136d65555cfdaccbefebfd908 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 15 Oct 2023 22:10:00 -0400 Subject: [PATCH 16/41] test: small updates --- test/Will4USNFTDeployTest.t.sol | 16 ++++++++-------- test/Will4USNFTTest.t.sol | 10 +--------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/test/Will4USNFTDeployTest.t.sol b/test/Will4USNFTDeployTest.t.sol index cd708eb..4e1cc6e 100644 --- a/test/Will4USNFTDeployTest.t.sol +++ b/test/Will4USNFTDeployTest.t.sol @@ -20,15 +20,15 @@ contract Will4USNFTDeployTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); nftContract = new Will4USNFT(deployerAddress, 5); - - vm.startPrank(deployerAddress); - nftContract.addCampaignMember(deployerAddress); - nftContract.addClass( - "name", "description", "imagePointer", "https://a_new_pointer_to_json_object.io", 1e7 - ); - nftContract.awardCampaignItem(makeAddr("recipient1"), 1); - vm.stopPrank(); + string memory url = vm.rpcUrl("arbitrumGoerli"); + assertEq(url, "https://arb-goerli.g.alchemy.com/v2/RqTiyvS7OspxaAQUQupKKCTjmf94JL-I"); } // todo: test that the contract is deployed with the correct parameters + function test_deploy() public { + assertEq(nftContract.maxMintablePerClass(), 5, "maxMintablePerClass should be 5"); + assertEq(nftContract.totalClassesSupply(), 0, "totalSupply should be 0"); + assertEq(nftContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); + assertEq(nftContract.owner(), deployerAddress, "owner should be deployerAddress"); + } } diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 63d5266..727c486 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -183,15 +183,7 @@ contract Will4USNFTTest is Test { vm.prank(deployerAddress); nftContract.setClassTokenSupply(1, 1e10); - ( - uint256 id, - uint256 supply, - uint256 minted, - string memory name, - string memory description, - string memory imagePointer, - string memory metadataPointer - ) = nftContract.classes(1); + (, uint256 supply,,,,,) = nftContract.classes(1); assertEq(supply, 1e10, "Total supply should be 1e10"); } From a341c45b8365a928479f3e46c05e98c033617bf5 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Tue, 17 Oct 2023 18:13:27 -0400 Subject: [PATCH 17/41] chore: add token/updates --- DEPLOYMENTS.md | 40 ++++++++++- script/Will4USNFT.s.sol | 3 +- src/Will4USGovernor.sol | 122 ++++++++++++++++++++++++++++++++ src/Will4USNFT.sol | 26 ++++--- src/Will5USToken.sol | 42 +++++++++++ test/Will4USNFTDeployTest.t.sol | 10 ++- test/Will4USNFTTest.t.sol | 2 +- 7 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 src/Will4USGovernor.sol create mode 100644 src/Will5USToken.sol diff --git a/DEPLOYMENTS.md b/DEPLOYMENTS.md index af6e0ee..16c5ba3 100644 --- a/DEPLOYMENTS.md +++ b/DEPLOYMENTS.md @@ -2,4 +2,42 @@ ## Arbitrum Mainnet -| Contract | Address | \ No newline at end of file +| Contract | Address | Link | +| --- | --- | --- | +| Will4UsNFT | 0x0 | [LINK](https://goerli.arbiscan.io/address/0x0) | + +## Arbitrum Testnet + +| Contract | Address | Link | +| --- | --- | --- | +| Will4UsNFT | 0xe2e1f1c872842350c85623c2323914fd24a6c17c | [LINK](https://goerli.arbiscan.io/address/0xe2e1f1c872842350c85623c2323914fd24a6c17c) | + +## Goerli Testnet + +| Contract | Address | Link | +| --- | --- | --- | +| Will4UsNFT | 0x0 | [LINK](https://goerli.etherscan.io/address/0x0) | + +## Base Goerli + +| Contract | Address | Link | +| --- | --- | --- | +| Will4UsNFT | 0x0 | [LINK](https://basescan.org/address/0x0) | + +## Base Mainnet + +| Contract | Address | Link | +| --- | --- | --- | +| Will4UsNFT | 0x0 | [LINK](https://basescan.org/address/0x0) | + +## Optimism Goerli + +| Contract | Address | Link | +| --- | --- | --- | +| Will4UsNFT | 0x0 | [LINK](https://optimistic.etherscan.io/address/0x0) | + +## Optimism Mainnet + +| Contract | Address | Link | +| --- | --- | --- | +| Will4UsNFT | 0x0 | [LINK](https://optimistic.etherscan.io/address/0x0) | diff --git a/script/Will4USNFT.s.sol b/script/Will4USNFT.s.sol index da6aba3..616abed 100644 --- a/script/Will4USNFT.s.sol +++ b/script/Will4USNFT.s.sol @@ -20,7 +20,8 @@ contract Will4USNFTScript is Script { // assertEq(url, "https://arb-goerli.g.alchemy.com/v2/RqTiyvS7OspxaAQUQupKKCTjmf94JL-I"); vm.startBroadcast(deployerPrivateKey); - Will4USNFT nftContract = new Will4USNFT(deployerAddress, 5); + Will4USNFT nftContract = + new Will4USNFT(deployerAddress, deployerAddress, deployerAddress, 5); // nftContract.awardCampaignItem(deployerAddress, 1); diff --git a/src/Will4USGovernor.sol b/src/Will4USGovernor.sol new file mode 100644 index 0000000..e99d66c --- /dev/null +++ b/src/Will4USGovernor.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/governance/Governor.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorStorage.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; + +contract Will4USGovernor is + Governor, + GovernorSettings, + GovernorCountingSimple, + GovernorStorage, + GovernorVotes, + GovernorVotesQuorumFraction, + GovernorTimelockControl +{ + constructor(IVotes _token, TimelockController _timelock) + Governor("Will4USGovernor") + GovernorSettings(7200, /* 1 day */ 50400, /* 1 week */ 0) + GovernorVotes(_token) + GovernorVotesQuorumFraction(4) + GovernorTimelockControl(_timelock) + { } + + // The following functions are overrides required by Solidity. + + function votingDelay() public view override(Governor, GovernorSettings) returns (uint256) { + return super.votingDelay(); + } + + function votingPeriod() public view override(Governor, GovernorSettings) returns (uint256) { + return super.votingPeriod(); + } + + function quorum(uint256 blockNumber) + public + view + override(Governor, GovernorVotesQuorumFraction) + returns (uint256) + { + return super.quorum(blockNumber); + } + + function state(uint256 proposalId) + public + view + override(Governor, GovernorTimelockControl) + returns (ProposalState) + { + return super.state(proposalId); + } + + function proposalNeedsQueuing(uint256 proposalId) + public + view + override(Governor, GovernorTimelockControl) + returns (bool) + { + return super.proposalNeedsQueuing(proposalId); + } + + function proposalThreshold() + public + view + override(Governor, GovernorSettings) + returns (uint256) + { + return super.proposalThreshold(); + } + + function _propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + address proposer + ) internal override(Governor, GovernorStorage) returns (uint256) { + return super._propose(targets, values, calldatas, description, proposer); + } + + function _queueOperations( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) returns (uint48) { + return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash); + } + + function _executeOperations( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) { + super._executeOperations(proposalId, targets, values, calldatas, descriptionHash); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) returns (uint256) { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function _executor() + internal + view + override(Governor, GovernorTimelockControl) + returns (address) + { + return super._executor(); + } +} diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index b3ce459..877bc86 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -5,17 +5,21 @@ import { ERC721URIStorage } from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol"; /// @notice This contract is the Main NFT contract for the Will 4 US Campaign /// @dev This contract is used to mint NFTs for the Will 4 US Campaign /// @author @codenamejason -contract Will4USNFT is ERC721URIStorage, Ownable { +contract Will4USNFT is ERC721URIStorage, AccessControl { using Strings for uint256; /** * State Variables ******** */ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + uint256 private _tokenIds; uint256 public classIds; uint256 public totalClassesSupply; @@ -78,17 +82,17 @@ contract Will4USNFT is ERC721URIStorage, Ownable { /** * Constructor ********* */ - constructor(address owner, uint256 _maxMintablePerClass) + constructor(address defaultAdmin, address minter, address pauser, uint256 _maxMintablePerClass) ERC721("Will 4 US NFT Collection", "WILL4USNFT") - Ownable(owner) { // add the owner to the campaign members - _addCampaignMember(owner); + _addCampaignMember(defaultAdmin); - maxMintablePerClass = _maxMintablePerClass; + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); + _grantRole(PAUSER_ROLE, minter); + _grantRole(MINTER_ROLE, pauser); - // set the owner address - _transferOwnership(owner); + maxMintablePerClass = _maxMintablePerClass; } /** @@ -100,7 +104,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @dev This function is only callable by the owner * @param _member The member to add */ - function addCampaignMember(address _member) external onlyOwner { + function addCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { _addCampaignMember(_member); } @@ -120,7 +124,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { * @dev This function is only callable by the owner * @param _member The member to remove */ - function removeCampaignMember(address _member) external onlyOwner { + function removeCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { campaignMembers[_member] = false; emit CampaignMemberRemoved(_member); @@ -248,7 +252,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { */ function updateTokenMetadata(uint256 _classId, uint256 _tokenId, string memory _newTokenURI) external - onlyOwner + onlyRole(DEFAULT_ADMIN_ROLE) returns (string memory) { if (super.ownerOf(_tokenId) != address(0)) { @@ -382,7 +386,7 @@ contract Will4USNFT is ERC721URIStorage, Ownable { public view virtual - override(ERC721URIStorage) + override(AccessControl, ERC721URIStorage) returns (bool) { } diff --git a/src/Will5USToken.sol b/src/Will5USToken.sol new file mode 100644 index 0000000..0cfaf37 --- /dev/null +++ b/src/Will5USToken.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; + +contract Will4USToken is ERC20, ERC20Burnable, ERC20Pausable, Ownable, ERC20Permit, ERC20Votes { + constructor(address initialOwner) + ERC20("Will4USToken", "W4US") + Ownable(initialOwner) + ERC20Permit("Will4USToken") + { } + + function pause() public onlyOwner { + _pause(); + } + + function unpause() public onlyOwner { + _unpause(); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } + + // The following functions are overrides required by Solidity. + + function _update(address from, address to, uint256 value) + internal + override(ERC20, ERC20Pausable, ERC20Votes) + { + super._update(from, to, value); + } + + function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/test/Will4USNFTDeployTest.t.sol b/test/Will4USNFTDeployTest.t.sol index 4e1cc6e..f7a63d2 100644 --- a/test/Will4USNFTDeployTest.t.sol +++ b/test/Will4USNFTDeployTest.t.sol @@ -9,6 +9,8 @@ contract Will4USNFTDeployTest is Test { Will4USNFT public nftContract; address deployerAddress; + bytes32 constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); + event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); event TokenMetadataUpdated( address indexed sender, uint256 indexed classId, uint256 indexed tokenId, string tokenURI @@ -19,7 +21,7 @@ contract Will4USNFTDeployTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); - nftContract = new Will4USNFT(deployerAddress, 5); + nftContract = new Will4USNFT(deployerAddress, deployerAddress, deployerAddress, 5); string memory url = vm.rpcUrl("arbitrumGoerli"); assertEq(url, "https://arb-goerli.g.alchemy.com/v2/RqTiyvS7OspxaAQUQupKKCTjmf94JL-I"); } @@ -29,6 +31,10 @@ contract Will4USNFTDeployTest is Test { assertEq(nftContract.maxMintablePerClass(), 5, "maxMintablePerClass should be 5"); assertEq(nftContract.totalClassesSupply(), 0, "totalSupply should be 0"); assertEq(nftContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); - assertEq(nftContract.owner(), deployerAddress, "owner should be deployerAddress"); + assertEq( + nftContract.hasRole(DEFAULT_ADMIN_ROLE, deployerAddress), + true, + "default admin should be deployerAddress" + ); } } diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 727c486..b55071d 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -26,7 +26,7 @@ contract Will4USNFTTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); - nftContract = new Will4USNFT(deployerAddress, 5); + nftContract = new Will4USNFT(deployerAddress, deployerAddress, deployerAddress, 5); vm.startPrank(deployerAddress); nftContract.addCampaignMember(deployerAddress); From 810495c8f18863f5eb91b93c1058113cc370ad8c Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Tue, 17 Oct 2023 18:19:25 -0400 Subject: [PATCH 18/41] test: fix test --- test/Will4USNFTDeployTest.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Will4USNFTDeployTest.t.sol b/test/Will4USNFTDeployTest.t.sol index f7a63d2..36d74ba 100644 --- a/test/Will4USNFTDeployTest.t.sol +++ b/test/Will4USNFTDeployTest.t.sol @@ -9,7 +9,7 @@ contract Will4USNFTDeployTest is Test { Will4USNFT public nftContract; address deployerAddress; - bytes32 constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); + bytes32 public constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); event TokenMetadataUpdated( @@ -32,7 +32,7 @@ contract Will4USNFTDeployTest is Test { assertEq(nftContract.totalClassesSupply(), 0, "totalSupply should be 0"); assertEq(nftContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); assertEq( - nftContract.hasRole(DEFAULT_ADMIN_ROLE, deployerAddress), + nftContract.hasRole(nftContract.getRoleAdmin(DEFAULT_ADMIN_ROLE), deployerAddress), true, "default admin should be deployerAddress" ); From 7a83cba087768252518c40b18fd6ec8c57e2a6e9 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Tue, 17 Oct 2023 18:49:09 -0400 Subject: [PATCH 19/41] chore: add access control/remove ownable --- src/Will4USToken.sol | 71 +++++++++++++++++++++++++++++++++++++ src/Will5USToken.sol | 42 ---------------------- test/Will4USTokenTest.t.sol | 30 ++++++++++++++++ 3 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 src/Will4USToken.sol delete mode 100644 src/Will5USToken.sol create mode 100644 test/Will4USTokenTest.t.sol diff --git a/src/Will4USToken.sol b/src/Will4USToken.sol new file mode 100644 index 0000000..3fe16e8 --- /dev/null +++ b/src/Will4USToken.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; + +contract Will4USToken is + ERC20, + ERC20Burnable, + ERC20Pausable, + AccessControl, + ERC20Permit, + ERC20Votes +{ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + modifier onlyAdminAndPauser() { + require( + hasRole(DEFAULT_ADMIN_ROLE, msg.sender) || hasRole(PAUSER_ROLE, msg.sender), + "Caller is not an admin or pauser" + ); + _; + } + + modifier onlyAdminAndMinter() { + require( + hasRole(DEFAULT_ADMIN_ROLE, msg.sender) || hasRole(MINTER_ROLE, msg.sender), + "Caller is not an admin or minter" + ); + _; + } + + constructor(address defaultAdmin, address minter, address pauser) + ERC20("Will4USToken", "W4US") + ERC20Permit("Will4USToken") + { + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); + _grantRole(MINTER_ROLE, minter); + _grantRole(PAUSER_ROLE, pauser); + } + + function pause() public onlyAdminAndPauser { + _pause(); + } + + function unpause() public onlyAdminAndPauser { + _unpause(); + } + + function mint(address to, uint256 amount) public onlyAdminAndMinter { + _mint(to, amount); + } + + // The following functions are overrides required by Solidity. + + function _update(address from, address to, uint256 value) + internal + override(ERC20, ERC20Pausable, ERC20Votes) + { + super._update(from, to, value); + } + + function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/src/Will5USToken.sol b/src/Will5USToken.sol deleted file mode 100644 index 0cfaf37..0000000 --- a/src/Will5USToken.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.20; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; - -contract Will4USToken is ERC20, ERC20Burnable, ERC20Pausable, Ownable, ERC20Permit, ERC20Votes { - constructor(address initialOwner) - ERC20("Will4USToken", "W4US") - Ownable(initialOwner) - ERC20Permit("Will4USToken") - { } - - function pause() public onlyOwner { - _pause(); - } - - function unpause() public onlyOwner { - _unpause(); - } - - function mint(address to, uint256 amount) public onlyOwner { - _mint(to, amount); - } - - // The following functions are overrides required by Solidity. - - function _update(address from, address to, uint256 value) - internal - override(ERC20, ERC20Pausable, ERC20Votes) - { - super._update(from, to, value); - } - - function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) { - return super.nonces(owner); - } -} diff --git a/test/Will4USTokenTest.t.sol b/test/Will4USTokenTest.t.sol new file mode 100644 index 0000000..b5fd7ff --- /dev/null +++ b/test/Will4USTokenTest.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import { Test, console2, StdUtils } from "forge-std/Test.sol"; + +import { Will4USToken } from "../src/Will4USToken.sol"; + +contract Will4USTokenTest is Test { + Will4USToken public tokenContract; + address deployerAddress; + + bytes32 public constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); + + function setUp() public { + deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); + tokenContract = new Will4USToken(deployerAddress, deployerAddress, deployerAddress); + } + + function test_deploy() public { + assertEq(tokenContract.name(), "Will4USToken", "name should be Will4USToken"); + assertEq(tokenContract.symbol(), "W4US", "symbol should be W4US"); + assertEq(tokenContract.totalSupply(), 0, "totalSupply should be 0"); + assertEq(tokenContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); + assertEq( + tokenContract.hasRole(tokenContract.getRoleAdmin(DEFAULT_ADMIN_ROLE), deployerAddress), + true, + "default admin should be deployerAddress" + ); + } +} From ab2196d7e2cd699c0845db54282d56670cab24c4 Mon Sep 17 00:00:00 2001 From: 0xKurt Date: Thu, 19 Oct 2023 23:50:41 +0200 Subject: [PATCH 20/41] small changes --- src/Will4USNFT.sol | 48 +++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 877bc86..971c67a 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -4,8 +4,7 @@ pragma solidity 0.8.20; import { ERC721URIStorage } from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; -import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/access/AccessControl.sol"; +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol"; /// @notice This contract is the Main NFT contract for the Will 4 US Campaign @@ -56,8 +55,6 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { event TokenMetadataUpdated( address indexed sender, uint256 indexed classId, uint256 indexed tokenId, string tokenURI ); - event CampaignMemberAdded(address indexed member); - event CampaignMemberRemoved(address indexed member); event ClassAdded(uint256 indexed classId, string metadata); event UpdatedClassTokenSupply(uint256 indexed classId, uint256 supply); event UpdatedMaxMintablePerClass(uint256 maxMintable); @@ -73,7 +70,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { * @param sender The sender address */ modifier onlyCampaingnMember(address sender) { - if (!campaignMembers[sender]) { + if (!hasRole(MINTER_ROLE, sender)) { revert Unauthorized(sender); } _; @@ -82,17 +79,20 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { /** * Constructor ********* */ - constructor(address defaultAdmin, address minter, address pauser, uint256 _maxMintablePerClass) - ERC721("Will 4 US NFT Collection", "WILL4USNFT") - { + constructor( + address _defaultAdmin, + address _minter, + address _pauser, + uint256 _maxMintablePerClass + ) ERC721("Will 4 US NFT Collection", "WILL4USNFT") { // add the owner to the campaign members - _addCampaignMember(defaultAdmin); + _addCampaignMember(_defaultAdmin); - _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); - _grantRole(PAUSER_ROLE, minter); - _grantRole(MINTER_ROLE, pauser); + _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _grantRole(PAUSER_ROLE, _pauser); + _grantRole(MINTER_ROLE, _minter); - maxMintablePerClass = _maxMintablePerClass; + _setMaxMintablePerClass(_maxMintablePerClass); } /** @@ -114,9 +114,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { * @param _member The member to add */ function _addCampaignMember(address _member) internal { - campaignMembers[_member] = true; - - emit CampaignMemberAdded(_member); + _grantRole(MINTER_ROLE, _member); } /** @@ -125,9 +123,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { * @param _member The member to remove */ function removeCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { - campaignMembers[_member] = false; - - emit CampaignMemberRemoved(_member); + revokeRole(MINTER_ROLE, _member); } /** @@ -303,9 +299,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { external onlyCampaingnMember(msg.sender) { - maxMintablePerClass = _maxMintable; - - emit UpdatedMaxMintablePerClass(_maxMintable); + _setMaxMintablePerClass(_maxMintable); } /** @@ -361,6 +355,16 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { * Internal Functions ****** */ + /** + * @notice Sets the max mintable per wallet + * @dev Intenral function to set the max mintable per wallet + * @param _maxMintable The new max mintable + */ + function _setMaxMintablePerClass(uint256 _maxMintable) internal { + maxMintablePerClass = _maxMintable; + emit UpdatedMaxMintablePerClass(_maxMintable); + } + /** * @notice Mints a new campaign item * @param _recipient The recipient of the item From 544a178c24e00486963e4fb492d363ea4a3182e3 Mon Sep 17 00:00:00 2001 From: 0xKurt Date: Fri, 20 Oct 2023 00:06:06 +0200 Subject: [PATCH 21/41] rework redeem --- src/Will4USNFT.sol | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 971c67a..e0b9196 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -27,7 +27,8 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { mapping(uint256 => Class) public classes; mapping(address => bool) public campaignMembers; mapping(address => mapping(uint256 => uint256)) public mintedPerClass; - mapping(address => mapping(uint256 => bool)) public redeemed; + // eventId => token => bool + mapping(uint256 => mapping(uint256 => bool)) public redeemed; struct Class { uint256 id; @@ -44,7 +45,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { */ error InvalidTokenId(uint256 tokenId); error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); - error AlreadyRedeemed(address redeemer, uint256 tokenId); + error AlreadyRedeemed(uint256 eventId, uint256 tokenId); error Unauthorized(address sender); error NewSupplyTooLow(uint256 minted, uint256 supply); @@ -58,7 +59,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { event ClassAdded(uint256 indexed classId, string metadata); event UpdatedClassTokenSupply(uint256 indexed classId, uint256 supply); event UpdatedMaxMintablePerClass(uint256 maxMintable); - event Redeemed(address indexed redeemer, uint256 indexed tokenId, uint256 indexed classId); + event Redeemed(uint256 indexed eventId, uint256 indexed tokenId, uint256 indexed classId); /** * Modifiers ************ @@ -184,21 +185,21 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { /** * @notice Redeems a campaign item * @dev This function is only callable by campaign members + * @param _eventId The event ID * @param _tokenId The token ID - * @param _redeemer The owner/redeemer of the token */ - function redeem(uint256 _tokenId, address _redeemer) external onlyCampaingnMember(msg.sender) { + function redeem(uint256 _eventId, uint256 _tokenId) external onlyCampaingnMember(msg.sender) { if (super.ownerOf(_tokenId) == address(0)) { revert InvalidTokenId(_tokenId); } - if (redeemed[_redeemer][_tokenId]) { - revert AlreadyRedeemed(_redeemer, _tokenId); + if (redeemed[_eventId][_tokenId]) { + revert AlreadyRedeemed(_eventId, _tokenId); } - redeemed[_redeemer][_tokenId] = true; + redeemed[_eventId][_tokenId] = true; - emit Redeemed(_redeemer, _tokenId, classes[_tokenId].id); + emit Redeemed(_eventId, _tokenId, classes[_tokenId].id); } /** @@ -306,6 +307,17 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { * View Functions ****** */ + /** + * @notice Returns if the token has been redeemed for an event + * @param _eventId The event ID + * @param _tokenId The token ID + * @return bool Returns true if the token has been redeemed + */ + + function getRedeemed(uint256 _eventId, uint256 _tokenId) external view returns (bool) { + return redeemed[_eventId][_tokenId]; + } + /** * @notice Returns the total supply for a class * @param _classId The class ID From 88be97458143f2d46859888d17c58f66d0ff1db9 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Thu, 19 Oct 2023 19:47:47 -0400 Subject: [PATCH 22/41] test: update tests --- test/Will4USNFTTest.t.sol | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index b55071d..764c72e 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -6,6 +6,10 @@ import { Will4USNFT } from "../src/Will4USNFT.sol"; /// @notice This contract is used to test the Will4USNFT contract contract Will4USNFTTest is Test { + bytes32 public constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + Will4USNFT public nftContract; address deployerAddress; @@ -22,7 +26,8 @@ contract Will4USNFTTest is Test { event CampaignMemberAdded(address indexed member); event CampaignMemberRemoved(address indexed member); event ClassAdded(uint256 indexed classId, string metadata); - event Redeemed(address indexed redeemer, uint256 indexed tokenId, uint256 indexed classId); + event Redeemed(uint256 indexed eventId, uint256 indexed tokenId, uint256 indexed classId); + event RoleGranted(bytes32 indexed role, address indexed account, address sender); function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); @@ -101,40 +106,42 @@ contract Will4USNFTTest is Test { function test_addCampaignMember() public { vm.startPrank(deployerAddress); - vm.expectEmit(true, true, true, true); - emit CampaignMemberAdded(makeAddr("member1")); nftContract.addCampaignMember(makeAddr("member1")); vm.stopPrank(); - assertEq(nftContract.campaignMembers(makeAddr("member1")), true, "Member should be added"); + assertEq( + nftContract.hasRole(MINTER_ROLE, makeAddr("member1")), + true, + "Member should have MINTER_ROLE" + ); } function test_removeCampaignMember() public { vm.startPrank(deployerAddress); - vm.expectEmit(true, true, true, true); - emit CampaignMemberRemoved(makeAddr("member1")); nftContract.removeCampaignMember(makeAddr("member1")); vm.stopPrank(); assertEq( - nftContract.campaignMembers(makeAddr("member1")), false, "Member should be removed" + nftContract.hasRole(MINTER_ROLE, makeAddr("member1")), + false, + "Member should NOT have MINTER_ROLE" ); } function test_redeem() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); - emit Redeemed(deployerAddress, 1, 1); + emit Redeemed(1, 1, 1); - nftContract.redeem(1, deployerAddress); + nftContract.redeem(1, 1); } function test_revert_redeem_AlreadyRedeemed() public { vm.startPrank(deployerAddress); - nftContract.redeem(1, deployerAddress); + nftContract.redeem(1, 1); vm.expectRevert(); - nftContract.redeem(1, deployerAddress); + nftContract.redeem(1, 1); vm.stopPrank(); } @@ -142,7 +149,7 @@ contract Will4USNFTTest is Test { vm.startPrank(makeAddr("chad")); vm.expectRevert(); - nftContract.redeem(1, deployerAddress); + nftContract.redeem(1, 1); vm.stopPrank(); } From 1e37880c6ec3d26c5e08d6bff5c45105e0e23f87 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Thu, 19 Oct 2023 22:03:14 -0400 Subject: [PATCH 23/41] chore: try to fix action --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4d4bd9..39cc646 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,8 +16,10 @@ jobs: strategy: fail-fast: true - name: Foundry project + name: Will4USNFT Foundry project runs-on: ubuntu-latest + env: + ARBITRUM_GOERLI_RPC_URL: ${{ secrets.ARBITRUM_GOERLI_RPC_URL }} steps: - uses: actions/checkout@v3 with: From 44b6c311dc10608eab18027138cf6fdd3e0a79bb Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Thu, 19 Oct 2023 22:06:29 -0400 Subject: [PATCH 24/41] chore: update test --- test/Will4USNFTDeployTest.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Will4USNFTDeployTest.t.sol b/test/Will4USNFTDeployTest.t.sol index 36d74ba..a4577aa 100644 --- a/test/Will4USNFTDeployTest.t.sol +++ b/test/Will4USNFTDeployTest.t.sol @@ -22,8 +22,8 @@ contract Will4USNFTDeployTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); nftContract = new Will4USNFT(deployerAddress, deployerAddress, deployerAddress, 5); - string memory url = vm.rpcUrl("arbitrumGoerli"); - assertEq(url, "https://arb-goerli.g.alchemy.com/v2/RqTiyvS7OspxaAQUQupKKCTjmf94JL-I"); + // string memory url = vm.rpcUrl("arbitrumGoerli"); + // assertEq(url, "https://arb-goerli.g.alchemy.com/v2/RqTiyvS7OspxaAQUQupKKCTjmf94JL-I"); } // todo: test that the contract is deployed with the correct parameters From a6bacaac97ed8ac158913339de8e06c3a91bc82b Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Thu, 19 Oct 2023 23:25:10 -0400 Subject: [PATCH 25/41] chore: add token tests/basic --- test/Will4USTokenTest.t.sol | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/Will4USTokenTest.t.sol b/test/Will4USTokenTest.t.sol index b5fd7ff..5f3cda3 100644 --- a/test/Will4USTokenTest.t.sol +++ b/test/Will4USTokenTest.t.sol @@ -27,4 +27,48 @@ contract Will4USTokenTest is Test { "default admin should be deployerAddress" ); } + + function test_mint() public { + vm.startPrank(deployerAddress); + // vm.expectEmit(true, true, true, true); + // emit Transfer(address(0), makeAddr("recipient1"), 10e18); + tokenContract.mint(makeAddr("recipient1"), 10e18); + vm.stopPrank(); + + assertEq(tokenContract.totalSupply(), 10e18, "totalSupply should be 100"); + assertEq(tokenContract.balanceOf(makeAddr("recipient1")), 10e18, "balanceOf should be 100"); + } + + function test_pause() public { + vm.startPrank(deployerAddress); + // vm.expectEmit(true, true, true, true); + // emit Paused(deployerAddress); + tokenContract.pause(); + vm.stopPrank(); + + assertEq(tokenContract.paused(), true, "paused should be true"); + } + + function test_unpause() public { + vm.startPrank(deployerAddress); + // vm.expectEmit(true, true, true, true); + // emit Unpaused(deployerAddress); + tokenContract.pause(); + tokenContract.unpause(); + vm.stopPrank(); + + assertEq(tokenContract.paused(), false, "paused should be false"); + } + + function test_burn() public { + vm.startPrank(deployerAddress); + // vm.expectEmit(true, true, true, true); + // emit Transfer(deployerAddress, address(0), 10e18); + tokenContract.mint(deployerAddress, 10e18); + tokenContract.burn(10e18); + vm.stopPrank(); + + assertEq(tokenContract.totalSupply(), 0, "totalSupply should be 0"); + assertEq(tokenContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); + } } From 60027ba813678dd5694f8b95d21c419a7a9dea74 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 21:40:59 -0400 Subject: [PATCH 26/41] chore: some token work --- src/{Will4USToken.sol => IVBaseToken.sol} | 10 +- src/IVTokenFactory.sol | 118 ++++++++++++++++++ ...STokenTest.t.sol => IVBaseTokenTest.t.sol} | 55 +++++++- 3 files changed, 177 insertions(+), 6 deletions(-) rename src/{Will4USToken.sol => IVBaseToken.sol} (87%) create mode 100644 src/IVTokenFactory.sol rename test/{Will4USTokenTest.t.sol => IVBaseTokenTest.t.sol} (61%) diff --git a/src/Will4USToken.sol b/src/IVBaseToken.sol similarity index 87% rename from src/Will4USToken.sol rename to src/IVBaseToken.sol index 3fe16e8..1cd9d87 100644 --- a/src/Will4USToken.sol +++ b/src/IVBaseToken.sol @@ -8,7 +8,7 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; -contract Will4USToken is +contract IVBaseToken is ERC20, ERC20Burnable, ERC20Pausable, @@ -68,4 +68,12 @@ contract Will4USToken is function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) { return super.nonces(owner); } + + function transfer(address, uint256) public pure override(ERC20) returns (bool) { + revert("Transfer is disabled"); + } + + function transferFrom(address, address, uint256) public pure override(ERC20) returns (bool) { + revert("Transfer is disabled"); + } } diff --git a/src/IVTokenFactory.sol b/src/IVTokenFactory.sol new file mode 100644 index 0000000..dc157d9 --- /dev/null +++ b/src/IVTokenFactory.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +// External +import {CREATE3} from "solady/src/utils/CREATE3.sol"; + +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⢿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⡟⠘⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣾⣿⣿⣿⣿⣾⠻⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⡿⠀⠀⠸⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⢀⣠⣴⣴⣶⣶⣶⣦⣦⣀⡀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⡿⠃⠀⠙⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠁⠀⠀⠀⢻⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠘⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⠃⠀⠀⠀⠀⠈⢿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⣰⣿⣿⣿⡿⠋⠁⠀⠀⠈⠘⠹⣿⣿⣿⣿⣆⠀⠀⠀ +// ⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡀⠀⠀ +// ⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣟⠀⡀⢀⠀⡀⢀⠀⡀⢈⢿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⡇⠀⠀ +// ⠀⠀⣠⣿⣿⣿⣿⣿⣿⡿⠋⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⡿⢿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣷⡀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠸⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠂⠀⠀ +// ⠀⠀⠙⠛⠿⠻⠻⠛⠉⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣧⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⢻⣿⣿⣿⣷⣀⢀⠀⠀⠀⡀⣰⣾⣿⣿⣿⠏⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣧⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠹⢿⣿⣿⣿⣿⣾⣾⣷⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠃⠀⠀⠀⠀⠀⠀⠀⠀⠠⠿⠻⠟⠿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠟⠿⠟⠿⠆⠀⠸⠿⠿⠟⠯⠀⠀⠀⠸⠿⠿⠿⠏⠀⠀⠀⠀⠀⠈⠉⠻⠻⡿⣿⢿⡿⡿⠿⠛⠁⠀⠀⠀⠀⠀⠀ +// allo.gitcoin.co + +/// @title ContractFactory +/// @author @thelostone-mc , @0xKurt , @codenamejason , @0xZakk , @nfrgosselin +/// @dev ContractFactory is used internally to deploy our contracts using CREATE3 +contract ContractFactory { + /// ====================== + /// ======= Errors ======= + /// ====================== + + /// @notice Thrown when the requested salt has already been used. + error SALT_USED(); + + /// @notice Thrown when the caller is not authorized to deploy. + error UNAUTHORIZED(); + + /// ====================== + /// ======= Events ======= + /// ====================== + + /// @notice Emitted when a contract is deployed. + event Deployed(address indexed deployed, bytes32 indexed salt); + + /// ====================== + /// ======= Storage ====== + /// ====================== + + /// @notice Collection of used salts. + mapping(bytes32 => bool) public usedSalts; + + /// @notice Collection of authorized deployers. + mapping(address => bool) public isDeployer; + + /// ====================== + /// ======= Modifiers ==== + /// ====================== + + /// @notice Modifier to ensure the caller is authorized to deploy and returns if not. + modifier onlyDeployer() { + _checkIsDeployer(); + _; + } + + /// ====================== + /// ===== Constructor ==== + /// ====================== + + /// @notice On deployment sets the 'msg.sender' to allowed deployer. + constructor() { + isDeployer[msg.sender] = true; + } + + /// =============================== + /// ====== Internal Functions ===== + /// =============================== + + /// @notice Checks if the caller is authorized to deploy. + function _checkIsDeployer() internal view { + if (!isDeployer[msg.sender]) revert UNAUTHORIZED(); + } + + /// =============================== + /// ====== External Functions ===== + /// =============================== + + /// @notice Deploys a contract using CREATE3. + /// @dev Used for our deployments. + /// @param _contractName Name of the contract to deploy + /// @param _version Version of the contract to deploy + /// @param creationCode Creation code of the contract to deploy + /// @return deployedContract Address of the deployed contract + function deploy(string memory _contractName, string memory _version, bytes memory creationCode) + external + payable + onlyDeployer + returns (address deployedContract) + { + // hash salt with the contract name and version + bytes32 salt = keccak256(abi.encodePacked(_contractName, _version)); + + // ensure salt has not been used + if (usedSalts[salt]) revert SALT_USED(); + + usedSalts[salt] = true; + + deployedContract = CREATE3.deploy(salt, creationCode, msg.value); + + emit Deployed(deployedContract, salt); + } + + /// @notice Set the allowed deployer. + /// @dev 'msg.sender' must be a deployer. + /// @param _deployer Address of the deployer to set + /// @param _allowedToDeploy Boolean to set the deployer to + function setDeployer(address _deployer, bool _allowedToDeploy) external onlyDeployer { + // Set the deployer to the allowedToDeploy mapping + isDeployer[_deployer] = _allowedToDeploy; + } +} diff --git a/test/Will4USTokenTest.t.sol b/test/IVBaseTokenTest.t.sol similarity index 61% rename from test/Will4USTokenTest.t.sol rename to test/IVBaseTokenTest.t.sol index 5f3cda3..107d840 100644 --- a/test/Will4USTokenTest.t.sol +++ b/test/IVBaseTokenTest.t.sol @@ -3,21 +3,21 @@ pragma solidity 0.8.20; import { Test, console2, StdUtils } from "forge-std/Test.sol"; -import { Will4USToken } from "../src/Will4USToken.sol"; +import { IVBaseToken } from "../src/IVBaseToken.sol"; -contract Will4USTokenTest is Test { - Will4USToken public tokenContract; +contract IVBaseTokenTest is Test { + IVBaseToken public tokenContract; address deployerAddress; bytes32 public constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); - tokenContract = new Will4USToken(deployerAddress, deployerAddress, deployerAddress); + tokenContract = new IVBaseToken(deployerAddress, deployerAddress, deployerAddress); } function test_deploy() public { - assertEq(tokenContract.name(), "Will4USToken", "name should be Will4USToken"); + assertEq(tokenContract.name(), "IVBaseToken", "name should be IVBaseToken"); assertEq(tokenContract.symbol(), "W4US", "symbol should be W4US"); assertEq(tokenContract.totalSupply(), 0, "totalSupply should be 0"); assertEq(tokenContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); @@ -39,6 +39,13 @@ contract Will4USTokenTest is Test { assertEq(tokenContract.balanceOf(makeAddr("recipient1")), 10e18, "balanceOf should be 100"); } + function test_revert_mint() public { + vm.startPrank(makeAddr("chad")); + vm.expectRevert(); + tokenContract.mint(makeAddr("recipient1"), 10e18); + vm.stopPrank(); + } + function test_pause() public { vm.startPrank(deployerAddress); // vm.expectEmit(true, true, true, true); @@ -49,6 +56,13 @@ contract Will4USTokenTest is Test { assertEq(tokenContract.paused(), true, "paused should be true"); } + function test_revert_pause() public { + vm.startPrank(makeAddr("chad")); + vm.expectRevert(); + tokenContract.pause(); + vm.stopPrank(); + } + function test_unpause() public { vm.startPrank(deployerAddress); // vm.expectEmit(true, true, true, true); @@ -60,6 +74,16 @@ contract Will4USTokenTest is Test { assertEq(tokenContract.paused(), false, "paused should be false"); } + function test_revert_unpause() public { + vm.prank(deployerAddress); + tokenContract.pause(); + vm.startPrank(makeAddr("chad")); + vm.expectRevert(); + + tokenContract.unpause(); + vm.stopPrank(); + } + function test_burn() public { vm.startPrank(deployerAddress); // vm.expectEmit(true, true, true, true); @@ -71,4 +95,25 @@ contract Will4USTokenTest is Test { assertEq(tokenContract.totalSupply(), 0, "totalSupply should be 0"); assertEq(tokenContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); } + + function test_revert_burn() public { + vm.startPrank(makeAddr("chad")); + vm.expectRevert(); + tokenContract.burn(10e18); + vm.stopPrank(); + } + + function test_revert_transfer() public { + vm.startPrank(makeAddr("chad")); + vm.expectRevert(); + tokenContract.transfer(makeAddr("recipient1"), 10e18); + vm.stopPrank(); + } + + function test_revert_transferFrom() public { + vm.startPrank(makeAddr("chad")); + vm.expectRevert(); + tokenContract.transferFrom(makeAddr("sender1"), makeAddr("recipient1"), 10e18); + vm.stopPrank(); + } } From 8e2d6642f859be03e8ccea3ce1fc83ff30977803 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 21:41:05 -0400 Subject: [PATCH 27/41] forge install: solady v0.0.131 --- .gitmodules | 3 +++ lib/solady | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/solady diff --git a/.gitmodules b/.gitmodules index 69058bd..62dabd3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/chainlink"] path = lib/chainlink url = https://github.com/smartcontractkit/chainlink +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady diff --git a/lib/solady b/lib/solady new file mode 160000 index 0000000..22fa9a5 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 22fa9a52a8012d7bf95ec7b042d99710c524533b From 5fff5dc9e2c6a96e3dad63ee1233740326a2f2a6 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 21:42:20 -0400 Subject: [PATCH 28/41] chore: add libs --- remappings.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/remappings.txt b/remappings.txt index bd83602..ceeca13 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,3 +4,4 @@ erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ @chainlink/=lib/chainlink/contracts/ +eas-contracts/=lib/eas-contracts/contracts/ \ No newline at end of file From 5739eec8376734db9a50979f85506a9c1fbcec53 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 21:42:30 -0400 Subject: [PATCH 29/41] forge install: eas-contracts --- .gitmodules | 3 +++ lib/eas-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/eas-contracts diff --git a/.gitmodules b/.gitmodules index 62dabd3..805e2e5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady +[submodule "lib/eas-contracts"] + path = lib/eas-contracts + url = https://github.com/ethereum-attestation-service/eas-contracts diff --git a/lib/eas-contracts b/lib/eas-contracts new file mode 160000 index 0000000..9df0f78 --- /dev/null +++ b/lib/eas-contracts @@ -0,0 +1 @@ +Subproject commit 9df0f78603b4c686b4800a711f4f5ce355cc801d From 40f6f25eadff9f7a0a890d1a31dea59fa543b5ae Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 22:44:15 -0400 Subject: [PATCH 30/41] chore: work on token factory/update naming --- src/IVBaseToken.sol | 15 ++- src/{Will4USGovernor.sol => IVGovernor.sol} | 7 +- src/IVTokenContractFactory.sol | 115 +++++++++++++++++++ src/IVTokenFactory.sol | 118 -------------------- test/IVBaseTokenTest.t.sol | 7 +- test/IVTokenContractFactory.t.sol | 64 +++++++++++ test/utils/MockIVToken.sol | 14 +++ 7 files changed, 213 insertions(+), 127 deletions(-) rename src/{Will4USGovernor.sol => IVGovernor.sol} (95%) create mode 100644 src/IVTokenContractFactory.sol delete mode 100644 src/IVTokenFactory.sol create mode 100644 test/IVTokenContractFactory.t.sol create mode 100644 test/utils/MockIVToken.sol diff --git a/src/IVBaseToken.sol b/src/IVBaseToken.sol index 1cd9d87..78e1fbe 100644 --- a/src/IVBaseToken.sol +++ b/src/IVBaseToken.sol @@ -8,6 +8,10 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; +/// @title IVBaseToken +/// @notice This is the base token contract used for the IVToken contracts. +/// @dev This contract is used to deploy the projects ERC20 token contracts. +/// @author @codenamejason contract IVBaseToken is ERC20, ERC20Burnable, @@ -35,10 +39,13 @@ contract IVBaseToken is _; } - constructor(address defaultAdmin, address minter, address pauser) - ERC20("Will4USToken", "W4US") - ERC20Permit("Will4USToken") - { + constructor( + address defaultAdmin, + address minter, + address pauser, + string memory name, + string memory symbol + ) ERC20(name, symbol) ERC20Permit(name) { _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); _grantRole(MINTER_ROLE, minter); _grantRole(PAUSER_ROLE, pauser); diff --git a/src/Will4USGovernor.sol b/src/IVGovernor.sol similarity index 95% rename from src/Will4USGovernor.sol rename to src/IVGovernor.sol index e99d66c..80a4225 100644 --- a/src/Will4USGovernor.sol +++ b/src/IVGovernor.sol @@ -9,7 +9,10 @@ import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; -contract Will4USGovernor is +/// @title IVGovernor +/// @notice This is the governor contract used for IV Projects. +/// @author @codenamejason +contract IVGovernor is Governor, GovernorSettings, GovernorCountingSimple, @@ -19,7 +22,7 @@ contract Will4USGovernor is GovernorTimelockControl { constructor(IVotes _token, TimelockController _timelock) - Governor("Will4USGovernor") + Governor("IVGovernor") GovernorSettings(7200, /* 1 day */ 50400, /* 1 week */ 0) GovernorVotes(_token) GovernorVotesQuorumFraction(4) diff --git a/src/IVTokenContractFactory.sol b/src/IVTokenContractFactory.sol new file mode 100644 index 0000000..2d270f2 --- /dev/null +++ b/src/IVTokenContractFactory.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +// External +import { IVBaseToken } from "./IVBaseToken.sol"; + +/// @title IVTokenContractFactory +/// @author @codenamejason +/// @dev IVTokenContractFactory is used to deploy the projects ERC20 token contracts. Please +/// see the README for more information. +contract IVTokenContractFactory { + /// ====================== + /// ======= Errors ======= + /// ====================== + + /// @notice Thrown when the caller is not authorized to deploy. + error UNAUTHORIZED(); + + /// ====================== + /// ======= Events ======= + /// ====================== + + /// @notice Emitted when a contract is deployed. + event Deployed(address indexed deployed); + + /// ====================== + /// ======= Storage ====== + /// ====================== + + struct Token { + address defaultAdmin; + address minter; + address pauser; + string name; + string symbol; + address contractAddress; + } + + /// @notice Collection of authorized deployers. + mapping(address => bool) public isDeployer; + + /// @notice Collection of deployed contracts. + mapping(address => Token) public deployedTokens; + + /// ====================== + /// ======= Modifiers ==== + /// ====================== + + /// @notice Modifier to ensure the caller is authorized to deploy and returns if not. + modifier onlyDeployer() { + _checkIsDeployer(); + _; + } + + /// ====================== + /// ===== Constructor ==== + /// ====================== + + /// @notice On deployment sets the 'msg.sender' to allowed deployer. + constructor() { + isDeployer[msg.sender] = true; + } + + /// =============================== + /// ====== Internal Functions ===== + /// =============================== + + /// @notice Checks if the caller is authorized to deploy. + function _checkIsDeployer() internal view { + if (!isDeployer[msg.sender]) revert UNAUTHORIZED(); + } + + /// =============================== + /// ====== External Functions ===== + /// =============================== + + /// @notice Deploys a token contract. + /// @dev Used for our deployments. + /// @param _defaultAdmin Address of the default admin + /// @param _minter Address of the minter + /// @param _pauser Address of the pauser + /// @param _name Name of the token + /// @param _symbol Symbol of the token + /// @return deployedContract Address of the deployed contract + function deploy( + address _defaultAdmin, + address _minter, + address _pauser, + string memory _name, + string memory _symbol + ) external payable onlyDeployer returns (address deployedContract) { + deployedContract = address(new IVBaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol)); + + // Set the token to the deployedTokens mapping + deployedTokens[deployedContract] = Token({ + defaultAdmin: _defaultAdmin, + minter: _minter, + pauser: _pauser, + name: _name, + symbol: _symbol, + contractAddress: deployedContract + }); + + emit Deployed(deployedContract); + } + + /// @notice Set the allowed deployer. + /// @dev 'msg.sender' must be a deployer. + /// @param _deployer Address of the deployer to set + /// @param _allowedToDeploy Boolean to set the deployer to + function setDeployer(address _deployer, bool _allowedToDeploy) external onlyDeployer { + // Set the deployer to the allowedToDeploy mapping + isDeployer[_deployer] = _allowedToDeploy; + } +} diff --git a/src/IVTokenFactory.sol b/src/IVTokenFactory.sol deleted file mode 100644 index dc157d9..0000000 --- a/src/IVTokenFactory.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.20; - -// External -import {CREATE3} from "solady/src/utils/CREATE3.sol"; - -// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⢿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⡟⠘⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -// ⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣾⣿⣿⣿⣿⣾⠻⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⡿⠀⠀⠸⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⢀⣠⣴⣴⣶⣶⣶⣦⣦⣀⡀⠀⠀⠀⠀⠀⠀ -// ⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⡿⠃⠀⠙⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠁⠀⠀⠀⢻⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀⠀⠀⠀ -// ⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠘⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⠃⠀⠀⠀⠀⠈⢿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⣰⣿⣿⣿⡿⠋⠁⠀⠀⠈⠘⠹⣿⣿⣿⣿⣆⠀⠀⠀ -// ⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡀⠀⠀ -// ⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣟⠀⡀⢀⠀⡀⢀⠀⡀⢈⢿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⡇⠀⠀ -// ⠀⠀⣠⣿⣿⣿⣿⣿⣿⡿⠋⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⡿⢿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣷⡀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠸⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠂⠀⠀ -// ⠀⠀⠙⠛⠿⠻⠻⠛⠉⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣧⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⢻⣿⣿⣿⣷⣀⢀⠀⠀⠀⡀⣰⣾⣿⣿⣿⠏⠀⠀⠀ -// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣧⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠹⢿⣿⣿⣿⣿⣾⣾⣷⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀ -// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠃⠀⠀⠀⠀⠀⠀⠀⠀⠠⠿⠻⠟⠿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠟⠿⠟⠿⠆⠀⠸⠿⠿⠟⠯⠀⠀⠀⠸⠿⠿⠿⠏⠀⠀⠀⠀⠀⠈⠉⠻⠻⡿⣿⢿⡿⡿⠿⠛⠁⠀⠀⠀⠀⠀⠀ -// allo.gitcoin.co - -/// @title ContractFactory -/// @author @thelostone-mc , @0xKurt , @codenamejason , @0xZakk , @nfrgosselin -/// @dev ContractFactory is used internally to deploy our contracts using CREATE3 -contract ContractFactory { - /// ====================== - /// ======= Errors ======= - /// ====================== - - /// @notice Thrown when the requested salt has already been used. - error SALT_USED(); - - /// @notice Thrown when the caller is not authorized to deploy. - error UNAUTHORIZED(); - - /// ====================== - /// ======= Events ======= - /// ====================== - - /// @notice Emitted when a contract is deployed. - event Deployed(address indexed deployed, bytes32 indexed salt); - - /// ====================== - /// ======= Storage ====== - /// ====================== - - /// @notice Collection of used salts. - mapping(bytes32 => bool) public usedSalts; - - /// @notice Collection of authorized deployers. - mapping(address => bool) public isDeployer; - - /// ====================== - /// ======= Modifiers ==== - /// ====================== - - /// @notice Modifier to ensure the caller is authorized to deploy and returns if not. - modifier onlyDeployer() { - _checkIsDeployer(); - _; - } - - /// ====================== - /// ===== Constructor ==== - /// ====================== - - /// @notice On deployment sets the 'msg.sender' to allowed deployer. - constructor() { - isDeployer[msg.sender] = true; - } - - /// =============================== - /// ====== Internal Functions ===== - /// =============================== - - /// @notice Checks if the caller is authorized to deploy. - function _checkIsDeployer() internal view { - if (!isDeployer[msg.sender]) revert UNAUTHORIZED(); - } - - /// =============================== - /// ====== External Functions ===== - /// =============================== - - /// @notice Deploys a contract using CREATE3. - /// @dev Used for our deployments. - /// @param _contractName Name of the contract to deploy - /// @param _version Version of the contract to deploy - /// @param creationCode Creation code of the contract to deploy - /// @return deployedContract Address of the deployed contract - function deploy(string memory _contractName, string memory _version, bytes memory creationCode) - external - payable - onlyDeployer - returns (address deployedContract) - { - // hash salt with the contract name and version - bytes32 salt = keccak256(abi.encodePacked(_contractName, _version)); - - // ensure salt has not been used - if (usedSalts[salt]) revert SALT_USED(); - - usedSalts[salt] = true; - - deployedContract = CREATE3.deploy(salt, creationCode, msg.value); - - emit Deployed(deployedContract, salt); - } - - /// @notice Set the allowed deployer. - /// @dev 'msg.sender' must be a deployer. - /// @param _deployer Address of the deployer to set - /// @param _allowedToDeploy Boolean to set the deployer to - function setDeployer(address _deployer, bool _allowedToDeploy) external onlyDeployer { - // Set the deployer to the allowedToDeploy mapping - isDeployer[_deployer] = _allowedToDeploy; - } -} diff --git a/test/IVBaseTokenTest.t.sol b/test/IVBaseTokenTest.t.sol index 107d840..ae1a9bc 100644 --- a/test/IVBaseTokenTest.t.sol +++ b/test/IVBaseTokenTest.t.sol @@ -13,12 +13,13 @@ contract IVBaseTokenTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); - tokenContract = new IVBaseToken(deployerAddress, deployerAddress, deployerAddress); + tokenContract = + new IVBaseToken(deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST"); } function test_deploy() public { - assertEq(tokenContract.name(), "IVBaseToken", "name should be IVBaseToken"); - assertEq(tokenContract.symbol(), "W4US", "symbol should be W4US"); + assertEq(tokenContract.name(), "TestToken", "name should be TestToken"); + assertEq(tokenContract.symbol(), "TST", "symbol should be TST"); assertEq(tokenContract.totalSupply(), 0, "totalSupply should be 0"); assertEq(tokenContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); assertEq( diff --git a/test/IVTokenContractFactory.t.sol b/test/IVTokenContractFactory.t.sol new file mode 100644 index 0000000..708b2ed --- /dev/null +++ b/test/IVTokenContractFactory.t.sol @@ -0,0 +1,64 @@ +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import { IVTokenContractFactory } from "../src/IVTokenContractFactory.sol"; +import { MockIVToken } from "./utils/MockIVToken.sol"; +import { IVBaseToken } from "../src/IVBaseToken.sol"; + +contract IVTokenContractFactoryTest is Test { + IVTokenContractFactory factoryInstance; + address public deployerAddress; + IVBaseToken public ivBaseToken; + + uint256 private _nonces; + + function setUp() public { + deployerAddress = makeAddr("deployerAddress"); + factoryInstance = new IVTokenContractFactory(); + factoryInstance.setDeployer(deployerAddress, true); + + _nonces = 0; + } + + function test_constructor() public { + assertTrue(factoryInstance.isDeployer(address(this))); + assertTrue(factoryInstance.isDeployer(deployerAddress)); + } + + function test_deploy_shit() public { + address deployedAddress = factoryInstance.deploy( + deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" + ); + + assertNotEq(deployedAddress, address(0)); + vm.startPrank(deployerAddress); + MockIVToken(deployedAddress).mint(deployerAddress, 100e18); + assertEq(MockIVToken(deployedAddress).balanceOf(deployerAddress), 100e18); + vm.stopPrank(); + } + + function testRevert_deploy_UNAUTHORIZED() public { + vm.expectRevert(IVTokenContractFactory.UNAUTHORIZED.selector); + vm.prank(makeAddr("alice")); + factoryInstance.deploy( + deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" + ); + } + + function test_setDeployer() public { + address newContractFactoryAddress = makeAddr("bob"); + + assertFalse(factoryInstance.isDeployer(newContractFactoryAddress)); + factoryInstance.setDeployer(newContractFactoryAddress, true); + assertTrue(factoryInstance.isDeployer(newContractFactoryAddress)); + } + + function testRevert_setDeployer_UNAUTHORIZED() public { + address newContractFactoryAddress = makeAddr("bob"); + + vm.expectRevert(IVTokenContractFactory.UNAUTHORIZED.selector); + vm.prank(makeAddr("alice")); + factoryInstance.setDeployer(newContractFactoryAddress, true); + } +} diff --git a/test/utils/MockIVToken.sol b/test/utils/MockIVToken.sol new file mode 100644 index 0000000..11480b6 --- /dev/null +++ b/test/utils/MockIVToken.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { IVBaseToken } from "../../src/IVBaseToken.sol"; + +contract MockIVToken is IVBaseToken { + constructor( + address defaultAdmin, + address minter, + address pauser, + string memory name, + string memory symbol + ) IVBaseToken(defaultAdmin, minter, pauser, name, symbol) { } +} From f42a0a4e91f7ebd84b0b0b92b7854456a9b9be0b Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 22:47:49 -0400 Subject: [PATCH 31/41] chore: renaming --- src/{IVBaseToken.sol => IVBaseERC20Token.sol} | 4 ++-- ...ctory.sol => IVERC20TokenContractFactory.sol} | 6 +++--- ...aseTokenTest.t.sol => IVBaseERC20Token.t.sol} | 8 ++++---- ...y.t.sol => IVERC20TokenContractFactory.t.sol} | 16 ++++++++-------- test/utils/MockIVToken.sol | 6 +++--- 5 files changed, 20 insertions(+), 20 deletions(-) rename src/{IVBaseToken.sol => IVBaseERC20Token.sol} (97%) rename src/{IVTokenContractFactory.sol => IVERC20TokenContractFactory.sol} (94%) rename test/{IVBaseTokenTest.t.sol => IVBaseERC20Token.t.sol} (93%) rename test/{IVTokenContractFactory.t.sol => IVERC20TokenContractFactory.t.sol} (77%) diff --git a/src/IVBaseToken.sol b/src/IVBaseERC20Token.sol similarity index 97% rename from src/IVBaseToken.sol rename to src/IVBaseERC20Token.sol index 78e1fbe..ffea702 100644 --- a/src/IVBaseToken.sol +++ b/src/IVBaseERC20Token.sol @@ -8,11 +8,11 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; -/// @title IVBaseToken +/// @title IVBaseERC20Token /// @notice This is the base token contract used for the IVToken contracts. /// @dev This contract is used to deploy the projects ERC20 token contracts. /// @author @codenamejason -contract IVBaseToken is +contract IVBaseERC20Token is ERC20, ERC20Burnable, ERC20Pausable, diff --git a/src/IVTokenContractFactory.sol b/src/IVERC20TokenContractFactory.sol similarity index 94% rename from src/IVTokenContractFactory.sol rename to src/IVERC20TokenContractFactory.sol index 2d270f2..5dff7c3 100644 --- a/src/IVTokenContractFactory.sol +++ b/src/IVERC20TokenContractFactory.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.20; // External -import { IVBaseToken } from "./IVBaseToken.sol"; +import { IVBaseERC20Token } from "./IVBaseERC20Token.sol"; /// @title IVTokenContractFactory /// @author @codenamejason /// @dev IVTokenContractFactory is used to deploy the projects ERC20 token contracts. Please /// see the README for more information. -contract IVTokenContractFactory { +contract IVERC20TokenContractFactory { /// ====================== /// ======= Errors ======= /// ====================== @@ -89,7 +89,7 @@ contract IVTokenContractFactory { string memory _name, string memory _symbol ) external payable onlyDeployer returns (address deployedContract) { - deployedContract = address(new IVBaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol)); + deployedContract = address(new IVBaseERC20Token(_defaultAdmin, _minter, _pauser, _name, _symbol)); // Set the token to the deployedTokens mapping deployedTokens[deployedContract] = Token({ diff --git a/test/IVBaseTokenTest.t.sol b/test/IVBaseERC20Token.t.sol similarity index 93% rename from test/IVBaseTokenTest.t.sol rename to test/IVBaseERC20Token.t.sol index ae1a9bc..d29916a 100644 --- a/test/IVBaseTokenTest.t.sol +++ b/test/IVBaseERC20Token.t.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.20; import { Test, console2, StdUtils } from "forge-std/Test.sol"; -import { IVBaseToken } from "../src/IVBaseToken.sol"; +import { IVBaseERC20Token } from "../src/IVBaseERC20Token.sol"; -contract IVBaseTokenTest is Test { - IVBaseToken public tokenContract; +contract IVBaseERC20TokenTest is Test { + IVBaseERC20Token public tokenContract; address deployerAddress; bytes32 public constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); @@ -14,7 +14,7 @@ contract IVBaseTokenTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); tokenContract = - new IVBaseToken(deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST"); + new IVBaseERC20Token(deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST"); } function test_deploy() public { diff --git a/test/IVTokenContractFactory.t.sol b/test/IVERC20TokenContractFactory.t.sol similarity index 77% rename from test/IVTokenContractFactory.t.sol rename to test/IVERC20TokenContractFactory.t.sol index 708b2ed..7d5c490 100644 --- a/test/IVTokenContractFactory.t.sol +++ b/test/IVERC20TokenContractFactory.t.sol @@ -2,20 +2,20 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import { IVTokenContractFactory } from "../src/IVTokenContractFactory.sol"; +import { IVERC20TokenContractFactory } from "../src/IVERC20TokenContractFactory.sol"; import { MockIVToken } from "./utils/MockIVToken.sol"; -import { IVBaseToken } from "../src/IVBaseToken.sol"; +import { IVBaseERC20Token } from "../src/IVBaseERC20Token.sol"; -contract IVTokenContractFactoryTest is Test { - IVTokenContractFactory factoryInstance; +contract IVERC20TokenContractFactoryTest is Test { + IVERC20TokenContractFactory factoryInstance; address public deployerAddress; - IVBaseToken public ivBaseToken; + IVBaseERC20Token public ivBaseToken; uint256 private _nonces; function setUp() public { deployerAddress = makeAddr("deployerAddress"); - factoryInstance = new IVTokenContractFactory(); + factoryInstance = new IVERC20TokenContractFactory(); factoryInstance.setDeployer(deployerAddress, true); _nonces = 0; @@ -39,7 +39,7 @@ contract IVTokenContractFactoryTest is Test { } function testRevert_deploy_UNAUTHORIZED() public { - vm.expectRevert(IVTokenContractFactory.UNAUTHORIZED.selector); + vm.expectRevert(IVERC20TokenContractFactory.UNAUTHORIZED.selector); vm.prank(makeAddr("alice")); factoryInstance.deploy( deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" @@ -57,7 +57,7 @@ contract IVTokenContractFactoryTest is Test { function testRevert_setDeployer_UNAUTHORIZED() public { address newContractFactoryAddress = makeAddr("bob"); - vm.expectRevert(IVTokenContractFactory.UNAUTHORIZED.selector); + vm.expectRevert(IVERC20TokenContractFactory.UNAUTHORIZED.selector); vm.prank(makeAddr("alice")); factoryInstance.setDeployer(newContractFactoryAddress, true); } diff --git a/test/utils/MockIVToken.sol b/test/utils/MockIVToken.sol index 11480b6..9255552 100644 --- a/test/utils/MockIVToken.sol +++ b/test/utils/MockIVToken.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.20; -import { IVBaseToken } from "../../src/IVBaseToken.sol"; +import { IVBaseERC20Token } from "../../src/IVBaseERC20Token.sol"; -contract MockIVToken is IVBaseToken { +contract MockIVToken is IVBaseERC20Token { constructor( address defaultAdmin, address minter, address pauser, string memory name, string memory symbol - ) IVBaseToken(defaultAdmin, minter, pauser, name, symbol) { } + ) IVBaseERC20Token(defaultAdmin, minter, pauser, name, symbol) { } } From 97c52999cf252b7f7db7a58d9df2f3b8de44e320 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 23:36:10 -0400 Subject: [PATCH 32/41] chore: some refactoring/cleanup/nft factory --- ...PersonaAPIConsumer.sol => APIConsumer.sol} | 6 +- ...aseERC20Token.sol => IVERC20BaseToken.sol} | 4 +- src/IVERC20TokenContractFactory.sol | 16 +- src/IVERC721BaseToken.sol | 402 ++++++++++++++++++ src/IVERC721TokenContractFactory.sol | 105 +++++ src/library/Errors.sol | 10 + src/library/Event.sol | 7 + src/library/Metadata.sol | 12 + ...RC20Token.t.sol => IVERC20BaseToken.t.sol} | 16 +- test/IVERC20TokenContractFactory.t.sol | 11 +- test/IVERC721BaseToken.t.sol | 39 ++ test/{utils => mocks}/MockIVToken.sol | 6 +- 12 files changed, 598 insertions(+), 36 deletions(-) rename src/{PersonaAPIConsumer.sol => APIConsumer.sol} (93%) rename src/{IVBaseERC20Token.sol => IVERC20BaseToken.sol} (97%) create mode 100644 src/IVERC721BaseToken.sol create mode 100644 src/IVERC721TokenContractFactory.sol create mode 100644 src/library/Errors.sol create mode 100644 src/library/Event.sol create mode 100644 src/library/Metadata.sol rename test/{IVBaseERC20Token.t.sol => IVERC20BaseToken.t.sol} (83%) create mode 100644 test/IVERC721BaseToken.t.sol rename test/{utils => mocks}/MockIVToken.sol (57%) diff --git a/src/PersonaAPIConsumer.sol b/src/APIConsumer.sol similarity index 93% rename from src/PersonaAPIConsumer.sol rename to src/APIConsumer.sol index 865fe32..d00620f 100644 --- a/src/PersonaAPIConsumer.sol +++ b/src/APIConsumer.sol @@ -5,10 +5,10 @@ import "@chainlink/src/v0.8/ChainlinkClient.sol"; import "@chainlink/src/v0.8/shared/access/ConfirmedOwner.sol"; /** - * @title The PersonaAPIConsumer contract - * @notice An API Consumer contract that makes GET requests to obtain KYC data + * @title The APIConsumer contract + * @notice An API Consumer contract that makes GET requests */ -contract PersonaAPIConsumer is ChainlinkClient, ConfirmedOwner { +contract APIConsumer is ChainlinkClient, ConfirmedOwner { using Chainlink for Chainlink.Request; bytes32 private jobId; diff --git a/src/IVBaseERC20Token.sol b/src/IVERC20BaseToken.sol similarity index 97% rename from src/IVBaseERC20Token.sol rename to src/IVERC20BaseToken.sol index ffea702..5843ef0 100644 --- a/src/IVBaseERC20Token.sol +++ b/src/IVERC20BaseToken.sol @@ -8,11 +8,11 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; -/// @title IVBaseERC20Token +/// @title IVERC20BaseToken /// @notice This is the base token contract used for the IVToken contracts. /// @dev This contract is used to deploy the projects ERC20 token contracts. /// @author @codenamejason -contract IVBaseERC20Token is +contract IVERC20BaseToken is ERC20, ERC20Burnable, ERC20Pausable, diff --git a/src/IVERC20TokenContractFactory.sol b/src/IVERC20TokenContractFactory.sol index 5dff7c3..f2f8d65 100644 --- a/src/IVERC20TokenContractFactory.sol +++ b/src/IVERC20TokenContractFactory.sol @@ -1,21 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.20; -// External -import { IVBaseERC20Token } from "./IVBaseERC20Token.sol"; +import { IVERC20BaseToken } from "./IVERC20BaseToken.sol"; +import { Errors } from "./library/Errors.sol"; /// @title IVTokenContractFactory /// @author @codenamejason /// @dev IVTokenContractFactory is used to deploy the projects ERC20 token contracts. Please /// see the README for more information. contract IVERC20TokenContractFactory { - /// ====================== - /// ======= Errors ======= - /// ====================== - - /// @notice Thrown when the caller is not authorized to deploy. - error UNAUTHORIZED(); - /// ====================== /// ======= Events ======= /// ====================== @@ -67,7 +60,7 @@ contract IVERC20TokenContractFactory { /// @notice Checks if the caller is authorized to deploy. function _checkIsDeployer() internal view { - if (!isDeployer[msg.sender]) revert UNAUTHORIZED(); + if (!isDeployer[msg.sender]) revert Errors.Unauthorized(msg.sender); } /// =============================== @@ -89,7 +82,8 @@ contract IVERC20TokenContractFactory { string memory _name, string memory _symbol ) external payable onlyDeployer returns (address deployedContract) { - deployedContract = address(new IVBaseERC20Token(_defaultAdmin, _minter, _pauser, _name, _symbol)); + deployedContract = + address(new IVERC20BaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol)); // Set the token to the deployedTokens mapping deployedTokens[deployedContract] = Token({ diff --git a/src/IVERC721BaseToken.sol b/src/IVERC721BaseToken.sol new file mode 100644 index 0000000..cbb2930 --- /dev/null +++ b/src/IVERC721BaseToken.sol @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +import "./library/Errors.sol"; + +contract IVERC721BaseToken is + ERC721, + ERC721Enumerable, + ERC721URIStorage, + ERC721Pausable, + AccessControl, + ERC721Burnable, + EIP712, + ERC721Votes +{ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + uint256 private _tokenIds; + uint256 public classIds; + uint256 public totalClassesSupply; + uint256 public maxMintablePerClass; + + mapping(uint256 => Class) public classes; + mapping(address => bool) public campaignMembers; + mapping(address => mapping(uint256 => uint256)) public mintedPerClass; + // eventId => token => bool + mapping(uint256 => mapping(uint256 => bool)) public redeemed; + + struct Class { + uint256 id; + uint256 supply; // total supply of this class? do we want this? + uint256 minted; + string name; + string description; + string imagePointer; + string metadata; // this is a pointer to json object that contains the metadata for this class + } + + /** + * Events ************ + */ + event ItemAwarded(uint256 indexed tokenId, address indexed recipient, uint256 indexed classId); + event TokenMetadataUpdated( + address indexed sender, uint256 indexed classId, uint256 indexed tokenId, string tokenURI + ); + event ClassAdded(uint256 indexed classId, string metadata); + event UpdatedClassTokenSupply(uint256 indexed classId, uint256 supply); + event UpdatedMaxMintablePerClass(uint256 maxMintable); + event Redeemed(uint256 indexed eventId, uint256 indexed tokenId, uint256 indexed classId); + + /** + * Modifiers ************ + */ + + /** + * @notice Checks if the sender is a campaign member + * @dev This modifier is used to check if the sender is a campaign member + * @param sender The sender address + */ + modifier onlyCampaingnMember(address sender) { + if (!hasRole(MINTER_ROLE, sender)) { + revert Errors.Unauthorized(sender); + } + _; + } + + constructor( + address _defaultAdmin, + address _pauser, + address _minter, + string memory _name, + string memory _symbol + ) ERC721(_name, _symbol) EIP712(_name, "1") { + _addCampaignMember(_defaultAdmin); + + _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _grantRole(PAUSER_ROLE, _pauser); + _grantRole(MINTER_ROLE, _minter); + } + + /** + * External Functions ***** + */ + + /** + * @notice Adds a campaign member + * @dev This function is only callable by the owner + * @param _member The member to add + */ + function addCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { + _addCampaignMember(_member); + } + + /** + * @notice Adds a campaign member + * @dev This function is internal + * @param _member The member to add + */ + function _addCampaignMember(address _member) internal { + _grantRole(MINTER_ROLE, _member); + } + + /** + * @notice Removes a campaign member + * @dev This function is only callable by the owner + * @param _member The member to remove + */ + function removeCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { + revokeRole(MINTER_ROLE, _member); + } + + /** + * @notice Awards campaign nft to supporter + * @dev This function is only callable by campaign members + * @param _recipient The recipient of the item + * @param _classId The class ID + */ + function awardCampaignItem(address _recipient, uint256 _classId) + external + onlyCampaingnMember(msg.sender) + returns (uint256) + { + if (mintedPerClass[_recipient][_classId] > maxMintablePerClass) { + revert Errors.MaxMintablePerClassReached(_recipient, _classId, maxMintablePerClass); + } + + uint256 tokenId = _mintCampaingnItem(_recipient, _classId); + mintedPerClass[_recipient][_classId]++; + + emit ItemAwarded(tokenId, _recipient, _classId); + + return tokenId; + } + + /** + * @notice Awards campaign nft to a batch of supporters + * @dev This function is only callable by campaign members + * @param _recipients The recipients of the item + * @param _classIds The class IDs + */ + function batchAwardCampaignItem(address[] memory _recipients, uint256[] memory _classIds) + external + onlyCampaingnMember(msg.sender) + returns (uint256[] memory) + { + uint256 length = _recipients.length; + uint256[] memory tokenIds = new uint256[](length); + + for (uint256 i = 0; i < length;) { + if (mintedPerClass[_recipients[i]][_classIds[i]] > maxMintablePerClass) { + revert("You have reached the max mintable for this class"); + } + + tokenIds[i] = _mintCampaingnItem(_recipients[i], _classIds[i]); + mintedPerClass[_recipients[i]][_classIds[i]]++; + + emit ItemAwarded(tokenIds[i], _recipients[i], _classIds[i]); + + unchecked { + ++i; + } + } + + return tokenIds; + } + + /** + * @notice Redeems a campaign item + * @dev This function is only callable by campaign members + * @param _eventId The event ID + * @param _tokenId The token ID + */ + function redeem(uint256 _eventId, uint256 _tokenId) external onlyCampaingnMember(msg.sender) { + if (super.ownerOf(_tokenId) == address(0)) { + revert Errors.InvalidTokenId(_tokenId); + } + + if (redeemed[_eventId][_tokenId]) { + revert Errors.AlreadyRedeemed(_eventId, _tokenId); + } + + redeemed[_eventId][_tokenId] = true; + + emit Redeemed(_eventId, _tokenId, classes[_tokenId].id); + } + + /** + * @notice Adds a new class to the campaign for issuance + * @dev This function is only callable by campaign members + * @param _name The name of the class + * @param _description The description of the class + * @param _imagePointer The image pointer for the class + * @param _metadata The metadata pointer for the class + * @param _supply The total supply of the class + */ + function addClass( + string memory _name, + string memory _description, + string memory _imagePointer, + string memory _metadata, + uint256 _supply + ) external onlyCampaingnMember(msg.sender) { + uint256 id = ++classIds; + totalClassesSupply += _supply; + + classes[id] = Class(id, _supply, 0, _name, _description, _imagePointer, _metadata); + + emit ClassAdded(id, _metadata); + } + + /** + * @notice Returns all classes + */ + function getAllClasses() public view returns (Class[] memory) { + Class[] memory _classes = new Class[](classIds); + + for (uint256 i = 0; i < classIds; i++) { + _classes[i] = classes[i + 1]; + } + + return _classes; + } + + /** + * @notice Updates the token metadata + * @dev This function is only callable by campaign members - only use if you really need to + * @param _tokenId The token ID to update + * @param _classId The class ID + * @param _newTokenURI The new token URI 🚨 must be a pointer to a json object 🚨 + * @return The new token URI + */ + function updateTokenMetadata(uint256 _classId, uint256 _tokenId, string memory _newTokenURI) + external + onlyRole(DEFAULT_ADMIN_ROLE) + returns (string memory) + { + if (super.ownerOf(_tokenId) != address(0)) { + _setTokenURI(_tokenId, _newTokenURI); + + emit TokenMetadataUpdated(msg.sender, _classId, _tokenId, tokenURI(_tokenId)); + + return tokenURI(_tokenId); + } else { + revert Errors.InvalidTokenId(_tokenId); + } + } + + /** + * @notice Sets the class token supply + * @dev This function is only callable by campaign members + * @param _classId The class ID + * @param _supply The new supply + */ + function setClassTokenSupply(uint256 _classId, uint256 _supply) + external + onlyCampaingnMember(msg.sender) + { + uint256 currentSupply = classes[_classId].supply; + uint256 minted = classes[_classId].minted; + + if (_supply < currentSupply) { + // if the new supply is less than the current supply, we need to check if the new supply is less than the minted + // if it is, then we need to revert + if (_supply < minted) { + revert Errors.NewSupplyTooLow(minted, _supply); + } + } + + // update the total supply + totalClassesSupply = totalClassesSupply - currentSupply + _supply; + classes[_classId].supply = _supply; + + emit UpdatedClassTokenSupply(_classId, _supply); + } + + /** + * View Functions ****** + */ + + /** + * @notice Returns if the token has been redeemed for an event + * @param _eventId The event ID + * @param _tokenId The token ID + * @return bool Returns true if the token has been redeemed + */ + + function getRedeemed(uint256 _eventId, uint256 _tokenId) external view returns (bool) { + return redeemed[_eventId][_tokenId]; + } + + /** + * @notice Returns the total supply for a class + * @param _classId The class ID + */ + function getTotalSupplyForClass(uint256 _classId) external view returns (uint256) { + return classes[_classId].supply; + } + + /** + * @notice Returns the total supply for all classes + */ + function getTotalSupplyForAllClasses() external view returns (uint256) { + return totalClassesSupply; + } + + /** + * @notice Returns `_baseURI` for the `tokenURI` + */ + function _baseURI() internal pure override returns (string memory) { + // TODO: 🚨 update this when production ready 🚨 + return string.concat( + "https://pharo.mypinata.cloud/ipfs/QmSnzdnhtCuJ6yztHmtYFT7eU2hFF17QNM6rsNohFn6csg/" + ); + } + + /** + * @notice Returns the `tokenURI` + * @param _classId The class ID + * @param _tokenId The token ID + */ + function getTokenURI(uint256 _classId, uint256 _tokenId) public pure returns (string memory) { + string memory classId = Strings.toString(_classId); + string memory tokenId = Strings.toString(_tokenId); + + return string.concat(classId, "/", tokenId, ".json"); + } + + /** + * @notice Returns the owner of the token + * @param _tokenId The token ID + */ + function getOwnerOfToken(uint256 _tokenId) external view returns (address) { + return super.ownerOf(_tokenId); + } + + /** + * Internal Functions ****** + */ + + /** + * @notice Mints a new campaign item + * @param _recipient The recipient of the item + * @param _classId The class ID + */ + function _mintCampaingnItem(address _recipient, uint256 _classId) internal returns (uint256) { + uint256 tokenId = ++_tokenIds; + + // update the class minted count + classes[_classId].minted++; + + _safeMint(_recipient, tokenId); + _setTokenURI(tokenId, getTokenURI(_classId, tokenId)); + + return tokenId; + } + + /** + * Overrides + */ + + function _update(address to, uint256 tokenId, address auth) + internal + override(ERC721, ERC721Enumerable, ERC721Pausable, ERC721Votes) + returns (address) + { + return super._update(to, tokenId, auth); + } + + function _increaseBalance(address account, uint128 value) + internal + override(ERC721, ERC721Enumerable, ERC721Votes) + { + super._increaseBalance(account, value); + } + + function tokenURI(uint256 tokenId) + public + view + override(ERC721, ERC721URIStorage) + returns (string memory) + { + return super.tokenURI(tokenId); + } + + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC721, ERC721Enumerable, ERC721URIStorage, AccessControl) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} diff --git a/src/IVERC721TokenContractFactory.sol b/src/IVERC721TokenContractFactory.sol new file mode 100644 index 0000000..bf9d7cd --- /dev/null +++ b/src/IVERC721TokenContractFactory.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { IVERC721BaseToken } from "./IVERC721BaseToken.sol"; +import { Errors } from "./library/Errors.sol"; + +contract IVERC721TokenContractFactory { + /// ====================== + /// ======= Events ======= + /// ====================== + + /// @notice Emitted when a contract is deployed. + event Deployed(address indexed deployed); + + /// ====================== + /// ======= Storage ====== + /// ====================== + + struct Token { + address defaultAdmin; + address minter; + address pauser; + string name; + string symbol; + address contractAddress; + } + + /// @notice Collection of authorized deployers. + mapping(address => bool) public isDeployer; + + /// @notice Collection of deployed contracts. + mapping(address => Token) public deployedTokens; + + /// ====================== + /// ======= Modifiers ==== + /// ====================== + + /// @notice Modifier to ensure the caller is authorized to deploy and returns if not. + modifier onlyDeployer() { + _checkIsDeployer(); + _; + } + + /// ====================== + /// ===== Constructor ==== + /// ====================== + + /// @notice On deployment sets the 'msg.sender' to allowed deployer. + constructor() { + isDeployer[msg.sender] = true; + } + + /// =============================== + /// ====== Internal Functions ===== + /// =============================== + + /// @notice Checks if the caller is authorized to deploy. + function _checkIsDeployer() internal view { + if (!isDeployer[msg.sender]) revert Errors.Unauthorized(msg.sender); + } + + /// =============================== + /// ====== External Functions ===== + /// =============================== + + /// @notice Deploys a token contract. + /// @dev Used for our deployments. + /// @param _defaultAdmin Address of the default admin + /// @param _minter Address of the minter + /// @param _pauser Address of the pauser + /// @param _name Name of the token + /// @param _symbol Symbol of the token + /// @return deployedContract Address of the deployed contract + function create( + address _defaultAdmin, + address _minter, + address _pauser, + string memory _name, + string memory _symbol + ) external payable onlyDeployer returns (address deployedContract) { + deployedContract = + address(new IVERC721BaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol)); + + // Set the token to the deployedTokens mapping + deployedTokens[deployedContract] = Token({ + defaultAdmin: _defaultAdmin, + minter: _minter, + pauser: _pauser, + name: _name, + symbol: _symbol, + contractAddress: deployedContract + }); + + emit Deployed(deployedContract); + } + + /// @notice Set the allowed deployer. + /// @dev 'msg.sender' must be a deployer. + /// @param _deployer Address of the deployer to set + /// @param _allowedToDeploy Boolean to set the deployer to + function setDeployer(address _deployer, bool _allowedToDeploy) external onlyDeployer { + // Set the deployer to the allowedToDeploy mapping + isDeployer[_deployer] = _allowedToDeploy; + } +} diff --git a/src/library/Errors.sol b/src/library/Errors.sol new file mode 100644 index 0000000..48fd391 --- /dev/null +++ b/src/library/Errors.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +library Errors { + error Unauthorized(address caller); + error InvalidTokenId(uint256 tokenId); + error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); + error AlreadyRedeemed(uint256 eventId, uint256 tokenId); + error NewSupplyTooLow(uint256 minted, uint256 supply); +} diff --git a/src/library/Event.sol b/src/library/Event.sol new file mode 100644 index 0000000..3ef60d2 --- /dev/null +++ b/src/library/Event.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +struct Event { + bytes32 id; + address creator; +} diff --git a/src/library/Metadata.sol b/src/library/Metadata.sol new file mode 100644 index 0000000..d5dd93d --- /dev/null +++ b/src/library/Metadata.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +/// @title Metadata +/// @author @codenamejason +/// @notice Metadata is used to define the metadata that is used throughout the system. +struct Metadata { + /// @notice Protocol ID corresponding to a specific protocol (currently using IPFS = 1) + uint256 protocol; + /// @notice Pointer (hash) to fetch metadata for the specified protocol + string pointer; +} diff --git a/test/IVBaseERC20Token.t.sol b/test/IVERC20BaseToken.t.sol similarity index 83% rename from test/IVBaseERC20Token.t.sol rename to test/IVERC20BaseToken.t.sol index d29916a..44b48f6 100644 --- a/test/IVBaseERC20Token.t.sol +++ b/test/IVERC20BaseToken.t.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.20; import { Test, console2, StdUtils } from "forge-std/Test.sol"; -import { IVBaseERC20Token } from "../src/IVBaseERC20Token.sol"; +import { IVERC20BaseToken } from "../src/IVERC20BaseToken.sol"; -contract IVBaseERC20TokenTest is Test { - IVBaseERC20Token public tokenContract; +contract IVERC20BaseTokenTest is Test { + IVERC20BaseToken public tokenContract; address deployerAddress; bytes32 public constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); @@ -14,7 +14,7 @@ contract IVBaseERC20TokenTest is Test { function setUp() public { deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); tokenContract = - new IVBaseERC20Token(deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST"); + new IVERC20BaseToken(deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST"); } function test_deploy() public { @@ -31,8 +31,6 @@ contract IVBaseERC20TokenTest is Test { function test_mint() public { vm.startPrank(deployerAddress); - // vm.expectEmit(true, true, true, true); - // emit Transfer(address(0), makeAddr("recipient1"), 10e18); tokenContract.mint(makeAddr("recipient1"), 10e18); vm.stopPrank(); @@ -49,8 +47,6 @@ contract IVBaseERC20TokenTest is Test { function test_pause() public { vm.startPrank(deployerAddress); - // vm.expectEmit(true, true, true, true); - // emit Paused(deployerAddress); tokenContract.pause(); vm.stopPrank(); @@ -66,8 +62,6 @@ contract IVBaseERC20TokenTest is Test { function test_unpause() public { vm.startPrank(deployerAddress); - // vm.expectEmit(true, true, true, true); - // emit Unpaused(deployerAddress); tokenContract.pause(); tokenContract.unpause(); vm.stopPrank(); @@ -87,8 +81,6 @@ contract IVBaseERC20TokenTest is Test { function test_burn() public { vm.startPrank(deployerAddress); - // vm.expectEmit(true, true, true, true); - // emit Transfer(deployerAddress, address(0), 10e18); tokenContract.mint(deployerAddress, 10e18); tokenContract.burn(10e18); vm.stopPrank(); diff --git a/test/IVERC20TokenContractFactory.t.sol b/test/IVERC20TokenContractFactory.t.sol index 7d5c490..ed739bc 100644 --- a/test/IVERC20TokenContractFactory.t.sol +++ b/test/IVERC20TokenContractFactory.t.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; import { IVERC20TokenContractFactory } from "../src/IVERC20TokenContractFactory.sol"; -import { MockIVToken } from "./utils/MockIVToken.sol"; -import { IVBaseERC20Token } from "../src/IVBaseERC20Token.sol"; +import { MockIVToken } from "./mocks/MockIVToken.sol"; +import { IVERC20BaseToken } from "../src/IVERC20BaseToken.sol"; +import { Errors } from "../src/library/Errors.sol"; contract IVERC20TokenContractFactoryTest is Test { IVERC20TokenContractFactory factoryInstance; address public deployerAddress; - IVBaseERC20Token public ivBaseToken; + IVERC20BaseToken public ivBaseToken; uint256 private _nonces; @@ -39,7 +40,7 @@ contract IVERC20TokenContractFactoryTest is Test { } function testRevert_deploy_UNAUTHORIZED() public { - vm.expectRevert(IVERC20TokenContractFactory.UNAUTHORIZED.selector); + vm.expectRevert(); vm.prank(makeAddr("alice")); factoryInstance.deploy( deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" @@ -57,7 +58,7 @@ contract IVERC20TokenContractFactoryTest is Test { function testRevert_setDeployer_UNAUTHORIZED() public { address newContractFactoryAddress = makeAddr("bob"); - vm.expectRevert(IVERC20TokenContractFactory.UNAUTHORIZED.selector); + vm.expectRevert(); vm.prank(makeAddr("alice")); factoryInstance.setDeployer(newContractFactoryAddress, true); } diff --git a/test/IVERC721BaseToken.t.sol b/test/IVERC721BaseToken.t.sol new file mode 100644 index 0000000..61fe09a --- /dev/null +++ b/test/IVERC721BaseToken.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.20; + +import { Test, console2, StdUtils } from "forge-std/Test.sol"; + +import { IVERC721BaseToken } from "../src/IVERC721BaseToken.sol"; + +contract IVERC721BaseTokenTest is Test { + address deployerAddress; + IVERC721BaseToken public tokenContract; + + function setUp() public { + deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); + tokenContract = new IVERC721BaseToken( + deployerAddress, + deployerAddress, + deployerAddress, + "TestToken NFT", + "TST" + ); + } + + function test_deploy() public { + assertEq(tokenContract.name(), "TestToken NFT", "name should be TestToken NFT"); + assertEq(tokenContract.symbol(), "TST", "symbol should be TST"); + assertEq(tokenContract.totalSupply(), 0, "totalSupply should be 0"); + assertEq(tokenContract.balanceOf(deployerAddress), 0, "balanceOf should be 0"); + } + + function test_mint() public { + vm.startPrank(deployerAddress); + tokenContract.addClass("Volunteer", "Test volunteer class", "https://yourpointer", "", 500000); + tokenContract.awardCampaignItem(makeAddr("recipient1"), 1); + vm.stopPrank(); + + assertEq(tokenContract.totalSupply(), 1, "totalSupply should be 1"); + assertEq(tokenContract.balanceOf(makeAddr("recipient1")), 1, "balanceOf should be 1"); + } +} \ No newline at end of file diff --git a/test/utils/MockIVToken.sol b/test/mocks/MockIVToken.sol similarity index 57% rename from test/utils/MockIVToken.sol rename to test/mocks/MockIVToken.sol index 9255552..27b2a5a 100644 --- a/test/utils/MockIVToken.sol +++ b/test/mocks/MockIVToken.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.20; -import { IVBaseERC20Token } from "../../src/IVBaseERC20Token.sol"; +import { IVERC20BaseToken } from "../../src/IVERC20BaseToken.sol"; -contract MockIVToken is IVBaseERC20Token { +contract MockIVToken is IVERC20BaseToken { constructor( address defaultAdmin, address minter, address pauser, string memory name, string memory symbol - ) IVBaseERC20Token(defaultAdmin, minter, pauser, name, symbol) { } + ) IVERC20BaseToken(defaultAdmin, minter, pauser, name, symbol) { } } From c6f659247d1d24ad1c491b9e76297c245970620f Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 23:36:19 -0400 Subject: [PATCH 33/41] fmt --- test/IVERC721BaseToken.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/IVERC721BaseToken.t.sol b/test/IVERC721BaseToken.t.sol index 61fe09a..703adff 100644 --- a/test/IVERC721BaseToken.t.sol +++ b/test/IVERC721BaseToken.t.sol @@ -29,11 +29,13 @@ contract IVERC721BaseTokenTest is Test { function test_mint() public { vm.startPrank(deployerAddress); - tokenContract.addClass("Volunteer", "Test volunteer class", "https://yourpointer", "", 500000); + tokenContract.addClass( + "Volunteer", "Test volunteer class", "https://yourpointer", "", 500000 + ); tokenContract.awardCampaignItem(makeAddr("recipient1"), 1); vm.stopPrank(); assertEq(tokenContract.totalSupply(), 1, "totalSupply should be 1"); assertEq(tokenContract.balanceOf(makeAddr("recipient1")), 1, "balanceOf should be 1"); } -} \ No newline at end of file +} From cc668ee2c89572df053f464275c5466c7e589d67 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 23:49:32 -0400 Subject: [PATCH 34/41] chore: add tests/add mock --- src/IVERC20TokenContractFactory.sol | 2 +- src/IVERC721TokenContractFactory.sol | 5 +- test/IVERC20TokenContractFactory.t.sol | 10 +-- test/IVERC721TokenContractFactory.t.sol | 66 +++++++++++++++++++ .../{MockIVToken.sol => MockIVERC20Token.sol} | 2 +- test/mocks/MockIVERC721Token.sol | 14 ++++ 6 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 test/IVERC721TokenContractFactory.t.sol rename test/mocks/{MockIVToken.sol => MockIVERC20Token.sol} (88%) create mode 100644 test/mocks/MockIVERC721Token.sol diff --git a/src/IVERC20TokenContractFactory.sol b/src/IVERC20TokenContractFactory.sol index f2f8d65..09037ab 100644 --- a/src/IVERC20TokenContractFactory.sol +++ b/src/IVERC20TokenContractFactory.sol @@ -75,7 +75,7 @@ contract IVERC20TokenContractFactory { /// @param _name Name of the token /// @param _symbol Symbol of the token /// @return deployedContract Address of the deployed contract - function deploy( + function create( address _defaultAdmin, address _minter, address _pauser, diff --git a/src/IVERC721TokenContractFactory.sol b/src/IVERC721TokenContractFactory.sol index bf9d7cd..a0e66aa 100644 --- a/src/IVERC721TokenContractFactory.sol +++ b/src/IVERC721TokenContractFactory.sol @@ -78,8 +78,9 @@ contract IVERC721TokenContractFactory { string memory _name, string memory _symbol ) external payable onlyDeployer returns (address deployedContract) { - deployedContract = - address(new IVERC721BaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol)); + deployedContract = address( + new IVERC721BaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol) + ); // Set the token to the deployedTokens mapping deployedTokens[deployedContract] = Token({ diff --git a/test/IVERC20TokenContractFactory.t.sol b/test/IVERC20TokenContractFactory.t.sol index ed739bc..fc21ed0 100644 --- a/test/IVERC20TokenContractFactory.t.sol +++ b/test/IVERC20TokenContractFactory.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; import { IVERC20TokenContractFactory } from "../src/IVERC20TokenContractFactory.sol"; -import { MockIVToken } from "./mocks/MockIVToken.sol"; +import { MockIVERC20Token } from "./mocks/MockIVERC20Token.sol"; import { IVERC20BaseToken } from "../src/IVERC20BaseToken.sol"; import { Errors } from "../src/library/Errors.sol"; @@ -28,21 +28,21 @@ contract IVERC20TokenContractFactoryTest is Test { } function test_deploy_shit() public { - address deployedAddress = factoryInstance.deploy( + address deployedAddress = factoryInstance.create( deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" ); assertNotEq(deployedAddress, address(0)); vm.startPrank(deployerAddress); - MockIVToken(deployedAddress).mint(deployerAddress, 100e18); - assertEq(MockIVToken(deployedAddress).balanceOf(deployerAddress), 100e18); + MockIVERC20Token(deployedAddress).mint(deployerAddress, 100e18); + assertEq(MockIVERC20Token(deployedAddress).balanceOf(deployerAddress), 100e18); vm.stopPrank(); } function testRevert_deploy_UNAUTHORIZED() public { vm.expectRevert(); vm.prank(makeAddr("alice")); - factoryInstance.deploy( + factoryInstance.create( deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" ); } diff --git a/test/IVERC721TokenContractFactory.t.sol b/test/IVERC721TokenContractFactory.t.sol new file mode 100644 index 0000000..7d5ad66 --- /dev/null +++ b/test/IVERC721TokenContractFactory.t.sol @@ -0,0 +1,66 @@ +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import { IVERC721TokenContractFactory } from "../src/IVERC721TokenContractFactory.sol"; +import { MockIVERC721Token } from "./mocks/MockIVERC721Token.sol"; +import { IVERC20BaseToken } from "../src/IVERC20BaseToken.sol"; +import { Errors } from "../src/library/Errors.sol"; + +contract IVERC721TokenContractFactoryTest is Test { + IVERC721TokenContractFactory factoryInstance; + address public deployerAddress; + IVERC20BaseToken public ivBaseToken; + + uint256 private _nonces; + + function setUp() public { + deployerAddress = makeAddr("deployerAddress"); + factoryInstance = new IVERC721TokenContractFactory(); + factoryInstance.setDeployer(deployerAddress, true); + + _nonces = 0; + } + + function test_constructor() public { + assertTrue(factoryInstance.isDeployer(address(this))); + assertTrue(factoryInstance.isDeployer(deployerAddress)); + } + + function test_deploy_shit() public { + vm.startPrank(deployerAddress); + address deployedAddress = factoryInstance.create( + deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" + ); + + assertNotEq(deployedAddress, address(0)); + + MockIVERC721Token(deployedAddress).awardCampaignItem(deployerAddress, 1); + assertEq(MockIVERC721Token(deployedAddress).balanceOf(deployerAddress), 1); + vm.stopPrank(); + } + + function testRevert_deploy_UNAUTHORIZED() public { + vm.expectRevert(); + vm.prank(makeAddr("alice")); + factoryInstance.create( + deployerAddress, deployerAddress, deployerAddress, "TestToken", "TST" + ); + } + + function test_setDeployer() public { + address newContractFactoryAddress = makeAddr("bob"); + + assertFalse(factoryInstance.isDeployer(newContractFactoryAddress)); + factoryInstance.setDeployer(newContractFactoryAddress, true); + assertTrue(factoryInstance.isDeployer(newContractFactoryAddress)); + } + + function testRevert_setDeployer_UNAUTHORIZED() public { + address newContractFactoryAddress = makeAddr("bob"); + + vm.expectRevert(); + vm.prank(makeAddr("alice")); + factoryInstance.setDeployer(newContractFactoryAddress, true); + } +} diff --git a/test/mocks/MockIVToken.sol b/test/mocks/MockIVERC20Token.sol similarity index 88% rename from test/mocks/MockIVToken.sol rename to test/mocks/MockIVERC20Token.sol index 27b2a5a..adf96a3 100644 --- a/test/mocks/MockIVToken.sol +++ b/test/mocks/MockIVERC20Token.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.20; import { IVERC20BaseToken } from "../../src/IVERC20BaseToken.sol"; -contract MockIVToken is IVERC20BaseToken { +contract MockIVERC20Token is IVERC20BaseToken { constructor( address defaultAdmin, address minter, diff --git a/test/mocks/MockIVERC721Token.sol b/test/mocks/MockIVERC721Token.sol new file mode 100644 index 0000000..55a229c --- /dev/null +++ b/test/mocks/MockIVERC721Token.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { IVERC721BaseToken } from "../../src/IVERC721BaseToken.sol"; + +contract MockIVERC721Token is IVERC721BaseToken { + constructor( + address defaultAdmin, + address minter, + address pauser, + string memory name, + string memory symbol + ) IVERC721BaseToken(defaultAdmin, minter, pauser, name, symbol) { } +} From 1425e7f8bd2c40a1e10a50c9cc1efd24f95968aa Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Mon, 23 Oct 2023 23:49:42 -0400 Subject: [PATCH 35/41] fmt --- src/IVERC721TokenContractFactory.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/IVERC721TokenContractFactory.sol b/src/IVERC721TokenContractFactory.sol index a0e66aa..bf9d7cd 100644 --- a/src/IVERC721TokenContractFactory.sol +++ b/src/IVERC721TokenContractFactory.sol @@ -78,9 +78,8 @@ contract IVERC721TokenContractFactory { string memory _name, string memory _symbol ) external payable onlyDeployer returns (address deployedContract) { - deployedContract = address( - new IVERC721BaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol) - ); + deployedContract = + address(new IVERC721BaseToken(_defaultAdmin, _minter, _pauser, _name, _symbol)); // Set the token to the deployedTokens mapping deployedTokens[deployedContract] = Token({ From 0e7cf4698e20293ca80c3e5c0d79fdddef559542 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Tue, 24 Oct 2023 00:08:58 -0400 Subject: [PATCH 36/41] chore: start on manager contract --- src/IVEventManager.sol | 30 ++++++++++++++++++++++++++++++ src/library/Event.sol | 5 ++++- src/library/Staff.sol | 19 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/IVEventManager.sol create mode 100644 src/library/Staff.sol diff --git a/src/IVEventManager.sol b/src/IVEventManager.sol new file mode 100644 index 0000000..7471d01 --- /dev/null +++ b/src/IVEventManager.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import { Event } from "./library/Event.sol"; +import { Staff } from "./library/Staff.sol"; +import { StaffStatus } from "./library/Staff.sol"; +import { Metadata } from "./library/Metadata.sol"; + +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; + +contract IVEventManager is AccessControl { + enum EventStatus { + Pending, + Active, + Cancelled, + Completed + } + + mapping(address => Staff) public staff; + + constructor(address _defaultAdmin) { + _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + + Staff storage _staff = staff[_defaultAdmin]; + _staff.id = keccak256(abi.encodePacked("admin")); + _staff.member = _defaultAdmin; + _staff.metadata = Metadata({ protocol: 1, pointer: "https://mypointer.com" }); + _staff.status = StaffStatus.Active; + } +} diff --git a/src/library/Event.sol b/src/library/Event.sol index 3ef60d2..cfeec62 100644 --- a/src/library/Event.sol +++ b/src/library/Event.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.20; +import { Metadata } from "./Metadata.sol"; + struct Event { bytes32 id; - address creator; + bytes32 staffId; + Metadata metadata; } diff --git a/src/library/Staff.sol b/src/library/Staff.sol new file mode 100644 index 0000000..35165e2 --- /dev/null +++ b/src/library/Staff.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import { Metadata } from "./Metadata.sol"; + +enum StaffStatus { + Pending, + Active, + Inactive +} + +struct Staff { + bytes32 id; + address member; + Metadata metadata; + // member address to their level (the user can have multiple levels and use how they want) + mapping(address => uint256[]) levels; + StaffStatus status; +} From aefa7c5c1d4d2d60bfe95e1387727fbfd432883f Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 29 Oct 2023 13:22:28 -0400 Subject: [PATCH 37/41] chore: rename Event-Occurrence --- src/IVEventManager.sol | 30 ----- src/IVOccurrenceManager.sol | 228 +++++++++++++++++++++++++++++++++ src/library/Enums.sol | 11 ++ src/library/Event.sol | 10 -- src/library/Occurrence.sol | 20 +++ src/library/Staff.sol | 12 +- test/IVOccurrenceManager.t.sol | 45 +++++++ 7 files changed, 308 insertions(+), 48 deletions(-) delete mode 100644 src/IVEventManager.sol create mode 100644 src/IVOccurrenceManager.sol create mode 100644 src/library/Enums.sol delete mode 100644 src/library/Event.sol create mode 100644 src/library/Occurrence.sol create mode 100644 test/IVOccurrenceManager.t.sol diff --git a/src/IVEventManager.sol b/src/IVEventManager.sol deleted file mode 100644 index 7471d01..0000000 --- a/src/IVEventManager.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; - -import { Event } from "./library/Event.sol"; -import { Staff } from "./library/Staff.sol"; -import { StaffStatus } from "./library/Staff.sol"; -import { Metadata } from "./library/Metadata.sol"; - -import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; - -contract IVEventManager is AccessControl { - enum EventStatus { - Pending, - Active, - Cancelled, - Completed - } - - mapping(address => Staff) public staff; - - constructor(address _defaultAdmin) { - _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - - Staff storage _staff = staff[_defaultAdmin]; - _staff.id = keccak256(abi.encodePacked("admin")); - _staff.member = _defaultAdmin; - _staff.metadata = Metadata({ protocol: 1, pointer: "https://mypointer.com" }); - _staff.status = StaffStatus.Active; - } -} diff --git a/src/IVOccurrenceManager.sol b/src/IVOccurrenceManager.sol new file mode 100644 index 0000000..0b6537f --- /dev/null +++ b/src/IVOccurrenceManager.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { Occurrence } from "./library/Occurrence.sol"; +import { Staff } from "./library/Staff.sol"; +import { Enums } from "./library/Enums.sol"; +import { Metadata } from "./library/Metadata.sol"; + +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title IVOccurrenceManager + * @notice The IVOccurrenceManager contract is responsible for managing the occurrences + * @dev We use the term occurrence to describe an event, appointment, or any other type of gathering. + * @author @codenamejason + */ +contract IVOccurrenceManager is AccessControl { + bytes32 public constant CREATOR_ROLE = keccak256("CREATOR_ROLE"); + bytes32 public constant STAFF_ROLE = keccak256("STAFF_ROLE"); + + mapping(address => Staff) public staff; + mapping(bytes32 => Occurrence) public occurrences; + + modifier onlyCreator() { + require(hasRole(CREATOR_ROLE, msg.sender), "IVOccurrenceManager: caller is not a creator"); + _; + } + + modifier onlyStaff() { + require(hasRole(STAFF_ROLE, msg.sender), "IVOccurrenceManager: caller is not a staff"); + _; + } + + modifier onlyAdmin() { + require( + hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "IVOccurrenceManager: caller is not an admin" + ); + _; + } + + constructor(address _defaultAdmin) { + _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /** + * @notice Create an occurrence + * @param _name The name of the occurrence + * @param _description The description of the occurrence + * @param _start The start time of the occurrence + * @param _end The end time of the occurrence + * @param _price The price of the occurrence + * @param _token The token address of the occurrence + * @param _staff The staff addresses of the occurrence + * @param _metadata The metadata of the occurrence + * @return The id of the occurrence + */ + function createOccurrence( + string memory _name, + string memory _description, + uint256 _start, + uint256 _end, + uint256 _price, + address _token, + address[] memory _staff, + Metadata memory _metadata + ) external returns (bytes32) { + Occurrence memory _occurenceId = Occurrence({ + id: keccak256(abi.encodePacked(_name, _start, _end)), + creator: msg.sender, + name: _name, + description: _description, + start: _start, + end: _end, + price: _price, + token: _token, + status: Enums.Status.Pending, + staff: _staff, + metadata: _metadata + }); + + occurrences[_occurenceId.id] = _occurenceId; + + return _occurenceId.id; + } + + function updateOccurrence( + bytes32 _occurenceIdId, + string memory _name, + string memory _description, + uint256 _start, + uint256 _end, + uint256 _price, + address _token, + address[] memory _staff, + Metadata memory _metadata + ) external onlyCreator { + require( + occurrences[_occurenceIdId].id == _occurenceIdId, + "IVOccurrenceManager: occurrence does not exist" + ); + + Occurrence memory _occurenceId = Occurrence({ + id: keccak256(abi.encodePacked(_name, _start, _end)), + creator: msg.sender, + name: _name, + description: _description, + start: _start, + end: _end, + price: _price, + token: _token, + status: Enums.Status.Pending, + staff: _staff, + metadata: _metadata + }); + + occurrences[_occurenceId.id] = _occurenceId; + } + + function addStaffMember( + address _member, + // uint256[] memory _levels, + Metadata memory _metadata + ) external onlyCreator { + Staff memory _staff = Staff({ + id: keccak256(abi.encodePacked(_member)), + member: _member, + metadata: _metadata, + // levels: _levels, + status: Enums.Status.Pending + }); + + _grantRole(STAFF_ROLE, _member); + + // for (uint256 i = 0; i < _levels.length; i++) { + // _staff.levels[_member].push(_levels[i]); + // } + + staff[_staff.member] = _staff; + } + + function updateStaffMember( + address _member, + // uint256[] memory _levels, + Metadata memory _metadata + ) external onlyCreator { + Staff memory _staff = Staff({ + id: keccak256(abi.encodePacked(_member)), + member: _member, + metadata: _metadata, + // levels: _levels, + status: Enums.Status.Pending + }); + + // for (uint256 i = 0; i < _levels.length; i++) { + // _staff.levels[_member].push(_levels[i]); + // } + + staff[_staff.member] = _staff; + } + + function updateStaffMemberStatus(address _member, Enums.Status _status) external onlyCreator { + Staff memory _staff = staff[_member]; + _staff.status = _status; + + staff[_staff.member] = _staff; + } + + function updateOccurrenceStatus(bytes32 _occurenceIdId, Enums.Status _status) + external + onlyCreator + { + require( + occurrences[_occurenceIdId].id == _occurenceIdId, + "IVOccurrenceManager: occurrence does not exist" + ); + + Occurrence memory _occurenceId = occurrences[_occurenceIdId]; + _occurenceId.status = _status; + + occurrences[_occurenceId.id] = _occurenceId; + } + + function getOccurrence(bytes32 _occurenceIdId) external view returns (Occurrence memory) { + require( + occurrences[_occurenceIdId].id == _occurenceIdId, + "IVOccurrenceManager: occurrence does not exist" + ); + + return occurrences[_occurenceIdId]; + } + + function getStaffMember(bytes32 _occurenceIdId, address _member) + external + view + returns (Staff memory) + { + require( + occurrences[_occurenceIdId].id == _occurenceIdId, + "IVOccurrenceManager: occurrence does not exist" + ); + + return staff[_member]; + } + + function getStaffMembers(bytes32 _occurenceIdId) external view returns (Staff[] memory) { + require( + occurrences[_occurenceIdId].id == _occurenceIdId, + "IVOccurrenceManager: occurrence does not exist" + ); + + Occurrence memory _occurenceId = occurrences[_occurenceIdId]; + Staff[] memory _staff = new Staff[](_occurenceId.staff.length); + + for (uint256 i = 0; i < _occurenceId.staff.length; i++) { + _staff[i] = staff[_occurenceId.staff[i]]; + } + + return _staff; + } + + function getOccurrences() external view returns (Occurrence[] memory) { + // TODO: + } + + function getOccurrenceById(bytes32 _occurenceIdId) external view returns (Occurrence memory) { + return occurrences[_occurenceIdId]; + } +} diff --git a/src/library/Enums.sol b/src/library/Enums.sol new file mode 100644 index 0000000..ffc3f3a --- /dev/null +++ b/src/library/Enums.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +contract Enums { + enum Status { + Pending, + Active, + Inactive, + Rejected + } +} diff --git a/src/library/Event.sol b/src/library/Event.sol deleted file mode 100644 index cfeec62..0000000 --- a/src/library/Event.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; - -import { Metadata } from "./Metadata.sol"; - -struct Event { - bytes32 id; - bytes32 staffId; - Metadata metadata; -} diff --git a/src/library/Occurrence.sol b/src/library/Occurrence.sol new file mode 100644 index 0000000..a71fd07 --- /dev/null +++ b/src/library/Occurrence.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import { Metadata } from "./Metadata.sol"; +import { Enums } from "./Enums.sol"; + +struct Occurrence { + // keccak256(abi.encodePacked(_name, _start, _end)) + bytes32 id; + address creator; + string name; + string description; + uint256 start; + uint256 end; + uint256 price; + address token; + Enums.Status status; + address[] staff; + Metadata metadata; +} diff --git a/src/library/Staff.sol b/src/library/Staff.sol index 35165e2..728a0fd 100644 --- a/src/library/Staff.sol +++ b/src/library/Staff.sol @@ -2,18 +2,14 @@ pragma solidity 0.8.20; import { Metadata } from "./Metadata.sol"; - -enum StaffStatus { - Pending, - Active, - Inactive -} +import { Enums } from "./Enums.sol"; struct Staff { + // keccak256(abi.encodePacked(_eventId, _member)), bytes32 id; address member; Metadata metadata; // member address to their level (the user can have multiple levels and use how they want) - mapping(address => uint256[]) levels; - StaffStatus status; + // uint256[] levels; + Enums.Status status; } diff --git a/test/IVOccurrenceManager.t.sol b/test/IVOccurrenceManager.t.sol new file mode 100644 index 0000000..8382583 --- /dev/null +++ b/test/IVOccurrenceManager.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import { Test, console2, StdUtils } from "forge-std/Test.sol"; + +import { IVOccurrenceManager } from "../src/IVOccurrenceManager.sol"; +import { Occurrence } from "../src/library/Occurrence.sol"; +import { Enums } from "../src/library/Enums.sol"; +import { Metadata } from "../src/library/Metadata.sol"; + +contract IVOccurrenceManagerTest is Test { + IVOccurrenceManager ivOccurrenceManager; + + function setUp() public { + address admin = makeAddr("admin"); + ivOccurrenceManager = new IVOccurrenceManager(admin); + // ivOccurrenceManager.addStaffMember(_member, _metadata); + } + + function test_CreateOccurrence() public { + address creator = makeAddr("creator"); + address[] memory staff = new address[](1); + staff[0] = makeAddr("staff"); + vm.prank(creator); + bytes32 occurrence = ivOccurrenceManager.createOccurrence( + "name", + "description", + 1, + 2, + 3, + // todo: add mock + address(makeAddr("token")), + staff, + Metadata({ protocol: 1, pointer: "0x230847695gbv2-3" }) + ); + + (bytes32 _occurrence,, string memory name, string memory description, uint256 start,,,,,) = + ivOccurrenceManager.occurrences(occurrence); + + assertEq(_occurrence, occurrence); + assertEq(name, "name"); + assertEq(description, "description"); + assertEq(start, 1); + } +} From 18fa5d43e7df5a64800e8c4272bbd774b88c1f28 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 29 Oct 2023 14:10:31 -0400 Subject: [PATCH 38/41] chore: some refactoring --- src/IVOccurrenceManager.sol | 121 ++++++++------------------------- src/IVStaffManager.sol | 78 +++++++++++++++++++++ src/library/Metadata.sol | 12 ---- src/library/Occurrence.sol | 20 ------ src/library/Recover.sol | 11 +++ src/library/Staff.sol | 15 ---- src/library/Structs.sol | 38 +++++++++++ test/IVOccurrenceManager.t.sol | 5 +- 8 files changed, 156 insertions(+), 144 deletions(-) create mode 100644 src/IVStaffManager.sol delete mode 100644 src/library/Metadata.sol delete mode 100644 src/library/Occurrence.sol create mode 100644 src/library/Recover.sol delete mode 100644 src/library/Staff.sol create mode 100644 src/library/Structs.sol diff --git a/src/IVOccurrenceManager.sol b/src/IVOccurrenceManager.sol index 0b6537f..158bb94 100644 --- a/src/IVOccurrenceManager.sol +++ b/src/IVOccurrenceManager.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.20; -import { Occurrence } from "./library/Occurrence.sol"; -import { Staff } from "./library/Staff.sol"; +import { IVStaffManager } from "./IVStaffManager.sol"; import { Enums } from "./library/Enums.sol"; -import { Metadata } from "./library/Metadata.sol"; +import { Structs } from "./library/Structs.sol"; -import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +// import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; /** * @title IVOccurrenceManager @@ -14,31 +13,17 @@ import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol" * @dev We use the term occurrence to describe an event, appointment, or any other type of gathering. * @author @codenamejason */ -contract IVOccurrenceManager is AccessControl { +contract IVOccurrenceManager is IVStaffManager { bytes32 public constant CREATOR_ROLE = keccak256("CREATOR_ROLE"); - bytes32 public constant STAFF_ROLE = keccak256("STAFF_ROLE"); - mapping(address => Staff) public staff; - mapping(bytes32 => Occurrence) public occurrences; + mapping(bytes32 => Structs.Occurrence) public occurrences; modifier onlyCreator() { require(hasRole(CREATOR_ROLE, msg.sender), "IVOccurrenceManager: caller is not a creator"); _; } - modifier onlyStaff() { - require(hasRole(STAFF_ROLE, msg.sender), "IVOccurrenceManager: caller is not a staff"); - _; - } - - modifier onlyAdmin() { - require( - hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "IVOccurrenceManager: caller is not an admin" - ); - _; - } - - constructor(address _defaultAdmin) { + constructor(address _defaultAdmin) IVStaffManager(_defaultAdmin) { _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); } @@ -62,9 +47,9 @@ contract IVOccurrenceManager is AccessControl { uint256 _price, address _token, address[] memory _staff, - Metadata memory _metadata + Structs.Metadata memory _metadata ) external returns (bytes32) { - Occurrence memory _occurenceId = Occurrence({ + Structs.Occurrence memory _occurenceId = Structs.Occurrence({ id: keccak256(abi.encodePacked(_name, _start, _end)), creator: msg.sender, name: _name, @@ -92,14 +77,14 @@ contract IVOccurrenceManager is AccessControl { uint256 _price, address _token, address[] memory _staff, - Metadata memory _metadata + Structs.Metadata memory _metadata ) external onlyCreator { require( occurrences[_occurenceIdId].id == _occurenceIdId, "IVOccurrenceManager: occurrence does not exist" ); - Occurrence memory _occurenceId = Occurrence({ + Structs.Occurrence memory _occurenceId = Structs.Occurrence({ id: keccak256(abi.encodePacked(_name, _start, _end)), creator: msg.sender, name: _name, @@ -116,83 +101,23 @@ contract IVOccurrenceManager is AccessControl { occurrences[_occurenceId.id] = _occurenceId; } - function addStaffMember( - address _member, - // uint256[] memory _levels, - Metadata memory _metadata - ) external onlyCreator { - Staff memory _staff = Staff({ - id: keccak256(abi.encodePacked(_member)), - member: _member, - metadata: _metadata, - // levels: _levels, - status: Enums.Status.Pending - }); - - _grantRole(STAFF_ROLE, _member); - - // for (uint256 i = 0; i < _levels.length; i++) { - // _staff.levels[_member].push(_levels[i]); - // } - - staff[_staff.member] = _staff; - } - - function updateStaffMember( - address _member, - // uint256[] memory _levels, - Metadata memory _metadata - ) external onlyCreator { - Staff memory _staff = Staff({ - id: keccak256(abi.encodePacked(_member)), - member: _member, - metadata: _metadata, - // levels: _levels, - status: Enums.Status.Pending - }); - - // for (uint256 i = 0; i < _levels.length; i++) { - // _staff.levels[_member].push(_levels[i]); - // } - - staff[_staff.member] = _staff; - } - - function updateStaffMemberStatus(address _member, Enums.Status _status) external onlyCreator { - Staff memory _staff = staff[_member]; - _staff.status = _status; - - staff[_staff.member] = _staff; - } - - function updateOccurrenceStatus(bytes32 _occurenceIdId, Enums.Status _status) + function getOccurrence(bytes32 _occurenceIdId) external - onlyCreator + view + returns (Structs.Occurrence memory) { require( occurrences[_occurenceIdId].id == _occurenceIdId, "IVOccurrenceManager: occurrence does not exist" ); - Occurrence memory _occurenceId = occurrences[_occurenceIdId]; - _occurenceId.status = _status; - - occurrences[_occurenceId.id] = _occurenceId; - } - - function getOccurrence(bytes32 _occurenceIdId) external view returns (Occurrence memory) { - require( - occurrences[_occurenceIdId].id == _occurenceIdId, - "IVOccurrenceManager: occurrence does not exist" - ); - return occurrences[_occurenceIdId]; } function getStaffMember(bytes32 _occurenceIdId, address _member) external view - returns (Staff memory) + returns (Structs.Staff memory) { require( occurrences[_occurenceIdId].id == _occurenceIdId, @@ -202,14 +127,18 @@ contract IVOccurrenceManager is AccessControl { return staff[_member]; } - function getStaffMembers(bytes32 _occurenceIdId) external view returns (Staff[] memory) { + function getStaffMembers(bytes32 _occurenceIdId) + external + view + returns (Structs.Staff[] memory) + { require( occurrences[_occurenceIdId].id == _occurenceIdId, "IVOccurrenceManager: occurrence does not exist" ); - Occurrence memory _occurenceId = occurrences[_occurenceIdId]; - Staff[] memory _staff = new Staff[](_occurenceId.staff.length); + Structs.Occurrence memory _occurenceId = occurrences[_occurenceIdId]; + Structs.Staff[] memory _staff = new Structs.Staff[](_occurenceId.staff.length); for (uint256 i = 0; i < _occurenceId.staff.length; i++) { _staff[i] = staff[_occurenceId.staff[i]]; @@ -218,11 +147,15 @@ contract IVOccurrenceManager is AccessControl { return _staff; } - function getOccurrences() external view returns (Occurrence[] memory) { + function getOccurrences() external view returns (Structs.Occurrence[] memory) { // TODO: } - function getOccurrenceById(bytes32 _occurenceIdId) external view returns (Occurrence memory) { + function getOccurrenceById(bytes32 _occurenceIdId) + external + view + returns (Structs.Occurrence memory) + { return occurrences[_occurenceIdId]; } } diff --git a/src/IVStaffManager.sol b/src/IVStaffManager.sol new file mode 100644 index 0000000..4485e49 --- /dev/null +++ b/src/IVStaffManager.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { Enums } from "./library/Enums.sol"; +import { Structs } from "../src/library/Structs.sol"; + +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; + +contract IVStaffManager is AccessControl { + bytes32 public constant STAFF_ROLE = keccak256("STAFF_ROLE"); + + mapping(address => Structs.Staff) public staff; + + modifier onlyAdmin() { + require( + hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "IVOccurrenceManager: caller is not an admin" + ); + _; + } + + modifier onlyStaff() { + require(hasRole(STAFF_ROLE, msg.sender), "IVOccurrenceManager: caller is not a staff"); + _; + } + + constructor(address _defaultAdmin) { + _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + function addStaffMember( + address _member, + // uint256[] memory _levels, + Structs.Metadata memory _metadata + ) external onlyAdmin { + Structs.Staff memory _staff = Structs.Staff({ + id: keccak256(abi.encodePacked(_member)), + member: _member, + metadata: _metadata, + // levels: _levels, + status: Enums.Status.Pending + }); + + _grantRole(STAFF_ROLE, _member); + + // for (uint256 i = 0; i < _levels.length; i++) { + // _staff.levels[_member].push(_levels[i]); + // } + + staff[_staff.member] = _staff; + } + + function updateStaffMember( + address _member, + // uint256[] memory _levels, + Structs.Metadata memory _metadata + ) external onlyAdmin { + Structs.Staff memory _staff = Structs.Staff({ + id: keccak256(abi.encodePacked(_member)), + member: _member, + metadata: _metadata, + // levels: _levels, + status: Enums.Status.Pending + }); + + // for (uint256 i = 0; i < _levels.length; i++) { + // _staff.levels[_member].push(_levels[i]); + // } + + staff[_staff.member] = _staff; + } + + function updateStaffMemberStatus(address _member, Enums.Status _status) external onlyAdmin { + Structs.Staff memory _staff = staff[_member]; + _staff.status = _status; + + staff[_staff.member] = _staff; + } +} diff --git a/src/library/Metadata.sol b/src/library/Metadata.sol deleted file mode 100644 index d5dd93d..0000000 --- a/src/library/Metadata.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.20; - -/// @title Metadata -/// @author @codenamejason -/// @notice Metadata is used to define the metadata that is used throughout the system. -struct Metadata { - /// @notice Protocol ID corresponding to a specific protocol (currently using IPFS = 1) - uint256 protocol; - /// @notice Pointer (hash) to fetch metadata for the specified protocol - string pointer; -} diff --git a/src/library/Occurrence.sol b/src/library/Occurrence.sol deleted file mode 100644 index a71fd07..0000000 --- a/src/library/Occurrence.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; - -import { Metadata } from "./Metadata.sol"; -import { Enums } from "./Enums.sol"; - -struct Occurrence { - // keccak256(abi.encodePacked(_name, _start, _end)) - bytes32 id; - address creator; - string name; - string description; - uint256 start; - uint256 end; - uint256 price; - address token; - Enums.Status status; - address[] staff; - Metadata metadata; -} diff --git a/src/library/Recover.sol b/src/library/Recover.sol new file mode 100644 index 0000000..8c07ff0 --- /dev/null +++ b/src/library/Recover.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Recover { + function recoverERC20(address _token) external { + uint256 balance = IERC20(_token).balanceOf(address(this)); + IERC20(_token).transfer(msg.sender, balance); + } +} diff --git a/src/library/Staff.sol b/src/library/Staff.sol deleted file mode 100644 index 728a0fd..0000000 --- a/src/library/Staff.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; - -import { Metadata } from "./Metadata.sol"; -import { Enums } from "./Enums.sol"; - -struct Staff { - // keccak256(abi.encodePacked(_eventId, _member)), - bytes32 id; - address member; - Metadata metadata; - // member address to their level (the user can have multiple levels and use how they want) - // uint256[] levels; - Enums.Status status; -} diff --git a/src/library/Structs.sol b/src/library/Structs.sol new file mode 100644 index 0000000..141f703 --- /dev/null +++ b/src/library/Structs.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { Enums } from "./Enums.sol"; + +library Structs { + struct Metadata { + /// @notice Protocol ID corresponding to a specific protocol (currently using IPFS = 1) + uint256 protocol; + /// @notice Pointer (hash) to fetch metadata for the specified protocol + string pointer; + } + + struct Staff { + // keccak256(abi.encodePacked(_eventId, _member)), + bytes32 id; + address member; + Metadata metadata; + // member address to their level (the user can have multiple levels and use how they want) + // uint256[] levels; + Enums.Status status; + } + + struct Occurrence { + // keccak256(abi.encodePacked(_name, _start, _end)) + bytes32 id; + address creator; + string name; + string description; + uint256 start; + uint256 end; + uint256 price; + address token; + Enums.Status status; + address[] staff; + Metadata metadata; + } +} diff --git a/test/IVOccurrenceManager.t.sol b/test/IVOccurrenceManager.t.sol index 8382583..b873d58 100644 --- a/test/IVOccurrenceManager.t.sol +++ b/test/IVOccurrenceManager.t.sol @@ -4,9 +4,8 @@ pragma solidity 0.8.20; import { Test, console2, StdUtils } from "forge-std/Test.sol"; import { IVOccurrenceManager } from "../src/IVOccurrenceManager.sol"; -import { Occurrence } from "../src/library/Occurrence.sol"; import { Enums } from "../src/library/Enums.sol"; -import { Metadata } from "../src/library/Metadata.sol"; +import { Structs } from "../src/library/Structs.sol"; contract IVOccurrenceManagerTest is Test { IVOccurrenceManager ivOccurrenceManager; @@ -31,7 +30,7 @@ contract IVOccurrenceManagerTest is Test { // todo: add mock address(makeAddr("token")), staff, - Metadata({ protocol: 1, pointer: "0x230847695gbv2-3" }) + Structs.Metadata({ protocol: 1, pointer: "0x230847695gbv2-3" }) ); (bytes32 _occurrence,, string memory name, string memory description, uint256 start,,,,,) = From 452fcae1accf2ea7d92bc7f70cebf0450c617f76 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 29 Oct 2023 14:54:15 -0400 Subject: [PATCH 39/41] chore: add Errors lib --- script/Will4USNFT.s.sol | 3 +-- src/IVOccurrenceManager.sol | 31 ++++++++++++++++++++----------- src/IVStaffManager.sol | 1 + src/Will4USNFT.sol | 23 ++++++++--------------- src/library/Errors.sol | 1 + test/Will4USNFTTest.t.sol | 11 +++++++++++ 6 files changed, 42 insertions(+), 28 deletions(-) diff --git a/script/Will4USNFT.s.sol b/script/Will4USNFT.s.sol index 616abed..be5c2bc 100644 --- a/script/Will4USNFT.s.sol +++ b/script/Will4USNFT.s.sol @@ -20,8 +20,7 @@ contract Will4USNFTScript is Script { // assertEq(url, "https://arb-goerli.g.alchemy.com/v2/RqTiyvS7OspxaAQUQupKKCTjmf94JL-I"); vm.startBroadcast(deployerPrivateKey); - Will4USNFT nftContract = - new Will4USNFT(deployerAddress, deployerAddress, deployerAddress, 5); + new Will4USNFT(deployerAddress, deployerAddress, deployerAddress, 5); // nftContract.awardCampaignItem(deployerAddress, 1); diff --git a/src/IVOccurrenceManager.sol b/src/IVOccurrenceManager.sol index 158bb94..19b59f5 100644 --- a/src/IVOccurrenceManager.sol +++ b/src/IVOccurrenceManager.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.20; import { IVStaffManager } from "./IVStaffManager.sol"; import { Enums } from "./library/Enums.sol"; import { Structs } from "./library/Structs.sol"; +import { Errors } from "./library/Errors.sol"; // import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; @@ -17,6 +18,7 @@ contract IVOccurrenceManager is IVStaffManager { bytes32 public constant CREATOR_ROLE = keccak256("CREATOR_ROLE"); mapping(bytes32 => Structs.Occurrence) public occurrences; + uint256 private _occurrenceCount; modifier onlyCreator() { require(hasRole(CREATOR_ROLE, msg.sender), "IVOccurrenceManager: caller is not a creator"); @@ -64,6 +66,7 @@ contract IVOccurrenceManager is IVStaffManager { }); occurrences[_occurenceId.id] = _occurenceId; + _occurrenceCount++; return _occurenceId.id; } @@ -114,7 +117,7 @@ contract IVOccurrenceManager is IVStaffManager { return occurrences[_occurenceIdId]; } - function getStaffMember(bytes32 _occurenceIdId, address _member) + function getStaffMemberByOccurrenceId(bytes32 _occurenceIdId, address _member) external view returns (Structs.Staff memory) @@ -127,28 +130,34 @@ contract IVOccurrenceManager is IVStaffManager { return staff[_member]; } - function getStaffMembers(bytes32 _occurenceIdId) + function getStaffMembersForOccurrence(bytes32 _occurenceId) external view returns (Structs.Staff[] memory) { - require( - occurrences[_occurenceIdId].id == _occurenceIdId, - "IVOccurrenceManager: occurrence does not exist" - ); + if (occurrences[_occurenceId].id != _occurenceId) { + revert Errors.OccurrenceDoesNotExist(_occurenceId); + } - Structs.Occurrence memory _occurenceId = occurrences[_occurenceIdId]; - Structs.Staff[] memory _staff = new Structs.Staff[](_occurenceId.staff.length); + Structs.Occurrence memory occurrence = occurrences[_occurenceId]; + Structs.Staff[] memory _staff = new Structs.Staff[](occurrence.staff.length); - for (uint256 i = 0; i < _occurenceId.staff.length; i++) { - _staff[i] = staff[_occurenceId.staff[i]]; + for (uint256 i = 0; i < occurrence.staff.length; i++) { + _staff[i] = staff[occurrence.staff[i]]; } return _staff; } function getOccurrences() external view returns (Structs.Occurrence[] memory) { - // TODO: + Structs.Occurrence[] memory _occurrences = new Structs.Occurrence[](_occurrenceCount); + + for (uint256 i = 0; i < _occurrenceCount; i++) { + // FIXME: this is not the correct way to do this + _occurrences[i] = occurrences[keccak256(abi.encodePacked(i))]; + } + + return _occurrences; } function getOccurrenceById(bytes32 _occurenceIdId) diff --git a/src/IVStaffManager.sol b/src/IVStaffManager.sol index 4485e49..df8ad37 100644 --- a/src/IVStaffManager.sol +++ b/src/IVStaffManager.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.20; import { Enums } from "./library/Enums.sol"; import { Structs } from "../src/library/Structs.sol"; +import { Errors } from "../src/library/Errors.sol"; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index e0b9196..8290609 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.20; +import { Errors } from "./library/Errors.sol"; + import { ERC721URIStorage } from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; @@ -40,15 +42,6 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { string metadata; // this is a pointer to json object that contains the metadata for this class } - /** - * Errors ************ - */ - error InvalidTokenId(uint256 tokenId); - error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); - error AlreadyRedeemed(uint256 eventId, uint256 tokenId); - error Unauthorized(address sender); - error NewSupplyTooLow(uint256 minted, uint256 supply); - /** * Events ************ */ @@ -72,7 +65,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { */ modifier onlyCampaingnMember(address sender) { if (!hasRole(MINTER_ROLE, sender)) { - revert Unauthorized(sender); + revert Errors.Unauthorized(sender); } _; } @@ -139,7 +132,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { returns (uint256) { if (mintedPerClass[_recipient][_classId] > maxMintablePerClass) { - revert MaxMintablePerClassReached(_recipient, _classId, maxMintablePerClass); + revert Errors.MaxMintablePerClassReached(_recipient, _classId, maxMintablePerClass); } uint256 tokenId = _mintCampaingnItem(_recipient, _classId); @@ -190,11 +183,11 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { */ function redeem(uint256 _eventId, uint256 _tokenId) external onlyCampaingnMember(msg.sender) { if (super.ownerOf(_tokenId) == address(0)) { - revert InvalidTokenId(_tokenId); + revert Errors.InvalidTokenId(_tokenId); } if (redeemed[_eventId][_tokenId]) { - revert AlreadyRedeemed(_eventId, _tokenId); + revert Errors.AlreadyRedeemed(_eventId, _tokenId); } redeemed[_eventId][_tokenId] = true; @@ -259,7 +252,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { return tokenURI(_tokenId); } else { - revert InvalidTokenId(_tokenId); + revert Errors.InvalidTokenId(_tokenId); } } @@ -280,7 +273,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { // if the new supply is less than the current supply, we need to check if the new supply is less than the minted // if it is, then we need to revert if (_supply < minted) { - revert NewSupplyTooLow(minted, _supply); + revert Errors.NewSupplyTooLow(minted, _supply); } } diff --git a/src/library/Errors.sol b/src/library/Errors.sol index 48fd391..530711b 100644 --- a/src/library/Errors.sol +++ b/src/library/Errors.sol @@ -7,4 +7,5 @@ library Errors { error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); error AlreadyRedeemed(uint256 eventId, uint256 tokenId); error NewSupplyTooLow(uint256 minted, uint256 supply); + error OccurrenceDoesNotExist(bytes32 occurrenceId); } diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index 764c72e..dff72a2 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -171,7 +171,18 @@ contract Will4USNFTTest is Test { string memory imagePointer, string memory metadataPointer ) = nftContract.classes(2); + assertEq(name, "name2", "Class name should be name"); + assertEq(description, "description", "Class description should be description"); + assertEq(imagePointer, "imagePointer", "Class imagePointer should be imagePointer"); + assertEq( + metadataPointer, + "https://a_new_pointer_to_json_object.io", + "Class metadataPointer should be metadataPointer" + ); + assertEq(supply, 1e7, "Class supply should be 1e7"); + assertEq(minted, 0, "Class minted should be 0"); + assertEq(id, 2, "Class id should be 2"); } function test_revert_addClass_Unauthorized() public { From d049173d21a0f9e365e8768083420b70b3b7c224 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 29 Oct 2023 16:20:46 -0400 Subject: [PATCH 40/41] chore: some refactoring/add interface --- src/IVERC721BaseToken.sol | 80 ++---------- src/IVOccurrenceManager.sol | 158 +++++++++++++++--------- src/IVStaffManager.sol | 14 +++ src/interfaces/IIVOccurrenceManager.sol | 53 ++++++++ src/library/Enums.sol | 2 + src/library/Errors.sol | 1 + src/library/Structs.sol | 11 ++ 7 files changed, 192 insertions(+), 127 deletions(-) create mode 100644 src/interfaces/IIVOccurrenceManager.sol diff --git a/src/IVERC721BaseToken.sol b/src/IVERC721BaseToken.sol index cbb2930..e61073e 100644 --- a/src/IVERC721BaseToken.sol +++ b/src/IVERC721BaseToken.sol @@ -10,7 +10,8 @@ import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; -import "./library/Errors.sol"; +import { Errors } from "./library/Errors.sol"; +import { Structs } from "./library/Structs.sol"; contract IVERC721BaseToken is ERC721, @@ -29,22 +30,12 @@ contract IVERC721BaseToken is uint256 public totalClassesSupply; uint256 public maxMintablePerClass; - mapping(uint256 => Class) public classes; + mapping(uint256 => Structs.Class) public classes; mapping(address => bool) public campaignMembers; mapping(address => mapping(uint256 => uint256)) public mintedPerClass; // eventId => token => bool mapping(uint256 => mapping(uint256 => bool)) public redeemed; - struct Class { - uint256 id; - uint256 supply; // total supply of this class? do we want this? - uint256 minted; - string name; - string description; - string imagePointer; - string metadata; // this is a pointer to json object that contains the metadata for this class - } - /** * Events ************ */ @@ -66,7 +57,7 @@ contract IVERC721BaseToken is * @dev This modifier is used to check if the sender is a campaign member * @param sender The sender address */ - modifier onlyCampaingnMember(address sender) { + modifier onlyMinter(address sender) { if (!hasRole(MINTER_ROLE, sender)) { revert Errors.Unauthorized(sender); } @@ -80,8 +71,6 @@ contract IVERC721BaseToken is string memory _name, string memory _symbol ) ERC721(_name, _symbol) EIP712(_name, "1") { - _addCampaignMember(_defaultAdmin); - _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); _grantRole(PAUSER_ROLE, _pauser); _grantRole(MINTER_ROLE, _minter); @@ -91,33 +80,6 @@ contract IVERC721BaseToken is * External Functions ***** */ - /** - * @notice Adds a campaign member - * @dev This function is only callable by the owner - * @param _member The member to add - */ - function addCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { - _addCampaignMember(_member); - } - - /** - * @notice Adds a campaign member - * @dev This function is internal - * @param _member The member to add - */ - function _addCampaignMember(address _member) internal { - _grantRole(MINTER_ROLE, _member); - } - - /** - * @notice Removes a campaign member - * @dev This function is only callable by the owner - * @param _member The member to remove - */ - function removeCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { - revokeRole(MINTER_ROLE, _member); - } - /** * @notice Awards campaign nft to supporter * @dev This function is only callable by campaign members @@ -126,7 +88,7 @@ contract IVERC721BaseToken is */ function awardCampaignItem(address _recipient, uint256 _classId) external - onlyCampaingnMember(msg.sender) + onlyMinter(msg.sender) returns (uint256) { if (mintedPerClass[_recipient][_classId] > maxMintablePerClass) { @@ -149,7 +111,7 @@ contract IVERC721BaseToken is */ function batchAwardCampaignItem(address[] memory _recipients, uint256[] memory _classIds) external - onlyCampaingnMember(msg.sender) + onlyMinter(msg.sender) returns (uint256[] memory) { uint256 length = _recipients.length; @@ -173,26 +135,6 @@ contract IVERC721BaseToken is return tokenIds; } - /** - * @notice Redeems a campaign item - * @dev This function is only callable by campaign members - * @param _eventId The event ID - * @param _tokenId The token ID - */ - function redeem(uint256 _eventId, uint256 _tokenId) external onlyCampaingnMember(msg.sender) { - if (super.ownerOf(_tokenId) == address(0)) { - revert Errors.InvalidTokenId(_tokenId); - } - - if (redeemed[_eventId][_tokenId]) { - revert Errors.AlreadyRedeemed(_eventId, _tokenId); - } - - redeemed[_eventId][_tokenId] = true; - - emit Redeemed(_eventId, _tokenId, classes[_tokenId].id); - } - /** * @notice Adds a new class to the campaign for issuance * @dev This function is only callable by campaign members @@ -208,11 +150,11 @@ contract IVERC721BaseToken is string memory _imagePointer, string memory _metadata, uint256 _supply - ) external onlyCampaingnMember(msg.sender) { + ) external onlyMinter(msg.sender) { uint256 id = ++classIds; totalClassesSupply += _supply; - classes[id] = Class(id, _supply, 0, _name, _description, _imagePointer, _metadata); + classes[id] = Structs.Class(id, _supply, 0, _name, _description, _imagePointer, _metadata); emit ClassAdded(id, _metadata); } @@ -220,8 +162,8 @@ contract IVERC721BaseToken is /** * @notice Returns all classes */ - function getAllClasses() public view returns (Class[] memory) { - Class[] memory _classes = new Class[](classIds); + function getAllClasses() public view returns (Structs.Class[] memory) { + Structs.Class[] memory _classes = new Structs.Class[](classIds); for (uint256 i = 0; i < classIds; i++) { _classes[i] = classes[i + 1]; @@ -262,7 +204,7 @@ contract IVERC721BaseToken is */ function setClassTokenSupply(uint256 _classId, uint256 _supply) external - onlyCampaingnMember(msg.sender) + onlyMinter(msg.sender) { uint256 currentSupply = classes[_classId].supply; uint256 minted = classes[_classId].minted; diff --git a/src/IVOccurrenceManager.sol b/src/IVOccurrenceManager.sol index 19b59f5..2edcdda 100644 --- a/src/IVOccurrenceManager.sol +++ b/src/IVOccurrenceManager.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.20; import { IVStaffManager } from "./IVStaffManager.sol"; +import { IIVOccurrenceManager } from "./interfaces/IIVOccurrenceManager.sol"; import { Enums } from "./library/Enums.sol"; import { Structs } from "./library/Structs.sol"; import { Errors } from "./library/Errors.sol"; @@ -14,14 +15,23 @@ import { Errors } from "./library/Errors.sol"; * @dev We use the term occurrence to describe an event, appointment, or any other type of gathering. * @author @codenamejason */ -contract IVOccurrenceManager is IVStaffManager { +contract IVOccurrenceManager is IIVOccurrenceManager, IVStaffManager { bytes32 public constant CREATOR_ROLE = keccak256("CREATOR_ROLE"); mapping(bytes32 => Structs.Occurrence) public occurrences; uint256 private _occurrenceCount; modifier onlyCreator() { - require(hasRole(CREATOR_ROLE, msg.sender), "IVOccurrenceManager: caller is not a creator"); + if (!hasRole(CREATOR_ROLE, msg.sender)) { + revert Errors.NotCreator(msg.sender); + } + _; + } + + modifier occurrenceExists(bytes32 _occurenceIdId) { + if (occurrences[_occurenceIdId].id != _occurenceIdId) { + revert Errors.OccurrenceDoesNotExist(_occurenceIdId); + } _; } @@ -51,28 +61,12 @@ contract IVOccurrenceManager is IVStaffManager { address[] memory _staff, Structs.Metadata memory _metadata ) external returns (bytes32) { - Structs.Occurrence memory _occurenceId = Structs.Occurrence({ - id: keccak256(abi.encodePacked(_name, _start, _end)), - creator: msg.sender, - name: _name, - description: _description, - start: _start, - end: _end, - price: _price, - token: _token, - status: Enums.Status.Pending, - staff: _staff, - metadata: _metadata - }); - - occurrences[_occurenceId.id] = _occurenceId; - _occurrenceCount++; - - return _occurenceId.id; + return + _createOccurrence(_name, _description, _start, _end, _price, _token, _staff, _metadata); } function updateOccurrence( - bytes32 _occurenceIdId, + bytes32 _occurenceId, string memory _name, string memory _description, uint256 _start, @@ -81,64 +75,52 @@ contract IVOccurrenceManager is IVStaffManager { address _token, address[] memory _staff, Structs.Metadata memory _metadata - ) external onlyCreator { - require( - occurrences[_occurenceIdId].id == _occurenceIdId, - "IVOccurrenceManager: occurrence does not exist" + ) external onlyCreator occurrenceExists(_occurenceId) { + return _updateOccurrence( + _occurenceId, _name, _description, _start, _end, _price, _token, _staff, _metadata ); - - Structs.Occurrence memory _occurenceId = Structs.Occurrence({ - id: keccak256(abi.encodePacked(_name, _start, _end)), - creator: msg.sender, - name: _name, - description: _description, - start: _start, - end: _end, - price: _price, - token: _token, - status: Enums.Status.Pending, - staff: _staff, - metadata: _metadata - }); - - occurrences[_occurenceId.id] = _occurenceId; } - function getOccurrence(bytes32 _occurenceIdId) + function getOccurrence(bytes32 _occurenceId) external view + occurrenceExists(_occurenceId) returns (Structs.Occurrence memory) { - require( - occurrences[_occurenceIdId].id == _occurenceIdId, - "IVOccurrenceManager: occurrence does not exist" - ); + return occurrences[_occurenceId]; + } - return occurrences[_occurenceIdId]; + function hostOccurrence(bytes32 _occurenceId, address[] memory _attendees) + external + onlyCreator + occurrenceExists(_occurenceId) + { + occurrences[_occurenceId].status = Enums.Status.Hosted; + } + + function recognizeOccurrence(bytes32 _occurenceId, Structs.Metadata memory _content) + external + onlyStaff + occurrenceExists(_occurenceId) + { + occurrences[_occurenceId].status = Enums.Status.Recognized; } - function getStaffMemberByOccurrenceId(bytes32 _occurenceIdId, address _member) + function getStaffMemberByOccurrenceId(bytes32 _occurenceId, address _member) external view + occurrenceExists(_occurenceId) returns (Structs.Staff memory) { - require( - occurrences[_occurenceIdId].id == _occurenceIdId, - "IVOccurrenceManager: occurrence does not exist" - ); - return staff[_member]; } - function getStaffMembersForOccurrence(bytes32 _occurenceId) + function getStaffMembersForOccurrenceId(bytes32 _occurenceId) external view + occurrenceExists(_occurenceId) returns (Structs.Staff[] memory) { - if (occurrences[_occurenceId].id != _occurenceId) { - revert Errors.OccurrenceDoesNotExist(_occurenceId); - } - Structs.Occurrence memory occurrence = occurrences[_occurenceId]; Structs.Staff[] memory _staff = new Structs.Staff[](occurrence.staff.length); @@ -167,4 +149,64 @@ contract IVOccurrenceManager is IVStaffManager { { return occurrences[_occurenceIdId]; } + + /** + * Internal Functions + */ + + function _createOccurrence( + string memory _name, + string memory _description, + uint256 _start, + uint256 _end, + uint256 _price, + address _token, + address[] memory _staff, + Structs.Metadata memory _metadata + ) internal returns (bytes32) { + Structs.Occurrence memory _occurenceId = Structs.Occurrence({ + id: keccak256(abi.encodePacked(_name, _start, _end)), + creator: msg.sender, + name: _name, + description: _description, + start: _start, + end: _end, + price: _price, + token: _token, + status: Enums.Status.Pending, + staff: _staff, + metadata: _metadata, + attendees: new address[](9999) + }); + + occurrences[_occurenceId.id] = _occurenceId; + _occurrenceCount++; + + return _occurenceId.id; + } + + function _updateOccurrence( + bytes32 _occurenceId, + string memory _name, + string memory _description, + uint256 _start, + uint256 _end, + uint256 _price, + address _token, + address[] memory _staff, + Structs.Metadata memory _metadata + ) internal { + Structs.Occurrence memory _occurence = occurrences[_occurenceId]; + + _occurence.name = _name; + _occurence.description = _description; + _occurence.start = _start; + _occurence.end = _end; + _occurence.price = _price; + _occurence.token = _token; + _occurence.staff = _staff; + _occurence.metadata = _metadata; + + occurrences[_occurenceId] = _occurence; + } } diff --git a/src/IVStaffManager.sol b/src/IVStaffManager.sol index df8ad37..5b04f13 100644 --- a/src/IVStaffManager.sol +++ b/src/IVStaffManager.sol @@ -9,6 +9,7 @@ import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol" contract IVStaffManager is AccessControl { bytes32 public constant STAFF_ROLE = keccak256("STAFF_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); mapping(address => Structs.Staff) public staff; @@ -76,4 +77,17 @@ contract IVStaffManager is AccessControl { staff[_staff.member] = _staff; } + + function addStaffMemberMinterRole(address _member) external onlyAdmin { + _grantRole(MINTER_ROLE, _member); + } + + /** + * @notice Removes a campaign member + * @dev This function is only callable by the owner + * @param _member The member to remove + */ + function removeCampaignMember(address _member) external onlyRole(DEFAULT_ADMIN_ROLE) { + revokeRole(MINTER_ROLE, _member); + } } diff --git a/src/interfaces/IIVOccurrenceManager.sol b/src/interfaces/IIVOccurrenceManager.sol new file mode 100644 index 0000000..2900aef --- /dev/null +++ b/src/interfaces/IIVOccurrenceManager.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { Structs } from "../../src/library/Structs.sol"; + +/** + * @title IIVOccurrenceManager + * @notice Interface for the IVOccurrenceManager contract + * @dev We use the term occurrence to describe an event, appointment, or any other type of gathering. + * @author @codenamejason + */ +interface IIVOccurrenceManager { + function createOccurrence( + string memory name, + string memory description, + uint256 start, + uint256 end, + uint256 price, + address token, + address[] memory staff, + Structs.Metadata memory metadata + ) external returns (bytes32); + function updateOccurrence( + bytes32 occurenceId, + string memory name, + string memory description, + uint256 start, + uint256 end, + uint256 price, + address token, + address[] memory staff, + Structs.Metadata memory metadata + ) external; + function getOccurrence(bytes32 _occurenceId) + external + view + returns (Structs.Occurrence memory); + function hostOccurrence(bytes32 _occurenceId, address[] memory _attendees) external; + function recognizeOccurrence(bytes32 _occurenceId, Structs.Metadata memory _content) external; + function getStaffMemberByOccurrenceId(bytes32 _occurenceId, address _member) + external + view + returns (Structs.Staff memory); + function getStaffMembersForOccurrenceId(bytes32 _occurenceId) + external + view + returns (Structs.Staff[] memory); + function getOccurrences() external view returns (Structs.Occurrence[] memory); + function getOccurrenceById(bytes32 _occurenceIdId) + external + view + returns (Structs.Occurrence memory); +} diff --git a/src/library/Enums.sol b/src/library/Enums.sol index ffc3f3a..be56403 100644 --- a/src/library/Enums.sol +++ b/src/library/Enums.sol @@ -5,6 +5,8 @@ contract Enums { enum Status { Pending, Active, + Recognized, + Hosted, Inactive, Rejected } diff --git a/src/library/Errors.sol b/src/library/Errors.sol index 530711b..bf4b024 100644 --- a/src/library/Errors.sol +++ b/src/library/Errors.sol @@ -8,4 +8,5 @@ library Errors { error AlreadyRedeemed(uint256 eventId, uint256 tokenId); error NewSupplyTooLow(uint256 minted, uint256 supply); error OccurrenceDoesNotExist(bytes32 occurrenceId); + error NotCreator(address caller); } diff --git a/src/library/Structs.sol b/src/library/Structs.sol index 141f703..2b64340 100644 --- a/src/library/Structs.sol +++ b/src/library/Structs.sol @@ -34,5 +34,16 @@ library Structs { Enums.Status status; address[] staff; Metadata metadata; + address[] attendees; + } + + struct Class { + uint256 id; + uint256 supply; // total supply of this class? do we want this? + uint256 minted; + string name; + string description; + string imagePointer; + string metadata; // this is a pointer to json object that contains the metadata for this class } } From 5926e5e510be27330aac53ecfce53ad1d7a9b4b6 Mon Sep 17 00:00:00 2001 From: Jaxcoder Date: Sun, 29 Oct 2023 16:49:12 -0400 Subject: [PATCH 41/41] chore: add redemtion module --- src/IVERC721BaseToken.sol | 45 +++++++++---------------- src/IVERC721TokenContractFactory.sol | 2 ++ src/RedemtionModule.sol | 33 ++++++++++++++++++ src/Will4USNFT.sol | 26 +++++++------- src/library/Errors.sol | 5 ++- test/IVERC721BaseToken.t.sol | 2 +- test/IVERC721TokenContractFactory.t.sol | 2 +- test/Will4USNFTTest.t.sol | 12 +++---- 8 files changed, 76 insertions(+), 51 deletions(-) create mode 100644 src/RedemtionModule.sol diff --git a/src/IVERC721BaseToken.sol b/src/IVERC721BaseToken.sol index e61073e..9f6a8c9 100644 --- a/src/IVERC721BaseToken.sol +++ b/src/IVERC721BaseToken.sol @@ -12,8 +12,10 @@ import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; import { Errors } from "./library/Errors.sol"; import { Structs } from "./library/Structs.sol"; +import { RedemtionModule } from "./RedemtionModule.sol"; contract IVERC721BaseToken is + RedemtionModule, ERC721, ERC721Enumerable, ERC721URIStorage, @@ -31,11 +33,8 @@ contract IVERC721BaseToken is uint256 public maxMintablePerClass; mapping(uint256 => Structs.Class) public classes; - mapping(address => bool) public campaignMembers; mapping(address => mapping(uint256 => uint256)) public mintedPerClass; - // eventId => token => bool - mapping(uint256 => mapping(uint256 => bool)) public redeemed; - + /** * Events ************ */ @@ -46,7 +45,6 @@ contract IVERC721BaseToken is event ClassAdded(uint256 indexed classId, string metadata); event UpdatedClassTokenSupply(uint256 indexed classId, uint256 supply); event UpdatedMaxMintablePerClass(uint256 maxMintable); - event Redeemed(uint256 indexed eventId, uint256 indexed tokenId, uint256 indexed classId); /** * Modifiers ************ @@ -81,12 +79,12 @@ contract IVERC721BaseToken is */ /** - * @notice Awards campaign nft to supporter - * @dev This function is only callable by campaign members + * @notice Awards nft to address + * @dev This function is only callable by staff * @param _recipient The recipient of the item * @param _classId The class ID */ - function awardCampaignItem(address _recipient, uint256 _classId) + function awardItem(address _recipient, uint256 _classId) external onlyMinter(msg.sender) returns (uint256) @@ -95,7 +93,7 @@ contract IVERC721BaseToken is revert Errors.MaxMintablePerClassReached(_recipient, _classId, maxMintablePerClass); } - uint256 tokenId = _mintCampaingnItem(_recipient, _classId); + uint256 tokenId = _mintItem(_recipient, _classId); mintedPerClass[_recipient][_classId]++; emit ItemAwarded(tokenId, _recipient, _classId); @@ -104,12 +102,12 @@ contract IVERC721BaseToken is } /** - * @notice Awards campaign nft to a batch of supporters - * @dev This function is only callable by campaign members + * @notice Awards nft to a batch of addresses + * @dev This function is only callable by staff * @param _recipients The recipients of the item * @param _classIds The class IDs */ - function batchAwardCampaignItem(address[] memory _recipients, uint256[] memory _classIds) + function batchAwardItem(address[] memory _recipients, uint256[] memory _classIds) external onlyMinter(msg.sender) returns (uint256[] memory) @@ -122,7 +120,7 @@ contract IVERC721BaseToken is revert("You have reached the max mintable for this class"); } - tokenIds[i] = _mintCampaingnItem(_recipients[i], _classIds[i]); + tokenIds[i] = _mintItem(_recipients[i], _classIds[i]); mintedPerClass[_recipients[i]][_classIds[i]]++; emit ItemAwarded(tokenIds[i], _recipients[i], _classIds[i]); @@ -136,8 +134,8 @@ contract IVERC721BaseToken is } /** - * @notice Adds a new class to the campaign for issuance - * @dev This function is only callable by campaign members + * @notice Adds a new class + * @dev This function is only callable by staff * @param _name The name of the class * @param _description The description of the class * @param _imagePointer The image pointer for the class @@ -174,7 +172,7 @@ contract IVERC721BaseToken is /** * @notice Updates the token metadata - * @dev This function is only callable by campaign members - only use if you really need to + * @dev This function is only callable by staff - only use if you really need to * @param _tokenId The token ID to update * @param _classId The class ID * @param _newTokenURI The new token URI 🚨 must be a pointer to a json object 🚨 @@ -198,7 +196,7 @@ contract IVERC721BaseToken is /** * @notice Sets the class token supply - * @dev This function is only callable by campaign members + * @dev This function is only callable by staff * @param _classId The class ID * @param _supply The new supply */ @@ -228,17 +226,6 @@ contract IVERC721BaseToken is * View Functions ****** */ - /** - * @notice Returns if the token has been redeemed for an event - * @param _eventId The event ID - * @param _tokenId The token ID - * @return bool Returns true if the token has been redeemed - */ - - function getRedeemed(uint256 _eventId, uint256 _tokenId) external view returns (bool) { - return redeemed[_eventId][_tokenId]; - } - /** * @notice Returns the total supply for a class * @param _classId The class ID @@ -293,7 +280,7 @@ contract IVERC721BaseToken is * @param _recipient The recipient of the item * @param _classId The class ID */ - function _mintCampaingnItem(address _recipient, uint256 _classId) internal returns (uint256) { + function _mintItem(address _recipient, uint256 _classId) internal returns (uint256) { uint256 tokenId = ++_tokenIds; // update the class minted count diff --git a/src/IVERC721TokenContractFactory.sol b/src/IVERC721TokenContractFactory.sol index bf9d7cd..e29d596 100644 --- a/src/IVERC721TokenContractFactory.sol +++ b/src/IVERC721TokenContractFactory.sol @@ -92,6 +92,8 @@ contract IVERC721TokenContractFactory { }); emit Deployed(deployedContract); + + return deployedContract; } /// @notice Set the allowed deployer. diff --git a/src/RedemtionModule.sol b/src/RedemtionModule.sol new file mode 100644 index 0000000..15caab3 --- /dev/null +++ b/src/RedemtionModule.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.20; + +import { Errors } from "./library/Errors.sol"; +import { Structs } from "./library/Structs.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract RedemtionModule { + // occurrenceId => token => bool + mapping(bytes32 => mapping(uint256 => bool)) public redeemed; + + constructor() { } + + /** + * @notice Returns if the token has been redeemed for an event + * @param _occurrenceId The event ID + * @param _tokenId The token ID + * @return bool Returns true if the token has been redeemed + */ + function isRedeemed(bytes32 _occurrenceId, uint256 _tokenId) external view returns (bool) { + return redeemed[_occurrenceId][_tokenId]; + } + + function redeem(bytes32 _occurrenceId, uint256 _tokenId, address _recipient) external { + if (_recipient == address(0)) revert Errors.ZeroAddress(); + if (redeemed[_occurrenceId][_tokenId]) { + revert Errors.AlreadyRedeemed(_occurrenceId, _tokenId); + } + + redeemed[_occurrenceId][_tokenId] = true; + } +} diff --git a/src/Will4USNFT.sol b/src/Will4USNFT.sol index 8290609..9aec2a4 100644 --- a/src/Will4USNFT.sol +++ b/src/Will4USNFT.sol @@ -29,8 +29,8 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { mapping(uint256 => Class) public classes; mapping(address => bool) public campaignMembers; mapping(address => mapping(uint256 => uint256)) public mintedPerClass; - // eventId => token => bool - mapping(uint256 => mapping(uint256 => bool)) public redeemed; + // occurrenceId => token => bool + mapping(bytes32 => mapping(uint256 => bool)) public redeemed; struct Class { uint256 id; @@ -52,7 +52,7 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { event ClassAdded(uint256 indexed classId, string metadata); event UpdatedClassTokenSupply(uint256 indexed classId, uint256 supply); event UpdatedMaxMintablePerClass(uint256 maxMintable); - event Redeemed(uint256 indexed eventId, uint256 indexed tokenId, uint256 indexed classId); + event Redeemed(bytes32 indexed occurrenceId, uint256 indexed tokenId, uint256 indexed classId); /** * Modifiers ************ @@ -178,21 +178,21 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { /** * @notice Redeems a campaign item * @dev This function is only callable by campaign members - * @param _eventId The event ID + * @param _occurrenceId The occurrence ID * @param _tokenId The token ID */ - function redeem(uint256 _eventId, uint256 _tokenId) external onlyCampaingnMember(msg.sender) { + function redeem(bytes32 _occurrenceId, uint256 _tokenId) external onlyCampaingnMember(msg.sender) { if (super.ownerOf(_tokenId) == address(0)) { revert Errors.InvalidTokenId(_tokenId); } - if (redeemed[_eventId][_tokenId]) { - revert Errors.AlreadyRedeemed(_eventId, _tokenId); + if (redeemed[_occurrenceId][_tokenId]) { + revert Errors.AlreadyRedeemed(_occurrenceId, _tokenId); } - redeemed[_eventId][_tokenId] = true; + redeemed[_occurrenceId][_tokenId] = true; - emit Redeemed(_eventId, _tokenId, classes[_tokenId].id); + emit Redeemed(_occurrenceId, _tokenId, classes[_tokenId].id); } /** @@ -301,14 +301,14 @@ contract Will4USNFT is ERC721URIStorage, AccessControl { */ /** - * @notice Returns if the token has been redeemed for an event - * @param _eventId The event ID + * @notice Returns if the token has been redeemed for an occurrence + * @param _occurrenceId The occurrence ID * @param _tokenId The token ID * @return bool Returns true if the token has been redeemed */ - function getRedeemed(uint256 _eventId, uint256 _tokenId) external view returns (bool) { - return redeemed[_eventId][_tokenId]; + function getRedeemed(bytes32 _occurrenceId, uint256 _tokenId) external view returns (bool) { + return redeemed[_occurrenceId][_tokenId]; } /** diff --git a/src/library/Errors.sol b/src/library/Errors.sol index bf4b024..b7130db 100644 --- a/src/library/Errors.sol +++ b/src/library/Errors.sol @@ -5,8 +5,11 @@ library Errors { error Unauthorized(address caller); error InvalidTokenId(uint256 tokenId); error MaxMintablePerClassReached(address recipient, uint256 classId, uint256 maxMintable); - error AlreadyRedeemed(uint256 eventId, uint256 tokenId); + error AlreadyRedeemed(bytes32 occurrenceId, uint256 tokenId); error NewSupplyTooLow(uint256 minted, uint256 supply); error OccurrenceDoesNotExist(bytes32 occurrenceId); error NotCreator(address caller); + error ZeroAddress(); + error ZeroAmount(); + error TransferFailed(); } diff --git a/test/IVERC721BaseToken.t.sol b/test/IVERC721BaseToken.t.sol index 703adff..44828ec 100644 --- a/test/IVERC721BaseToken.t.sol +++ b/test/IVERC721BaseToken.t.sol @@ -32,7 +32,7 @@ contract IVERC721BaseTokenTest is Test { tokenContract.addClass( "Volunteer", "Test volunteer class", "https://yourpointer", "", 500000 ); - tokenContract.awardCampaignItem(makeAddr("recipient1"), 1); + tokenContract.awardItem(makeAddr("recipient1"), 1); vm.stopPrank(); assertEq(tokenContract.totalSupply(), 1, "totalSupply should be 1"); diff --git a/test/IVERC721TokenContractFactory.t.sol b/test/IVERC721TokenContractFactory.t.sol index 7d5ad66..3c2597c 100644 --- a/test/IVERC721TokenContractFactory.t.sol +++ b/test/IVERC721TokenContractFactory.t.sol @@ -35,7 +35,7 @@ contract IVERC721TokenContractFactoryTest is Test { assertNotEq(deployedAddress, address(0)); - MockIVERC721Token(deployedAddress).awardCampaignItem(deployerAddress, 1); + MockIVERC721Token(deployedAddress).awardItem(deployerAddress, 1); assertEq(MockIVERC721Token(deployedAddress).balanceOf(deployerAddress), 1); vm.stopPrank(); } diff --git a/test/Will4USNFTTest.t.sol b/test/Will4USNFTTest.t.sol index dff72a2..7502e9a 100644 --- a/test/Will4USNFTTest.t.sol +++ b/test/Will4USNFTTest.t.sol @@ -26,7 +26,7 @@ contract Will4USNFTTest is Test { event CampaignMemberAdded(address indexed member); event CampaignMemberRemoved(address indexed member); event ClassAdded(uint256 indexed classId, string metadata); - event Redeemed(uint256 indexed eventId, uint256 indexed tokenId, uint256 indexed classId); + event Redeemed(bytes32 indexed occurrenceId, uint256 indexed tokenId, uint256 indexed classId); event RoleGranted(bytes32 indexed role, address indexed account, address sender); function setUp() public { @@ -131,17 +131,17 @@ contract Will4USNFTTest is Test { function test_redeem() public { vm.startPrank(deployerAddress); vm.expectEmit(true, true, true, true); - emit Redeemed(1, 1, 1); + emit Redeemed("0x01", 1, 1); - nftContract.redeem(1, 1); + nftContract.redeem("0x01", 1); } function test_revert_redeem_AlreadyRedeemed() public { vm.startPrank(deployerAddress); - nftContract.redeem(1, 1); + nftContract.redeem("0x01", 1); vm.expectRevert(); - nftContract.redeem(1, 1); + nftContract.redeem("0x01", 1); vm.stopPrank(); } @@ -149,7 +149,7 @@ contract Will4USNFTTest is Test { vm.startPrank(makeAddr("chad")); vm.expectRevert(); - nftContract.redeem(1, 1); + nftContract.redeem("0x01", 1); vm.stopPrank(); }