Yul: Push, Pop & Return Array Object in Full Solidity Inline Assembly (Jul 2023)
With cryptocurrency entering winter, the requirement of blockchain technology declines by the day. It is therefore a good time to pick up blockchain skills as more people have the time to share or they do not feel the need to hide their expertise.
Solidity is the most commonly used language for smart contracts as most chains existed under EVM standard. If we go deeper into solidity, it merely compiles your codes into opcode, which is short for operation code. System only recognises machine codes which are so called the low level language.
By using such language not only it allows more control over your code and it also better optimises the memory usage. The only downside is that machine codes are not as readable as the high level programming language.
Solidity allows infusion of near-assembly codes. Yul is not exactly machine code but more like an intermediary between high level and low level codes. We cannot go full machine code in Solidity and Yul is the best we can do. Most operations save between 5% to 75% of gas.
In my example of array manipulation, it saves more than 50% of gas as compared to traditional syntax. As of currently I am writing this article, there are no online examples better than what I have coded. My functions are in full assembly and I am going to explain step-by-step.
Pushing an array - array.push()
Traditionally, it is:
mapping(address=>mapping(address=>uint[])) public arrayTraditional;
arrayTraditional[a][b].push(c);
Now, the Yul way of pushing array is:
function arrayPush(address a, address b, uint c) external {
assembly {
mstore(0x0, a)
mstore(0x20, b)
// The above 2 lines are to store the variables into memeory so
// keccak256 can be performed on them.
// It acts as identifier for to the position of variable[a][b].
let ptr := keccak256(0x0, 0x40)
// This will be the memory location of the array.
let len := sload(ptr)
// All global variables will be stored and loaded using the keccak256
// pointer as it is not possible that there will be a repeat.
sstore(ptr, add(len, 0x1))
// As an array variable, the immediate storage will be the length
// of how many items it holds.
mstore(0x0, ptr)
// The position of the array variable will need to be keccak256 again
// to store all its child element in it.
// Don't ask me why it don't store directly after its memory,
// probably because String is already doing that, so array element
// will be the keccak256 pointer of it.
sstore(add(keccak256(0x0, 0x20), len), c)
// The position of the latest push will be place at the end of the
// chunk so length is being used. Let me illustrate in a simple
// example by using shortened pointer:
// Keccak256 storage pointer of arrayNFTOwned = 0xA0001
// The length is 3, meaning it has 3 items
// Array index start from 0 so 0xA0001 already has an element
// 0xA0002 and 0xA0003 will already have elements in it as
// the length is 3.
// It is logical to store in 0xA0004 so we have to add length to
// the keccak256 pointer of the array.
}
}
Popping an array - array.pop()
Traditionally, it is:
mapping(address=>mapping(address=>uint[])) public arrayTraditional;
// A for loop to put last element to the index where it will be removed.
arrayTraditional[a][b].pop();
Now, the Yul way of popping array is:
function arrayPop(address a, address b, uint c) external {
assembly {
mstore(0x0, a)
mstore(0x20, b)
let ptr := keccak256(0x0, 0x40)
let len := sload(ptr)
// Look at the previous explanation of the above 4 lines.
if iszero(gt(len, c)) {
revert(0x0, 0x0)
}
// Prevent removal of index outside the length.
sstore(ptr, sub(len, 0x1))
// This is to reduce the array length
mstore(0x0, ptr)
ptr := keccak256(0x0, 0x20)
// Look at the previous explanation of the above 2 lines.
sstore(add(ptr, c), sload(add(ptr, sub(len, 0x1))))
// This is the interesting operation to move the last element into
// the index that is going to be popped. We load the last index and
// put it into the index that is going to be deleted. We will be
// neglecting whether the last element is the index to delete and
// the storage of the last index is still around. This is to reduce
// the gas require to process them.
}
}
Returning an array - returns (uint[])
Traditionally, it is:
mapping(address=>mapping(address=>uint[])) public arrayTraditional;
function arrayReturn(address a, address b) returns(uint[]) {
return arrayTraditional[a][b];
}
Returning a full array is going to be lengthier than just both pushing and popping.
function uintEnum(address a, address b) external view returns (uint[] memory val) {
uint len;
assembly {
mstore(0x0, a)
mstore(0x20, b)
let ptr := keccak256(0x0, 0x40)
mstore(0x0, ptr)
len := sload(ptr)
// Standard loading items, refer to previous codes for explanation.
}
val = new uint[](len);
// This is necessary, trust me, I have tried putting it in assembly
// and it is not easy. Using this method manages the gas fee more
// effeciently and not messing up the memory.
assembly {
let ptr := keccak256(0x0, 0x20)
// Same explanation as previously.
for { let i := 0x0 } lt(i, len) { i := add(i, 0x1) } {
// We need a for loop using the length of the array to populate
// local storage from global storage.
mstore(add(val, mul(add(i, 0x1), 0x20)), sload(add(ptr, i)))
// Since the first memory slot of val is already input with the
// length of the array, all subsequent elements will be below
// its memory.
// We will loop through the global variables to select the entire
// range of elements and populate it downwards accordingly.
}
}
}

Comments
Post a Comment