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'; export { getEventNameTopics } from './logs';

View File

@ -74,21 +74,29 @@ it('get storage information', async function () {
}); });
describe('Get value from storage', 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 () { it('get value for integer type variables packed together', async function () {
const Integers = await ethers.getContractFactory('TestIntegers'); const Integers = await ethers.getContractFactory('TestIntegers');
const integers = await Integers.deploy(); const integers = await Integers.deploy();
await integers.deployed(); await integers.deployed();
const storageLayout = await getStorageLayout('TestIntegers'); const storageLayout = await getStorageLayout('TestIntegers');
let value = 12; let expectedValue = 12;
await integers.setInt1(value); await integers.setInt1(expectedValue);
let storageValue = await getStorageValue(integers.address, storageLayout, getStorageAt, 'int1'); let blockHash = await getBlockHash();
expect(storageValue).to.equal(value); let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, integers.address, 'int1');
expect(value).to.equal(expectedValue);
value = 34; expectedValue = 34;
await integers.setInt2(value); await integers.setInt2(expectedValue);
storageValue = await getStorageValue(integers.address, storageLayout, getStorageAt, 'int2'); blockHash = await getBlockHash();
expect(storageValue).to.equal(value); ({ 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 () { 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(); await integers.deployed();
const storageLayout = await getStorageLayout('TestIntegers'); const storageLayout = await getStorageLayout('TestIntegers');
const value = 123; const expectedValue = 123;
await integers.setInt3(value); await integers.setInt3(expectedValue);
const storageValue = await getStorageValue(integers.address, storageLayout, getStorageAt, 'int3'); const blockHash = await getBlockHash();
expect(storageValue).to.equal(value); 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 () { 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(); await unsignedIntegers.deployed();
const storageLayout = await getStorageLayout('TestUnsignedIntegers'); const storageLayout = await getStorageLayout('TestUnsignedIntegers');
let value = 12; let expectedValue = 12;
await unsignedIntegers.setUint1(value); await unsignedIntegers.setUint1(expectedValue);
let storageValue = await getStorageValue(unsignedIntegers.address, storageLayout, getStorageAt, 'uint1'); let blockHash = await getBlockHash();
expect(storageValue).to.equal(value); let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, unsignedIntegers.address, 'uint1');
expect(value).to.equal(expectedValue);
value = 34; expectedValue = 34;
await unsignedIntegers.setUint2(value); await unsignedIntegers.setUint2(expectedValue);
storageValue = await getStorageValue(unsignedIntegers.address, storageLayout, getStorageAt, 'uint2'); blockHash = await getBlockHash();
expect(storageValue).to.equal(value); ({ 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 () { 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(); await unsignedIntegers.deployed();
const storageLayout = await getStorageLayout('TestUnsignedIntegers'); const storageLayout = await getStorageLayout('TestUnsignedIntegers');
const value = 123; const expectedValue = 123;
await unsignedIntegers.setUint3(value); await unsignedIntegers.setUint3(expectedValue);
const storageValue = await getStorageValue(unsignedIntegers.address, storageLayout, getStorageAt, 'uint3'); const blockHash = await getBlockHash();
expect(storageValue).to.equal(value); const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, unsignedIntegers.address, 'uint3');
expect(value).to.equal(expectedValue);
}); });
it('get value for boolean type', async function () { it('get value for boolean type', async function () {
@ -138,15 +150,17 @@ describe('Get value from storage', function () {
await booleans.deployed(); await booleans.deployed();
const storageLayout = await getStorageLayout('TestBooleans'); const storageLayout = await getStorageLayout('TestBooleans');
let value = true; let expectedValue = true;
await booleans.setBool1(value); await booleans.setBool1(expectedValue);
let storageValue = await getStorageValue(booleans.address, storageLayout, getStorageAt, 'bool1'); let blockHash = await getBlockHash();
expect(storageValue).to.equal(value); let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, booleans.address, 'bool1');
expect(value).to.equal(expectedValue);
value = false; expectedValue = false;
await booleans.setBool2(value); await booleans.setBool2(expectedValue);
storageValue = await getStorageValue(booleans.address, storageLayout, getStorageAt, 'bool2'); blockHash = await getBlockHash();
expect(storageValue).to.equal(value); ({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, booleans.address, 'bool2'));
expect(value).to.equal(expectedValue);
}); });
it('get value for address type', async function () { it('get value for address type', async function () {
@ -157,9 +171,10 @@ describe('Get value from storage', function () {
const [signer] = await ethers.getSigners(); const [signer] = await ethers.getSigners();
await address.setAddress1(signer.address); await address.setAddress1(signer.address);
const storageValue = await getStorageValue(address.address, storageLayout, getStorageAt, 'address1'); const blockHash = await getBlockHash();
expect(storageValue).to.be.a('string'); const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, address.address, 'address1');
expect(String(storageValue).toLowerCase()).to.equal(signer.address.toLowerCase()); expect(value).to.be.a('string');
expect(String(value).toLowerCase()).to.equal(signer.address.toLowerCase());
}); });
it('get value for contract type', async function () { it('get value for contract type', async function () {
@ -175,8 +190,9 @@ describe('Get value from storage', function () {
const storageLayout = await getStorageLayout('TestContractTypes'); const storageLayout = await getStorageLayout('TestContractTypes');
await testContractTypes.setAddressContract1(testAddress.address); await testContractTypes.setAddressContract1(testAddress.address);
const storageValue = await getStorageValue(testContractTypes.address, storageLayout, getStorageAt, 'addressContract1'); const blockHash = await getBlockHash();
expect(storageValue).to.equal(testAddress.address.toLowerCase()); 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 () { 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(); await testBytes.deployed();
const storageLayout = await getStorageLayout('TestBytes'); const storageLayout = await getStorageLayout('TestBytes');
let value = ethers.utils.hexlify(ethers.utils.randomBytes(10)); let expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(10));
await testBytes.setBytesTen(value); await testBytes.setBytesTen(expectedValue);
let storageValue = await getStorageValue(testBytes.address, storageLayout, getStorageAt, 'bytesTen'); let blockHash = await getBlockHash();
expect(storageValue).to.equal(value); let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesTen');
expect(value).to.equal(expectedValue);
value = ethers.utils.hexlify(ethers.utils.randomBytes(20)); expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(20));
await testBytes.setBytesTwenty(value); await testBytes.setBytesTwenty(expectedValue);
storageValue = await getStorageValue(testBytes.address, storageLayout, getStorageAt, 'bytesTwenty'); blockHash = await getBlockHash();
expect(storageValue).to.equal(value); ({ 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 () { 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(); await testBytes.deployed();
const storageLayout = await getStorageLayout('TestBytes'); const storageLayout = await getStorageLayout('TestBytes');
const value = ethers.utils.hexlify(ethers.utils.randomBytes(30)); const expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(30));
await testBytes.setBytesThirty(value); await testBytes.setBytesThirty(expectedValue);
const storageValue = await getStorageValue(testBytes.address, storageLayout, getStorageAt, 'bytesThirty'); const blockHash = await getBlockHash();
expect(storageValue).to.equal(value); const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesThirty');
expect(value).to.equal(expectedValue);
}); });
it('get value for enum types', async function () { it('get value for enum types', async function () {
@ -214,10 +233,11 @@ describe('Get value from storage', function () {
await testEnums.deployed(); await testEnums.deployed();
const storageLayout = await getStorageLayout('TestEnums'); const storageLayout = await getStorageLayout('TestEnums');
const value = 1; const expectedValue = 1;
await testEnums.setChoicesEnum1(value); await testEnums.setChoicesEnum1(expectedValue);
const storageValue = await getStorageValue(testEnums.address, storageLayout, getStorageAt, 'choicesEnum1'); const blockHash = await getBlockHash();
expect(storageValue).to.equal(value); const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testEnums.address, 'choicesEnum1');
expect(value).to.equal(expectedValue);
}); });
describe('string type', function () { 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 () { it('get value for string length less than 32 bytes', async function () {
const value = 'Hello world.'; const expectedValue = 'Hello world.';
await strings.setString1(value); await strings.setString1(expectedValue);
const storageValue = await getStorageValue(strings.address, storageLayout, getStorageAt, 'string1'); const blockHash = await getBlockHash();
expect(storageValue).to.equal(value); 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 () { it('get value for string length more than 32 bytes', async function () {
const value = 'This sentence is more than 32 bytes long.'; const expectedValue = 'This sentence is more than 32 bytes long.';
await strings.setString2(value); await strings.setString2(expectedValue);
const storageValue = await getStorageValue(strings.address, storageLayout, getStorageAt, 'string2'); const blockHash = await getBlockHash();
expect(storageValue).to.equal(value); 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; } 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. * 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. * Function to get the value from storage for a contract variable.
* @param address
* @param storageLayout * @param storageLayout
* @param getStorageAt * @param getStorageAt
* @param blockHash
* @param address
* @param variableName * @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 { 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. // 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 // https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#json-output
switch (encoding) { switch (encoding) {
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#layout-of-state-variables-in-storage // https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#layout-of-state-variables-in-storage
case 'inplace': { case 'inplace':
const valueArray = await getInplaceArray(address, slot, offset, numberOfBytes, getStorageAt); ({ value, proof } = await getInplaceValue(blockHash, address, slot, offset, numberOfBytes, getStorageAt));
break;
// 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);
}
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string // https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string
case 'bytes': { case 'bytes':
const valueArray = await getBytesArray(address, slot, getStorageAt); ({ value, proof } = await getBytesValue(blockHash, address, slot, getStorageAt));
break;
return utils.toUtf8String(valueArray);
}
default: 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 address
* @param slot * @param slot
* @param offset * @param offset
* @param numberOfBytes * @param numberOfBytes
* @param getStorageAt * @param getStorageAt
*/ */
const getInplaceArray = async (address: string, slot: string, offset: number, numberOfBytes: string, getStorageAt: GetStorageAt) => { const getInplaceValue = async (blockHash: string, address: string, slot: string, offset: number, numberOfBytes: string, getStorageAt: GetStorageAt) => {
const value = await getStorageAt(address, slot); const { value, proof } = await getStorageAt({ blockHash, contract: address, slot });
const uintArray = utils.arrayify(value); const valueLength = utils.hexDataLength(value);
// Get value according to offset. // Get value according to offset.
const start = uintArray.length - (offset + Number(numberOfBytes)); const start = valueLength - (offset + Number(numberOfBytes));
const end = uintArray.length - offset; const end = valueLength - offset;
const offsetArray = uintArray.slice(start, end);
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 address
* @param slot * @param slot
* @param getStorageAt * @param getStorageAt
*/ */
const getBytesArray = async (address: string, slot: string, getStorageAt: GetStorageAt) => { const getBytesValue = async (blockHash: string, address: string, slot: string, getStorageAt: GetStorageAt) => {
const value = await getStorageAt(address, slot); const { value, proof } = await getStorageAt({ blockHash, contract: address, slot });
const uintArray = utils.arrayify(value);
let length = 0; let length = 0;
// Get length of bytes stored. // 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. // If first byte is not set, get length directly from the zero padded byte array.
const slotValue = BigNumber.from(value); const slotValue = BigNumber.from(value);
length = slotValue.sub(1).div(2).toNumber(); length = slotValue.sub(1).div(2).toNumber();
} else { } else {
// If first byte is set the length is lesser than 32 bytes. // If first byte is set the length is lesser than 32 bytes.
// Length of the value can be computed from the last byte. // 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. // Get value from the byte array directly if length is less than 32.
if (length < 32) { if (length < 32) {
return uintArray.slice(0, length); return {
value: utils.hexDataSlice(value, 0, length),
proof
};
} }
// Array to hold multiple bytes32 data. // Array to hold multiple bytes32 data.
const values = []; const values = [];
const proofs = [
JSON.parse(proof.data)
];
// 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
@ -145,10 +172,21 @@ const getBytesArray = async (address: string, slot: string, getStorageAt: GetSto
// 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++) {
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); values.push(value);
proofs.push(JSON.parse(proof.data));
} }
// Slice trailing bytes according to length of value. // 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 address
* @param position * @param position
*/ */
export const getStorageAt: GetStorageAt = async (address, position) => { export const getStorageAt: GetStorageAt = async ({ blockHash, contract, slot }) => {
const value = await ethers.provider.getStorageAt(address, position); // 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 { Connection } from "typeorm";
import { invert } from "lodash"; import { invert } from "lodash";
import { EthClient, getMappingSlot, topictoAddress } from "@vulcanize/ipld-eth-client"; 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'; import { storageLayout, abi } from './artifacts/ERC20.json';
@ -70,32 +70,28 @@ export class Indexer {
} }
async name(blockHash, token) { async name(blockHash, token) {
const { slot } = getStorageInfo(storageLayout, '_name'); const result = await getStorageValue(
storageLayout,
const vars = { this._ethClient.getStorageAt.bind(this._ethClient),
blockHash, blockHash,
contract: token, token,
slot '_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)); log(JSON.stringify(result, null, 2));
return result; return result;
} }
async symbol(blockHash, token) { async symbol(blockHash, token) {
const { slot } = getStorageInfo(storageLayout, '_symbol'); const result = await getStorageValue(
storageLayout,
const vars = { this._ethClient.getStorageAt.bind(this._ethClient),
blockHash, blockHash,
contract: token, token,
slot '_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)); log(JSON.stringify(result, null, 2));
return result; return result;