0xweb
Version:
Contract package manager and other web3 tools
303 lines (275 loc) • 12 kB
text/typescript
import { SlotsParser } from '@dequanto/solidity/SlotsParser';
import { ISlotVarDefinition } from '@dequanto/solidity/SlotsParser/models';
import { $types } from '@dequanto/solidity/utils/$types';
import { $abiType } from '@dequanto/utils/$abiType';
import alot from 'alot';
export namespace $proxyDeploy {
export async function compareStorageLayout (oldVars: ISlotVarDefinition[], newVars: ISlotVarDefinition[]): Promise<{
path: string
variable: ISlotVarDefinition
conflicts: ISlotVarDefinition[]
message: string
}> {
let result = await compareStorageLayoutInner(oldVars, newVars, {
isExtendableMemory: true
});
if (result) {
let conflicts = result.conflicts.map(x => `${serializePath(result.oldPath, x)}(${x.type})`).join(', ');
return {
variable: result.newVar,
path: serializePath(result.newPath, result.newVar),
conflicts: result.conflicts,
message: `${result.type}: Variable ${serializePath(result.newPath, result.newVar)}(${result.newVar.type}) at slot ${result.newVar.slot} conflicts ${conflicts}`
};
}
return null;
}
async function compareStorageLayoutInner (
oldVars: ISlotVarDefinition[]
, newVars: ISlotVarDefinition[]
, ctx?: ICtx): Promise<BaseError> {
ctx ??= {};
let oldMemory = oldVars.map(getMemoryPosition);
let oldLastSlot = alot(oldVars).max(x => x.slot);
for (let i = 0; i < newVars.length; i++) {
let newVar = newVars[i];
if (newVar.slot > oldLastSlot) {
// New variable was added after the last slot in current deployment
if (ctx.isExtendableMemory === false) {
return new BaseError(
ELayoutError.MEMORY_OVERFLOW,
newVar,
[],
ctx
);
}
// Find the new variable name in old storage
let oldVarWithName = oldVars.find(x => x.name === newVar.name);
if (oldVarWithName != null) {
return new BaseError(
ELayoutError.NAME_MISMATCH,
newVar,
[oldVarWithName],
ctx
);
}
continue;
}
let isLastVariable = newVar.slot === oldLastSlot;
let oldVar = oldVars.find(x => x.slot === newVar.slot && x.position === newVar.position);
if (oldVar != null) {
if (oldVar.type === newVar.type) {
// New variable is the same
if (oldVar.name !== newVar.name) {
let oldVarWithName = oldVars.find(x => x.name === newVar.name);
if (oldVarWithName != null) {
return new BaseError(
ELayoutError.NAME_MISMATCH,
newVar,
[oldVar, oldVarWithName],
ctx
);
}
}
continue;
}
if (isDynamicVariable(newVar)) {
let error = await Variables.compare(oldVar, newVar, ctx);
if (error != null) {
return error;
}
continue;
}
}
let mem = getMemoryPosition(newVar);
let collisions = oldMemory.filter(current => {
if (current.offset + current.length <= mem.offset) {
return false;
}
if (current.offset > mem.offset + mem.length) {
return false;
}
return true;
});
collisions = collisions.filter(x => {
return /gap/.test(x.variable.name) === false;
});
if (collisions.length > 0) {
if (collisions.length === 1) {
let oldVar = collisions[0].variable;
let error = Variables.compare(oldVar, newVar, {
...ctx,
isLastVariable
});
if (error != null) {
return error;
}
continue;
}
return new BaseError(
ELayoutError.TYPE_COLLISION,
newVar,
collisions.map(x => x.variable),
ctx
);
}
}
return null;
}
namespace Variables {
export async function compare (oldVar: ISlotVarDefinition, newVar: ISlotVarDefinition, ctx: ICtx) {
if ($types.isDynamicArray(newVar.type)) {
if (!$types.isDynamicArray(oldVar.type)) {
return new TypeMismatchError(newVar, oldVar, ctx)
}
let oldBaseType = $abiType.array.getBaseType(oldVar.type);
let newBaseType = $abiType.array.getBaseType(newVar.type);
let [ oldSlots, newSlots ] = await Promise.all([
SlotsParser.slotsFromAbi(oldBaseType),
SlotsParser.slotsFromAbi(newBaseType)
]);
let result = await compareStorageLayoutInner(oldSlots, newSlots, {
oldPath: serializePath(ctx.oldPath, oldVar),
newPath: serializePath(ctx.newPath, newVar),
isExtendableMemory: false
});
return result;
}
if ($types.isMapping(newVar.type)) {
if (!$types.isMapping(oldVar.type)) {
return new TypeMismatchError(newVar, oldVar, ctx)
}
let oldBaseType = $abiType.mapping.getValueType(oldVar.type);
let newBaseType = $abiType.mapping.getValueType(newVar.type);
let [ oldSlots, newSlots ] = await Promise.all([
SlotsParser.slotsFromAbi(oldBaseType),
SlotsParser.slotsFromAbi(newBaseType)
]);
let result = await compareStorageLayoutInner(oldSlots, newSlots, {
oldPath: serializePath(ctx.oldPath, oldVar),
newPath: serializePath(ctx.newPath, newVar),
isExtendableMemory: true
});
return result;
}
if ($types.isFixedArray(newVar.type)) {
if (!$types.isFixedArray(oldVar.type)) {
return new TypeMismatchError(newVar, oldVar, ctx)
}
let oldLength = $abiType.array.getLength(oldVar.type);
let newLength = $abiType.array.getLength(newVar.type);
if (newLength > oldLength && ctx?.isExtendableMemory === false) {
return new BaseError(
ELayoutError.ARRAY_LENGTH_MISMATCH,
newVar,
[ oldVar ],
ctx
);
}
let oldBaseType = $abiType.array.getBaseType(oldVar.type);
let newBaseType = $abiType.array.getBaseType(newVar.type);
let [ oldSlots, newSlots ] = await Promise.all([
SlotsParser.slotsFromAbi(oldBaseType),
SlotsParser.slotsFromAbi(newBaseType)
]);
let result = await compareStorageLayoutInner(oldSlots, newSlots, {
oldPath: serializePath(ctx.oldPath, oldVar),
newPath: serializePath(ctx.newPath, newVar),
isExtendableMemory: ctx.isLastVariable ? ctx.isExtendableMemory : false
});
return result;
}
if ($types.isStruct(newVar.type)) {
if (!$types.isStruct(oldVar.type)) {
return new TypeMismatchError(newVar, oldVar, ctx)
}
let oldVarDynamicSlots = await SlotsParser.slotsFromAbi(oldVar.type);
let newVarDynamicSlots = await SlotsParser.slotsFromAbi(newVar.type);
let result = await compareStorageLayoutInner(oldVarDynamicSlots, newVarDynamicSlots, {
oldPath: serializePath(ctx.oldPath, oldVar),
newPath: serializePath(ctx.newPath, newVar),
isExtendableMemory: ctx.isLastVariable ? ctx.isExtendableMemory : false
});
return result;
}
let oldType = oldVar.type;
let newType = newVar.type;
if (oldType !== newType) {
return new TypeMismatchError(newVar, oldVar, ctx);
}
return null;
}
}
interface ICtx {
newPath?: string
oldPath?: string
// root storage is extendable, but if we compare base array types, or structs, the memory is limited due to outer next variable slots
isExtendableMemory?: boolean
isLastVariable?: boolean
}
enum ELayoutError {
TYPE_MISMATCH = 'TYPE_MISMATCH',
TYPE_COLLISION = 'TYPE_COLLISION',
/** throws error if memory is not extendable */
ARRAY_LENGTH_MISMATCH = 'ARRAY_LENGTH_MISMATCH',
/** in arrays the memory for a single item is not extendable, but in contract's root storage or mapping values is */
MEMORY_OVERFLOW = 'MEMORY_OVERFLOW',
/** Variables on same slot with different names (rename possible) but additionally the some variable exists in old layout on different slot */
NAME_MISMATCH = 'NAME_MISMATCH'
}
class BaseError {
type: ELayoutError
newPath: string
newVar: ISlotVarDefinition
oldPath: string
conflicts: ISlotVarDefinition[]
constructor (type: ELayoutError, newVar: ISlotVarDefinition, conflicts: ISlotVarDefinition[], ctx: ICtx) {
this.type = type;
this.newVar = newVar;
this.conflicts = conflicts;
this.newPath = ctx.newPath;
this.oldPath = ctx.oldPath;
}
}
class TypeMismatchError extends BaseError {
constructor (newVar: ISlotVarDefinition, oldVar: ISlotVarDefinition, ctx: ICtx) {
super(ELayoutError.TYPE_MISMATCH, newVar, [ oldVar ], ctx);
}
}
function getMemoryPosition ($var: ISlotVarDefinition) {
if (isDynamicVariable($var)) {
// dynamic variables occupy the single slot
return {
variable: $var,
offset: $var.slot * 256,
length: 256
}
};
return {
variable: $var,
offset: $var.slot * 256 + $var.position,
length: $var.size
};
}
function serializePath (path: string, $var: ISlotVarDefinition) {
if (path == null) {
return $var.name;
}
return `${path}.${$var.name}`;
}
function isDynamicVariable ($var: ISlotVarDefinition) {
return $var.size == null || $var.size === Infinity;
}
function requireBoth (a: ISlotVarDefinition, b: ISlotVarDefinition, aCheck: boolean, bCheck: boolean) {
if (!aCheck) {
return {
error: `Current variable is not dynamic ${a.name}(${a.type}) but the new one is: ${b.name}(${b.type})`
};
}
if (!bCheck) {
return {
error: `Current variable is dynamic ${a.name}(${a.type}) but the new one is not: ${b.name}(${b.type})`
};
}
}
}