3

How is contract creation code output for modern versions of solc? Where can I find the exact transaction data format?

I want to be able to quickly identify where constructor arguments start, as well as where actual code starts and ends.

On Etherscan, Constructor Arguments are provided for you in the Code section at the very bottom for a contract, but I would like to figure out how to decode this from the transaction data alone.

1
  • A quick workaround, but not 100% safe, is to spot the end of the metadata (which is 51bytes for the length, 0x0033) - everything after are constructor args (only a comment as it only works for solidity and might change one day) May 19, 2023 at 22:25

1 Answer 1

3

I want to be able to quickly identify where constructor arguments start, as well as where actual code starts and ends.

This part is relatively easy, but not trivial. I'm assuming that you don't have the contract solidity code, neither the ABI or anything else. The compiler follows a well defined set of rules which lead to opcode patterns that you can recognize and split the deployement bytecode into 3 sections :

  1. Constructor Code
  2. Contract Code
  3. Constructor Arguments

Let me first take an arbitrary contract :

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Example {

  constructor(uint256 a, uint256 b) {
  }

  function aRandomFunction() public view {
    
  }
}

Which I deployed here on Ropsten with a = 1 and b = 2 for the example.

We can use a pattern that is an integral part of the deployment bytecode : the constructor CODECOPY block. Which should be located right before the Contract starting offset. There are others, but the longer the better (less chances of false positive + this one actually includes information that we need to find for the split) It has the following format :

  • 0x5B JUMPDEST (OPTIONAL)
  • 0x60 PUSH1 / 0x61 PUSH2 / 0x62 PUSH3
  • 0x80 DUP1
  • 0x60 PUSH1 / 0x61 PUSH2 / 0x62 PUSH3
  • 0x60 0x00 PUSH1 (0)
  • 0x39 CODECOPY
  • 0x60 0x00 PUSH1 (0)
  • 0xF3 RETURN
  • 0xFE INVALID

The first PUSHX is actually the contract bytecode length, and the second is its offset in the txdata. Since the constructor starts at offset 0 we can deduce the position of the arguments too.

  • constructor code = data[0:contract offset]
  • contract code = data[contract offset: contract offset + contract length]
  • arguments = data[contract offset + contract length:]

I made a JavaScript version of this process targeting the contract deployment tx of the example contract :

const Web3 = require('web3');

const ENDPOINT = "THE-ENDPOINT-ADDRESS"
const TX_HASH = "0xf4dce67834b31918c84cb9041cf910bb34e53319fe08550fce24fa623a536274"

function splitContractDeployment(input) {
   input = input.toLowerCase().replace("0x", "");

   // Look for a special pattern of contract deployement tx present at the end of the constructor bytecode
   // 0x5B JUMPDEST (OPTIONAL)
   // 0x60 PUSH1 / 0x61 PUSH2 / 0x62 PUSH3 : This is the contract bytecode length
   // 0x80 DUP1
   // 0x60 PUSH1 / 0x61 PUSH2 / 0x62 PUSH3 : This the the contract bytecode offset
   // 0x60 0x00 PUSH1 (0)
   // 0x39 CODECOPY
   // 0x60 0x00 PUSH1 (0)
   // 0xF3 RETURN
   // 0xFE INVALID
   const CODECOPY_REGEX = /(?:5b)?(?:60([a-z0-9]{2})|61([a-z0-9_]{4})|62([a-z0-9_]{6}))80(?:60([a-z0-9]{2})|61([a-z0-9_]{4})|62([a-z0-9_]{6}))6000396000f3fe/gm;

   m = CODECOPY_REGEX.exec(input)
   
   if (m == null || m == undefined) {
     throw Error("Input is not a standard deployement bytecode");
   }

   const CONSTRUCTOR_OFFSET = 0
   const CONTRACT_LENGTH = parseInt(m[1] || m[2] || m[3], 16) * 2
   const CONTRACT_OFFSET = parseInt(m[4] || m[5] || m[6], 16) * 2
   
   // Also get embedded metadata from the contract's code : located at the end of it
   const METADATA_LENGTH = parseInt(input.slice(CONTRACT_OFFSET + CONTRACT_LENGTH - 4, CONTRACT_OFFSET + CONTRACT_LENGTH), 16) * 2 + 4;

   return {
    constructor: input.slice(CONSTRUCTOR_OFFSET, CONTRACT_OFFSET),
    contract: input.slice(CONTRACT_OFFSET, CONTRACT_OFFSET + CONTRACT_LENGTH),
    metadata: input.slice(CONTRACT_OFFSET + CONTRACT_LENGTH - METADATA_LENGTH, CONTRACT_OFFSET + CONTRACT_LENGTH),
    arguments: input.slice(CONTRACT_OFFSET + CONTRACT_LENGTH),
   };
}

async function main() {
   const web3 = new Web3(ENDPOINT);
   const tx = await web3.eth.getTransaction(TX_HASH);

   // split contains :
   // - constructor : the constructor bytecode
   // - contract : the contract bytecode
   // - metadata: the contract metadata CBOR encoded
   // - arguments : the constructor arguments
   const split = splitContractDeployment(tx.input)

   // Decoding the arguments requires you to know the constructor ABI
   // Outputs : 1 & 2
   console.log(web3.eth.abi.decodeParameters(["uint256", "uint256"], split.arguments))
}

main();

As I commented, to decode the arguments, you must know the constructor ABI. I tested it on several deployment tx, but if you happen to find a mistake, don't hesitate to let me know.

I hope that answers your question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.