0

Is there any difference when initializing variables in the contract constructor or directly in the declaration? What is the best practice?

contract A {
   uint public storage = 10000;
   constructor(){}
}

or

contract B {
   uint public storage;
   constructor(){
      storage = 10000;
   }
}
2

The place where you initialize each variable determines when the initialization code will run. During initialization the code is executed in the following order:

  1. Expressions used as initializers in state variable declarations (least to most derived).
  2. Base constructor arguments (most to the least derived).
  3. Constructor bodies (least to the most derived).

If the initialization of each variable is independent, this makes no practical difference. You might simply get bytecode that is functionally equivalent but has some instructions ordered slightly differently.

If variables depend on each other, you might actually get different results. In all of these places you can refer to other state variables and call arbitrary functions (even external ones). The function you call can have side-effects or modify state variables (even before they are initialized).

To avoid problems that can arise from this it's best to keep things simple. Avoid dependencies and for declarations and base constructor arguments try to only use simple constants or functions that do not read or write other state variables. If you need to do something more complex, keep that logic inside the constructor body to make it easier to reason about what runs in what order.

Practical examples

Bytecode differences in your example

Your example is a very simple scenario where there's obviously no difference in contract behavior but let's take look at what happens under the hood.

a.sol

contract A {
    uint public s = 10000;
}

b.sol

contract B {
    uint public s;

    constructor() {
        s = 10000;
    }
}

Here's the difference between the assembly produced in both cases:

solc a.sol --optimize --asm --debug-info none > a.asm
solc b.sol --optimize --asm --debug-info none > b.asm
diff --unified a.asm b.asm
@@ -1,10 +1,7 @@

-======= a.sol:A =======
+======= b.sol:B =======
 EVM assembly:
   mstore(0x40, 0x80)
-  0x2710
-  0x00
-  sstore
   callvalue
   dup1
   iszero
@@ -15,6 +12,9 @@
   revert
 tag_1:
   pop
+  0x2710
+  0x00
+  sstore
   dataSize(sub_0)
   dup1
   dataOffset(sub_0)
@@ -66,6 +66,6 @@
       swap1
       return

-    auxdata: 0xa26469706673582212201cfb33d8ca060adae5b385c6039c60778e487b6364ff4076e825b1d418fd206964736f6c634300080a0033
+    auxdata: 0xa264697066735822122027a7491b2543ebe695a4bc9b47f2110e1e14a5212f2c0ae7b5a34cda7f996e3d64736f6c634300080a0033
 }

As you can see, the only real difference is that in the case with no constructor the initialization happens a bit earlier but the end result is still the same.

And if you try the new (still experimental) code generator instead (solc --optimize --ir-optimized) you'll actually end up with the same exact assembly code.

Initialization order

Let's look at an example that illustrates the initialization order:

contract A {
    string[] order;

    constructor(uint _x) {
        log("constructor of A");
    }

    function log(string memory _word) internal returns (uint) {
        order.push(_word);
        return 42;
    }
}

contract B is A {
    uint b = log("declaration of b");

    constructor(uint _x) A(log("args of A")) {
        log("constructor of B");
    }
}

contract C is B {
    uint c = log("declaration of c");

    constructor() B(log("args of B")) {
        log("constructor of C");
    }

    function get() public view returns (string[] memory) {
        return order;
    }
}

If you deploy the code above and run C.get(), you'll see the following values:

  • "declaration of b"
  • "declaration of c"
  • "args of B"
  • "args of A"
  • "constructor of A"
  • "constructor of B"
  • "constructor of C"

Dependencies between variables

The consequence of these rules is that it may be possible to access a variable before it is initialized:

contract C {
    uint a = 1;
    uint b;
    uint c = b + 1;

    constructor() {
        b = 2;
    }

    function get() public view returns (uint, uint, uint) {
        return (a, b, c);
    }
}

In this example running C.get() will give you (1, 2, 1) rather than the (1, 2, 3) you might expect. This is because C.c gets initialized before C.b. Unused storage is always zero-initialized so C.c is assigned 0 + 1.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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