diff --git a/docs/source/mocks.rst b/docs/source/mocks.rst index a397d08..9ad5bcb 100644 --- a/docs/source/mocks.rst +++ b/docs/source/mocks.rst @@ -133,4 +133,27 @@ Setting the value of multiple variables [myKey]: 1234 } }) - \ No newline at end of file + +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 + + const myMappingValue = await myMock.getVariable('myMappingVariableName', [mappingKeyA, mappingKeyB]); diff --git a/src/factories/smock-contract.ts b/src/factories/smock-contract.ts index 75e3b64..d07588e 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..5eed348 --- /dev/null +++ b/src/logic/readable-storage-logic.ts @@ -0,0 +1,40 @@ +import { SmockVMManager } from '../types'; +import { fromHexString, remove0x, toFancyAddress, toHexString } from '../utils'; +import { + decodeVariable, + getVariableStorageSlots, + SolidityStorageLayout, + StorageSlotKeyTypePair, + StorageSlotKeyValuePair, +} from '../utils/storage'; + +export class ReadableStorageLogic { + private storageLayout: SolidityStorageLayout; + private contractAddress: string; + private vmManager: SmockVMManager; + + constructor(storageLayout: SolidityStorageLayout, vmManager: SmockVMManager, contractAddress: string) { + this.storageLayout = storageLayout; + this.vmManager = vmManager; + this.contractAddress = contractAddress; + } + + 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))) + ), + })) + ); + return decodeVariable(slotValueTypePairs); + } +} diff --git a/src/types.ts b/src/types.ts index ff264ec..873c5d9 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< @@ -72,6 +73,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/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 489f61e..6c82c74 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, xor } 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). +export 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,351 @@ 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 + * @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( + storageLayout: SolidityStorageLayout, + variableName: string, + vmManager: SmockVMManager, + contractAddress: string, + 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 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 = 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'); + + 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 = 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') { + slotKeysTypes = await getBytesTypeStorageSlots(vmManager, contractAddress, storageObjectType, storageObj, key); + } else if (storageObjectType.encoding === 'mapping') { + 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}`); + } + slotKeysTypes = await getMappingTypeStorageSlots( + storageLayout, + variableName, + vmManager, + contractAddress, + key, + storageObjectType, + mappingKey + ); + } else if (storageObjectType.encoding === 'dynamic_array') { + 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: 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; +} + +/** + * 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 + + 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(); + 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..83a279d 100644 --- a/test/contracts/mock/StorageGetter.sol +++ b/test/contracts/mock/StorageGetter.sol @@ -7,11 +7,16 @@ struct SimpleStruct { } struct PackedStruct { - bool packedA; - address packedB; + 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; @@ -41,6 +46,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 +74,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 +94,10 @@ contract StorageGetter { _uint256 = _in; } + function setUint256Array(uint256[] memory _in) public { + _uint256Array = _in; + } + function getBool() public view returns (bool _out) { return _bool; } 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 new file mode 100644 index 0000000..751322e --- /dev/null +++ b/test/unit/mock/readable-storage-logic.spec.ts @@ -0,0 +1,154 @@ +import { MockContract, MockContractFactory, smock } from '@src'; +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); + + const getValue = await mock.getVariable('_uint256'); + expect(getValue).to.equal(await mock.getUint256()); + }); + + 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); + + const getValue = await mock.getVariable('_packedUintB'); + expect(getValue).to.equal(await mock.getPackedUintB()); + }); + + it('should be able to get an int56', async () => { + const value = 1; + await mock.setVariable('_int56', value); + + const getValue = await mock.getVariable('_int56'); + expect(getValue).to.equal(await mock.getInt56()); + }); + + 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'); + expect(getValue).to.equal(await mock.getInt256()); + }); + + it('should be able to get an address', async () => { + await mock.setVariable('_address', ADDRESS_EXAMPLE); + + const getValue = await mock.getVariable('_address'); + expect(getValue).to.equal(await mock.getAddress()); + }); + + it('should be able to get a boolean', async () => { + await mock.setVariable('_bool', true); + + const getValue = await mock.getVariable('_bool'); + 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); + + const getValue = await mock.getVariable('_bytes'); + 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); + + const getValue = await mock.getVariable('_bytes'); + expect(getValue).to.equal(await mock.getBytes()); + }); + + it('should be able to get bytes32', async () => { + await mock.setVariable('_bytes32', BYTES32_EXAMPLE); + + const getValue = await mock.getVariable('_bytes32'); + expect(getValue).to.equal(await mock.getBytes32()); + }); + + it('should be able to get a simple struct', async () => { + const struct = { + valueA: BigNumber.from(1234), + valueB: true, + }; + await mock.setVariable('_simpleStruct', 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: BigNumber.from(2), + packedB: BigNumber.from(1), + packedC: BigNumber.from(2), + packedD: BigNumber.from(1), + packedE: ADDRESS_EXAMPLE, + }; + await mock.setVariable('_packedStruct', struct); + + const getValue = await mock.getVariable('_packedStruct'); + 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 }); + + const getValue = await mock.getVariable('_uint256Map', [mapKey]); + expect(getValue).to.equal(await mock.getUint256MapValue(mapKey)); + }); + + 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 }); + + 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'); + expect(getValue).to.deep.equal(await mock.getUint256Array()); + }); + }); +});