diff --git a/packages/compilers-types/src/CompilationTypes.ts b/packages/compilers-types/src/CompilationTypes.ts index 9ecd0323d..4a0684084 100644 --- a/packages/compilers-types/src/CompilationTypes.ts +++ b/packages/compilers-types/src/CompilationTypes.ts @@ -1,6 +1,10 @@ -import { JsonFragment } from "ethers"; -import { SolidityOutputError, SoliditySettings } from "./SolidityTypes"; -import { VyperOutputError } from "./VyperTypes"; +import type { JsonFragment } from "ethers"; +import type { + SolidityJsonInput, + SolidityOutputError, + SoliditySettings, +} from "./SolidityTypes"; +import type { VyperJsonInput, VyperOutputError } from "./VyperTypes"; export interface LinkReferences { [filePath: string]: { @@ -105,3 +109,5 @@ export interface Metadata { sources: MetadataSourceMap; version: number; } + +export type AnyJsonInput = SolidityJsonInput | VyperJsonInput; diff --git a/packages/lib-sourcify/src/Compilation/CompilationTypes.ts b/packages/lib-sourcify/src/Compilation/CompilationTypes.ts index 63815bab6..fc2a3d6a1 100644 --- a/packages/lib-sourcify/src/Compilation/CompilationTypes.ts +++ b/packages/lib-sourcify/src/Compilation/CompilationTypes.ts @@ -6,6 +6,8 @@ import type { } from '@ethereum-sourcify/compilers-types'; import type { SourcifyLibErrorParameters } from '../SourcifyLibError'; import { SourcifyLibError } from '../SourcifyLibError'; +import type { SolidityCompilation } from './SolidityCompilation'; +import type { VyperCompilation } from './VyperCompilation'; export interface CompiledContractCborAuxdata { [key: string]: { @@ -28,9 +30,10 @@ export interface CompilationTarget { path: string; } -export type CompilationLanguage = 'Solidity' | 'Vyper'; +export type CompilationLanguage = 'Solidity' | 'Vyper' | 'Yul'; export type CompilationErrorCode = + | 'invalid_language' | 'cannot_generate_cbor_auxdata_positions' | 'invalid_compiler_version' | 'unsupported_compiler_version' @@ -66,3 +69,5 @@ export interface IVyperCompiler { vyperJsonInput: VyperJsonInput, ): Promise; } + +export type AnyCompilation = SolidityCompilation | VyperCompilation; diff --git a/packages/lib-sourcify/src/Compilation/PreRunCompilation.ts b/packages/lib-sourcify/src/Compilation/PreRunCompilation.ts index 2f1a8da44..3f33aae1c 100644 --- a/packages/lib-sourcify/src/Compilation/PreRunCompilation.ts +++ b/packages/lib-sourcify/src/Compilation/PreRunCompilation.ts @@ -79,6 +79,7 @@ export class PreRunCompilation extends AbstractCompilation { get immutableReferences(): ImmutableReferences { switch (this.language) { + case 'Yul': case 'Solidity': { const compilationTarget = this .contractCompilerOutput as SolidityOutputContract; @@ -97,6 +98,7 @@ export class PreRunCompilation extends AbstractCompilation { get runtimeLinkReferences(): LinkReferences { switch (this.language) { + case 'Yul': case 'Solidity': { const compilationTarget = this .contractCompilerOutput as SolidityOutputContract; @@ -109,6 +111,7 @@ export class PreRunCompilation extends AbstractCompilation { get creationLinkReferences(): LinkReferences { switch (this.language) { + case 'Yul': case 'Solidity': { const compilationTarget = this .contractCompilerOutput as SolidityOutputContract; diff --git a/packages/lib-sourcify/src/Compilation/VyperCompilation.ts b/packages/lib-sourcify/src/Compilation/VyperCompilation.ts index 7c196ff80..21d51d3ff 100644 --- a/packages/lib-sourcify/src/Compilation/VyperCompilation.ts +++ b/packages/lib-sourcify/src/Compilation/VyperCompilation.ts @@ -125,12 +125,18 @@ export class VyperCompilation extends AbstractCompilation { {}, ); + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + outputSelection: _outputSelection, + ...settingsWithoutOutputSelection + } = this.jsonInput.settings || {}; + this._metadata = { compiler: { version: this.compilerVersion }, language: 'Vyper', output: outputMetadata, settings: { - ...this.jsonInput.settings, + ...settingsWithoutOutputSelection, compilationTarget: { [this.compilationTarget.path]: this.compilationTarget.name, }, diff --git a/packages/lib-sourcify/src/Compilation/YulCompilation.ts b/packages/lib-sourcify/src/Compilation/YulCompilation.ts new file mode 100644 index 000000000..c7309310d --- /dev/null +++ b/packages/lib-sourcify/src/Compilation/YulCompilation.ts @@ -0,0 +1,75 @@ +import type { + MetadataCompilerSettings, + SoliditySettings, +} from '@ethereum-sourcify/compilers-types'; +import type { CompilationLanguage } from './CompilationTypes'; +import { SolidityCompilation } from './SolidityCompilation'; +import { id as keccak256str } from 'ethers'; +import { convertLibrariesToMetadataFormat } from '../utils/utils'; + +/** + * Abstraction of a Yul compilation + */ +export class YulCompilation extends SolidityCompilation { + public language: CompilationLanguage = 'Yul'; + + public async compile(forceEmscripten = false) { + await this.compileAndReturnCompilationTarget(forceEmscripten); + this.generateMetadata(); + } + + /** + * Yul compiler does not produce a metadata but we generate it ourselves for backward + * compatibility reasons e.g. in the legacy Sourcify API that always assumes a metadata.json + */ + generateMetadata() { + const contract = this.contractCompilerOutput; + const outputMetadata = { + abi: contract.abi, + devdoc: contract.devdoc, + userdoc: contract.userdoc, + }; + + const sourcesWithHashes = Object.entries(this.jsonInput.sources).reduce( + (acc, [path, source]) => ({ + ...acc, + [path]: { + keccak256: keccak256str(source.content), + }, + }), + {}, + ); + + const soliditySettings = JSON.parse( + JSON.stringify(this.jsonInput.settings), + ) as SoliditySettings; + + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + outputSelection: _outputSelection, + libraries, + ...settingsWithoutOutputSelection + } = soliditySettings; + + const metadataSettings: Omit< + MetadataCompilerSettings, + 'compilationTarget' + > = { + ...settingsWithoutOutputSelection, + libraries: convertLibrariesToMetadataFormat(libraries), + }; + this._metadata = { + compiler: { version: this.compilerVersion }, + language: 'Yul', + output: outputMetadata, + settings: { + ...metadataSettings, + compilationTarget: { + [this.compilationTarget.path]: this.compilationTarget.name, + }, + }, + sources: sourcesWithHashes, + version: 1, + }; + } +} diff --git a/packages/lib-sourcify/src/Validation/SolidityMetadataContract.ts b/packages/lib-sourcify/src/Validation/SolidityMetadataContract.ts index e80ae27e4..64801bb28 100644 --- a/packages/lib-sourcify/src/Validation/SolidityMetadataContract.ts +++ b/packages/lib-sourcify/src/Validation/SolidityMetadataContract.ts @@ -3,7 +3,6 @@ import semver from 'semver'; import { performFetch } from './fetchUtils'; import { SolidityCompilation } from '../Compilation/SolidityCompilation'; import type { - Libraries, SolidityJsonInput, Metadata, MetadataCompilerSettings, @@ -33,7 +32,7 @@ import { getVariationsByContentHash, } from './variationsUtils'; import { logDebug } from '../logger'; -import { splitFullyQualifiedName } from '../utils/utils'; +import { convertLibrariesToStdJsonFormat } from '../utils/utils'; export class SolidityMetadataContract { metadata: Metadata; @@ -317,32 +316,9 @@ export class SolidityMetadataContract { this.solcJsonInput.language = this.metadata.language; - // Convert the libraries from the metadata format to the compiler_settings format - // metadata format: "contracts/1_Storage.sol:Journal": "0x7d53f102f4d4aa014db4e10d6deec2009b3cda6b" - // settings format: "contracts/1_Storage.sol": { Journal: "0x7d53f102f4d4aa014db4e10d6deec2009b3cda6b" } - if (metadataLibraries) { - this.solcJsonInput.settings.libraries = Object.keys( - metadataLibraries, - ).reduce((libraries, libraryKey) => { - // Before Solidity v0.7.5: { "ERC20": "0x..."} - if (!libraryKey.includes(':')) { - if (!libraries['']) { - libraries[''] = {}; - } - // try using the global method, available for pre 0.7.5 versions - libraries[''][libraryKey] = metadataLibraries[libraryKey]; - return libraries; - } - - // After Solidity v0.7.5: { "ERC20.sol:ERC20": "0x..."} - const { contractPath, contractName } = - splitFullyQualifiedName(libraryKey); - if (!libraries[contractPath]) { - libraries[contractPath] = {}; - } - libraries[contractPath][contractName] = metadataLibraries[libraryKey]; - return libraries; - }, {} as Libraries); + const libraries = convertLibrariesToStdJsonFormat(metadataLibraries); + if (libraries) { + this.solcJsonInput.settings.libraries = libraries; } } diff --git a/packages/lib-sourcify/src/index.ts b/packages/lib-sourcify/src/index.ts index ee0490388..bd8575de1 100644 --- a/packages/lib-sourcify/src/index.ts +++ b/packages/lib-sourcify/src/index.ts @@ -10,6 +10,7 @@ export type ILibSourcifyLogger = ILogger; export * from './Compilation/AbstractCompilation'; export * from './Compilation/SolidityCompilation'; export * from './Compilation/VyperCompilation'; +export * from './Compilation/YulCompilation'; export * from './Compilation/PreRunCompilation'; export * from './Compilation/CompilationTypes'; diff --git a/packages/lib-sourcify/src/utils/utils.ts b/packages/lib-sourcify/src/utils/utils.ts index 6f609cada..1dad57de4 100644 --- a/packages/lib-sourcify/src/utils/utils.ts +++ b/packages/lib-sourcify/src/utils/utils.ts @@ -1,3 +1,8 @@ +import type { + Libraries, + MetadataCompilerSettings, +} from '@ethereum-sourcify/compilers-types'; + /** * Checks whether the provided object contains any keys or not. * @param obj The object whose emptiness is tested. @@ -21,3 +26,62 @@ export function splitFullyQualifiedName(fullyQualifiedName: string): { const contractPath = splitIdentifier.slice(0, -1).join(':'); return { contractPath, contractName }; } + +/** + * Converts libraries from the solc JSON input format to the metadata format. + * jsonInput format: { "contracts/1_Storage.sol": { Journal: "0x..." } } + * metadata format: { "contracts/1_Storage.sol:Journal": "0x..." } + */ +export function convertLibrariesToMetadataFormat( + libraries?: Libraries, +): MetadataCompilerSettings['libraries'] { + if (!libraries) { + return undefined; + } + + const metadataLibraries: NonNullable = + {}; + + for (const [contractPath, libraryMap] of Object.entries(libraries)) { + for (const [libraryName, libraryAddress] of Object.entries(libraryMap)) { + const metadataKey = + contractPath === '' ? libraryName : `${contractPath}:${libraryName}`; + metadataLibraries[metadataKey] = libraryAddress; + } + } + + return Object.keys(metadataLibraries).length ? metadataLibraries : undefined; +} + +/** + * Converts libraries from the metadata format to the solc JSON input format. + * metadata format: { "contracts/1_Storage.sol:Journal": "0x..." } + * jsonInput format: { "contracts/1_Storage.sol": { Journal: "0x..." } } + */ +export function convertLibrariesToStdJsonFormat( + metadataLibraries?: MetadataCompilerSettings['libraries'], +): Libraries | undefined { + if (!metadataLibraries) { + return undefined; + } + + return Object.keys(metadataLibraries).reduce((libraries, libraryKey) => { + // Before Solidity v0.7.5: { "ERC20": "0x..."} + if (!libraryKey.includes(':')) { + if (!libraries['']) { + libraries[''] = {}; + } + // try using the global method, available for pre 0.7.5 versions + libraries[''][libraryKey] = metadataLibraries[libraryKey]; + return libraries; + } + + // After Solidity v0.7.5: { "ERC20.sol:ERC20": "0x..."} + const { contractPath, contractName } = splitFullyQualifiedName(libraryKey); + if (!libraries[contractPath]) { + libraries[contractPath] = {}; + } + libraries[contractPath][contractName] = metadataLibraries[libraryKey]; + return libraries; + }, {} as Libraries); +} diff --git a/packages/lib-sourcify/test/Compilation/VyperCompilation.spec.ts b/packages/lib-sourcify/test/Compilation/VyperCompilation.spec.ts index 44b5eb0f3..bd9106d94 100644 --- a/packages/lib-sourcify/test/Compilation/VyperCompilation.spec.ts +++ b/packages/lib-sourcify/test/Compilation/VyperCompilation.spec.ts @@ -614,6 +614,9 @@ describe('VyperCompilation', () => { await compilation.compile(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { outputSelection, ...settings } = compilation.jsonInput.settings; + expect(compilation.metadata).to.deep.equal({ compiler: { version: vyperVersion }, language: 'Vyper', @@ -636,7 +639,7 @@ describe('VyperCompilation', () => { userdoc: {}, }, settings: { - ...compilation.jsonInput.settings, + ...settings, compilationTarget: { [contractFileName]: contractName }, }, sources: { diff --git a/packages/lib-sourcify/test/Compilation/YulCompilation.spec.ts b/packages/lib-sourcify/test/Compilation/YulCompilation.spec.ts new file mode 100644 index 000000000..1c28cb31b --- /dev/null +++ b/packages/lib-sourcify/test/Compilation/YulCompilation.spec.ts @@ -0,0 +1,74 @@ +import { describe, it } from 'mocha'; +import { expect, use } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import { id as keccak256str } from 'ethers'; +import chaiAsPromised from 'chai-as-promised'; +import { YulCompilation } from '../../src/Compilation/YulCompilation'; +import { solc } from '../utils'; +import type { SolidityJsonInput } from '@ethereum-sourcify/compilers-types'; + +use(chaiAsPromised); + +const compilerVersion = '0.8.26+commit.8a97fa7a'; +const contractName = 'cas-forwarder'; +const contractPath = 'cas-forwarder.yul'; +const fixturesBasePath = path.join( + __dirname, + '..', + 'sources', + 'Yul', + 'cas-forwarder', +); + +function loadJsonInput(): SolidityJsonInput { + const jsonInputPath = path.join(fixturesBasePath, 'jsonInput.json'); + return JSON.parse(fs.readFileSync(jsonInputPath, 'utf8')); +} + +describe('YulCompilation', () => { + it('should compile a Yul contract and generate metadata', async () => { + const jsonInput = loadJsonInput(); + + const compilation = new YulCompilation(solc, compilerVersion, jsonInput, { + name: contractName, + path: contractPath, + }); + + await compilation.compile(true); + + expect(compilation.creationBytecode).to.equal( + '0x603780600a5f395ff3fe5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd', + ); + expect(compilation.runtimeBytecode).to.equal( + '0x5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd', + ); + + const metadata = compilation.metadata; + expect(metadata.language).to.equal('Yul'); + expect(metadata.compiler.version).to.equal(compilerVersion); + expect(metadata.settings.compilationTarget).to.deep.equal({ + [contractPath]: contractName, + }); + + const expectedSourceHash = keccak256str( + jsonInput.sources[contractPath].content, + ); + expect(metadata.sources[contractPath].keccak256).to.equal( + expectedSourceHash, + ); + }); + + it('should throw when compilation target is invalid', async () => { + const jsonInput = loadJsonInput(); + + const compilation = new YulCompilation(solc, compilerVersion, jsonInput, { + name: 'non-existent', + path: 'wrong-path.yul', + }); + + await expect(compilation.compile(true)).to.be.rejectedWith( + 'Contract not found in compiler output.', + ); + }); +}); diff --git a/packages/lib-sourcify/test/Verification/Verification.spec.ts b/packages/lib-sourcify/test/Verification/Verification.spec.ts index 89d13a7b1..a4e8ada24 100644 --- a/packages/lib-sourcify/test/Verification/Verification.spec.ts +++ b/packages/lib-sourcify/test/Verification/Verification.spec.ts @@ -12,6 +12,7 @@ import { deployFromAbiAndBytecode, expectVerification, getCompilationFromMetadata, + solc, vyperCompiler, } from '../utils'; import { @@ -24,6 +25,8 @@ import chaiAsPromised from 'chai-as-promised'; import { findSolcPlatform } from '@ethereum-sourcify/compilers'; import { SourcifyChain } from '../../src'; import Sinon from 'sinon'; +import { YulCompilation } from '../../src/Compilation/YulCompilation'; +import type { SolidityJsonInput } from '@ethereum-sourcify/compilers-types'; use(chaiAsPromised); @@ -109,6 +112,49 @@ describe('Verification Class Tests', () => { }); }); + it('should verify a simple Yul contract', async () => { + const contractFolderPath = path.join( + __dirname, + '..', + 'sources', + 'Yul', + 'cas-forwarder', + ); + const { contractAddress, txHash } = await deployFromAbiAndBytecode( + signer, + contractFolderPath, + ); + + const jsonInput: SolidityJsonInput = JSON.parse( + fs.readFileSync( + path.join(contractFolderPath, 'jsonInput.json'), + 'utf8', + ), + ); + + const yulCompilation = new YulCompilation( + solc, + '0.8.26+commit.8a97fa7a', + jsonInput, + { name: 'cas-forwarder', path: 'cas-forwarder.yul' }, + ); + + const verification = new Verification( + yulCompilation, + sourcifyChainHardhat, + contractAddress, + txHash, + ); + await verification.verify(); + + expectVerification(verification, { + status: { + runtimeMatch: 'partial', + creationMatch: 'partial', + }, + }); + }); + it('should partially verify a simple contract', async () => { const contractFolderPath = path.join( __dirname, diff --git a/packages/lib-sourcify/test/sources/Yul/cas-forwarder/artifact.json b/packages/lib-sourcify/test/sources/Yul/cas-forwarder/artifact.json new file mode 100644 index 000000000..da95fd54a --- /dev/null +++ b/packages/lib-sourcify/test/sources/Yul/cas-forwarder/artifact.json @@ -0,0 +1,4 @@ +{ + "abi": [], + "bytecode": "0x603780600a5f395ff3fe5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd" +} diff --git a/packages/lib-sourcify/test/sources/Yul/cas-forwarder/jsonInput.json b/packages/lib-sourcify/test/sources/Yul/cas-forwarder/jsonInput.json new file mode 100644 index 000000000..1c4546478 --- /dev/null +++ b/packages/lib-sourcify/test/sources/Yul/cas-forwarder/jsonInput.json @@ -0,0 +1,22 @@ +{ + "language": "Yul", + "sources": { + "cas-forwarder.yul": { + "content": "// SPDX-License-Identifier: MIT\nobject \"cas-forwarder\" {\n code {\n datacopy(0, dataoffset(\"runtime\"), datasize(\"runtime\"))\n return(0, datasize(\"runtime\"))\n }\n object \"runtime\" {\n code {\n let targetAddress := shr(96, calldataload(0))\n let codeSize := extcodesize(targetAddress)\n extcodecopy(targetAddress, 0, 0, codeSize)\n \n let success := call(gas(), 0xcA11bde05977b3631167028862bE2a173976CA11, 0, 0, codeSize, 0, 0)\n \n let returnSize := returndatasize()\n returndatacopy(0, 0, returnSize)\n \n switch success\n case 0 {\n revert(0, returnSize)\n }\n default {\n return(0, returnSize)\n }\n }\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "details": { + "yul": true + } + }, + "viaIR": true, + "outputSelection": { + "*": { + "*": ["metadata"] + } + } + } +} diff --git a/packages/lib-sourcify/test/sources/Yul/cas-forwarder/sources/cas-forwarder.yul b/packages/lib-sourcify/test/sources/Yul/cas-forwarder/sources/cas-forwarder.yul new file mode 100644 index 000000000..1dbdb3864 --- /dev/null +++ b/packages/lib-sourcify/test/sources/Yul/cas-forwarder/sources/cas-forwarder.yul @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +object "cas-forwarder" { + code { + datacopy(0, dataoffset("runtime"), datasize("runtime")) + return(0, datasize("runtime")) + } + object "runtime" { + code { + let targetAddress := shr(96, calldataload(0)) + let codeSize := extcodesize(targetAddress) + extcodecopy(targetAddress, 0, 0, codeSize) + + let success := call(gas(), 0xcA11bde05977b3631167028862bE2a173976CA11, 0, 0, codeSize, 0, 0) + + let returnSize := returndatasize() + returndatacopy(0, 0, returnSize) + + switch success + case 0 { + revert(0, returnSize) + } + default { + return(0, returnSize) + } + } + } +} diff --git a/services/server/src/server/apiv1/verification/private/stateless/private.stateless.handlers.ts b/services/server/src/server/apiv1/verification/private/stateless/private.stateless.handlers.ts index 21957ed96..f83c6ce6d 100644 --- a/services/server/src/server/apiv1/verification/private/stateless/private.stateless.handlers.ts +++ b/services/server/src/server/apiv1/verification/private/stateless/private.stateless.handlers.ts @@ -14,9 +14,8 @@ import type { import { createMetadataContractsFromFiles, Verification, - SolidityCompilation, - VyperCompilation, splitFullyQualifiedName, + CompilationError, } from "@ethereum-sourcify/lib-sourcify"; import { BadRequestError, @@ -33,6 +32,7 @@ import SourcifyChainMock from "../../../../services/utils/SourcifyChainMock"; import { getCreatorTx } from "../../../../services/utils/contract-creation-util"; import type { CustomReplaceMethod } from "./customReplaceMethods"; import { REPLACE_METHODS } from "./customReplaceMethods"; +import { createCompilationFromJsonInput } from "../../../../services/utils/compilation"; export async function verifyDeprecated( req: LegacyVerifyRequest, @@ -236,25 +236,25 @@ export async function replaceContract( "jsonInput, compilerVersion and compilationTarget are required when forceCompilation is true", ); } - if (jsonInput?.language === "Solidity") { - compilation = new SolidityCompilation( - solc, + try { + compilation = createCompilationFromJsonInput( + { solc, vyper }, compilerVersion, - jsonInput as SolidityJsonInput, + jsonInput, compilationTarget, ); - } else if (jsonInput?.language === "Vyper") { - compilation = new VyperCompilation( - vyper, - compilerVersion, - jsonInput as VyperJsonInput, - compilationTarget, - ); - } else { - throw new BadRequestError( - "Invalid language. Only Solidity and Vyper are supported", - ); + } catch (err: any) { + if ( + err instanceof CompilationError && + err.code === "invalid_language" + ) { + throw new BadRequestError( + "Invalid language. Only Solidity and Vyper are supported", + ); + } + throw err; } + await compilation.compile(); } diff --git a/services/server/src/server/services/VerificationService.ts b/services/server/src/server/services/VerificationService.ts index 653541338..d1dbd1927 100644 --- a/services/server/src/server/services/VerificationService.ts +++ b/services/server/src/server/services/VerificationService.ts @@ -4,14 +4,13 @@ import type { SolidityJsonInput, VyperJsonInput, PathBuffer, - SolidityCompilation, - VyperCompilation, SourcifyChainMap, VerificationExport, SourcifyChainInstance, CompilationTarget, Metadata, EtherscanResult, + AnyCompilation, } from "@ethereum-sourcify/lib-sourcify"; import { Verification } from "@ethereum-sourcify/lib-sourcify"; import { getCreatorTx } from "./utils/contract-creation-util"; @@ -241,7 +240,7 @@ export class VerificationService { } public async verifyFromCompilation( - compilation: SolidityCompilation | VyperCompilation, + compilation: AnyCompilation, sourcifyChain: SourcifyChain, address: string, creatorTxHash?: string, diff --git a/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts b/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts index 46eb554c1..019d82a33 100644 --- a/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts +++ b/services/server/src/server/services/storageServices/SourcifyDatabaseService.ts @@ -914,6 +914,9 @@ export class SourcifyDatabaseService const abi = verification.compilation.contractCompilerOutput.abi; if (!abi) { + if (verification.compilation.language === "Yul") { + return; + } throw new Error("No ABI found in compilation output"); } diff --git a/services/server/src/server/services/utils/compilation.ts b/services/server/src/server/services/utils/compilation.ts new file mode 100644 index 000000000..d9b551a1b --- /dev/null +++ b/services/server/src/server/services/utils/compilation.ts @@ -0,0 +1,57 @@ +import type { + AnyCompilation, + CompilationTarget, + ISolidityCompiler, + IVyperCompiler, +} from "@ethereum-sourcify/lib-sourcify"; +import { + CompilationError, + SolidityCompilation, + VyperCompilation, + YulCompilation, +} from "@ethereum-sourcify/lib-sourcify"; +import type { + AnyJsonInput, + SolidityJsonInput, + VyperJsonInput, +} from "@ethereum-sourcify/compilers-types"; + +export function createCompilationFromJsonInput( + compilers: { + solc: ISolidityCompiler; + vyper: IVyperCompiler; + }, + compilerVersion: string, + jsonInput: AnyJsonInput, + compilationTarget: CompilationTarget, +): AnyCompilation { + switch (jsonInput?.language) { + case "Solidity": { + return new SolidityCompilation( + compilers.solc, + compilerVersion, + jsonInput as SolidityJsonInput, + compilationTarget, + ); + } + case "Yul": { + return new YulCompilation( + compilers.solc, + compilerVersion, + jsonInput as SolidityJsonInput, + compilationTarget, + ); + } + case "Vyper": { + return new VyperCompilation( + compilers.vyper, + compilerVersion, + jsonInput as VyperJsonInput, + compilationTarget, + ); + } + default: { + throw new CompilationError({ code: "invalid_language" }); + } + } +} diff --git a/services/server/src/server/services/utils/database-util.ts b/services/server/src/server/services/utils/database-util.ts index 79c077810..9a91e80a7 100644 --- a/services/server/src/server/services/utils/database-util.ts +++ b/services/server/src/server/services/utils/database-util.ts @@ -815,6 +815,19 @@ export async function getDatabaseColumnsFromVerification( }, ); + let compiler; + switch (verification.compilation.language.toLocaleLowerCase()) { + case "yul": + case "solidity": + compiler = "solc"; + break; + case "vyper": + compiler = "vyper"; + break; + default: + throw new Error("Language not supported"); + } + return { recompiledCreationCode, recompiledRuntimeCode: { @@ -840,10 +853,7 @@ export async function getDatabaseColumnsFromVerification( }, compiledContract: { language: verification.compilation.language.toLocaleLowerCase(), - compiler: - verification.compilation.language.toLocaleLowerCase() === "solidity" - ? "solc" - : "vyper", + compiler, compiler_settings: prepareCompilerSettingsFromVerification(verification), name: verification.compilation.compilationTarget.name, version: verification.compilation.compilerVersion, diff --git a/services/server/src/server/services/workers/verificationWorker.ts b/services/server/src/server/services/workers/verificationWorker.ts index 5a22086ae..e153183e2 100644 --- a/services/server/src/server/services/workers/verificationWorker.ts +++ b/services/server/src/server/services/workers/verificationWorker.ts @@ -1,13 +1,11 @@ import Piscina from "piscina"; import type { - SolidityJsonInput, - VyperJsonInput, SourcifyChainInstance, SourcifyChainMap, + AnyCompilation, + SolidityCompilation, } from "@ethereum-sourcify/lib-sourcify"; import { - SolidityCompilation, - VyperCompilation, Verification, SourcifyLibError, SourcifyChain, @@ -34,6 +32,7 @@ import { getCompilationFromEtherscanResult } from "../utils/etherscan-util"; import { asyncLocalStorage } from "../../../common/async-context"; import SourcifyChainMock from "../utils/SourcifyChainMock"; import { createPreRunCompilationFromStoredCandidate } from "../utils/database-util"; +import { createCompilationFromJsonInput } from "../utils/compilation"; export const filename = resolve(__filename); @@ -108,23 +107,14 @@ async function _verifyFromJsonInput({ compilationTarget, creationTransactionHash, }: VerifyFromJsonInput): Promise { - let compilation: SolidityCompilation | VyperCompilation | undefined; + let compilation: AnyCompilation; try { - if (jsonInput.language === "Solidity") { - compilation = new SolidityCompilation( - solc, - compilerVersion, - jsonInput as SolidityJsonInput, - compilationTarget, - ); - } else if (jsonInput.language === "Vyper") { - compilation = new VyperCompilation( - vyper, - compilerVersion, - jsonInput as VyperJsonInput, - compilationTarget, - ); - } + compilation = createCompilationFromJsonInput( + { solc, vyper }, + compilerVersion, + jsonInput, + compilationTarget, + ); } catch (error: any) { return { errorExport: createErrorExport(error), diff --git a/services/server/test/helpers/helpers.ts b/services/server/test/helpers/helpers.ts index 99be890ef..1990c5520 100644 --- a/services/server/test/helpers/helpers.ts +++ b/services/server/test/helpers/helpers.ts @@ -53,11 +53,11 @@ export type DeploymentInfo = { */ export async function deployFromAbiAndBytecodeForCreatorTxHash( signer: JsonRpcSigner, - abi: JsonFragment[], + abi: JsonFragment[] | undefined, bytecode: BytesLike | { object: string }, args?: any[], ): Promise { - const contractFactory = new ContractFactory(abi, bytecode, signer); + const contractFactory = new ContractFactory(abi || [], bytecode, signer); console.log(`Deploying contract ${args?.length ? `with args ${args}` : ""}`); const deployment = await contractFactory.deploy(...(args || [])); await deployment.waitForDeployment(); diff --git a/services/server/test/integration/apiv2/verification/verify.json.spec.ts b/services/server/test/integration/apiv2/verification/verify.json.spec.ts index 60ddf1ba5..f1372685f 100644 --- a/services/server/test/integration/apiv2/verification/verify.json.spec.ts +++ b/services/server/test/integration/apiv2/verification/verify.json.spec.ts @@ -130,6 +130,54 @@ describe("POST /v2/verify/:chainId/:address", function () { ); }); + it("should verify a Yul contract", async () => { + const { resolveWorkers } = makeWorkersWait(); + + const yulContractPath = path.join( + __dirname, + "..", + "..", + "..", + "sources", + "yul", + "cas-forwarder", + ); + const yulArtifact = JSON.parse( + fs.readFileSync(path.join(yulContractPath, "artifact.json"), "utf8"), + ); + const jsonInput = JSON.parse( + fs.readFileSync(path.join(yulContractPath, "jsonInput.json"), "utf8"), + ); + const sourceFileName = "cas-forwarder.yul"; + const contractIdentifier = `${sourceFileName}:cas-forwarder`; + + const { contractAddress, txHash } = + await deployFromAbiAndBytecodeForCreatorTxHash( + chainFixture.localSigner, + yulArtifact.abi, + yulArtifact.bytecode, + ); + + const verifyRes = await chai + .request(serverFixture.server.app) + .post(`/v2/verify/${chainFixture.chainId}/${contractAddress}`) + .send({ + stdJsonInput: jsonInput, + compilerVersion: "0.8.26+commit.8a97fa7a", + contractIdentifier, + creationTransactionHash: txHash, + }); + + await assertJobVerification( + serverFixture, + verifyRes, + resolveWorkers, + chainFixture.chainId, + contractAddress, + "match", + ); + }); + it("should fetch the creation transaction hash if not provided", async () => { const { resolveWorkers } = makeWorkersWait(); diff --git a/services/server/test/integration/database.spec.ts b/services/server/test/integration/database.spec.ts index 151edfdba..13b047151 100644 --- a/services/server/test/integration/database.spec.ts +++ b/services/server/test/integration/database.spec.ts @@ -1,6 +1,9 @@ import chai from "chai"; import chaiHttp from "chai-http"; -import { deployFromAbiAndBytecodeForCreatorTxHash } from "../helpers/helpers"; +import { + deployFromAbiAndBytecodeForCreatorTxHash, + hookIntoVerificationWorkerRun, +} from "../helpers/helpers"; import { id as keccak256str, keccak256 } from "ethers"; import { LocalChainFixture } from "../helpers/LocalChainFixture"; import { ServerFixture } from "../helpers/ServerFixture"; @@ -32,6 +35,12 @@ function sha3_256(data: Bytes) { describe("Verifier Alliance database", function () { const chainFixture = new LocalChainFixture(); const serverFixture = new ServerFixture(); + const sandbox = sinon.createSandbox(); + const makeWorkersWait = hookIntoVerificationWorkerRun(sandbox, serverFixture); + + afterEach(async () => { + sandbox.restore(); + }); const verifierAllianceTest = async (testCase: any) => { const constructorArguments = @@ -149,6 +158,55 @@ describe("Verifier Alliance database", function () { ); }; + const verifierAllianceTestYul = async (testCase: any) => { + const { resolveWorkers } = makeWorkersWait(); + const constructorArguments = + testCase?.creation_values?.constructorArguments; + const { contractAddress, txHash, blockNumber, txIndex } = + await deployFromAbiAndBytecodeForCreatorTxHash( + chainFixture.localSigner, + testCase.compilation_artifacts.abi, + constructorArguments + ? testCase.compiled_creation_code + : testCase.deployed_creation_code, + constructorArguments ? [constructorArguments] : undefined, + ); + + const stdJsonInput = { + language: "Yul", + sources: Object.keys(testCase.sources).reduce( + (sources: any, sourceKey: string) => { + sources[sourceKey] = { + content: testCase.sources[sourceKey], + }; + return sources; + }, + {}, + ), + settings: testCase.compiler_settings, + }; + const verifyRes = await chai + .request(serverFixture.server.app) + .post(`/v2/verify/${chainFixture.chainId}/${contractAddress}`) + .send({ + contractIdentifier: testCase.fully_qualified_name, + creationTransactionHash: txHash, + stdJsonInput, + compilerVersion: "0.8.26+commit.8a97fa7a", + }); + + chai.expect(verifyRes.status).to.equal(202); + await resolveWorkers(); + + await assertDatabase( + testCase, + contractAddress, + txHash, + blockNumber, + txIndex, + ); + }; + const assertDatabase = async ( testCase: any, address: string, @@ -388,6 +446,13 @@ describe("Verifier Alliance database", function () { await verifierAllianceTest(verifierAllianceTestDoubleAuxdata); }); + it("Store Yul match in database", async () => { + const verifierAllianceTestYulMatch = await import( + "../verifier-alliance/yul.json" + ); + await verifierAllianceTestYul(verifierAllianceTestYulMatch); + }); + // Tests to be implemented: // - genesis: right now not supported, // - partial_match_2: I don't know why we have this test diff --git a/services/server/test/sources/yul/cas-forwarder/artifact.json b/services/server/test/sources/yul/cas-forwarder/artifact.json new file mode 100644 index 000000000..da95fd54a --- /dev/null +++ b/services/server/test/sources/yul/cas-forwarder/artifact.json @@ -0,0 +1,4 @@ +{ + "abi": [], + "bytecode": "0x603780600a5f395ff3fe5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd" +} diff --git a/services/server/test/sources/yul/cas-forwarder/cas-forwarder.yul b/services/server/test/sources/yul/cas-forwarder/cas-forwarder.yul new file mode 100644 index 000000000..1dbdb3864 --- /dev/null +++ b/services/server/test/sources/yul/cas-forwarder/cas-forwarder.yul @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +object "cas-forwarder" { + code { + datacopy(0, dataoffset("runtime"), datasize("runtime")) + return(0, datasize("runtime")) + } + object "runtime" { + code { + let targetAddress := shr(96, calldataload(0)) + let codeSize := extcodesize(targetAddress) + extcodecopy(targetAddress, 0, 0, codeSize) + + let success := call(gas(), 0xcA11bde05977b3631167028862bE2a173976CA11, 0, 0, codeSize, 0, 0) + + let returnSize := returndatasize() + returndatacopy(0, 0, returnSize) + + switch success + case 0 { + revert(0, returnSize) + } + default { + return(0, returnSize) + } + } + } +} diff --git a/services/server/test/sources/yul/cas-forwarder/jsonInput.json b/services/server/test/sources/yul/cas-forwarder/jsonInput.json new file mode 100644 index 000000000..f3dff1f87 --- /dev/null +++ b/services/server/test/sources/yul/cas-forwarder/jsonInput.json @@ -0,0 +1,22 @@ +{ + "language": "Yul", + "sources": { + "cas-forwarder.yul": { + "content": "// SPDX-License-Identifier: MIT\nobject \"cas-forwarder\" {\n code {\n datacopy(0, dataoffset(\"runtime\"), datasize(\"runtime\"))\n return(0, datasize(\"runtime\"))\n }\n object \"runtime\" {\n code {\n let targetAddress := shr(96, calldataload(0))\n let codeSize := extcodesize(targetAddress)\n extcodecopy(targetAddress, 0, 0, codeSize)\n \n let success := call(gas(), 0xcA11bde05977b3631167028862bE2a173976CA11, 0, 0, codeSize, 0, 0)\n \n let returnSize := returndatasize()\n returndatacopy(0, 0, returnSize)\n \n switch success\n case 0 {\n revert(0, returnSize)\n }\n default {\n return(0, returnSize)\n }\n }\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "details": { + "yul": true + } + }, + "viaIR": true, + "outputSelection": { + "*": { + "*": ["*"] + } + } + } +} diff --git a/services/server/test/verifier-alliance/yul.json b/services/server/test/verifier-alliance/yul.json new file mode 100644 index 000000000..e4628af87 --- /dev/null +++ b/services/server/test/verifier-alliance/yul.json @@ -0,0 +1,49 @@ +{ + "_comment": "Store Yul match in database", + + "deployed_creation_code": "0x603780600a5f395ff3fe5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd", + "deployed_runtime_code": "0x5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd", + + "compiled_creation_code": "0x603780600a5f395ff3fe5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd", + "compiled_runtime_code": "0x5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd", + "compiler": "solc", + "version": "0.8.26+commit.8a97fa7a", + "language": "yul", + "name": "cas-forwarder", + "fully_qualified_name": "cas-forwarder.yul:cas-forwarder", + "sources": { + "cas-forwarder.yul": "// SPDX-License-Identifier: MIT\nobject \"cas-forwarder\" {\n code {\n datacopy(0, dataoffset(\"runtime\"), datasize(\"runtime\"))\n return(0, datasize(\"runtime\"))\n }\n object \"runtime\" {\n code {\n let targetAddress := shr(96, calldataload(0))\n let codeSize := extcodesize(targetAddress)\n extcodecopy(targetAddress, 0, 0, codeSize)\n \n let success := call(gas(), 0xcA11bde05977b3631167028862bE2a173976CA11, 0, 0, codeSize, 0, 0)\n \n let returnSize := returndatasize()\n returndatacopy(0, 0, returnSize)\n \n switch success\n case 0 {\n revert(0, returnSize)\n }\n default {\n return(0, returnSize)\n }\n }\n }\n}\n" + }, + "compiler_settings": { + "viaIR": true, + "optimizer": { "details": { "yul": true }, "enabled": true } + }, + "compilation_artifacts": { + "abi": null, + "devdoc": null, + "sources": null, + "userdoc": null, + "storageLayout": null + }, + "creation_code_artifacts": { + "sourceMap": "111:19:0:-:0;88:21;;85:1;76:55;85:1;140:30", + "cborAuxdata": {}, + "linkReferences": {} + }, + "runtime_code_artifacts": { + "sourceMap": "269:1:0:-:0;256:15;;;;252:2;248:24;301:26;;;340:42;;;;428:5;;435:42;428:5;423:77;544:16;573:32;;269:1;573:32;;658:60;;;269:1;757:21;665:53;269:1;683:21", + "cborAuxdata": {}, + "linkReferences": {}, + "immutableReferences": null + }, + + "creation_match": true, + "creation_values": {}, + "creation_transformations": [], + "creation_metadata_match": false, + + "runtime_match": true, + "runtime_values": {}, + "runtime_transformations": [], + "runtime_metadata_match": false +}