Skip to content

Instantly share code, notes, and snippets.

@HickupHH3
Last active October 1, 2022 00:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save HickupHH3/d214cfe6e4d003f428a63ae7d127af2d to your computer and use it in GitHub Desktop.
Save HickupHH3/d214cfe6e4d003f428a63ae7d127af2d to your computer and use it in GitHub Desktop.
VulnerableC4LotteryAns
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.13;
/// @dev interfaces have been minimalised to contain only relevant functions
interface IERC20 {
function transferFrom(address from, address to, uint value) external returns (bool);
}
interface IWeth is IERC20 {
function withdraw(uint) external;
}
/// Lottery contract that requires users to guess a randomly generated number from 0 to 99_999
/// A user can make multiple guesses, with each guess costing 1 eth / weth
/// A user can also withdraw their guess and be refunded half of GUESS_COST
/// First person to call winLottery() gets entire balance
contract C4Lottery {
IWeth weth;
// each guess costs 1 ETH
uint256 constant GUESS_COST = 1 ether;
// list of lottery participants
address[] public participants;
// end block number for guessing period
uint32 public endBlockNum;
// guesses of each participiant
mapping(address => uint16[]) public guesses;
// total deposited funds per guesser
mapping(address => uint256) public funds;
/// @param _weth canonical wrapped native asset contract
/// @param _guessDuration duration for which guess can be submitted, denominated in 100 block chunks
constructor(IWeth _weth, uint8 _guessDuration) {
require(_guessDuration > 0, '0 guess duration');
weth = _weth;
/// @audit: calculation here will likely overflow because 100 literal fits into type uint8
/// since _guessDuration is also of type uint8, an uint8 is allocated to hold the result
/// thus max value for multiplication is of type uint8 = 255
/// note: special thanks to pauliax | thunder for this bug recommendation =)
endBlockNum = uint32(block.number) + _guessDuration * 100;
}
modifier beforeEnd() {
require(uint32(block.number) < endBlockNum, 'guessing period ended');
_;
}
// @audit: Missing receive() function as ETH is expected from WETH contract (only)
// make a guess with ETH
/// @param entry guess number
function guessEth(uint16 entry) public payable beforeEnd {
// @audit: change to == instead, since user will only be credited GUESS_COST
require(msg.value >= GUESS_COST, 'insufficient funds sent');
_guess(entry);
}
// make a guess with WETH
/// @param entry guess number
function guessWeth(uint16 entry) public payable beforeEnd {
weth.transferFrom(msg.sender, address(this), GUESS_COST);
// unwrap WETH
weth.withdraw(GUESS_COST);
_guess(entry);
}
function makeMultipleGuesses(uint16[] calldata entries, bool isEth) public payable beforeEnd {
uint256 i;
uint256 entriesLength = entries.length;
for (i; i < entriesLength; ++i) {
// @audit using msg.value in a loop means multiple guesses can be made with the cost of 1 guess
// @audit (gas op): either make isEth boolean array or select function outside the loop
// bytes4 func = isEth ? this.guessEth.selector : this.guessWeth.selector
// address(this).call(abi.encodeWithSelector(func, entries[i]));
isEth ? guessEth(entries[i]) : guessWeth(entries[i]);
}
}
/// @param index guess index in user's guesses array
// @audit: missing beforeEnd modifier
// users shouldnt be allowed to withdraw after endBlockNum
// because the winning number can be determined by then
function withdrawGuess(uint256 index) public payable {
// @audit: this check can be replaced with a better check
// that the user's parsed index is within the guess length
require(funds[msg.sender] >= GUESS_COST, 'user did not participate');
// remove guess
// @audit: this merely zeroes the value, but isn't removed from the array
// can call this function multiple times with the same index
delete guesses[msg.sender][index];
// process refund
// @audit use .call() instead of .transfer() because it forwards all gas to recipient
// as opposed to .transfer() of only 2300 gas
// @audit: contract description says half of GUESS_COST, not full amount
payable(msg.sender).transfer(GUESS_COST);
// @audit: I wanted this to be vulnerable to reentrancy
// but wardens @0xDjango and @hyh (and @cmichel) pointed out
// that this is a false positive
funds[msg.sender] -= GUESS_COST;
// remove user from participant list if no more guesses remaining
if (guesses[msg.sender].length == 0) {
uint256 i;
uint256 numParticipants = participants.length - 1;
// @audit: might run out of gas if participant list is too long
// in these cases it might make sense to take index as a fn parameter
// @audit: Warden @Dravee pointed out that this will revert for base case i == 0
// but he was unfortunately too shy to mention it in person =p
for (i = numParticipants; i >= 0; i--) {
// @audit: missing bracket means partcipants.pop() gets executed each iteration
if (participants[i] == msg.sender)
participants[i] = participants[numParticipants];
participants.pop();
// @audit: missing a break (in this case, return works too) statement
}
}
}
// whoever made a correct guess and is first to call this function after the guess period
// will be able to withdraw entire contract balance
function winLottery(uint256 index) external {
require(uint32(block.number) >= endBlockNum, 'guessing period has not ended');
// @audit using blockhash is bad for RNG
// only the last 256 block hashes are available
// meaning calling this function after 256 blocks are mined would be
// uint256(keccak256(0x0)) % 100_000 = 2947
// @audit guesses are uint16, max number possible is 65_535
require(guesses[msg.sender][index] == uint256(keccak256(abi.encodePacked(blockhash(endBlockNum)))) % 100_000);
payable(msg.sender).transfer(address(this).balance);
}
function getGuesses(address user) public view returns(uint16[] memory) {
return guesses[user];
}
function _guess(uint16 number) internal {
// if new guesser, add to participant list
if (guesses[msg.sender].length == 0) {
participants.push(msg.sender);
}
funds[msg.sender] += GUESS_COST;
guesses[msg.sender].push(number);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment