Tests for getStorageValue on mapping and nested mappings with different key types (#52)

* Tests for mapping with value type keys.

* Add test for mapping with string type keys.

* Add test for mapping with dynamically-sized byte array as keys.

* Add tests for nested mapping.

Co-authored-by: nikugogoi <95nikass@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-06-10 11:22:03 +05:30 committed by GitHub
parent c916e61a9b
commit df025433ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 272 additions and 41 deletions

0
packages/cache/src/cache.spec.ts vendored Normal file
View File

View File

@ -56,10 +56,11 @@ $ yarn test
* [ ] Value Types
* [ ] Reference Types
* [ ] Mapping Types
* [ ] Value Type keys
* [ ] Dynamically-sized byte array keys
* [x] Value Type keys
* [ ] Fixed-size byte array keys
* [x] Dynamically-sized byte array keys
* [ ] Reference Type Mapping values
* [ ] Nested Mapping
* [x] Nested Mapping
## Observations

View File

@ -310,17 +310,17 @@ describe('Get value from storage', () => {
expect(value).to.eql(expectedValue.map(el => BigInt(el)));
});
describe('mapping type', () => {
describe('basic mapping type', () => {
let testMappingTypes: Contract, storageLayout: StorageLayout;
before(async () => {
const TestMappingTypes = await ethers.getContractFactory('TestMappingTypes');
const TestMappingTypes = await ethers.getContractFactory('TestBasicMapping');
testMappingTypes = await TestMappingTypes.deploy();
await testMappingTypes.deployed();
storageLayout = await getStorageLayout('TestMappingTypes');
storageLayout = await getStorageLayout('TestBasicMapping');
});
it('get value for basic mapping type', async () => {
it('get value for mapping with address type keys', async () => {
const expectedValue = 123;
const [, signer1] = await ethers.getSigners();
await testMappingTypes.connect(signer1).setAddressUintMap(expectedValue);
@ -329,12 +329,126 @@ describe('Get value from storage', () => {
expect(value).to.equal(BigInt(expectedValue));
});
it('get value for nested mapping type', async () => {
it('get value for mapping with boolean type keys', async () => {
const expectedValue = 123;
const mapKey = true;
await testMappingTypes.setBoolIntMap(mapKey, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'boolIntMap', mapKey);
expect(value).to.equal(BigInt(expectedValue));
});
it('get value for mapping with signed integer type keys', async () => {
const mapKey = 123;
const [, signer1] = await ethers.getSigners();
await testMappingTypes.setIntAddressMap(mapKey, signer1.address);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'intAddressMap', mapKey);
expect(value).to.equal(signer1.address.toLowerCase());
});
it('get value for mapping with unsigned integer type keys', async () => {
const expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(16));
const mapKey = 123;
await testMappingTypes.setUintBytesMap(mapKey, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'uintBytesMap', mapKey);
expect(value).to.equal(expectedValue);
});
it.skip('get value for mapping with fixed-size byte array keys', async () => {
const mapKey = ethers.utils.hexlify(ethers.utils.randomBytes(8));
const [, signer1] = await ethers.getSigners();
await testMappingTypes.setBytesAddressMap(mapKey, signer1.address);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'bytesAddressMap', mapKey);
expect(value).to.equal(signer1.address);
});
it('get value for mapping with enum type keys', async () => {
const mapKey = 1;
const expectedValue = 123;
await testMappingTypes.setEnumIntMap(mapKey, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'enumIntMap', mapKey);
expect(value).to.equal(BigInt(expectedValue));
});
it('get value for mapping with string type keys', async () => {
const mapKey = 'abc';
const expectedValue = 123;
await testMappingTypes.setStringIntMap(mapKey, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'stringIntMap', mapKey);
expect(value).to.equal(BigInt(expectedValue));
});
it('get value for mapping with dynamically-sized byte array as keys', async () => {
const mapKey = ethers.utils.hexlify(ethers.utils.randomBytes(64));
const expectedValue = 123;
await testMappingTypes.setBytesUintMap(mapKey, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'bytesUintMap', mapKey);
expect(value).to.equal(BigInt(expectedValue));
});
});
describe('nested mapping type', () => {
let testNestedMapping: Contract, storageLayout: StorageLayout;
before(async () => {
const TestNestedMapping = await ethers.getContractFactory('TestNestedMapping');
testNestedMapping = await TestNestedMapping.deploy();
await testNestedMapping.deployed();
storageLayout = await getStorageLayout('TestNestedMapping');
});
it('get value for nested mapping with address type keys', async () => {
const expectedValue = 123;
const [, signer1, signer2] = await ethers.getSigners();
await testMappingTypes.connect(signer1).setNestedAddressUintMap(signer2.address, expectedValue);
await testNestedMapping.connect(signer1).setNestedAddressUintMap(signer2.address, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'addressUintMap', signer1.address, signer2.address);
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'nestedAddressUintMap', signer1.address, signer2.address);
expect(value).to.equal(BigInt(expectedValue));
});
it('get value for nested mapping with signed integer type keys', async () => {
const expectedValue = false;
const key = 123;
const [, signer1] = await ethers.getSigners();
await testNestedMapping.setIntAddressBoolMap(key, signer1.address, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'intAddressBoolMap', key, signer1.address);
expect(value).to.equal(expectedValue);
});
it('get value for nested mapping with unsigned integer type keys', async () => {
const expectedValue = 123;
const key = 456;
const nestedKey = 'abc';
await testNestedMapping.setUintStringIntMap(key, nestedKey, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'uintStringIntMap', key, nestedKey);
expect(value).to.equal(BigInt(expectedValue));
});
it('get value for nested mapping with dynamically-sized byte array as keys', async () => {
const key = ethers.utils.hexlify(ethers.utils.randomBytes(64));
const nestedKey = 123;
const [, signer1] = await ethers.getSigners();
await testNestedMapping.setBytesIntAddressMap(key, nestedKey, signer1.address);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'bytesIntAddressMap', key, nestedKey);
expect(value).to.equal(signer1.address.toLowerCase());
});
it('get value for nested mapping with string type keys', async () => {
const key = 'abc';
const expectedValue = 123;
const [, signer1] = await ethers.getSigners();
await testNestedMapping.setStringAddressIntMap(key, signer1.address, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'stringAddressIntMap', key, signer1.address);
expect(value).to.equal(BigInt(expectedValue));
});
});

View File

@ -14,9 +14,12 @@ interface Types {
label: string;
base?: string;
value?: string;
key?: string;
};
}
type MappingKey = string | boolean | number;
export interface StorageLayout {
storage: Storage[];
types: Types
@ -58,7 +61,7 @@ export const getStorageInfo = (storageLayout: StorageLayout, variableName: strin
* @param variableName
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt: GetStorageAt, blockHash: string, address: string, variableName: string, ...mappingKeys: Array<string>): Promise<{ value: any, proof: { data: string } }> => {
export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt: GetStorageAt, blockHash: string, address: string, variableName: string, ...mappingKeys: Array<MappingKey>): Promise<{ value: any, proof: { data: string } }> => {
const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName);
return getDecodedValue(getStorageAt, blockHash, address, types, { slot, offset, type }, mappingKeys);
@ -97,9 +100,9 @@ export const getValueByType = (storageValue: string, typeLabel: string): bigint
* @param storageInfo
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, address: string, types: Types, storageInfo: { slot: string, offset: number, type: string }, mappingKeys: Array<string>): Promise<{ value: any, proof: { data: string } }> => {
const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, address: string, types: Types, storageInfo: { slot: string, offset: number, type: string }, mappingKeys: Array<MappingKey>): Promise<{ value: any, proof: { data: string } }> => {
const { slot, offset, type } = storageInfo;
const { encoding, numberOfBytes, label: typeLabel, base, value: mappingValueType } = types[type];
const { encoding, numberOfBytes, label: typeLabel, base, value: mappingValueType, key: mappingKeyType } = types[type];
const [isArray, arraySize] = typeLabel.match(/\[([0-9]*)\]/) || [false];
let value: string, proof: { data: string };
@ -142,10 +145,10 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
break;
case 'mapping': {
const mappingSlot = await getMappingSlot(slot, mappingKeys[0]);
if (mappingValueType && mappingKeyType) {
const mappingSlot = await getMappingSlot(slot, types, mappingKeyType, mappingKeys[0]);
if (mappingValueType) {
({ value, proof } = await getDecodedValue(getStorageAt, blockHash, address, types, { slot: mappingSlot, offset: 0, type: mappingValueType }, mappingKeys.slice(1)));
return getDecodedValue(getStorageAt, blockHash, address, types, { slot: mappingSlot, offset: 0, type: mappingValueType }, mappingKeys.slice(1));
} else {
throw new Error(`Mapping value type not specified for ${mappingKeys[0]}`);
}
@ -168,11 +171,32 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
* @param mappingSlot
* @param key
*/
export const getMappingSlot = (mappingSlot: string, key: string): string => {
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
const mappingSlotPadded = utils.hexZeroPad(BigNumber.from(mappingSlot).toHexString(), 32);
const keyPadded = utils.hexZeroPad(key, 32);
export const getMappingSlot = (mappingSlot: string, types: Types, keyType: string, key: MappingKey): string => {
const { encoding, label: typeLabel } = types[keyType];
// If key is boolean type convert to 1 or 0 which is the way value is stored in memory.
if (typeLabel === 'bool') {
key = key ? 1 : 0;
}
// If key is string convert to hex string representation.
if (typeLabel.includes('string') && typeof key === 'string') {
key = utils.hexlify(utils.toUtf8Bytes(key));
}
// If key is still boolean type the argument passed as key is invalid.
if (typeof key === 'boolean') {
throw new Error('Invalid key.');
}
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
const mappingSlotPadded = utils.hexZeroPad(mappingSlot, 32);
const keyPadded = encoding === 'bytes'
? utils.hexlify(key)
: utils.hexZeroPad(utils.hexlify(key), 32);
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const fullKey = utils.concat([
keyPadded,
mappingSlotPadded

View File

@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestBasicMapping {
// Mapping type variable occupies one single slot but the actual elements are stored at a different storage slot that is computed using a Keccak-256 hash.
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
mapping(address => uint) public addressUintMap;
// Mapping type variable occupies the next single slot.
mapping(bool => int) public boolIntMap;
// Mapping with int128 keys and contract type values.
mapping(int128 => address) public intAddressMap;
// Mapping with uint32 keys and fixed-size byte array values.
mapping(uint32 => bytes16) public uintBytesMap;
// Mapping with fixed-size byte array keys and address type values.
mapping(bytes8 => address) public bytesAddressMap;
// Enum declaration.
enum Choices { Choice0, Choice1, Choice2, Choice3 }
// Mapping with enum type keys and integer type values.
mapping(Choices => int) public enumIntMap;
// Mapping with string type keys and integer type values.
mapping(string => int) public stringIntMap;
// Mapping with dynamically-sized byte array as keys and unsigned integer type values.
mapping(bytes => uint) public bytesUintMap;
// Set variable addressUintMap.
function setAddressUintMap(uint value) external {
addressUintMap[msg.sender] = value;
}
// Set variable boolIntMap.
function setBoolIntMap(bool key, int value) external {
boolIntMap[key] = value;
}
// Set variable intAddressMap.
function setIntAddressMap(int128 key, address value) external {
intAddressMap[key] = value;
}
// Set variable uintBytesMap.
function setUintBytesMap(uint32 key, bytes16 value) external {
uintBytesMap[key] = value;
}
// Set variable bytesAddressMap.
function setBytesAddressMap(bytes8 key, address value) external {
bytesAddressMap[key] = value;
}
// Set variable enumIntMap.
function setEnumIntMap(Choices key, int value) external {
enumIntMap[key] = value;
}
// Set variable stringIntMap.
function setStringIntMap(string calldata key, int value) external {
stringIntMap[key] = value;
}
// Set variable bytesUintMap.
function setBytesUintMap(bytes calldata key, uint value) external {
bytesUintMap[key] = value;
}
}

View File

@ -1,21 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestMappingTypes {
// Mapping type variable occupies one single slot but the actual elements are stored at a different storage slot that is computed using a Keccak-256 hash.
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
mapping (address => mapping (address => uint)) private nestedAddressUintMap;
// Mapping type variable occupies the next single slot.
mapping(address => uint) public addressUintMap;
// Set variable addressUintMap.
function setAddressUintMap(uint value) external {
addressUintMap[msg.sender] = value;
}
// Set variable nestedAddressUintMap.
function setNestedAddressUintMap(address addressValue, uint uintValue) external {
nestedAddressUintMap[msg.sender][addressValue] = uintValue;
}
}

View File

@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestNestedMapping {
// Mapping type variable occupies one single slot but the actual elements are stored at a different storage slot that is computed using a Keccak-256 hash.
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
mapping (address => mapping (address => uint)) private nestedAddressUintMap;
mapping (int => mapping (address => bool)) private intAddressBoolMap;
mapping (uint => mapping (string => int)) private uintStringIntMap;
mapping (bytes => mapping (int => address)) private bytesIntAddressMap;
mapping (string => mapping (address => int)) private stringAddressIntMap;
// Set variable nestedAddressUintMap.
function setNestedAddressUintMap(address nestedKey, uint value) external {
nestedAddressUintMap[msg.sender][nestedKey] = value;
}
// Set variable intAddressBoolMap.
function setIntAddressBoolMap(int key, address nestedKey, bool value) external {
intAddressBoolMap[key][nestedKey] = value;
}
// Set variable uintStringIntMap.
function setUintStringIntMap(uint key, string calldata nestedKey, int value) external {
uintStringIntMap[key][nestedKey] = value;
}
// Set variable bytesIntAddressMap.
function setBytesIntAddressMap(bytes calldata key, int nestedKey, address value) external {
bytesIntAddressMap[key][nestedKey] = value;
}
// Set variable stringAddressIntMap.
function setStringAddressIntMap(string calldata key, address nestedKey, int value) external {
stringAddressIntMap[key][nestedKey] = value;
}
}