watcher-ts/packages/solidity-mapper/src/storage.ts
Ashwin Phatak b243025ca8
Test cases in solidity-mapper for contract, fixed-size byte arrays and enum types (#26)
* Add tests for getStorageInfo and getEventNameTopics.

* Lint solidity-mapper package code.

* Add test for contract type.

* Add test for fixed size byte arrays.

* Add test for Enum types.

* Add tests for variables packed together and using single slot.

* Fix comments in test contracts.

Co-authored-by: nikugogoi <95nikass@gmail.com>
2021-06-02 11:23:33 +05:30

155 lines
4.7 KiB
TypeScript

import { utils, BigNumber } from 'ethers';
interface Storage {
slot: string;
offset: number;
type: string;
label: string;
}
interface Type {
encoding: string;
numberOfBytes: string;
label: string;
}
export interface StorageLayout {
storage: Storage[];
types: { [type: string]: Type; }
}
export interface StorageInfo extends Storage {
types: { [type: string]: Type; }
}
export type GetStorageAt = (address: string, position: string) => Promise<string>
/**
* Function to get storage information of variable from storage layout.
* @param storageLayout
* @param variableName
*/
export const getStorageInfo = (storageLayout: StorageLayout, variableName: string): StorageInfo => {
const { storage, types } = storageLayout;
const targetState = storage.find((state) => state.label === variableName);
// Throw if state variable could not be found in storage layout.
if (!targetState) {
throw new Error('Variable not present in storage layout.');
}
return {
...targetState,
slot: utils.hexlify(BigNumber.from(targetState.slot)),
types
};
};
/**
* Function to get the value from storage for a contract variable.
* @param address
* @param storageLayout
* @param getStorageAt
* @param variableName
*/
export const getStorageValue = async (address: string, storageLayout: StorageLayout, getStorageAt: GetStorageAt, variableName: string): Promise<number | string | boolean | undefined> => {
const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName);
const { encoding, numberOfBytes, label } = types[type];
// Get value according to encoding i.e. how the data is encoded in storage.
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#json-output
switch (encoding) {
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#layout-of-state-variables-in-storage
case 'inplace': {
const valueArray = await getInplaceArray(address, slot, offset, numberOfBytes, getStorageAt);
// Parse value for boolean type.
if (label === 'bool') {
return !BigNumber.from(valueArray).isZero();
}
// Parse value for uint/int type.
if (label.match(/^enum|u?int[0-9]+/)) {
return BigNumber.from(valueArray).toNumber();
}
return utils.hexlify(valueArray);
}
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string
case 'bytes': {
const valueArray = await getBytesArray(address, slot, getStorageAt);
return utils.toUtf8String(valueArray);
}
default:
break;
}
};
/**
* Function to get array value for inplace encoding.
* @param address
* @param slot
* @param offset
* @param numberOfBytes
* @param getStorageAt
*/
const getInplaceArray = async (address: string, slot: string, offset: number, numberOfBytes: string, getStorageAt: GetStorageAt) => {
const value = await getStorageAt(address, slot);
const uintArray = utils.arrayify(value);
// Get value according to offset.
const start = uintArray.length - (offset + Number(numberOfBytes));
const end = uintArray.length - offset;
const offsetArray = uintArray.slice(start, end);
return offsetArray;
};
/**
* Function to get array value for bytes encoding.
* @param address
* @param slot
* @param getStorageAt
*/
const getBytesArray = async (address: string, slot: string, getStorageAt: GetStorageAt) => {
const value = await getStorageAt(address, slot);
const uintArray = utils.arrayify(value);
let length = 0;
// Get length of bytes stored.
if (BigNumber.from(uintArray[0]).isZero()) {
// If first byte is not set, get length directly from the zero padded byte array.
const slotValue = BigNumber.from(value);
length = slotValue.sub(1).div(2).toNumber();
} else {
// If first byte is set the length is lesser than 32 bytes.
// Length of the value can be computed from the last byte.
length = BigNumber.from(uintArray[uintArray.length - 1]).div(2).toNumber();
}
// Get value from the byte array directly if length is less than 32.
if (length < 32) {
return uintArray.slice(0, length);
}
// Array to hold multiple bytes32 data.
const values = [];
// Compute zero padded hexstring to calculate hashed position of storage.
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
const paddedSlotHex = utils.hexZeroPad(slot, 32);
const position = utils.keccak256(paddedSlotHex);
// Get value from consecutive storage slots for longer data.
for (let i = 0; i < length / 32; i++) {
const value = await getStorageAt(address, BigNumber.from(position).add(i).toHexString());
values.push(value);
}
// Slice trailing bytes according to length of value.
return utils.concat(values).slice(0, length);
};