Paradigm CTF is an online Web3 focused security competition, running 48 hours from August 20 to August 22. It is organized by PARADIGM, an investment firm focused on supporting the great crypto/Web3 companies.
Definitely, this competition is one of the most extensive and complex in the field. It consists of multiple tasks based on Solidity, Rust and Cairo. Very often the solutions are not obvious and required both complex knowledge of technology and highly specialized knowledge for a specific task.

I solved the task called sourcecode and shared my solution in the article below. This challenge is about EVM assembly. Not just because it is tagged as a ‘PUZZLE’, the goal is to create a specific smart contract using a limited range of assembly opcodes.
Setup.sol
There are two solidity smart contracts. The first one is Setup.sol, it is a standardized contract, the main purpose is to deploy challenge contract and check if it solved with an isSolved()
function.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.16;
import "./Challenge.sol";
contract Setup {
Challenge public challenge;
constructor() {
challenge = new Challenge();
}
function isSolved() public view returns (bool) {
return challenge.solved();
}
}
Challenge.sol
The second one is a Challenge.sol which consists the main logic of the challenge. When the challenge is solved public variable solved
changes its value to true.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.16;
contract Deployer {
constructor(bytes memory code) { assembly { return (add(code, 0x20), mload(code)) } }
}
contract Challenge {
bool public solved = false;
function safe(bytes memory code) private pure returns (bool) {
uint i = 0;
while (i < code.length) {
uint8 op = uint8(code[i]);
if (op >= 0x30 && op <= 0x48) {
return false;
}
if (
op == 0x54 // SLOAD
|| op == 0x55 // SSTORE
|| op == 0xF0 // CREATE
|| op == 0xF1 // CALL
|| op == 0xF2 // CALLCODE
|| op == 0xF4 // DELEGATECALL
|| op == 0xF5 // CREATE2
|| op == 0xFA // STATICCALL
|| op == 0xFF // SELFDESTRUCT
) return false;
if (op >= 0x60 && op < 0x80) i += (op - 0x60) + 1;
i++;
}
return true;
}
function solve(bytes memory code) external {
require(code.length > 0);
require(safe(code), "deploy/code-unsafe");
address target = address(new Deployer(code));
(bool ok, bytes memory result) = target.staticcall("");
require(
ok &&
keccak256(code) == target.codehash &&
keccak256(result) == target.codehash
);
solved = true;
}
}
solve()
function
As I can see, new contract Deployer
is created, after that it called with the target.staticcall("")
. To successfully solve the challenge I should create a specific array of bytes and use it as a parameter of a solve()
function. It must satisfy 3 conditions:
save()
function limits of using specific opcodes- array of bytes should be the same as a deployed
Deployer
contract bytecode - array of bytes should be the same as a result of
Deployer
contract call
Deployer
contract
First of all, let’s go inside a Deployer
contract.
contract Deployer {
constructor(bytes memory code) { assembly { return (add(code, 0x20), mload(code)) } }
}
Notice that the constructor()
code of a smart contract is executed before the deploy and never gets inside a contract code. Calling assembly opcode RETURN inside a constructor()
is pretty unusual.
In this inline assembler block there are 3 opcodes: ADD, MLOAD and RETURN. Go to the list of EVM opcodes and find all of them.



In the memory the size of the buffer stored in a word immediately before its content. The constructor()
is doing nothing but:
- set the offset to 0x20 (32 bytes, size of uint256, also called a word in a stack based EVM)
- read length from the first word of the buffer
- use previously initialized offset and length in a call of RETURN opcode
The main point is the returned array of bytes from a constructor()
will be deployed as a new contract bytecode. To test the theory I deployed a cropped version of Challenge.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.16;
contract Deployer {
constructor(bytes memory code) { assembly { return (add(code, 0x20), mload(code)) } }
}
contract Challenge {
function solve(bytes memory code) external {
address target = address(new Deployer(code));
target.staticcall("");
}
}
Then I called solve()
function passing hex bytes string 0x01 as a function parameter. New contract Deployer
was created with a bytecode as expected:

Problem formulation
As a result I came to the conclusion. I need to create a contract bytecode which will return its bytecode in response to a staticcall()
.
Limitations
The first idea was to write a solidity contract using something like address(this).code
to get self contract bytecode. Unfortunately that would be too easy. The main problem is in a code line below:
require(safe(code), "deploy/code-unsafe");
This function disallows of using a wild range of EVM opcodes. I would to say, most of them excluding stack and memory operations. List of denied opcodes includes a range from 0x30 to 0x48:
if (op >= 0x30 && op <= 0x48) {
return false;
}


Additional 9 opcodes are also denied:
if (
op == 0x54 // SLOAD
|| op == 0x55 // SSTORE
|| op == 0xF0 // CREATE
|| op == 0xF1 // CALL
|| op == 0xF2 // CALLCODE
|| op == 0xF4 // DELEGATECALL
|| op == 0xF5 // CREATE2
|| op == 0xFA // STATICCALL
|| op == 0xFF // SELFDESTRUCT
) return false;
In that way I cannot write a contract in a high level language. The only way is to use an opcodes level.
Solution
On the one hand, disallowing so many opcodes creates a lot of difficulties. On the other hand, the list of allowed opcodes becomes very limited. Now it consists of:
- math opcodes ADD, MUL, SUB etc
- stack manipulation opcodes POP, PUSHx, DUPx, SWAPx
- memory opcodes MLOAD, MSTORE and MSTORE8
- RETURN opcode
- and several useless opcodes like STOP, PC, MSIZE, GAS, LOGx, REVERT
While looking at this list I came to the conclusion of creating a contract that saves self code to a memory using MSTORE/MSTORE8 opcodes and response to a staticcall()
with a RETURN opcode that reads saved array of bytes from a memory.
The contracts will be based on the opcodes below:


The skeleton of the code should look something like this:
- Store code to a memory
- push the entire code
- push memory offset
- mstore to a memory
- Return code from a memory
- push buffer length
- push memory offset
- return
Problem number one. I can push and store to a memory the entire code of the contract, but someone need to do the same with this piece of code too. Solution will be to use an opcode DUPx which allows me to clone a word in the stack.
Problem number two. The opcode MSTORE saves a whole word to a memory, which is a 32-bytes big-endian in EVM. Manipulations with an order of calling MSTORE and MSTORE8 helps here.
As a result, I created the following bytecode:
70806011526000526070600E536023600EF3 PUSH17 806011526000526070600E536023600EF3
80 DUP1
6011 PUSH1 11
52 MSTORE
6000 PUSH1 00
52 MSTORE
6070 PUSH1 70
600E PUSH1 0E
53 MSTORE8
6023 PUSH1 23
600E PUSH1 0E
F3 RETURN
Hex string of the bytecode will be a solution for a challedge.
0x70806011526000526070600E536023600EF3806011526000526070600E536023600EF3
Analysis
Let’s go to a debugger to see what’s going on under the hood. For me the previously deployed cropped version of Challenge.sol is more than enough. I can see, that a Deployer
contract was successfully deployed:


With Remix IDE debugger I went inside a staticcall()
and got a Deployer
contract bytecode.

I wrote to a memory 3 times in total. Let’s see how the memory changed.
Memory dump after the first call of MSTORE:

0x00000000000000000000000000000000
0x00000000000000000000000000000000
0x80600C5260005260706000536014601C
0xf3000000000000000000000000000000
After the second call of MSTORE:

0x00000000000000000000000000000080
0x600C5260005260706000536014601CF3
0x80600C5260005260706000536014601C
0xf3000000000000000000000000000000
After the third and the last call of MSTORE8:

0x00000000000000000000000000007080
0x600C5260005260706000536014601CF3
0x80600C5260005260706000536014601C
0xf3000000000000000000000000000000
Finally, I have a copy of a Deployer
contract bytecode in a memory. RETURN opcode will read it and return back in request of staticcall()
to a contract.
That’s all! I was thrilled to participate, thanks Paradigm for organizing such an outstanding event.