Skip to content

(v0.8.21) Contract code changes with additional files in compilation #14494

Closed
@kuzdogan

Description

@kuzdogan

Here I am again with another issue with extra files in compilation causing trouble 🫡

By extra files I mean files that the target contract does not depend on.

Recap

First a recap on previous issues similarly on extra files in compilation:

  1. Different bytecode with same metadata on 0.6.12 #12281
    This was an older issue that seems to affect the AST IDs in 0.6.12 and 0.7.0. We also encountered this and handled in Sourcify's case Handle Solidity v0.6.12 and v0.7.0 extra files bytecode mismatch sourcify#618
  2. Contract bytecode changes vastly when independent contracts added to the compiler  #14250
    I've encountered this in a 0.8.19 contract and got fixed in 0.8.21 with Deterministically choose memory slots for variables during stack-to-memory. #14311.

Current Issue

I've encountered the current issue in ethereum/sourcify#1065.

My initial suspicion was that it's an instance of the 2. case because the contract was a 0.8.19 contract. Unfortunately, I was able to reproduce the case in 0.8.21.

Although the 1. case should have been resolved, I suspect it still can be related because the metadata hashes are identical but the bytecodes differ.

To reproduce

I named inputs as "hardhat" and "sourcify". "Sourcify" uses the minimum number of source files, because it is based on the ones listed in the contract metadata. "Hardhat" uses extra because by default Hardhat inputs everything to the compiler. Sourcify has two different inputs 0.8.19 and 0.8.21 because it includes the evmVersion inside the input i.e. paris vs shanghai.

N2MERC721-hardhat-input.json.txt
N2MERC721-sourcify-input-0.8.21.json.txt
N2MERC721-sourcify-input-0.8.19.json.txt

You can see the two inputs differ only in some extra files + some of the compiler settings

These are the extra files in Hardhat input
[
  '@nfts2me/contracts/interfaces/IN2MBeaconFactory.sol',
  '@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol',
  '@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol',
  '@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/IERC1155MetadataURIUpgradeable.sol',
  '@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol',
  '@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol',
  '@openzeppelin/contracts/governance/utils/IVotes.sol',
  '@openzeppelin/contracts/proxy/Proxy.sol',
  '@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol',
  '@openzeppelin/contracts/token/ERC1155/IERC1155.sol',
  '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol',
  '@openzeppelin/contracts/token/ERC721/IERC721.sol',
  '@openzeppelin/contracts/utils/introspection/IERC165.sol',
  'contracts/beacon/N2MERC1155Upgradeable.sol',
  'contracts/beacon/N2MERC721Upgradeable.sol',
  'contracts/beacon/N2MUpgradeable.sol',
  'contracts/interfaces/IN2M_ERCBase.sol',
  'contracts/interfaces/IN2M_ERCCommon.sol',
  'contracts/interfaces/IN2M_ERCLibrary.sol',
  'contracts/interfaces/IN2M_ERCStorage.sol',
  'contracts/interfaces/IN2MBeaconFactory.sol',
  'contracts/interfaces/IN2MCrossFactory.sol',
  'contracts/interfaces/IN2MERC1155.sol',
  'contracts/interfaces/IN2MERC721.sol',
  'contracts/interfaces/IOperatorFilterRegistry.sol',
  'contracts/interfaces/IReverseRegistrar.sol',
  'contracts/N2MERC1155.sol',
  'contracts/ownable/NFTOwnableUpgradeable.sol',
  'contracts/TextUtils.sol'
]
Also leaving the JS script I used to check the differences of the keys of `.sources` in the JSON inputs

Usage:
node checkFileKeysDifferences.js solcInput-1.json solcInput-2.json

const fs = require("fs");

function findKeyDifferences(file1, file2) {
  const json1 = JSON.parse(fs.readFileSync(file1, "utf8"));
  const json2 = JSON.parse(fs.readFileSync(file2, "utf8"));

  const sources1 = new Set(Object.keys(json1.sources) || []);
  const sources2 = new Set(Object.keys(json2.sources) || []);

  const keysOnlyInFile1 = Array.from(sources1).filter(
    (key) => !sources2.has(key)
  );
  const keysOnlyInFile2 = Array.from(sources2).filter(
    (key) => !sources1.has(key)
  );

  return {
    keysOnlyInFile1,
    keysOnlyInFile2,
  };
}

// Retrieve file paths from command-line arguments
const [file1Path, file2Path] = process.argv.slice(2);

if (!file1Path || !file2Path) {
  console.log("Please provide two file paths as command-line arguments.");
  process.exit(1);
}

const { keysOnlyInFile1, keysOnlyInFile2 } = findKeyDifferences(
  file1Path,
  file2Path
);

console.log("Keys only in file 1:", keysOnlyInFile1);
console.log("Keys only in file 2:", keysOnlyInFile2);

To extract the runtime bytecode:

cat N2MERC721-sourcify-input-0.8.19.json | solc --standard-json | jq '.contracts."contracts/N2MERC721.sol".N2MERC721.evm.deployedBytecode.object' > N2MERC721-0.8.19-sourcify-runtime-bytecode-from-solc.txt

My Bytecode outputs

N2MERC721-0.8.21-sourcify-runtime-bytecode.txt
N2MERC721-0.8.21-hardhat-runtime-bytecode.txt
N2MERC721-0.8.19-sourcify-runtime-bytecode.txt
N2MERC721-0.8.19-hardhat-runtime-bytecode.txt

To check the diff:

git diff --word-diff --word-diff-regex=. N2MERC721-0.8.19-hardhat-runtime-bytecode.txt N2MERC721-0.8.19-sourcify-runtime-bytecode.txt

Will give you a diff like this:
image

Environment

  • Compiler version: 0.8.19 and 0.8.21 (both Emscripten and Darwin.appleclang)
  • Target EVM version (as per compiler settings): paris and shanghai
  • Framework/IDE (e.g. Truffle or Remix): none
  • EVM execution environment / backend / blockchain client:
  • Operating system: MacOS

Again, one particular thing that got my attention was that the metadata hashes were identical in all cases. This leaves me thinking if this is similar to the 1. case I mentioned.

Metadata

Metadata

Type

No type

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions