Transactions, Messages, and Calls, oh My!— Ethernaut Level 4 Telephone

Calls

Kevan Mordan
DraftKings Engineering
4 min readNov 4, 2022

--

These are invoked locally and are read-only operations that do not consume any Ether nor emit any events. In this context, when something is invoked locally, that means all the information required to complete it exists on a node and does not need to reach out to the network. Because each node has the same copy of state, any node can be called. Additionally, these execute synchronously and return the function values.

Smart contract functions can be annotated with view or pure (previously constant) to indicate that they are read-only and should be invoked with call(). For example, the function getOwner returns the contract owner’s address.

However, just because a function is not annotated with a read-only keyword does not mean it cannot be called. It is commonplace and often recommended to first call any function to test or to “dry run.” This uses the local resources of the node to execute the function and receive return values without changing state and is often used to estimate gas fees. The only limitation is that because it does not reach out to the network and it does not change any state, it will not emit expected events to test.

Transactions

To start let’s review the official Yellow Paper definition:

Transaction: A piece of data, signed by an External Actor. It represents either a Message or a new Autonomous Object. Transactions are recorded into each block of the blockchain.

These can potentially alter the network state and are first verified by the node before being passed onto the network. If any of these initial node validations fail (is it signed?, is it even a valid message?), then it will return an error message. Otherwise, it will return the transaction hash instead of the function data (like in call).

This is because an accepted transaction is placed into the mem-pool that may eventually be mined in a new block, depending on network and gas conditions. It is up to the sender to subscribe to blockchain events or poll the transaction hash to determine its status and review the resultant logs to find out what happened.

Messages

Let’s review the official definition as well:

Message: Data (as a set of bytes) and Value (specified as Ether) that is passed between two Accounts, either through the deterministic operation of an Autonomous Object or the cryptographically secure signature of the Transaction

Messages are equivalent to the data (and ETH) that is sent from one account to another account. As mentioned in the transaction definition, a message can be a transaction (i.e. sending ETH). However, not all messages are transactions and any time data needs to be sent from one account to another, a message is needed.

It is easiest to think of Transactions as the simple state record to the blockchain- all blocks are composed purely of transactions- and messages as the internal communication between accounts needed to successfully execute transactions.

Example Transaction and Messages

For example, let’s say we have three smart contracts: EthSender, ValidateSender, and ValidateDestination.

Simplified Contracts

EthSender has a transfer function that takes two arguments: _value indicating the amount of ETH and _to, indicating the destination address. Prior to transferring, the transfer function calls the other two validator contracts via messages. The contract validates that the transaction sender is approved by calling the validateSender and validates the destination address by calling the contract validateDesination.

Let’s say account 0x0123abc sends a transaction calling the transfer function with 5 ETH to account 0x765ade. The resultant block this transaction was mined in will simply have a log stating the outcome (value x was transferred to address y), not the internal messages sent to the other contracts, even though they were a part of the overall transaction completion.

Message and Transaction Diagram

Who is sending what?

The original transaction sender will always and forever be the tx.origin. If you want to know who initiated the transaction, this is your answer.

The originating account that is sending data to another account will always and forever be the msg.sender. This is important because the message sender to the validateSender and validateDestination contracts will always be the ethSender contract, no matter the transaction sender. If either validate contract is relying on the message sender to validate what should be the transaction origin address, very bad things can happen.

Take extreme care in using message sender for any validation over the initial transaction sender (tx.origin).

The Problem

https://ethernaut.openzeppelin.com/level/0x0b6F6CE4BCfB70525A31454292017F640C10c768

Telephone

There’s not much going on in the problem- there is only one function changeOwner. The function will assign an owner to the new address if the tx.origin does not equal msg.sender. In order to create this mismatch, we simply need to deploy our own smart contract that calls the changeOwner function with our address.

Contract Solution

This function takes the instance address and newOwner address so it can be reused on multiple instances. We simply need to pass the instance address and our address to the takeContractOwnerhsip function and the contract is ours!

The next problem, Ethernaut Level 5, will focus on Solidity types and over/underflow errors.

Simply deploy via remix and pass your relevant parameters!

--

--