import { BigNumber , ethers } from 'ethers' ; import { artifacts } from 'hardhat' ; import semver from 'semver' ; import { bigNumberToHex , fromHexString , remove0x } from './hex-utils' ; interface SolidityStorageObj { astId : number ; contract : string ; label : string ; offset : number ; slot : string ; type : string ; } interface SolidityStorageType { encoding : string ; label : string ; numberOfBytes : string ; key ? : string ; value ? : string ; base ? : string ; members ? : SolidityStorageObj [ ] ; } export interface SolidityStorageLayout { storage : SolidityStorageObj [ ] ; types : { [ name ] : SolidityStorageType ; } ; } interface StorageSlotPair { key : string ; val : string ; } param hre param name return export async function getStorageLayout ( name ) { const { sourceName , contractName } = await artifacts . readArtifactSync ( name ) ; const buildInfo = await artifacts . getBuildInfo ( `${ sourceName }:${ contractName }` ) ; if ( ! buildInfo ) throw new Error ( `Build info not found for contract ${ sourceName }:${ contractName }` ) ; const output = buildInfo . output . contracts [ sourceName ] [ contractName ] ; if ( ! semver . satisfies ( buildInfo . solcVersion , '>=0.4.x <0.9.x' ) ) { throw new Error ( `Storage layout for Solidity version ${ buildInfo . solcVersion } not yet supported. Sorry!` ) ; } if ( ! ( 'storageLayout' in output ) ) { throw new Error ( `Storage layout for ${ name } not found. Did you forget to set the storage layout compiler option in your hardhat config? Read more: https://smock.readthedocs.io/en/latest/getting-started.html#enabling-mocks` ) ; } return ( output as any ) . storageLayout ; } param storageLayout param variables returns export function computeStorageSlots ( storageLayout , variables = { } ) { let slots = [ ] ; for ( const [ variableName , variableValue ] of Object . entries ( variables ) ) { const storageObj = storageLayout . storage . find ( ( entry ) => { return entry . label === variableName ; } ) ; if ( ! storageObj ) { throw new Error ( `Variable name not found in storage layout: ${ variableName }` ) ; } slots = slots . concat ( encodeVariable ( variableValue , storageObj , storageLayout . types ) ) ; } slots = slots . reduce ( ( prevSlots , slot ) => { const prevSlot = prevSlots . find ( ( otherSlot ) => { return otherSlot . key === slot . key ; } ) ; if ( prevSlot === undefined ) { prevSlots . push ( slot ) ; } else { prevSlots = prevSlots . filter ( ( otherSlot ) => { return otherSlot . key !== prevSlot . key ; } ) ; let mergedVal = '0x' ; const valA = remove0x ( slot . val ) ; const valB = remove0x ( prevSlot . val ) ; for ( let i = 0 ; i < 64 ; i += 2 ) { const byteA = valA . slice ( i , i + 2 ) ; const byteB = valB . slice ( i , i + 2 ) ; if ( byteA === '00' && byteB === '00' ) { mergedVal += '00' ; } else if ( byteA === '00' && byteB !== '00' ) { mergedVal += byteB ; } else if ( byteA !== '00' && byteB === '00' ) { mergedVal += byteA ; } else { throw new Error ( 'detected badly encoded packed value, should not happen' ) ; } } prevSlots . push ( { key : slot . key , val : mergedVal , } ) ; } return prevSlots ; } , [ ] ) ; return slots ; } param val param offset return function padNumHexSlotValue ( val , offset ) { const bn = BigNumber . from ( val ) ; return ( '0x' + bigNumberToHex ( bn ) . padStart ( 64 - offset * 2 , bn . isNegative ( ) ? 'f' : '0' ) . padEnd ( 64 , '0' ) . toLowerCase ( ) ) ; } param val param offset return function padBytesHexSlotValue ( val , offset ) { return ( '0x' + remove0x ( val ) . padStart ( 64 - offset * 2 , '0' ) . padEnd ( 64 , '0' ) . toLowerCase ( ) ) ; } param variable param storageObj param storageTypes param nestedSlotOffset param baseSlotKey returns function encodeVariable ( variable , storageObj , storageTypes , nestedSlotOffset = 0 , baseSlotKey ? ) { let slotKey = '0x' + remove0x ( BigNumber . from ( baseSlotKey || nestedSlotOffset ) . add ( BigNumber . from ( parseInt ( storageObj . slot , 10 ) ) ) . toHexString ( ) ) . padStart ( 64 , '0' ) ; const variableType = storageTypes [ storageObj . type ] ; if ( variableType . encoding === 'inplace' ) { if ( variableType . label === 'address' || variableType . label . startsWith ( 'contract' ) ) { if ( ! ethers . utils . isAddress ( variable ) ) { throw new Error ( `invalid address type: ${ variable }` ) ; } return [ { key : slotKey , val : padNumHexSlotValue ( variable , storageObj . offset ) , } , ] ; } else if ( variableType . label === 'bool' ) { if ( typeof variable === 'string' ) { if ( variable === 'false' ) { variable = false ; } if ( variable === 'true' ) { variable = true ; } } if ( typeof variable !== 'boolean' ) { throw new Error ( `invalid bool type: ${ variable }` ) ; } return [ { key : slotKey , val : padNumHexSlotValue ( variable ? '1' : '0' , storageObj . offset ) , } , ] ; } else if ( variableType . label . startsWith ( 'bytes' ) ) { if ( ! ethers . utils . isHexString ( variable , parseInt ( variableType . numberOfBytes , 10 ) ) ) { throw new Error ( `invalid bytesN type` ) ; } return [ { key : slotKey , val : padBytesHexSlotValue ( remove0x ( variable ) . padEnd ( parseInt ( variableType . numberOfBytes , 10 ) * 2 , '0' ) , storageObj . offset ) , } , ] ; } else if ( variableType . label . startsWith ( 'uint' ) || variableType . label . startsWith ( 'int' ) ) { if ( remove0x ( BigNumber . from ( variable ) . toHexString ( ) ) . length / 2 > parseInt ( variableType . numberOfBytes , 10 ) ) { throw new Error ( `provided ${ variableType . label } is too big: ${ variable }` ) ; } return [ { key : slotKey , val : padNumHexSlotValue ( variable , storageObj . offset ) , } , ] ; } else if ( variableType . label . startsWith ( 'struct' ) ) { let slots = [ ] ; for ( const [ varName , varVal ] of Object . entries ( variable ) ) { slots = slots . concat ( encodeVariable ( varVal , ( variableType . members as SolidityStorageObj [ ] ) . find ( ( member ) => { return member . label === varName ; } ) as SolidityStorageObj , storageTypes , nestedSlotOffset + parseInt ( storageObj . slot as any , 10 ) , baseSlotKey ) ) ; } return slots ; } } else if ( variableType . encoding === 'bytes' ) { if ( storageObj . offset !== 0 ) { throw new Error ( `got offset for string/bytes type, should never happen` ) ; } const bytes = storageObj . type === 'string' ? ethers . utils . toUtf8Bytes ( variable ) : fromHexString ( variable ) ; if ( bytes . length < 32 ) { return [ { key : slotKey , val : ethers . utils . hexlify ( ethers . utils . concat ( [ ethers . utils . concat ( [ bytes , ethers . constants . HashZero ] ) . slice ( 0 , 31 ) , ethers . BigNumber . from ( bytes . length * 2 ) . toHexString ( ) , ] ) ) , } , ] ; } else { throw new Error ( 'large strings (> 31 bytes) not supported. Follow this issue for more info: https://github.com/defi-wonderland/smock/issues/30' ) ; } } else if ( variableType . encoding === 'mapping' ) { if ( variableType . key === undefined || variableType . value === undefined ) { throw new Error ( `variable is a mapping but has no key field or has no value field: ${ variableType }` ) ; } let slots = [ ] ; for ( const [ varName , varVal ] of Object . entries ( variable ) ) { let key ; if ( variableType . key . startsWith ( 't_uint' ) ) { key = BigNumber . from ( varName ) . toHexString ( ) ; } else if ( variableType . key . startsWith ( 't_bytes' ) ) { key = '0x' + remove0x ( varName ) . padEnd ( 64 , '0' ) ; } else { key = varName ; } const prevBaseSlotKey = baseSlotKey || padNumHexSlotValue ( storageObj . slot , 0 ) ; const nextBaseSlotKey = ethers . utils . keccak256 ( padNumHexSlotValue ( key , 0 ) + remove0x ( prevBaseSlotKey ) ) ; slots = slots . concat ( encodeVariable ( varVal , { label : varName , offset : 0 , slot : '0' , type : variableType . value , astId : 0 , contract : '' , } , storageTypes , nestedSlotOffset + parseInt ( storageObj . slot , 10 ) , nextBaseSlotKey ) ) ; } return slots ; } else if ( variableType . encoding === 'dynamic_array' ) { throw new Error ( 'array types not yet supported. Follow this issue for more info https://github.com/defi-wonderland/smock/issues/31' ) ; } throw new Error ( `unknown unsupported type ${ variableType . encoding } ${ variableType . label }` ) ; }