diff --git a/.gitmodules b/.gitmodules index 888d42d..a976d7c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,12 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/chainlink-brownie-contracts"] + path = lib/chainlink-brownie-contracts + url = https://github.com/smartcontractkit/chainlink-brownie-contracts +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate +[submodule "lib/foundry-devops"] + path = lib/foundry-devops + url = https://github.com/Cyfrin/foundry-devops diff --git a/foundry.toml b/foundry.toml index 25b918f..cbda107 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,6 @@ src = "src" out = "out" libs = ["lib"] - +remappings =['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/'] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/script/DeployRaffle.s.sol b/script/DeployRaffle.s.sol new file mode 100644 index 0000000..c674c97 --- /dev/null +++ b/script/DeployRaffle.s.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {Raffle} from "../src/Raffle.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; +import {CreateSubscription, FundSubscription, AddConsumer} from "./Interaction.s.sol"; + +contract DeployRaffle is Script{ + function run() external returns(Raffle, HelperConfig) { + HelperConfig helperConfig = new HelperConfig(); + ( + uint256 entranceFee, + uint256 interval, + address vrfCoordinator, + bytes32 gasLane, + uint64 subscriptionId, + uint32 callbackGasLimit, + address link, + uint256 deployKey + ) = helperConfig.activeNetworkConfig(); // 末尾要加上(),不然只是一个变量 + + if (subscriptionId == 0){ + CreateSubscription createSubscription = new CreateSubscription(); + subscriptionId = createSubscription.createSubscription(vrfCoordinator); + + // FUND + FundSubscription fundSubscription = new FundSubscription(); + fundSubscription.fundSubscription( + vrfCoordinator, + subscriptionId, + link + ); + } + + vm.startBroadcast(); + Raffle raffle = new Raffle( + entranceFee, + interval, + vrfCoordinator, + gasLane, + subscriptionId, + callbackGasLimit + ); + vm.stopBroadcast(); + + AddConsumer addConsumer = new AddConsumer(); + addConsumer.addConsumer(address(raffle), vrfCoordinator, subscriptionId, deployKey); + + return (raffle, helperConfig); + } +} \ No newline at end of file diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol new file mode 100644 index 0000000..3076049 --- /dev/null +++ b/script/HelperConfig.s.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {VRFCoordinatorV2Mock} from "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; +import {LinkToken} from "test/mocks/LinkToken.sol"; + +contract HelperConfig is Script { + + struct NetworkConfig { + uint256 entranceFee; + uint256 interval; + address vrfCoordinator; + bytes32 gasLane; + uint64 subscriptionId; + uint32 callbackGasLimit; + address link; + uint256 deployKey; + } + + address public FOUNDRY_DEFAULT_SENDER = 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38; + uint256 public FOUNDRY_DEFAULT_SENDER_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + NetworkConfig public activeNetworkConfig; + + constructor(){ + if (block.chainid == 11155111){ + activeNetworkConfig = getSepoliaEthConfig(); + } else { + activeNetworkConfig = getOrCreatAnvilEthConfig(); + } + } + + function getSepoliaEthConfig() public view returns(NetworkConfig memory){ + return NetworkConfig({ + entranceFee: 0.01 ether, + interval: 30, + vrfCoordinator: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625, + gasLane: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c, + subscriptionId: 0, + callbackGasLimit: 500000, + link: 0x779877A7B0D9E8603169DdbD7836e478b4624789, + deployKey: vm.envUint("PRIVATE_KEY") // 放置自己钱包的私钥 + }); + } + + function getOrCreatAnvilEthConfig() public returns(NetworkConfig memory){ + + if (activeNetworkConfig.vrfCoordinator != address(0)) { + return activeNetworkConfig; + } + + uint96 baseFee = 0.25 ether; // 0.25 LINK + uint96 gaspriceLink = 1e9; // 1 gwei LINK + vm.startBroadcast(); + VRFCoordinatorV2Mock vrfCoordinatorV2Mock = new VRFCoordinatorV2Mock( + baseFee, + gaspriceLink + ); + LinkToken link = new LinkToken(); + + vm.stopBroadcast(); + + return NetworkConfig({ + entranceFee: 0.01 ether, + interval: 30, + vrfCoordinator: address(vrfCoordinatorV2Mock), + gasLane: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c, + subscriptionId: 0, + callbackGasLimit: 500000, + link: address(link), + deployKey: FOUNDRY_DEFAULT_SENDER_KEY + }); + } +} \ No newline at end of file diff --git a/script/Interaction.s.sol b/script/Interaction.s.sol new file mode 100644 index 0000000..f4402c4 --- /dev/null +++ b/script/Interaction.s.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Script,console} from "forge-std/Script.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; +import {VRFCoordinatorV2Mock} from "lib/chainlink-brownie-contracts/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; +import {LinkToken} from "test/mocks/LinkToken.sol"; +import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol"; + +contract CreateSubscription is Script { + + function createSubscriptionUsingConfig() public returns(uint64) { + HelperConfig helperConfig = new HelperConfig(); + (, , address vrfCoordinator, , , , ,) = helperConfig.activeNetworkConfig(); + return createSubscription(vrfCoordinator); + } + + function createSubscription(address vrfCoordinator) public returns(uint64){ + console.log("ChainId is:", block.chainid); + + vm.startBroadcast(); + uint64 subId = VRFCoordinatorV2Mock(vrfCoordinator).createSubscription(); + vm.stopBroadcast(); + console.log("Your sub Id is:", subId); + console.log("Please update subscriptionId in HelperConfig.s.sol"); + + return subId; + } + + function run() external returns (uint64) { + return createSubscriptionUsingConfig(); + } +} + +contract FundSubscription is Script { + uint96 public constant FUND_AMOUNT = 3 ether; + + function fundSubscriptionUsingConfig() public { + HelperConfig helperConfig = new HelperConfig(); + ( + , + , + address vrfCoordinator, + , + uint64 subId, + , + address link, + uint256 deployKey) = helperConfig.activeNetworkConfig(); + fundSubscription(vrfCoordinator,subId,link); + } + + function fundSubscription( + address vrfCoordinator, + uint64 subId, + address link + ) public { + console.log("Funding subscription:", subId); + console.log("Using vrfCoordinator:", vrfCoordinator); + console.log("On ChainID:", block.chainid); + + if (block.chainid == 31337){ + vm.startBroadcast(); + + VRFCoordinatorV2Mock(vrfCoordinator).fundSubscription( + subId, + FUND_AMOUNT + ); + + vm.stopBroadcast(); + } else { + vm.startBroadcast(); + + LinkToken(link).transferAndCall( + vrfCoordinator, + FUND_AMOUNT, + abi.encode(subId) + ); + + vm.stopBroadcast(); + } + } + + function run() external { + fundSubscriptionUsingConfig(); + } + +} + +contract AddConsumer is Script { + + function addConsumerUsingConfig(address raffle) public { + HelperConfig helperConfig = new HelperConfig(); + ( + , + , + address vrfCoordinator, + , + uint64 subId, + , + , + uint256 deployKey + ) = helperConfig.activeNetworkConfig(); + addConsumer(raffle,vrfCoordinator,subId,deployKey); + } + + function addConsumer( + address raffle, + address vrfCoordinator, + uint64 subId, + uint256 deployKey + ) public { + console.log("Adding consumer contract:", raffle); + console.log("Using vrfCoordinator:", vrfCoordinator); + console.log("On ChainID:", block.chainid); + + vm.startBroadcast(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38); + // vm.startBroadcast(deployKey) + VRFCoordinatorV2Mock(vrfCoordinator).addConsumer(subId, raffle); + vm.stopBroadcast(); + } + + function run() external { + address raffle = DevOpsTools.get_most_recent_deployment("MyContract", block.chainid); + addConsumerUsingConfig(raffle); + } + +} \ No newline at end of file diff --git a/src/Raffle.sol b/src/Raffle.sol new file mode 100644 index 0000000..432a00a --- /dev/null +++ b/src/Raffle.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; +import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; +import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol"; + +/** + * @title a sample Raffle Contract + * @author 0xNov1ce + * @notice Raffle + */ + +contract Raffle is VRFConsumerBaseV2{ + + error Raffle__NotEnoughEthSent(); + error Raffle__TransferFailed(); + error Raffle__RaffleNotOpen(); + error Raffle__UpkeepNotNeeded( + uint256 currentBalance, + uint256 numPlayers, + RaffleState raffleState + ); + + enum RaffleState { + OPEN, + CALCULATING + } + + uint16 private constant REQUEST_CONFIRMATIONS = 3; + uint32 private constant NUM_WORDs = 1; + + uint256 private immutable i_entranceFee; + uint256 private immutable i_interval; + VRFCoordinatorV2Interface private immutable i_vrfCoordinator; + bytes32 private immutable i_gasLane; + uint64 private immutable i_subscriptionId; + uint32 private immutable i_callbackGasLimit; + + + address payable[] private s_players; + uint256 private s_lastTimeStamp; + address private s_recent_Winner; + RaffleState private s_raffleState; + + event EnterRaffle(address indexed player); + event WinnerPicked(address indexed winner); + event RequestedRaffleWinner(uint256 indexed requestId); + + constructor( + uint256 entranceFee, + uint256 interval, + address vrfCoordinator, + bytes32 gasLane, + uint64 subscriptionId, + uint32 callbackGasLimit + )VRFConsumerBaseV2(vrfCoordinator){ + i_entranceFee = entranceFee; + i_interval = interval; + // 接口这里需要传入一个地址再做强制类型转换 + i_vrfCoordinator = VRFCoordinatorV2Interface(vrfCoordinator); + i_gasLane = gasLane; + i_subscriptionId = subscriptionId; + i_callbackGasLimit = callbackGasLimit; + s_lastTimeStamp = block.timestamp; + s_raffleState = RaffleState.OPEN; + } + + function enterRaffle() external payable { + // require(msg.value >= i_entranceFee, "Not enough Eth sent"); + if (msg.value < i_entranceFee) { + revert Raffle__NotEnoughEthSent(); + } + + if (s_raffleState != RaffleState.OPEN){ + revert Raffle__RaffleNotOpen(); + } + + s_players.push(payable(msg.sender)); + + emit EnterRaffle(msg.sender); + + } + + // 调用checkUpkeep的几个条件: + // 1.时间间隔满足要求 + // 2.raffle是开启状态 + // 3.有人参加(合约里有ETH) + // 4.chainlink的订阅开启 + function checkUpkeep( + bytes memory /* checkData */ + ) public view returns (bool upkeepNeeded, bytes memory /* performData */) + { + bool timeHasPassed = (block.timestamp - s_lastTimeStamp >= i_interval); + bool isOpen = (s_raffleState == RaffleState.OPEN); + bool hasBalance = (address(this).balance > 0); + bool hasPlayers = (s_players.length > 0); + upkeepNeeded = (timeHasPassed && isOpen && hasBalance && hasPlayers); + + return (upkeepNeeded,"0x0"); + } + + + function performUpkeep(bytes calldata /* performData */) external { + + (bool upkeepNeeded, ) = checkUpkeep(""); + if (!upkeepNeeded){ + revert Raffle__UpkeepNotNeeded( + address(this).balance, + s_players.length, + s_raffleState + ); + } + + s_raffleState = RaffleState.CALCULATING; + + uint256 requestId = i_vrfCoordinator.requestRandomWords( + i_gasLane, // GAS lane + i_subscriptionId, + REQUEST_CONFIRMATIONS, + i_callbackGasLimit, + NUM_WORDs + ); + emit RequestedRaffleWinner(requestId); + } + + // chainlink接受响应后,会调用下面这个函数进行响应,从而获得随机数 + function fulfillRandomWords( + uint256 /* requestId */, + uint256[] memory randomWords + ) internal override{ + uint256 indexOfWinner = randomWords[0] % s_players.length; + address payable winner = s_players[indexOfWinner]; + s_recent_Winner = winner; + + s_raffleState = RaffleState.OPEN; + // 重置数组 + s_players = new address payable[](0); + s_lastTimeStamp = block.timestamp; + + emit WinnerPicked(winner); + + (bool success, ) = winner.call{value:address(this).balance}(""); + if (!success){ + revert Raffle__TransferFailed(); + } + + } + + + function getEntranceFee() external view returns(uint256){ + return i_entranceFee; + } + + function getRaffleState() public view returns (RaffleState) { + return s_raffleState; + } + + function getPlayer(uint256 index) public view returns (address) { + return s_players[index]; + } + + function getRecentWinner() public view returns (address) { + return s_recent_Winner; + } + + function getNumberOfPlayers() public view returns (uint256) { + return s_players.length; + } + + function getLastTimeStamp() public view returns (uint256) { + return s_lastTimeStamp; + } + + function getInterval() public view returns (uint256) { + return i_interval; + } + +} \ No newline at end of file diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/intergartion/intergration.t.sol b/test/intergartion/intergration.t.sol new file mode 100644 index 0000000..c305d76 --- /dev/null +++ b/test/intergartion/intergration.t.sol @@ -0,0 +1,7 @@ +// TO DO + +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +contract IntergrationTest{} \ No newline at end of file diff --git a/test/mocks/LinkToken.sol b/test/mocks/LinkToken.sol new file mode 100644 index 0000000..346b0e5 --- /dev/null +++ b/test/mocks/LinkToken.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +// @dev This contract has been adapted to fit with foundry +pragma solidity ^0.8.0; + +import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; + +interface ERC677Receiver { + function onTokenTransfer(address _sender, uint256 _value, bytes memory _data) external; +} + +contract LinkToken is ERC20 { + uint256 constant INITIAL_SUPPLY = 1000000000000000000000000; + uint8 constant DECIMALS = 18; + + constructor() ERC20("LinkToken", "LINK", DECIMALS) { + _mint(msg.sender, INITIAL_SUPPLY); + } + + function mint(address to, uint256 value) public { + _mint(to, value); + } + + event Transfer(address indexed from, address indexed to, uint256 value, bytes data); + + /** + * @dev transfer token to a contract address with additional data if the recipient is a contact. + * @param _to The address to transfer to. + * @param _value The amount to be transferred. + * @param _data The extra data to be passed to the receiving contract. + */ + function transferAndCall(address _to, uint256 _value, bytes memory _data) public virtual returns (bool success) { + super.transfer(_to, _value); + // emit Transfer(msg.sender, _to, _value, _data); + emit Transfer(msg.sender, _to, _value, _data); + if (isContract(_to)) { + contractFallback(_to, _value, _data); + } + return true; + } + + // PRIVATE + + function contractFallback(address _to, uint256 _value, bytes memory _data) private { + ERC677Receiver receiver = ERC677Receiver(_to); + receiver.onTokenTransfer(msg.sender, _value, _data); + } + + function isContract(address _addr) private view returns (bool hasCode) { + uint256 length; + assembly { + length := extcodesize(_addr) + } + return length > 0; + } +} \ No newline at end of file diff --git a/test/uint/Interactions.t.sol b/test/uint/Interactions.t.sol new file mode 100644 index 0000000..e69de29 diff --git a/test/uint/RaffleTest.t.sol b/test/uint/RaffleTest.t.sol new file mode 100644 index 0000000..26bf4df --- /dev/null +++ b/test/uint/RaffleTest.t.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Test, console} from "forge-std/Test.sol"; +import {DeployRaffle} from "script/DeployRaffle.s.sol"; +import {Raffle} from "src/Raffle.sol"; +import {HelperConfig} from "script/HelperConfig.s.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {VRFCoordinatorV2Mock} from "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; + +contract RaffleTest is Test { + Raffle raffle; + HelperConfig helperConfig; + + uint256 entranceFee; + uint256 interval; + address vrfCoordinator; + bytes32 gasLane; + uint64 subscriptionId; + uint32 callbackGasLimit; + address link; + + address public PLAYER = makeAddr("player"); + uint256 public constant STARTING_USER_BALANCE = 10 ether; + + event EnterRaffle(address indexed player); + + function setUp() external { + DeployRaffle deployer = new DeployRaffle(); + (raffle, helperConfig)= deployer.run(); + + ( + entranceFee, + interval, + vrfCoordinator, + gasLane, + subscriptionId, + callbackGasLimit, + link, + + ) = helperConfig.activeNetworkConfig(); + + vm.deal(PLAYER,STARTING_USER_BALANCE); + } + + function testRaffleInitializesInOpenState() public view { + assert(raffle.getRaffleState() == Raffle.RaffleState.OPEN); + } + + function testRaffleRevertsWHenYouDontPayEnought() public { + // Arrange + vm.prank(PLAYER); + // Act / Assert + vm.expectRevert(Raffle.Raffle__NotEnoughEthSent.selector); + raffle.enterRaffle(); + } + + function testRaffleRecordsPlayerWhenTheyEnter() public { + // Arrange + vm.prank(PLAYER); + // Act + raffle.enterRaffle{value: entranceFee}(); + // Assert + address playerRecorded = raffle.getPlayer(0); + assert(playerRecorded == PLAYER); + } + + function testEmitsEventOnEntrance() public { + // Arrange + vm.prank(PLAYER); + + // Act / Assert + vm.expectEmit(true, false, false, false, address(raffle)); + emit EnterRaffle(PLAYER); + raffle.enterRaffle{value: entranceFee}(); + } + + function testDontAllowPlayersToEnterWhileRaffleIsCalculating() public { + // Arrange + vm.prank(PLAYER); + raffle.enterRaffle{value: entranceFee}(); + + vm.warp(block.timestamp + interval + 1); // vm.warp可以设置时间 + vm.roll(block.number + 1); // vm.roll可以设置区块 + raffle.performUpkeep(""); + + // Act / Assert + vm.expectRevert(Raffle.Raffle__RaffleNotOpen.selector); + vm.prank(PLAYER); + raffle.enterRaffle{value: entranceFee}(); + + } + + function testCheckUpkeepReturnsFalseIfItHasNoBalance() public { + // Arrange + vm.warp(block.timestamp + interval + 1); + vm.roll(block.number + 1); + + // Act + (bool upkeepNeeded,) = raffle.checkUpkeep(""); + + // Assert + assert(!upkeepNeeded); + } + + function testCheckUpkeepReturnsFalseIfRaffleIsntOpen() public { + // Arrange + vm.prank(PLAYER); + raffle.enterRaffle{value: entranceFee}(); + vm.warp(block.timestamp + interval + 1); + vm.roll(block.number + 1); + raffle.performUpkeep(""); + Raffle.RaffleState raffleState = raffle.getRaffleState(); + // Act + (bool upkeepNeeded,) = raffle.checkUpkeep(""); + // Assert + assert(raffleState == Raffle.RaffleState.CALCULATING); + assert(upkeepNeeded == false); + } + + function testPerformUpkeepCanOnlyRunIfCheckUpkeepIsTrue() public { + // Arrange + vm.prank(PLAYER); + raffle.enterRaffle{value: entranceFee}(); + vm.warp(block.timestamp + interval + 1); + vm.roll(block.number + 1); + + // Act / Assert + raffle.performUpkeep(""); + } + + function testPerformUpkeepRevertsIfCheckUpkeepIsFalse() public { + // Arrange + uint256 currentBalance = 0; + uint256 numPlayers = 0; + Raffle.RaffleState rState = raffle.getRaffleState(); + // Act / Assert + vm.expectRevert( + abi.encodeWithSelector( + Raffle.Raffle__UpkeepNotNeeded.selector, + currentBalance, + numPlayers, + rState + ) + ); + raffle.performUpkeep(""); + } + + function testPerformUpkeepUpdatesRaffleStateAndEmitsRequestId() public { + // Arrange + vm.prank(PLAYER); + raffle.enterRaffle{value: entranceFee}(); + vm.warp(block.timestamp + interval + 1); + vm.roll(block.number + 1); + + // Act + vm.recordLogs(); + raffle.performUpkeep(""); // emits requestId + Vm.Log[] memory entries = vm.getRecordedLogs(); + bytes32 requestId = entries[1].topics[1]; + + // Assert + Raffle.RaffleState raffleState = raffle.getRaffleState(); + // requestId = raffle.getLastRequestId(); + assert(uint256(requestId) > 0); + assert(uint256(raffleState) == 1); // 0 = open, 1 = calculating + } + + modifier raffleEntered() { + vm.prank(PLAYER); + raffle.enterRaffle{value: entranceFee}(); + vm.warp(block.timestamp + interval + 1); + vm.roll(block.number + 1); + _; + } + + modifier skipFork() { + if (block.chainid != 31337) { + return; + } + _; + } + + function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep(uint256 randomRequestId) public raffleEntered skipFork { + // Arrange + // Act / Assert + vm.expectRevert("nonexistent request"); + // vm.mockCall could be used here... + VRFCoordinatorV2Mock(vrfCoordinator).fulfillRandomWords(randomRequestId, address(raffle)); + } + + function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() public raffleEntered skipFork { + address expectedWinner = address(1); + + // Arrange + uint256 additionalEntrances = 5; + uint256 startingIndex = 1; + + for (uint256 i = startingIndex; i < startingIndex + additionalEntrances; i++) { + address player = address(uint160(i)); + hoax(player, STARTING_USER_BALANCE); // deal 1 eth to the player + raffle.enterRaffle{value: entranceFee}(); + } + + uint256 prize = entranceFee * (additionalEntrances + 1); + + // 需要事件来接收requestid + vm.recordLogs(); + raffle.performUpkeep(""); // emits requestId + Vm.Log[] memory entries = vm.getRecordedLogs(); + bytes32 requestId = entries[1].topics[1]; + + uint256 startingTimeStamp = raffle.getLastTimeStamp(); + + // 模拟chainlink的节点触发fulfillRandomWords函数获得随机数 + VRFCoordinatorV2Mock(vrfCoordinator).fulfillRandomWords(uint256(requestId), address(raffle)); + + uint256 endingTimeStamp = raffle.getLastTimeStamp(); + + assert(uint256(raffle.getRaffleState()) == 0); + assert(raffle.getRecentWinner() != address(0)); + assert(raffle.getNumberOfPlayers() == 0); + assert(endingTimeStamp > startingTimeStamp); + + // console.log(raffle.getRecentWinner().balance); + // console.log(prize); + // console.log(STARTING_USER_BALANCE ); + // console.log(entranceFee); + // console.log(prize + STARTING_USER_BALANCE - entranceFee); + assert(raffle.getRecentWinner().balance == prize + STARTING_USER_BALANCE - entranceFee); + } +} \ No newline at end of file