Revamping the Foundation: Enhancing Smart Contract and Proxies for Future-proof Performance [6/8]

Amir Doreh
8 min readApr 25, 2023

--

In previous series of articles, we discussed the fundamentals and various useful basics regarding smart contract proxies. I highly suggest that you refer back to those articles and review them. (The hyperlinks located at the bottom of this piece of writing.)

In this current publication, we intend to bring attention to comprehending EIP-1538: Transparent Contract Standard and EIP-2535: Diamond Standard.

EIP-1538: Transparent Contract Standard

Now, let’s talk about two things:

EIP-1538 and Transparent contract standards and EIP-2535 :
diamond standard. EIP-2535 replaces his EIP-1538, both developed by Nick Mudge. So without going into too much detail, I’ll briefly describe the idea of ​​EIP-1538, just to give you an idea of ​​what’s going on.

This is the first implementation that does something very clever.
Instead of defining the entire logical contract, we basically extract the functionality of the logical contract and set the address.

This way, you can create as many logic contracts as you need and gradually upgrade functionality.

An example implementation can be found in the repository. I’ll explain how it works, but since EIP-1538 has been deprecated and replaced by EIP-2535, I won’t go into further detail.

From the test case we can see that it’s all about “MyTransparentContract” which also contains a fallback function that makes a delegate call. This gets the address of an ERC1538Delegate containing a function that maps the function signature (bytes4) to the address.

MyTransparentContract’s fallback function then uses a lookup to determine which function signature executes at which address and makes a delegate call within MyTransparentContract.

Requires quite a bit of setup.
For example, for ERC20 tokens, you need to provide all the function signatures that run on the ERC20 address and add them to the mapping via MyTransparentContracts updateContract, which is the logic used in ERC1538Delegate. It’s very complicated to understand and only one thing can be solved in my opinion:
You can bypass the maximum contract size of 24 KB.

I may be wrong, but I don’t see any gas savings from adding features atomically because upgrading features requires deploying the entire contract first. I understand that providing a virtual function could work around this to some extent, but my understanding is that it’s not enough to be considered an atomic update. So, for example, if you have deployed a Mint-enabled ERC20 contract and want to change the Mint functionality in some way, you will still need to redeploy the entire ERC20 contract including all functionality that the Mint functionality depends on, including the new Mint . Ability to change it.

But Nick Mudge seems to have found a better solution. Now let’s talk diamonds…

EIP-2535: Diamond Standard

Compared to EIP-1538, the Diamond Standard is an improvement. The concept is the same: to map individual functions for a delegatecall to addresses rather than proxying a complete contract.

The Diamond Standard’s storage system is its key component. In contrast to OpenZeppelin’s unstructured storage style, the Diamond Storage allocates a single struct to each storage slot.

Function-wise, it appears as follows, as seen on the EIP Page:

// A contract that implements diamond storage.
library LibA {

// This struct contains state variables we care about.
struct DiamondStorage {
address owner;
bytes32 dataA;
}

// Returns the struct from a specified position in contract storage
// ds is short for DiamondStorage
function diamondStorage() internal pure returns(DiamondStorage storage ds) {
// Specifies a random position from a hash of a string
bytes32 storagePosition = keccak256("diamond.storage.LibA")
// Set the position of our struct in contract storage
assembly {ds.slot := storagePosition}
}
}

// Our facet uses the diamond storage defined above.
contract FacetA {

function setDataA(bytes32 _dataA) external {
LibA.DiamondStorage storage ds = LibA.diamondStorage();
require(ds.owner == msg.sender, "Must be owner.");
ds.dataA = _dataA
}

function getDataA() external view returns (bytes32) {
return LibDiamond.diamondStorage().dataA
}
}

With this, you may have as many LibXYZ and FacetXYZ as you like; due to the whole struct, they are always in different storage slots as a whole. To fully comprehend it, keep in mind that this information is maintained in the proxy contract that executes the delegatecall, not in the faucet itself.

Because of this, you may share storage with other faucets. Each storage space is explicitly specified (keccak256(“diamond.storage.LibXYZ”)).

The Proxy Contract

Everything in the “Diamond Standard” is based on diamond terminology. Cutting a diamond to add functions (or mapping addresses to functions and vice versa) is a good graphic representation of the concept.

“diamondCut” is the name of the function that adds Facets and other functions.

“Loupe” is a feature that allows you to see the function addresses, signatures, and any other information about a Facet that you might be interested in.

This capability may be implemented in a variety of ways. On his repository, Nick went ahead and produced three distinct approaches to performing a reference implementation.

Check out the migration file’s Smart Contract deployment first. This demonstrates that setting up the Diamond contract already provides the addresses and function selectors for the DiamondCutFacet and DiamondLoupeFacet. They are essentially integrated into the Diamond Proxy.

If you look out the test-case, you’ll find that the initial test cases are retrieving address-to-signature mapping and verifying that they were indeed established in the Diamond proxy. The Test1Facet function is added on line 121, followed by the addition of the Test2Facet function.

Trying It Out

First, we need to clone the repository:

git clone https://github.com/mudgen/diamond-1.git

then we start ganache-cli (download it with npm install -g ganache-cli if you don't have it), in a second terminal window:

ganache-cli

then we simply run the tests and have a look what happens

truffle test

What you can see is that the constructor of the Diamond contract calls the diamondCut interface, which is only accessible through the library. Simply delete the diamondCut function if you want to disable the entire update feature.

To construct a straightforward variable and add it to the Diamond in the test case, let’s create a new file called “FacetA.sol” in the contracts/facets folder with a bugfixed version of the material given above.

contracts/facets/FacetA.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

library LibA {

// This struct contains state variables we care about.
struct DiamondStorage {
address owner;
bytes32 dataA;
}

// Returns the struct from a specified position in contract storage
// ds is short for DiamondStorage
function diamondStorage() internal pure returns(DiamondStorage storage ds) {
// Specifies a random position from a hash of a string
bytes32 storagePosition = keccak256("diamond.storage.LibA");
// Set the position of our struct in contract storage
assembly {
ds.slot := storagePosition
}
}
}

// Our facet uses the diamond storage defined above.
contract FacetA {

function setDataA(bytes32 _dataA) external {
LibA.DiamondStorage storage ds = LibA.diamondStorage();
ds.dataA = _dataA;
}

function getDataA() external view returns (bytes32) {
return LibA.diamondStorage().dataA;
}
}

Let’s modify our migrations file as well:

/migrations/03_faceta.js

const FacetA = artifacts.require('Test2Facet')

module.exports = function (deployer, network, accounts) {
deployer.deploy(FacetA)

}

The code isn’t particularly safe as it stands because anyone in any facet may access keccak256(“diamond.storage.LibA”); and replace the storage slot, if you’ve been paying attention up to this point.

Include the next unit-test:

/test/facetA.test.js

/* eslint-disable prefer-const */
/* global contract artifacts web3 before it assert */

const Diamond = artifacts.require('Diamond')
const DiamondCutFacet = artifacts.require('DiamondCutFacet')
const DiamondLoupeFacet = artifacts.require('DiamondLoupeFacet')
const OwnershipFacet = artifacts.require('OwnershipFacet')
const FacetA = artifacts.require('FacetA')
const FacetCutAction = {
Add: 0,
Replace: 1,
Remove: 2
}


const zeroAddress = '0x0000000000000000000000000000000000000000';

function getSelectors (contract) {
const selectors = contract.abi.reduce((acc, val) => {
if (val.type === 'function') {
acc.push(val.signature)
return acc
} else {
return acc
}
}, [])
return selectors
}

contract('FacetA Test', async (accounts) => {

it('should add FacetA functions', async () => {
let facetA = await FacetA.deployed();
let selectors = getSelectors(facetA);
let addresses = [];
addresses.push(facetA.address);
let diamond = await Diamond.deployed();
let diamondCutFacet = await DiamondCutFacet.at(diamond.address);
await diamondCutFacet.diamondCut([[facetA.address, FacetCutAction.Add, selectors]], zeroAddress, '0x');

let diamondLoupeFacet = await DiamondLoupeFacet.at(diamond.address);
result = await diamondLoupeFacet.facetFunctionSelectors(addresses[0]);
assert.sameMembers(result, selectors)
})

it('should test function call', async () => {
let diamond = await Diamond.deployed();
let facetAViaDiamond = await FacetA.at(diamond.address);
const dataToStore = '0xabcdef';
await facetAViaDiamond.setDataA(dataToStore);
let dataA = await facetAViaDiamond.getDataA();
assert.equal(dataA,web3.eth.abi.encodeParameter('bytes32', dataToStore));
})

})

You can observe that the functions from FacetA.sol are added to the Diamond by running the test with truffle test test/facetA.test.js. In the second test scenario, a value is stored and retrieved twice.

Cons and Benefits

On the bright side, this is a clever idea for getting around really high Smart Contracts restrictions and upgrading your Contracts gradually. It is undoubtedly in its early stages and needs more research.

I hoped that a system would be developed that would enable you to partition your Smart Contracts into smaller pieces and deploy and update each one independently. It provides that in some way, but it also doesn’t since Facets still require an accurate image of all functions and signatures that are utilized internally.

Overall, I think Nick is headed in the right direction. However, there are a couple significant flaws that prevent us from using it:

  • The proxy may serve as the hub for an extensive network of Smart Contracts. Sadly, bigger systems sometimes largely rely on inheritance, therefore you must be particularly cautious when adding functions to the Diamond proxy. Additionally, function signatures for two distinct system components with the same name might easily overlap.
  • Unless you just utilize one single aspect that uses unstructured storage, every Smart Contract in the System needs to be adopted for the Diamond Storage. It would not be advisable to just add the OpenZeppelin ERC20 or ERC777 tokens since they would begin writing to the Diamond Contract storage slot 0.
  • It is risky to share storage between aspects. The admin is saddled with a lot of responsibility.
  • It is quite laborious to add functionality to the Diamond using diamondCut. I am aware that there are alternative approaches where the aspects provide their own configuration — which is far superior, like in this blog post — and I do understand such approaches.
  • By adding features with DiamondCut, the Diamond could become quite gas-intensive. For our FacetA Contract, adding the two functions will cost 109316. That’s $20. Extra.

What follows is the next step?

In our next article, we will be discussing Metamorphosis Smart Contracts using CREATE2. It is essential that you have followed the article series and comprehended the underlying logic!

Useful links :

Basics, definitions, and the potential issues that can arise when using Smart Contracts without Proxies

Standards for Smart Contract Upgrades and Eternal Storage without Proxy

The very first Proxy!

Storage and Storage Collisions, and elucidate the EIP-897 standard for our initial genuine Proxy

EIP-1822 UUPS standard and EIP-1967 Standard Proxy Storage Slots .

discord: am.dd#3991

--

--