This is a smart contract I built while learning Solidity and Foundry. It's a simple lottery/raffle where people pay an entrance fee, and after some time passes, a winner is picked completely at random and gets the whole pot. No admin picks the winner, no one can cheat the randomness — it's all handled on-chain using Chainlink.
I built this as a way to actually understand how Chainlink VRF (random numbers) and Chainlink Automation (automatic function calls) work together in a real contract, instead of just reading about them.
- Anyone can enter the raffle by calling
enterRaffle()and sending at least the entrance fee. - Every X seconds (set when the contract is deployed), Chainlink Automation checks if it's time to pick a winner.
- If it is time (and there's at least 1 player and some ETH in the pot), Automation calls
performUpkeep(), which asks Chainlink VRF for a random number. - Once Chainlink VRF sends back the random number, the contract picks a winner from the list of players and sends them the entire balance.
- The raffle resets and opens back up for a new round.
Nobody has to manually trigger anything — that's the whole point of using Automation. And the randomness can't be manipulated by miners/validators or the owner, because it comes from Chainlink VRF instead of something like block.timestamp.
- Single winner takes the whole pot. No splitting it up, keeps things simple.
- The entrance fee can be changed by the owner, but only for people entering after the change. If you already entered at the old price, you're not affected by a later fee update.
- There's no minimum number of players. Even if only 1 person has entered when the timer runs out, they can still "win" (basically just getting their own entry fee back, minus gas). It's a bit of a weird edge case but not a bug.
- Solidity ^0.8.19
- Foundry for building, testing, and deploying
- Chainlink VRF v2.5 for verifiable randomness
- Chainlink Automation for the time-based upkeep trigger
src/
Raffle.sol <- the actual contract
script/
DeployRaffle.s.sol <- deploys everything (subscription, funding, the contract itself)
HelperConfig.s.sol <- figures out which network we're on and gives the right config
Interactions.s.sol <- scripts for creating/funding a VRF subscription and adding a consumer
test/
unit/RaffleTest.t.sol <- fast tests using local mocks (Anvil)
staging/RaffleStagingTest.t.sol <- tests that run against a real deployed contract (e.g. Sepolia)
mocks/LinkToken.sol <- fake LINK token, only used for local testing
Clone the repo and install dependencies:
forge installRun the unit tests (uses local Anvil + Chainlink mocks, no real network needed):
forge testDeploy to a local Anvil chain:
anvilthen in another terminal:
forge script script/DeployRaffle.s.sol --rpc-url http://127.0.0.1:8545 --broadcastYou'll need:
- A Sepolia RPC URL (Alchemy/Infura both work)
- A funded testnet wallet
- Some testnet LINK for the VRF subscription
forge script script/DeployRaffle.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast --verifyThe deploy script handles creating and funding the VRF subscription automatically if one doesn't already exist, and registers the contract as a consumer for you.
After deploying, you can run the staging tests against the live contract:
forge test --match-contract RaffleStagingTest --fork-url $SEPOLIA_RPC_URL- Why you should never use
block.timestamporblock.difficultyfor randomness (miners/validators can influence them) - The difference between requesting randomness and receiving it — VRF is a two-step process (request, then callback), not instant
- Checks-Effects-Interactions and why the order of operations in
fulfillRandomWordsactually matters for security, not just style - How Chainlink Automation decides when to call your contract (
checkUpkeep/performUpkeep) - Why local mock testing and "real network" staging testing are two genuinely different things, not just the same tests run twice
- Add a minimum player count option instead of always allowing single-player rounds
- Look into splitting the prize among top N winners instead of a single winner
- Add better logging/events for the frontend to hook into
- Maybe support ERC20 entrance fees instead of just ETH (once I actually learn ERC20 properly)
Built while learning Solidity/Foundry