-
Notifications
You must be signed in to change notification settings - Fork 499
Add support for Yul compilation and verification #2521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
a1f21e7
cd3d815
9f35cc3
1837d97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
Comment on lines
+29
to
+41
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this a duplicate of this code? sourcify/packages/lib-sourcify/src/Validation/SolidityMetadataContract.ts Lines 320 to 327 in a1f21e7
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the opposite code, now I created another function so it's more clear. Let me know if it is more understandable now |
||||||||||||||||||
| const metadataLibraries: NonNullable<MetadataCompilerSettings['libraries']> = | ||||||||||||||||||
| {}; | ||||||||||||||||||
|
|
||||||||||||||||||
| 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); | ||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.', | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small thing, this should exclude
outputSelection, it's not in metadata.settings: https://docs.soliditylang.org/en/latest/metadata.htmlSame change needed in Vyper metadata generation