Get slot for ERC20 variable from storage layout (#13)

* Get slot for ERC20 variable from storage layout.

* Fix solidity-mapper build for importing library functions.

* Implement lint command in solidity-mapper package.

Co-authored-by: nikugogoi <95nikass@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-05-31 14:50:05 +05:30 committed by GitHub
parent 72ca980198
commit a0aae09f83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1182 additions and 70 deletions

View File

@ -0,0 +1,5 @@
# Don't lint node_modules.
node_modules
# Don't lint build output.
dist

View File

@ -0,0 +1,20 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"semistandard",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}

View File

@ -17,17 +17,17 @@ $ yarn test
## Different Types
* Booleans
* Integers
* Fixed Point Numbers
* Address
* Contract Types
* Fixed-size byte arrays
* Enums
* Function Types
* Arrays
* Structs
* Mapping Types
* [x] Booleans
* [x] Integers
* [ ] Fixed Point Numbers
* [x] Address
* [ ] Contract Types
* [ ] Fixed-size byte arrays
* [ ] Enums
* [ ] Function Types
* [ ] Arrays
* [ ] Structs
* [ ] Mapping Types
## Observations

View File

@ -1,8 +1,7 @@
{
"name": "@vulcanize/solidity-mapper",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "src/index.ts",
"license": "UNLICENSED",
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.2",
@ -10,7 +9,16 @@
"@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2",
"@types/node": "^15.6.1",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"chai": "^4.3.4",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.2.0",
"hardhat": "^2.3.0",
@ -19,6 +27,7 @@
},
"scripts": {
"build": "tsc",
"test": "hardhat test"
"test": "hardhat test",
"lint": "eslint ."
}
}

View File

@ -1 +1 @@
export { getStorageValue, StorageLayout, GetStorageAt } from './storage';
export { getStorageValue, getStorageInfo, StorageLayout, GetStorageAt } from './storage';

View File

@ -1,23 +1,50 @@
import { utils, BigNumber } from 'ethers';
interface Storage {
slot: string;
offset: number;
type: string;
label: string;
}
interface Type {
encoding: string;
numberOfBytes: string;
label: string;
}
export interface StorageLayout {
storage: [{
slot: string;
offset: number;
type: string;
label: string;
}];
types: {
[type: string]: {
encoding: string;
numberOfBytes: string;
label: string;
}
};
storage: Storage[];
types: { [type: string]: Type; }
}
export interface StorageInfo extends Storage {
types: { [type: string]: Type; }
}
export type GetStorageAt = (address: string, position: string) => Promise<string>
/**
* Function to get storage information of variable from storage layout.
* @param storageLayout
* @param variableName
*/
export const getStorageInfo = (storageLayout: StorageLayout, variableName: string): StorageInfo => {
const { storage, types } = storageLayout;
const targetState = storage.find((state) => state.label === variableName)
// Throw if state variable could not be found in storage layout.
if (!targetState) {
throw new Error('Variable not present in storage layout.');
}
return {
...targetState,
slot: utils.hexlify(BigNumber.from(targetState.slot)),
types
}
}
/**
* Function to get the value from storage for a contract variable.
* @param address
@ -26,15 +53,7 @@ export type GetStorageAt = (address: string, position: string) => Promise<string
* @param variableName
*/
export const getStorageValue = async (address: string, storageLayout: StorageLayout, getStorageAt: GetStorageAt, variableName: string): Promise<number | string | boolean | undefined> => {
const { storage, types } = storageLayout;
const targetState = storage.find((state) => state.label === variableName)
// Return if state variable could not be found in storage layout.
if (!targetState) {
return;
}
const { slot, offset, type } = targetState;
const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName);
const { encoding, numberOfBytes, label } = types[type]
// Get value according to encoding i.e. how the data is encoded in storage.
@ -79,7 +98,7 @@ export const getStorageValue = async (address: string, storageLayout: StorageLay
* @param getStorageAt
*/
const getInplaceArray = async (address: string, slot: string, offset: number, numberOfBytes: string, getStorageAt: GetStorageAt) => {
const value = await getStorageAt(address, BigNumber.from(slot).toHexString());
const value = await getStorageAt(address, slot);
const uintArray = utils.arrayify(value);
// Get value according to offset.
@ -97,7 +116,7 @@ const getInplaceArray = async (address: string, slot: string, offset: number, nu
* @param getStorageAt
*/
const getBytesArray = async (address: string, slot: string, getStorageAt: GetStorageAt) => {
let value = await getStorageAt(address, BigNumber.from(slot).toHexString());
let value = await getStorageAt(address, slot);
const uintArray = utils.arrayify(value);
let length = 0;
@ -122,8 +141,8 @@ const getBytesArray = async (address: string, slot: string, getStorageAt: GetSto
// Compute zero padded hexstring to calculate hashed position of storage.
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
const slotHex = utils.hexZeroPad(BigNumber.from(slot).toHexString(), 32);
const position = utils.keccak256(slotHex);
const paddedSlotHex = utils.hexZeroPad(slot, 32);
const position = utils.keccak256(paddedSlotHex);
// Get value from consecutive storage slots for longer data.
for(let i = 0; i < length / 32; i++) {

View File

@ -70,5 +70,5 @@
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": ["src/**/*"],
"exclude": ["node_modules"],
"exclude": ["src/**/*.test.ts"]
}

View File

@ -22,6 +22,7 @@
"@types/lodash": "^4.14.168",
"@vulcanize/cache": "^0.1.0",
"@vulcanize/ipld-eth-client": "^0.1.0",
"@vulcanize/solidity-mapper": "^0.1.0",
"apollo-type-bigint": "^0.1.3",
"canonical-json": "^0.0.4",
"debug": "^4.3.1",

View File

@ -0,0 +1,365 @@
{
"abi": [
{
"inputs": [
{
"internalType": "string",
"name": "name_",
"type": "string"
},
{
"internalType": "string",
"name": "symbol_",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "subtractedValue",
"type": "uint256"
}
],
"name": "decreaseAllowance",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "addedValue",
"type": "uint256"
}
],
"name": "increaseAllowance",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
],
"storageLayout": {
"storage": [
{
"astId": 15,
"contract": "@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20",
"label": "_balances",
"offset": 0,
"slot": "0",
"type": "t_mapping(t_address,t_uint256)"
},
{
"astId": 21,
"contract": "@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20",
"label": "_allowances",
"offset": 0,
"slot": "1",
"type": "t_mapping(t_address,t_mapping(t_address,t_uint256))"
},
{
"astId": 23,
"contract": "@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20",
"label": "_totalSupply",
"offset": 0,
"slot": "2",
"type": "t_uint256"
},
{
"astId": 25,
"contract": "@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20",
"label": "_name",
"offset": 0,
"slot": "3",
"type": "t_string_storage"
},
{
"astId": 27,
"contract": "@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20",
"label": "_symbol",
"offset": 0,
"slot": "4",
"type": "t_string_storage"
}
],
"types": {
"t_address": {
"encoding": "inplace",
"label": "address",
"numberOfBytes": "20"
},
"t_mapping(t_address,t_mapping(t_address,t_uint256))": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => mapping(address => uint256))",
"numberOfBytes": "32",
"value": "t_mapping(t_address,t_uint256)"
},
"t_mapping(t_address,t_uint256)": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => uint256)",
"numberOfBytes": "32",
"value": "t_uint256"
},
"t_string_storage": {
"encoding": "bytes",
"label": "string",
"numberOfBytes": "32"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
}
}
}
}

View File

@ -4,11 +4,9 @@ import debug from 'debug';
import { getCache } from '@vulcanize/cache';
import { EthClient, getMappingSlot, topictoAddress } from '@vulcanize/ipld-eth-client';
import { getStorageInfo } from '@vulcanize/solidity-mapper';
// Event slots.
// TODO: Read from storage layout file.
const ERC20_BALANCE_OF_SLOT = "0x00";
const ERC20_ALLOWANCE_SLOT = "0x01";
import { storageLayout } from './artifacts/ERC20.json';
// Event signatures.
// TODO: Generate from ABI.
@ -55,7 +53,8 @@ export const createResolvers = async (config) => {
balanceOf: async (_, { blockHash, token, owner }) => {
log('balanceOf', blockHash, token, owner);
const slot = getMappingSlot(ERC20_BALANCE_OF_SLOT, owner);
const { slot: balancesSlot } = getStorageInfo(storageLayout, '_balances')
const slot = getMappingSlot(balancesSlot, owner);
const vars = {
blockHash,
@ -89,7 +88,8 @@ export const createResolvers = async (config) => {
allowance: async (_, { blockHash, token, owner, spender }) => {
log('allowance', blockHash, token, owner, spender);
const slot = getMappingSlot(getMappingSlot(ERC20_ALLOWANCE_SLOT, owner), spender);
const { slot: allowancesSlot } = getStorageInfo(storageLayout, '_allowances')
const slot = getMappingSlot(getMappingSlot(allowancesSlot, owner), spender);
const vars = {
blockHash,

View File

@ -66,6 +66,7 @@
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"resolveJsonModule": true /* Enabling the option allows importing JSON, and validating the types in that JSON file. */
}
}

734
yarn.lock

File diff suppressed because it is too large Load Diff