diff --git a/packages/solidity-mapper/src/storage.test.ts b/packages/solidity-mapper/src/storage.test.ts index 1c22e712..9661268b 100644 --- a/packages/solidity-mapper/src/storage.test.ts +++ b/packages/solidity-mapper/src/storage.test.ts @@ -383,76 +383,148 @@ describe('Get value from storage', () => { describe('byte array', () => { let testBytes: Contract, storageLayout: StorageLayout, blockHash: string; - const bytesTenValue = ethers.utils.hexlify(ethers.utils.randomBytes(10)); - const bytesTwentyValue = ethers.utils.hexlify(ethers.utils.randomBytes(20)); - const bytesThirtyValue = ethers.utils.hexlify(ethers.utils.randomBytes(30)); - const bytesArray1 = ethers.utils.hexlify(ethers.utils.randomBytes(24)); - const bytesArray2 = ethers.utils.hexlify(ethers.utils.randomBytes(100)); before(async () => { ({ contract: testBytes, storageLayout } = contracts.TestBytes); - - const transactions = await Promise.all([ - testBytes.setBytesTen(bytesTenValue), - testBytes.setBytesTwenty(bytesTwentyValue), - testBytes.setBytesThirty(bytesThirtyValue), - testBytes.setBytesArray1(bytesArray1), - testBytes.setBytesArray2(bytesArray2) - ]); - - await Promise.all(transactions.map(transaction => transaction.wait())); - blockHash = await getBlockHash(); }); - it('get value for fixed size byte arrays packed together', async () => { - let { value, proof: { data: proofData } } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesTen'); - expect(value).to.equal(bytesTenValue); + describe('fixed size byte array', () => { + const bytesTenValue = ethers.utils.hexlify(ethers.utils.randomBytes(10)); + const bytesTwentyValue = ethers.utils.hexlify(ethers.utils.randomBytes(20)); + const bytesThirtyValue = ethers.utils.hexlify(ethers.utils.randomBytes(30)); - if (isIpldGql) { - assertProofData(blockHash, testBytes.address, JSON.parse(proofData)); - } + before(async () => { + const transactions = await Promise.all([ + testBytes.setBytesTen(bytesTenValue), + testBytes.setBytesTwenty(bytesTwentyValue), + testBytes.setBytesThirty(bytesThirtyValue) + ]); - ({ value, proof: { data: proofData } } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesTwenty')); - expect(value).to.equal(bytesTwentyValue); + await Promise.all(transactions.map(transaction => transaction.wait())); + blockHash = await getBlockHash(); + }); - if (isIpldGql) { - assertProofData(blockHash, testBytes.address, JSON.parse(proofData)); - } - }); + it('get value for fixed size byte arrays packed together', async () => { + let { value, proof: { data: proofData } } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesTen'); + expect(value).to.equal(bytesTenValue); - it('get value for fixed size byte arrays using single slot', async () => { - const { value, proof: { data: proofData } } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesThirty'); - expect(value).to.equal(bytesThirtyValue); + if (isIpldGql) { + assertProofData(blockHash, testBytes.address, JSON.parse(proofData)); + } - if (isIpldGql) { - assertProofData(blockHash, testBytes.address, JSON.parse(proofData)); - } + ({ value, proof: { data: proofData } } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesTwenty')); + expect(value).to.equal(bytesTwentyValue); + + if (isIpldGql) { + assertProofData(blockHash, testBytes.address, JSON.parse(proofData)); + } + }); + + it('get value for fixed size byte arrays using single slot', async () => { + const { value, proof: { data: proofData } } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesThirty'); + expect(value).to.equal(bytesThirtyValue); + + if (isIpldGql) { + assertProofData(blockHash, testBytes.address, JSON.parse(proofData)); + } + }); }); // Dynamically sized byte array. - it('get value for dynamic byte array of length less than 32 bytes', async () => { - const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesArray1'); - expect(value).to.equal(bytesArray1); - const proofData = JSON.parse(proof.data); - expect(proofData.length).to.equal(1); + describe('dynamic byte array', () => { + const byteArray1 = ethers.utils.hexlify(ethers.utils.randomBytes(20)); + const byteArray2 = ethers.utils.hexlify(ethers.utils.randomBytes(100)); - if (isIpldGql) { - assertProofArray(blockHash, testBytes.address, proofData); - } - }); + const setBytesAndGetBlock = async (value: string) => { + const transaction = await testBytes.setByteArray(value); + await transaction.wait(); + return getBlockHash(); + }; - it('get value for dynamic byte array of length more than 32 bytes', async () => { - const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'bytesArray2'); - expect(value).to.equal(bytesArray2); - const proofData = JSON.parse(proof.data); + it('get value for dynamic byte array of length less than 32 bytes', async () => { + blockHash = await setBytesAndGetBlock(byteArray1); + const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'byteArray'); + expect(value).to.equal(byteArray1); + const proofData = JSON.parse(proof.data); + expect(proofData.length).to.equal(1); - // Length is equal to slots required by the data plus the initial slot used to calculate the actual slots holding the data. - const proofDataLength = (Math.ceil(ethers.utils.hexDataLength(bytesArray2) / 32)) + 1; - expect(proofData.length).to.equal(proofDataLength); + if (isIpldGql) { + assertProofArray(blockHash, testBytes.address, proofData); + } + }); - if (isIpldGql) { - assertProofArray(blockHash, testBytes.address, proofData); - } + it('get value for dynamic byte array of length more than 32 bytes', async () => { + blockHash = await setBytesAndGetBlock(byteArray2); + const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'byteArray'); + expect(value).to.equal(byteArray2); + const proofData = JSON.parse(proof.data); + + // Length is equal to slots required by the data plus the initial slot used to calculate the actual slots holding the data. + const proofDataLength = (Math.ceil(ethers.utils.hexDataLength(byteArray2) / 32)) + 1; + expect(proofData.length).to.equal(proofDataLength); + + if (isIpldGql) { + assertProofArray(blockHash, testBytes.address, proofData); + } + }); + + it('get value for dynamic byte array with leading zeros and of length less than 32', async () => { + const byteArray = ethers.utils.hexZeroPad(byteArray1, 24); + blockHash = await setBytesAndGetBlock(byteArray); + const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'byteArray'); + expect(value).to.equal(byteArray); + const proofData = JSON.parse(proof.data); + expect(proofData.length).to.equal(1); + + if (isIpldGql) { + assertProofArray(blockHash, testBytes.address, proofData); + } + }); + + it('get value for dynamic byte array with leading zeros and of length more than 32', async () => { + const byteArray = ethers.utils.hexZeroPad(byteArray2, 110); + blockHash = await setBytesAndGetBlock(byteArray); + const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'byteArray'); + expect(value).to.equal(byteArray); + const proofData = JSON.parse(proof.data); + + // Length is equal to slots required by the data plus the initial slot used to calculate the actual slots holding the data. + const proofDataLength = (Math.ceil(ethers.utils.hexDataLength(byteArray) / 32)) + 1; + expect(proofData.length).to.equal(proofDataLength); + + if (isIpldGql) { + assertProofArray(blockHash, testBytes.address, proofData); + } + }); + + it('get value for dynamic byte array of length 31', async () => { + const byteArray = ethers.utils.hexlify(ethers.utils.randomBytes(31)); + blockHash = await setBytesAndGetBlock(byteArray); + const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'byteArray'); + expect(value).to.equal(byteArray); + const proofData = JSON.parse(proof.data); + expect(proofData.length).to.equal(1); + + if (isIpldGql) { + assertProofArray(blockHash, testBytes.address, proofData); + } + }); + + it('get value for dynamic byte array of length 32', async () => { + const byteArray = ethers.utils.hexlify(ethers.utils.randomBytes(32)); + blockHash = await setBytesAndGetBlock(byteArray); + const { value, proof } = await getStorageValue(storageLayout, getStorageAt, blockHash, testBytes.address, 'byteArray'); + expect(value).to.equal(byteArray); + const proofData = JSON.parse(proof.data); + + // Length is equal to slots required by the data plus the initial slot used to calculate the actual slots holding the data. + const proofDataLength = (Math.ceil(ethers.utils.hexDataLength(byteArray) / 32)) + 1; + expect(proofData.length).to.equal(proofDataLength); + + if (isIpldGql) { + assertProofArray(blockHash, testBytes.address, proofData); + } + }); }); }); diff --git a/packages/solidity-mapper/src/storage.ts b/packages/solidity-mapper/src/storage.ts index 376218a8..f5fe7023 100644 --- a/packages/solidity-mapper/src/storage.ts +++ b/packages/solidity-mapper/src/storage.ts @@ -351,17 +351,20 @@ const getInplaceValue = async (getStorageAt: GetStorageAt, blockHash: string, ad */ const getBytesValue = async (getStorageAt: GetStorageAt, blockHash: string, address: string, slot: string) => { const { value, proof } = await getStorageAt({ blockHash, contract: address, slot }); - let length = 0; const proofs = [JSON.parse(proof.data)]; + const slotValue = BigNumber.from(value); + let length = 0; // Get length of bytes stored. - 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); + // https://docs.soliditylang.org/en/v0.7.6/internals/layout_in_storage.html#bytes-and-string + // Check if the lowest bit is set. + if ((slotValue.and(1)).toNumber() !== 0) { + // If the lowest bit is set, the value is an odd number. + // So subtract 1 and divide by 2 to get the length. 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. + // If the lowest bit is not set, the value is an even number. + // Extract the last byte of the hex string and divide by 2 to get the length. const lastByteHex = utils.hexDataSlice(value, 31, 32); length = BigNumber.from(lastByteHex).div(2).toNumber(); } diff --git a/packages/solidity-mapper/test/contracts/TestBytes.sol b/packages/solidity-mapper/test/contracts/TestBytes.sol index 802c13fb..36ba0819 100644 --- a/packages/solidity-mapper/test/contracts/TestBytes.sol +++ b/packages/solidity-mapper/test/contracts/TestBytes.sol @@ -13,8 +13,7 @@ contract TestBytes { // If data is 32 or more bytes, the main slot stores the value length * 2 + 1 and the data is stored in keccak256(slot). // Else the main slot stores the data and value length * 2. // https://docs.soliditylang.org/en/v0.7.4/internals/layout_in_storage.html#bytes-and-string - bytes bytesArray1; - bytes bytesArray2; + bytes byteArray; // Set variable bytesTen. function setBytesTen(bytes10 value) external { @@ -31,13 +30,8 @@ contract TestBytes { bytesThirty = value; } - // Set variable bytesArray1. - function setBytesArray1(bytes calldata value) external { - bytesArray1 = value; - } - - // Set variable bytesArray2. - function setBytesArray2(bytes calldata value) external { - bytesArray2 = value; + // Set variable byteArray. + function setByteArray(bytes calldata value) external { + byteArray = value; } }