From ebdd00a4bf8907013d6d8fa9b8f4c601e703535c Mon Sep 17 00:00:00 2001 From: Antonis Kogias Date: Tue, 5 Jul 2022 18:49:24 +0300 Subject: [PATCH 1/7] feat: add getVariable function This commit introduces a new function for mock contracts. With `getVariable` you can view an internal variable's value. --- docs/source/mocks.rst | 15 +- src/factories/smock-contract.ts | 3 + src/logic/readable-storage-logic.ts | 37 +++ src/types.ts | 2 + src/utils/storage.ts | 272 +++++++++++++++++- test/contracts/mock/StorageGetter.sol | 35 +++ test/unit/mock/readable-storage-logic.spec.ts | 175 +++++++++++ 7 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 src/logic/readable-storage-logic.ts create mode 100644 test/unit/mock/readable-storage-logic.spec.ts diff --git a/docs/source/mocks.rst b/docs/source/mocks.rst index 4a6e39b..6a531d7 100644 --- a/docs/source/mocks.rst +++ b/docs/source/mocks.rst @@ -133,4 +133,17 @@ Setting the value of multiple variables [myKey]: 1234 } }) - \ No newline at end of file + +Getting the value of an internal variable +******************** + +.. code-block:: typescript + + const myUint256 = await myMock.getVariable('myUint256VariableName'); + +Getting the value of an internal mapping given the value's key +******************** + +.. code-block:: typescript + + const myMappingValue = await myMock.getVariable('myMappingVariableName', mappingKey); diff --git a/src/factories/smock-contract.ts b/src/factories/smock-contract.ts index 0978cc1..9d8bbbf 100644 --- a/src/factories/smock-contract.ts +++ b/src/factories/smock-contract.ts @@ -7,6 +7,7 @@ import { Observable } from 'rxjs'; import { distinct, filter, map, share, withLatestFrom } from 'rxjs/operators'; import { EditableStorageLogic as EditableStorage } from '../logic/editable-storage-logic'; import { ProgrammableFunctionLogic, SafeProgrammableContract } from '../logic/programmable-function-logic'; +import { ReadableStorageLogic as ReadableStorage } from '../logic/readable-storage-logic'; import { ObservableVM } from '../observable-vm'; import { Sandbox } from '../sandbox'; import { ContractCall, FakeContract, MockContractFactory, ProgrammableContractFunction, ProgrammedReturnValue } from '../types'; @@ -51,8 +52,10 @@ function mockifyContractFactory( // attach to every internal variable, all the editable logic const editableStorage = new EditableStorage(await getStorageLayout(contractName), vm.getManager(), mock.address); + const readableStorage = new ReadableStorage(await getStorageLayout(contractName), vm.getManager(), mock.address); mock.setVariable = editableStorage.setVariable.bind(editableStorage); mock.setVariables = editableStorage.setVariables.bind(editableStorage); + mock.getVariable = readableStorage.getVariable.bind(readableStorage); // We attach a wallet to the contract so that users can send transactions *from* a watchablecontract. mock.wallet = await impersonate(mock.address); diff --git a/src/logic/readable-storage-logic.ts b/src/logic/readable-storage-logic.ts new file mode 100644 index 0000000..3bf1df3 --- /dev/null +++ b/src/logic/readable-storage-logic.ts @@ -0,0 +1,37 @@ +import { SmockVMManager } from '../types'; +import { fromHexString, remove0x, toFancyAddress, toHexString } from '../utils'; +import { decodeVariable, getVariableStorageSlots, StorageSlotKeyValuePair } from '../utils/storage'; + +export class ReadableStorageLogic { + private storageLayout: any; + private contractAddress: string; + private vmManager: SmockVMManager; + + constructor(storageLayout: any, vmManager: SmockVMManager, contractAddress: string) { + this.storageLayout = storageLayout; + this.vmManager = vmManager; + this.contractAddress = contractAddress; + } + + async getVariable(variableName: string, mappingKey?: string | number): Promise { + const slots = await getVariableStorageSlots(this.storageLayout, variableName, this.vmManager, this.contractAddress, mappingKey); + + let slotValueTypePairs: StorageSlotKeyValuePair[] = []; + + for (const slotKeyPair of slots) { + slotValueTypePairs = slotValueTypePairs.concat({ + value: remove0x( + toHexString(await this.vmManager.getContractStorage(toFancyAddress(this.contractAddress), fromHexString(slotKeyPair.key))) + ), + type: slotKeyPair.type, + length: slotKeyPair.length, + label: slotKeyPair.label, + offset: slotKeyPair.offset, + }); + } + + const result = decodeVariable(slotValueTypePairs); + + return result; + } +} diff --git a/src/types.ts b/src/types.ts index 06c909c..5c23396 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ import { Provider } from '@ethersproject/abstract-provider'; import { Signer } from '@ethersproject/abstract-signer'; import { BaseContract, ContractFactory, ethers } from 'ethers'; import { EditableStorageLogic } from './logic/editable-storage-logic'; +import { ReadableStorageLogic } from './logic/readable-storage-logic'; import { WatchableFunctionLogic } from './logic/watchable-function-logic'; type Abi = ReadonlyArray< @@ -71,6 +72,7 @@ export type MockContract = SmockContractB connect: (...args: Parameters) => MockContract; setVariable: EditableStorageLogic['setVariable']; setVariables: EditableStorageLogic['setVariables']; + getVariable: ReadableStorageLogic['getVariable']; } & { [Property in keyof T['functions']]: ProgrammableContractFunction; }; diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 489f61e..61ddab7 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -1,7 +1,8 @@ import { BigNumber, ethers } from 'ethers'; import { artifacts } from 'hardhat'; import semver from 'semver'; -import { bigNumberToHex, fromHexString, remove0x } from './hex-utils'; +import { SmockVMManager } from '../types'; +import { bigNumberToHex, fromHexString, remove0x, toFancyAddress, toHexString } from '../utils'; // Represents the JSON objects outputted by the Solidity compiler that describe the structure of // state within the contract. See @@ -42,6 +43,24 @@ interface StorageSlotPair { val: string; } +// This object represents the storage slot that a variable is stored (key) +// and the type of the variable (type). +interface StorageSlotKeyTypePair { + key: string; + type: SolidityStorageType; + length?: number; // used only for bytes type, helps during decoding + label?: string; // used for structs to get the members key + offset?: number; // used when we deal with packed variables +} + +export interface StorageSlotKeyValuePair { + value: any; + type: SolidityStorageType; + length?: number; // used only for bytes type, helps during decoding + label?: string; // used for structs to get the members key + offset?: number; // used when we deal with packed variables +} + /** * Retrieves the storageLayout portion of the compiler artifact for a given contract by name. This * function is hardhat specific. @@ -393,3 +412,254 @@ function encodeVariable( throw new Error(`unknown unsupported type ${variableType.encoding} ${variableType.label}`); } + +/** + * Computes the slot keys and types of the storage slots that a variable lives + * + * @param storageLayout Solidity storage layout to use as a template for determining storage slots. + * @param variableName Variable name to find against the given storage layout. + * @param vmManager SmockVMManager is used to get certain storage values given a specific slot key and a contract address + * @param contractAddress Contract address to use for vmManager + * @param mappingKey Only used for mappings, represents they key of a mapping value + * @returns An array of storage slot key/type pair that would result in the value of the variable. + */ +export async function getVariableStorageSlots( + storageLayout: SolidityStorageLayout, + variableName: string, + vmManager: SmockVMManager, + contractAddress: string, + mappingKey?: string | number +): Promise { + // Find the entry in the storage layout that corresponds to this variable name. + const storageObj = storageLayout.storage.find((entry) => { + return entry.label === variableName; + }); + + // Complain very loudly if attempting to set a variable that doesn't exist within this layout. + if (!storageObj) { + throw new Error(`Variable name not found in storage layout: ${variableName}`); + } + + const storageObjectType: SolidityStorageType = storageLayout.types[storageObj.type]; + + // Here we will store all the key/type pairs that we need to get the variable's value + let slotKeysTypes: StorageSlotKeyTypePair[] = []; + let key: string = + '0x' + + remove0x( + BigNumber.from(0) + .add(BigNumber.from(parseInt(storageObj.slot, 10))) + .toHexString() + ).padStart(64, '0'); + + if (storageObjectType.encoding === 'inplace') { + // For `inplace` encoding we only need to be aware of structs where they take more slots to store a variable + if (storageObjectType.label.startsWith('struct')) { + if (storageObjectType.members === undefined) { + throw new Error(`There are no members in object type ${storageObjectType}`); + } + // Slot key that represents the struct + slotKeysTypes = slotKeysTypes.concat({ + key: key, + type: storageObjectType, + offset: storageObj.offset, + }); + // These slots are for the members of the struct + for (let i = 0; i < storageObjectType.members.length; i++) { + // We calculate the slot key for each member + key = '0x' + remove0x(BigNumber.from(key).add(BigNumber.from(storageObjectType.members[i].slot)).toHexString()).padStart(64, '0'); + slotKeysTypes = slotKeysTypes.concat({ + key: key, + type: storageLayout.types[storageObjectType.members[i].type], + label: storageObjectType.members[i].label, + offset: storageObjectType.members[i].offset, + }); + } + } else { + // In cases we deal with other types than structs we already know the slot key and type + slotKeysTypes = slotKeysTypes.concat({ + key: key, + type: storageObjectType, + offset: storageObj.offset, + }); + } + } else if (storageObjectType.encoding === 'bytes') { + // The last 2 bytes of the slot represent the length of the string/bytes variable + // If it's bigger than 31 then we have to deal with a long string/bytes array + const bytesValue = toHexString(await vmManager.getContractStorage(toFancyAddress(contractAddress), fromHexString(key))); + // It is known that if the last byte is set then we are dealing with a long string + // if it is 0 then we are dealing with a short string, you can find more details here (https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#bytes-and-string) + if (bytesValue.slice(-1) === '1') { + // We calculate the total number of slots that this long string/bytes use + const numberOfSlots = Math.ceil((parseInt(bytesValue, 16) - 1) / 32); + // Since we are dealing with bytes, their values are stored contiguous + // we are storing their slotkeys, type and the length which will help us in `decodeVariable` + for (let i = 0; i < numberOfSlots; i++) { + slotKeysTypes = slotKeysTypes.concat({ + key: ethers.utils.keccak256(key) + i, + type: storageObjectType, + length: i + 1 <= numberOfSlots ? 32 : (parseInt(bytesValue, 16) - 1) % 32, + }); + } + } else { + // If we are dealing with a short string/bytes then we already know the slotkey, type & length + slotKeysTypes = slotKeysTypes.concat({ + key: key, + type: storageObjectType, + length: parseInt(bytesValue.slice(-2), 16), + offset: storageObj.offset, + }); + } + } else if (storageObjectType.encoding === 'mapping') { + if (storageObjectType.key === undefined || storageObjectType.value === undefined) { + // Should never happen in practice but required to maintain proper typing. + throw new Error(`variable is a mapping but has no key field or has no value field: ${storageObjectType}`); + } + + if (mappingKey === undefined) { + // Throw an error if the user didn't provide a mappingKey + throw new Error(`You need to pass a mapping key to get it's value.`); + } + + // In order to find the value's storage slot we need to calculate the slot key + // The slot key for a mapping is calculated like `keccak256(h(k) . p)` for more information (https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#mappings-and-dynamic-arrays) + // In this part we calculate the `h(k)` where k is the mapping key the user provided and h is a function that is applied to the key depending on its type + let mappKey: string; + if (storageObjectType.key.startsWith('t_uint')) { + mappKey = BigNumber.from(mappingKey).toHexString(); + } else if (storageObjectType.key.startsWith('t_bytes')) { + mappKey = '0x' + remove0x(mappingKey as string).padEnd(64, '0'); + } else { + // Seems to work for everything else. + mappKey = mappingKey as string; + } + + // Since we have `h(k) = mappKey` and `p = key` now we can calculate the slot key + let slotKey = ethers.utils.keccak256(padNumHexSlotValue(mappKey, 0) + remove0x(key)); + // As type we have to provide the type of the mapping value has + slotKeysTypes = slotKeysTypes.concat({ + key: slotKey, + type: storageLayout.types[storageObjectType.value], + }); + } else if (storageObjectType.encoding === 'dynamic_array') { + // We know that the array length is stored in position `key` + let arrayLength = parseInt(toHexString(await vmManager.getContractStorage(toFancyAddress(contractAddress), fromHexString(key))), 16); + + // The values of the array are stored in `keccak256(key)` where key is the storage location of the array + key = ethers.utils.keccak256(key); + for (let i = 0; i < arrayLength; i++) { + // Array values are stored contiguous so we need to calculate the new slot keys in each iteration + let slotKey = BigNumber.from(key) + .add(BigNumber.from(i.toString(16))) + .toHexString(); + slotKeysTypes = slotKeysTypes.concat({ + key: slotKey, + type: storageObjectType, + }); + } + } + + return slotKeysTypes; +} + +/** + * Decodes a single variable from a series of key/value storage slot pairs. Using some storage layout + * as instructions for how to perform this decoding. Works recursively with struct and array types. + * ref: https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#layout-of-state-variables-in-storage + * + * @param slotValueTypePairs StorageSlotKeyValuePairs to decode. + * @returns Variable decoded. + */ +export function decodeVariable(slotValueTypePairs: StorageSlotKeyValuePair | StorageSlotKeyValuePair[]): any { + slotValueTypePairs = slotValueTypePairs instanceof Array ? slotValueTypePairs : [slotValueTypePairs]; + let result: string | any = ''; + const numberOfBytes = parseInt(slotValueTypePairs[0].type.numberOfBytes) * 2; + if (slotValueTypePairs[0].type.encoding === 'inplace') { + if (slotValueTypePairs[0].type.label === 'address' || slotValueTypePairs[0].type.label.startsWith('contract')) { + result = ethers.utils.getAddress('0x' + slotValueTypePairs[0].value.slice(0, numberOfBytes)); + } else if (slotValueTypePairs[0].type.label === 'bool') { + result = slotValueTypePairs[0].value.slice(0, numberOfBytes) === '01' ? true : false; + } else if (slotValueTypePairs[0].type.label.startsWith('bytes')) { + result = '0x' + slotValueTypePairs[0].value.slice(0, numberOfBytes); + } else if (slotValueTypePairs[0].type.label.startsWith('uint')) { + let value = slotValueTypePairs[0].value; + if (slotValueTypePairs[0].offset !== 0 && slotValueTypePairs[0].offset !== undefined) { + value = value.slice(-slotValueTypePairs[0].type.numberOfBytes * 2 - slotValueTypePairs[0].offset * 2, -slotValueTypePairs[0].offset * 2); + } + // When we deal with uint we can just return the number + result = BigNumber.from('0x' + value); + } else if (slotValueTypePairs[0].type.label.startsWith('int')) { + // When we deal with signed integers we have to convert the value from signed hex to decimal + // Doesn't work for negative numbers + // TODO: convert 2's complement hex to decimal to make it work properly + result = parseInt(slotValueTypePairs[0].value, 16).toString(); + } else if (slotValueTypePairs[0].type.label.startsWith('struct')) { + // We remove the first pair since we only need the members now + slotValueTypePairs.shift(); + let structObject = {}; + for (const member of slotValueTypePairs) { + if (member.label === undefined) { + // Should never happen in practice but required to maintain proper typing. + throw new Error(`label for ${member} is undefined`); + } + + if (member.offset === undefined) { + // Should never happen in practice but required to maintain proper typing. + throw new Error(`offset for ${member} is undefined`); + } + + let value; + // If we are dealing with string/bytes we need to decode based on big endian + // otherwise values are stored as little endian so we have to decode based on that + // We use the `offset` and `numberOfBytes` to deal with packed variables + if (member.type.label.startsWith('bytes')) { + value = member.value.slice(member.offset * 2, parseInt(member.type.numberOfBytes) * 2 + member.offset * 2); + } else { + if (member.offset === 0) value = member.value.slice(-member.type.numberOfBytes * 2); + else value = member.value.slice(-member.type.numberOfBytes * 2 - member.offset * 2, -member.offset * 2); + } + + structObject = Object.assign(structObject, { + [member.label]: decodeVariable({ + value: value, + type: member.type, + } as StorageSlotKeyValuePair), + }); + result = structObject; + } + } + } else if (slotValueTypePairs[0].type.encoding === 'bytes') { + for (const slotKeyPair of slotValueTypePairs) { + if (slotKeyPair.length === undefined) { + // Should never happen in practice but required to maintain proper typing. + throw new Error(`length is undefined for bytes: ${slotValueTypePairs[0]}`); + } + if (slotKeyPair.length < 32) { + result = '0x' + result.concat(slotKeyPair.value.slice(0, slotKeyPair.length)); + } else { + result = remove0x(result); + result = '0x' + result.concat(slotKeyPair.value.slice(0, 32)); + } + } + } else if (slotValueTypePairs[0].type.encoding === 'mapping') { + // Should never happen in practise since mappings are handled based on a certain mapping key + throw new Error(`Error in decodeVariable. Encoding: mapping.`); + } else if (slotValueTypePairs[0].type.encoding === 'dynamic_array') { + let arr: any[] = []; + for (let i = 0; i < slotValueTypePairs.length; i++) { + arr = arr.concat( + decodeVariable({ + value: slotValueTypePairs[i].value, + type: { + encoding: 'inplace', + label: slotValueTypePairs[i].type.label, + numberOfBytes: slotValueTypePairs[i].type.numberOfBytes, + }, + }) + ); + } + result = arr; + } + + return result; +} diff --git a/test/contracts/mock/StorageGetter.sol b/test/contracts/mock/StorageGetter.sol index 4e0b148..4987363 100644 --- a/test/contracts/mock/StorageGetter.sol +++ b/test/contracts/mock/StorageGetter.sol @@ -11,7 +11,17 @@ struct PackedStruct { address packedB; } +struct PackedStruct2 { + uint16 packedA; + uint16 packedB; + uint16 packedC; + uint16 packedD; + address packedE; +} + contract StorageGetter { + uint16 _packedUintA; + uint16 _packedUintB; address internal _address; uint256 internal _constructorUint256; int56 internal _int56; @@ -22,6 +32,7 @@ contract StorageGetter { bool internal _bool; SimpleStruct internal _simpleStruct; PackedStruct internal _packedStruct; + PackedStruct2 internal _packedStruct2; mapping(uint256 => uint256) _uint256Map; mapping(uint256 => mapping(uint256 => uint256)) _uint256NestedMap; mapping(bytes5 => bool) _bytes5ToBoolMap; @@ -41,6 +52,14 @@ contract StorageGetter { return _constructorUint256; } + function getPackedUintA() public view returns (uint16 _out) { + return _packedUintA; + } + + function getPackedUintB() public view returns (uint16 _out) { + return _packedUintB; + } + function getInt56() public view returns (int56 _out) { return _int56; } @@ -61,6 +80,14 @@ contract StorageGetter { return _bytes32; } + function setPackedUintA(uint16 _val) external { + _packedUintA = _val; + } + + function setPackedUintB(uint16 _val) external { + _packedUintB = _val; + } + function setPackedA(bool _val) external { _packedA = _val; } @@ -73,6 +100,10 @@ contract StorageGetter { _uint256 = _in; } + function setUint256Array(uint256[] memory _in) public { + _uint256Array = _in; + } + function getBool() public view returns (bool _out) { return _bool; } @@ -89,6 +120,10 @@ contract StorageGetter { return _packedStruct; } + function getPackedStruct2() public view returns (PackedStruct2 memory _out) { + return _packedStruct2; + } + function getUint256MapValue(uint256 _key) public view returns (uint256 _out) { return _uint256Map[_key]; } diff --git a/test/unit/mock/readable-storage-logic.spec.ts b/test/unit/mock/readable-storage-logic.spec.ts new file mode 100644 index 0000000..f66d782 --- /dev/null +++ b/test/unit/mock/readable-storage-logic.spec.ts @@ -0,0 +1,175 @@ +import { MockContract, MockContractFactory, smock } from '@src'; +import { convertStructToPojo } from '@src/utils'; +import { ADDRESS_EXAMPLE, BYTES32_EXAMPLE, BYTES_EXAMPLE } from '@test-utils'; +import { StorageGetter, StorageGetter__factory } from '@typechained'; +import { expect } from 'chai'; +import { BigNumber, utils } from 'ethers'; + +describe('Mock: Readable storage logic', () => { + let storageGetterFactory: MockContractFactory; + let mock: MockContract; + + before(async () => { + storageGetterFactory = await smock.mock('StorageGetter'); + }); + + beforeEach(async () => { + mock = await storageGetterFactory.deploy(1); + }); + + describe('getVariable', () => { + it('should be able to get a uint256', async () => { + const value = utils.parseUnits('123'); + await mock.setVariable('_uint256', value); + expect(await mock.getUint256()).to.equal(value); + + const getValue = await mock.getVariable('_uint256'); + const expectedValue = await mock.getUint256(); + expect(getValue).to.equal(expectedValue); + }); + + it('should be able to get a uint16 in a packed slot', async () => { + const value = BigNumber.from('1'); + const value2 = BigNumber.from('2'); + await mock.setPackedUintA(value); + await mock.setPackedUintB(value2); + expect(await mock.getPackedUintA()).to.equal(value); + expect(await mock.getPackedUintB()).to.equal(value2); + + const getValue = await mock.getVariable('_packedUintB'); + const expectedValue = await mock.getPackedUintB(); + expect(getValue).to.equal(expectedValue); + }); + + it('should be able to get a int56', async () => { + const value = 1; + await mock.setVariable('_int56', value); + expect(await mock.getInt56()).to.equal(value); + + const getValue = await mock.getVariable('_int56'); + const expectedValue = await mock.getInt56(); + expect(getValue.toString()).to.equal(expectedValue.toString()); + }); + + // it('should be able to get a int256', async () => { + // const value = utils.parseUnits('-1'); + // await mock.setVariable('_int256', value); + + // const getValue = await mock.getVariable('_int256'); + // const expectedValue = await mock.getInt256(); + // console.log("getValue: ", getValue); + // expect(getValue).to.equal(expectedValue.toString()); + // }); + + it('should be able to get an address', async () => { + await mock.setVariable('_address', ADDRESS_EXAMPLE); + expect(await mock.getAddress()).to.equal(ADDRESS_EXAMPLE); + + const getValue = await mock.getVariable('_address'); + const expectedValue = await mock.getAddress(); + expect(getValue).to.equal(expectedValue); + }); + + it('should be able to get a boolean', async () => { + await mock.setVariable('_bool', true); + expect(await mock.getBool()).to.equal(true); + + const getValue = await mock.getVariable('_bool'); + const expectedValue = await mock.getBool(); + expect(getValue).to.equal(expectedValue); + }); + + it('should be able to get a small value < 31 bytes', async () => { + const value = BYTES_EXAMPLE.slice(0, 10); + await mock.setVariable('_bytes', value); + expect(await mock.getBytes()).to.equal(value); + + const getValue = await mock.getVariable('_bytes'); + const expectedValue = await mock.getBytes(); + expect(getValue).to.equal(expectedValue); + }); + + it('should be able to get a large value > 31 bytes', async () => { + await mock.setVariable('_bytes', BYTES_EXAMPLE); + expect(await mock.getBytes()).to.equal(BYTES_EXAMPLE); + + const getValue = await mock.getVariable('_bytes'); + const expectedValue = await mock.getBytes(); + expect(getValue).to.equal(expectedValue); + }); + + it('should be able to get bytes32', async () => { + await mock.setVariable('_bytes32', BYTES32_EXAMPLE); + expect(await mock.getBytes32()).to.equal(BYTES32_EXAMPLE); + + const getValue = await mock.getVariable('_bytes32'); + const expectedValue = await mock.getBytes32(); + expect(getValue).to.equal(expectedValue); + }); + + it('should be able to set a simple struct', async () => { + const struct = { + valueA: BigNumber.from(1234), + valueB: true, + }; + await mock.setVariable('_simpleStruct', struct); + + expect(convertStructToPojo(await mock.getSimpleStruct())).to.deep.equal(struct); + + const getValue = await mock.getVariable('_simpleStruct'); + expect(getValue).to.deep.equal(struct); + }); + + it('should be able to get an address in a packed struct', async () => { + const struct = { + packedA: true, + packedB: ADDRESS_EXAMPLE, + }; + await mock.setVariable('_packedStruct', struct); + + expect(convertStructToPojo(await mock.getPackedStruct())).to.deep.equal(struct); + + const getValue = await mock.getVariable('_packedStruct'); + expect(getValue).to.deep.equal(struct); + }); + + it('should be able to get an address in a packed struct', async () => { + const struct = { + packedA: BigNumber.from(2), + packedB: BigNumber.from(1), + packedC: BigNumber.from(2), + packedD: BigNumber.from(1), + packedE: ADDRESS_EXAMPLE, + }; + await mock.setVariable('_packedStruct2', struct); + const getValue = await mock.getVariable('_packedStruct2'); + expect(getValue).to.deep.equal(struct); + }); + + it('should be able to get a uint256 mapping value', async () => { + const mapKey = 1234; + const mapValue = 5678; + await mock.setVariable('_uint256Map', { [mapKey]: mapValue }); + + expect(await mock.getUint256MapValue(mapKey)).to.equal(mapValue); + const getValue = await mock.getVariable('_uint256Map', mapKey); + expect(getValue).to.equal(await mock.getUint256MapValue(mapKey)); + }); + + it('should be able to gry values in a bytes5 => bool mapping', async () => { + const mapKey = '0x0000005678'; + const mapValue = true; + await mock.setVariable('_bytes5ToBoolMap', { [mapKey]: mapValue }); + + expect(await mock.getBytes5ToBoolMapValue(mapKey)).to.equal(mapValue); + const getValue = await mock.getVariable('_bytes5ToBoolMap', mapKey); + expect(getValue).to.equal(await mock.getBytes5ToBoolMapValue(mapKey)); + }); + + it('should be able to get a uint256[] variable', async () => { + await mock.setUint256Array([1, 2, 3]); + const getValue = await mock.getVariable('_uint256Array'); + expect(getValue).to.deep.equal(await mock.getUint256Array()); + }); + }); +}); From 5d594d1a832c1158552572492b1501d890d3e41b Mon Sep 17 00:00:00 2001 From: Antonis Kogias Date: Wed, 6 Jul 2022 16:38:46 +0300 Subject: [PATCH 2/7] fix: resolve some comments & support nested mappings --- docs/source/mocks.rst | 9 +- src/logic/readable-storage-logic.ts | 45 ++++---- src/utils/storage.ts | 61 ++++++---- test/contracts/mock/StorageGetter.sol | 10 -- test/unit/mock/editable-storage-logic.spec.ts | 7 +- test/unit/mock/readable-storage-logic.spec.ts | 105 ++++++++---------- 6 files changed, 127 insertions(+), 110 deletions(-) diff --git a/docs/source/mocks.rst b/docs/source/mocks.rst index f6d391c..dde017b 100644 --- a/docs/source/mocks.rst +++ b/docs/source/mocks.rst @@ -146,4 +146,11 @@ Getting the value of an internal mapping given the value's key .. code-block:: typescript - const myMappingValue = await myMock.getVariable('myMappingVariableName', mappingKey); + const myMappingValue = await myMock.getVariable('myMappingVariableName', [mappingKey]); + +Getting the value of an internal nested mapping given the value's keys +******************** + +.. code-block:: typescript + + const myMappingValue = await myMock.getVariable('myMappingVariableName', [mappingKeyA, mappingKeyB]); diff --git a/src/logic/readable-storage-logic.ts b/src/logic/readable-storage-logic.ts index 3bf1df3..987b112 100644 --- a/src/logic/readable-storage-logic.ts +++ b/src/logic/readable-storage-logic.ts @@ -1,37 +1,42 @@ import { SmockVMManager } from '../types'; import { fromHexString, remove0x, toFancyAddress, toHexString } from '../utils'; -import { decodeVariable, getVariableStorageSlots, StorageSlotKeyValuePair } from '../utils/storage'; +import { + decodeVariable, + getVariableStorageSlots, + SolidityStorageLayout, + StorageSlotKeyTypePair, + StorageSlotKeyValuePair, +} from '../utils/storage'; export class ReadableStorageLogic { - private storageLayout: any; + private storageLayout: SolidityStorageLayout; private contractAddress: string; private vmManager: SmockVMManager; - constructor(storageLayout: any, vmManager: SmockVMManager, contractAddress: string) { + constructor(storageLayout: SolidityStorageLayout, vmManager: SmockVMManager, contractAddress: string) { this.storageLayout = storageLayout; this.vmManager = vmManager; this.contractAddress = contractAddress; } - async getVariable(variableName: string, mappingKey?: string | number): Promise { - const slots = await getVariableStorageSlots(this.storageLayout, variableName, this.vmManager, this.contractAddress, mappingKey); - - let slotValueTypePairs: StorageSlotKeyValuePair[] = []; - - for (const slotKeyPair of slots) { - slotValueTypePairs = slotValueTypePairs.concat({ + async getVariable(variableName: string): Promise; + async getVariable(variableName: string, mappingKeys: string[] | number[]): Promise; + async getVariable(variableName: string, mappingKeys?: string[] | number[]): Promise { + const slots: StorageSlotKeyTypePair[] = await getVariableStorageSlots( + this.storageLayout, + variableName, + this.vmManager, + this.contractAddress, + mappingKeys + ); + const slotValueTypePairs: StorageSlotKeyValuePair[] = await Promise.all( + slots.map(async (slotKeyPair) => ({ + ...slotKeyPair, value: remove0x( toHexString(await this.vmManager.getContractStorage(toFancyAddress(this.contractAddress), fromHexString(slotKeyPair.key))) ), - type: slotKeyPair.type, - length: slotKeyPair.length, - label: slotKeyPair.label, - offset: slotKeyPair.offset, - }); - } - - const result = decodeVariable(slotValueTypePairs); - - return result; + })) + ); + return decodeVariable(slotValueTypePairs); } } diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 61ddab7..8a96683 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -45,7 +45,7 @@ interface StorageSlotPair { // This object represents the storage slot that a variable is stored (key) // and the type of the variable (type). -interface StorageSlotKeyTypePair { +export interface StorageSlotKeyTypePair { key: string; type: SolidityStorageType; length?: number; // used only for bytes type, helps during decoding @@ -421,6 +421,10 @@ function encodeVariable( * @param vmManager SmockVMManager is used to get certain storage values given a specific slot key and a contract address * @param contractAddress Contract address to use for vmManager * @param mappingKey Only used for mappings, represents they key of a mapping value + * @param baseSlotKey Only used for maps. Keeps track of the base slot that other elements of the + * mapping need to work off of. + * @param storageType Only used for nested mappings. Since we can't get the SolidityStorageObj of a nested mapping value + we need to pass it's SolidityStorageType to work from * @returns An array of storage slot key/type pair that would result in the value of the variable. */ export async function getVariableStorageSlots( @@ -428,29 +432,32 @@ export async function getVariableStorageSlots( variableName: string, vmManager: SmockVMManager, contractAddress: string, - mappingKey?: string | number + mappingKey?: any[] | number | string, + baseSlotKey?: string, + storageType?: SolidityStorageType ): Promise { // Find the entry in the storage layout that corresponds to this variable name. const storageObj = storageLayout.storage.find((entry) => { return entry.label === variableName; }); - // Complain very loudly if attempting to set a variable that doesn't exist within this layout. + // Complain very loudly if attempting to get a variable that doesn't exist within this layout. if (!storageObj) { throw new Error(`Variable name not found in storage layout: ${variableName}`); } - const storageObjectType: SolidityStorageType = storageLayout.types[storageObj.type]; + const storageObjectType: SolidityStorageType = storageType || storageLayout.types[storageObj.type]; // Here we will store all the key/type pairs that we need to get the variable's value let slotKeysTypes: StorageSlotKeyTypePair[] = []; let key: string = + baseSlotKey || '0x' + - remove0x( - BigNumber.from(0) - .add(BigNumber.from(parseInt(storageObj.slot, 10))) - .toHexString() - ).padStart(64, '0'); + remove0x( + BigNumber.from(0) + .add(BigNumber.from(parseInt(storageObj.slot, 10))) + .toHexString() + ).padStart(64, '0'); if (storageObjectType.encoding === 'inplace') { // For `inplace` encoding we only need to be aware of structs where they take more slots to store a variable @@ -513,34 +520,46 @@ export async function getVariableStorageSlots( } else if (storageObjectType.encoding === 'mapping') { if (storageObjectType.key === undefined || storageObjectType.value === undefined) { // Should never happen in practice but required to maintain proper typing. - throw new Error(`variable is a mapping but has no key field or has no value field: ${storageObjectType}`); + throw new Error(`Variable is a mapping but has no key field or has no value field: ${storageObjectType}`); } if (mappingKey === undefined) { // Throw an error if the user didn't provide a mappingKey - throw new Error(`You need to pass a mapping key to get it's value.`); + throw new Error(`Mapping key must be provided to get variable value: ${variableName}`); } - + mappingKey = mappingKey instanceof Array ? mappingKey : [mappingKey]; // In order to find the value's storage slot we need to calculate the slot key // The slot key for a mapping is calculated like `keccak256(h(k) . p)` for more information (https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#mappings-and-dynamic-arrays) // In this part we calculate the `h(k)` where k is the mapping key the user provided and h is a function that is applied to the key depending on its type let mappKey: string; if (storageObjectType.key.startsWith('t_uint')) { - mappKey = BigNumber.from(mappingKey).toHexString(); + mappKey = BigNumber.from(mappingKey[0]).toHexString(); } else if (storageObjectType.key.startsWith('t_bytes')) { - mappKey = '0x' + remove0x(mappingKey as string).padEnd(64, '0'); + mappKey = '0x' + remove0x(mappingKey[0] as string).padEnd(64, '0'); } else { // Seems to work for everything else. - mappKey = mappingKey as string; + mappKey = mappingKey[0] as string; } + // Figure out the base slot key that the mapped values need to work off of. + // If baseSlotKey is defined here, then we're inside of a nested mapping and we should work + // off of that previous baseSlotKey. Otherwise the base slot will be the key we already have. + const prevBaseSlotKey = baseSlotKey || key; // Since we have `h(k) = mappKey` and `p = key` now we can calculate the slot key - let slotKey = ethers.utils.keccak256(padNumHexSlotValue(mappKey, 0) + remove0x(key)); - // As type we have to provide the type of the mapping value has - slotKeysTypes = slotKeysTypes.concat({ - key: slotKey, - type: storageLayout.types[storageObjectType.value], - }); + let nextSlotKey = ethers.utils.keccak256(padNumHexSlotValue(mappKey, 0) + remove0x(prevBaseSlotKey)); + + mappingKey.shift(); + slotKeysTypes = slotKeysTypes.concat( + await getVariableStorageSlots( + storageLayout, + variableName, + vmManager, + contractAddress, + mappingKey, + nextSlotKey, + storageLayout.types[storageObjectType.value] + ) + ); } else if (storageObjectType.encoding === 'dynamic_array') { // We know that the array length is stored in position `key` let arrayLength = parseInt(toHexString(await vmManager.getContractStorage(toFancyAddress(contractAddress), fromHexString(key))), 16); diff --git a/test/contracts/mock/StorageGetter.sol b/test/contracts/mock/StorageGetter.sol index 4987363..83a279d 100644 --- a/test/contracts/mock/StorageGetter.sol +++ b/test/contracts/mock/StorageGetter.sol @@ -7,11 +7,6 @@ struct SimpleStruct { } struct PackedStruct { - bool packedA; - address packedB; -} - -struct PackedStruct2 { uint16 packedA; uint16 packedB; uint16 packedC; @@ -32,7 +27,6 @@ contract StorageGetter { bool internal _bool; SimpleStruct internal _simpleStruct; PackedStruct internal _packedStruct; - PackedStruct2 internal _packedStruct2; mapping(uint256 => uint256) _uint256Map; mapping(uint256 => mapping(uint256 => uint256)) _uint256NestedMap; mapping(bytes5 => bool) _bytes5ToBoolMap; @@ -120,10 +114,6 @@ contract StorageGetter { return _packedStruct; } - function getPackedStruct2() public view returns (PackedStruct2 memory _out) { - return _packedStruct2; - } - function getUint256MapValue(uint256 _key) public view returns (uint256 _out) { return _uint256Map[_key]; } diff --git a/test/unit/mock/editable-storage-logic.spec.ts b/test/unit/mock/editable-storage-logic.spec.ts index b6d16ae..3796a93 100644 --- a/test/unit/mock/editable-storage-logic.spec.ts +++ b/test/unit/mock/editable-storage-logic.spec.ts @@ -70,8 +70,11 @@ describe('Mock: Editable storage logic', () => { it('should be able to set an address in a packed struct', async () => { const struct = { - packedA: true, - packedB: ADDRESS_EXAMPLE, + packedA: 2, + packedB: 1, + packedC: 2, + packedD: 1, + packedE: ADDRESS_EXAMPLE, }; await mock.setVariable('_packedStruct', struct); diff --git a/test/unit/mock/readable-storage-logic.spec.ts b/test/unit/mock/readable-storage-logic.spec.ts index f66d782..08b85fc 100644 --- a/test/unit/mock/readable-storage-logic.spec.ts +++ b/test/unit/mock/readable-storage-logic.spec.ts @@ -1,5 +1,4 @@ import { MockContract, MockContractFactory, smock } from '@src'; -import { convertStructToPojo } from '@src/utils'; import { ADDRESS_EXAMPLE, BYTES32_EXAMPLE, BYTES_EXAMPLE } from '@test-utils'; import { StorageGetter, StorageGetter__factory } from '@typechained'; import { expect } from 'chai'; @@ -21,11 +20,9 @@ describe('Mock: Readable storage logic', () => { it('should be able to get a uint256', async () => { const value = utils.parseUnits('123'); await mock.setVariable('_uint256', value); - expect(await mock.getUint256()).to.equal(value); const getValue = await mock.getVariable('_uint256'); - const expectedValue = await mock.getUint256(); - expect(getValue).to.equal(expectedValue); + expect(getValue).to.equal(await mock.getUint256()); }); it('should be able to get a uint16 in a packed slot', async () => { @@ -33,78 +30,62 @@ describe('Mock: Readable storage logic', () => { const value2 = BigNumber.from('2'); await mock.setPackedUintA(value); await mock.setPackedUintB(value2); - expect(await mock.getPackedUintA()).to.equal(value); - expect(await mock.getPackedUintB()).to.equal(value2); const getValue = await mock.getVariable('_packedUintB'); - const expectedValue = await mock.getPackedUintB(); - expect(getValue).to.equal(expectedValue); + expect(getValue).to.equal(await mock.getPackedUintB()); }); it('should be able to get a int56', async () => { const value = 1; await mock.setVariable('_int56', value); - expect(await mock.getInt56()).to.equal(value); const getValue = await mock.getVariable('_int56'); - const expectedValue = await mock.getInt56(); - expect(getValue.toString()).to.equal(expectedValue.toString()); + expect(getValue).to.equal(await mock.getInt56()); }); - // it('should be able to get a int256', async () => { - // const value = utils.parseUnits('-1'); - // await mock.setVariable('_int256', value); + // TODO: Make this work for negatives + it.skip('should be able to get a int256', async () => { + const value = utils.parseUnits('-1'); + await mock.setVariable('_int256', value); - // const getValue = await mock.getVariable('_int256'); - // const expectedValue = await mock.getInt256(); - // console.log("getValue: ", getValue); - // expect(getValue).to.equal(expectedValue.toString()); - // }); + const getValue = await mock.getVariable('_int256'); + expect(getValue).to.equal(await mock.getInt256()); + }); it('should be able to get an address', async () => { await mock.setVariable('_address', ADDRESS_EXAMPLE); - expect(await mock.getAddress()).to.equal(ADDRESS_EXAMPLE); const getValue = await mock.getVariable('_address'); - const expectedValue = await mock.getAddress(); - expect(getValue).to.equal(expectedValue); + expect(getValue).to.equal(await mock.getAddress()); }); it('should be able to get a boolean', async () => { await mock.setVariable('_bool', true); - expect(await mock.getBool()).to.equal(true); const getValue = await mock.getVariable('_bool'); - const expectedValue = await mock.getBool(); - expect(getValue).to.equal(expectedValue); + expect(getValue).to.equal(await mock.getBool()); }); it('should be able to get a small value < 31 bytes', async () => { const value = BYTES_EXAMPLE.slice(0, 10); await mock.setVariable('_bytes', value); - expect(await mock.getBytes()).to.equal(value); const getValue = await mock.getVariable('_bytes'); - const expectedValue = await mock.getBytes(); - expect(getValue).to.equal(expectedValue); + expect(getValue).to.equal(await mock.getBytes()); }); it('should be able to get a large value > 31 bytes', async () => { await mock.setVariable('_bytes', BYTES_EXAMPLE); - expect(await mock.getBytes()).to.equal(BYTES_EXAMPLE); const getValue = await mock.getVariable('_bytes'); - const expectedValue = await mock.getBytes(); - expect(getValue).to.equal(expectedValue); + expect(getValue).to.equal(await mock.getBytes()); }); it('should be able to get bytes32', async () => { await mock.setVariable('_bytes32', BYTES32_EXAMPLE); - expect(await mock.getBytes32()).to.equal(BYTES32_EXAMPLE); const getValue = await mock.getVariable('_bytes32'); - const expectedValue = await mock.getBytes32(); - expect(getValue).to.equal(expectedValue); + expect(getValue).to.equal(await mock.getBytes32()); }); it('should be able to set a simple struct', async () => { @@ -114,25 +95,10 @@ describe('Mock: Readable storage logic', () => { }; await mock.setVariable('_simpleStruct', struct); - expect(convertStructToPojo(await mock.getSimpleStruct())).to.deep.equal(struct); - const getValue = await mock.getVariable('_simpleStruct'); expect(getValue).to.deep.equal(struct); }); - it('should be able to get an address in a packed struct', async () => { - const struct = { - packedA: true, - packedB: ADDRESS_EXAMPLE, - }; - await mock.setVariable('_packedStruct', struct); - - expect(convertStructToPojo(await mock.getPackedStruct())).to.deep.equal(struct); - - const getValue = await mock.getVariable('_packedStruct'); - expect(getValue).to.deep.equal(struct); - }); - it('should be able to get an address in a packed struct', async () => { const struct = { packedA: BigNumber.from(2), @@ -141,31 +107,58 @@ describe('Mock: Readable storage logic', () => { packedD: BigNumber.from(1), packedE: ADDRESS_EXAMPLE, }; - await mock.setVariable('_packedStruct2', struct); - const getValue = await mock.getVariable('_packedStruct2'); + await mock.setVariable('_packedStruct', struct); + + const getValue = await mock.getVariable('_packedStruct'); expect(getValue).to.deep.equal(struct); }); + // it('should be able to get an address in a packed struct', async () => { + // const struct = { + // packedA: BigNumber.from(2), + // packedB: BigNumber.from(1), + // packedC: BigNumber.from(2), + // packedD: BigNumber.from(1), + // packedE: ADDRESS_EXAMPLE, + // }; + // await mock.setVariable('_packedStruct2', struct); + // const getValue = await mock.getVariable('_packedStruct2'); + // expect(getValue).to.deep.equal(struct); + // }); + it('should be able to get a uint256 mapping value', async () => { const mapKey = 1234; const mapValue = 5678; await mock.setVariable('_uint256Map', { [mapKey]: mapValue }); - expect(await mock.getUint256MapValue(mapKey)).to.equal(mapValue); - const getValue = await mock.getVariable('_uint256Map', mapKey); + const getValue = await mock.getVariable('_uint256Map', [mapKey]); expect(getValue).to.equal(await mock.getUint256MapValue(mapKey)); }); - it('should be able to gry values in a bytes5 => bool mapping', async () => { + it('should be able to get values in a bytes5 => bool mapping', async () => { const mapKey = '0x0000005678'; const mapValue = true; await mock.setVariable('_bytes5ToBoolMap', { [mapKey]: mapValue }); - expect(await mock.getBytes5ToBoolMapValue(mapKey)).to.equal(mapValue); - const getValue = await mock.getVariable('_bytes5ToBoolMap', mapKey); + const getValue = await mock.getVariable('_bytes5ToBoolMap', [mapKey]); expect(getValue).to.equal(await mock.getBytes5ToBoolMapValue(mapKey)); }); + it('should be able to get a nested uint256 mapping value', async () => { + const mapKeyA = 1234; + const mapKeyB = 4321; + const mapVal = 5678; + + await mock.setVariable('_uint256NestedMap', { + [mapKeyA]: { + [mapKeyB]: mapVal, + }, + }); + + const getValue = await mock.getVariable('_uint256NestedMap', [mapKeyA, mapKeyB]); + expect(getValue).to.equal(await mock.getNestedUint256MapValue(mapKeyA, mapKeyB)); + }); + it('should be able to get a uint256[] variable', async () => { await mock.setUint256Array([1, 2, 3]); const getValue = await mock.getVariable('_uint256Array'); From a50af2611ac7868f1ed0716cc2db15e79b949a8d Mon Sep 17 00:00:00 2001 From: Antonis Kogias Date: Wed, 6 Jul 2022 21:05:29 +0300 Subject: [PATCH 3/7] fix: split each type logic into functions --- src/utils/storage.ts | 254 +++++++++++------- test/unit/mock/readable-storage-logic.spec.ts | 2 +- 2 files changed, 161 insertions(+), 95 deletions(-) diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 8a96683..6a4710a 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -462,120 +462,186 @@ export async function getVariableStorageSlots( if (storageObjectType.encoding === 'inplace') { // For `inplace` encoding we only need to be aware of structs where they take more slots to store a variable if (storageObjectType.label.startsWith('struct')) { - if (storageObjectType.members === undefined) { - throw new Error(`There are no members in object type ${storageObjectType}`); - } - // Slot key that represents the struct - slotKeysTypes = slotKeysTypes.concat({ - key: key, - type: storageObjectType, - offset: storageObj.offset, - }); - // These slots are for the members of the struct - for (let i = 0; i < storageObjectType.members.length; i++) { - // We calculate the slot key for each member - key = '0x' + remove0x(BigNumber.from(key).add(BigNumber.from(storageObjectType.members[i].slot)).toHexString()).padStart(64, '0'); - slotKeysTypes = slotKeysTypes.concat({ - key: key, - type: storageLayout.types[storageObjectType.members[i].type], - label: storageObjectType.members[i].label, - offset: storageObjectType.members[i].offset, - }); - } + slotKeysTypes = await getStructTypeStorageSlots(storageLayout, key, storageObjectType, storageObj); } else { // In cases we deal with other types than structs we already know the slot key and type slotKeysTypes = slotKeysTypes.concat({ key: key, type: storageObjectType, offset: storageObj.offset, + label: storageObj.label, }); } } else if (storageObjectType.encoding === 'bytes') { - // The last 2 bytes of the slot represent the length of the string/bytes variable - // If it's bigger than 31 then we have to deal with a long string/bytes array - const bytesValue = toHexString(await vmManager.getContractStorage(toFancyAddress(contractAddress), fromHexString(key))); - // It is known that if the last byte is set then we are dealing with a long string - // if it is 0 then we are dealing with a short string, you can find more details here (https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#bytes-and-string) - if (bytesValue.slice(-1) === '1') { - // We calculate the total number of slots that this long string/bytes use - const numberOfSlots = Math.ceil((parseInt(bytesValue, 16) - 1) / 32); - // Since we are dealing with bytes, their values are stored contiguous - // we are storing their slotkeys, type and the length which will help us in `decodeVariable` - for (let i = 0; i < numberOfSlots; i++) { - slotKeysTypes = slotKeysTypes.concat({ - key: ethers.utils.keccak256(key) + i, - type: storageObjectType, - length: i + 1 <= numberOfSlots ? 32 : (parseInt(bytesValue, 16) - 1) % 32, - }); - } - } else { - // If we are dealing with a short string/bytes then we already know the slotkey, type & length - slotKeysTypes = slotKeysTypes.concat({ - key: key, - type: storageObjectType, - length: parseInt(bytesValue.slice(-2), 16), - offset: storageObj.offset, - }); - } + slotKeysTypes = await getBytesTypeStorageSlots(vmManager, contractAddress, storageObjectType, storageObj, key); } else if (storageObjectType.encoding === 'mapping') { - if (storageObjectType.key === undefined || storageObjectType.value === undefined) { - // Should never happen in practice but required to maintain proper typing. - throw new Error(`Variable is a mapping but has no key field or has no value field: ${storageObjectType}`); - } - if (mappingKey === undefined) { // Throw an error if the user didn't provide a mappingKey throw new Error(`Mapping key must be provided to get variable value: ${variableName}`); } - mappingKey = mappingKey instanceof Array ? mappingKey : [mappingKey]; - // In order to find the value's storage slot we need to calculate the slot key - // The slot key for a mapping is calculated like `keccak256(h(k) . p)` for more information (https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#mappings-and-dynamic-arrays) - // In this part we calculate the `h(k)` where k is the mapping key the user provided and h is a function that is applied to the key depending on its type - let mappKey: string; - if (storageObjectType.key.startsWith('t_uint')) { - mappKey = BigNumber.from(mappingKey[0]).toHexString(); - } else if (storageObjectType.key.startsWith('t_bytes')) { - mappKey = '0x' + remove0x(mappingKey[0] as string).padEnd(64, '0'); - } else { - // Seems to work for everything else. - mappKey = mappingKey[0] as string; - } - - // Figure out the base slot key that the mapped values need to work off of. - // If baseSlotKey is defined here, then we're inside of a nested mapping and we should work - // off of that previous baseSlotKey. Otherwise the base slot will be the key we already have. - const prevBaseSlotKey = baseSlotKey || key; - // Since we have `h(k) = mappKey` and `p = key` now we can calculate the slot key - let nextSlotKey = ethers.utils.keccak256(padNumHexSlotValue(mappKey, 0) + remove0x(prevBaseSlotKey)); - - mappingKey.shift(); - slotKeysTypes = slotKeysTypes.concat( - await getVariableStorageSlots( - storageLayout, - variableName, - vmManager, - contractAddress, - mappingKey, - nextSlotKey, - storageLayout.types[storageObjectType.value] - ) + slotKeysTypes = await getMappingTypeStorageSlots( + storageLayout, + variableName, + vmManager, + contractAddress, + key, + storageObjectType, + mappingKey ); } else if (storageObjectType.encoding === 'dynamic_array') { - // We know that the array length is stored in position `key` - let arrayLength = parseInt(toHexString(await vmManager.getContractStorage(toFancyAddress(contractAddress), fromHexString(key))), 16); - - // The values of the array are stored in `keccak256(key)` where key is the storage location of the array - key = ethers.utils.keccak256(key); - for (let i = 0; i < arrayLength; i++) { - // Array values are stored contiguous so we need to calculate the new slot keys in each iteration - let slotKey = BigNumber.from(key) - .add(BigNumber.from(i.toString(16))) - .toHexString(); + slotKeysTypes = await getDynamicArrayTypeStorageSlots(vmManager, contractAddress, storageObjectType, key); + } + + return slotKeysTypes; +} + +function getStructTypeStorageSlots( + storageLayout: SolidityStorageLayout, + key: string, + storageObjectType: SolidityStorageType, + storageObj: SolidityStorageObj +): StorageSlotKeyTypePair[] { + if (storageObjectType.members === undefined) { + throw new Error(`There are no members in object type ${storageObjectType}`); + } + + let slotKeysTypes: StorageSlotKeyTypePair[] = []; + // Slot key that represents the struct + slotKeysTypes = slotKeysTypes.concat({ + key: key, + type: storageObjectType, + label: storageObj.label, + offset: storageObj.offset, + }); + + // These slots are for the members of the struct + slotKeysTypes = slotKeysTypes.concat( + storageObjectType.members.map((member) => ({ + key: '0x' + remove0x(BigNumber.from(key).add(BigNumber.from(member.slot)).toHexString()).padStart(64, '0'), + type: storageLayout.types[member.type], + label: member.label, + offset: member.offset, + })) + ); + + return slotKeysTypes; +} + +async function getBytesTypeStorageSlots( + vmManager: SmockVMManager, + contractAddress: string, + storageObjectType: SolidityStorageType, + storageObj: SolidityStorageObj, + key: string +): Promise { + let slotKeysTypes: StorageSlotKeyTypePair[] = []; + // The last 2 bytes of the slot represent the length of the string/bytes variable + // If it's bigger than 31 then we have to deal with a long string/bytes array + const bytesValue = toHexString(await vmManager.getContractStorage(toFancyAddress(contractAddress), fromHexString(key))); + // It is known that if the last byte is set then we are dealing with a long string + // if it is 0 then we are dealing with a short string, you can find more details here (https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#bytes-and-string) + if (bytesValue.slice(-1) === '1') { + // We calculate the total number of slots that this long string/bytes use + const numberOfSlots = Math.ceil((parseInt(bytesValue, 16) - 1) / 32); + // Since we are dealing with bytes, their values are stored contiguous + // we are storing their slotkeys, type and the length which will help us in `decodeVariable` + for (let i = 0; i < numberOfSlots; i++) { slotKeysTypes = slotKeysTypes.concat({ - key: slotKey, + key: ethers.utils.keccak256(key) + i, type: storageObjectType, + length: i + 1 <= numberOfSlots ? 32 : (parseInt(bytesValue, 16) - 1) % 32, + label: storageObj.label, + offset: storageObj.offset, }); } + } else { + // If we are dealing with a short string/bytes then we already know the slotkey, type & length + slotKeysTypes = slotKeysTypes.concat({ + key: key, + type: storageObjectType, + length: parseInt(bytesValue.slice(-2), 16), + label: storageObj.label, + offset: storageObj.offset, + }); + } + + return slotKeysTypes; +} + +async function getMappingTypeStorageSlots( + storageLayout: SolidityStorageLayout, + variableName: string, + vmManager: SmockVMManager, + contractAddress: string, + key: string, + storageObjectType: SolidityStorageType, + mappingKey: any[] | number | string, + baseSlotKey?: string +): Promise { + if (storageObjectType.key === undefined || storageObjectType.value === undefined) { + // Should never happen in practice but required to maintain proper typing. + throw new Error(`Variable is a mapping but has no key field or has no value field: ${storageObjectType}`); + } + mappingKey = mappingKey instanceof Array ? mappingKey : [mappingKey]; + // In order to find the value's storage slot we need to calculate the slot key + // The slot key for a mapping is calculated like `keccak256(h(k) . p)` for more information (https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#mappings-and-dynamic-arrays) + // In this part we calculate the `h(k)` where k is the mapping key the user provided and h is a function that is applied to the key depending on its type + let mappKey: string; + if (storageObjectType.key.startsWith('t_uint')) { + mappKey = BigNumber.from(mappingKey[0]).toHexString(); + } else if (storageObjectType.key.startsWith('t_bytes')) { + mappKey = '0x' + remove0x(mappingKey[0] as string).padEnd(64, '0'); + } else { + // Seems to work for everything else. + mappKey = mappingKey[0] as string; + } + + // Figure out the base slot key that the mapped values need to work off of. + // If baseSlotKey is defined here, then we're inside of a nested mapping and we should work + // off of that previous baseSlotKey. Otherwise the base slot will be the key we already have. + const prevBaseSlotKey = baseSlotKey || key; + // Since we have `h(k) = mappKey` and `p = key` now we can calculate the slot key + let nextSlotKey = ethers.utils.keccak256(padNumHexSlotValue(mappKey, 0) + remove0x(prevBaseSlotKey)); + + let slotKeysTypes: StorageSlotKeyTypePair[] = []; + + mappingKey.shift(); + slotKeysTypes = slotKeysTypes.concat( + await getVariableStorageSlots( + storageLayout, + variableName, + vmManager, + contractAddress, + mappingKey, + nextSlotKey, + storageLayout.types[storageObjectType.value] + ) + ); + + return slotKeysTypes; +} + +async function getDynamicArrayTypeStorageSlots( + vmManager: SmockVMManager, + contractAddress: string, + storageObjectType: SolidityStorageType, + key: string +): Promise { + let slotKeysTypes: StorageSlotKeyTypePair[] = []; + // We know that the array length is stored in position `key` + let arrayLength = parseInt(toHexString(await vmManager.getContractStorage(toFancyAddress(contractAddress), fromHexString(key))), 16); + + // The values of the array are stored in `keccak256(key)` where key is the storage location of the array + key = ethers.utils.keccak256(key); + for (let i = 0; i < arrayLength; i++) { + // Array values are stored contiguous so we need to calculate the new slot keys in each iteration + let slotKey = BigNumber.from(key) + .add(BigNumber.from(i.toString(16))) + .toHexString(); + slotKeysTypes = slotKeysTypes.concat({ + key: slotKey, + type: storageObjectType, + }); } return slotKeysTypes; diff --git a/test/unit/mock/readable-storage-logic.spec.ts b/test/unit/mock/readable-storage-logic.spec.ts index 08b85fc..d1835c6 100644 --- a/test/unit/mock/readable-storage-logic.spec.ts +++ b/test/unit/mock/readable-storage-logic.spec.ts @@ -88,7 +88,7 @@ describe('Mock: Readable storage logic', () => { expect(getValue).to.equal(await mock.getBytes32()); }); - it('should be able to set a simple struct', async () => { + it('should be able to get a simple struct', async () => { const struct = { valueA: BigNumber.from(1234), valueB: true, From ed30ea7bb0758d01eb7bce102560aa4c962d27cd Mon Sep 17 00:00:00 2001 From: Antonis Kogias Date: Thu, 7 Jul 2022 01:57:41 +0300 Subject: [PATCH 4/7] fix: bug with negative ints --- src/utils/hex-utils.ts | 18 +++++++++++++++++ src/utils/storage.ts | 20 +++++++++++++++---- test/unit/mock/readable-storage-logic.spec.ts | 20 +++---------------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/utils/hex-utils.ts b/src/utils/hex-utils.ts index e36e593..92436d6 100644 --- a/src/utils/hex-utils.ts +++ b/src/utils/hex-utils.ts @@ -101,3 +101,21 @@ function bitnot(bi: BigInt) { .join(''); return BigInt('0b' + prefix + bin) + BigInt(1); } + +/** + * XOR operation between 2 Buffers + * Source: https://github.com/crypto-browserify/buffer-xor/blob/master/index.js + * @param a Buffer to XOR + * @param b Buffer is the mask + * @returns hex representation of the big number + */ +export function xor(a: Buffer, b: Buffer) { + var length = Math.max(a.length, b.length); + var buffer = Buffer.allocUnsafe(length); + + for (var i = 0; i < length; ++i) { + buffer[i] = a[i] ^ b[i]; + } + + return buffer; +} diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 6a4710a..f47a6da 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -2,7 +2,7 @@ import { BigNumber, ethers } from 'ethers'; import { artifacts } from 'hardhat'; import semver from 'semver'; import { SmockVMManager } from '../types'; -import { bigNumberToHex, fromHexString, remove0x, toFancyAddress, toHexString } from '../utils'; +import { bigNumberToHex, fromHexString, remove0x, toFancyAddress, toHexString, xor } from '../utils'; // Represents the JSON objects outputted by the Solidity compiler that describe the structure of // state within the contract. See @@ -675,9 +675,21 @@ export function decodeVariable(slotValueTypePairs: StorageSlotKeyValuePair | Sto result = BigNumber.from('0x' + value); } else if (slotValueTypePairs[0].type.label.startsWith('int')) { // When we deal with signed integers we have to convert the value from signed hex to decimal - // Doesn't work for negative numbers - // TODO: convert 2's complement hex to decimal to make it work properly - result = parseInt(slotValueTypePairs[0].value, 16).toString(); + + let intHex = slotValueTypePairs[0].value; + // If the first character is `f` then we know we have to deal with a negative number + if (intHex.slice(0, 1) === 'f') { + // In order to get the negative number we need to find the two's complement of the hex value (more info: https://en.wikipedia.org/wiki/Two%27s_complement) + // To do that we have to XOR our hex with the appropriate mask and then add 1 to the result + // First convert the hexStrings to Buffer in order to XOR them + intHex = fromHexString('0x' + intHex); + // We choose this mask because we want to flip all the hex bytes in order to find the two's complement + const mask = fromHexString('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'); + // After the XOR and the addition we have the positive number of the original hex value, we want the negative value so we add `-` infront + intHex = -BigNumber.from(toHexString(xor(intHex, mask))).add(BigNumber.from(1)); + } + + result = intHex; } else if (slotValueTypePairs[0].type.label.startsWith('struct')) { // We remove the first pair since we only need the members now slotValueTypePairs.shift(); diff --git a/test/unit/mock/readable-storage-logic.spec.ts b/test/unit/mock/readable-storage-logic.spec.ts index d1835c6..751322e 100644 --- a/test/unit/mock/readable-storage-logic.spec.ts +++ b/test/unit/mock/readable-storage-logic.spec.ts @@ -35,7 +35,7 @@ describe('Mock: Readable storage logic', () => { expect(getValue).to.equal(await mock.getPackedUintB()); }); - it('should be able to get a int56', async () => { + it('should be able to get an int56', async () => { const value = 1; await mock.setVariable('_int56', value); @@ -43,9 +43,8 @@ describe('Mock: Readable storage logic', () => { expect(getValue).to.equal(await mock.getInt56()); }); - // TODO: Make this work for negatives - it.skip('should be able to get a int256', async () => { - const value = utils.parseUnits('-1'); + it('should be able to get an int256', async () => { + const value = BigNumber.from(-1); await mock.setVariable('_int256', value); const getValue = await mock.getVariable('_int256'); @@ -113,19 +112,6 @@ describe('Mock: Readable storage logic', () => { expect(getValue).to.deep.equal(struct); }); - // it('should be able to get an address in a packed struct', async () => { - // const struct = { - // packedA: BigNumber.from(2), - // packedB: BigNumber.from(1), - // packedC: BigNumber.from(2), - // packedD: BigNumber.from(1), - // packedE: ADDRESS_EXAMPLE, - // }; - // await mock.setVariable('_packedStruct2', struct); - // const getValue = await mock.getVariable('_packedStruct2'); - // expect(getValue).to.deep.equal(struct); - // }); - it('should be able to get a uint256 mapping value', async () => { const mapKey = 1234; const mapValue = 5678; From 6ace20e79a5b5c48fbf7f3662b0118970e683447 Mon Sep 17 00:00:00 2001 From: Antonis Kogias Date: Thu, 7 Jul 2022 13:20:00 +0300 Subject: [PATCH 5/7] fix: typo --- src/utils/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/storage.ts b/src/utils/storage.ts index f47a6da..6c82c74 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -462,7 +462,7 @@ export async function getVariableStorageSlots( if (storageObjectType.encoding === 'inplace') { // For `inplace` encoding we only need to be aware of structs where they take more slots to store a variable if (storageObjectType.label.startsWith('struct')) { - slotKeysTypes = await getStructTypeStorageSlots(storageLayout, key, storageObjectType, storageObj); + slotKeysTypes = getStructTypeStorageSlots(storageLayout, key, storageObjectType, storageObj); } else { // In cases we deal with other types than structs we already know the slot key and type slotKeysTypes = slotKeysTypes.concat({ From 3650a856be773d3a5db69c255b45366f04cb9ace Mon Sep 17 00:00:00 2001 From: Antonis Kogias Date: Thu, 7 Jul 2022 14:16:28 +0300 Subject: [PATCH 6/7] fix: more fixes --- src/logic/readable-storage-logic.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/logic/readable-storage-logic.ts b/src/logic/readable-storage-logic.ts index 987b112..5eed348 100644 --- a/src/logic/readable-storage-logic.ts +++ b/src/logic/readable-storage-logic.ts @@ -19,8 +19,6 @@ export class ReadableStorageLogic { this.contractAddress = contractAddress; } - async getVariable(variableName: string): Promise; - async getVariable(variableName: string, mappingKeys: string[] | number[]): Promise; async getVariable(variableName: string, mappingKeys?: string[] | number[]): Promise { const slots: StorageSlotKeyTypePair[] = await getVariableStorageSlots( this.storageLayout, From 71e5a52353ac69a3bcc919f2942cbfa4096e1857 Mon Sep 17 00:00:00 2001 From: Antonis Kogias Date: Mon, 11 Jul 2022 15:40:10 +0300 Subject: [PATCH 7/7] fix: add warning in docs --- docs/source/mocks.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/mocks.rst b/docs/source/mocks.rst index dde017b..9ad5bcb 100644 --- a/docs/source/mocks.rst +++ b/docs/source/mocks.rst @@ -137,19 +137,22 @@ Setting the value of multiple variables Getting the value of an internal variable ******************** +.. warning:: + This is an experimental feature and it does not support multidimensional or packed arrays + .. code-block:: typescript const myUint256 = await myMock.getVariable('myUint256VariableName'); Getting the value of an internal mapping given the value's key -******************** +####################################### .. code-block:: typescript const myMappingValue = await myMock.getVariable('myMappingVariableName', [mappingKey]); Getting the value of an internal nested mapping given the value's keys -******************** +####################################### .. code-block:: typescript