diff --git a/README.md b/README.md index 023e9c5..740edc0 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,109 @@ Deploy and verify a contract source .env # To deploy and verify our contract -forge script script/WikiNoValidator.s.sol:WikiNoValidator --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY -vvvv --gas-price 60 --legacy +forge script script/BrainPassDeployer.s.sol:BrainPassDeployer --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY -vvvv --gas-price 60 --legacy ``` + +# Expalaining How BrainPassCollectibles Contract Works + +BrainPassCollectibles is a Solidity smart contract that enables the creation and management of BrainPass NFTs. It allows users to buy passes to have some Wiki privileges on IQ Wiki and provides functionalities to mint NFTs, increase pass time, and manage pass types. + +## Usage + +### Contract Deployment +Deploy the `BrainPassCollectibles` contract by providing the address of the IQ token and the `baseTokenURI` contract as constructor parameters. + +## `pause()` +Pauses the contract and prevents any further actions. Can only be called by the contract owner. + +## `unpause()` +Unpauses the contract and resumes normal operations. Can only be called by the contract owner. + +## `configureMintLimit()` +Configures the lower and upper limits for minting NFTs. +- `lowerLimit`: Minimum duration (in days) for a subscription. +- `upperLimit`: Maximum duration (in days) for a subscription. +Only the contract owner can call this function. + +## `addPassType()` +Adds a new pass type. +- `pricePerDay`: Price per day of the new pass type. +- `name`: Name of the new pass type. +- `maxTokens`: Total number of tokens in the pass. +Only the contract owner can call this function and only when the contract is not paused + +## `togglePassTypeStatus()` +toggles the status of a specific pass type from paused to unpaused. +- `passId`: ID of the pass type to be deactivated. +Only the contract owner can call this function and only when the contract is not paused + +## `mintNFT()` +Mints an NFT of a particular pass type. +- `passId`: ID of the pass type to mint. +- `startTimestamp`: Time when the NFT subscription starts. +- `endTimestamp`: Time when the NFT subscription ends. +Can call this function only when the contract is not paused, using the `whenNotPaused` modifier + +## `increaseEndTime()` +Increases the EndTime time of an NFT. +- `tokenId`: ID of the NFT whose time is to be increased. +- `newEndTime`: New subscription end time for the NFT. +Only when the contract is not paused can this function be called, using the `whenNotPaused` modifier + +## `withdrawEther()` +Withdraws any amount of Ether held in the contract. +- `receiver`: The address the Ether would be sent to. +- `amount`: The amount to be sent to the address from the contract. +Only the contract owner can call this function. + + +## `withdrawIQ()` +Withdraws any amount of IQ tokens held in the contract. +- `receiver`: The address the Ether would be sent to. +- `amount`: The amount to be sent to the address from the contract. +Only the contract owner can call this function. + +## `setBaseURI` +sets the BaseUri in the contructor, during deployment of the contract. + +## `_baseURI` +Returns the baseUri of the all tokens + +## `getUserPassDetails()` +Retrieves the details of an NFT owned by a specific user for a given pass type. +- `user`: Address of the user. + +## `getAllPassType()` +Retrieves the details of all the pass types added to the contract. + +## `getPassType()` +Retrieves the details of a specific pass type. +- `passId`: ID of the pass type + +## Events + +The contract emits the following events: + +- `BrainPassBought`: Emitted when a user buys a BrainPass NFT. +- `PassTimeIncreased`: Emitted when the time of a BrainPass NFT is increased. +- `NewPassAdded`: Emitted when a new pass type is added. +- `PassTypeStatusToggled`: Emitted when a pass is paused or unpaused. + +## PICTORAL EXPLANATION +![image](https://github.com/EveripediaNetwork/ep-contract/assets/75235148/eee4d631-28d9-4ca4-bc0e-62e5a02998a2) + + +# Expalaining How BrainPassValidiator Contract Works +BrainPassValidiator is a Solidity smart contract that validates a user to have the ability to post a wiki on IQ Wiki. + + +## Usage + +### Contract Deployment +Deploy the `BrainPassValidiator` contract by providing the address of the BrainPassCollectibles as a constructor parameter. + +## `validate()` +`user`: The user address that is validated +Checks the validity of an address and returns a boolean values based on the result. + diff --git a/remappings.txt b/remappings.txt index 2cf5bd5..eed2212 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,3 +4,4 @@ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ prb-test/=lib/prb-test/src/ solmate/=lib/solmate/src/ + diff --git a/script/BrainPassDeployer.s.sol b/script/BrainPassDeployer.s.sol new file mode 100644 index 0000000..3c86b78 --- /dev/null +++ b/script/BrainPassDeployer.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "../lib/forge-std/src/Script.sol"; +import {console} from "../lib/forge-std/src/console.sol"; +import {BrainPassCollectibles} from "../src/BrainPass/BrainPass.sol"; + +contract BrainPassDeployer is Script { + function run() external { + vm.startBroadcast(); + console.log("Deploying Brainpass deployer...."); + BrainPassCollectibles brainPass = new BrainPassCollectibles( + 0x5E959c60f86D17fb7D764AB69B654227d464E820, + "https://api.dev.braindao.org/brainpass/" + ); + console.log("Brainpass Deployed To:", address(brainPass)); + vm.stopBroadcast(); + } +} diff --git a/script/BrainPassValidator.s.sol b/script/BrainPassValidator.s.sol new file mode 100644 index 0000000..4447eff --- /dev/null +++ b/script/BrainPassValidator.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "../lib/forge-std/src/Script.sol"; +import {console} from "../lib/forge-std/src/console.sol"; +import {BrainPassValidiator} from "../src/BrainPass/BrainPassValidator.sol"; + +contract BrainPassValidiatorDeployer is Script { + function run() external { + vm.startBroadcast(); + console.log("Deploying Brainpass Valdiator...."); + BrainPassValidiator brainPassValidator = new BrainPassValidiator( + 0x6e213cE219d7ef282ACCC7734040D67875828be4 + ); + console.log("Brainpass Deployed To", address(brainPassValidator)); + vm.stopBroadcast(); + } +} diff --git a/script/WikiWhitelistValidator.s.sol b/script/WikiWhitelistValidator.s.sol index 27ae7b2..11c37f7 100644 --- a/script/WikiWhitelistValidator.s.sol +++ b/script/WikiWhitelistValidator.s.sol @@ -22,10 +22,8 @@ contract WikiWhitelistValidator is Script { validator.whitelistEditor(address(0xF6d9467758C08d05571f1bFa0a03A2286cE1F043)); validator.whitelistEditor(address(0x2fE6aCD015384E1ee5138eF79fe1a434dA8FA12e)); validator.whitelistEditor(address(0xb029c0367CCFeEFBc6D00B4cc22fcbFd6A781F5c)); - validator.whitelistEditor(address(0x9fEAB70f3c4a944B97b7565BAc4991dF5B7A69ff)); validator.whitelistEditor(address(0x14B68b85E1037d1C75726b7794e99C20554f9CC3)); - validator.setOwner(owner); vm.stopBroadcast(); diff --git a/src/BrainPass/BrainPass.sol b/src/BrainPass/BrainPass.sol new file mode 100644 index 0000000..7af8255 --- /dev/null +++ b/src/BrainPass/BrainPass.sol @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import {ERC721Pausable} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Pausable.sol"; +import {Counters} from "openzeppelin-contracts/contracts/utils/Counters.sol"; +import "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {Wiki} from "../../src/Wiki.sol"; + +interface IERC20 { + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 amount) external returns (bool); +} + +/// @title BRAIN Pass NFT +/// @author Oleanji +/// @notice A pass for IQ Wiki Editors + +contract BrainPassCollectibles is ERC721, ERC721Pausable, Ownable { + /// ----------------------------------------------------------------------- + /// Errors + /// ----------------------------------------------------------------------- + error MintingPaymentFailed(); + error IncreseTimePaymentFailed(); + error AlreadyMintedAPass(); + error NotTheOwnerOfThisNft(); + error InvalidMaxTokensForAPass(); + error PassTypeNotFound(); + error PassMaxSupplyReached(); + error EtherNotEnoughToWithdraw(); + error TransferFailed(); + error DurationNotInTimeFrame(); + error PassTypeIsPaused(); + error IQNotEnoughToWithdraw(); + + /// ----------------------------------------------------------------------- + /// Inheritances + /// ----------------------------------------------------------------------- + using Counters for Counters.Counter; + + /// ----------------------------------------------------------------------- + /// Structs + /// ----------------------------------------------------------------------- + + struct UserPassItem { + uint256 tokenId; + uint256 passId; + uint256 startTimestamp; + uint256 endTimestamp; + } + + struct PassType { + uint256 passId; + string name; + uint256 pricePerDay; + uint256 maxTokens; + uint256 lastMintedId; + bool isPaused; + } + + /// ----------------------------------------------------------------------- + /// Mappings + /// ----------------------------------------------------------------------- + + mapping(uint256 => PassType) public passTypes; + mapping(address => UserPassItem) internal addressToNFTPass; + + /// ----------------------------------------------------------------------- + /// Constant + /// ----------------------------------------------------------------------- + + address public iqToken; + + /// ----------------------------------------------------------------------- + /// Variables + /// ----------------------------------------------------------------------- + + Counters.Counter private passIdTracker; + Counters.Counter private tokenIdTracker; + uint256 public MINT_LOWER_LIMIT = 28; + uint256 public MINT_UPPER_LIMIT = 365; + string public baseTokenURI; + + using Strings for uint256; + + /// ----------------------------------------------------------------------- + /// Constructor + /// ----------------------------------------------------------------------- + + constructor( + address IqAddr, + string memory _baseTokenURI + ) ERC721("BRAINPASS", "BRP") { + iqToken = IqAddr; + passIdTracker.increment(); + tokenIdTracker.increment(); + setBaseURI(_baseTokenURI); + } + + /// ----------------------------------------------------------------------- + /// External functions + /// ----------------------------------------------------------------------- + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + /// @notice Change the MintLimit for the Nfts + /// @param lowerLimit the new lower limit for how short a nft can be subscribed for + /// @param upperLimit the new upper limit for how long a nft can be subscribed for + function configureMintLimit( + uint256 lowerLimit, + uint256 upperLimit + ) external onlyOwner { + MINT_UPPER_LIMIT = upperLimit; + MINT_LOWER_LIMIT = lowerLimit; + } + + /// @notice Add a new Pass Type + /// @param pricePerDay the price per day of the new pass type + /// @param name the name of the new pass type to be added + /// @param maxTokens the total number of tokens in the pass + function addPassType( + uint256 pricePerDay, + string memory name, + uint256 maxTokens + ) external onlyOwner whenNotPaused { + if (maxTokens <= 0) revert InvalidMaxTokensForAPass(); + uint256 passId = passIdTracker.current(); + passTypes[passId] = PassType( + passId, + name, + pricePerDay, + maxTokens, + 0, + false + ); + passIdTracker.increment(); + emit NewPassAdded(passId, name, maxTokens, pricePerDay); + } + + /// @notice Toggles the staus of a pass type (paused or unpaused) + /// @param passId the Id of the pass + function togglePassTypeStatus( + uint256 passId + ) external onlyOwner whenNotPaused { + if (passId >= passIdTracker.current()) revert PassTypeNotFound(); + PassType storage passType = passTypes[passId]; + bool newStatus = !passType.isPaused; + + passTypes[passId] = PassType( + passType.passId, + passType.name, + passType.pricePerDay, + passType.maxTokens, + passType.lastMintedId, + newStatus + ); + + emit PassTypeStatusToggled(passId, passType.name); + } + + /// @notice Mint and NFT of a particular passtype + /// @param passId The id of the passtype to mint + /// @param startTimestamp The time when the NFT subcription time starts + /// @param endTimestamp The time when the NFT subcription time ends + function mintNFT( + uint256 passId, + uint256 startTimestamp, + uint256 endTimestamp + ) external whenNotPaused { + if (passId >= passIdTracker.current()) revert PassTypeNotFound(); + if (addressToNFTPass[msg.sender].tokenId != 0) + revert AlreadyMintedAPass(); + PassType storage passType = passTypes[passId]; + if (passType.isPaused) revert PassTypeIsPaused(); + if (passType.lastMintedId >= passType.maxTokens) + revert PassMaxSupplyReached(); + if (!validatePassDuration(startTimestamp, endTimestamp)) + revert DurationNotInTimeFrame(); + + uint256 price = calculatePrice(passId, startTimestamp, endTimestamp); + bool success = IERC20(iqToken).transferFrom( + msg.sender, + address(this), + price + ); + if (!success) revert MintingPaymentFailed(); + uint256 tokenId = tokenIdTracker.current(); + passType.lastMintedId += 1; + UserPassItem memory purchase = UserPassItem( + tokenId, + passId, + startTimestamp, + endTimestamp + ); + addressToNFTPass[msg.sender] = purchase; + tokenIdTracker.increment(); + _safeMint(msg.sender, tokenId); + + emit BrainPassBought( + msg.sender, + passType.name, + price, + passId, + tokenId, + startTimestamp, + endTimestamp + ); + } + + /// @notice Increase the time to hold a PassNft + /// @param tokenId The Id of the NFT whose time is to be increased + /// @param newEndTime The new subcription endTime for the of the NFT + function increaseEndTime( + uint256 tokenId, + uint256 newEndTime + ) external whenNotPaused { + UserPassItem memory pass = addressToNFTPass[msg.sender]; + + PassType memory passType = passTypes[pass.passId]; + if (passType.isPaused) revert PassTypeIsPaused(); + + if (addressToNFTPass[msg.sender].tokenId != tokenId) + revert NotTheOwnerOfThisNft(); + uint256 newStartTime; + if (pass.endTimestamp < block.timestamp) { + newStartTime = block.timestamp; + } else { + newStartTime = pass.endTimestamp; + } + if (!validatePassDuration(newStartTime, newEndTime)) + revert DurationNotInTimeFrame(); + + uint256 price = calculatePrice(pass.passId, newStartTime, newEndTime); + bool success = IERC20(iqToken).transferFrom( + msg.sender, + address(this), + price + ); + if (!success) revert IncreseTimePaymentFailed(); + UserPassItem memory purchase = UserPassItem( + pass.tokenId, + pass.passId, + pass.startTimestamp, + newEndTime + ); + + addressToNFTPass[msg.sender] = purchase; + emit PassTimeIncreased( + msg.sender, + price, + pass.passId, + tokenId, + pass.startTimestamp, + pass.endTimestamp + ); + } + + /// @notice Withdraws any amount in the contract + function withdrawEther( + address receiver, + uint256 amount + ) external payable onlyOwner { + uint256 ethbalance = address(this).balance; + if (ethbalance < amount) revert EtherNotEnoughToWithdraw(); + (bool success, ) = (receiver).call{value: amount}(""); + if (!success) revert TransferFailed(); + } + + function withdrawIQ( + address receiver, + uint256 amount + ) external payable onlyOwner { + uint256 tokenBalance = IERC20(iqToken).balanceOf(address(this)); + if (tokenBalance < amount) revert IQNotEnoughToWithdraw(); + bool tokenSuccess = IERC20(iqToken).transfer(receiver, amount); + if (!tokenSuccess) revert TransferFailed(); + } + + /// ----------------------------------------------------------------------- + /// Internal Functions + /// ----------------------------------------------------------------------- + + /// @notice Calculate the price of an Nft + /// @param startTimestamp The start time to calculate the price of the Nft + /// @param endTimestamp The end time to calculate the price of the Nft + function calculatePrice( + uint256 passId, + uint256 startTimestamp, + uint256 endTimestamp + ) internal view returns (uint256) { + PassType memory passType = passTypes[passId]; + uint256 subscriptionPeriodInDays = (endTimestamp - startTimestamp) / + 1 days; + // Calculate the total price + uint256 totalPrice = subscriptionPeriodInDays * passType.pricePerDay; + return totalPrice; + } + + /// @notice Validates the Timestamp Duration for minting Nft + /// @param startTimestamp The start time for checking the validity of a pass + /// @param endTimestamp The end time for checking the validity of a pass + function validatePassDuration( + uint256 startTimestamp, + uint256 endTimestamp + ) internal view returns (bool) { + uint256 durationInDays = (endTimestamp - startTimestamp) / 1 days; + if (endTimestamp < block.timestamp) { + revert DurationNotInTimeFrame(); + } + return + durationInDays >= MINT_LOWER_LIMIT && + durationInDays <= MINT_UPPER_LIMIT; + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(ERC721, ERC721Pausable) { + super._beforeTokenTransfer(from, to, amount); + } + + function tokenURI( + uint256 tokenId + ) public view virtual override returns (string memory) { + require(_exists(tokenId), "Not Exist"); + + string memory baseURI = _baseURI(); + + return + bytes(baseURI).length > 0 + ? string(abi.encodePacked(baseURI, tokenId.toString(), ".json")) + : ""; + } + + /// ----------------------------------------------------------------------- + /// Public + /// ----------------------------------------------------------------------- + + function setBaseURI(string memory _baseTokenURI) public onlyOwner { + baseTokenURI = _baseTokenURI; + } + + /// ----------------------------------------------------------------------- + /// Getters + /// ----------------------------------------------------------------------- + + /// @notice Gets the baseUri for the contract + function _baseURI() internal view virtual override returns (string memory) { + return baseTokenURI; + } + + /// @notice Gets all the NFT owned by an address + /// @param user The address of the user + function getUserPassDetails( + address user + ) public view returns (UserPassItem memory) { + UserPassItem memory userToken = addressToNFTPass[user]; + return userToken; + } + + /// @notice Gets all the PassType created + function getAllPassType() external view returns (PassType[] memory) { + uint256 total = passIdTracker.current(); + PassType[] memory passType = new PassType[](total); + for (uint256 i = 1; i < total; i++) { + passType[i] = passTypes[i]; + } + return passType; + } + + /// @notice Gets all the details of a passtype + /// @param passId The id of the passtype + function getPassType( + uint256 passId + ) external view returns (PassType memory) { + PassType memory passType = passTypes[passId]; + return (passType); + } + + /// ----------------------------------------------------------------------- + /// Events + /// ----------------------------------------------------------------------- + + event BrainPassBought( + address indexed _owner, + string _passName, + uint256 _price, + uint256 _passId, + uint256 _tokenId, + uint256 _startTimestamp, + uint256 _endTimestamp + ); + + event PassTimeIncreased( + address indexed _owner, + uint256 _price, + uint256 _passId, + uint256 _tokenId, + uint256 _startTimestamp, + uint256 _newEndTimestamp + ); + + event NewPassAdded( + uint256 indexed _passId, + string _name, + uint256 _maxtokens, + uint256 _pricePerDay + ); + + event PassTypeStatusToggled(uint256 indexed _passId, string _name); +} diff --git a/src/BrainPass/BrainPassValidator.sol b/src/BrainPass/BrainPassValidator.sol new file mode 100644 index 0000000..7f3bc80 --- /dev/null +++ b/src/BrainPass/BrainPassValidator.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {BrainPassCollectibles} from "./BrainPass.sol"; + +/// @title BRAIN Pass Validator +/// @author Oleanji +/// @notice A validation for the Nft +contract BrainPassValidiator { + + /// ----------------------------------------------------------------------- + /// variables + /// ----------------------------------------------------------------------- + BrainPassCollectibles brainPass; + + constructor(address brainPassAddr) { + brainPass = BrainPassCollectibles(brainPassAddr); + } + + /// ----------------------------------------------------------------------- + /// External functions + /// ----------------------------------------------------------------------- + + /// @notice Validate Post + /// @param user The user to validiate + function validate(address user) external view returns (bool) { + if (brainPass.balanceOf(user) == 0) return false; + if (brainPass.getUserPassDetails(user).endTimestamp < block.timestamp) { + return false; + } + return true; + } +} diff --git a/test/BrainPass.t.sol b/test/BrainPass.t.sol new file mode 100644 index 0000000..217f314 --- /dev/null +++ b/test/BrainPass.t.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +import {PRBTest} from "prb-test/PRBTest.sol"; +import {Cheats} from "forge-std/Cheats.sol"; +import "forge-std/console.sol"; +import {stdError} from "forge-std/Errors.sol"; +import {BrainPassCollectibles} from "../src/BrainPass/BrainPass.sol"; +import {ERC721TokenReceiver} from "solmate/tokens/ERC721.sol"; +import {MockERC20} from "../lib/solmate/src/test/utils/mocks/MockERC20.sol"; + +contract BrainPassTest is PRBTest, Cheats { + BrainPassCollectibles BrainPass; + address alice = vm.addr(0x2); + address bob = vm.addr(0x3); + address doe = vm.addr(0x4); + MockERC20 mockERC20; + + function setUp() public { + mockERC20 = new MockERC20("Mock IQ Token", "MIT", 18); //mocking IQ token + BrainPass = new BrainPassCollectibles( + address(mockERC20), + "https://example.com/" + ); + BrainPass.addPassType(15e18, "GoldPass", 200); + mockERC20.mint(alice, 20000e18); + mockERC20.mint(bob, 20000e18); + mockERC20.mint(doe, 20000e18); + } + + function testAddPassType() public { + BrainPass.addPassType(15e18, "GoldPass", 200); + string memory _name = BrainPass.getPassType(1).name; + assertEq(_name, "GoldPass"); + } + + function testmintNFTWrong() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 9000e18); + assertEq(BrainPass.balanceOf(alice), 0); + BrainPass.mintNFT(1, 172800, 5184000); + assertEq(BrainPass.balanceOf(alice), 1); + vm.expectRevert(BrainPassCollectibles.AlreadyMintedAPass.selector); + BrainPass.mintNFT(1, 172800, 5184000); + vm.stopPrank(); + } + + function testMintDurationNotInTimeFrame() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 9000e18); + vm.expectRevert(BrainPassCollectibles.DurationNotInTimeFrame.selector); + BrainPass.mintNFT(1, 172800, 518400); + vm.stopPrank(); + } + + function testInvalidMaxTokensForAPass() public { + vm.expectRevert( + BrainPassCollectibles.InvalidMaxTokensForAPass.selector + ); + BrainPass.addPassType(15e18, "OleanjiPass", 0); + } + + function testPassTypeNotFound() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 9000e18); + vm.expectRevert(BrainPassCollectibles.PassTypeNotFound.selector); + BrainPass.mintNFT(4, 172800, 5184000); + } + + function testCannotMintPausedPass() public { + BrainPass.togglePassTypeStatus(1); + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 9000e18); + vm.expectRevert(BrainPassCollectibles.PassTypeIsPaused.selector); + BrainPass.mintNFT(1, 172800, 5184000); + } + + function testmintNFT() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 3e18); + vm.expectRevert(stdError.arithmeticError); + BrainPass.mintNFT(1, 172800, 5184000); + mockERC20.approve(address(BrainPass), 9000e18); + assertEq(BrainPass.balanceOf(alice), 0); + BrainPass.mintNFT(1, 172800, 5184000); + assertEq(BrainPass.balanceOf(alice), 1); + assertEq(mockERC20.balanceOf(address(BrainPass)), 870e18); + uint256 mintedPass = BrainPass.getUserPassDetails(alice).tokenId; + assertEq(mintedPass, 1); + vm.stopPrank(); + } + + function testDifferentPassMint() public { + BrainPass.addPassType(15e18, "OleanjiPass", 2); + BrainPass.addPassType(15e18, "KesarPass", 2); + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 20000e18); + BrainPass.mintNFT(1, 172800, 5184000); + uint256 firstId = BrainPass.getUserPassDetails(alice).tokenId; + assertEq(BrainPass.tokenURI(firstId), "https://example.com/1.json"); + console.log(BrainPass.tokenURI(firstId), firstId); + vm.stopPrank(); + vm.startPrank(doe); + mockERC20.approve(address(BrainPass), 20000e18); + BrainPass.mintNFT(2, 172800, 5184000); + uint256 newId = BrainPass.getUserPassDetails(doe).tokenId; + assertEq(BrainPass.tokenURI(newId), "https://example.com/2.json"); + console.log(BrainPass.tokenURI(newId), newId); + vm.stopPrank(); + vm.startPrank(bob); + mockERC20.approve(address(BrainPass), 20000e18); + BrainPass.mintNFT(3, 172800, 5184000); + uint256 newIds = BrainPass.getUserPassDetails(bob).tokenId; + assertEq(BrainPass.tokenURI(newIds), "https://example.com/3.json"); + console.log(BrainPass.tokenURI(newIds), newIds); + uint256 mintedPass = BrainPass.getUserPassDetails(doe).tokenId; + console.log(BrainPass.balanceOf(alice)); + assertEq(mintedPass, 2); + vm.stopPrank(); + } + + function testIncreaseTimeWrong() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 9000e18); + BrainPass.mintNFT(1, 172800, 5184000); + uint256 _tokenId = BrainPass.getUserPassDetails(alice).tokenId; + BrainPass.increaseEndTime(_tokenId, 8640000); + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(BrainPassCollectibles.NotTheOwnerOfThisNft.selector); + BrainPass.increaseEndTime(_tokenId, 8640000); + } + + function testIncreaseTime() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 12000e18); + assertEq(BrainPass.balanceOf(alice), 0); + BrainPass.mintNFT(1, 172800, 5184000); + assertEq(BrainPass.balanceOf(alice), 1); + uint256 _tokenId = BrainPass.getUserPassDetails(alice).tokenId; + assertEq(_tokenId, 1); + BrainPass.increaseEndTime(_tokenId, 8640000); + assertEq(mockERC20.balanceOf(address(BrainPass)), 1470e18); + BrainPass.getUserPassDetails(alice); + uint _endTine = BrainPass.getUserPassDetails(alice).endTimestamp; + assertEq(_endTine, 8640000); + } + + function testGetAllPassType() public { + assertEq(BrainPass.getAllPassType().length, 2); + BrainPass.addPassType(400e18, "PlatinumPass", 3000); + assertEq(BrainPass.getPassType(2).name, "PlatinumPass"); + assertEq(BrainPass.getAllPassType().length, 3); + } + + function testMintNftWrong() public { + BrainPass.addPassType(15e18, "OleanjiPass", 2); + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 1700e18); + assertEq(BrainPass.balanceOf(alice), 0); + BrainPass.mintNFT(2, 172800, 5184000); + assertEq(BrainPass.balanceOf(alice), 1); + vm.stopPrank(); + vm.startPrank(bob); + mockERC20.approve(address(BrainPass), 1700e18); + BrainPass.mintNFT(2, 172800, 5184000); + assertEq(BrainPass.balanceOf(bob), 1); + vm.stopPrank(); + vm.startPrank(doe); + mockERC20.approve(address(BrainPass), 1700e18); + vm.expectRevert(BrainPassCollectibles.PassMaxSupplyReached.selector); + BrainPass.mintNFT(2, 172800, 5184000); + } + + function testWithdrawTokensErrors() public { + vm.expectRevert(BrainPassCollectibles.IQNotEnoughToWithdraw.selector); + BrainPass.withdrawIQ(alice, 200e18); + assertEq(mockERC20.balanceOf(address(BrainPass)), 0); + mockERC20.mint(address(BrainPass), 20000e18); + assertEq(mockERC20.balanceOf(address(BrainPass)), 20000e18); + vm.expectRevert( + BrainPassCollectibles.EtherNotEnoughToWithdraw.selector + ); + BrainPass.withdrawEther(alice, 200e18); + } + + function testWithdrawTokens() public { + mockERC20.mint(address(BrainPass), 200e18); + BrainPass.withdrawIQ(alice, 200e18); + assertEq(mockERC20.balanceOf(address(alice)), 20200e18); + vm.deal(address(BrainPass), 4 ether); + assertEq(address(BrainPass).balance, 4e18); + assertEq(address(alice).balance, 0); + BrainPass.withdrawEther(alice, 4e18); + assertEq(address(alice).balance, 4e18); + } + + function testConfigureMintLimit() public { + assertEq(BrainPass.MINT_LOWER_LIMIT(), 28); + assertEq(BrainPass.MINT_UPPER_LIMIT(), 365); + BrainPass.configureMintLimit(50, 730); + assertEq(BrainPass.MINT_LOWER_LIMIT(), 50); + assertEq(BrainPass.MINT_UPPER_LIMIT(), 730); + } + + function testPauseAndUnPauseContract() public { + BrainPass.pause(); + vm.startPrank(alice); + vm.expectRevert("Pausable: paused"); + BrainPass.mintNFT(1, 172800, 5184000); + vm.stopPrank(); + BrainPass.unpause(); + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 19700e18); + assertEq(BrainPass.balanceOf(alice), 0); + BrainPass.mintNFT(1, 172800, 5184000); + assertEq(BrainPass.balanceOf(alice), 1); + vm.stopPrank(); + } + + function testTokenTranferWrong() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 19700e18); + assertEq(BrainPass.balanceOf(alice), 0); + BrainPass.mintNFT(1, 172800, 5184000); + vm.stopPrank(); + BrainPass.pause(); + vm.startPrank(alice); + vm.expectRevert("ERC721Pausable: token transfer while paused"); + BrainPass.safeTransferFrom(alice, bob, 1); + assertEq(BrainPass.balanceOf(alice), 1); + assertEq(BrainPass.balanceOf(bob), 0); + } + + function testTokenTranfer() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 19700e18); + assertEq(BrainPass.balanceOf(alice), 0); + BrainPass.mintNFT(1, 172800, 5184000); + BrainPass.safeTransferFrom(alice, bob, 1); + assertEq(BrainPass.balanceOf(alice), 0); + assertEq(BrainPass.balanceOf(bob), 1); + } +} diff --git a/test/BrainPassValidator.t.sol b/test/BrainPassValidator.t.sol new file mode 100644 index 0000000..67fde91 --- /dev/null +++ b/test/BrainPassValidator.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +import {PRBTest} from "prb-test/PRBTest.sol"; +import {Cheats} from "forge-std/Cheats.sol"; +import {stdError} from "forge-std/Errors.sol"; +import {BrainPassCollectibles} from "../src/BrainPass/BrainPass.sol"; +import {ERC721TokenReceiver} from "solmate/tokens/ERC721.sol"; +import {MockERC20} from "../lib/solmate/src/test/utils/mocks/MockERC20.sol"; +import {BrainPassValidiator} from "../src/BrainPass/BrainPassValidator.sol"; + +contract BrainPassValidatorTest is PRBTest, Cheats { + BrainPassCollectibles BrainPass; + BrainPassValidiator brainPassValidator; + address alice = vm.addr(0x2); + address bob = vm.addr(0x3); + MockERC20 mockERC20; + + function setUp() public { + mockERC20 = new MockERC20("Mock IQ Token", "MIT", 18); //mocking IQ token + BrainPass = new BrainPassCollectibles( + address(mockERC20), + "http://example.com" + ); + brainPassValidator = new BrainPassValidiator(address(BrainPass)); + BrainPass.addPassType(15e18, "Gold", 200); + mockERC20.mint(alice, 20000e18); + } + + function testPostWikiUserWithNoPass() public { + assertEq(brainPassValidator.validate(alice), false); + } + + function testPostWikiRight() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 1700e18); + BrainPass.mintNFT(1, 1685638993, 1693587793); // june 1st - sept 1st (3 months) + assertEq(brainPassValidator.validate(alice), true); + vm.stopPrank(); + } + + function testPostWikiPassExpired() public { + vm.startPrank(alice); + mockERC20.approve(address(BrainPass), 1700e18); + BrainPass.mintNFT(1, 1685638993, 1693587793); + assertEq(brainPassValidator.validate(alice), true); + skip(1685638993 + 7948800); + assertEq(brainPassValidator.validate(alice), false); + } +}