Closed Aurel300 closed 5 years ago
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
(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 |
The YUL grammar also doesn't include anything for exception catching (unsurprisingly). I suspect to implement proper exception handling, we would need to:
abort()
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).
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).
Int
in Flint map to int8
/ uint8
/ int32
/ uint32
, int256
, uint256
…?external trait
and warn on collision.@Aurel300 split
Int8
, ...Int256
for external traits only, and figure out how to do the other type conversionsas!
construct.as!
construct incurs runtime check (and associated cost) by doing numeric comparisons.See above comment
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
andvalue
hyper-parameters should be respected. Exception handling is in scope for this ticket also, at least at a rudimentary level (no need for nesteddo-catch
blocks, for now the outer ones wouldn't have any effect since we only have one type of exception,ExternalCallError
).