Get value for array of integer and boolean types in solidity mapper (#30)

* Implement getting value for array type.

* Add subtypes in solidit mapper readme.

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

View File

@ -17,20 +17,49 @@ $ yarn test
## Different Types
* [x] Booleans
* [x] Integers
* [ ] Fixed Point Numbers
* [x] Address
* [x] Contract Types
* [x] Fixed-size byte arrays
* [x] Enums
* [ ] Function Types
* [ ] Arrays
* [ ] Dynamically-sized byte array
* [ ] Bytes
* [x] String
* [ ] Structs
* [ ] Mapping Types
* [ ] Value Types
* [x] Booleans
* [x] Integers
* [ ] Fixed Point Numbers
* [x] Address
* [x] Contract Types
* [x] Fixed-size byte arrays
* [x] Enums
* [ ] Function Types
* [ ] Reference Types
* [ ] Arrays
* [ ] Fixed size arrays
* [x] Integer Type
* [x] Boolean Type
* [ ] Address Type
* [ ] Fixed-size byte arrays
* [ ] Enum type
* [ ] Dynamically-sized byte array
* [ ] Struct Type
* [ ] Mapping Type
* [ ] Dynamically-sized arrays
* [ ] Integer Type
* [ ] Boolean Type
* [ ] Address Type
* [ ] Fixed-size byte arrays
* [ ] Enum Type
* [ ] Dynamically-sized byte array
* [ ] Struct Type
* [ ] Mapping Type
* [ ] Nested Arrays
* [ ] Fixed size arrays
* [ ] Dynamically-sized arrays
* [ ] Dynamically-sized byte array
* [ ] Bytes
* [x] String
* [ ] Structs
* [ ] Value Types
* [ ] Reference Types
* [ ] Mapping Types
* [ ] Value Type keys
* [ ] Dynamically-sized byte array keys
* [ ] Reference Type Mapping values
* [ ] Nested Mapping
## Observations

View File

@ -29,5 +29,6 @@
"build": "tsc",
"test": "hardhat test",
"lint": "eslint ."
}
},
"dependencies": {}
}

View File

@ -18,7 +18,7 @@ const TEST_DATA = [
}
];
it('get event name topics', async function () {
it('get event name topics', async () => {
const testPromises = TEST_DATA.map(async ({ name, output }) => {
const Contract = await ethers.getContractFactory(name);
const contract = await Contract.deploy();

View File

@ -59,7 +59,7 @@ const TEST_DATA = [
}
];
it('get storage information', async function () {
it('get storage information', async () => {
const testPromises = TEST_DATA.map(async ({ name, variable, output }) => {
const Contract = await ethers.getContractFactory(name);
const contract = await Contract.deploy();
@ -73,14 +73,14 @@ it('get storage information', async function () {
await Promise.all(testPromises);
});
describe('Get value from storage', function () {
describe('Get value from storage', () => {
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 () => {
const Integers = await ethers.getContractFactory('TestIntegers');
const integers = await Integers.deploy();
await integers.deployed();
@ -99,7 +99,7 @@ describe('Get value from storage', function () {
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 () => {
const Integers = await ethers.getContractFactory('TestIntegers');
const integers = await Integers.deploy();
await integers.deployed();
@ -112,7 +112,7 @@ describe('Get value from storage', function () {
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 () => {
const UnsignedIntegers = await ethers.getContractFactory('TestUnsignedIntegers');
const unsignedIntegers = await UnsignedIntegers.deploy();
await unsignedIntegers.deployed();
@ -131,7 +131,7 @@ describe('Get value from storage', function () {
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 () => {
const UnsignedIntegers = await ethers.getContractFactory('TestUnsignedIntegers');
const unsignedIntegers = await UnsignedIntegers.deploy();
await unsignedIntegers.deployed();
@ -144,7 +144,7 @@ describe('Get value from storage', function () {
expect(value).to.equal(expectedValue);
});
it('get value for boolean type', async function () {
it('get value for boolean type', async () => {
const Booleans = await ethers.getContractFactory('TestBooleans');
const booleans = await Booleans.deploy();
await booleans.deployed();
@ -163,7 +163,7 @@ describe('Get value from storage', function () {
expect(value).to.equal(expectedValue);
});
it('get value for address type', async function () {
it('get value for address type', async () => {
const Address = await ethers.getContractFactory('TestAddress');
const address = await Address.deploy();
await address.deployed();
@ -177,7 +177,7 @@ describe('Get value from storage', function () {
expect(String(value).toLowerCase()).to.equal(signer.address.toLowerCase());
});
it('get value for contract type', async function () {
it('get value for contract type', async () => {
const contracts = ['TestContractTypes', 'TestAddress'];
const contractPromises = contracts.map(async (contractName) => {
@ -195,7 +195,7 @@ describe('Get value from storage', function () {
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 () => {
const TestBytes = await ethers.getContractFactory('TestBytes');
const testBytes = await TestBytes.deploy();
await testBytes.deployed();
@ -214,7 +214,7 @@ describe('Get value from storage', function () {
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 () => {
const TestBytes = await ethers.getContractFactory('TestBytes');
const testBytes = await TestBytes.deploy();
await testBytes.deployed();
@ -227,7 +227,7 @@ describe('Get value from storage', function () {
expect(value).to.equal(expectedValue);
});
it('get value for enum types', async function () {
it('get value for enum types', async () => {
const TestEnums = await ethers.getContractFactory('TestEnums');
const testEnums = await TestEnums.deploy();
await testEnums.deployed();
@ -240,7 +240,7 @@ describe('Get value from storage', function () {
expect(value).to.equal(expectedValue);
});
describe('string type', function () {
describe('string type', () => {
let strings: Contract, storageLayout: StorageLayout;
before(async () => {
@ -250,7 +250,8 @@ describe('Get value from storage', function () {
storageLayout = await getStorageLayout('TestStrings');
});
it('get value for string length less than 32 bytes', async function () {
// Test for string of size less than 32 bytes which use only one slot.
it('get value for string length less than 32 bytes', async () => {
const expectedValue = 'Hello world.';
await strings.setString1(expectedValue);
const blockHash = await getBlockHash();
@ -258,7 +259,8 @@ describe('Get value from storage', function () {
expect(value).to.equal(expectedValue);
});
it('get value for string length more than 32 bytes', async function () {
// Test for string of size 32 bytes or more which use multiple slots.
it('get value for string length more than 32 bytes', async () => {
const expectedValue = 'This sentence is more than 32 bytes long.';
await strings.setString2(expectedValue);
const blockHash = await getBlockHash();
@ -266,4 +268,45 @@ describe('Get value from storage', function () {
expect(value).to.equal(expectedValue);
});
});
// Test for array variables which are 32 bytes or less and packed into a single slot.
it('get value for fixed size arrays using single slot', async () => {
const TestFixedArrays = await ethers.getContractFactory('TestFixedArrays');
const testFixedArrays = await TestFixedArrays.deploy();
await testFixedArrays.deployed();
const storageLayout = await getStorageLayout('TestFixedArrays');
let expectedValue: Array<number|boolean> = [true, false];
await testFixedArrays.setBoolArray(expectedValue);
let blockHash = await getBlockHash();
let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testFixedArrays.address, 'boolArray');
expect(value).to.eql(expectedValue);
expectedValue = [1, 2, 3, 4, 5];
await testFixedArrays.setUint16Array(expectedValue);
blockHash = await getBlockHash();
({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testFixedArrays.address, 'uint16Array'));
expect(value).to.eql(expectedValue);
});
// Test for array variables which are more than 32 bytes and use multiple slots.
it('get value for fixed size arrays using multiple slots', async () => {
const TestFixedArrays = await ethers.getContractFactory('TestFixedArrays');
const testFixedArrays = await TestFixedArrays.deploy();
await testFixedArrays.deployed();
const storageLayout = await getStorageLayout('TestFixedArrays');
const expectedValue = [1, 2, 3, 4, 5];
await testFixedArrays.setInt128Array(expectedValue);
let blockHash = await getBlockHash();
let { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testFixedArrays.address, 'int128Array');
expect(value).to.eql(expectedValue);
await testFixedArrays.setUintArray(expectedValue);
blockHash = await getBlockHash();
({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testFixedArrays.address, 'uintArray'));
expect(value).to.eql(expectedValue);
});
});

View File

@ -7,19 +7,22 @@ interface Storage {
label: string;
}
interface Type {
encoding: string;
numberOfBytes: string;
label: string;
interface Types {
[type: string]: {
encoding: string;
numberOfBytes: string;
label: string;
base?: string;
};
}
export interface StorageLayout {
storage: Storage[];
types: { [type: string]: Type; }
types: Types
}
export interface StorageInfo extends Storage {
types: { [type: string]: Type; }
types: Types
}
export type GetStorageAt = (param: { blockHash: string, contract: string, slot: string }) => Promise<{ value: string, proof: { data: string } }>
@ -56,9 +59,74 @@ export const getStorageInfo = (storageLayout: StorageLayout, variableName: strin
/* 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: typeLabel } = types[type];
return getDecodedValue(getStorageAt, blockHash, address, types, { slot, offset, type });
};
/**
* Get value according to type described by the label.
* @param storageValue
* @param typeLabel
*/
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 decoded value according to type and encoding.
* @param getStorageAt
* @param blockHash
* @param address
* @param types
* @param storageInfo
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, address: string, types: Types, storageInfo: { slot: string, offset: number, type: string }): Promise<{ value: any, proof: { data: string } }> => {
const { slot, offset, type } = storageInfo;
const { encoding, numberOfBytes, label: typeLabel, base } = types[type];
const [isArray, arraySize] = typeLabel.match(/\[([0-9]*)\]/) || [false];
let value: string, proof: { data: string };
// If variable is array type.
if (isArray && base) {
const resultArray = [];
const proofs = [];
const { numberOfBytes: baseNumberOfBytes } = types[base];
// TODO: Get values in single call and parse according to type.
// Loop over elements of array and get value.
for (let i = 0; i < Number(baseNumberOfBytes) * Number(arraySize); i = i + Number(baseNumberOfBytes)) {
const arraySlot = BigNumber.from(slot).add(Math.floor(i / 32)).toHexString();
const slotOffset = i % 32;
const { value, proof } = await getDecodedValue(getStorageAt, blockHash, address, types, { slot: arraySlot, offset: slotOffset, type: base });
resultArray.push(value);
proofs.push(JSON.parse(proof.data));
}
return {
value: resultArray,
proof: {
data: JSON.stringify(proofs)
}
};
}
// 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) {
@ -82,31 +150,6 @@ export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt
};
};
/**
* 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
@ -160,10 +203,10 @@ const getBytesValue = async (blockHash: string, address: string, slot: string, g
}
// Array to hold multiple bytes32 data.
const values = [];
const proofs = [
JSON.parse(proof.data)
];
const hexStringArray = [];
// Compute zero padded hexstring to calculate hashed position of storage.
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
@ -178,13 +221,13 @@ const getBytesValue = async (blockHash: string, address: string, slot: string, g
slot: BigNumber.from(position).add(i).toHexString()
});
values.push(value);
hexStringArray.push(value);
proofs.push(JSON.parse(proof.data));
}
// Slice trailing bytes according to length of value.
return {
value: utils.hexDataSlice(utils.hexConcat(values), 0, length),
value: utils.hexDataSlice(utils.hexConcat(hexStringArray), 0, length),
proof: {
data: JSON.stringify(proofs)
}

View File

@ -5,28 +5,33 @@ contract TestFixedArrays {
// Fixed size array variable will use 5 consecutive slots as size of 1 element is 32 bytes.
uint[5] uintArray;
// Fixed size array variable will use 10 consecutive slots as size of 1 element is 32 bytes.
int[10] intArray;
// Fixed size array variable will use 3 slots as size of 1 element is 16 bytes.
int128[5] int128Array;
// Fixed size array variable will use 1 slot as size of one element is 1 byte.
int8[2] int8Array;
bool[2] boolArray;
// Fixed size array variable will use the next consecutive slot as it is of array type.
// Fixed size array variable will use the next slot as it is of array type.
// https://docs.soliditylang.org/en/v0.7.4/internals/layout_in_storage.html#layout-of-state-variables-in-storage
uint128[5] uint128Array;
uint16[5] uint16Array;
// Set varaible boolArray.
function setBoolArray(bool[2] calldata value) external {
boolArray = value;
}
// Set varaible uintArray.
function setUintArray(uint[5] calldata value) external {
uintArray = value;
}
// Set varaible int8Array.
function setInt8Array(int8[2] calldata value) external {
int8Array = value;
// Set varaible uint16Array.
function setUint16Array(uint16[5] calldata value) external {
uint16Array = value;
}
// Set varaible uint128Array.
function setUint128Array(uint128[5] calldata value) external {
uint128Array = value;
function setInt128Array(int128[5] calldata value) external {
int128Array = value;
}
}

View File

@ -55,7 +55,7 @@ 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.
// Using latest tag for temporary fix in test scenario.
blockHash = 'latest';
const value = await ethers.provider.getStorageAt(contract, slot, blockHash);