Skip to content

Commit

Permalink
feat: fix log file: storage layout & contract-code-size (#291)
Browse files Browse the repository at this point in the history
  • Loading branch information
huyhuynh3103 authored Oct 2, 2023
1 parent e6efc36 commit fa65258
Show file tree
Hide file tree
Showing 7 changed files with 628 additions and 636 deletions.
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ set -ex
yarn lint-staged
yarn clean
yarn compile
yarn plugin:storage-layout --override true
yarn plugin:storage-layout
git add logs/storage_layout.log
git add logs/contract_code_sizes.log
5 changes: 3 additions & 2 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import '@nomiclabs/hardhat-ethers';
import 'hardhat-deploy';
import 'hardhat-gas-reporter';
import '@nomicfoundation/hardhat-chai-matchers';
import 'hardhat-contract-sizer';
import '@solidstate/hardhat-4byte-uploader';
import 'hardhat-storage-layout';
import '@bahuy3103/hardhat-storage-layout';
import '@bahuy3103/hardhat-contract-sizer';

import * as dotenv from 'dotenv';
import { HardhatUserConfig, NetworkUserConfig, SolcUserConfig } from 'hardhat/types';
Expand Down Expand Up @@ -131,6 +131,7 @@ const config: HardhatUserConfig = {
outDir: 'src/types',
},
paths: {
newStorageLayoutPath: './logs',
deploy: ['src/deploy', 'src/upgrades'],
tests: 'test/hardhat_test',
},
Expand Down
350 changes: 175 additions & 175 deletions logs/contract_code_sizes.log

Large diffs are not rendered by default.

560 changes: 280 additions & 280 deletions logs/storage_layout.log

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
"plugin:init-foundry": "hardhat init-foundry",
"plugin:size-contracts": "hardhat size-contracts",
"plugin:upload-selectors": "hardhat upload-selectors",
"plugin:storage-layout": "hardhat check > logs/storage.txt && hardhat generate-storage-layout --source logs/storage.txt",
"plugin:storage-layout-table": "hardhat check > logs/storage.txt && hardhat generate-storage-layout-table --source logs/storage.txt",
"plugin:storage-layout": "hardhat generate-storage-layout --inline --table",
"plugin:sourcify": "hardhat sourcify --endpoint https://sourcify.roninchain.com/server"
},
"lint-staged": {
Expand All @@ -31,6 +30,8 @@
"@openzeppelin/contracts": "4.7.3"
},
"devDependencies": {
"@bahuy3103/hardhat-contract-sizer": "2.10.3",
"@bahuy3103/hardhat-storage-layout": "0.2.6",
"@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
"@nomicfoundation/hardhat-foundry": "^1.0.1",
"@nomiclabs/hardhat-ethers": "^2.0.3",
Expand All @@ -46,12 +47,12 @@
"ethers": "^5.5.2",
"fs-extra": "11.1.1",
"hardhat": "2.14.0",
"hardhat-contract-sizer": "2.8.0",
"hardhat-deploy": "0.11.29",
"hardhat-gas-reporter": "^1.0.8",
"hardhat-storage-layout": "^0.1.7",
"husky": "^7.0.4",
"lint-staged": ">=10",
"lodash.isequal": "^4.5.0",
"lodash.uniqwith": "^4.5.0",
"prettier": "^2.5.1",
"prettier-plugin-solidity": "^1.0.0-beta.19",
"rimraf": "^3.0.2",
Expand Down
296 changes: 138 additions & 158 deletions src/tasks/generate-storage-layout.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,27 @@
import fs from 'fs-extra';
import fs from 'fs';
import { table } from 'table';
import { task } from 'hardhat/config';
import { boolean } from 'hardhat/internal/core/params/argumentTypes';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import isEqual from 'lodash.isequal';
import uniqWith from 'lodash.uniqwith';

const TABLE_STYLE = {
/*
Default Style
┌────────────┬─────┬──────┐
│ foo │ bar │ baz │
├────────────┼─────┼──────┤
│ frobnicate │ bar │ quuz │
└────────────┴─────┴──────┘
*/
headerTop: {
left: '┌',
mid: '┬',
right: '┐',
other: '─',
},
headerBottom: {
left: '├',
mid: '┼',
right: '┤',
other: '─',
},
tableBottom: {
left: '└',
mid: '┴',
right: '┘',
other: '─',
},
vertical: '│',
rowSeparator: {
left: '├',
mid: '┼',
right: '┤',
other: '─',
},
};

const preprocessFile = (fileContent: string) => {
const whiteSpaceRegex = /[\s,\|]/g; // white space
// remove all white space
const fileContentWithoutWhiteSpace = fileContent.replace(whiteSpaceRegex, '');

// get only table contents
const startIndex = fileContentWithoutWhiteSpace.indexOf(TABLE_STYLE.headerTop.right);
const endIndex = fileContentWithoutWhiteSpace.indexOf(TABLE_STYLE.tableBottom.left);
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
const result = fileContentWithoutWhiteSpace.substring(startIndex + 2, endIndex);
return result;
} else {
throw new Error('File does not contain any table');
}
};

const preprocessTable = (tableContent: string) => {
const colorRegex = /\x1B\[\d{1,3}(;\d{1,3})*m/g; // \x1B[30m \x1B[305m \x1B[38;5m
// remove all color code
const contentsWithoutColorCode = tableContent.replace(colorRegex, '');
// get list items by split vertical sperator
const listItemOfTable = contentsWithoutColorCode.split(TABLE_STYLE.vertical);
interface StateVariable {
contractName: string;
name: string;
slot: string;
offset: number;
type: string;
numberOfBytes: string;
}
enum ExportType {
TABLE,
INLINE,
TABLE_AND_INLINE,
UNKNOWN,
}

return listItemOfTable;
};
const TABLE_FILE_NAME = `storage_layout_table.log`;
const INLINE_FILE_NAME = `storage_layout.log`;

const removeIdentifierSuffix = (type: string) => {
const suffixIdRegex = /\d+_(storage|memory|calldata|ptr)/g; // id_memory id_storage
Expand All @@ -72,111 +30,133 @@ const removeIdentifierSuffix = (type: string) => {
return type.replace(suffixIdRegex, '_$1').replace(contractRegex, '$1($2)').replace(enumRegex, '$1($2)');
};

const generateStorageLayoutTable = async ({ source, destination }: { source: string; destination: string }) => {
try {
if (fs.existsSync(source)) {
const fileContent = await fs.readFile(source, 'utf-8');
const tableContent = preprocessFile(fileContent);
const listItemOfTable = preprocessTable(tableContent);
const data = [];
for (let i = 0; i < listItemOfTable.length; i += 9) {
// remove two collums: idx (index = 5) and artifacts (index =6)
const row = listItemOfTable.slice(i, i + 8).filter((_, idx) => idx != 5 && idx != 6);
const getAndPreprocessData = async (hre: HardhatRuntimeEnvironment): Promise<StateVariable[]> => {
const data = await hre.storageLayout.getStorageLayout();
const result = data.contracts.reduce(function (filtered: StateVariable[], row) {
const stateVars: StateVariable[] = row.stateVariables.map((variable) => ({
contractName: row.name,
name: variable.name,
slot: variable.slot,
offset: variable.offset,
type: removeIdentifierSuffix(variable.type),
numberOfBytes: variable.numberOfBytes,
}));
if (stateVars.length == 0) {
return filtered;
}
const result = uniqWith(filtered.concat(stateVars), isEqual);
return result;
}, []);
return result;
};
class StorageLayoutFactory {
static async build(env: HardhatRuntimeEnvironment, exportedType: ExportType): Promise<BaseStorageLayout[]> {
const data = await getAndPreprocessData(env);
switch (exportedType) {
case ExportType.TABLE:
return [new TableStorageLayout(env, data)];
case ExportType.INLINE:
return [new InLineStorageLayout(env, data)];
case ExportType.TABLE_AND_INLINE:
return [new TableStorageLayout(env, data), new InLineStorageLayout(env, data)];
default:
throw new Error('Invalid exported type');
}
}
}

// remove the suffix identifier of data type: <id>_(storage|memory|calldata)
const dataType = row[4];
row[4] = removeIdentifierSuffix(dataType);
data.push(row);
abstract class BaseStorageLayout {
env: HardhatRuntimeEnvironment;
data: StateVariable[];
constructor(env: HardhatRuntimeEnvironment, data: StateVariable[]) {
this.env = env;
this.data = data;
}
async prepareData(): Promise<StateVariable[]> {
const data = await this.env.storageLayout.getStorageLayout();
const result = data.contracts.reduce(function (filtered: StateVariable[], row) {
const stateVars: StateVariable[] = row.stateVariables.map((variable) => ({
contractName: row.name,
name: variable.name,
slot: variable.slot,
offset: variable.offset,
type: removeIdentifierSuffix(variable.type),
numberOfBytes: variable.numberOfBytes,
}));
if (stateVars.length == 0) {
return filtered;
}
const output = table(data);
await fs.writeFile(destination, output, 'utf8');
console.log(`Successful generate storage layout table at ${destination}`);
} else {
throw Error(`File storage layout at ${source} not exits`);
const result = uniqWith(filtered.concat(stateVars), isEqual);
return result;
}, []);
return result;
}
abstract getContent(): string;
abstract getFilePath(): string;
async export() {
const filePath = this.getFilePath();
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
fs.writeFileSync(filePath, '');
fs.writeFileSync(filePath, this.getContent(), 'utf8');
console.log(`Successful generate storage layout at ${filePath}`);
} catch (err) {
console.error(err);
}
} catch (err) {
console.error(err);
}
};
}

const generateStorageLayoutInline = async ({
source,
destination,
override,
}: {
source: string;
destination: string;
override: boolean;
}) => {
try {
if (fs.existsSync(source)) {
if (!fs.existsSync(destination) || override) {
const logger = fs.createWriteStream(destination, { flags: 'w' });
const fileContent = await fs.readFile(source, 'utf-8');
const tableContent = preprocessFile(fileContent);
const listItemOfTable = preprocessTable(tableContent);
let headers: string[] = [];
const data: string[] = [];
for (let i = 0; i < listItemOfTable.length; i += 9) {
// remove two collums: idx (index = 5) and artifacts (index =6)
const row = listItemOfTable.slice(i, i + 8).filter((_, idx) => idx != 5 && idx != 6);
class InLineStorageLayout extends BaseStorageLayout {
getContent(): string {
const lines: string[] = [];
this.data.forEach((stateVar) => {
const line = `${stateVar.contractName}:${stateVar.name} (storage_slot: ${stateVar.slot}) (offset: ${stateVar.offset}) (type: ${stateVar.type}) (numberOfBytes: ${stateVar.numberOfBytes})`;
lines.push(line);
});
return lines.join('\n');
}

// remove the suffix identifier of data type: <id>_(storage|memory|calldata)
const dataType = row[4];
row[4] = removeIdentifierSuffix(dataType);
if (i == 0) {
headers = row;
} else {
data.push(
`${row[0]}:${row[1]} (${headers[2]}: ${row[2]}) (${headers[3]}: ${row[3]}) (${headers[4]}: ${row[4]}) (${headers[5]}: ${row[5]})`
);
}
}
logger.write(data.join('\n'));
} else {
throw Error(
`Cannot generate storage layout because file ${destination} already exists. Use the "override" flag to overwrite.`
);
}
console.log(`Successful generate storage layout at ${destination}`);
} else {
throw Error(`File storage layout at ${source} not exits`);
}
} catch (err) {
console.error(err);
getFilePath(): string {
return this.env.config.paths.newStorageLayoutPath + '/' + INLINE_FILE_NAME;
}
};
const removeTempStorageLayout = async ({ path }: { path: string }) => {
try {
if (fs.existsSync(path)) {
await fs.unlink(path);
console.log(`Successful delete temporary storage file`);
} else {
throw Error(`File storage layout at ${path} not exits`);
}
} catch (err) {
console.error(err);
}

class TableStorageLayout extends BaseStorageLayout {
getContent(): string {
const rows: string[][] = [['Contract', 'Name', 'Slot', 'Offset', 'Type', 'Number of bytes']];
this.data.forEach((stateVar) => {
rows.push(Object.values(stateVar));
});
return table(rows);
}
};
/// @notice Generate storage layout table from `source` file to `destination` file.
task('generate-storage-layout-table')
.addParam('source', 'The path to storage layout file extracted from hardhat-storage-layout')
.addOptionalParam('destination', 'The path to store storage layout after generating', 'logs/storage_layout_table.log')
.setAction(async ({ source, destination }, _) => {
await generateStorageLayoutTable({ source, destination });
await removeTempStorageLayout({ path: source });
});

/// @notice Generate storage layout in both live from `source` file.
getFilePath(): string {
return this.env.config.paths.newStorageLayoutPath + '/' + TABLE_FILE_NAME;
}
}

/// @notice Generate storage layout.
task('generate-storage-layout')
.addParam('source', 'The path to storage layout file extracted from hardhat-storage-layout')
.addOptionalParam('override', 'Indicates whether override the destination if it already exits', true, boolean)
.setAction(async ({ source, override }, hre) => {
try {
await generateStorageLayoutTable({ source, destination: 'logs/storage_layout_table.log' });
await generateStorageLayoutInline({ source, override, destination: 'logs/storage_layout.log' });
await removeTempStorageLayout({ path: source });
} catch (err) {
console.error(err);
.addFlag('table', 'Export storage layout as table')
.addFlag('inline', 'Export storage layout as inline')
.setAction(async ({ table, inline }, hre) => {
let exportedType: ExportType;
if (table && inline) {
exportedType = ExportType.TABLE_AND_INLINE;
} else if (table) {
exportedType = ExportType.TABLE;
} else if (inline) {
exportedType = ExportType.INLINE;
} else {
exportedType = ExportType.UNKNOWN;
}

const storageLayouts = await StorageLayoutFactory.build(hre, exportedType);
await Promise.all(
storageLayouts.map(async (storageLayout) => {
await storageLayout.export();
})
);
});
Loading

0 comments on commit fa65258

Please sign in to comment.