Repository: defi-wonderland/smock (ts)

Issue:
Add a way to view internal variables value
Just as `await mock.setVariable('_myVar', 123);`
I believe we should have: `await mock.getVariable('_myVar');` that returns the current value of the variable

Pull request description:
feat: add getVariable function
**Description**
This commit introduces a new function for mock contracts. With `getVariable` you can view an internal variable's value.

**Approach**
A rough idea of how I approached this problem is:
1) I get an array of slot keys using `getVariableStorageSlots` that represent the position of the variable's value.
2) I get the values that are stored in those slots using the `vmManager.getContractStorage` function and providing the contract address and the appropriate slot key.
3) I decoded the values using the `decodeVariable` function and providing all the values from the above step which results in the variable's value

**Metadata**

- Fixes #14 


Patch:
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<T extends ContractFactory>(
 
     // 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<unknown> {
+    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<T extends BaseContract = BaseContract> = SmockContractB
   connect: (...args: Parameters<T['connect']>) => MockContract<T>;
   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<StorageSlotKeyTypePair[]> {
+  // 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<StorageSlotKeyTypePair[]> {
+  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<StorageSlotKeyTypePair[]> {
+  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<StorageSlotKeyTypePair[]> {
+  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;
+}


Test patch:
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<StorageGetter__factory>;
+  let mock: MockContract<StorageGetter>;
+
+  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());
+    });
+  });
+});