Context

Context is used frequently in Fe smart contracts. It is used to gate access to EVM features for reading and modifyng the blockchain.

Rationale

Smart contracts execute on the Ethereum Virtual Machine (EVM). The EVM exposes features that allow smart contracts to query or change some of the blockchain data, for example emitting logs that are included in transaction receipts, creating contracts, obtaining the current block number and altering the data stored at certain addresses.

To make Fe maximally explicit and as easy as possible to audit, these functions are gated behind a Context object. This is passed as an argument to functions, making it clear whether a function interacts with EVM features from the function signature alone.

For example, the following function looks pure from its signature (i.e. it is not expected to alter any blockchain data) but in reality it does modify the blockchain (by emitting a log).

pub fn looks_pure_but_isnt() {
  // COMPILE ERROR
  block_number()
}

Using Context to control access to EVM functions such as emit solves this problem by requiring an instance of Context to be passed explicitly to the function, making it clear from the function signature that the function executes some blockchain interaction. The function above, rewritten using Context, looks as follows:

pub fn uses_context(ctx: Context) -> u256 {
    return ctx.block_number()
}

The Context object

The Context object gates access to features such as:

  • emitting logs
  • creating contracts
  • transferring ether
  • reading message info
  • reading block info

The Context object needs to be passed as a parameter to the function. The Context object has a defined location in the parameter list. It is the first parameter unless the function also takes self. Context or self appearing at any other position in the parameter list causes a compile time error.

The Context object is automatically injected when a function is called externally but it has to be passed explicitly when the function is called from another Fe function e.g.

// The context object is automatically injected when this is called externally
pub fn multiply_block_number(ctx: Context) -> u256 {
  // but it has to be passed along in this function call
  return retrieves_blocknumber(ctx) * 1000
}

fn retrieves_blocknumber(ctx: Context) -> u256 {
  return ctx.block_number()
}

Context mutability

All functionality that modifies the blockchain such as creating logs or contracts or transferring ether would require a mutable Context reference whereas read-only access such as ctx.block_number() does not need require a mutable reference to the context. To pass a mutable Context object, prepend the object name with mut in the function definition, e.g.:

struct SomeEvent{
}

pub fn mutable(mut ctx: Context) {
    ctx.emit(SomeEvent())
}

ABI conformity

The use of Context enables tighter rules and extra clarity compared wth the existing function categories in the ABI, especially when paired with self. The following table shows how combinations of self, mut self, Context and mut Context map to ABI function types.

CategoryCharacteristicsFe SyntaxABI
PureCan only operate on input arguments and not produce any information besides its return value. Can not take self and therefore has no access to things that would make it impurefoo(val: u256)pure
Read ContractReading information from the contract instance (broad definition includes reading constants from contract code)foo(self)view
Storage WritingWriting to contract storage (own or that of other contracts)foo(mut self)payable or nonpayable
Context ReadingReading contextual information from the blockchain (msg, block etc)foo(ctx: Context)view
Context ModifyingEmitting logs, transferring ether, creating contractsfoo(ctx: mut Context)payable or nonpayable
Read Contract & ContextReading information from the contract instance and Contextfoo(self, ctx:Context)view
Read Contract & write ContextReading information from the contract instance and modify Contextfoo(self, ctx: mut Context)view
Storage Writing & read ContextWriting to contract storage and read from Contextfoo(mut self, ctx: Context)payable or nonpayable
Storage Writing & write ContextWriting to contract storage and Contextfoo(mut self, ctx: mut Context)payable or nonpayable

This means Fe has nine different categories of function that can be derived from the function signatures that map to four different ABI types.

Examples

msg_sender and msg_value

Context includes information about inbound transactions. For example, the following function receives ether and adds the sender's address and the transaction value to a mapping. No blockchain data is being changed, so Context does not need to be mutable.


#![allow(unused)]
fn main() {
// assumes existence of state variable named 'ledger' with type Map<address, u256>
pub fn add_to_ledger(mut self, ctx: Context) {
    self.ledger[ctx.msg_sender()] = ctx.msg_value();
}
}

Transferring ether

Transferring ether modifies the blockchain state, so it requires access to a mutable Context object.

pub fn send_ether(mut ctx: Context, _addr: address, amount: u256) {
    ctx.send_value(to: _addr, wei: amount)
}

create/create2

Creating a contract via create/create2 requires access to a mutable Context object because it modifies the blockchain state data:


#![allow(unused)]
fn main() {
pub fn creates_contract(ctx: mut Context):
  ctx.create2(...)
}

block number

Reading block chain information such as the current block number requires Context (but does not require it to be mutable).

pub fn retrieves_blocknumber(ctx: Context) {
  ctx.block_number()
}