add lottery smart contract from updraft

This commit is contained in:
han
2025-01-02 17:13:18 +07:00
parent 5a3d550691
commit a9bf1f8fe6
19 changed files with 1348 additions and 0 deletions

View File

@@ -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 "@solmate/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;
}
}

View File

@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {DeployRaffle} from "../../script/DeployRaffle.s.sol";
import {Raffle} from "../../src/Raffle.sol";
import {HelperConfig} from "../../script/HelperConfig.s.sol";
import {Test, console} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {StdCheats} from "forge-std/StdCheats.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
import {CreateSubscription} from "../../script/Interactions.s.sol";
contract RaffleTest is StdCheats, Test {
/* Errors */
event RequestedRaffleWinner(uint256 indexed requestId);
event RaffleEnter(address indexed player);
event WinnerPicked(address indexed player);
Raffle public raffle;
HelperConfig public helperConfig;
uint256 subscriptionId;
bytes32 gasLane;
uint256 automationUpdateInterval;
uint256 raffleEntranceFee;
uint32 callbackGasLimit;
address vrfCoordinatorV2_5;
address public PLAYER = makeAddr("player");
uint256 public constant STARTING_USER_BALANCE = 10 ether;
function setUp() external {
DeployRaffle deployer = new DeployRaffle();
(raffle, helperConfig) = deployer.run();
vm.deal(PLAYER, STARTING_USER_BALANCE);
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
subscriptionId = config.subscriptionId;
gasLane = config.gasLane;
automationUpdateInterval = config.automationUpdateInterval;
raffleEntranceFee = config.raffleEntranceFee;
callbackGasLimit = config.callbackGasLimit;
vrfCoordinatorV2_5 = config.vrfCoordinatorV2_5;
}
/////////////////////////
// fulfillRandomWords //
////////////////////////
modifier raffleEntered() {
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
_;
}
modifier onlyOnDeployedContracts() {
if (block.chainid == 31337) {
return;
}
try vm.activeFork() returns (uint256) {
return;
} catch {
_;
}
}
function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep() public raffleEntered onlyOnDeployedContracts {
// Arrange
// Act / Assert
vm.expectRevert("nonexistent request");
// vm.mockCall could be used here...
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(0, address(raffle));
vm.expectRevert("nonexistent request");
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(1, address(raffle));
}
function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() public raffleEntered onlyOnDeployedContracts {
address expectedWinner = address(1);
// Arrange
uint256 additionalEntrances = 3;
uint256 startingIndex = 1; // We have starting index be 1 so we can start with address(1) and not address(0)
for (uint256 i = startingIndex; i < startingIndex + additionalEntrances; i++) {
address player = address(uint160(i));
hoax(player, 1 ether); // deal 1 eth to the player
raffle.enterRaffle{value: raffleEntranceFee}();
}
uint256 startingTimeStamp = raffle.getLastTimeStamp();
uint256 startingBalance = expectedWinner.balance;
// Act
vm.recordLogs();
raffle.performUpkeep(""); // emits requestId
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 requestId = entries[1].topics[1]; // get the requestId from the logs
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(uint256(requestId), address(raffle));
// Assert
address recentWinner = raffle.getRecentWinner();
Raffle.RaffleState raffleState = raffle.getRaffleState();
uint256 winnerBalance = recentWinner.balance;
uint256 endingTimeStamp = raffle.getLastTimeStamp();
uint256 prize = raffleEntranceFee * (additionalEntrances + 1);
assert(recentWinner == expectedWinner);
assert(uint256(raffleState) == 0);
assert(winnerBalance == startingBalance + prize);
assert(endingTimeStamp > startingTimeStamp);
}
}

View File

@@ -0,0 +1,280 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {DeployRaffle} from "../../script/DeployRaffle.s.sol";
import {Raffle} from "../../src/Raffle.sol";
import {HelperConfig} from "../../script/HelperConfig.s.sol";
import {Test, console2} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
import {LinkToken} from "../../test/mocks/LinkToken.sol";
import {CodeConstants} from "../../script/HelperConfig.s.sol";
contract RaffleTest is Test, CodeConstants {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
event RequestedRaffleWinner(uint256 indexed requestId);
event RaffleEnter(address indexed player);
event WinnerPicked(address indexed player);
Raffle public raffle;
HelperConfig public helperConfig;
uint256 subscriptionId;
bytes32 gasLane;
uint256 automationUpdateInterval;
uint256 raffleEntranceFee;
uint32 callbackGasLimit;
address vrfCoordinatorV2_5;
LinkToken link;
address public PLAYER = makeAddr("player");
uint256 public constant STARTING_USER_BALANCE = 10 ether;
uint256 public constant LINK_BALANCE = 100 ether;
function setUp() external {
DeployRaffle deployer = new DeployRaffle();
(raffle, helperConfig) = deployer.run();
vm.deal(PLAYER, STARTING_USER_BALANCE);
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
subscriptionId = config.subscriptionId;
gasLane = config.gasLane;
automationUpdateInterval = config.automationUpdateInterval;
raffleEntranceFee = config.raffleEntranceFee;
callbackGasLimit = config.callbackGasLimit;
vrfCoordinatorV2_5 = config.vrfCoordinatorV2_5;
link = LinkToken(config.link);
vm.startPrank(msg.sender);
if (block.chainid == LOCAL_CHAIN_ID) {
link.mint(msg.sender, LINK_BALANCE);
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fundSubscription(subscriptionId, LINK_BALANCE);
}
link.approve(vrfCoordinatorV2_5, LINK_BALANCE);
vm.stopPrank();
}
function testRaffleInitializesInOpenState() public view {
assert(raffle.getRaffleState() == Raffle.RaffleState.OPEN);
}
/*//////////////////////////////////////////////////////////////
ENTER RAFFLE
//////////////////////////////////////////////////////////////*/
function testRaffleRevertsWHenYouDontPayEnought() public {
// Arrange
vm.prank(PLAYER);
// Act / Assert
vm.expectRevert(Raffle.Raffle__SendMoreToEnterRaffle.selector);
raffle.enterRaffle();
}
function testRaffleRecordsPlayerWhenTheyEnter() public {
// Arrange
vm.prank(PLAYER);
// Act
raffle.enterRaffle{value: raffleEntranceFee}();
// 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 RaffleEnter(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
}
function testDontAllowPlayersToEnterWhileRaffleIsCalculating() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
raffle.performUpkeep("");
// Act / Assert
vm.expectRevert(Raffle.Raffle__RaffleNotOpen.selector);
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
}
/*//////////////////////////////////////////////////////////////
CHECKUPKEEP
//////////////////////////////////////////////////////////////*/
function testCheckUpkeepReturnsFalseIfItHasNoBalance() public {
// Arrange
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
// Act
(bool upkeepNeeded,) = raffle.checkUpkeep("");
// Assert
assert(!upkeepNeeded);
}
function testCheckUpkeepReturnsFalseIfRaffleIsntOpen() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 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);
}
// Challenge 1. testCheckUpkeepReturnsFalseIfEnoughTimeHasntPassed
function testCheckUpkeepReturnsFalseIfEnoughTimeHasntPassed() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
// Act
(bool upkeepNeeded,) = raffle.checkUpkeep("");
// Assert
assert(!upkeepNeeded);
}
// Challenge 2. testCheckUpkeepReturnsTrueWhenParametersGood
function testCheckUpkeepReturnsTrueWhenParametersGood() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
// Act
(bool upkeepNeeded,) = raffle.checkUpkeep("");
// Assert
assert(upkeepNeeded);
}
/*//////////////////////////////////////////////////////////////
PERFORMUPKEEP
//////////////////////////////////////////////////////////////*/
function testPerformUpkeepCanOnlyRunIfCheckUpkeepIsTrue() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
// Act / Assert
// It doesnt revert
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: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 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
}
/*//////////////////////////////////////////////////////////////
FULFILLRANDOMWORDS
//////////////////////////////////////////////////////////////*/
modifier raffleEntered() {
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
_;
}
modifier skipFork() {
if (block.chainid != 31337) {
return;
}
_;
}
function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep() public raffleEntered skipFork {
// Arrange
// Act / Assert
vm.expectRevert(VRFCoordinatorV2_5Mock.InvalidRequest.selector);
// vm.mockCall could be used here...
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(0, address(raffle));
vm.expectRevert(VRFCoordinatorV2_5Mock.InvalidRequest.selector);
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(1, address(raffle));
}
function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() public raffleEntered skipFork {
address expectedWinner = address(1);
// Arrange
uint256 additionalEntrances = 3;
uint256 startingIndex = 1; // We have starting index be 1 so we can start with address(1) and not address(0)
for (uint256 i = startingIndex; i < startingIndex + additionalEntrances; i++) {
address player = address(uint160(i));
hoax(player, 1 ether); // deal 1 eth to the player
raffle.enterRaffle{value: raffleEntranceFee}();
}
uint256 startingTimeStamp = raffle.getLastTimeStamp();
uint256 startingBalance = expectedWinner.balance;
// Act
vm.recordLogs();
raffle.performUpkeep(""); // emits requestId
Vm.Log[] memory entries = vm.getRecordedLogs();
console2.logBytes32(entries[1].topics[1]);
bytes32 requestId = entries[1].topics[1]; // get the requestId from the logs
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(uint256(requestId), address(raffle));
// Assert
address recentWinner = raffle.getRecentWinner();
Raffle.RaffleState raffleState = raffle.getRaffleState();
uint256 winnerBalance = recentWinner.balance;
uint256 endingTimeStamp = raffle.getLastTimeStamp();
uint256 prize = raffleEntranceFee * (additionalEntrances + 1);
assert(recentWinner == expectedWinner);
assert(uint256(raffleState) == 0);
assert(winnerBalance == startingBalance + prize);
assert(endingTimeStamp > startingTimeStamp);
}
}