Christmas Dinner

First Flight #31
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Host can rug participants by draining all funds anytime before Christmas dinner via unrestricted withdraw function

Summary

The ChristmasDinner contract allows the host to withdraw all participant deposits at any time before the Christmas dinner event, potentially stealing user funds.

Vulnerability Details

the withdraw() function has no time restrictions:

function withdraw() external onlyHost {
address _host = getHost();
i_WETH.safeTransfer(_host, i_WETH.balanceOf(address(this)));
i_WBTC.safeTransfer(_host, i_WBTC.balanceOf(address(this)));
i_USDC.safeTransfer(_host, i_USDC.balanceOf(address(this)));
}

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;
import {Test, console} from "forge-std/Test.sol";
import {ChristmasDinner} from "../src/ChristmasDinner.sol";
import {MockERC20} from "./mocks/MockERC20.sol";
contract WithdrawalPoC is Test {
ChristmasDinner public dinner;
MockERC20 public wbtc;
MockERC20 public weth;
MockERC20 public usdc;
address public host = makeAddr("host");
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
function setUp() public {
wbtc = new MockERC20("Wrapped BTC", "WBTC", 8);
weth = new MockERC20("Wrapped ETH", "WETH", 18);
usdc = new MockERC20("USD Coin", "USDC", 6);
vm.prank(host);
dinner = new ChristmasDinner(
address(wbtc),
address(weth),
address(usdc)
);
vm.prank(host);
dinner.setDeadline(7);
// Give Alice 10k USDC, 1 WBTC, 10 WETH
deal(address(usdc), alice, 10_000 * 1e6);
deal(address(wbtc), alice, 1 * 1e8);
deal(address(weth), alice, 10 * 1e18);
vm.startPrank(alice);
usdc.approve(address(dinner), type(uint256).max);
wbtc.approve(address(dinner), type(uint256).max);
weth.approve(address(dinner), type(uint256).max);
dinner.deposit(address(usdc), 10_000 * 1e6); // 10k USDC
dinner.deposit(address(wbtc), 1 * 1e8); // 1 BTC
dinner.deposit(address(weth), 10 * 1e18); // 10 ETH
vm.stopPrank();
// Show initial balances
console.log("=== Initial State ===");
console.log("Contract USDC Balance:", usdc.balanceOf(address(dinner)) / 1e6);
console.log("Contract WBTC Balance:", wbtc.balanceOf(address(dinner)) / 1e8);
console.log("Contract WETH Balance:", weth.balanceOf(address(dinner)) / 1e18);
}
function test_HostCanDrainFundsPrematurely() public {
console.log("\n=== Pre-Withdrawal ===");
console.log("Host USDC Balance:", usdc.balanceOf(host) / 1e6);
console.log("Host WBTC Balance:", wbtc.balanceOf(host) / 1e8);
console.log("Host WETH Balance:", weth.balanceOf(host) / 1e18);
// Host drains funds 3 days before dinner
vm.warp(block.timestamp + 4 days);
vm.prank(host);
dinner.withdraw();
console.log("\n=== Post-Withdrawal ===");
console.log("Host USDC Balance:", usdc.balanceOf(host) / 1e6);
console.log("Host WBTC Balance:", wbtc.balanceOf(host) / 1e8);
console.log("Host WETH Balance:", weth.balanceOf(host) / 1e18);
console.log("\n=== Contract Balances ===");
console.log("Contract USDC Balance:", usdc.balanceOf(address(dinner)) / 1e6);
console.log("Contract WBTC Balance:", wbtc.balanceOf(address(dinner)) / 1e8);
console.log("Contract WETH Balance:", weth.balanceOf(address(dinner)) / 1e18);
}
}

PoC demonstrates that the host can drain all tokens 3 days before the deadline:

forge test --match-test test_HostCanDrainFundsPrematurely -vvv
[⠆] Compiling...
[⠒] Compiling 1 files with Solc 0.8.27
[⠢] Solc 0.8.27 finished in 736.53ms
Compiler run successful!
Ran 1 test for test/WithdrawalPoC.t.sol:WithdrawalPoC
[PASS] test_HostCanDrainFundsPrematurely() (gas: 128934)
Logs:
=== Initial State ===
Contract USDC Balance: 10000
Contract WBTC Balance: 1
Contract WETH Balance: 10
=== Pre-Withdrawal ===
Host USDC Balance: 0
Host WBTC Balance: 0
Host WETH Balance: 0
=== Post-Withdrawal ===
Host USDC Balance: 10000
Host WBTC Balance: 1
Host WETH Balance: 10
=== Contract Balances ===
Contract USDC Balance: 0
Contract WBTC Balance: 0
Contract WETH Balance: 0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.71ms (126.86µs CPU time)
Ran 1 test suite in 4.07ms (1.71ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Loss of all participant deposits (USDC, WBTC, WETH)

Participants unable to refund after withdrawal

Complete loss of Christmas dinner funds

Tools Used

Foundry

Manual code review

Recommendations

Add deadline check:

function withdraw() external onlyHost {
if(block.timestamp <= deadline) {
revert BeforeDeadline();
}

also:

Time-lock for withdrawals

Multi-sig requirement

Participant vote mechanism

Updates

Lead Judging Commences

0xtimefliez Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

withdraw is callable before deadline ends

Appeal created

strapontin Auditor
7 months ago
0xtimefliez Lead Judge
7 months ago
0xtimefliez Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

withdraw is callable before deadline ends

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.