diff --git a/contracts/Raffle.sol b/contracts/Raffle.sol index d075bea..0dc1f82 100644 --- a/contracts/Raffle.sol +++ b/contracts/Raffle.sol @@ -77,14 +77,14 @@ contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface { /* Function */ constructor( - address vrfCoordinatorV2, + address vrfCoordinatorV2_5, uint256 subscriptionId, bytes32 gasLane, // keyHash uint256 interval, uint256 entranceFee, uint32 callbackGasLimit - ) VRFConsumerBaseV2Plus(vrfCoordinatorV2) { - i_vrfCoordinator = IVRFCoordinatorV2Plus(vrfCoordinatorV2); + ) VRFConsumerBaseV2Plus(vrfCoordinatorV2_5) { + i_vrfCoordinator = IVRFCoordinatorV2Plus(vrfCoordinatorV2_5); i_gasLane = gasLane; i_interval = interval; i_subscriptionId = subscriptionId; diff --git a/foundry.toml b/foundry.toml index 822c4e0..73b9022 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,11 +4,16 @@ out = "out" libs = ["lib"] remappings = [ + "foundry-devops/=lib/foundry-devops/src/", '@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/', '@solmate=lib/solmate/src/', - "@chainlink/=lib/chainlink/contracts/src/v0.8/", - "hardhat/=./contracts/" + "@chainlink/=lib/chainlink/contracts/src/v0.8/" ] +[etherscan] +sepolia = {key = "${ETHERSCAN_API_KEY}"} -[profile.test] +[rpc_endpoints] +sepolia = "${SEPOLIA_RPC_URL}" + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index d25c31f..3ed45b9 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -101,7 +101,7 @@ module.exports = { solidity: { compilers: [ { - version: "0.8.19", + version: "0.8.18", }, ], }, diff --git a/script/DeployRaffle.s.sol b/script/DeployRaffle.s.sol new file mode 100644 index 0000000..a69c8cb --- /dev/null +++ b/script/DeployRaffle.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; +import {Raffle} from "../contracts/Raffle.sol"; +import {AddConsumer, CreateSubscription, FundSubscription} from "./Interactions.s.sol"; + +contract DeployRaffle is Script { + function run() external returns (Raffle, HelperConfig) { + HelperConfig helperConfig = new HelperConfig(); // This comes with our mocks! + AddConsumer addConsumer = new AddConsumer(); + HelperConfig.NetworkConfig memory config = helperConfig.getConfig(); + + if (config.subscriptionId == 0) { + CreateSubscription createSubscription = new CreateSubscription(); + (config.subscriptionId, config.vrfCoordinatorV2_5) = createSubscription + .createSubscription(config.vrfCoordinatorV2_5, config.account); + + FundSubscription fundSubscription = new FundSubscription(); + fundSubscription.fundSubscription( + config.vrfCoordinatorV2_5, + config.subscriptionId, + config.link, + config.account + ); + + helperConfig.setConfig(block.chainid, config); + } + + vm.startBroadcast(config.account); + Raffle raffle = new Raffle( + config.vrfCoordinatorV2_5, + config.subscriptionId, + config.gasLane, + config.automationUpdateInterval, + config.raffleEntranceFee, + config.callbackGasLimit + ); + vm.stopBroadcast(); + + // We already have a broadcast in here + addConsumer.addConsumer( + address(raffle), + config.vrfCoordinatorV2_5, + config.subscriptionId, + config.account + ); + return (raffle, helperConfig); + } +} diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol new file mode 100644 index 0000000..9ff140c --- /dev/null +++ b/script/HelperConfig.s.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {LinkToken} from "../test/mocks/LinkToken.sol"; +import {Script, console2} from "forge-std/Script.sol"; +import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol"; + +abstract contract CodeConstants { + uint96 public MOCK_BASE_FEE = 0.25 ether; + uint96 public MOCK_GAS_PRICE_LINK = 1e9; + // LINK / ETH price + int256 public MOCK_WEI_PER_UINT_LINK = 4e15; + + address public FOUNDRY_DEFAULT_SENDER = 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38; + + uint256 public constant ETH_SEPOLIA_CHAIN_ID = 11155111; + uint256 public constant ETH_MAINNET_CHAIN_ID = 1; + uint256 public constant LOCAL_CHAIN_ID = 31337; +} + +contract HelperConfig is CodeConstants, Script { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + error HelperConfig__InvalidChainId(); + + /*////////////////////////////////////////////////////////////// + TYPES + //////////////////////////////////////////////////////////////*/ + struct NetworkConfig { + uint256 subscriptionId; + bytes32 gasLane; + uint256 automationUpdateInterval; + uint256 raffleEntranceFee; + uint32 callbackGasLimit; + address vrfCoordinatorV2_5; + address link; + address account; + } + + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + // Local network state variables + NetworkConfig public localNetworkConfig; + mapping(uint256 chainId => NetworkConfig) public networkConfigs; + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + constructor() { + networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getSepoliaEthConfig(); + networkConfigs[ETH_MAINNET_CHAIN_ID] = getMainnetEthConfig(); + // Note: We skip doing the local config + } + + function getConfig() public returns (NetworkConfig memory) { + return getConfigByChainId(block.chainid); + } + + function setConfig(uint256 chainId, NetworkConfig memory networkConfig) public { + networkConfigs[chainId] = networkConfig; + } + + function getConfigByChainId(uint256 chainId) public returns (NetworkConfig memory) { + if (networkConfigs[chainId].vrfCoordinatorV2_5 != address(0)) { + return networkConfigs[chainId]; + } else if (chainId == LOCAL_CHAIN_ID) { + return getOrCreateAnvilEthConfig(); + } else { + revert HelperConfig__InvalidChainId(); + } + } + + function getMainnetEthConfig() public pure returns (NetworkConfig memory mainnetNetworkConfig) { + mainnetNetworkConfig = NetworkConfig({ + subscriptionId: 0, // If left as 0, our scripts will create one! + gasLane: 0x9fe0eebf5e446e3c998ec9bb19951541aee00bb90ea201ae456421a2ded86805, + automationUpdateInterval: 30, // 30 seconds + raffleEntranceFee: 0.01 ether, + callbackGasLimit: 500000, // 500,000 gas + vrfCoordinatorV2_5: 0x271682DEB8C4E0901D1a1550aD2e64D568E69909, + link: 0x514910771AF9Ca656af840dff83E8264EcF986CA, + account: 0x643315C9Be056cDEA171F4e7b2222a4ddaB9F88D + }); + } + + function getSepoliaEthConfig() public pure returns (NetworkConfig memory sepoliaNetworkConfig) { + sepoliaNetworkConfig = NetworkConfig({ + subscriptionId: 0, // If left as 0, our scripts will create one! + gasLane: 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae, + automationUpdateInterval: 30, // 30 seconds + raffleEntranceFee: 0.01 ether, + callbackGasLimit: 500000, // 500,000 gas + vrfCoordinatorV2_5: 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B, + link: 0x779877A7B0D9E8603169DdbD7836e478b4624789, + account: 0x643315C9Be056cDEA171F4e7b2222a4ddaB9F88D + }); + } + + function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) { + // Check to see if we set an active network config + if (localNetworkConfig.vrfCoordinatorV2_5 != address(0)) { + return localNetworkConfig; + } + + console2.log(unicode"⚠️ You have deployed a mock conract!"); + console2.log("Make sure this was intentional"); + vm.startBroadcast(); + VRFCoordinatorV2_5Mock vrfCoordinatorV2_5Mock = new VRFCoordinatorV2_5Mock( + MOCK_BASE_FEE, + MOCK_GAS_PRICE_LINK, + MOCK_WEI_PER_UINT_LINK + ); + LinkToken link = new LinkToken(); + uint256 subscriptionId = vrfCoordinatorV2_5Mock.createSubscription(); + vm.stopBroadcast(); + + localNetworkConfig = NetworkConfig({ + subscriptionId: subscriptionId, + gasLane: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c, // doesn't really matter + automationUpdateInterval: 30, // 30 seconds + raffleEntranceFee: 0.01 ether, + callbackGasLimit: 500000, // 500,000 gas + vrfCoordinatorV2_5: address(vrfCoordinatorV2_5Mock), + link: address(link), + account: FOUNDRY_DEFAULT_SENDER + }); + vm.deal(localNetworkConfig.account, 100 ether); + return localNetworkConfig; + } +} diff --git a/script/Interactions.s.sol b/script/Interactions.s.sol new file mode 100644 index 0000000..7bca6c5 --- /dev/null +++ b/script/Interactions.s.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Script, console} from "forge-std/Script.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; +import {Raffle} from "../contracts/Raffle.sol"; +// import {DevOpsTools} from "foundry-devops/src/DevOpsTools.sol"; // Deprecated +import {DevOpsTools} from "foundry-devops/DevOpsTools.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 "./HelperConfig.s.sol"; + +contract CreateSubscription is Script { + function createSubscriptionUsingConfig() public returns (uint256, address) { + HelperConfig helperConfig = new HelperConfig(); + address vrfCoordinatorV2_5 = helperConfig + .getConfigByChainId(block.chainid) + .vrfCoordinatorV2_5; + address account = helperConfig.getConfigByChainId(block.chainid).account; + return createSubscription(vrfCoordinatorV2_5, account); + } + + function createSubscription( + address vrfCoordinatorV2_5, + address account + ) public returns (uint256, address) { + console.log("Creating subscription on chainId: ", block.chainid); + vm.startBroadcast(account); + uint256 subId = VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).createSubscription(); + vm.stopBroadcast(); + console.log("Your subscription Id is: ", subId); + console.log("Please update the subscriptionId in HelperConfig.s.sol"); + return (subId, vrfCoordinatorV2_5); + } + + function run() external returns (uint256, address) { + return createSubscriptionUsingConfig(); + } +} + +contract AddConsumer is Script { + function addConsumer( + address contractToAddToVrf, + address vrfCoordinator, + uint256 subId, + address account + ) public { + console.log("Adding consumer contract: ", contractToAddToVrf); + console.log("Using vrfCoordinator: ", vrfCoordinator); + console.log("On ChainID: ", block.chainid); + vm.startBroadcast(account); + VRFCoordinatorV2_5Mock(vrfCoordinator).addConsumer(subId, contractToAddToVrf); + vm.stopBroadcast(); + } + + function addConsumerUsingConfig(address mostRecentlyDeployed) public { + HelperConfig helperConfig = new HelperConfig(); + uint256 subId = helperConfig.getConfig().subscriptionId; + address vrfCoordinatorV2_5 = helperConfig.getConfig().vrfCoordinatorV2_5; + address account = helperConfig.getConfig().account; + + addConsumer(mostRecentlyDeployed, vrfCoordinatorV2_5, subId, account); + } + + function run() external { + address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment( + "Raffle", + block.chainid + ); + addConsumerUsingConfig(mostRecentlyDeployed); + } +} + +contract FundSubscription is CodeConstants, Script { + uint96 public constant FUND_AMOUNT = 3 ether; + + function fundSubscriptionUsingConfig() public { + HelperConfig helperConfig = new HelperConfig(); + uint256 subId = helperConfig.getConfig().subscriptionId; + address vrfCoordinatorV2_5 = helperConfig.getConfig().vrfCoordinatorV2_5; + address link = helperConfig.getConfig().link; + address account = helperConfig.getConfig().account; + + if (subId == 0) { + CreateSubscription createSub = new CreateSubscription(); + (uint256 updatedSubId, address updatedVRFv2) = createSub.run(); + subId = updatedSubId; + vrfCoordinatorV2_5 = updatedVRFv2; + console.log("New SubId Created! ", subId, "VRF Address: ", vrfCoordinatorV2_5); + } + + fundSubscription(vrfCoordinatorV2_5, subId, link, account); + } + + function fundSubscription( + address vrfCoordinatorV2_5, + uint256 subId, + address link, + address account + ) public { + console.log("Funding subscription: ", subId); + console.log("Using vrfCoordinator: ", vrfCoordinatorV2_5); + console.log("On ChainID: ", block.chainid); + if (block.chainid == LOCAL_CHAIN_ID) { + vm.startBroadcast(account); + VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fundSubscription(subId, FUND_AMOUNT); + vm.stopBroadcast(); + } else { + console.log(LinkToken(link).balanceOf(msg.sender)); + console.log(msg.sender); + console.log(LinkToken(link).balanceOf(address(this))); + console.log(address(this)); + vm.startBroadcast(account); + LinkToken(link).transferAndCall(vrfCoordinatorV2_5, FUND_AMOUNT, abi.encode(subId)); + vm.stopBroadcast(); + } + } + + function run() external { + fundSubscriptionUsingConfig(); + } +} diff --git a/test/mocks/LinkToken.sol b/test/mocks/LinkToken.sol new file mode 100644 index 0000000..4d83801 --- /dev/null +++ b/test/mocks/LinkToken.sol @@ -0,0 +1,59 @@ +// 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; + } +}