diff --git a/contracts/Raffle.sol b/contracts/Raffle.sol index 5ef18b3..590fbbb 100644 --- a/contracts/Raffle.sol +++ b/contracts/Raffle.sol @@ -72,7 +72,9 @@ contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface { function enterRaffle() public payable { // require(msg.value > i_entranceFee, "ETH too less!"); - if (msg.value < i_entranceFee) { + uint256 msgValue = msg.value; + + if (msgValue < i_entranceFee) { revert Raffle__NotEnoughETHEntered(); } if (s_raffleState != RaffleState.OPEN) { @@ -240,4 +242,12 @@ contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface { function getRecentWinner() public view returns (address) { return s_recentWinner; } + + function getRaffleState() public view returns (RaffleState) { + return s_raffleState; + } + + function getInterval() public view returns (uint256) { + return i_interval; + } } diff --git a/deploy/01-deploy-raffle.js b/deploy/01-deploy-raffle.js index a36dff5..3016913 100644 --- a/deploy/01-deploy-raffle.js +++ b/deploy/01-deploy-raffle.js @@ -1,4 +1,4 @@ -const { network, ethers } = require("hardhat"); +const { network, ethers, deployments } = require("hardhat"); const { networkConfig, developmentChains, @@ -8,13 +8,13 @@ const { verify } = require("../utils/verify"); module.exports = async ({ getNamedAccounts, deployments }) => { const { deploy, log, get } = deployments; - const { deployer } = await getNamedAccounts(); + const { deployer, player } = await getNamedAccounts(); const chainId = network.config.chainId; let vrfCoordinatorV2_5Address, subscriptionId, vrfCoordinatorV2_5Mock; + const FUND_AMOUNT = ethers.utils.parseEther("1"); // 1 Ether, or 1e18 (10^18) Wei - if (chainId == 31337) { - const FUND_AMOUNT = ethers.utils.parseEther("1"); // 1 Ether, or 1e18 (10^18) Wei - + // 31337 is the chainId for localhost + if (developmentChains.includes(network.name)) { // Mock Auto Create VRF V2.5 Subscription /* // Deprecated @@ -37,9 +37,8 @@ module.exports = async ({ getNamedAccounts, deployments }) => { const createSubReceipt = await createSubTx.wait(1); subscriptionId = createSubReceipt.events[0].args.subId; // Keep as BigNumber - const formattedSubscriptionId = ethers.BigNumber.from(subscriptionId); // Fund the subscription - await vrfCoordinatorV2_5MockInstance.fundSubscription(formattedSubscriptionId, FUND_AMOUNT); + await vrfCoordinatorV2_5MockInstance.fundSubscription(subscriptionId, FUND_AMOUNT); } else { vrfCoordinatorV2_5Address = networkConfig[chainId]["vrfCoordinatorV2"]; subscriptionId = networkConfig[chainId]["subscriptionId"]; @@ -74,7 +73,7 @@ module.exports = async ({ getNamedAccounts, deployments }) => { //await vrfCoordinatorV2_5Mock.addConsumer(subscriptionId, raffle.address); */ - vrfCoordinatorV2_5Mock = await get("VRFCoordinatorV2_5Mock"); + vrfCoordinatorV2_5Mock = await deployments.get("VRFCoordinatorV2_5Mock"); vrfCoordinatorV2_5Address = vrfCoordinatorV2_5Mock.address; // Get the contract instance at the retrieved address @@ -83,7 +82,7 @@ module.exports = async ({ getNamedAccounts, deployments }) => { vrfCoordinatorV2_5Address, ); - vrfCoordinatorV2_5MockInstance.addConsumer(subscriptionId, raffle.address); + await vrfCoordinatorV2_5MockInstance.addConsumer(subscriptionId, raffle.address); } // Verify the deployment diff --git a/hardhat.config.js b/hardhat.config.js index 4482842..00eafba 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -92,7 +92,7 @@ module.exports = { namedAccounts: { deployer: { default: 0, // here this will by default take the first account as deployer - 1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another + //1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another }, player: { default: 1, diff --git a/package.json b/package.json index fc278f2..4ce88f9 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "license": "Unlicense", + "_comment1": "@nomiclabs/hardhat-ethers ^2.2.3", + "_comment2": "@nomicfoundation/hardhat-ethers ^3.0.2", "devDependencies": { - "@nomicfoundation/hardhat-ethers": "^3.0.2", - "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13", "@nomiclabs/hardhat-etherscan": "^3.1.8", "@nomiclabs/hardhat-waffle": "^2.0.6", - "chai": "^5.1.1", - "dotenv": "^16.4.5", + "chai": "^4.3.4", "ethereum-waffle": "^4.0.10", "ethers": "^5.7.2", "hardhat": "^2.22.8", @@ -23,7 +23,16 @@ "@chainlink/test-helpers": "^0.0.7-alpha", "@chainlink/token": "^1.1.0", "@openzeppelin/contracts": "^5.0.2", - "babel-eslint": "^10.1.0" + "babel-eslint": "^10.1.0", + "dotenv": "^16.4.5" + }, + "scripts": { + "test": "yarn hardhat test", + "test-staging": "yarn hardhat test --network sepolia", + "lint": "yarn solhint 'contracts/*.sol'", + "lint:fix": "yarn solhint 'contracts/**/*.sol' --fix", + "format": "yarn prettier --write .", + "coverage": "yarn hardhat coverage" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/test/unit/Raffle.test.js b/test/unit/Raffle.test.js new file mode 100644 index 0000000..0af2b55 --- /dev/null +++ b/test/unit/Raffle.test.js @@ -0,0 +1,183 @@ +const { ethers, deployments, getNamedAccounts, network } = require("hardhat"); +const { expect, assert } = require("chai"); +const { + networkConfig, + developmentChains, + VERIFICATION_BLOCK_CONFIRMATIONS, +} = require("../../helper-hardhat-config"); + +const isDevelopment = developmentChains.includes(network.name); + +// Run on development chain +!developmentChains.includes(network.name) + ? describe.skip + : describe("Raffle Unit Test", function () { + let addressMock, addressRaffle, addressDeployer, addressPlayer; + let raffle, vrfCoordinatorV2_5Mock, vrfCoordinatorV2_5Address, player; + let chainId, raffleEntranceFee, interval; + + beforeEach(async () => { + const namedAccounts = await getNamedAccounts(); + addressDeployer = namedAccounts.deployer; + addressPlayer = namedAccounts.player; + await deployments.fixture(["all"]); + + // Implementation for dependencies without "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13", + addressMock = (await deployments.get("VRFCoordinatorV2_5Mock")).address; + addressRaffle = (await deployments.get("Raffle")).address; + vrfCoordinatorV2_5Mock = await ethers.getContractAt( + "VRFCoordinatorV2_5Mock", + addressMock, + ); + raffle = await ethers.getContractAt("Raffle", addressRaffle); + + // raffle = await deployments.get("Raffle"); // Wrong, not the contract instance + // raffle = await ethers.getContract("Raffle"); // With hardaht-ethers dependency override + chainId = network.config.chainId; + + raffleEntranceFee = await raffle.getEntranceFee(); + interval = await raffle.getInterval(); + }); + + describe("constructor", function () { + it("initial correct raffle state", async () => { + const raffleState = await raffle.getRaffleState(); + assert.equal(raffleState, 0, "Incorrect raffle state"); + }); + it("initial correct interval", async () => { + const interval = (await raffle.getInterval()).toString(); + const networkConfigInterval = networkConfig[chainId]["keepersUpdateInterval"]; + assert.equal(interval, networkConfigInterval, "Incorrect interval"); + }); + }); //endof constructor + + describe("enterRaffle", function () { + it("should revert if not enough payment", async () => { + /* + const entranceFee = networkConfig[chainId]["raffleEntranceFee"]; + const entranceFeeString = ethers.utils.formatEther(entranceFee); + const entranceFeeWei = ethers.utils.parseEther(entranceFeeString); + const insufficientPayment = entranceFeeWei.sub(ethers.utils.parseEther("0.01")); // Set payment less than entrance fee + */ + const entranceFee = await raffle.getEntranceFee(); + const entranceFeeString = ethers.utils.formatEther(entranceFee); + console.log(entranceFeeString); // 0.01 + await expect(raffle.enterRaffle()).to.be.revertedWith( + "Raffle__NotEnoughETHEntered", + ); + }); + + it("records player when they enter", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + const contractPlayer = await raffle.getPlayer(0); + assert.equal(contractPlayer, addressDeployer); + }); + + it("should emit the correct event on enter", async () => { + await expect(raffle.enterRaffle({ value: raffleEntranceFee })).to.emit( + raffle, + "RaffleEnter", + ); + }); + + it("not allow entrance when raffle is calculating state", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.send("evm_mine", []); + + // Pretend to be a chainlink upkeep + await raffle.performUpkeep([]); + await expect(raffle.enterRaffle({ value: raffleEntranceFee })).to.be.revertedWith( + "Raffle__RaffleNotOpen", + ); + }); + }); //endof enterRaffle + + describe("checkUpkeep", function () { + it("should return false if nobody send any ETH", async () => { + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.send("evm_mine", []); + // Static call to not spend gas & modify state + const { upkeepNeeded } = await raffle.callStatic.checkUpkeep([]); + assert.isFalse(upkeepNeeded); + }); + + it("should return true if raffle is in calculating state", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.send("evm_mine", []); + + // Pretend to be a chainlink upkeep + const { upkeepNeeded } = await raffle.callStatic.checkUpkeep([]); + assert.isTrue(upkeepNeeded); + }); + + it("should return false if raffle is not open", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.send("evm_mine", []); + await raffle.performUpkeep("0x"); + const raffleState = await raffle.getRaffleState(); + const { upkeepNeeded } = await raffle.callStatic.checkUpkeep([]); + assert.isFalse(upkeepNeeded); + }); + it("returns false if enough time hasn't passed", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() - 5]); // use a higher number here if this test fails + await network.provider.request({ method: "evm_mine", params: [] }); // Alternative to write + const { upkeepNeeded } = await raffle.callStatic.checkUpkeep("0x"); // upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers) + assert(!upkeepNeeded); + }); + it("returns true if enough time has passed, has players, eth, and is open", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.send("evm_mine", []); + const { upkeepNeeded } = await raffle.callStatic.checkUpkeep("0x"); // upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers) + assert(upkeepNeeded); + }); + }); //endof checkUpkeep + + describe("performUpkeep", function () { + it("it can only run if checkupkeep is true", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.send("evm_mine", []); + const tx = await raffle.performUpkeep("0x"); + + assert(tx); + }); + it("it should revert if checkupkeep is false", async () => { + await expect(raffle.performUpkeep("0x")).to.be.revertedWith( + "Raffle__UpkeepNotNeeded", + ); + }); + it("update the raffle sate, emits and event, and call the vrf coordinator", async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.request({ method: "evm_mine", params: [] }); + const txResponse = await raffle.performUpkeep("0x"); + + const txReceipt = await txResponse.wait(1); + const requestId = txReceipt.events[1].args.requestId; // 2nd Event + const currentRaffleState = await raffle.getRaffleState(); + + assert(requestId.toNumber() > 0); + assert(currentRaffleState == 1); + }); + }); + + describe("fulfillRandomWords", function () { + beforeEach(async () => { + await raffle.enterRaffle({ value: raffleEntranceFee }); + await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]); + await network.provider.send("evm_mine", []); + }); + + it("can only be called after performUpKeep", async () => { + const requestId = 0; // Not using + await expect( + vrfCoordinatorV2_5Mock.fulfillRandomWords(0, raffle.address), + ).to.be.revertedWith("InvalidRequest"); + }); + }); + }); diff --git a/yarn.lock b/yarn.lock index 4f80e3a..874dbcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1865,14 +1865,6 @@ "@nomicfoundation/ethereumjs-rlp" "5.0.4" ethereum-cryptography "0.1.3" -"@nomicfoundation/hardhat-ethers@^3.0.2": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.6.tgz#e8ba7f9719de360c03501b85dae4999bb3a7e1c5" - integrity sha512-/xzkFQAaHQhmIAYOQmvHBPwL+NkwLzT9gRZBsgWUYeV+E6pzXsBQsHfRYbAZ3XEYare+T7S+5Tg/1KDJgepSkA== - dependencies: - debug "^4.1.1" - lodash.isequal "^4.5.0" - "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz#3a9c3b20d51360b20affb8f753e756d553d49557" @@ -1921,10 +1913,10 @@ "@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.1.2" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.2" -"@nomiclabs/hardhat-ethers@^2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.2.3.tgz#b41053e360c31a32c2640c9a45ee981a7e603fe0" - integrity sha512-YhzPdzb612X591FOe68q+qXVXGG2ANZRvDo0RRUtimev85rCrAlv/TLMEZw5c+kq9AbzocLTVX/h2jVIFPL9Xg== +"@nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers@^0.3.0-beta.13": + version "0.3.0-beta.13" + resolved "https://registry.yarnpkg.com/hardhat-deploy-ethers/-/hardhat-deploy-ethers-0.3.0-beta.13.tgz#b96086ff768ddf69928984d5eb0a8d78cfca9366" + integrity sha512-PdWVcKB9coqWV1L7JTpfXRCI91Cgwsm7KLmBcwZ8f0COSm1xtABHZTyz3fvF6p42cTnz1VM0QnfDvMFlIRkSNw== "@nomiclabs/hardhat-etherscan@^3.1.8": version "3.1.8" @@ -2971,11 +2963,6 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -assertion-error@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" - integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -4237,17 +4224,6 @@ chai@^4.0.1, chai@^4.2.0, chai@^4.3.4: pathval "^1.1.1" type-detect "^4.1.0" -chai@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.1.tgz#f035d9792a22b481ead1c65908d14bb62ec1c82c" - integrity sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA== - dependencies: - assertion-error "^2.0.1" - check-error "^2.1.1" - deep-eql "^5.0.1" - loupe "^3.1.0" - pathval "^2.0.0" - chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -4317,11 +4293,6 @@ check-error@^1.0.2, check-error@^1.0.3: dependencies: get-func-name "^2.0.2" -check-error@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" - integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== - checkpoint-store@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/checkpoint-store/-/checkpoint-store-1.1.0.tgz#04e4cb516b91433893581e6d4601a78e9552ea06" @@ -4985,11 +4956,6 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" -deep-eql@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" - integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== - deep-equal@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.2.tgz#78a561b7830eef3134c7f6f3a3d6af272a678761" @@ -8628,11 +8594,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -8700,13 +8661,6 @@ loupe@^2.3.6: dependencies: get-func-name "^2.0.1" -loupe@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.1.tgz#71d038d59007d890e3247c5db97c1ec5a92edc54" - integrity sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw== - dependencies: - get-func-name "^2.0.1" - lower-case-first@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" @@ -9895,11 +9849,6 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pathval@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" - integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== - pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9, pbkdf2@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"