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 ## Different Types
* Booleans * [x] Booleans
* Integers * [x] Integers
* Fixed Point Numbers * [ ] Fixed Point Numbers
* Address * [x] Address
* Contract Types * [ ] Contract Types
* Fixed-size byte arrays * [ ] Fixed-size byte arrays
* Enums * [ ] Enums
* Function Types * [ ] Function Types
* Arrays * [ ] Arrays
* Structs * [ ] Structs
* Mapping Types * [ ] Mapping Types
## Observations ## Observations

View File

@ -1,8 +1,7 @@
{ {
"name": "@vulcanize/solidity-mapper", "name": "@vulcanize/solidity-mapper",
"version": "0.1.0", "version": "0.1.0",
"main": "dist/index.js", "main": "src/index.ts",
"types": "dist/index.d.ts",
"license": "UNLICENSED", "license": "UNLICENSED",
"devDependencies": { "devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
@ -10,7 +9,16 @@
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
"@types/node": "^15.6.1", "@types/node": "^15.6.1",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"chai": "^4.3.4", "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", "ethereum-waffle": "^3.3.0",
"ethers": "^5.2.0", "ethers": "^5.2.0",
"hardhat": "^2.3.0", "hardhat": "^2.3.0",
@ -19,6 +27,7 @@
}, },
"scripts": { "scripts": {
"build": "tsc", "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'; 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 { export interface StorageLayout {
storage: [{ storage: Storage[];
slot: string; types: { [type: string]: Type; }
offset: number; }
type: string;
label: string; export interface StorageInfo extends Storage {
}]; types: { [type: string]: Type; }
types: {
[type: string]: {
encoding: string;
numberOfBytes: string;
label: string;
}
};
} }
export type GetStorageAt = (address: string, position: string) => Promise<string> 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. * Function to get the value from storage for a contract variable.
* @param address * @param address
@ -26,15 +53,7 @@ export type GetStorageAt = (address: string, position: string) => Promise<string
* @param variableName * @param variableName
*/ */
export const getStorageValue = async (address: string, storageLayout: StorageLayout, getStorageAt: GetStorageAt, variableName: string): Promise<number | string | boolean | undefined> => { export const getStorageValue = async (address: string, storageLayout: StorageLayout, getStorageAt: GetStorageAt, variableName: string): Promise<number | string | boolean | undefined> => {
const { storage, types } = storageLayout; const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName);
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 { encoding, numberOfBytes, label } = types[type] const { encoding, numberOfBytes, label } = types[type]
// Get value according to encoding i.e. how the data is encoded in storage. // 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 * @param getStorageAt
*/ */
const getInplaceArray = async (address: string, slot: string, offset: number, numberOfBytes: string, getStorageAt: 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); const uintArray = utils.arrayify(value);
// Get value according to offset. // Get value according to offset.
@ -97,7 +116,7 @@ const getInplaceArray = async (address: string, slot: string, offset: number, nu
* @param getStorageAt * @param getStorageAt
*/ */
const getBytesArray = async (address: string, slot: string, getStorageAt: 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); const uintArray = utils.arrayify(value);
let length = 0; 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. // Compute zero padded hexstring to calculate hashed position of storage.
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242 // https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
const slotHex = utils.hexZeroPad(BigNumber.from(slot).toHexString(), 32); const paddedSlotHex = utils.hexZeroPad(slot, 32);
const position = utils.keccak256(slotHex); const position = utils.keccak256(paddedSlotHex);
// Get value from consecutive storage slots for longer data. // Get value from consecutive storage slots for longer data.
for(let i = 0; i < length / 32; i++) { for(let i = 0; i < length / 32; i++) {

View File

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

View File

@ -22,6 +22,7 @@
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@vulcanize/cache": "^0.1.0", "@vulcanize/cache": "^0.1.0",
"@vulcanize/ipld-eth-client": "^0.1.0", "@vulcanize/ipld-eth-client": "^0.1.0",
"@vulcanize/solidity-mapper": "^0.1.0",
"apollo-type-bigint": "^0.1.3", "apollo-type-bigint": "^0.1.3",
"canonical-json": "^0.0.4", "canonical-json": "^0.0.4",
"debug": "^4.3.1", "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 { getCache } from '@vulcanize/cache';
import { EthClient, getMappingSlot, topictoAddress } from '@vulcanize/ipld-eth-client'; import { EthClient, getMappingSlot, topictoAddress } from '@vulcanize/ipld-eth-client';
import { getStorageInfo } from '@vulcanize/solidity-mapper';
// Event slots. import { storageLayout } from './artifacts/ERC20.json';
// TODO: Read from storage layout file.
const ERC20_BALANCE_OF_SLOT = "0x00";
const ERC20_ALLOWANCE_SLOT = "0x01";
// Event signatures. // Event signatures.
// TODO: Generate from ABI. // TODO: Generate from ABI.
@ -55,7 +53,8 @@ export const createResolvers = async (config) => {
balanceOf: async (_, { blockHash, token, owner }) => { balanceOf: async (_, { blockHash, token, owner }) => {
log('balanceOf', 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 = { const vars = {
blockHash, blockHash,
@ -89,7 +88,8 @@ export const createResolvers = async (config) => {
allowance: async (_, { blockHash, token, owner, spender }) => { allowance: async (_, { blockHash, token, owner, spender }) => {
log('allowance', 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 = { const vars = {
blockHash, blockHash,

View File

@ -66,6 +66,7 @@
/* Advanced Options */ /* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */ "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