Use getStorageValue to parse value for string type variable (#29)

* Get string value of variable name.

* Use getStorageValue to parse value for string type variable.

Co-authored-by: nikugogoi <95nikass@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-06-03 11:52:23 +05:30 committed by GitHub
parent b243025ca8
commit 00eb129536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 191 additions and 125 deletions

View File

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

View File

@ -74,21 +74,29 @@ it('get storage information', async function () {
});
describe('Get value from storage', function () {
const getBlockHash = async () => {
const blockNumber = await ethers.provider.getBlockNumber();
const { hash } = await ethers.provider.getBlock(blockNumber);
return hash;
};
it('get value for integer type variables packed together', async function () {
const Integers = await ethers.getContractFactory('TestIntegers');
const integers = await Integers.deploy();
await integers.deployed();
const storageLayout = await getStorageLayout('TestIntegers');
let value = 12;
await integers.setInt1(value);
let storageValue = await getStorageValue(integers.address, storageLayout, getStorageAt, 'int1');
expect(storageValue).to.equal(value);
let expectedValue = 12;
await integers.setInt1(expectedValue);
let blockHash = await getBlockHash();
let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, integers.address, 'int1');
expect(value).to.equal(expectedValue);
value = 34;
await integers.setInt2(value);
storageValue = await getStorageValue(integers.address, storageLayout, getStorageAt, 'int2');
expect(storageValue).to.equal(value);
expectedValue = 34;
await integers.setInt2(expectedValue);
blockHash = await getBlockHash();
({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, integers.address, 'int2'));
expect(value).to.equal(expectedValue);
});
it('get value for integer type variables using single slot', async function () {
@ -97,10 +105,11 @@ describe('Get value from storage', function () {
await integers.deployed();
const storageLayout = await getStorageLayout('TestIntegers');
const value = 123;
await integers.setInt3(value);
const storageValue = await getStorageValue(integers.address, storageLayout, getStorageAt, 'int3');
expect(storageValue).to.equal(value);
const expectedValue = 123;
await integers.setInt3(expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, integers.address, 'int3');
expect(value).to.equal(expectedValue);
});
it('get value for unsigned integer type variables packed together', async function () {
@ -109,15 +118,17 @@ describe('Get value from storage', function () {
await unsignedIntegers.deployed();
const storageLayout = await getStorageLayout('TestUnsignedIntegers');
let value = 12;
await unsignedIntegers.setUint1(value);
let storageValue = await getStorageValue(unsignedIntegers.address, storageLayout, getStorageAt, 'uint1');
expect(storageValue).to.equal(value);
let expectedValue = 12;
await unsignedIntegers.setUint1(expectedValue);
let blockHash = await getBlockHash();
let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, unsignedIntegers.address, 'uint1');
expect(value).to.equal(expectedValue);
value = 34;
await unsignedIntegers.setUint2(value);
storageValue = await getStorageValue(unsignedIntegers.address, storageLayout, getStorageAt, 'uint2');
expect(storageValue).to.equal(value);
expectedValue = 34;
await unsignedIntegers.setUint2(expectedValue);
blockHash = await getBlockHash();
({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, unsignedIntegers.address, 'uint2'));
expect(value).to.equal(expectedValue);
});
it('get value for unsigned integer type variables using single slot', async function () {
@ -126,10 +137,11 @@ describe('Get value from storage', function () {
await unsignedIntegers.deployed();
const storageLayout = await getStorageLayout('TestUnsignedIntegers');
const value = 123;
await unsignedIntegers.setUint3(value);
const storageValue = await getStorageValue(unsignedIntegers.address, storageLayout, getStorageAt, 'uint3');
expect(storageValue).to.equal(value);
const expectedValue = 123;
await unsignedIntegers.setUint3(expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, unsignedIntegers.address, 'uint3');
expect(value).to.equal(expectedValue);
});
it('get value for boolean type', async function () {
@ -138,15 +150,17 @@ describe('Get value from storage', function () {
await booleans.deployed();
const storageLayout = await getStorageLayout('TestBooleans');
let value = true;
await booleans.setBool1(value);
let storageValue = await getStorageValue(booleans.address, storageLayout, getStorageAt, 'bool1');
expect(storageValue).to.equal(value);
let expectedValue = true;
await booleans.setBool1(expectedValue);
let blockHash = await getBlockHash();
let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, booleans.address, 'bool1');
expect(value).to.equal(expectedValue);
value = false;
await booleans.setBool2(value);
storageValue = await getStorageValue(booleans.address, storageLayout, getStorageAt, 'bool2');
expect(storageValue).to.equal(value);
expectedValue = false;
await booleans.setBool2(expectedValue);
blockHash = await getBlockHash();
({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, booleans.address, 'bool2'));
expect(value).to.equal(expectedValue);
});
it('get value for address type', async function () {
@ -157,9 +171,10 @@ describe('Get value from storage', function () {
const [signer] = await ethers.getSigners();
await address.setAddress1(signer.address);
const storageValue = await getStorageValue(address.address, storageLayout, getStorageAt, 'address1');
expect(storageValue).to.be.a('string');
expect(String(storageValue).toLowerCase()).to.equal(signer.address.toLowerCase());
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, address.address, 'address1');
expect(value).to.be.a('string');
expect(String(value).toLowerCase()).to.equal(signer.address.toLowerCase());
});
it('get value for contract type', async function () {
@ -175,8 +190,9 @@ describe('Get value from storage', function () {
const storageLayout = await getStorageLayout('TestContractTypes');
await testContractTypes.setAddressContract1(testAddress.address);
const storageValue = await getStorageValue(testContractTypes.address, storageLayout, getStorageAt, 'addressContract1');
expect(storageValue).to.equal(testAddress.address.toLowerCase());
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testContractTypes.address, 'addressContract1');
expect(value).to.equal(testAddress.address.toLowerCase());
});
it('get value for fixed size byte arrays packed together', async function () {
@ -185,15 +201,17 @@ describe('Get value from storage', function () {
await testBytes.deployed();
const storageLayout = await getStorageLayout('TestBytes');
let value = ethers.utils.hexlify(ethers.utils.randomBytes(10));
await testBytes.setBytesTen(value);
let storageValue = await getStorageValue(testBytes.address, storageLayout, getStorageAt, 'bytesTen');
expect(storageValue).to.equal(value);
let expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(10));
await testBytes.setBytesTen(expectedValue);
let blockHash = await getBlockHash();
let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesTen');
expect(value).to.equal(expectedValue);
value = ethers.utils.hexlify(ethers.utils.randomBytes(20));
await testBytes.setBytesTwenty(value);
storageValue = await getStorageValue(testBytes.address, storageLayout, getStorageAt, 'bytesTwenty');
expect(storageValue).to.equal(value);
expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(20));
await testBytes.setBytesTwenty(expectedValue);
blockHash = await getBlockHash();
({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesTwenty'));
expect(value).to.equal(expectedValue);
});
it('get value for fixed size byte arrays using single slot', async function () {
@ -202,10 +220,11 @@ describe('Get value from storage', function () {
await testBytes.deployed();
const storageLayout = await getStorageLayout('TestBytes');
const value = ethers.utils.hexlify(ethers.utils.randomBytes(30));
await testBytes.setBytesThirty(value);
const storageValue = await getStorageValue(testBytes.address, storageLayout, getStorageAt, 'bytesThirty');
expect(storageValue).to.equal(value);
const expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(30));
await testBytes.setBytesThirty(expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesThirty');
expect(value).to.equal(expectedValue);
});
it('get value for enum types', async function () {
@ -214,10 +233,11 @@ describe('Get value from storage', function () {
await testEnums.deployed();
const storageLayout = await getStorageLayout('TestEnums');
const value = 1;
await testEnums.setChoicesEnum1(value);
const storageValue = await getStorageValue(testEnums.address, storageLayout, getStorageAt, 'choicesEnum1');
expect(storageValue).to.equal(value);
const expectedValue = 1;
await testEnums.setChoicesEnum1(expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testEnums.address, 'choicesEnum1');
expect(value).to.equal(expectedValue);
});
describe('string type', function () {
@ -231,17 +251,19 @@ describe('Get value from storage', function () {
});
it('get value for string length less than 32 bytes', async function () {
const value = 'Hello world.';
await strings.setString1(value);
const storageValue = await getStorageValue(strings.address, storageLayout, getStorageAt, 'string1');
expect(storageValue).to.equal(value);
const expectedValue = 'Hello world.';
await strings.setString1(expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, strings.address, 'string1');
expect(value).to.equal(expectedValue);
});
it('get value for string length more than 32 bytes', async function () {
const value = 'This sentence is more than 32 bytes long.';
await strings.setString2(value);
const storageValue = await getStorageValue(strings.address, storageLayout, getStorageAt, 'string2');
expect(storageValue).to.equal(value);
const expectedValue = 'This sentence is more than 32 bytes long.';
await strings.setString2(expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, strings.address, 'string2');
expect(value).to.equal(expectedValue);
});
});
});

View File

@ -22,7 +22,7 @@ export interface StorageInfo extends Storage {
types: { [type: string]: Type; }
}
export type GetStorageAt = (address: string, position: string) => Promise<string>
export type GetStorageAt = (param: { blockHash: string, contract: string, slot: string }) => Promise<{ value: string, proof: { data: string } }>
/**
* Function to get storage information of variable from storage layout.
@ -47,96 +47,123 @@ export const getStorageInfo = (storageLayout: StorageLayout, variableName: strin
/**
* Function to get the value from storage for a contract variable.
* @param address
* @param storageLayout
* @param getStorageAt
* @param blockHash
* @param address
* @param variableName
*/
export const getStorageValue = async (address: string, storageLayout: StorageLayout, getStorageAt: GetStorageAt, variableName: string): Promise<number | string | boolean | undefined> => {
/* eslint-disable @typescript-eslint/no-explicit-any */
export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt: GetStorageAt, blockHash: string, address: string, variableName: string): Promise<{ value: any, proof: { data: string } }> => {
const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName);
const { encoding, numberOfBytes, label } = types[type];
const { encoding, numberOfBytes, label: typeLabel } = types[type];
let value: string, proof: { data: string };
// Get value according to encoding i.e. how the data is encoded in storage.
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#json-output
switch (encoding) {
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#layout-of-state-variables-in-storage
case 'inplace': {
const valueArray = await getInplaceArray(address, slot, offset, numberOfBytes, getStorageAt);
// Parse value for boolean type.
if (label === 'bool') {
return !BigNumber.from(valueArray).isZero();
}
// Parse value for uint/int type.
if (label.match(/^enum|u?int[0-9]+/)) {
return BigNumber.from(valueArray).toNumber();
}
return utils.hexlify(valueArray);
}
case 'inplace':
({ value, proof } = await getInplaceValue(blockHash, address, slot, offset, numberOfBytes, getStorageAt));
break;
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string
case 'bytes': {
const valueArray = await getBytesArray(address, slot, getStorageAt);
return utils.toUtf8String(valueArray);
}
case 'bytes':
({ value, proof } = await getBytesValue(blockHash, address, slot, getStorageAt));
break;
default:
break;
throw new Error(`Encoding ${encoding} not implemented.`);
}
return {
value: getValueByType(value, typeLabel),
proof
};
};
/**
* Function to get array value for inplace encoding.
* Get value according to type described by the label.
* @param storageValue
* @param typeLabel
*/
// getStorageByType
export const getValueByType = (storageValue: string, typeLabel: string): number | string | boolean => {
// Parse value for boolean type.
if (typeLabel === 'bool') {
return !BigNumber.from(storageValue).isZero();
}
// Parse value for uint/int type or enum type.
if (typeLabel.match(/^enum|u?int[0-9]+/)) {
return BigNumber.from(storageValue).toNumber();
}
// Parse value for string type.
if (typeLabel.includes('string')) {
return utils.toUtf8String(storageValue);
}
return storageValue;
};
/**
* Function to get value for inplace encoding.
* @param address
* @param slot
* @param offset
* @param numberOfBytes
* @param getStorageAt
*/
const getInplaceArray = async (address: string, slot: string, offset: number, numberOfBytes: string, getStorageAt: GetStorageAt) => {
const value = await getStorageAt(address, slot);
const uintArray = utils.arrayify(value);
const getInplaceValue = async (blockHash: string, address: string, slot: string, offset: number, numberOfBytes: string, getStorageAt: GetStorageAt) => {
const { value, proof } = await getStorageAt({ blockHash, contract: address, slot });
const valueLength = utils.hexDataLength(value);
// Get value according to offset.
const start = uintArray.length - (offset + Number(numberOfBytes));
const end = uintArray.length - offset;
const offsetArray = uintArray.slice(start, end);
const start = valueLength - (offset + Number(numberOfBytes));
const end = valueLength - offset;
return offsetArray;
return {
value: utils.hexDataSlice(value, start, end),
proof
};
};
/**
* Function to get array value for bytes encoding.
* Function to get value for bytes encoding.
* @param address
* @param slot
* @param getStorageAt
*/
const getBytesArray = async (address: string, slot: string, getStorageAt: GetStorageAt) => {
const value = await getStorageAt(address, slot);
const uintArray = utils.arrayify(value);
const getBytesValue = async (blockHash: string, address: string, slot: string, getStorageAt: GetStorageAt) => {
const { value, proof } = await getStorageAt({ blockHash, contract: address, slot });
let length = 0;
// Get length of bytes stored.
if (BigNumber.from(uintArray[0]).isZero()) {
if (BigNumber.from(utils.hexDataSlice(value, 0, 1)).isZero()) {
// If first byte is not set, get length directly from the zero padded byte array.
const slotValue = BigNumber.from(value);
length = slotValue.sub(1).div(2).toNumber();
} else {
// If first byte is set the length is lesser than 32 bytes.
// Length of the value can be computed from the last byte.
length = BigNumber.from(uintArray[uintArray.length - 1]).div(2).toNumber();
const lastByteHex = utils.hexDataSlice(value, 31, 32);
length = BigNumber.from(lastByteHex).div(2).toNumber();
}
// Get value from the byte array directly if length is less than 32.
if (length < 32) {
return uintArray.slice(0, length);
return {
value: utils.hexDataSlice(value, 0, length),
proof
};
}
// Array to hold multiple bytes32 data.
const values = [];
const proofs = [
JSON.parse(proof.data)
];
// Compute zero padded hexstring to calculate hashed position of storage.
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
@ -145,10 +172,21 @@ const getBytesArray = async (address: string, slot: string, getStorageAt: GetSto
// Get value from consecutive storage slots for longer data.
for (let i = 0; i < length / 32; i++) {
const value = await getStorageAt(address, BigNumber.from(position).add(i).toHexString());
const { value, proof } = await getStorageAt({
blockHash,
contract: address,
slot: BigNumber.from(position).add(i).toHexString()
});
values.push(value);
proofs.push(JSON.parse(proof.data));
}
// Slice trailing bytes according to length of value.
return utils.concat(values).slice(0, length);
return {
value: utils.hexDataSlice(utils.hexConcat(values), 0, length),
proof: {
data: JSON.stringify(proofs)
}
};
};

View File

@ -51,8 +51,18 @@ export const getStorageLayout = async (contractName: string): Promise<StorageLay
* @param address
* @param position
*/
export const getStorageAt: GetStorageAt = async (address, position) => {
const value = await ethers.provider.getStorageAt(address, position);
export const getStorageAt: GetStorageAt = async ({ blockHash, contract, slot }) => {
// TODO: Fix use of blockHash as hex string in getStorageAt.
// Using blockHash in getStorageAt throws error.
// https://github.com/ethers-io/ethers.js/pull/1550#issuecomment-841746994
// Using latest tag for temporary fix.
blockHash = 'latest';
const value = await ethers.provider.getStorageAt(contract, slot, blockHash);
return value;
return {
value,
proof: {
data: JSON.stringify(null)
}
};
};

View File

@ -3,7 +3,7 @@ import debug from 'debug';
import { Connection } from "typeorm";
import { invert } from "lodash";
import { EthClient, getMappingSlot, topictoAddress } from "@vulcanize/ipld-eth-client";
import { getStorageInfo, getEventNameTopics } from '@vulcanize/solidity-mapper';
import { getStorageInfo, getEventNameTopics, getStorageValue } from '@vulcanize/solidity-mapper';
import { storageLayout, abi } from './artifacts/ERC20.json';
@ -70,32 +70,28 @@ export class Indexer {
}
async name(blockHash, token) {
const { slot } = getStorageInfo(storageLayout, '_name');
const vars = {
const result = await getStorageValue(
storageLayout,
this._ethClient.getStorageAt.bind(this._ethClient),
blockHash,
contract: token,
slot
};
token,
'_name'
)
// TODO: Integrate with storage-mapper to get string value (currently hex encoded).
const result = await this._ethClient.getStorageAt(vars);
log(JSON.stringify(result, null, 2));
return result;
}
async symbol(blockHash, token) {
const { slot } = getStorageInfo(storageLayout, '_symbol');
const vars = {
const result = await getStorageValue(
storageLayout,
this._ethClient.getStorageAt.bind(this._ethClient),
blockHash,
contract: token,
slot
};
token,
'_symbol'
)
// TODO: Integrate with storage-mapper to get string value (currently hex encoded).
const result = await this._ethClient.getStorageAt(vars);
log(JSON.stringify(result, null, 2));
return result;
@ -163,4 +159,4 @@ export class Indexer {
}
});
}
}
}