Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gate certain features behind a context object. #558

Closed
cburgdorf opened this issue Oct 4, 2021 · 4 comments
Closed

Gate certain features behind a context object. #558

cburgdorf opened this issue Oct 4, 2021 · 4 comments
Labels

Comments

@cburgdorf
Copy link
Collaborator

cburgdorf commented Oct 4, 2021

What is wrong?

One of the main objectives of Fe is to be explicit and easy to audit. The EVM exposes certain features such as writing logs, creating contracts or obtaining the current block number and it should be easy to determine from the signature of a function whether the function has the capabilities to use such features or not.

This is currently not the case, e.g we might currently have a function such as the following which - from it's signature - looks pure when in reality it does modify the blockchain (by emitting a log).

pub def looks_pure_but_isnt():
  emit SomeEvent()

The Context object

We propose to introduce a Context object which 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 e.g. the example above would be rewritten as:

pub fn emits_event(ctx: mut Context):
  ctx.emit(SomeEvent())

Similar, creating a contract via create/create2 would also require to have access to the Context object:

pub fn creates_contract(ctx: mut Context):
  ctx.create2(...)

As well does reading block chain information such as the current block number.

pub fn retrieves_blocknumber(ctx: Context):
  ctx.blocknumber()

There are a few special rules about the context object:

  1. The Context object has a defined location in the parameter list. It either is the first parameter if the function does not take self (e.g. pub fn foo(context:Context)) or it comes second if the function does take self (e.g. pub fn foo(self, context:Context)). Any other index in the parameter list is a compile time error.

  2. 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 amplified_block(ctx: Context) -> u256:
  # but it has to be passed along in this function call
  return retrieves_blocknumber(ctx) * 1000

fn retrieves_blocknumber(ctx: Context):
  return ctx.blocknumber()

Taking mut Context vs Context

As the eagle-eyed reader might have noticed the examples above either use mut Context or Context. This proposal can be implemented independent of having support for mut in Fe but it would naturally benefit from having mut support because all functionality that modifies the blockchain such as creating logs or contracts would require to obtain a mut Context reference whereas readonly access such as ctx.blocknumber() does not need require a mutable reference to the context.

ABI conformity

The proposed system works nicely with the existing function categories in the ABI but offers even tighter rules for added clarity.

Category Characteristics Fe Syntax ABI
Pure Can 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 impure foo(val: u256) pure
Read Contract Reading information from the contract instance (broad definition includes reading constants from contract code) foo(self) view
Storage Writing Writing to contract storage (own or that of other contracts) foo(mut self) payable or nonpayable
Context Reading Reading contextual information from the blockchain (msg, block etc) foo(ctx: Context) view
Context Modifying Emitting logs, transferring ether, creating contracts foo(ctx: mut Context) payable or nonpayable
Read Contract & Context Reading information from the contract instance and Context foo(self, ctx:Context) view
Read Contract & write Context Reading information from the contract instance and modify Context foo(self, ctx: mut Context) view
Storage Writing & read Context Writing to contract storage and read from Context foo(mut self, ctx: Context) payable or nonpayable
Storage Writing & write Context Writing to contract storage and Context foo(mut self, ctx: mut Context) payable or nonpayable

With the proposed system Fe would have nine different categories that can be derived from the function signatures that map to four different ABI types.


This brain dump is based on the PR discussion in #527 which offers more detailed reasoning and is worth a read.

@g-r-a-n-t
Copy link
Member

This looks good as far as I can tell.

@cburgdorf
Copy link
Collaborator Author

Currently we can read the address of the contract via self.address. I wonder if that should continue to be the case when the context object gets implemented or if it should be moved to ctx.address(). I think I'm leaning towards having it on ctx because there are a few similar cases that it would align nicely with. E.g. one could argue to introduce a self.balance to read the contract balance from but certainly balance_of(account: address) seems to be clearly suited to become a method on ctx.

To avoid any confusion I would say that they should all become context methods: ctx.address(), ctx.balance(), ctx.balance_of(account: address). This also has the nice side effect of not polluting the contract with any getters which leaves more room to developer :)

Thoughts?

@g-r-a-n-t
Copy link
Member

I'm also leaning towards ctx.address().

I think any value that is not determined purely by the contract's code and past transactions should fall in the category of ctx, whereas values that are determined purely by the contract's code and past transactions should fall under self.

So, for example, say you deploy a contract and invoke some mut self functions with a series of txs. Since the mut self functions do not take ctx, you will arrive at some state that is deterministic with relation to the initial state. However, if you throw in a call to a function that takes ctx, the final state is not deterministic with relation to the initial state. I'm not sure that this sort of guarantee is valuable, but it seems logical.

@sbillig
Copy link
Collaborator

sbillig commented Dec 9, 2021

Capturing from discord:

With #601 and #603, we're getting close to being able to define this in fe. Stuff like ctx.block_number(), ctx.msg_sender(), etc are doable now, but create and emit are tricky.

struct Context:
  _phantom: ()  // private field to prevent direct construction.
  // (Need to implement struct field visibility)

  pub fn msg_sender(self) -> address:
    unsafe:
      return std::evm::msg_sender()

  pub fn emit<E: event>(mut self, ev: E): // ?? takes any event type
    // Events aren't proper types at the moment. They can only be used with the `emit` stmt;
    // it's not possible to pass an event into a function right now.
    __magic_emit_intrinsic(ev)
    // ^ This needs to call the appropriate __log* fn depending on the number of idx fields.

  pub fn create<C: contract>(mut self, wei: u256) -> C: // ?? takes any contract type
    // Continuing to ignore constructor args. Dunno what to do about those yet.
    let (ctor_offset, ctor_len) = __contract_constructor_bytecode<C>()

    let mptr = std::mem::alloc(ctor_len)
    std::evm::code_copy(mptr, ctor_offset, ctor_len)
    return std::evm::create(wei, mptr, ctor_len)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants