From 8851882144b7c73f07508a86e51cc8b9056fa6b3 Mon Sep 17 00:00:00 2001 From: Ashwin Phatak Date: Fri, 11 Jun 2021 11:19:20 +0530 Subject: [PATCH] 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> --- packages/solidity-mapper/README.md | 2 + packages/solidity-mapper/src/storage.test.ts | 105 ++++++++++++++++++ packages/solidity-mapper/src/storage.ts | 37 +++++- .../test/contracts/TestStructs.sol | 98 ++++++++++++++++ 4 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 packages/solidity-mapper/test/contracts/TestStructs.sol diff --git a/packages/solidity-mapper/README.md b/packages/solidity-mapper/README.md index 1db7a8e1..21a30f5c 100644 --- a/packages/solidity-mapper/README.md +++ b/packages/solidity-mapper/README.md @@ -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 diff --git a/packages/solidity-mapper/src/storage.test.ts b/packages/solidity-mapper/src/storage.test.ts index 47f8d0dd..f3903189 100644 --- a/packages/solidity-mapper/src/storage.test.ts +++ b/packages/solidity-mapper/src/storage.test.ts @@ -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; diff --git a/packages/solidity-mapper/src/storage.ts b/packages/solidity-mapper/src/storage.ts index bf3ff12f..82852c47 100644 --- a/packages/solidity-mapper/src/storage.ts +++ b/packages/solidity-mapper/src/storage.ts @@ -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): 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 { diff --git a/packages/solidity-mapper/test/contracts/TestStructs.sol b/packages/solidity-mapper/test/contracts/TestStructs.sol new file mode 100644 index 00000000..0c9601e3 --- /dev/null +++ b/packages/solidity-mapper/test/contracts/TestStructs.sol @@ -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; + } +}