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.PUSH0pushes 0 to the stack.CALLDATALOADloads the first 32 bytes of calldataSHRshifts a 32-byte value to the right by the amount of bits specifiedJUMPIjumps to a destination program counter if a condition is metDUP1duplicates the value on top of the stack. Similarly,DUP2adds a duplicate of the value second in the stack to the top of the stack, and so on.REVERTreverts the code so that if no conditions are met, the function doesn't keep executing whatever is next.- In memory, the first slot is
0x00, the second slot is not0x01, it is0x20, since it is the first slot after 32-bytes, then0x40, and so on. 0x40is the free memory pointer in Solidity. You access that to see which slot in memory is free to use. During contract creation,0x80(128-bytes) is set as the free memory slot. In Huff, we usually don't use 0x40 as the free memory pointer as we didn't need it.-
Example -
contract Example { function addTwo(uint256 _param) public pure returns(uint256) { uint256 two = 2; // This line as reference return _param + two; } }
That particular line's opcode:
PUSH1 0x2 // [2] PUSH1 0x40 // [0x40, 2] MLOAD // [0x80, 2] (Loads the free memory slot from 0x40) MSTORE // [] (Stores the value 2 in the slot 0x80)After that is stored at 0x80, the free memory pointer will point to
0x6a, which is 32-bytes after0x80
-
CODECOPYstores the runtime code in memory on-chain. It takes the top 3 values from stack, the top one being the byte offset in memory where result will be copied, the next one being the offset in code from where we need to start copying and the third one being the byte size to copy.CALLDATASIZEpushes the byte size of the calldata to stack.SWAP1swaps the places of the value on top of the stack with the value right below it. Similarly,SWAP2swaps the places of the value on top of the stack and the value third in stack, and so on.- Note: Go through EVM precompiles, which are special addresses bundled with EVM to perform certain tasks. You can see them in some bytecodes with hardcoded addresses or
STATICCALLs.
-
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.
- Mappings algorithm in Huff is quite complicated so use a library called Huffmate to just import the data structure's code and use it.
- Just to make the code easier to understand and to avoid having to manually get each function selector.
- Inline assembly language, can be integrated into Solidity code.
datacopykeyword is the same asCODECOPYin opcodes.- Use
letto create a variable in Yul, followed by identifier and then:=as the assignment operator. - Functions are created as
function fnName() -> n {...}, where-> nimplies that the function will return a value n. Remove that part if no value needs to be returned by function. - To run,
- Specify solc compiler version using
solc-select use <VERSION> - Then, run
solc --strict-assembly --optimize --optimize-runs 2000 <FILE_LOCATION> --bin--optimize-runs 2000, 2000 here is just used as an example, any value can be used here depending on your specifications; this step can be skipped too.--binis to get the binary output (bytecode)- Add
| grep 60to just get the binary output line
- Specify solc compiler version using
not(x)basically inverts the value. For example,not(0)would be 0xffffff...fff (inv(0x000000...000))