The Lightest and Streamlined ERC897 Proxy Contract with Memory Slot Manipulation in Yul (Solidity Assembly)
The ERC897 Proxy Contract stands as a pivotal innovation in the realm of Ethereum-based smart contracts, reshaping the landscape of decentralised applications. At its core, ERC897 introduces a versatile proxy mechanism, providing an avenue for efficient and secure manipulation of memory slots within the Ethereum blockchain.
The fundamental concept of a proxy, in the context of smart contracts, serves as a bridge between users and complex decentralised systems. It acts as an intermediary, enabling seamless interaction with underlying functionalities while offering flexibility for upgrades and modifications without disrupting the core structure.
Understanding why proxies are utilised delves into the heart of decentralisation and adaptability. They serve as the cornerstone for upgradability, allowing developers to enhance and refine smart contracts without necessitating redeployment or risking the loss of data and accumulated value. This capability makes them invaluable in the rapidly evolving landscape of blockchain technology, empowering developers to iterate and improve without cumbersome overhauls.
What sets ERC897 apart lies in its streamlined design and optimised functionalities. Unlike traditional proxies, ERC897 redefines efficiency by enabling precise manipulation of memory slots. This precision unlocks a new level of optimization, facilitating more agile and resourceful smart contract development. Its superiority lies in its ability to offer a more refined and granular approach to proxy functionality, enhancing the overall performance and adaptability of Ethereum-based applications.
Below is the code of a minified ERC897 that is cost efficient yet secured and with memory manipulation.
// SPDX-License-Identifier: None
pragma solidity 0.8.0;
import {Ownable} from "https://github.com/aloycwl/Util.sol/blob/main/Access/Ownable.sol";
contract ERC897 is Ownable {
constructor(address adr) {
assembly { sstore(IN2, adr) }
}
fallback() external {
assembly {
calldatacopy(0x00, 0x00, calldatasize())
let res := delegatecall(
gas(), sload(IN2), 0x00, calldatasize(), 0x00, 0x00)
let sze := returndatasize()
returndatacopy(0x00, 0x00, sze)
switch res
case 0 { revert(0x00, sze) }
default { return(0x00, sze) }
}
}
receive() external payable {
assembly {
calldatacopy(0x00, 0x00, calldatasize())
let res := delegatecall(
gas(), sload(IN2), 0x00, calldatasize(), 0x00, 0x00)
let sze := returndatasize()
returndatacopy(0x00, 0x00, sze)
switch res
case 0 { revert(0x00, sze) }
default { return(0x00, sze) }
}
}
function implementation() external view returns(address adr) {
assembly { adr := sload(IN2) }
}
function mem(bytes32 byt) external view returns(bytes32 val) {
assembly { val := sload(byt) }
}
function mem(bytes32 byt, bytes32 val) external onlyOwner {
assembly { sstore(byt, val) }
}
}
In basic terms, a proxy contract acts as a messenger between the caller and the implementation contract. When a function is called on the proxy, it first checks if it has that function. If it does, it handles the request internally. However, if the function doesn't exist within the proxy, it relays the request to the implementation contract.
This mechanism requires careful consideration: functions and variables should not conflict between the proxy and the implementation contract. If there's overlap, the proxy's functions take precedence, and the implementation contract's functions will never be invoked. Hence, it's crucial to avoid redundancy or replication of functions and variables across both contracts to ensure seamless delegation and proper functioning of the implementation contract.
Below is an example of the usage of the ERC897 proxy.
ERC20 e20 = new ERC20("Proxy Token", "PXT");
Proxy pxy = new Proxy(address(e20));
In our example using an ERC20 token contract, the constructor in Proxy necessitates an address input. Therefore, converting the newly created contract becomes imperative. This conversion process establishes a fresh proxy, automatically setting the implementation address to the e20 address.
领英推荐
fallback() and receive() explained.
// Basically these functions are the same, they resend the calldata that
// was sent to the proxy contract without the correct function selector.
// fallback() - This function is executed when no ether is sent.
// receive() - This function is executed when ether is sent.
// Get all the data sent and the size of the data to determine end of
// message
calldatacopy(0x00, 0x00, calldatasize())
// Send the data to the implementation address inside IN2
let res :=
delegatecall(gas(), sload(IN2), 0x00, calldatasize(), 0x00, 0x00)
// Get the length of the return data to determine the end of message
let sze := returndatasize()
// Get all the data received from the implementation contract
returndatacopy(0x00, 0x00, sze)
// Prompt error if sending fail otherwise send the returned message
switch res
case 0 { revert(0x00, sze) }
default { return(0x00, sze) }
// Even through they did the same thing and can be re-called using a
// private function, it will cost a little extra gas to therefore all
// based codes are duplicated between the 2 functions.
implementation() usage.
// To know get the existing implementation address:
address adr = 0x000...000; // Proxy address
address imp = ERC897(adr).implementation();
// To set a new implementation address
address newContract = address(new ERC20("Another Token", "ANT"));
ERC897(adr).mem(IN2, newContract);
mem(bytes32) and mem(bytes32, bytes32) usage and explained.
These functions, while not standard in a typical ERC897 contract, hold immense utility as they grant access to all storage pointers. In this setup, all storage is defined by bytes32, effectively providing nearly limitless storage positions (16^64).
// To store a value
address tmp = msg.sender;
bytes32 ptr = keccak256(abi.encodePacked(tmp));
bytes32 val = bytes32(uint(0x1234));
ERC897(adr).mem(ptr, val);
// This map the caller's address into a bytes32 position and the
// information pertaining to this user is stored in the memory Solidity
// To retrieve a value
address tmp = msg.sender;
bytes32 ptr = keccak256(abi.encodePacked(tmp));
bytes32 val = ERC897(adr).mem(ptr);
// Using the same mapping manner the val can be recovered, to ensure no
// similar mapping, you can add salt in the keccak to better segregate it.
These 'mem' functions offer comprehensive control over your contract's storage. They provide access to manipulate or retrieve data from any storage position, including arrays and string values. Typically, for arrays and strings, the initial bytes32 holds the data size, with subsequent data points following.
Their versatility makes them a cornerstone in managing storage efficiently. In scenarios where conventional mappings or global variables fall short, direct memory manipulation becomes indispensable. These functions form the backbone of storage operations, catering to a wide array of contract functionalities.
For the source code, visit my GitHub repository at https://github.com/aloycwl/Proxy.sol/tree/main/ERC897 . Additionally, explore my previous article on ERC1822 proxy at https://www.dhirubhai.net/pulse/simplest-way-use-erc-1822-universal-upgradeable-proxy-aloysius-chan/ .