For making low-level calls to external contracts' functions
When encoding static types, it is just one part, the data itself in hex.
When encoding dynamic datatypes, there are three parts to it in the encoded format - offset, length and the data itself. Each part is 32 bytes long. The offset acts as the pointer to the actual data.
For example -
Input:
return abi.encode("hello",1337,"world");
Output:
0000000000000000000000000000000000000000000000000000000000000060 // offset of "hello"
0000000000000000000000000000000000000000000000000000000000000539 // 1337 in hex
00000000000000000000000000000000000000000000000000000000000000a0 // offset of "world"
0000000000000000000000000000000000000000000000000000000000000005 // length of "hello"
68656c6c6f000000000000000000000000000000000000000000000000000000 // data of "hello" in hex
0000000000000000000000000000000000000000000000000000000000000005 // length of "hello"
776f726c64000000000000000000000000000000000000000000000000000000 // data of "world" in hex
Note: When you are running EVM in the memory, dynamic types will be in only 2 parts - the length and the data (no offset). When there are 3 parts, it is the ABI encoding of the datatype.
Encoded in hex, without the three parts for dynamic types mentioned above
For example -
Input:
return abi.encode("hello",1337,"world");
Output:
68656c6c6f0539776f726c64
//basically gibberish, not useful
Just appends the function selector with the rest of the calldata staying the same as in encode()
For example -
Input:
return abi.encodeWithSignature("someFunc(string, uint256, string)", "hello", 1337, "world");
Output:
6a49c500 // function selector for "someFunc(string, uint256, string)", rest is the same...
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000539
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000005
68656c6c6f000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000005
776f726c64000000000000000000000000000000000000000000000000000000
Exactly the same output as encodeWithSignature()
, just the syntax of the parameters is different.
For example -
Input:
return abi.encodeWithSelector(Helper.someFunc.selector, "hello", 1337, "world");
Output: Exactly the same
Exactly the same output as encodeWithSignature()
, again, the syntax of the parameters is different.
For example -
Input:
return abi.encodeCall(Helper.someFunc, ("hello",1337,"world");
Output: Exactly the same
Suppose you have set a nonReentrant modifier of your own, and want to reset the status of the function after it has been executed, so that it can be used again later on.
uint256 _status = 1;
uint256 ENTERED = 2;
uint256 NOT_ENTERED = 1;
modifier nonReentrant {
require(_status != ENTERED);
_status = ENTERED;
_; // function execution here
_status = NOT_ENTERED; // post function execution
}
If you are doing the normal solidity return in a function with this above modifier, the EVM understands that there is code to be executed after the function call, and acts accordingly.
Example -
function testSolidityReturn() public nonReentrant returns(uint256) {
return 0x42;
}
However, if you are returning in Yul, it is immediate, brutal execution. Nothing runs after that return statement. If you are doing a Yul return in a function with the above modifier, the _status
will set its value to ENTERED
and never reset. This will lead to a DoS, since it will now forever be in the ENTERED
state and reentrancies are not allowed.
function testYulReturn() public nonReentrant returns(uint256) {
assembly {
//mstore(offset,value)
mstore(0x00, 0x42);
//return(offset,size)
return(0x00, 0x20);
}
}
Huff and Yul are low-level languages for writing smart contracts. They can be used to make code insanely optimized for gas.
- Everything costs gas. Solidity automatically identifies which variable goes in memory and which variable goes in storage.
- Storing in memory(
MSTORE
) costs a minimum of 3 gas while storing in storage(SSTORE
) costs a minimum of 100 gas. - Adding two values from stack (
ADD
) takes 3 gas. It takes two values, a(eg.,PUSH1 0x01
) and b(eg.,PUSH1 0x03
), from stack and returns the sum of the two(eg.,PUSH1 0x04
). It will only add the two values at the top of the stack. PUSH<number>
adds a value to the stack. The<number>
defines the number of bytes of the data.PUSH0
pushes 0 to the stack.CALLDATALOAD
loads the first 32 bytes of calldataSHR
shifts a 32-byte value to the right by the amount of bits specifiedJUMPI
jumps to a destination program counter if a condition is metDUP1
duplicates the value on top of the stack.REVERT
reverts the code so that if no conditions are met, the function doesn't keep executing whatever is next.
- Storing in memory(
-
Commands-
huffc <file-location>
- To compile the codehuffc <file-location> -b
- To compile the code and get the bytecode of the smart contracthuffc <file-location> --bin-runtime
- To compile the code and get the runtime bytecode
-
Features-
- Just to make the code easier to understand and to avoid having to manually get each function selector.
- 1st step :
#define function function_name() nonpayable returns() {}
- Define like in Solidity, nonpayable keyword in Huff. - 2nd step : Instead of pushing the function sig manually like
0x12345678
, you can write the__FUNC_SIG(function_name)
statement.
- 1st step :
- Huff makes working with storage easier. Every storage slot is going to be 32 bytes.
FREE_STORAGE_POINTER()
essentially a counter that provides with a slot that is currently open to store data in.
- Just to make the code easier to understand and to avoid having to manually get each function selector.