Skip to content

Commit a1f21e7

Browse files
Add support for Yul compilation and verification
1 parent fc68901 commit a1f21e7

File tree

24 files changed

+615
-51
lines changed

24 files changed

+615
-51
lines changed

packages/compilers-types/src/CompilationTypes.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { JsonFragment } from "ethers";
2-
import { SolidityOutputError, SoliditySettings } from "./SolidityTypes";
3-
import { VyperOutputError } from "./VyperTypes";
1+
import type { JsonFragment } from "ethers";
2+
import type {
3+
SolidityJsonInput,
4+
SolidityOutputError,
5+
SoliditySettings,
6+
} from "./SolidityTypes";
7+
import type { VyperJsonInput, VyperOutputError } from "./VyperTypes";
48

59
export interface LinkReferences {
610
[filePath: string]: {
@@ -105,3 +109,5 @@ export interface Metadata {
105109
sources: MetadataSourceMap;
106110
version: number;
107111
}
112+
113+
export type AnyJsonInput = SolidityJsonInput | VyperJsonInput;

packages/lib-sourcify/src/Compilation/CompilationTypes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type {
66
} from '@ethereum-sourcify/compilers-types';
77
import type { SourcifyLibErrorParameters } from '../SourcifyLibError';
88
import { SourcifyLibError } from '../SourcifyLibError';
9+
import type { SolidityCompilation } from './SolidityCompilation';
10+
import type { VyperCompilation } from './VyperCompilation';
911

1012
export interface CompiledContractCborAuxdata {
1113
[key: string]: {
@@ -28,9 +30,10 @@ export interface CompilationTarget {
2830
path: string;
2931
}
3032

31-
export type CompilationLanguage = 'Solidity' | 'Vyper';
33+
export type CompilationLanguage = 'Solidity' | 'Vyper' | 'Yul';
3234

3335
export type CompilationErrorCode =
36+
| 'invalid_language'
3437
| 'cannot_generate_cbor_auxdata_positions'
3538
| 'invalid_compiler_version'
3639
| 'unsupported_compiler_version'
@@ -66,3 +69,5 @@ export interface IVyperCompiler {
6669
vyperJsonInput: VyperJsonInput,
6770
): Promise<VyperOutput>;
6871
}
72+
73+
export type AnyCompilation = SolidityCompilation | VyperCompilation;

packages/lib-sourcify/src/Compilation/PreRunCompilation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class PreRunCompilation extends AbstractCompilation {
7979

8080
get immutableReferences(): ImmutableReferences {
8181
switch (this.language) {
82+
case 'Yul':
8283
case 'Solidity': {
8384
const compilationTarget = this
8485
.contractCompilerOutput as SolidityOutputContract;
@@ -97,6 +98,7 @@ export class PreRunCompilation extends AbstractCompilation {
9798

9899
get runtimeLinkReferences(): LinkReferences {
99100
switch (this.language) {
101+
case 'Yul':
100102
case 'Solidity': {
101103
const compilationTarget = this
102104
.contractCompilerOutput as SolidityOutputContract;
@@ -109,6 +111,7 @@ export class PreRunCompilation extends AbstractCompilation {
109111

110112
get creationLinkReferences(): LinkReferences {
111113
switch (this.language) {
114+
case 'Yul':
112115
case 'Solidity': {
113116
const compilationTarget = this
114117
.contractCompilerOutput as SolidityOutputContract;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type {
2+
MetadataCompilerSettings,
3+
SoliditySettings,
4+
} from '@ethereum-sourcify/compilers-types';
5+
import type { CompilationLanguage } from './CompilationTypes';
6+
import { SolidityCompilation } from './SolidityCompilation';
7+
import { id as keccak256str } from 'ethers';
8+
import { convertLibrariesToMetadataFormat } from '../utils/utils';
9+
10+
/**
11+
* Abstraction of a Yul compilation
12+
*/
13+
export class YulCompilation extends SolidityCompilation {
14+
public language: CompilationLanguage = 'Yul';
15+
16+
public async compile(forceEmscripten = false) {
17+
await this.compileAndReturnCompilationTarget(forceEmscripten);
18+
this.generateMetadata();
19+
}
20+
21+
/**
22+
* Yul compiler does not produce a metadata but we generate it ourselves for backward
23+
* compatibility reasons e.g. in the legacy Sourcify API that always assumes a metadata.json
24+
*/
25+
generateMetadata() {
26+
const contract = this.contractCompilerOutput;
27+
const outputMetadata = {
28+
abi: contract.abi,
29+
devdoc: contract.devdoc,
30+
userdoc: contract.userdoc,
31+
};
32+
33+
const sourcesWithHashes = Object.entries(this.jsonInput.sources).reduce(
34+
(acc, [path, source]) => ({
35+
...acc,
36+
[path]: {
37+
keccak256: keccak256str(source.content),
38+
},
39+
}),
40+
{},
41+
);
42+
43+
const soliditySettings = JSON.parse(
44+
JSON.stringify(this.jsonInput.settings),
45+
) as SoliditySettings;
46+
47+
const metadataSettings: Omit<
48+
MetadataCompilerSettings,
49+
'compilationTarget'
50+
> & { outputSelection: undefined } = {
51+
...soliditySettings,
52+
outputSelection: undefined,
53+
libraries: convertLibrariesToMetadataFormat(
54+
this.jsonInput.settings.libraries,
55+
),
56+
};
57+
this._metadata = {
58+
compiler: { version: this.compilerVersion },
59+
language: 'Yul',
60+
output: outputMetadata,
61+
settings: {
62+
...metadataSettings,
63+
compilationTarget: {
64+
[this.compilationTarget.path]: this.compilationTarget.name,
65+
},
66+
},
67+
sources: sourcesWithHashes,
68+
version: 1,
69+
};
70+
}
71+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type {
2+
AnyJsonInput,
3+
SolidityJsonInput,
4+
VyperJsonInput,
5+
} from '@ethereum-sourcify/compilers-types';
6+
import { SolidityCompilation } from './SolidityCompilation';
7+
import { VyperCompilation } from './VyperCompilation';
8+
import {
9+
CompilationError,
10+
type AnyCompilation,
11+
type CompilationTarget,
12+
type ISolidityCompiler,
13+
type IVyperCompiler,
14+
} from './CompilationTypes';
15+
import { YulCompilation } from './YulCompilation';
16+
17+
export function createCompilationFromJsonInput(
18+
compilers: {
19+
solc: ISolidityCompiler;
20+
vyper: IVyperCompiler;
21+
},
22+
compilerVersion: string,
23+
jsonInput: AnyJsonInput,
24+
compilationTarget: CompilationTarget,
25+
) {
26+
let compilation: AnyCompilation;
27+
switch (jsonInput?.language) {
28+
case 'Solidity': {
29+
compilation = new SolidityCompilation(
30+
compilers.solc,
31+
compilerVersion,
32+
jsonInput as SolidityJsonInput,
33+
compilationTarget,
34+
);
35+
break;
36+
}
37+
case 'Yul': {
38+
compilation = new YulCompilation(
39+
compilers.solc,
40+
compilerVersion,
41+
jsonInput as SolidityJsonInput,
42+
compilationTarget,
43+
);
44+
break;
45+
}
46+
case 'Vyper': {
47+
compilation = new VyperCompilation(
48+
compilers.vyper,
49+
compilerVersion,
50+
jsonInput as VyperJsonInput,
51+
compilationTarget,
52+
);
53+
break;
54+
}
55+
default: {
56+
throw new CompilationError({ code: 'invalid_language' });
57+
}
58+
}
59+
return compilation;
60+
}

packages/lib-sourcify/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './Compilation/SolidityCompilation';
1212
export * from './Compilation/VyperCompilation';
1313
export * from './Compilation/PreRunCompilation';
1414
export * from './Compilation/CompilationTypes';
15+
export * from './Compilation/utils';
1516

1617
// Verification exports
1718
export * from './Verification/Verification';

packages/lib-sourcify/src/utils/utils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import type {
2+
Libraries,
3+
MetadataCompilerSettings,
4+
} from '@ethereum-sourcify/compilers-types';
5+
16
/**
27
* Checks whether the provided object contains any keys or not.
38
* @param obj The object whose emptiness is tested.
@@ -21,3 +26,29 @@ export function splitFullyQualifiedName(fullyQualifiedName: string): {
2126
const contractPath = splitIdentifier.slice(0, -1).join(':');
2227
return { contractPath, contractName };
2328
}
29+
30+
/**
31+
* Converts libraries from the solc JSON input format to the metadata format.
32+
* jsonInput format: { "contracts/1_Storage.sol": { Journal: "0x..." } }
33+
* metadata format: { "contracts/1_Storage.sol:Journal": "0x..." }
34+
*/
35+
export function convertLibrariesToMetadataFormat(
36+
libraries?: Libraries,
37+
): MetadataCompilerSettings['libraries'] {
38+
if (!libraries) {
39+
return undefined;
40+
}
41+
42+
const metadataLibraries: NonNullable<MetadataCompilerSettings['libraries']> =
43+
{};
44+
45+
for (const [contractPath, libraryMap] of Object.entries(libraries)) {
46+
for (const [libraryName, libraryAddress] of Object.entries(libraryMap)) {
47+
const metadataKey =
48+
contractPath === '' ? libraryName : `${contractPath}:${libraryName}`;
49+
metadataLibraries[metadataKey] = libraryAddress;
50+
}
51+
}
52+
53+
return Object.keys(metadataLibraries).length ? metadataLibraries : undefined;
54+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it } from 'mocha';
2+
import { expect, use } from 'chai';
3+
import fs from 'fs';
4+
import path from 'path';
5+
import { id as keccak256str } from 'ethers';
6+
import chaiAsPromised from 'chai-as-promised';
7+
import { YulCompilation } from '../../src/Compilation/YulCompilation';
8+
import { solc } from '../utils';
9+
import type { SolidityJsonInput } from '@ethereum-sourcify/compilers-types';
10+
11+
use(chaiAsPromised);
12+
13+
const compilerVersion = '0.8.26+commit.8a97fa7a';
14+
const contractName = 'cas-forwarder';
15+
const contractPath = 'cas-forwarder.yul';
16+
const fixturesBasePath = path.join(
17+
__dirname,
18+
'..',
19+
'sources',
20+
'Yul',
21+
'cas-forwarder',
22+
);
23+
24+
function loadJsonInput(): SolidityJsonInput {
25+
const jsonInputPath = path.join(fixturesBasePath, 'jsonInput.json');
26+
return JSON.parse(fs.readFileSync(jsonInputPath, 'utf8'));
27+
}
28+
29+
describe('YulCompilation', () => {
30+
it('should compile a Yul contract and generate metadata', async () => {
31+
const jsonInput = loadJsonInput();
32+
33+
const compilation = new YulCompilation(solc, compilerVersion, jsonInput, {
34+
name: contractName,
35+
path: contractPath,
36+
});
37+
38+
await compilation.compile(true);
39+
40+
expect(compilation.creationBytecode).to.equal(
41+
'0x603780600a5f395ff3fe5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd',
42+
);
43+
expect(compilation.runtimeBytecode).to.equal(
44+
'0x5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd',
45+
);
46+
47+
const metadata = compilation.metadata;
48+
expect(metadata.language).to.equal('Yul');
49+
expect(metadata.compiler.version).to.equal(compilerVersion);
50+
expect(metadata.settings.compilationTarget).to.deep.equal({
51+
[contractPath]: contractName,
52+
});
53+
54+
const expectedSourceHash = keccak256str(
55+
jsonInput.sources[contractPath].content,
56+
);
57+
expect(metadata.sources[contractPath].keccak256).to.equal(
58+
expectedSourceHash,
59+
);
60+
});
61+
62+
it('should throw when compilation target is invalid', async () => {
63+
const jsonInput = loadJsonInput();
64+
65+
const compilation = new YulCompilation(solc, compilerVersion, jsonInput, {
66+
name: 'non-existent',
67+
path: 'wrong-path.yul',
68+
});
69+
70+
await expect(compilation.compile(true)).to.be.rejectedWith(
71+
'Contract not found in compiler output.',
72+
);
73+
});
74+
});

packages/lib-sourcify/test/Verification/Verification.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
deployFromAbiAndBytecode,
1313
expectVerification,
1414
getCompilationFromMetadata,
15+
solc,
1516
vyperCompiler,
1617
} from '../utils';
1718
import {
@@ -24,6 +25,8 @@ import chaiAsPromised from 'chai-as-promised';
2425
import { findSolcPlatform } from '@ethereum-sourcify/compilers';
2526
import { SourcifyChain } from '../../src';
2627
import Sinon from 'sinon';
28+
import { YulCompilation } from '../../src/Compilation/YulCompilation';
29+
import type { SolidityJsonInput } from '@ethereum-sourcify/compilers-types';
2730

2831
use(chaiAsPromised);
2932

@@ -109,6 +112,49 @@ describe('Verification Class Tests', () => {
109112
});
110113
});
111114

115+
it('should verify a simple Yul contract', async () => {
116+
const contractFolderPath = path.join(
117+
__dirname,
118+
'..',
119+
'sources',
120+
'Yul',
121+
'cas-forwarder',
122+
);
123+
const { contractAddress, txHash } = await deployFromAbiAndBytecode(
124+
signer,
125+
contractFolderPath,
126+
);
127+
128+
const jsonInput: SolidityJsonInput = JSON.parse(
129+
fs.readFileSync(
130+
path.join(contractFolderPath, 'jsonInput.json'),
131+
'utf8',
132+
),
133+
);
134+
135+
const yulCompilation = new YulCompilation(
136+
solc,
137+
'0.8.26+commit.8a97fa7a',
138+
jsonInput,
139+
{ name: 'cas-forwarder', path: 'cas-forwarder.yul' },
140+
);
141+
142+
const verification = new Verification(
143+
yulCompilation,
144+
sourcifyChainHardhat,
145+
contractAddress,
146+
txHash,
147+
);
148+
await verification.verify();
149+
150+
expectVerification(verification, {
151+
status: {
152+
runtimeMatch: 'partial',
153+
creationMatch: 'partial',
154+
},
155+
});
156+
});
157+
112158
it('should partially verify a simple contract', async () => {
113159
const contractFolderPath = path.join(
114160
__dirname,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"abi": [],
3+
"bytecode": "0x603780600a5f395ff3fe5f8080803560601c81813b9283923c818073ca11bde05977b3631167028862be2a173976ca115af13d90815f803e156034575ff35b5ffd"
4+
}

0 commit comments

Comments
 (0)