contract SolidityContract {
// "9a884bde": "get21()" - function signature
function get21() external pure returns (uint256) {
return 21;
}
}
In Yul, we have to load the function selector into memory, then point to the region that will be part of our abi call, i.e. bytes 28 to 32 which contain the function signature
Opcodes for making external calls: staticcall, call, and delegatecall
If we are inside of a view function, we have to use staticcall, because static calls do not change state. if state is being changed, the call will revert
The compiler will not allow us to use call within a view function, since the function is not supposed to modify state
Breakdown of arguments in staticcall :
staticcall(gas(), _a, 28, 32, 0x00, 0x20)
gas() - amount of remaining gas the contract has left. can be hardcoded to a smaller amount, can be used if we don’t trust the receiving contract, since it may attempt to consume all the gas in a DoS attack
_a - address of the contract we are calling
28, 32 - intended [tx.data](<http://tx.data>) , which is what we have loaded into memory
0x00, 0x20 - region in memory that we are going to copy the results back into. so when the function returns, it will override the function selector in 0x00, but it does not matter since we don’t need this info any more
Getting return values from a revert call in Yul
// function to call: "73712595": "revertWith999()",
function revertWith999() external pure returns (uint256) {
assembly {
mstore(0x00, 999)
revert(0x00, 0x20)
}
}
// Yul calling function
function getViaRevert(address _a) external view returns (uint256) {
assembly {
mstore(0x00, 0x73712595)
pop(staticcall(gas(), _a, 28, 32, 0x00, 0x20)) // discard the return boolean value since we know it will be 0 / false
return(0x00, 0x20) // returns 999
}
}
when a function reverts, it will still return the revert values to the same location that we assigned to it, in this case 0x00 - 0x20
Calling a function that includes arguments
since the arguments take up more than 64 bytes, we cant put it in scratch space
have to store the function arguments into locations in memory with mstore
// function to call:"196e6d84": "multiply(uint128,uint16)",
function multiply(uint128 _x, uint16 _y) external pure returns (uint256) {
return _x * _y;
}
// Yul calling function
function callMultiply(address _a) external view returns (uint256 result) {
assembly {
let mptr := mload(0x40)
let oldMptr := mptr
mstore(mptr, 0x196e6d84)
mstore(add(mptr, 0x20), 3) // _x
mstore(add(mptr, 0x40), 11) // _y
mstore(0x40, add(mptr, 0x60)) // advance the memory pointer 3 x 32 bytes
// 00000000000000000000000000000000000000000000000000000000196e6d84
// 0000000000000000000000000000000000000000000000000000000000000003
// 000000000000000000000000000000000000000000000000000000000000000b
let success := staticcall(
gas(),
_a,
add(oldMptr, 28), // fast forward to get the function signature
mload(0x40), // advanced memory pointer after arguments have been stored in memory
0x00,
0x20
)
if iszero(success) {
revert(0, 0)
}
result := mload(0x00)
}
}
CALL
Difference from staticcall is the addition of a callvalue() variable, which is used to forward the ETH received as part of the transaction (msg.value )
If a function is not payable, callvalue() can just be set to 0
// function to call : "4018d9aa": "setX(uint256)"
function setX(uint256 _x) external {
x = _x;
}
// Yul calling function
function externalStateChangingCall(address _a) external {
assembly {
mstore(0x00, 0x4018d9aa)
mstore(0x20, 999)
// memory now looks like this
// 0x000000000000000000000000000000000000000000000000000000004018d9aa
// 000000000000000000000000000000000000000000000000000000000000000999
let success := call(
gas(),
_a,
callvalue(),
28,
add(28, 32),
0x00,
0x00
)
if iszero(success) {
revert(0, 0)
}
}
}
Unknown return size
in some situations, we don’t know what the size of the return value will be
if the function returns some unknown data size, e.g. a string or an array, we use returndatasize() to obtain the return data size, and ignore the allocated positions in staticcall
returndatacopy will copy the return value into a location in memory: in this case, copy into slot 0, the data at 0, up to the total size of the return data
DELEGATECALL
Openzeppelin implementation of delegatecall
function _delegate(address implementation) internal virtual {
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
// msg.value: hardcoded to zero
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}