Emergency Governance Attack

close button

Emergency Governance Attack

Download quests in Questplay

View the contracts and any other additional content from your IDE.

By: Goncalo Magalhaes

Windersail's criminal gangs are governed by a council. The council's governance mechanism is powered by a governance token, where each token represents a unit of voting power. Council members, i.e. token holders, can vote on any active proposal. And anybody can create new proposals - including you.

Proposals undergo 3 main phases.

  1. Active: Newly created proposals start off as active. Token holders can vote for the proposal during this phase.

  2. Closed: After the voting period ends, proposals become closed. Token holders can no longer vote.

  3. Passed: If a closed proposal has received the majority of votes, it can be executed (i.e., be passed).

The Council's actions are fueled by its treasury. Crippling it would mean crippling them.

Your Task

Exploit the Council's governance vulnerability and empty the Council's balance of governance tokens.

Contract Code

// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./utils/Decimal.sol"; import "./ProposalHandler.sol"; contract Council is ProposalHandler { using Decimal for Decimal.D256; constructor(address _govToken) ProposalHandler(_govToken) {} function deposit(uint256 amount) external { require(GOV_TOKEN.transferFrom(msg.sender, address(this), amount), "Transfer failed"); accounts[msg.sender].power += amount; } function withdraw() external { AccountState memory account = accounts[msg.sender]; require(account.power > 0, "Nothing to withdraw"); require(account.votedUntil < block.timestamp, "Power locked in vote"); accounts[msg.sender].power = 0; require(GOV_TOKEN.transfer(msg.sender, account.power), "Transfer failed"); } function propose( address target, bytes calldata targetData ) external returns (uint32) { require(target != address(0), "Proposition is empty"); uint32 proposalId = _createProposal(target, targetData); return proposalId; } function vote(uint32 proposalId) external { require(status(proposalId) == ProposalStatus.Active, "Proposal not active"); require(accounts[msg.sender].power > 0, "Voter must have power"); require(!voted[proposalId][msg.sender], "Voter already voted"); voted[proposalId][msg.sender] = true; proposals[proposalId].power += accounts[msg.sender].power; uint256 newLock = proposals[proposalId].timestamp + VOTING_PERIOD; if (newLock > accounts[msg.sender].votedUntil) { accounts[msg.sender].votedUntil = newLock; } } function execute(uint32 proposalId) external { require(status(proposalId) == ProposalStatus.Closed, "Proposal not closed"); require(hasMajority(proposalId), "Must have majority"); _execute(proposalId); } function emergencyExecute(uint32 proposalId) external { require(status(proposalId) == ProposalStatus.Active, "Proposal not active"); require(hasSupermajority(proposalId), "Must have supermajority"); require( block.timestamp >= proposals[proposalId].timestamp + EMERGENCY_PERIOD, "Too early for emergency execution" ); _execute(proposalId); } function _execute(uint32 proposalId) private { (bool success, ) = proposals[proposalId] proposals[proposalId].targetData ); require(success, "Execution failed"); proposals[proposalId].executed = true; } }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./utils/Decimal.sol"; contract ProposalHandler { using Decimal for Decimal.D256; uint internal constant VOTING_PERIOD = 10_000 days; uint internal constant EMERGENCY_PERIOD = 1 minutes; ERC20 public immutable GOV_TOKEN; enum ProposalStatus { Active, Closed, Passed } struct AccountState { uint256 votedUntil; uint256 power; } struct Proposal { bool executed; uint128 timestamp; uint256 power; address target; bytes targetData; } uint32 internal proposalIndex; mapping(uint32 => mapping(address => bool)) public voted; mapping(uint32 => Proposal) public proposals; mapping(address => AccountState) public accounts; constructor(address _govToken) { GOV_TOKEN = ERC20(_govToken); } function powerFor(uint32 proposalId) public view returns (uint256) { return proposals[proposalId].power; } function totalPower() public view returns (uint256) { return GOV_TOKEN.totalSupply(); } function proposalVotePercent( uint32 proposalId ) public view returns (Decimal.D256 memory) { return Decimal.ratio(powerFor(proposalId), totalPower()); } function status( uint32 proposalId ) public view returns (ProposalStatus) { Proposal memory proposal = proposals[proposalId]; require(proposal.timestamp > 0, "Proposal doesn't exist"); if (proposal.executed) { return ProposalStatus.Passed; } uint256 endTime = proposal.timestamp + VOTING_PERIOD; if (block.timestamp < endTime) { return ProposalStatus.Active; } else { return ProposalStatus.Closed; } } function _createProposal( address target, bytes memory targetData ) internal returns (uint32) { uint32 proposalId = proposalIndex; proposals[proposalIndex] = Proposal ({ executed: false, timestamp: uint128(block.timestamp), power: 0, target: target, targetData: targetData }); proposalIndex += 1; return proposalId; } function hasMajority( uint32 proposalId ) internal view returns (bool) { return proposalVotePercent(proposalId) .greaterThanOrEqualTo(Decimal.ratio(1, 2)); } function hasSupermajority( uint32 proposalId ) internal view returns (bool) { return proposalVotePercent(proposalId) .greaterThanOrEqualTo(Decimal.ratio(2, 3)); } }

Disrupt the meeting of Windersail's criminal gangs...