Get value of struct type (#54)

* Implement getting value for struct types.

* Add tests for structs with value type memebers.

* Add tests for verifying proof in struct type.

Co-authored-by: nikugogoi <95nikass@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-06-11 11:19:20 +05:30 committed by GitHub
parent 5316b19fbf
commit 8851882144
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 240 additions and 2 deletions

View File

@ -55,7 +55,9 @@ $ yarn test
* [ ] Bytes
* [x] String
* [ ] Structs
* [ ] Get struct value with all members
* [ ] Value Types
* [ ] Get value of a single member in struct
* [ ] Reference Types
* [ ] Mapping Types
* [x] Value Type keys

View File

@ -343,6 +343,111 @@ describe('Get value from storage', () => {
});
});
describe('structs type', () => {
let testStructs: Contract, storageLayout: StorageLayout;
before(async () => {
const TestStructs = await ethers.getContractFactory('TestStructs');
testStructs = await TestStructs.deploy();
await testStructs.deployed();
storageLayout = await getStorageLayout('TestStructs');
});
it('get value for struct using a single slot', async () => {
const expectedValue = {
int1: BigInt(123),
uint1: BigInt(4)
};
await testStructs.setSingleSlotStruct(expectedValue.int1, expectedValue.uint1);
const blockHash = await getBlockHash();
const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testStructs.address, 'singleSlotStruct');
expect(value).to.eql(expectedValue);
const proofData = JSON.parse(proof.data);
expect(proofData).to.have.all.keys('int1', 'uint1');
});
it('get value for struct using multiple slots', async () => {
const expectedValue = {
uint1: BigInt(123),
bool1: false,
int1: BigInt(456)
};
await testStructs.setMultipleSlotStruct(expectedValue.uint1, expectedValue.bool1, expectedValue.int1);
const blockHash = await getBlockHash();
const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testStructs.address, 'multipleSlotStruct');
expect(value).to.eql(expectedValue);
const proofData = JSON.parse(proof.data);
expect(proofData).to.have.all.keys('uint1', 'bool1', 'int1');
});
it('get value for struct with address type members', async () => {
const [signer1, signer2] = await ethers.getSigners();
const expectedValue = {
int1: BigInt(123),
address1: signer1.address.toLowerCase(),
address2: signer2.address.toLowerCase(),
uint1: BigInt(456)
};
await testStructs.setAddressStruct(expectedValue.int1, expectedValue.address1, expectedValue.address2, expectedValue.uint1);
const blockHash = await getBlockHash();
const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testStructs.address, 'addressStruct');
expect(value).to.eql(expectedValue);
const proofData = JSON.parse(proof.data);
expect(proofData).to.have.all.keys('int1', 'address1', 'address2', 'uint1');
});
it('get value for struct with contract type members', async () => {
const Contract = await ethers.getContractFactory('TestContractTypes');
const contract = await Contract.deploy();
await contract.deployed();
const expectedValue = {
uint1: BigInt(123),
testContract: contract.address.toLowerCase()
};
await testStructs.setContractStruct(expectedValue.uint1, expectedValue.testContract);
const blockHash = await getBlockHash();
const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testStructs.address, 'contractStruct');
expect(value).to.eql(expectedValue);
const proofData = JSON.parse(proof.data);
expect(proofData).to.have.all.keys('uint1', 'testContract');
});
it('get value for struct with fixed-sized byte array members', async () => {
const expectedValue = {
uint1: BigInt(123),
bytesTen: ethers.utils.hexlify(ethers.utils.randomBytes(10)),
bytesTwenty: ethers.utils.hexlify(ethers.utils.randomBytes(20))
};
await testStructs.setFixedBytesStruct(expectedValue.uint1, expectedValue.bytesTen, expectedValue.bytesTwenty);
const blockHash = await getBlockHash();
const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testStructs.address, 'fixedBytesStruct');
expect(value).to.eql(expectedValue);
const proofData = JSON.parse(proof.data);
expect(proofData).to.have.all.keys('uint1', 'bytesTen', 'bytesTwenty');
});
it('get value for struct with enum type members', async () => {
const expectedValue = {
uint1: BigInt(123),
choice1: BigInt(2),
choice2: BigInt(3)
};
await testStructs.setEnumStruct(expectedValue.uint1, expectedValue.choice1, expectedValue.choice2);
const blockHash = await getBlockHash();
const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testStructs.address, 'enumStruct');
expect(value).to.eql(expectedValue);
const proofData = JSON.parse(proof.data);
expect(proofData).to.have.all.keys('uint1', 'choice1', 'choice2');
});
});
describe('basic mapping type', () => {
let testMappingTypes: Contract, storageLayout: StorageLayout;

View File

@ -15,6 +15,7 @@ interface Types {
base?: string;
value?: string;
key?: string;
members?: Storage[];
};
}
@ -102,7 +103,7 @@ export const getValueByType = (storageValue: string, typeLabel: string): bigint
/* 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 }, mappingKeys: Array<MappingKey>): Promise<{ value: any, proof: { data: string } }> => {
const { slot, offset, type } = storageInfo;
const { encoding, numberOfBytes, label: typeLabel, base, value: mappingValueType, key: mappingKeyType } = types[type];
const { encoding, numberOfBytes, label: typeLabel, base, value: mappingValueType, key: mappingKeyType, members } = types[type];
const [isArray, arraySize] = typeLabel.match(/\[([0-9]*)\]/) || [false];
let value: string, proof: { data: string };
@ -138,6 +139,38 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
};
}
const isStruct = /^struct .+/.test(typeLabel);
// If variable is struct type.
if (isStruct && members) {
// TODO: Get values in single call and parse according to type.
// Get member values specified for the struct in storage layout.
const resultPromises = members.map(async member => {
const structSlot = BigNumber.from(slot).add(member.slot).toHexString();
return getDecodedValue(getStorageAt, blockHash, address, types, { slot: structSlot, offset: member.offset, type: member.type }, []);
});
const results = await Promise.all(resultPromises);
const initialValue: {
value: {[key: string]: any},
proof: { data: string }
} = {
value: {},
proof: { data: JSON.stringify({}) }
};
// Return struct type value as an object with keys as the struct member labels.
return members.reduce((acc, member, index) => {
acc.value[member.label] = results[index].value;
const proofData = JSON.parse(acc.proof.data);
proofData[member.label] = results[index].proof;
acc.proof.data = JSON.stringify(proofData);
return acc;
}, initialValue);
}
// 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) {
@ -153,7 +186,7 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
case 'mapping': {
if (mappingValueType && mappingKeyType) {
const mappingSlot = await getMappingSlot(slot, types, mappingKeyType, mappingKeys[0]);
const mappingSlot = getMappingSlot(slot, types, mappingKeyType, mappingKeys[0]);
return getDecodedValue(getStorageAt, blockHash, address, types, { slot: mappingSlot, offset: 0, type: mappingValueType }, mappingKeys.slice(1));
} else {

View File

@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./TestContractTypes.sol";
contract TestStructs {
struct SingleSlotStruct {
int16 int1;
uint8 uint1;
}
// Struct variable will use one single slot as size of the members is less than 32 bytes.
SingleSlotStruct singleSlotStruct;
struct MultipleSlotStruct {
uint128 uint1;
bool bool1;
int192 int1;
}
// Struct variable will use multiple slots as size of the members is more than 32 bytes.
MultipleSlotStruct multipleSlotStruct;
struct AddressStruct {
int8 int1;
address address1;
address address2;
uint16 uint1;
}
AddressStruct addressStruct;
struct ContractStruct {
uint16 uint1;
TestContractTypes testContract;
}
ContractStruct contractStruct;
struct FixedBytesStruct {
uint8 uint1;
bytes10 bytesTen;
bytes20 bytesTwenty;
}
FixedBytesStruct fixedBytesStruct;
enum Choices { Choice0, Choice1, Choice2, Choice3 }
struct EnumStruct {
uint32 uint1;
Choices choice1;
Choices choice2;
}
EnumStruct enumStruct;
// Set variable singleSlotStruct.
function setSingleSlotStruct(int16 int1Value, uint8 uint1Value) external {
singleSlotStruct.int1 = int1Value;
singleSlotStruct.uint1 = uint1Value;
}
// Set variable multipleSlotStruct.
function setMultipleSlotStruct(uint128 uint1Value, bool bool1Value, int192 int1Value) external {
multipleSlotStruct.uint1 = uint1Value;
multipleSlotStruct.bool1 = bool1Value;
multipleSlotStruct.int1 = int1Value;
}
// Set variable addressStruct.
function setAddressStruct(int8 int1Value, address address1Value, address address2Value, uint16 uint1Value) external {
addressStruct.int1 = int1Value;
addressStruct.address1 = address1Value;
addressStruct.address2 = address2Value;
addressStruct.uint1 = uint1Value;
}
// Set variable contractStruct.
function setContractStruct(uint16 uint1Value, TestContractTypes contractValue) external {
contractStruct.uint1 = uint1Value;
contractStruct.testContract = contractValue;
}
// Set variable fixedBytesStruct.
function setFixedBytesStruct(uint8 uint1Value, bytes10 bytesTenValue, bytes20 bytesTwentyValue) external {
fixedBytesStruct.uint1 = uint1Value;
fixedBytesStruct.bytesTen = bytesTenValue;
fixedBytesStruct.bytesTwenty = bytesTwentyValue;
}
// Set variable enumStruct.
function setEnumStruct(uint32 uint1Value, Choices choice1Value, Choices choice2Value) external {
enumStruct.uint1 = uint1Value;
enumStruct.choice1 = choice1Value;
enumStruct.choice2 = choice2Value;
}
}