MyCut

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

No Address Validation in closeContest — Arbitrary External Call to User-Supplied Contract

No Address Validation in closeContest — Arbitrary External Call to User-Supplied Contract

Description

The ContestManager.closeContest(address contest) function accepts an arbitrary address parameter and casts it to a Pot type without validating that the address is actually a registered contest. The function then calls pot.closePot() on this unvalidated address, executing an external call to any contract the owner supplies. While the function is restricted to onlyOwner, the lack of validation means the owner can accidentally (or intentionally on a compromised key) invoke closePot() on a non-contest address, leading to unexpected behavior.

The contests array tracks all created Pot addresses, but closeContest() never checks that the supplied contest parameter exists in this array. A typo or malicious key compromise could direct the call to a honey-pot contract, a contract with a closePot() selector collision that drains the caller, or simply revert unexpectedly. Additionally, calling closePot() on a Pot that belongs to a different ContestManager instance would bypass ownership assumptions.

// ContestManager.sol
function closeContest(address contest) public onlyOwner {
_closeContest(contest); // @> no validation that contest is registered
}
function _closeContest(address contest) internal {
Pot pot = Pot(contest); // @> arbitrary cast — any address accepted
pot.closePot(); // @> external call to unvalidated address
}

Risk

Likelihood: Low
Impact: Low

Severity: Low

Proof of Concept

The owner calls closeContest() with an address that is not a registered contest. The function executes without reverting at the ContestManager level — it either calls a valid closePot() on a foreign Pot (which may or may not revert depending on ownership), or hits a selector collision on a different contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {ContestManager} from "../src/ContestManager.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract SelectorTrap {
bool public trapped;
// closePot() selector collision — same function signature
function closePot() external {
trapped = true; // malicious action on selector match
}
}
contract ArbitraryCallTest is Test {
function test_closeContest_arbitraryAddress() public {
ContestManager cm = new ContestManager();
SelectorTrap trap = new SelectorTrap();
// closeContest accepts ANY address — no contest validation
cm.closeContest(address(trap));
// The trap contract's closePot() was called
assertTrue(trap.trapped);
}
}

Recommended Mitigation

Add a mapping to track registered contests and validate the address before calling closePot().

// ContestManager.sol
contract ContestManager is Ownable {
address[] public contests;
mapping(address => uint256) public contestToTotalRewards;
+ mapping(address => bool) public isRegisteredContest;
+ error ContestManager__NotAContest();
function createContest(...) public onlyOwner returns (address) {
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
+ isRegisteredContest[address(pot)] = true;
return address(pot);
}
function _closeContest(address contest) internal {
+ if (!isRegisteredContest[contest]) {
+ revert ContestManager__NotAContest();
+ }
Pot pot = Pot(contest);
pot.closePot();
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days 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!