MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Pot ownership assigned to EOA instead of ContestManager — closePot is permanently DoS'd through the contract layer

Root + Impact

Description

  • ContestManager.createContest() deploys a Pot and later calls pot.closePot() via _closeContest(). The closePot function is restricted to the Pot owner, so ContestManager must be the owner for closure to work.

  • Pot uses Ownable(msg.sender) in its contract-level declaration. When ContestManager.createContest() executes new Pot(...), msg.sender inside that constructor call is the EOA that called createContest — not address(ContestManager). The Pot owner becomes the EOA directly, meaning every ContestManager._closeContest() call reverts with OwnableUnauthorizedAccount, permanently breaking contest closure.

// msg.sender here is the EOA caller of createContest, not ContestManager
contract Pot is Ownable(msg.sender) {
// In ContestManager:
function _closeContest(address pot) internal {
// caller is ContestManager, but Pot owner is the EOA — always reverts
Pot(pot).closePot();
}

Risk

Likelihood:

  • Every contest deployed through ContestManager exhibits this bug — it is not conditional on any external factor. The ownership mismatch is baked into the deployment sequence.

  • The existing test testCanCloseContest passes only because it calls pot.closePot() directly as the EOA user, bypassing ContestManager entirely and masking the production failure.

Impact:

  • ContestManager.closeContest() is permanently non-functional for every pot deployed through the system. All contest closure logic — manager cut distribution and claimant bonus distribution — is permanently locked.

  • Users interacting with the system as designed (through ContestManager) can never trigger closure. The only workaround is for the EOA to call pot.closePot() directly, which bypasses the abstraction layer entirely.

Proof of Concept

Place this test in test/ and run forge test --match-test testCloseContestRevertsViaContestManager. The test demonstrates that ContestManager.closeContest() always reverts because Pot.closePot() uses onlyOwner and ownership was never transferred to ContestManager at deployment.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {ContestManager} from "src/ContestManager.sol";
import {Pot} from "src/Pot.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
contract H3Test is Test {
MockERC20 token;
ContestManager manager;
address owner = address(0x1);
address player = address(0x2);
function setUp() public {
token = new MockERC20();
vm.prank(owner);
manager = new ContestManager();
}
function testCloseContestRevertsViaContestManager() public {
address[] memory players = new address[](1);
players[0] = player;
uint256[] memory rewards = new uint256[](1);
rewards[0] = 100e18;
token.mint(owner, 100e18);
vm.startPrank(owner);
address potAddr = manager.createContest(players, rewards, token, 100e18);
token.approve(address(manager), 100e18);
manager.fundContest(0);
vm.stopPrank();
vm.warp(block.timestamp + 91 days);
// closeContest through ContestManager reverts — owner of Pot is the EOA, not ContestManager
vm.prank(owner);
vm.expectRevert(); // OwnableUnauthorizedAccount(address(manager))
manager.closeContest(0);
}
}

Recommended Mitigation

Pass address(this) (the ContestManager address) as the owner argument when deploying Pot so ContestManager holds the onlyOwner role and can successfully call closePot().

// In ContestManager.createContest:
- Pot pot = new Pot(players, rewards, token, totalRewards);
+ Pot pot = new Pot(players, rewards, token, totalRewards, address(this));
// In Pot constructor:
constructor(
address[] memory players,
uint256[] memory rewards,
IERC20 token,
uint256 totalRewards,
+ address manager
- ) Ownable(msg.sender) {
+ ) Ownable(manager) {
// ...
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!