RACE #42 Of The Secureum Bootcamp Epoch∞

This is a write-up for RACE-42, Quiz of the Secureum Bootcamp (opens in a new tab) for Ethereum Smart Contract Auditors. It was designed by Kaden (opens in a new tab), an EVM Security Researcher currently specializing in assembly and automated market makers at Spearbit (opens in a new tab) and Cantina (opens in a new tab).

Participants of this quiz had a single attempt to answer 8 questions within the strict time limit of 16 minutes. If you’re reading this in preparation for participating yourself, it’s best to give it a try under the same time limit!

July 14, 2025 by patrickd


Question 1 of 8

MemSafe.sol
pragma solidity ^0.8.0;

contract MemSafe {
    function memSafe0(bytes4 selector, uint256 a, uint256 b, uint256 c) external pure {
        assembly ("memory-safe") {
            let fmp := mload(0x40)

            mstore(0x00, selector)
            mstore(0x04, a)
            mstore(0x24, b)
            mstore(0x44, c)

            let hash := keccak256(0x00, 0x64)

            mstore(0x40, fmp)
        }
    }

    function memSafe1(bytes4 selector, uint256 a, uint256 b) external pure {
        assembly ("memory-safe") {
            mstore(0x00, selector)
            mstore(0x04, a)
            mstore(0x24, b)

            let hash := keccak256(0x00, 0x44)

            mstore(0x24, 0x00)
        }
    }

    function memSafe2(uint256 a, uint256 b) external pure {
        assembly ("memory-safe") {
            mstore(0x00, a)
            mstore(0x20, b)

            let hash := keccak256(0x00, 0x40)
        }
    }

    function memSafe3(uint256 a, uint256 b, uint256 c, uint256 d) external pure {
        assembly ("memory-safe") {
            mstore(0x00, a)
            mstore(0x20, b)
            mstore(0x40, c)
            mstore(0x60, d)

            let hash := keccak256(0x00, 0x80)

            mstore(0x00, hash)
            revert(0x00, 0x20)
        }
    }
}

Which of the above assembly blocks are memory safe?

  • A. memSafe0
  • B. memSafe1
  • C. memSafe2
  • D. memSafe3
Solution

Correct is B, C, D.

You're probably already aware of two things: Solidity makes use of the Memory that the EVM offers, and the Solidity compiler has an integrated optimizer that may make use of said memory. The problem is that inline assembly allows using memory in an incompatible way. Therefore, by default, in the presence of any inline assembly block that contains a memory operation or assigns to Solidity variables in memory, the compiler's memory optimizations are globally disabled.

Annotating inline assembly blocks as "memory-safe" indicates that Solidity's memory model is respected and optimizations will remain active. The Solidity documentation (opens in a new tab) describes that doing so requires the assembly block to only access memory ranges that (1) were already allocated by Solidity, those (2) allocated manually using the free memory pointer, as well as (3) the scratch space.

First OffsetLast OffsetSizeDescription
0x000x3f64 bytesScratch Space, temporary memory used for hashing opcodes.
0x400x5f32 bytesFree Memory Pointer, points to the first unallocated byte in memory. Also represents the currently allocated memory size.
0x600x7f32 bytesThe Zero Slot, should always be the zero-value.

The documentation's Advanced Safe Use of Memory section describes circumstances under which using more than 64 bytes of scratch space from offset 0x00 until 0x7f is permissible: Either when by the end of the assembly block both the free memory pointer and the zero slot are restored. Or when the assembly block terminates and execution could never return to high-level Solidity code in the first place.

A: The memSafe0 function overwrites memory starting at offset 0x00, until 0x63, with the parameter values passed to the function. Finally, it restores the free memory pointer to its original value. However, it does not reset the Zero Slot, of which some bytes have been overwritten, back to the zero value. Therefore, the inline assembly block does not respect Solidity's memory model and cannot be annotated as "memory-safe".

B: The memSafe1 function overwrites memory starting at offset 0x00, until 0x43, with the parameter values passed to the function. This means the left-most 4 bytes of the Free Memory Pointer are overwritten. At the end of the function we see that the memory range from 0x24 to 0x43 is overwritten with the zero-value. Which, in turn, means that the bytes of the Free Memory Pointer, that had been overwritten, are reset to zero. Due to memory expansion costs (opens in a new tab), it's safe to assume that these bytes were zero to begin with.

C: The memSafe2 function only, and exactly, overwrites the Scratch Space, then makes use of the keccak256 opcode, (opens in a new tab) which hashes the Scratch Space and puts the resulting hash on top of the stack. This perfectly stays within the Solidity memory model without any tricks.

D: The memSafe3 function does not respect Solidity's memory layout, but in this case it's permissible as it always reverts and therefore never returns execution back to Solidity within the current EVM call context.


Question 2 of 8

WhichFunc.sol
pragma solidity ^0.8.0;

contract WhichFunc {
    function whichFunc(bytes4 selector, address a, uint256 b) external pure {
        assembly {
            let m := mload(0x40)

            mstore(add(m, 0x18), a)
            mstore(add(m, 0x04), shr(224, selector))
            mstore(add(m, 0x38), b)
            mstore(m, 0x38)

            mstore(0x40, add(m, add(0x38, 0x20)))
        }
    }
}

Which high level built-in function has the same resulting memory as the logic in WhichFunc.whichFunc when provided with the same parameters?

  • A. abi.encode
  • B. abi.encodePacked
  • C. abi.encodeWithSelector
  • D. abi.decode
Solution

Correct is B.

The given inline assembly block puts the values of the parameters passed to the function into memory starting from the offset returned by loading the current Free Memory Pointer's value at 0x40.

  • let m := mload(0x40): Loads the Free Memory Pointer value to allocate memory for storing the passed parameter values in memory.
  • mstore(add(m, 0x18), a): Parameter a is written into memory from m + 0x18 until m + 0x37, but as it is of address type, only the right-most 20 bytes will contain the expected address. Therefore the address will be located from m + 0x24 until m + 0x37.
  • mstore(add(m, 0x04), shr(224, selector)): The selector parameter, consisting only of 4 bytes aligned to the left-most side, is right-shifted by 224 bits (28 bytes) moving the value all the way to the right. This is then written from m + 0x04 until m + 0x23 – although, the actual value will only occupy m + 0x20 until m + 0x23.
  • mstore(add(m, 0x38), b): Parameter b is written to occupy the full 32 bytes slot from m + 0x38 to m + 0x57 as it is a uint256 type, the value of which may use the entire slot.
  • mstore(m, 0x38): Then, the static value 0x38 (56 in decimal) is written into the full 32 bytes starting at m, until m + 0x1f. 4 bytes + 20 bytes + 32 bytes = 56 bytes – therefore, this is the length of all parameters combined without padding, ie. packed.
  • mstore(0x40, add(m, add(0x38, 0x20))): Finally, the allocation of memory is completed by adding the length of the packed parameter values, as well as the size of the slot storing that length, to the Free Memory Pointer.

Based on this, it appears that this function is most similar to abi.encodePacked (opens in a new tab). You might have assumed that it was encodeWithSelector (opens in a new tab) instead due to the mention of a selector, but this function returns the values ABI-encoded, where they stay padded to each take a full 32 bytes-slot in memory.

You can validate the result in chisel (opens in a new tab) and check the resulting memory with !memdump.


Question 3 of 8

Which of the following has the greatest absolute value?

  • A. type(int160).min
  • B. type(int160).max
  • C. type(int256).min
  • D. type(int256).max
Solution

Correct is C.

In absolute terms, ie. ignoring the sign, Math.abs(type(int256).min) > type(int256).max by 1:

> Math.abs(type(int256).min)
57896044618658097711785492504343953926634992332820282019728792003956564819968
> type(int256).max
57896044618658097711785492504343953926634992332820282019728792003956564819967

You can validate this yourself in chisel (opens in a new tab).


Question 4 of 8

0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00

Which int256 value corresponds to this underlying low-level decimal value?

  • A. 115792089237316195423570985008687907853269984665640564039457584007913129639680{115792089237316195423570985008687907853269984665640564039457584007913129639680}
  • B. 452312848583266388373324160190187140051835877600158453279131187530910662400-{452312848583266388373324160190187140051835877600158453279131187530910662400}
  • C. 256{256}
  • D. 256-{256}
Solution

Correct is D.

You might remember what happens with unsigned integers when subtracting 1 from 0? They wrap back to the highest possible value. This remains true for signed integers, therefore

0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

is equivalent to 1-{1}.

Based on the fact that this is only 255 subtractions by 1 away from the hexdecimal string given in the question, we can assert that its value must be 255-{255}

You can validate this yourself in chisel (opens in a new tab).


Question 5 of 8

WhichRange.sol
pragma solidity ^0.8.0;

contract WhichRange {
    function whichRange(uint32 selector, bytes16 a) external pure {
        assembly {
            mstore(0x00, selector)
            mstore(0x20, a)
        }
    }
}

Which byte range in memory contains the values in their packed representation in WhichRange.whichRange (excluding unused bytes)?

  • A. [0x00,0x40]{\left[\text{0x00},\text{0x40}\right]}
  • B. [0x1c,0x30]{\left[\text{0x1c},\text{0x30}\right]}
  • C. [0x00,0x40){\left[\text{0x00},\text{0x40}\right)}
  • D. [0x1c,0x30){\left[\text{0x1c},\text{0x30}\right)}
Solution

Correct is D.

The bytes of an uintNN type are always right-aligned, while the bytes of a byteNN type are always left-aligned. That means selector is padded on the left, with its actual value starting at 0x1c. The a parameter is padded on the right, with its actual value starting at 0x20 but going only until 0x2f.

According to the interval-notation in math, [{[} and ]{]} include the value next to them in the defined range. While ({(} and ){)} include everything up to, but excluding, the value next to them.


Question 6 of 8

KeccakInput.sol
pragma solidity ^0.8.0;

contract KeccakInput {
    function keccakInput() external pure {
        bytes4 selector = 0xefefefef;
        address addr = 0x3111327EdD38890C3fe564afd96b4C73e8101747;
        uint256 num = 123;

        assembly {
            let m := mload(0x40)

            mstore(0x00, selector)
            mstore(0x20, addr)
            mstore(0x40, num)

            let hash := keccak256(0x1c, 0x44)

            mstore(0x40, m)
        }
    }
}

What is the input of the keccak256 in KeccakInput.keccakInput?

  • A. 0xefefefef0000000000000000000000003111327edd38890c3fe564afd96b4c73e81017470000000000000000000000000000000000000000000000000000000000000123
  • B. 0x000000000000000000000000000000003111327edd38890c3fe564afd96b4c73e81017470000000000000000000000000000000000000000000000000000000000000123
  • C. 0xefefefef0000000000000000000000003111327edd38890c3fe564afd96b4c73e8101747000000000000000000000000000000000000000000000000000000000000007b
  • D. 0x000000000000000000000000000000003111327edd38890c3fe564afd96b4c73e8101747000000000000000000000000000000000000000000000000000000000000007b
Solution

Correct is D.

For this question, it's important to notice that, although the memory from 0x00 until 0x5f was written to, the actual range specified as input of the keccak256 opcode is from 0x1c until 0x5f (Note that the second parameter 0x44 is the length). That means that the first 28 bytes are skipped.

Additionally, take into account that selector, by virtue of being of bytes4 type, has its actual value left-aligned, and its zero-padding on the right. This means that its value (0xefefefef) is actually being skipped, and only 4 bytes of the variable's padding are included as the input for the hashing.

With this, we have already eliminated A and C as possible solutions.

The final difference is that one of the hexdecimal strings ends with 123, while the other ends with 7b. Although 123 matches with what the code contains, here it's important to notice that this number was actually specified as decimal within the code. Converting 123{123} into its hexdecimal equivalent results in 0x7b.


Question 7 of 8

FuncSel.sol
pragma solidity ^0.8.0;

contract FuncSel {
    function funcSel(
        string memory str, 
        uint256[] memory b, 
        address addr, 
        uint256 num
    ) external pure {}
}

What is the function selector for FuncSel.funcSel

  • A. “funcSel(string,uint256[],address,uint256)”
  • B. “funcSel(string, uint256[], address, uint256)”
  • C. 0xe833c93d
  • D. 0x37ce562a
Solution

Correct is C.

Although the terms "function selector" and "function signature" are often used interchangeably, here we actually need to differentiate between them properly in order to answer the question.

Fortunately, we don't need to know which is which by heart, instead can find the answer within the previous questions of the quiz: All throughout the previous questions, the variables and parameters called selector referred to a 4-byte value. With this, we can eliminate options A and B.

Note that A is actually the correct function signature for the given code. By keccak hashing it, and disposing of everything but the first 4 bytes of the hash, we obtain the function selector we're looking for. In this case, we end up with the selector given in option C.

There's many ways how you could've come up with this answer. I personally really like using eth-toolbox.com (opens in a new tab) for quick things like this.


Question 8 of 8

WhichCalldata.sol
pragma solidity ^0.8.0;

contract WhichCalldata {
    function a(
        address[3] memory addrs, 
        uint256 val
    ) external pure {}

    function b(
        address addr0,
        address addr1,
        address addr2,
        uint256 val
    ) external pure {}
}

Given the following calldata (initial 4 bytes hidden) used to call the WhichCalldata contract, which function and parameters are being provided?

0xffffffff0000000000000000000000003111327edd38890c3fe564afd96b4c73e8101747000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000000000000000123

  • A. b(0x3111327EdD38890C3fe564afd96b4C73e8101747, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 0x123)
  • B. a([0x3111327EdD38890C3fe564afd96b4C73e8101747, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045], 0x123)
  • C. b(0x3111327EdD38890C3fe564afd96b4C73e8101747, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 123)
  • D. Cannot determine
Solution

Correct is D.

Fixed length arrays of a set length cannot be distinguished from multiple items of the same type and count when ABI-encoded. If this were a dynamic length array we'd instead have a pointer and a slot for the array length, making them distinguishable.

To really understand the difference, I'd recommend looking at the resulting ABI-encoding of various structures containing types such as bytes, bytes32, uint256[1], and uint256[].