Ethernaut Twitter Trivia Notes

Marius
Personal Blog
Published in
11 min readOct 4, 2021

--

Source: Twitter: https://twitter.com/the_ethernaut

I’ve composed some notes and references from the Trivia threads.

Thanks to @the_ethernaut for engaging in challenging conversations on tw.

Updated 11/11/2021: Adding more trivia from 13–19

Trivia 1

How would you call a function on a third party contract B, on behalf of the sender, through your contract A, guaranteeing no reverts?

From a user:

(bool success,) = contractB.delegatecall(callData);

require(success);

(Look up delegatecall, call)

Link to trivia

https://twitter.com/the_ethernaut/status/1442091740886880257

Trivia 2

What’s the main difference between a transparent proxy and a universal proxy?

From a user

I guess transparent proxies have a few selectors on the proxy contract which can only be called by the admin of the contract and not by the users (hence solving issues related to the selector collision, which universal proxies suffer from).

From a user

  1. They’re both solutions to the function selector collision. In UUPS we’ve just moved the process of upgrading into the implementation contract. But in transparent proxy we kept the upgradeTo function inside the proxy and we will check if the admin calls it or not.

2. If the admin calls for upgrade we will let her do the logic, otherwise we will delegatecall the inout into the implementation

(Look up proxy contracts, upgradeable proxies, https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680)

Link to trivia

https://twitter.com/the_ethernaut/status/1442094655324098563

Trivia 3

How could you destroy the implementation of, and effectively brick, a universal proxy?

From ethernaut

You own the implementation.

You upgrade the implementation of the implementation to a contract that self destructs when its upgradeTo function is called.

Bye bye implementation and proxy

From a user

You said “No one owns the implementation”

From ethernaut

I mean you become the owner of the implementation. Those are steps after the assumptions.

From user 2

How do you become the owner? You meant initializing uninitialized contract or using publicly available functions or st else?

From ethernaut

Yes! People dont init the implementation because no one cares about its storage space. However, the selfdestruct can effectively destroy the implementations code, and the proxy is unable to set a new implementation. Blown up admin code.

Link to trivia

https://twitter.com/the_ethernaut/status/1442155812294733832

Trivia 4

What’s the danger of using tx.origin for user authentication in a smart contract?

From a user

‘tx.origin’ is the address of the externally owned account (EOA) that initiated the transaction.

If a contract calls a malicious contract it can be abused, the call is referenced to the EOA instead of the calling contract.

Do not use tx.origin unless you know the consequences.

(Look-up the dangers of using tx.origin plus the amount of hacks happening due to the tx-origin, related: https://www.youtube.com/watch?v=e_2A9bNgW24, https://twitter.com/thormaximalist/status/1420776898003705856?s=21 )

Link to trivia

https://twitter.com/the_ethernaut/status/1442232362503393280

Trivia 5

What kind of proxy would you use to update an indefinite amount of instances with a single implementation upgrade? And how would it work?

From a user (a possible answer)

Minimal Proxy Contract, whose main contract is itself a proxy and on updating its implementation would update all instances of MPC

From a user

Minimal proxies are similar, but their implementation is baked in. It cannot be changed. Normally used to clone stuff. But what you say would work.

From a user

Proxy Factory. Proxy instances query the factory contract for the latest implementation address on each inbound call. Factory owner only needs to change the implementation address in the factory to upgrade all proxies

From ethernaut

Some call them beacon proxies

From a user

Beacon proxy. A proxy contract points to a beacon contract which points to the implementation. A simple implementation upgrade will upgrade all instances pointing at the beacon

Link to trivia

https://twitter.com/the_ethernaut/status/1442323767573680128

Trivia 6

What’s the deal with external vs public? When should you use external? When not? Why is it cheaper than public?

From a user

External: can be called from outside of the contract only.

Public: can be called from inside and outside.

From ethernaut

Yes! But there’s more to it.

External uses incoming params directly from calldata. Public uploads them to memory. You can call external from the contract using this.xxx().

From another user

But be careful the msg.sender will be changed to contract address

From a user

Public functions copy arguments into memory whereas external uses call data

Link to trivia

https://twitter.com/the_ethernaut/status/1442468598799237130

Trivia 7

3rd party contract B with function b() writes to state in ways outside of your control.

How would you simulate a call to b() from your contract A, observe the side effects, and then undo them entirely without reverting the main execution thread?

From a user

make a call to yourself: in that inner call you call b(), observe side effects, and then revert ‘returning’ this data.

With reference:

for an impl reference, this is what what Balancer v2 does under the hood to support the ‘query’ family of functons:

https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/vault/contracts/Swaps.sol

From a user

try catch statements

this allows external call failure handling and won’t revert the main execution if you don’t choose to

From ethernaut

try B.b() {

. // tests

. revert();

} catch {}

From a user

This should revert main execution because reverts (or failed requires) inside the try statement are not caught by catch.

Based on: https://ethereum.stackexchange.com/questions/78562/is-it-possible-to-perform-a-try-catch-in-solidity Has that been changed?

From a user

You are correct

A solution, (although not pretty) would be nested try catch

The following snippet will return false and not revert

Link to trivia

https://twitter.com/the_ethernaut/status/1442638775583186944

Trivia 8

Dynamically sized types in function signatures may be preceded by the keywords “memory”, “storage”, or “calldata”. When is it optimal to use each of them?

From a user

memory: when the variable just has to be stored during a function execution.

calldata: when the variable has to be passed around in function calls.

storage: when it has to be stored on-chain.

memory vs calldata took me the longest to grasp, and I may still not be clear them.

From a user

calldata is purely for external functions

It’s similar to memory in most aspects but is immutable

memory data is mutable

storage types can only be passed within internal functions and are sent as reference to enable writing into them…

From a user

You can have internal functions with dynamic calldata arguments!

Link to trivia

https://twitter.com/the_ethernaut/status/1442986744664711169

Trivia 9

Why do contract sizes decrease so much when you wrap the code of a modifier in an internal function?

From a user

The modifier inserts lines of code for each function it is included in! If it is used 10 times, it will write the same require statement 10 times into the deploy code.

Internal functions will only add the code once

Also security best practice maybe?

It’s a lot easier to find a missing reentrancy. guard than finding some missing internal function calls/improperly implemented ones

From a user

Fixing this (the copy-paste of the modifier code everywhere in the contract) seems like pretty low-hanging fruit for compiler optimization. The overhead of using JUMP is probably low enough that we could just always use it rather than copying the bytecode each time.

From a user

Wouldn’t you want it to be the other way around? Non inlined functions have a run time overhead. At least 20 gas over an inlined one. However if you really want to decrease bytecode, then we can definitely think about a function “outlining” step.

From a user

I guess it depends on the runs. config you set for the optimizer. At some value the gain you get from having a smaller contract is overshadowed by the compounded gas cost of the jump overhead over many tx.

From a user

And there is an open issue about it, which did not seem to get much traction: https://github.com/ethereum/solidity/issues/6584

cc @mrnventuro who made an excellent comment on the issue 👌

Link to trivia

https://twitter.com/the_ethernaut/status/1444084861787062280

Trivia 10

Is there a way to revert with dynamic error messages?

I.e. “Error: Price must be > 1 ETH”, where 1 is a value held in a state variable in your contract.

From a user

Yeah it’s possible. Using the natspec comments together with backticks in the place where the dynamic expression is. Like JavaScript 🌚

https://jeancvllr.medium.com/solidity-tutorial-all-about-comments-bc31c729975a

From a user

Yeah, custom errors. What are your thoughts about them? considering to use it as I think they’ll be match with TheGraph in the future 🧐

Link to trivia

https://twitter.com/the_ethernaut/status/1444305783525609479

Trivia 11

Can you read a private variable of another contract from your contract? If so, how?

Same for a private constant? Or even an immutable variable?

From a user

You can read immutables, since they are patched in the code you just spot where it’s located and copy the value using EXTCODECOPY.

@Agusx1211 wrote about this in this post. Abusing EXTCODECOPY can be more gas efficient than storage sometimes!

https://medium.com/@agusx1211/ethereum-storage-istambul

From a user

wild guess, but since private variables are visible at the blockchain layer — maybe it’s possible to set up an oracle that pulls this data from contract A (offchain call) and feeds it to contract B?

From a user

yup, that is pretty much what this does except it uses storage proofs to make it trustless

https://github.com/Keydonix/uniswap-oracle

I will update the post with Edit notes once we have more trivia questions.

Link to trivia

https://twitter.com/the_ethernaut/status/1444417121933475844

Trivia 12

Bob calls contract A, which delegate calls contract B, which delegate calls contract C, which calls contract D, which delegate calls contract E, which delegate calls contract A.

Who is msg.sender when the execution reaches back contract A?

User 1

1/ Electric light bulb The first thing to know is what a delegate_call does.

It is basically a type of call to another address, which can be a function *call* to a contract address with its function signature and encoded params Lock with ink pen

The difference is that delegate_call shares origin’s storage.

2/ What is the storage?

The storage is the part of the Smart Contract that is actually kept in the blockchain ⛓.

This means that using a delegate_call is letting another contract execute and accessing the current storage.

More info here: https://solidity-by-example.org/delegatecall/

3/ Given this shared storage condition Handshake, the only way afaik to pass other SC addresses across the delegate_call chain is to pass through params, so the compiler doesn’t access other contract’s storage

This means, from A call to B and passing C, D, E, and A addresses, and so on

4/ The idea of passing via params is to use memory instead of storage, which is volatile loaded into the stack during execution. Different from storage.

To solve this, my initial design was to deploy every contract (B, C, D and E) from A, something like this Down pointing backhand index

5/ I’ll skip details of why is useless to try to pass the next address via the constructor and save it in the next SC Face with hand over mouth, like saving d address in C contract for example.

But trust me, you’ll mess with how storage is loaded into memory during execution Man tipping hand

6/ Then, I created the final setSender function to store the msg.sender at the very end, and also, a function to delegate_call to the next one as I said, it looks something like this Down pointing backhand index

7/ Then, repeat for contract B but receiving every address in params and call the next one. I’ll attach an image of how does this look like and I’ll let you figure out how does it look for contract C, D, and E (link is at the end of the thread) Link symbol

8/ Once everything is set, just deploy on Remix, click on the “callB” button and then go for the “sender” button Speech balloon

The result will be the address you’re using to call the contract. In this case, the default one in Remix Thumbs up

9/ The reason for this is that delegate_call doesn’t just share storage, but also msg.sender and msg.value.

This means that although msg.sender used to be the address doing the call, in this scenario, the sender is always kept because every call is delegated, and that’s you Exploding head

10/ I recommend you to use the Debugger, by clicking on the “Debug” button at the transaction log on the console, I’ll leave you some images with the process

It helps hugely by looking step by step how the OPCODES were run, and the current memory on every instruction High-speed train with bullet nose

Link to trivia

https://twitter.com/the_ethernaut/status/1445465680669601800

Trivia 13

You use your smart account (a wallet contract you control) in L1 to deposit tokens in an L1 to L2 bridge. You eagerly wait for the tx to be relayed. It gets relayed. Ok

But holy sheitz!! Your funds are lost. What happened?!

User 1

The contract address is deterministic so if you already used the nonce (number that indicates how many transactions the EOA has sent) in other stuff you cannot deploy the same contract with the same address on the L2

Link to trivia

https://twitter.com/the_ethernaut/status/1445561945252196359

Trivia 14

When you compile a Solidity contract, you get “bytecode” and “deployedBytecode”. They are almost identical. What’s the difference? Where is the difference? And why is there a difference?

Ethernaut

Deploying is actually sending a tx to the zero address with the “bytecode”. The EVM will execute that bytecode, whose first chunk is the constructor stuff, which writes to state, including the contracts code which is “deployedBytecode”

User 2

‘bytecode’ is typically not constrained by the 24kB limit as it is not stored anywhere. Except in factories, which do hold it! So what do you do if your constructor is so large that your factory is not deployable? You put on your robe and wizard hat: https://bit.ly/3FryMuY

Link to trivia

https://twitter.com/the_ethernaut/status/1445819589217042440

Trivia 15

Can you use creation bytecode to bundle a bunch of txs together in a single tx, instead of deploying a contract?

User 1

You definitely can. And after the execution you can selfdestruct the contract to prevent having any bytecode stored on chain. This interactions breaks a lot of nft projects that prevent smart contracts to mint their nfts by checking in sender is a contract

Since we are still in the constructor, no bytecode exists, therefore the assemble call extcodesize(_addr) will return 0 and enable the constructor to buy unlimited nfts if they choose to

Link to trivia

Trivia 16

A contract’s runtime byte code is: 0x363d3d37363df3 What does it do?

@NoahCitron

I think it just immediately returns the calldata? Was kind of thrown by the fact that it stores the calldata at the location of returndatasize (0?).

Ethernaut

Using returndatasize is a cheap way of pushing a zero to the stack.

Link to trivia

Trivia 17

Can you guarantee that your complex smart contract system, which continuously evolves, will have the exact same contract addresses in all evm compatible networks it is deployed into, forever? If so, how?

@apoorvlathey

What if we have a deployer contract created at the same nonce on all networks. Now, this contract would use create2 to deploy new contracts with deterministic addresses across different chains.

Link to trivia

Trivia 18

A Universal proxy moves its upgradeability management code from the proxy to the implementation. This makes them simpler and more gas efficient. However an upgrade could contain damaged upgradability code and “brick” the proxy. How could this be avoided?

Link to trivia

Trivia 19

Smart contract A’s view function a() needs to call a third party contract B’s b() function, which is also supposed to be view. Can it guarantee that it will really be read only too by just calling it, or does it need to take any additional precautions?

@ernestognw

Compiler directly throw error in any combination of calls, the only way is to use <address>.staticcall, and you can use the (bool success) result to stop execution if it fails. Heres’s the example:

Link to trivia

https://twitter.com/the_ethernaut/status/1447341568269045760

--

--

Giving the best you can in everything that you do is the way to succeed!