mirror of
https://github.com/cerc-io/watcher-ts
synced 2026-09-09 01:04:07 +00:00
Solidity data mapper/parser (#12)
* Initial setup with hardhat and typescript. * Add test for integer type. * Add test for unsigned integer type. * Add test for boolean type. * Add test for address type. * Add test for string type. * Setup building library with typescript. * Remove hardhat dependency from getStorageValue library function. * Move contracts to test and remove deploy script. * Add readme for running tests. Co-authored-by: nikugogoi <95nikass@gmail.com>
This commit is contained in:
co-authored by
nikugogoi
parent
7213a1dc6d
commit
72ca980198
@@ -0,0 +1 @@
|
||||
export { getStorageValue, StorageLayout, GetStorageAt } from './storage';
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Contract } from "@ethersproject/contracts";
|
||||
import { expect } from "chai";
|
||||
import hre from "hardhat";
|
||||
import "@nomiclabs/hardhat-ethers";
|
||||
|
||||
import { getStorageValue, StorageLayout } from "./storage";
|
||||
import { getStorageLayout, getStorageAt } from "../test/utils";
|
||||
|
||||
describe("Storage", function() {
|
||||
it("get value for integer type", async function() {
|
||||
const Integers = await hre.ethers.getContractFactory("TestIntegers");
|
||||
const integers = await Integers.deploy();
|
||||
await integers.deployed();
|
||||
const storageLayout = await getStorageLayout("TestIntegers");
|
||||
|
||||
// if (storageLayout)
|
||||
let value = 12;
|
||||
await integers.setInt1(value);
|
||||
let storageValue = await getStorageValue(integers.address, storageLayout, getStorageAt, "int1");
|
||||
expect(storageValue).to.equal(value);
|
||||
});
|
||||
|
||||
it("get value for unsigned integer type", async function() {
|
||||
const UnsignedIntegers = await hre.ethers.getContractFactory("TestUnsignedIntegers");
|
||||
const unsignedIntegers = await UnsignedIntegers.deploy();
|
||||
await unsignedIntegers.deployed();
|
||||
const storageLayout = await getStorageLayout("TestUnsignedIntegers");
|
||||
|
||||
const value = 123;
|
||||
await unsignedIntegers.setUint1(value);
|
||||
const storageValue = await getStorageValue(unsignedIntegers.address, storageLayout, getStorageAt, "uint1");
|
||||
expect(storageValue).to.equal(value);
|
||||
});
|
||||
|
||||
it("get value for boolean type", async function() {
|
||||
const Booleans = await hre.ethers.getContractFactory("TestBooleans");
|
||||
const booleans = await Booleans.deploy();
|
||||
await booleans.deployed();
|
||||
const storageLayout = await getStorageLayout("TestBooleans");
|
||||
|
||||
let value = true
|
||||
await booleans.setBool1(value);
|
||||
let storageValue = await getStorageValue(booleans.address, storageLayout, getStorageAt, "bool1");
|
||||
expect(storageValue).to.equal(value)
|
||||
|
||||
value = false
|
||||
await booleans.setBool2(value);
|
||||
storageValue = await getStorageValue(booleans.address, storageLayout, getStorageAt, "bool2")
|
||||
expect(storageValue).to.equal(value)
|
||||
});
|
||||
|
||||
it("get value for address type", async function() {
|
||||
const Address = await hre.ethers.getContractFactory("TestAddress");
|
||||
const address = await Address.deploy();
|
||||
await address.deployed();
|
||||
const storageLayout = await getStorageLayout("TestAddress");
|
||||
|
||||
const [signer] = await hre.ethers.getSigners();
|
||||
await address.setAddress1(signer.address);
|
||||
const storageValue = await getStorageValue(address.address, storageLayout, getStorageAt, "address1");
|
||||
expect(storageValue).to.be.a('string');
|
||||
expect(String(storageValue).toLowerCase()).to.equal(signer.address.toLowerCase());
|
||||
});
|
||||
|
||||
describe("string type", function () {
|
||||
let strings: Contract, storageLayout: StorageLayout;
|
||||
|
||||
before(async () => {
|
||||
const Strings = await hre.ethers.getContractFactory("TestStrings");
|
||||
strings = await Strings.deploy();
|
||||
await strings.deployed();
|
||||
storageLayout = await getStorageLayout("TestStrings");
|
||||
})
|
||||
|
||||
it("get value for string length less than 32 bytes", async function() {
|
||||
const value = 'Hello world.'
|
||||
await strings.setString1(value);
|
||||
const storageValue = await getStorageValue(strings.address, storageLayout, getStorageAt, "string1");
|
||||
expect(storageValue).to.equal(value);
|
||||
});
|
||||
|
||||
it("get value for string length more than 32 bytes", async function() {
|
||||
const value = 'This sentence is more than 32 bytes long.'
|
||||
await strings.setString2(value);
|
||||
const storageValue = await getStorageValue(strings.address, storageLayout, getStorageAt, "string2");
|
||||
expect(storageValue).to.equal(value);
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { utils, BigNumber } from 'ethers';
|
||||
|
||||
export interface StorageLayout {
|
||||
storage: [{
|
||||
slot: string;
|
||||
offset: number;
|
||||
type: string;
|
||||
label: string;
|
||||
}];
|
||||
types: {
|
||||
[type: string]: {
|
||||
encoding: string;
|
||||
numberOfBytes: string;
|
||||
label: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type GetStorageAt = (address: string, position: string) => Promise<string>
|
||||
|
||||
/**
|
||||
* 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 { storage, types } = storageLayout;
|
||||
const targetState = storage.find((state) => state.label === variableName)
|
||||
|
||||
// Return if state variable could not be found in storage layout.
|
||||
if (!targetState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { slot, offset, type } = targetState;
|
||||
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 address type.
|
||||
if (['address', 'address payable'].some(type => type === label)) {
|
||||
return utils.hexlify(valueArray);
|
||||
}
|
||||
|
||||
// Parse value for boolean type.
|
||||
if (label === 'bool') {
|
||||
return !BigNumber.from(valueArray).isZero();
|
||||
}
|
||||
|
||||
// Parse value for uint/int type.
|
||||
return BigNumber.from(valueArray).toNumber();
|
||||
}
|
||||
|
||||
// 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, BigNumber.from(slot).toHexString());
|
||||
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) => {
|
||||
let value = await getStorageAt(address, BigNumber.from(slot).toHexString());
|
||||
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 slotHex = utils.hexZeroPad(BigNumber.from(slot).toHexString(), 32);
|
||||
const position = utils.keccak256(slotHex);
|
||||
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user