Ethernaut Lvl 17 Locked Walkthrough: How to properly use (and abuse) structs in Solidity

This is a in-depth series around Zeppelin team’s smart contract security puzzles. I’ll give you the direct resources and key concepts you’ll need to solve the puzzles 100% on your own.

Nicole Zhu
Sep 17, 2018 · 4 min read

This levels requires you to unlock a registrar by abusing a poorly initiated struct.

Best practices when using Structs

Structs can contain functions and other complex datatypes like mappings and arrays. These arrays and mappings can even contain more structs. However, structs cannot directly contain other structs (unless they are values in mappings or arrays).

Let’s step through what to do and what not to do when working with structs:

How to initialize a struct

struct Funder {
address addr;
uint amount;

struct StructOfStructs {
mapping (uint => Funder) funders;

There are various syntaxes for initializing a struct.

  1. You can directly pass values into the struct object:
... = Funder(msg.sender, msg.value);

2. Or, you can use object notation to pass values into the struct object, for better readability:

... = Funder({addr: msg.sender, amount: msg.value})

Common usage patterns (memory vs storage)

More commonly, you’ll use an array or a mapping to save a collection of structs. For example, let’s create an array of Funders and a mapping of Funders.

An array of Funders:

Funders[] public funders;function ... {
Funder memory f;
f.address = ...;
f.amount = ...;

Important to know: Struct declarations default to storage. You should always use a memory modifier when creating or copying structs. It is not recommended to use structs for any temporary computations inside functions.

A mapping of Funders:

mapping (uint => Funder) funders; function ... {
funders[index] = Funder(...);

Important to know: when you directly save a memory struct into a state variable, the memory struct is automatically forced into storage.

The following are examples of what NOT to do when creating a new struct.

Bad example 1

// Do NOT do this
function badFunction{
Funder f; //this defaults to storage
f.address = ...;
f.amount = ...;
funders.push(f); //this will fail

Bad example 2

// Do NOT do this
function badFunction{
Funder storage f = Funder(...);
// Do NOT do this
function badFunction(Funder _funder){
Funder storage f = _funder;

Notice that function input parameters are also memory, not storage reference pointers.

Detailed Walkthrough

Notice that the contract stores unlocked in its first storage slot. The next item is a bytes32 name so you know unlocked occupies the entire first slot. The bytecode for false is 0x00, so unlocked looks like this in the contract’s storage slot:


Notice that this level commits a big no-no when implementing a struct inside the public register() function:

function register(_name...){
NameRecord newRecord; //storage declaration = _name;
newRecord.mappedAddress = _mappedAddress;

newRecord defaults to storage! And any data saved inside newRecord will overwrite the existing slots 1 and 2 in storage.

Conveniently, unlocked is currently stored in slot 1. Let’s override unlocked by passing a true bool, masquerading as the _name variable, via the public register function.

  1. Convert true into a bytes32 variable:

2. In Remix, invoke register with your bytes32 true and an arbitrary contract address. Remember to add quotes around your values as per Remix requirements.

3. Ignore the Metamask warning message and allocate extra gas.

4. Double check that your 0x01 value has overridden unlocked to be true. In console, check that the following is now true:

await contract.unlocked();

Key Security Takeaways

  • You should not declare a new storage struct in your function, as it will overwrite other globally stored variables.

Learn more about structs


Coinmonks is a non-profit Crypto educational publication.