Skip to content

Commit 33e1ae1

Browse files
committed
refactor: factory owns VRF subscription — eliminate manual subId setup
- Introduce ILotteryEvents interface for clean event definitions - Remove subId from factory constructor and deploy script - Factory now creates subscription on deployment (permanent ownership) - No pre-created subscription or ownership transfer needed - Simplifies deployment and removes mainnet friction This aligns with production patterns (PoolTogether, Gelato) — factory owns sub, unlimited lotteries
1 parent 2c6f3eb commit 33e1ae1

File tree

10 files changed

+552
-41
lines changed

10 files changed

+552
-41
lines changed

broadcast/DeployLotteryFactory.s.sol/11155111/run-1765133759257.json

Lines changed: 101 additions & 0 deletions
Large diffs are not rendered by default.

broadcast/DeployLotteryFactory.s.sol/11155111/run-1765133849790.json

Lines changed: 101 additions & 0 deletions
Large diffs are not rendered by default.

broadcast/DeployLotteryFactory.s.sol/11155111/run-1765135172560.json

Lines changed: 101 additions & 0 deletions
Large diffs are not rendered by default.

broadcast/DeployLotteryFactory.s.sol/11155111/run-1765136821985.json

Lines changed: 101 additions & 0 deletions
Large diffs are not rendered by default.

broadcast/DeployLotteryFactory.s.sol/11155111/run-latest.json

Lines changed: 64 additions & 19 deletions
Large diffs are not rendered by default.

foundry.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5-
6-
5+
solc = "0.8.20"
6+
optimizer = true
7+
optimizer_runs = 200
8+
via_ir = false
79

810
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

src/LotteryFactory.sol

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,6 @@ contract LotteryFactory is Ownable {
6565
lotteryAddress = address(newLottery);
6666
allLotteries.push(lotteryAddress);
6767

68-
// Auto-accept ownership of lottery on behalf of caller
69-
// newLottery.acceptOwnership();
70-
7168

7269
// Factory (subscription owner) adds lottery as consumer
7370
vrfCoordinator.addConsumer(vrfSubscriptionId, lotteryAddress);
@@ -78,7 +75,7 @@ contract LotteryFactory is Ownable {
7875

7976
/// @notice Fund the factory-owned subscription with LINK
8077
/// @param amount Amount of LINK to add (in wei)
81-
function fundSubscription(uint256 amount) external {
78+
function fundSubscription(uint256 amount) external onlyOwner{
8279
// 1. Pull LINK from caller → factory
8380
LinkTokenInterface(linkToken).transferFrom(msg.sender, address(this), amount);
8481

@@ -98,4 +95,10 @@ contract LotteryFactory is Ownable {
9895
function getAllLotteries() external view returns (address[] memory) {
9996
return allLotteries;
10097
}
98+
99+
/// @notice Remove a lottery from the VRF subscription to free a consumer slot
100+
/// @param lotteryAddress Address of the finished lottery
101+
function removeConsumer(address lotteryAddress) external onlyOwner {
102+
vrfCoordinator.removeConsumer(vrfSubscriptionId, lotteryAddress);
103+
}
101104
}

src/Lotto.sol

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import {AutomationCompatibleInterface} from "@chainlink/automation/AutomationCom
77
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
88
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
99
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
10-
10+
import {ILotteryEvents} from "./interfaces/ILotteryEvents.sol";
1111

1212
/// @title Gas-Optimized Weighted Lottery with Chainlink VRF v2.5 & Automation
1313
/// @author Security Researcher
1414
/// @notice Fully autonomous lottery using cumulative sum pattern for O(1) entry and weighted randomness
1515
/// @dev Factory deploys this contract — subscription owned by factory — all fees go to owner
16-
contract Lottery is VRFConsumerBaseV2Plus, AutomationCompatibleInterface, ReentrancyGuard {
16+
contract Lottery is ILotteryEvents, VRFConsumerBaseV2Plus, AutomationCompatibleInterface, ReentrancyGuard {
1717
using SafeERC20 for IERC20;
1818

1919
/*//////////////////////////////////////////////////////////////
@@ -62,12 +62,7 @@ contract Lottery is VRFConsumerBaseV2Plus, AutomationCompatibleInterface, Reentr
6262
EVENTS
6363
//////////////////////////////////////////////////////////////*/
6464

65-
event Entered(address indexed player, uint256 tickets, uint256 rangeStart, uint256 rangeEnd);
66-
event WinnerPicked(address indexed winner, uint256 prizeAmount, uint256 winningTicketId);
67-
event AutoWinTriggered(address indexed winner, uint256 prizeAmount);
68-
event FeesDistributed(uint256 amount);
69-
event LotteryClosed(uint256 indexed requestId);
70-
event LotteryStateRecovered(uint256 timestamp);
65+
7166

7267
/*//////////////////////////////////////////////////////////////
7368
CONSTRUCTOR

src/interfaces/ILotteryEvents.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
interface ILotteryEvents {
5+
event Entered(address indexed player, uint256 tickets, uint256 rangeStart, uint256 rangeEnd);
6+
event WinnerPicked(address indexed winner, uint256 prizeAmount, uint256 winningTicketId);
7+
event AutoWinTriggered(address indexed winner, uint256 prizeAmount);
8+
event FeesDistributed(uint256 amount);
9+
event LotteryClosed(uint256 indexed requestId); // Indexed for easier searching
10+
event LotteryStateRecovered(uint256 timestamp);
11+
}

test/Lottery.t.sol

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {VRFCoordinatorV2_5Mock} from "@chainlink/vrf/mocks/VRFCoordinatorV2_5Moc
99
import {MockLinkToken} from "@chainlink/mocks/MockLinkToken.sol";
1010
import {MockUSDC} from "./mock/MockUSDC.sol";
1111
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
12-
13-
contract LotteryTest is Test {
12+
import {ILotteryEvents} from "../src/interfaces/ILotteryEvents.sol";
13+
contract LotteryTest is Test, ILotteryEvents {
1414
LotteryFactory public factory;
1515
Lottery public lottery;
1616
VRFCoordinatorV2_5Mock public vrfMock;
@@ -119,12 +119,14 @@ contract LotteryTest is Test {
119119
uint256 balanceBefore = usdc.balanceOf(player1);
120120

121121
vm.expectEmit(true, true, false, true);
122-
emit Lottery.Entered(
123-
player1,
124-
tickets,
125-
0, // rangeStart (first entry = 0)
126-
9 // rangeEnd (0 + 10)
127-
);
122+
// emit Lottery.Entered(
123+
// player1,
124+
// tickets,
125+
// 0, // rangeStart (first entry = 0)
126+
// 9 // rangeEnd (0 + 10)
127+
// );
128+
129+
emit Entered(player1,tickets,0,9);
128130

129131
vm.prank(player1);
130132
lottery.enter(tickets);
@@ -500,4 +502,53 @@ contract LotteryTest is Test {
500502
vm.prank(player1);
501503
factory.createLottery(1e6, address(usdc), 100, 1 hours, 500_000, 1 hours);
502504
}
505+
506+
function test_21_RemoveConsumerFreesSlotAndAllowsNewLottery() public {
507+
// === Fill up to 99 lotteries (max consumers) ===
508+
// === Mock counts owner(factory) as consumer ===
509+
for (uint i = 0; i < 99; i++) {
510+
vm.prank(owner);
511+
factory.createLottery(
512+
TICKET_PRICE,
513+
address(usdc),
514+
MAX_TICKETS,
515+
DURATION,
516+
500_000,
517+
1 hours
518+
);
519+
}
520+
521+
// === Try to create 100th lottery → should REVERT (no consumer slot) ===
522+
vm.expectRevert(); // Chainlink: "Too many consumers"
523+
vm.prank(owner);
524+
factory.createLottery(
525+
TICKET_PRICE,
526+
address(usdc),
527+
MAX_TICKETS,
528+
DURATION,
529+
500_000,
530+
1 hours
531+
);
532+
533+
// === Get the first lottery address ===
534+
address[] memory Lotteries = factory.getAllLotteries();
535+
address firstLottery = Lotteries[0];
536+
537+
// === Remove it as consumer ===
538+
vm.prank(owner);
539+
factory.removeConsumer(firstLottery);
540+
541+
// === Now create 101th lottery → should PASS ===
542+
vm.prank(owner);
543+
address newLottery = factory.createLottery(
544+
TICKET_PRICE,
545+
address(usdc),
546+
MAX_TICKETS,
547+
DURATION,
548+
500_000,
549+
1 hours
550+
);
551+
552+
assertTrue(newLottery != address(0), "100th lottery created after removal");
553+
}
503554
}

0 commit comments

Comments
 (0)