Repository: defi-wonderland/smock (ts)

Issue:
Mocks: support arrays when using setVariable
**Is your feature request related to a problem? Please describe.**
Currently, an error is thrown when using `setVariable` in a variable of type array (any array)

**Describe the solution you'd like**
Arrays do have an special handling in the storage, we need to adapt.
As well, tests for multiple array types and structs with arrays should be added

**Additional context**
Information about how Hardhat handles arrays in the storage can be found [here](https://docs.soliditylang.org/en/v0.6.8/internals/layout_in_storage.html#mappings-and-dynamic-arrays)

There is an skipped test that should be unskipped as soon as this is working:
`Mock: Editable storage logic - should be able to set a uint256[] variable`

Pull request description:
feat: arrays support for setVariable
**Description**
Add support for arrays on `setVariable` for Mock contracts.

**Metadata**

- Fixes #31 


Patch:
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
index 6c82c74..19d9116 100644
--- a/src/utils/storage.ts
+++ b/src/utils/storage.ts
@@ -149,6 +149,12 @@ export function computeStorageSlots(storageLayout: SolidityStorageLayout, variab
           mergedVal += byteB;
         } else if (byteA !== '00' && byteB === '00') {
           mergedVal += byteA;
+        } else if (byteA === 'ff' && byteB === 'ff') {
+          mergedVal += 'ff';
+        } else if (byteA === 'ff' && byteB !== '00') {
+          mergedVal += byteB;
+        } else if (byteA !== '00' && byteB === 'ff') {
+          mergedVal += byteA;
         } else {
           // Should never happen, means our encoding is broken. Values should *never* overlap.
           throw new Error('detected badly encoded packed value, should not happen');
@@ -282,7 +288,12 @@ function encodeVariable(
         },
       ];
     } else if (variableType.label.startsWith('uint') || variableType.label.startsWith('int')) {
-      if (remove0x(BigNumber.from(variable).toHexString()).length / 2 > parseInt(variableType.numberOfBytes, 10)) {
+      let valueLength = remove0x(BigNumber.from(variable).toHexString()).length;
+      if (variableType.label.startsWith('int')) {
+        valueLength = remove0x(BigNumber.from(variable).toHexString().slice(1)).length;
+      }
+
+      if (valueLength / 2 > parseInt(variableType.numberOfBytes, 10)) {
         throw new Error(`provided ${variableType.label} is too big: ${variable}`);
       }
 
@@ -406,8 +417,65 @@ function encodeVariable(
     }
     return slots;
   } else if (variableType.encoding === 'dynamic_array') {
-    // TODO: add support for array types
-    throw new Error('array types not yet supported. Follow this issue for more info https://github.com/defi-wonderland/smock/issues/31');
+    if (variableType.base === undefined) {
+      // Should never happen in practice but required to maintain proper typing.
+      throw new Error(`variable is an array but has no base: ${variableType}`);
+    }
+    // In slotKey we save the length of the array
+    let slots: StorageSlotPair[] = [
+      {
+        key: slotKey,
+        val: padNumHexSlotValue(variable.length, 0),
+      },
+    ];
+
+    let numberOfBytes: number = 0;
+    let nextBaseSlotKey = BigNumber.from(ethers.utils.keccak256(slotKey));
+
+    // We need to find the number of bytes the base type has.
+    // The `numberOfBytes` variable will help us deal with packed arrays.
+    // We should only care for packed values only if the `numberOfBytes` is less than 16 otherwise there is no packing.
+    if (variableType.base.startsWith('t_bool')) {
+      numberOfBytes = 1;
+    } else if (variableType.base.startsWith('t_uint') || variableType.base.startsWith('t_int')) {
+      // We find the number of bits from the base and divide it with 8 to get the number of bytes
+      numberOfBytes = Number(variableType.base.replace(/\D/g, '')) / 8;
+      // If we have more than 16Bytes for each value then we don't care about packed variables.
+      numberOfBytes > 16 ? 0 : numberOfBytes;
+    }
+
+    let offset: number = -numberOfBytes;
+    for (let i = 0; i < variable.length; i++) {
+      // If the values are packed then we need to keep track of the offset and when to change slot
+      if (numberOfBytes > 0) {
+        offset += numberOfBytes;
+        if (offset >= 32) {
+          offset = 0;
+          nextBaseSlotKey = nextBaseSlotKey.add(BigNumber.from(1));
+        }
+      } else {
+        offset = 0;
+        nextBaseSlotKey = BigNumber.from(ethers.utils.keccak256(slotKey)).add(BigNumber.from(i.toString(16)));
+      }
+
+      slots = slots.concat(
+        encodeVariable(
+          variable[i],
+          {
+            label: '',
+            offset: offset,
+            slot: '0',
+            type: variableType.base,
+            astId: 0,
+            contract: '',
+          },
+          storageTypes,
+          nestedSlotOffset,
+          nextBaseSlotKey.toHexString()
+        )
+      );
+    }
+    return slots;
   }
 
   throw new Error(`unknown unsupported type ${variableType.encoding} ${variableType.label}`);


Test patch:
diff --git a/test/contracts/mock/StorageGetter.sol b/test/contracts/mock/StorageGetter.sol
index 83a279d..c458dc3 100644
--- a/test/contracts/mock/StorageGetter.sol
+++ b/test/contracts/mock/StorageGetter.sol
@@ -33,6 +33,7 @@ contract StorageGetter {
   mapping(address => bool) _addressToBoolMap;
   mapping(address => address) _addressToAddressMap;
   uint256[] internal _uint256Array;
+  int16[][] internal _int2DArray;
 
   // Testing storage slot packing.
   bool internal _packedA;
@@ -94,10 +95,6 @@ contract StorageGetter {
     _uint256 = _in;
   }
 
-  function setUint256Array(uint256[] memory _in) public {
-    _uint256Array = _in;
-  }
-
   function getBool() public view returns (bool _out) {
     return _bool;
   }
@@ -138,6 +135,10 @@ contract StorageGetter {
     return _uint256Array;
   }
 
+  function getInt2D16Array() public view returns (int16[][] memory _out) {
+    return _int2DArray;
+  }
+
   function getPackedAddress() public view returns (address) {
     return _packedB;
   }
diff --git a/test/unit/mock/editable-storage-logic.spec.ts b/test/unit/mock/editable-storage-logic.spec.ts
index 3796a93..00f13f0 100644
--- a/test/unit/mock/editable-storage-logic.spec.ts
+++ b/test/unit/mock/editable-storage-logic.spec.ts
@@ -149,9 +149,19 @@ describe('Mock: Editable storage logic', () => {
       expect(await mock.getAddressToAddressMapValue(mapKey)).to.equal(mapValue);
     });
 
-    it.skip('should be able to set a uint256[] variable', async () => {
+    it('should be able to set a uint256[] variable', async () => {
       await mock.setVariable('_uint256Array', [1, 2]);
-      expect(await mock.getUint256Array()).to.equal([1, 2]);
+      expect(await mock.getUint256Array()).to.eql([BigNumber.from(1), BigNumber.from(2)]);
+    });
+
+    it('should be able to set a 2D packed int16[][] variable', async () => {
+      const arr = [
+        [-1, -2],
+        [-3, -4],
+        [5, -6],
+      ];
+      await mock.setVariable('_int2DArray', arr);
+      expect(await mock.getInt2D16Array()).to.eql(arr);
     });
   });
 
diff --git a/test/unit/mock/readable-storage-logic.spec.ts b/test/unit/mock/readable-storage-logic.spec.ts
index 751322e..df1ae45 100644
--- a/test/unit/mock/readable-storage-logic.spec.ts
+++ b/test/unit/mock/readable-storage-logic.spec.ts
@@ -146,7 +146,7 @@ describe('Mock: Readable storage logic', () => {
     });
 
     it('should be able to get a uint256[] variable', async () => {
-      await mock.setUint256Array([1, 2, 3]);
+      await mock.setVariable('_uint256Array', [1, 2, 3]);
       const getValue = await mock.getVariable('_uint256Array');
       expect(getValue).to.deep.equal(await mock.getUint256Array());
     });