diff --git a/packages/solidity-mapper/src/index.ts b/packages/solidity-mapper/src/index.ts index 61972464..db2eda2e 100644 --- a/packages/solidity-mapper/src/index.ts +++ b/packages/solidity-mapper/src/index.ts @@ -1,3 +1,3 @@ -export { getStorageValue, getStorageInfo, StorageLayout, GetStorageAt } from './storage'; +export { getStorageValue, getStorageInfo, getValueByType, StorageLayout, GetStorageAt } from './storage'; export { getEventNameTopics } from './logs'; diff --git a/packages/solidity-mapper/src/storage.test.ts b/packages/solidity-mapper/src/storage.test.ts index c9dd16e9..1f22c0af 100644 --- a/packages/solidity-mapper/src/storage.test.ts +++ b/packages/solidity-mapper/src/storage.test.ts @@ -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); }); }); }); diff --git a/packages/solidity-mapper/src/storage.ts b/packages/solidity-mapper/src/storage.ts index 2950c39f..972fd921 100644 --- a/packages/solidity-mapper/src/storage.ts +++ b/packages/solidity-mapper/src/storage.ts @@ -22,7 +22,7 @@ export interface StorageInfo extends Storage { types: { [type: string]: Type; } } -export type GetStorageAt = (address: string, position: string) => Promise +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 => { +/* 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) + } + }; }; diff --git a/packages/solidity-mapper/test/utils.ts b/packages/solidity-mapper/test/utils.ts index 68aff2d3..e1e6a04e 100644 --- a/packages/solidity-mapper/test/utils.ts +++ b/packages/solidity-mapper/test/utils.ts @@ -51,8 +51,18 @@ export const getStorageLayout = async (contractName: string): Promise { - 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) + } + }; }; diff --git a/packages/watcher/src/indexer.ts b/packages/watcher/src/indexer.ts index bdaac03d..f87b20e5 100644 --- a/packages/watcher/src/indexer.ts +++ b/packages/watcher/src/indexer.ts @@ -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 { } }); } -} \ No newline at end of file +}