Built this to get my hands dirty with Solidity and understand how smart contracts actually work on-chain, not just conceptually. A raffle felt like a good first project because it touches a lot of the core stuff (handling ETH, access control, storage patterns, randomness, and security considerations) without being too complex.
It's an on-chain raffle where an operator deploys the contract, seeds a prize pot, and players register + buy tickets for a chance to win. More tickets = higher odds.
The operator deploys the contract with a few parameters: max/min participants, ticket cost, entry fee, and optionally seeds the pot with some ETH. Players register by paying the entry fee, then buy tickets (up to 20 per person). Each ticket purchase pushes the buyer's address into a ticketPool array, so someone with 10 tickets has 10x the chance of someone with 1. Once the operator is ready, they call chooseWinner, which hashes block.prevrandao and block.timestamp to pick a random index from the pool. Winner gets the entire pot.
If nobody buys tickets, the operator gets the pot back. Entry fees and ticket costs all go into the prize pool.
Pull payments / withdrawal pattern. chooseWinner doesn't send ETH directly to the winner. It records their winnings in a mapping, and the winner calls claim() to pull their funds. This is important because if the winner's address is a contract that reverts on receive, a direct transfer would revert the entire chooseWinner call and lock everyone's funds. With pull payments that can't happen, the draw always succeeds and claiming is the winner's responsibility.
Swap-and-pop for array removal. When a player deregisters, I find their index in the players array, overwrite them with the last element, and pop. This avoids shifting every element after the removed one, which would be brutal on gas with a big array. Order doesn't matter here so there's no reason to pay for it.
Checks-effects-interactions. State changes happen before any external calls (ETH transfers). In claim(), winnings[msg.sender] is zeroed out before the .call{value: amount} goes out. If a malicious contract tried to re-enter claim() mid-transfer, their balance would already be 0 and the require would catch it.
Overpayment refund on registration. If someone sends more than costOfEntry, the contract refunds the difference automatically. Ticket purchases require exact payment though, since the user specifies a quantity and should know the total.
require ordering for gas. Cheap checks like isActive come first. If the raffle is closed there's no reason to burn gas reading registration status or ticket counts from storage before reverting.
- Randomness is not production-safe.
block.prevrandaoandblock.timestampare both known to validators before block proposal, so technically a validator could manipulate the outcome. For a real deployment you'd want Chainlink VRF or a commit-reveal scheme. - No events. A real dApp frontend would need
eventemissions for registration, ticket purchases, winner selection, etc. to index on-chain activity. Didn't add them here since this was focused on the contract logic. - No parameter updates after deployment. Ticket cost, entry fee, min/max participants are all set in the constructor and can't be changed. Would need owner-only setter functions or a proxy pattern for upgradability.
- Gas scales with ticket purchases. Each ticket bought pushes to storage in a loop, so buying 20 tickets is 20 storage writes. With the 20-ticket cap and 10 player max this is fine, but it wouldn't scale well to thousands of participants without a different approach.
Uses Foundry. Install it, then:
forge build
forge test -vvvv
forge test --gas-reportThe test suite covers registration (basic, owner exclusion, duplicates, underpayment, overpayment refund), deregistration (basic, locked after ticket purchase, unregistered user), ticket buying (basic, must be registered, 20 ticket cap, wrong payment), full end-to-end flow (winner gets pot, owner collects if no tickets sold), access control on chooseWinner, and gas reports for key operations.
- Solidity ^0.8.13
- Foundry / Forge for testing
