flintrocks / flint

The Flint Programming Language for Smart Contracts
MIT License
2 stars 0 forks source link

Analyze generate IR for external calls #73

Closed Aurel300 closed 5 years ago

Aurel300 commented 6 years ago

Blocked by #70.

IR should be generated for external calls. It may be needed to construct the data for the call conforming to the Solidity ABI, unless there is a built-in instruction for this in YUL.

Return value should be placed in the correct variable when using call?. gas and value hyper-parameters should be respected. Exception handling is in scope for this ticket also, at least at a rudimentary level (no need for nested do-catch blocks, for now the outer ones wouldn't have any effect since we only have one type of exception, ExternalCallError).

Aurel300 commented 5 years ago

Call process

Apart from exception handling (described below), the IR for an external call should be roughly:

// prepare hyper parameters
let gasAmount = ...
let externalContractAddress = ...
let weiAmount = ...

// prepare input (argument) memory
let inSize = ... // calculate number of bytes needed for the full argument stack
let input = allocate(inSize)
mstore8(input, /* first byte of Keccak-256 hash of target function signature */)
mstore8(input + 1, /* second byte of same */)
mstore8(input + 2, /* third byte of same */)
mstore8(input + 3, /* fourth byte of same */)
/* store remaining arguments according to the Solidity ABI */

// prepare output memory
let outSize = ... // calculate number of bytes needed for the output
let output = allocate(outSize)

// enter external call state
let previousState = self.flintState$
self.flintState$ = "$externalCall"

// execute call
let callSuccess = call(
    gasAmount,
    externalContractAddress,
    weiAmount,
    in, inSize,
    out, outSize
  )

// restore state
self.flintState$ = previousState

Exception handling

(Relevant for #85)

Currently, Solidity does not offer exception catching (see https://solidity.readthedocs.io/en/latest/control-structures.html#error-handling-assert-require-revert-and-exceptions), so it is not a good reference. However, the same document identifies two EVM instructions that we could call to actually revert the state of the contract (IF an exception was not caught). I also looked at what these instructions map to in YUL (see https://solidity.readthedocs.io/en/latest/yul.html#low-level-functions):

EVM opcode YUL function Description
0xFD revert(_, _) revert operation
0xFE abort() invalid operation

General exception handling

The YUL grammar also doesn't include anything for exception catching (unsurprisingly). I suspect to implement proper exception handling, we would need to:

The global stack can be implemented with a dynamic array, but the actual runtime check is rather complex. Most importantly, YUL does not actually have any mapping to the instructions 0x56 JUMP or 0x57 JUMPI (conditional jump). Switching to producing EVM code directly would solve this, but that is obviously a large task.

And even if we had JUMP(I), we would need to make sure our context is correct – most likely we need to keep track of how many items we need to pop off the stack to unwrap the function calls (from the function that raised the exception to the function that catches it).

External call error handling

For external calls specifically, we can simplify the situation a bit. In particular, if ExternalCallError is the only exception we are handling, we don't need to keep track of which handler can handle what type of exceptions – the top item on the stack will always handle the exception.

We might also be able to get away without JUMP, since external calls are opaque from the point of view of Flint (in particular, we simply have e.g. a CALL EVM instruction, which will put 0 on the stack in case of any call failure or exception). The idea would then be to transform code as follows (similar to ECMAScript async -> state machine transform):

do {
  call externalContract.funcA();
  // code after first outer success
  do {
    call externalContract.funcB();
    // code after inner success
  } catch is Error {
    // handle inner error
  }
  call externalContract.funcC();
  // code after second outer success
} catch is Error {
  // handle outer error
}

Into (more or less):

if (!externalContract.call("funcA").successful) {
  // handle outer error
} else {
  // code after first outer success
  if (!externalContract.call("funcB").successful) {
    // handle inner error
  } else {
    // code after inner success
  }
  if (!externalContract.call("funcC").successful) {
    // handle outer error
  } else {
    // code after second outer success
  }
}

Note that nested do-catch blocks result in duplication of IR code. It is more difficult to do the above transformation if we allow do-catch to be outside of the function that can raise the exception.

So for the time being we should require do-catch to be around any call (default mode) within the same function. This simplifies semantic analysis as well (i.e. we don't need to mark that a function can potentially throw).

Additional notes

nvgrw commented 5 years ago

@Aurel300 split

nvgrw commented 5 years ago

Tickets

  1. Error handling (transforming AST nodes) (#87)
  2. IR Generator scaffolding/template \ {input stack} (#74+)
  3. Input stack because Solidity ABI does not 1:1 map to Flint types. Add type modifiers for sizing + semantic checks. (#93 #90 #91)
    • Allow Int8, ...Int256 for external traits only, and figure out how to do the other type conversions
    • In calls, allow the types above only as part of a cast with as! construct.
    • as! construct incurs runtime check (and associated cost) by doing numeric comparisons.
  4. Keccak 256 in Swift (#88)
  5. Implicit constructors for external traits (#104)
nvgrw commented 5 years ago

See above comment