Shardeum Automates EIP-2930 — A Comprehensive Educational Guide
Table of contents
- Multicall Contract
- What is EIP-2930?
- Where is EIP-2930 Data Located for a Transaction?
- How do I Define an AccessList for an EIP-2930 Transaction?
- EIP-2930 Optional:
- EIP-2930 Required:
- Sphinx Dapp Address codeHash in Storage Slots:
- Solidity codeHash Example:
- EIP-2930 accessList Transactions for Multicall Contract to Modify slot0 in contractToCall:
- How Can I Create an EIP-2930 accessList Easily?
- Solidity Interfaces
- When should I use a Solidity Interface?
Multicall Contract
What is EIP-2930?
EIP-2930 enables users to specify addresses and storage slots for a transaction.
https://eips.ethereum.org/EIPS/eip-2930
Sphinx 1.X has automated the access list for Shardeum RPC nodes to route shards. Therefore, you no longer need to specify the access list for these networks, as automated access list generation is now in place.
This document is useful for:
-educational purposes
-situations where the automated access list fails and you need to specify the access list directly.
Where is EIP-2930 Data Located for a Transaction?
The access list transaction parameter is where the EIP-2930 address and storage slot data go.
How do I Define an AccessList for an EIP-2930 Transaction?
Based on the EIP-2930 specification, the general syntax should be:
-the address [20 bytes]
-then the storage slots being accessed at that address [32 bytes]
Example:
[
[
"0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae",
[
"0x0000000000000000000000000000000000000000000000000000000000000003",
"0x0000000000000000000000000000000000000000000000000000000000000007"
]
],
[
"0xbb9bc244d798123fde783fcc1c72d3bb8c189413",
[]
]
]
EIP-2930 Optional:
Transfer SHM on Shardeum Between Wallets:
Send an EIP-2930 transaction with an access list address that has no storage.
- Javascript
const Web3 = require('web3')
const ethers = require("ethers")
const rpcURL = "https://dapps.shardeum.org"
const web3 = new Web3(rpcURL)
const provider = new ethers.providers.JsonRpcProvider(rpcURL)
const signer = new ethers.Wallet(Buffer.from(process.env.devTestnetPrivateKey, 'hex'), provider);
console.log("User wallet address: " + signer.address)
const transferToWallet = new ethers.Wallet(Buffer.from(process.env.devTestnetPrivateKeyTwo, 'hex'), provider);
console.log("transferToWallet address: " + transferToWallet.address)
createAndSendTx();
async function createAndSendTx() {
const chainIdConnected = await web3.eth.getChainId();
console.log("chainIdConnected: "+ chainIdConnected)
const oneEtherInWeiSHM = "1000000000000000000"
console.log("oneEtherInWeiSHM: " + oneEtherInWeiSHM)
const userBalance = await provider.getBalance(signer.address);
console.log("User Balance [Shardeum SHM]" )
console.log(ethers.utils.formatEther(userBalance))
const receiverBalance = await provider.getBalance(transferToWallet.address);
console.log("Receiver Balance [Shardeum SHM]" )
console.log(ethers.utils.formatEther(receiverBalance))
const txCount = await provider.getTransactionCount(signer.address);
const tx = signer.sendTransaction({
chainId: chainIdConnected,
to: transferToWallet.address,
nonce: web3.utils.toHex(txCount),
gasLimit: web3.utils.toHex(300000), // Raise the gas limit to a much higher amount
gasPrice: web3.utils.toHex(web3.utils.toWei('30', 'gwei')),
value: oneEtherInWeiSHM,
type: 1,
accessList: [
{
address: transferToWallet.address,
storageKeys: []
}
]
});
console.log("WAIT FOR TX RECEIPT: ")
await tx
console.log("TX RECEIPT: ")
console.log(tx)
}
- Python
from web3 import Web3
import json
import os
import time
ShardeumConnectionHTTPS = "https://dapps.shardeum.org/";
web3 = Web3(Web3.HTTPProvider(ShardeumConnectionHTTPS))
chainIdConnected = web3.eth.chain_id
print("chainIdConnected: " + str(chainIdConnected))
devTestnetPrivateKey = str(os.environ['devTestnetPrivateKey']);
userWallet = (web3.eth.account.from_key(devTestnetPrivateKey)).address
print("User Wallet Address: " + userWallet)
devTestnetPrivateKeyTwo = str(os.environ['devTestnetPrivateKeyTwo']);
transferToWallet = (web3.eth.account.from_key(devTestnetPrivateKeyTwo)).address
print("transferToWallet address: " + transferToWallet)
oneEtherInWeiSHM = "1000000000000000000"
print("weiMsgValueToSend: " + oneEtherInWeiSHM)
userBalance = web3.eth.getBalance(userWallet);
print("User Balance [Shardeum SHM]" )
print(web3.fromWei(userBalance, "ether"))
receiverBalance = web3.eth.getBalance(transferToWallet);
print("Receiver Balance [Shardeum SHM]" )
print(web3.fromWei(receiverBalance, "ether"))
transferTx = {
'chainId' : chainIdConnected,
'nonce': web3.eth.getTransactionCount(userWallet) ,
'to': transferToWallet, #WORKS WITH REGULAR WALLETS BUT CANNOT SEND TO CONTRACT FOR SOME REASON?
'gas': 2100000, #WORKS WITH 1000000. If not try : Remix > deploy and run transactions
'gasPrice': web3.toWei('30', 'gwei'), # https://etherscan.io/gastracker
'value': int(oneEtherInWeiSHM),
'accessList' :
[
{
"address" : transferToWallet,
"storageKeys": []
}
]
}
signed_tx = web3.eth.account.signTransaction(transferTx, devTestnetPrivateKey)
tx_hash = web3.toHex(web3.eth.sendRawTransaction(signed_tx.rawTransaction))
print("TX HASH: " + tx_hash)
time.sleep(15)
receipt = web3.eth.getTransactionReceipt(tx_hash)
print("TX RECEIPT: " + str(receipt) )
Contracts contractToCall and Multicall:
- Solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract contractToCall {
uint public slot0; //uint is 32 bytes and fills a 32 byte slot. //Do not set 0 manually it wastes gas!
function set(uint x) public {
slot0 = x;
}
}
contract Multicall {
contractToCall public callContractToCall;
constructor(address setCallOne) {
callContractToCall = contractToCall(setCallOne);
}
function multiCallRead() public view returns(uint) {
return callContractToCall.slot0();
}
function multiCallWrite(uint x) public {
callContractToCall.set(x);
}
}
contractToCall (Single Address)
Send an EIP-2930 transaction with an access list. The access list contains the contract’s address and accessed storage slot (or slots). In this case, it will be storage slot 0 because it is a single uint storage variable (uint = 256 bits = 32 bytes) which is modified when we call “set(uint)”.
- Javascript
const Web3 = require('web3')
const ethers = require("ethers")
const rpcURL = "https://dapps.shardeum.org/"
const web3 = new Web3(rpcURL)
const provider = new ethers.providers.JsonRpcProvider(rpcURL)
const signer = new ethers.Wallet(Buffer.from(process.env.devTestnetPrivateKey, 'hex'), provider);
console.log("User wallet address: " + signer.address)
const simpleStorageAddress = '0xE8eb488bEe284ed5b9657D5fc928f90F40BC2d57'
const simpleStorageABI = [{"inputs":[{"internalType":"uint256","name":"x","type":"uint256"}],"name":"set","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]
const simpleStorageDeployed = new web3.eth.Contract(simpleStorageABI, simpleStorageAddress)
createAndSendTx();
async function createAndSendTx() {
const chainIdConnected = await web3.eth.getChainId();
console.log("chainIdConnected: "+ chainIdConnected)
const slot0 = await simpleStorageDeployed.methods.slot0().call()
console.log("slot0: "+ slot0)
const unixTime = Date.now();
console.log("UNIX TIME: " + unixTime)
const txCount = await provider.getTransactionCount(signer.address);
const tx = signer.sendTransaction({
chainId: chainIdConnected,
to: simpleStorageAddress,
nonce: web3.utils.toHex(txCount),
gasLimit: web3.utils.toHex(300000), // Raise the gas limit to a much higher amount
gasPrice: web3.utils.toHex(web3.utils.toWei('30', 'gwei')),
data: simpleStorageDeployed.methods.set(unixTime).encodeABI(),
type: 1,
accessList: [
{
address: simpleStorageAddress,
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000000",
]
}
]
});
console.log("WAIT FOR TX RECEIPT: ")
await tx
console.log("TX RECEIPT: ")
console.log(tx)
}
- Python
from web3 import Web3
import json
import os
import math
import time
ShardeumConnectionHTTPS = "https://dapps.shardeum.org/";
web3 = Web3(Web3.HTTPProvider(ShardeumConnectionHTTPS))
chainIdConnected = web3.eth.chain_id
print("chainIdConnected: " + str(chainIdConnected))
devTestnetPrivateKey = str(os.environ['devTestnetPrivateKey']);
userWallet = (web3.eth.account.from_key(devTestnetPrivateKey)).address
print("User Wallet Address: " + userWallet)
Contract_At_Address= web3.toChecksumAddress("0xE8eb488bEe284ed5b9657D5fc928f90F40BC2d57");
abi_Contract = json.loads('[{"inputs":[{"internalType":"uint256","name":"x","type":"uint256"}],"name":"set","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]')
contract_Call = web3.eth.contract(address = Contract_At_Address , abi = abi_Contract);
print(contract_Call.functions.slot0().call());
unixTime = int(math.floor( time.time()*(10**3)) )
print("UNIX TIME: " + str(unixTime) )
EIP_2930_tx = {
'chainId' : chainIdConnected,
'nonce': web3.eth.getTransactionCount(userWallet) ,
'to': Contract_At_Address, #WORKS WITH REGULAR WALLETS BUT CANNOT SEND TO CONTRACT FOR SOME REASON?
'gas': 2100000, #WORKS WITH 1000000. If not try : Remix > deploy and run transactions
'gasPrice': web3.toWei('30', 'gwei'), # https://etherscan.io/gastracker
'data' : contract_Call.encodeABI(fn_name='set', args=[unixTime]) ,
'accessList' :
[
{
"address" : Contract_At_Address,
"storageKeys": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
]
}
]
}
signed_tx = web3.eth.account.signTransaction(EIP_2930_tx, devTestnetPrivateKey)
tx_hash = web3.toHex(web3.eth.sendRawTransaction(signed_tx.rawTransaction))
print("TX HASH: " + tx_hash)
time.sleep(15)
receipt = web3.eth.getTransactionReceipt(tx_hash)
print("TX RECEIPT: " + str(receipt) )
Multicall Storage Read:
Reading contract states cross shard does not require an access list.
For example, ERC-20 multicall:
- Solidity
tokenObject.totalSupply()
It will work with no access list cross shard.
EIP-2930 Required:
Multicall Storage Write:
Writing contract states cross shard necessitates an access list.
For example, ERC-20 multicall:
- Solidity
tokenObject.transfer(recipient, amount)
will require an access list to function cross shard.
Contract Multicall can modify states in other contracts (in this case, contractToCall). For sharded Shardeum networks (like Liberty 2.X and Sphinx), we need to specify the addresses and storage slots being called outside of “from” and “to” in the transaction.
Sphinx Dapp Address codeHash in Storage Slots:
Sphinx (betanet) will not require the codeHash in storage slots for each corresponding externally called address.
In Solidity, you can obtain an address codeHash from a deployed contract on the matching network [along with checking if an address is a contract]. You can also obtain an address codeHash with the ethers library.
Solidity codeHash Example:
- Solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract addressCodeHash { //From https://soliditydeveloper.com/extcodehash
function getCodeHash(address account) public view returns (bytes32) {
bytes32 codeHash;
assembly { codeHash := extcodehash(account) }
return (codeHash);
}
function isContractBasedOnHash(address account) public view returns (bool) {
bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
bytes32 codeHash;
assembly { codeHash := extcodehash(account) }
return (codeHash != accountHash && codeHash != 0x0);
}
function isContractBasedOnSize(address addr) public view returns (bool) {
uint size;
assembly { size := extcodesize(addr) }
return size > 0;
}
}
EIP-2930 accessList Transactions for Multicall Contract to Modify slot0 in contractToCall:
- Javascript
const Web3 = require('web3')
const ethers = require("ethers")
const rpcURL = "https://dapps.shardeum.org/"
const web3 = new Web3(rpcURL)
const provider = new ethers.providers.JsonRpcProvider(rpcURL)
const signer = new ethers.Wallet(Buffer.from(process.env.devTestnetPrivateKey, 'hex'), provider);
console.log("User wallet address: " + signer.address)
const contractAddress_JS = '0xb1fEf690f84241738b188eF8b88e52B2cc59AbD2'
const contractABI_JS = [{"inputs":[{"internalType":"uint256","name":"x","type":"uint256"}],"name":"multiCallWrite","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"setCallOne","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"callContractToCall","outputs":[{"internalType":"contractcontractToCall","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"multiCallRead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]
const contractDefined_JS = new web3.eth.Contract(contractABI_JS, contractAddress_JS)
createAndSendTx();
async function createAndSendTx() {
const chainIdConnected = await web3.eth.getChainId();
console.log("chainIdConnected: "+ chainIdConnected)
const slot0 = await contractDefined_JS.methods.multiCallRead().call()
console.log("slot0: "+ slot0)
const contractOneAddress = await contractDefined_JS.methods.callContractToCall().call()
console.log("contractOneAddress: "+ contractOneAddress)
const codeHash = await provider.getCode(contractOneAddress)
console.log("contractOneAddress codeHash: " + codeHash)
const unixTime = Date.now();
console.log("UNIX TIME: " + unixTime)
const txCount = await provider.getTransactionCount(signer.address);
const tx = signer.sendTransaction({
chainId: chainIdConnected,
to: contractAddress_JS,
nonce: web3.utils.toHex(txCount),
gasLimit: web3.utils.toHex(2100000), // Raise the gas limit to a much higher amount
gasPrice: web3.utils.toHex(web3.utils.toWei('30', 'gwei')),
data: contractDefined_JS.methods.multiCallWrite(unixTime).encodeABI(),
type: 1,
accessList: [
{
address: contractOneAddress, //Contract address we are calling from the "to" contract at some point.
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000000",
codeHash, //Code hash from EXTCODEHASH https://blog.finxter.com/how-to-find-out-if-an-ethereum-address-is-a-contract/
]
}
]
});
console.log("WAIT FOR TX RECEIPT: ")
await tx
console.log("TX RECEIPT: ")
console.log(tx)
}
- Python
from web3 import Web3
import json
import os
import time
import math
ShardeumConnectionHTTPS = "https://dapps.shardeum.org/";
web3 = Web3(Web3.HTTPProvider(ShardeumConnectionHTTPS))
chainIdConnected = web3.eth.chain_id
print("chainIdConnected: " + str(chainIdConnected))
devTestnetPrivateKey = str(os.environ['devTestnetPrivateKey']);
userWallet = (web3.eth.account.from_key(devTestnetPrivateKey)).address
print("User Wallet Address: " + userWallet)
multicallContractAddress= web3.toChecksumAddress("0xb1fEf690f84241738b188eF8b88e52B2cc59AbD2");
multicallContractABI = json.loads('[{"inputs":[{"internalType":"uint256","name":"x","type":"uint256"}],"name":"multiCallWrite","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"setCallOne","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"callContractToCall","outputs":[{"internalType":"contractcontractToCall","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"multiCallRead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]')
multicallContractDeployed = web3.eth.contract(address = multicallContractAddress , abi = multicallContractABI);
contractOneAddress = multicallContractDeployed.functions.callContractToCall().call()
print("contractOneAddress: "+contractOneAddress)
slot0 = multicallContractDeployed.functions.multiCallRead().call()
print("slot0: "+ str(slot0) )
codeHashBytes32 = (web3.eth.get_code(contractOneAddress))
codeHashString = codeHashBytes32.hex()
print("contractOneAddress codeHash: " + codeHashString )
unixTime = int(math.floor( time.time()*(10**3)) )
print("UNIX TIME: " + str(unixTime) )
EIP_2930_tx = {
'chainId' : chainIdConnected,
'to': multicallContractAddress, #WORKS WITH REGULAR WALLETS BUT CANNOT SEND TO CONTRACT FOR SOME REASON?
'nonce': web3.eth.getTransactionCount(userWallet) ,
'gas': 2100000, #WORKS WITH 1000000. If not try : Remix > deploy and run transactions
'gasPrice': web3.toWei('30', 'gwei'), # https://etherscan.io/gastracker
'data' : multicallContractDeployed.encodeABI(fn_name='multiCallWrite', args=[unixTime]) ,
'type' : 1,
'accessList' :
[
{
"address" : contractOneAddress,
"storageKeys": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
codeHashString ##Code hash from EXTCODEHASH https://blog.finxter.com/how-to-find-out-if-an-ethereum-address-is-a-contract/
]
}
]
}
signed_tx = web3.eth.account.signTransaction(EIP_2930_tx, devTestnetPrivateKey)
tx_hash = web3.toHex(web3.eth.sendRawTransaction(signed_tx.rawTransaction))
print("TX HASH: " + tx_hash)
time.sleep(15)
receipt = web3.eth.getTransactionReceipt(tx_hash)
print("TX RECEIPT: " + str(receipt) )
How Can I Create an EIP-2930 accessList Easily?
EIP-2930 accessList Simulator
Tool generates accessList with 91% accuracy:
https://github.com/alexchenzl/predict-al
EIP-2930 access list generation for swap transactions can be found in this GitHub repository.
https://github.com/shardeum-globalswap/interface/tree/support-eip2930
Solidity Interfaces
What are Solidity Interfaces?
Solidity interfaces enable smart contracts to interact with each other.
Why are Solidity Interfaces Useful?
Solidity interfaces can:
-Call contracts with different Solidity versions.
-Use functions that are only needed for their use case.
How do I define a Solidity Interface?
The contract interface is defined above the contract that will be calling the contract interface. Functions are then defined within the interface based on name, inputs, and modifiers.
Where are Solidity Interface Functions Defined?
A contract interface instance needs to be created in the contract that will be using the interface. The contract interface is then defined when its address is set inside the constructor for the contract using the interface.
When should I use a Solidity Interface?
One popular contract that is useful with interfaces is WETH (Wrapped Ether). This contract converts MSG.VALUE into an ERC-20 instance. On Shardeum, SHM is MSG.VALUE. Therefore, WETH is WSHM on Shardeum. This contract has wrap and unwrap functions:
Deposit (Wrap): SHM => WSHM.
Withdraw (Unwrap): WSHM => SHM.
Using a WSHM interface, we can create a Solidity 0.8.0 contract which can interact with WSHM (Solidity 0.4.0).
WSHM Solidity 0.4.0
- Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.18;
contract WSHM {
string public name = "Wrapped SHM";
string public symbol = "WSHM";
uint8 public decimals = 18;
event Approval(address indexed src, address indexed guy, uint wad);
event Transfer(address indexed src, address indexed dst, uint wad);
event Deposit(address indexed dst, uint wad);
event Withdrawal(address indexed src, uint wad);
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
function() public payable {
deposit();
}
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
Deposit(msg.sender, msg.value);
}
function withdraw(uint wad) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
msg.sender.transfer(wad);
Withdrawal(msg.sender, wad);
}
function totalSupply() public view returns (uint) {
return this.balance;
}
function approve(address guy, uint wad) public returns (bool) {
allowance[msg.sender][guy] = wad;
Approval(msg.sender, guy, wad);
return true;
}
function transfer(address dst, uint wad) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}
function transferFrom(address src, address dst, uint wad)
public
returns (bool)
{
require(balanceOf[src] >= wad);
if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {
require(allowance[src][msg.sender] >= wad);
allowance[src][msg.sender] -= wad;
}
balanceOf[src] -= wad;
balanceOf[dst] += wad;
Transfer(src, dst, wad);
return true;
}
}
WSHM Interface Solidity 0.8.0 With Multicall Contract
- Solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
interface interfaceWSHM {
function transfer(address dst, uint wad) external returns (bool);
function transferFrom(address src, address dst, uint wad) external returns (bool);
function withdraw(uint wad) external;
function deposit() external payable;
}
contract multicallWSHM {
interfaceWSHM public WSHM;
receive() external payable {}
fallback() external payable {}
constructor() {
WSHM = interfaceWSHM(0xa80d5d2C8Cc0d06fBC1F1A89A05d76f86082C20e); // WSHM address.
}
function multicallDeposit() public payable {
WSHM.deposit{value: msg.value}();
WSHM.transfer(msg.sender, msg.value);
}
function multicallWithdraw(uint256 amount) public {
WSHM.transferFrom(msg.sender,address(this),amount);
WSHM.withdraw(amount);
payable(msg.sender).transfer(address(this).balance);
}
}