@gnus.ai/upgrade-safe-transpiler-diamond
Version:
Solidity preprocessor used to generate OpenZeppelin Contracts Upgrade Safe using Diamond Pattern (EIP-2535).
229 lines (183 loc) • 7.35 kB
text/typescript
import {SourceUnit, ContractDefinition, VariableDeclaration, UserDefinedTypeName, IdentifierPath} from 'solidity-ast';
import { findAll } from 'solidity-ast/utils';
import { getNodeBounds } from '../solc/ast-utils';
import {Transformation, TransformHelper} from './type';
import { TransformerTools } from '../transform';
import path from 'path';
import {hasOverride} from "../utils/upgrades-overrides";
import {OutputFile} from "../index";
import {getContractsImportPath, renameContract, renamePath} from "../rename";
import {newFunctionPosition} from "./utils/new-function-position";
import {Node} from "solidity-ast/node";
import {
addVariableScopedContract,
getScopedContractsForVariables,
getUniqueIdentifierVarsUsed,
IdentifierVariable,
} from "./utils/get-identifiers-used";
function* findUserDefinedTypes(node: Node): Generator<UserDefinedTypeName> {
const seen = new Set();
for (const id of findAll(['UserDefinedTypeName'], node)) {
if ('pathNode' in id && id.pathNode !== undefined && !seen.has(id)) {
seen.add(id.pathNode);
yield id;
}
}
}
export function addDiamondStorage(newFiles: OutputFile[]) {
return function* (sourceUnit: SourceUnit, tools: TransformerTools): Generator<Transformation> {
const { resolver } = tools;
const contracts = [...findAll('ContractDefinition', sourceUnit)];
if (!contracts.some(c => c.contractKind === 'contract')) {
return;
}
let buffer = '';
let contractNeedsStorage = false;
const contractScopes = new Map<number, string>();
const contractPaths = new Map<string, Set<string>>();
const thisContractSet = new Set<string>();
contractPaths.set(sourceUnit.absolutePath, thisContractSet);
for (const contract of contracts) {
const varDecls = [...findAll('VariableDeclaration', contract)];
const variableNodes = varDecls.filter(
v => v.stateVariable && !v.constant &&
!hasOverride(v, 'state-variable-assignment') &&
!hasOverride(v, 'state-variable-immutable'),
);
if ((contract.contractKind === 'contract') && (variableNodes.length > 0)) {
contractNeedsStorage = true;
if (!thisContractSet.has(contract.name)) {
thisContractSet.add(contract.name);
}
// move comments for each variable to this map
const commentMap = new Map();
for (const varNode of variableNodes) {
addVariableScopedContract(contractPaths, varNode, tools.resolver);
const vBounds = getNodeBounds(varNode);
// grab first line of contract.
const cStart = newFunctionPosition(contract, tools);
const contractCode = tools.originalSource;
const subContractCode = contractCode.substring(cStart, vBounds.start);
const commentSplit = extractComments(subContractCode);
let newSource = commentSplit[1].replace('/**', '/*')
newSource = newSource.replace(/[\t ]+$/, '');
commentMap.set(varNode.id, newSource);
yield {
start: vBounds.start - commentSplit[1].length,
length: commentSplit[1].length,
kind: 'remove-var-states-comments',
text: '',
};
}
const referencedTypeDeclarations: Map<number, IdentifierVariable> = getUniqueIdentifierVarsUsed(contract, tools);
for (const [_, identifierVar] of referencedTypeDeclarations) {
const { varDecl } = identifierVar;
const nodeInfo = resolver.resolveScope(varDecl.scope)!;
if (nodeInfo.scopeNode && (nodeInfo.scopeNode.id !== sourceUnit.id)) {
addVariableScopedContract(contractPaths, varDecl, tools.resolver);
}
}
buffer = makeStorageLib(contract.name, variableNodes, commentMap, buffer);
}
}
if (contractNeedsStorage) {
const newBuffer = `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
${ getContractsImportPath(contractPaths, path.parse(sourceUnit.absolutePath).dir) }
${ buffer }
`;
const {dir, name, ext} = path.parse(sourceUnit.absolutePath);
const newpath = path.format({dir, ext, name: name + 'Storage'});
newFiles.push({source: newBuffer, fileName: name + 'Storage', path: newpath});
}
};
}
function extractComments(source: string) : string[] {
enum CommentType {
none,
doubleSlash,
slashAsterisk
}
const whiteSpace: string = '\t \n';
let lastNonCommentIndex = -1;
let commentType = CommentType.none;
let sLen = source.length;
for (let i = 0; i < sLen; i++) {
// not currently processing comment
if (commentType === CommentType.none) {
// need to look ahead for comment start
if (i < sLen - 1) {
if ((source[i] === '/') && (source[i + 1] === '/')) {
commentType = CommentType.doubleSlash;
} else if ((source[i] === '/') && (source[i + 1] === '*')) {
commentType = CommentType.slashAsterisk;
}
}
} else {
if ((commentType === CommentType.slashAsterisk)) {
if ((source[i - 1] === '*') && (source[i] === '/')) {
commentType = CommentType.none;
continue;
}
} else if (commentType === CommentType.doubleSlash) {
if (source[i] === '\n') {
commentType = CommentType.none;
continue;
}
}
}
if ((commentType === CommentType.none) && !whiteSpace.includes(source[i])) {
lastNonCommentIndex = i;
}
}
// keep line ending of non-comment character with it
if ((lastNonCommentIndex < sLen-1) && (source[lastNonCommentIndex + 1] === '\n')) {
lastNonCommentIndex++;
}
return [source.substring(0, lastNonCommentIndex+1), source.substring(lastNonCommentIndex+1)];
}
function makeStorageLib(name: string, variables: VariableDeclaration[], comments: Map<number, string>, buffer: string) {
buffer += `
library ${name}Storage {
struct Layout {
${ variables.map(v => {
let typeString = v.typeDescriptions.typeString || '';
if (v.typeName?.nodeType === 'UserDefinedTypeName') {
const varTypeStrings = typeString.split(' ', 2);
if (varTypeStrings.length == 2) {
typeString = renamePath(varTypeStrings[1]);
}
}
typeString = filterIdentifierPaths(typeString);
return comments.get(v.id) + ' ' + typeString + ' ' + v.name + ';' }).join('\n')
}
}
bytes32 internal constant STORAGE_SLOT = keccak256('openzeppelin.contracts.storage.${name}');
function layout() internal pure returns (Layout storage l) {
bytes32 slot = STORAGE_SLOT;
assembly {
l.slot := slot
}
}
}
`;
return buffer;
}
function renameIdentifierPath(sourceStr: string) : string {
const matchStrings = sourceStr.split(/^[ \t]*([A-Za-z_][A-Za-z0-9_.]*)/, 3);
let retString = matchStrings[1];
retString = renamePath(retString);
if (matchStrings.length > 2) {
retString += matchStrings[2];
}
return retString;
}
// Filter the identifier paths (remove 'struct ', 'enum ' and append
function filterIdentifierPaths(sourceStr: string) : string {
let splitStrings = sourceStr.split(/(enum |struct |contract )/s);
let retString = splitStrings[0];
for (let i=2; i < splitStrings.length; i += 2) {
retString += renameIdentifierPath(splitStrings[i]);
}
return retString;
}