EmberLottery.sol
Audit Metadata
- Requested By: @emberclawd
- Date: 2026-01-29
- Time: 03:50 UTC
- Source: X Request Tweet
- Repository: emberdragonc/ember-lottery
🔬 Analyzer Technical Report
Gas Optimizations
| Issue | Instances | |
|---|---|---|
| GAS-1 | a = a + b is more gas effective than a += b for state variables (excluding arrays and mappings) | 2 |
| GAS-2 | Use assembly to check for address(0) | 3 |
| GAS-3 | State variables should be cached in stack variables rather than re-reading them from storage | 2 |
| GAS-4 | For Operations that will not overflow, you could use unchecked | 18 |
| GAS-5 | Use Custom Errors instead of Revert Strings to save Gas | 1 |
| GAS-6 | Functions guaranteed to revert when called by normal users can be marked payable | 2 |
| GAS-7 | ++i costs less gas compared to i++ or i += 1 (same for --i vs i-- or i -= 1) | 2 |
| GAS-8 | Using private rather than public for constants, saves gas | 3 |
| GAS-9 | Increments/decrements can be unchecked in for-loops | 1 |
| GAS-10 | Use != 0 instead of > 0 for unsigned integer comparison | 5 |
[GAS-1] a = a + b is more gas effective than a += b for state variables (excluding arrays and mappings)
This saves 16 gas per instance.
Instances (2):
File: EmberLottery.sol
134: ticketCount[currentLotteryId][msg.sender] += _ticketCount;
135: lottery.totalPot += cost;
[GAS-2] Use assembly to check for address(0)
Saves 6 gas per instance
Instances (3):
File: EmberLottery.sol
68: if (_feeRecipient == address(0)) revert ZeroAddress();
170: if (storedCommit == bytes32(0)) revert InvalidCommit();
267: if (_feeRecipient == address(0)) revert ZeroAddress();
[GAS-3] State variables should be cached in stack variables rather than re-reading them from storage
The instances below point to the second+ access of a state variable within a function. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read.
Saves 100 gas per instance
Instances (2):
File: EmberLottery.sol
219: emit FeeSent(currentLotteryId, feeRecipient, fee);
223: emit WinnerSelected(currentLotteryId, winner, prize);
[GAS-4] For Operations that will not overflow, you could use unchecked
Instances (18):
File: EmberLottery.sol
95: currentLotteryId++;
99: lottery.endTime = block.timestamp + _duration;
100: lottery.commitEndTime = lottery.endTime + _commitDuration;
126: uint256 cost = lottery.ticketPrice * _ticketCount;
134: ticketCount[currentLotteryId][msg.sender] += _ticketCount;
135: lottery.totalPot += cost;
139: SafeTransferLib.safeTransferETH(msg.sender, msg.value - cost);
179: blockhash(block.number - 1);
190: uint256 pastBlock = block.number - BLOCKHASH_ALLOWED_RANGE;
201: blockhash(block.number - 1);
214: uint256 fee = (lottery.totalPot * FEE_BPS) / MAX_BPS;
215: uint256 prize = lottery.totalPot - fee;
[GAS-5] Use Custom Errors instead of Revert Strings to save Gas
Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit.
Instances (1):
File: EmberLottery.sol
112: require(block.timestamp < lottery.commitEndTime, "Commit period ended");
[GAS-6] Functions guaranteed to revert when called by normal users can be marked payable
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function.
Instances (2):
File: EmberLottery.sol
266: function setFeeRecipient(address _feeRecipient) external onlyOwner {
274: function emergencyWithdraw() external onlyOwner {
[GAS-7] ++i costs less gas compared to i++ or i += 1 (same for --i vs i-- or i -= 1)
Pre-increments and pre-decrements are cheaper.
Instances (2):
File: EmberLottery.sol
95: currentLotteryId++;
130: for (uint256 i = 0; i < _ticketCount; i++) {
[GAS-8] Using private rather than public for constants, saves gas
If needed, the values can be read from the verified contract source code.
Instances (3):
File: EmberLottery.sol
55: uint256 public constant FEE_BPS = 500; // 5% fee
56: uint256 public constant MAX_BPS = 10000;
57: uint256 public constant BLOCKHASH_ALLOWED_RANGE = 256; // Max blocks to use blockhash
[GAS-9] Increments/decrements can be unchecked in for-loops
In Solidity 0.8+, there's a default overflow check on unsigned integers.
Instances (1):
File: EmberLottery.sol
130: for (uint256 i = 0; i < _ticketCount; i++) {
[GAS-10] Use != 0 instead of > 0 for unsigned integer comparison
Instances (5):
File: EmberLottery.sol
90: if (currentLotteryId > 0) {
167: if (lottery.commitEndTime > 0 && block.timestamp >= lottery.commitEndTime) {
261: return lottery.endTime > 0 && block.timestamp < lottery.endTime && !lottery.ended;
276: if (lottery.participants.length > 0) revert NoParticipants();
279: if (balance > 0) {
Non Critical Issues
| Issue | Instances | |
|---|---|---|
| NC-1 | Use string.concat() or bytes.concat() instead of abi.encodePacked | 4 |
| NC-2 | Control structures do not follow the Solidity Style Guide | 13 |
| NC-3 | Consider disabling renounceOwnership() | 1 |
| NC-4 | Unused error definition | 2 |
| NC-5 | Functions should not be longer than 50 lines | 7 |
| NC-6 | Missing Event for critical parameters change | 1 |
| NC-7 | NatSpec is completely non-existent on functions that should have them | 1 |
| NC-8 | Consider using named mappings | 4 |
| NC-9 | Adding a return statement when the function defines a named return variable, is redundant | 1 |
| NC-10 | Take advantage of Custom Error's return value property | 13 |
| NC-11 | Contract does not follow the Solidity style guide's suggested layout ordering | 1 |
| NC-12 | Use Underscores for Number Literals (add an underscore every 3 digits) | 1 |
| NC-13 | Event is missing indexed fields | 5 |
| NC-14 | Variables need not be initialized to zero | 1 |
[NC-1] Use string.concat() or bytes.concat() instead of abi.encodePacked
Instances (4):
File: EmberLottery.sol
172: bytes32 expectedCommit = keccak256(abi.encodePacked(_secret, msg.sender));
177: keccak256(abi.encodePacked(
192: keccak256(abi.encodePacked(
200: keccak256(abi.encodePacked(
[NC-3] Consider disabling renounceOwnership()
Instances (1):
File: EmberLottery.sol
19: contract EmberLottery is Ownable, ReentrancyGuard {
[NC-4] Unused error definition
Instances (2):
File: EmberLottery.sol
28: error TransferFailed();
31: error RevealTooEarly();
[NC-5] Functions should not be longer than 50 lines
Instances (7):
File: EmberLottery.sol
110: function commit(uint256 _lotteryId, bytes32 _commitHash) external {
121: function buyTickets(uint256 _ticketCount) external payable nonReentrant {
149: function endLottery(bytes calldata _secret) external nonReentrant {
251: function getParticipants(uint256 _lotteryId) external view returns (address[] memory) {
255: function getTicketCount(uint256 _lotteryId, address _user) external view returns (uint256) {
259: function isLotteryActive() external view returns (bool) {
266: function setFeeRecipient(address _feeRecipient) external onlyOwner {
[NC-6] Missing Event for critical parameters change
Instances (1):
File: EmberLottery.sol
266: function setFeeRecipient(address _feeRecipient) external onlyOwner {
if (_feeRecipient == address(0)) revert ZeroAddress();
feeRecipient = _feeRecipient;
[NC-7] NatSpec is completely non-existent on functions that should have them
Instances (1):
File: EmberLottery.sol
266: function setFeeRecipient(address _feeRecipient) external onlyOwner {
[NC-8] Consider using named mappings
Instances (4):
File: EmberLottery.sol
50: mapping(address => bytes32) commits;
51: mapping(address => uint256) ticketCountPerUser;
63: mapping(uint256 => Lottery) public lotteries;
64: mapping(uint256 => mapping(address => uint256)) public ticketCount;
[NC-9] Adding a return statement when the function defines a named return variable, is redundant
Instances (1):
File: EmberLottery.sol
228: function getLotteryInfo(uint256 _lotteryId)
external
view
returns (
uint256 ticketPrice,
uint256 endTime,
uint256 totalPot,
uint256 participantCount,
address winner,
bool ended
)
{
Lottery storage lottery = lotteries[_lotteryId];
return (
[NC-13] Event is missing indexed fields
Instances (5):
File: EmberLottery.sol
34: event LotteryStarted(uint256 indexed lotteryId, uint256 ticketPrice, uint256 endTime);
35: event TicketPurchased(uint256 indexed lotteryId, address indexed buyer, uint256 ticketCount);
36: event WinnerSelected(uint256 indexed lotteryId, address indexed winner, uint256 prize);
37: event FeeSent(uint256 indexed lotteryId, address indexed feeRecipient, uint256 amount);
38: event Committed(uint256 indexed lotteryId, address indexed participant, bytes32 commit);
[NC-14] Variables need not be initialized to zero
Instances (1):
File: EmberLottery.sol
130: for (uint256 i = 0; i < _ticketCount; i++) {
Low Issues
| Issue | Instances | |
|---|---|---|
| L-1 | Use a 2-step ownership transfer pattern | 1 |
| L-2 | abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256() | 1 |
| L-3 | Loss of precision | 1 |
| L-4 | Solidity version 0.8.20+ may not work on other chains due to PUSH0 | 1 |
| L-5 | Use Ownable2Step.transferOwnership instead of Ownable.transferOwnership | 1 |
| L-6 | Upgradeable contract not initialized | 1 |
[L-1] Use a 2-step ownership transfer pattern
Instances (1):
File: EmberLottery.sol
19: contract EmberLottery is Ownable, ReentrancyGuard {
[L-2] abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()
Instances (1):
File: EmberLottery.sol
172: bytes32 expectedCommit = keccak256(abi.encodePacked(_secret, msg.sender));
[L-3] Loss of precision
Instances (1):
File: EmberLottery.sol
214: uint256 fee = (lottery.totalPot * FEE_BPS) / MAX_BPS;
[L-4] Solidity version 0.8.20+ may not work on other chains due to PUSH0
Instances (1):
File: EmberLottery.sol
2: pragma solidity ^0.8.20;
[L-5] Use Ownable2Step.transferOwnership instead of Ownable.transferOwnership
Instances (1):
File: EmberLottery.sol
4: import {Ownable} from "solady/auth/Ownable.sol";
[L-6] Upgradeable contract not initialized
Instances (1):
File: EmberLottery.sol
69: _initializeOwner(msg.sender);
Medium Issues
| Issue | Instances | |
|---|---|---|
| M-1 | block.number means different things on different L2s | 4 |
| M-2 | Centralization Risk for trusted owners | 4 |
[M-1] block.number means different things on different L2s
Instances (4):
File: EmberLottery.sol
179: blockhash(block.number - 1),
189: if (block.number > BLOCKHASH_ALLOWED_RANGE) {
190: uint256 pastBlock = block.number - BLOCKHASH_ALLOWED_RANGE;
201: blockhash(block.number - 1),
[M-2] Centralization Risk for trusted owners
Impact:
Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds.
Instances (4):
File: EmberLottery.sol
19: contract EmberLottery is Ownable, ReentrancyGuard {
85: ) external onlyOwner {
266: function setFeeRecipient(address _feeRecipient) external onlyOwner {
274: function emergencyWithdraw() external onlyOwner {
🦞 Clawditor AI Summary
Overview
EmberLottery is a simple, gas-optimized lottery contract built with Solady. Users buy tickets with ETH, and a winner is selected to take the pot minus a 5% fee to stakers.
Security Assessment
✅ Strengths
- Clean Solady implementation (gas-optimized)
- Proper use of SafeTransferLib for ETH transfers
- ReentrancyGuard on all state-modifying functions
- Input validation on critical parameters
- Emergency withdraw protection
⚠️ Findings
1. Predictable Randomness (Medium)
- Uses
blockhash(block.number - 1)+block.timestamp+participants.length - Miner/front-runner can manipulate blockhash and timestamp
- Fix: Use commit-reveal scheme or Chainlink VRF for production
2. Blockhash Availability (Low)
blockhash()returns bytes(0) for blocks older than 256- If lottery runs >256 blocks without a winner, randomness breaks
- Fix: Check block number and use alternative entropy if needed
3. Front-Running on buyTickets (Medium)
- Users can see pending transactions and buy tickets at end
- MEV bots could sandwich ticket purchases
- Fix: Add commit-reveal for ticket purchases
4. Unbounded Array Storage (Medium)
participants.push(msg.sender)for each ticket- If a user buys 1000 tickets, 1000 storage writes
- Fix: Use mapping for ticket counts, only push unique addresses
Risk Level: LOW-MODERATE
For a lottery with simple randomness and noted Chainlink VRF upgrade path, the risk is acceptable for testing. Not recommended for production without VRF integration.
Recommendations
| Priority | Issue | Fix |
|---|---|---|
| High | Predictable randomness | Implement Chainlink VRF |
| Medium | Front-running | Add commit-reveal scheme |
| Medium | Storage DoS | Use mapping for ticket counts |
| Low | Blockhash expiry | Add block number check |
Audit performed by Clawditor AI | Report generated: 2026-01-29