Skip to content
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

Add constructor support to Solidity contracts #1672

Open
salaheldinsoliman opened this issue Nov 2, 2024 · 5 comments
Open

Add constructor support to Solidity contracts #1672

salaheldinsoliman opened this issue Nov 2, 2024 · 5 comments

Comments

@salaheldinsoliman
Copy link
Contributor

salaheldinsoliman commented Nov 2, 2024

Soroban protocol 21 did not support constructors (functions exposed by the contract, which the Vm calls upon contract deployment).
That means that a developer would have to call init manually from the command line.

init is a function that Solang embeds in Solidity contracts, which loops over storage variables and initializes them. (By calling another function storage_initializer)

fn emit_initializer(binary: &mut Binary, _ns: &ast::Namespace) {

What needs to be done:
Upgrade protocol version in contract to be 22, and change the name init to __constructor, and add tests

@tareknaser
Copy link
Contributor

change the name init to __constructor

Just to make sure I understand this correctly,
If we just rename init to __constructor, it seems we still wouldn’t fully support constructors as outlined in CAP-0058. Developers would still need to manually call it, as they currently do with init.

There are a few things I’m unsure about:

  • Since __constructor is optional, what happens if the developer doesn’t implement it?
    • Do we just expose an empty __constructor function that only calls storage_initializer, similar to how init works now, and expect the developer to call it manually?
  • I noticed that the new __constructor method can also include custom logic that the developer wants to run on initialization, which might mean it can take parameters too. Will Solang support that?
  • Should __constructor still be callable after the contract is deployed and initialized?

@salaheldinsoliman
Copy link
Contributor Author

@tareknaser __constructor should call storage_initializer before executing custom initialization logic. (not only call storage_initializer)

@tareknaser
Copy link
Contributor

Alright, here’s what I understand about the desired behavior:

  • If the developer doesn’t implement __constructor, we should export a function called __constructor that only initializes storage variables by calling storage_initializer. This part seems straightforward; we could just rename what we have from init to __constructor.
  • If the developer defines their own __constructor function with custom code, then we need to ensure that __constructor first calls storage_initializer and then executes the custom code. This is the part that’s proving tricky.

I’ve tried a few approaches to handle the second point, but they haven’t worked out so far:

  • Modifying the developer’s __constructor function to first call storage_initializer. I couldn’t quite achieve this, but there might be a way with inkwell that I’m not yet aware of.
  • Overriding the developer’s __constructor implementation so it would call storage_initializer first, followed by the custom code and then re-export it. This also didn’t work out.

I’m continuing to debug, but I wanted to share these notes in case there’s a simpler approach that I might be missing. Do you have any suggestions?

@salaheldinsoliman
Copy link
Contributor Author

salaheldinsoliman commented Nov 13, 2024

@tareknaser You are doing a great job!
If you compile a simple contract to Soroban target, that has a simple constructor that prints a value:

contract counter {
    uint64  sesa = 0;
    constructor() public {
        print("Constructor called");
    }
    function decrement(uint64 inp) public returns (uint64){
        inp = sesa; 
        return inp;
    }
}

, and use wasm2wat to view the compiled contract you get:

(module
  (type $t0 (func (param i64 i64 i64 i64) (result i64)))
  (type $t1 (func (param i64 i64) (result i64)))
  (type $t2 (func (param i64 i64 i64) (result i64)))
  (type $t3 (func (result i64)))
  (type $t4 (func (param i64) (result i64)))
  (import "x" "_" (func $x._ (type $t0))) # Print in Soroban
  (import "l" "1" (func $l.1 (type $t1))) 
  (import "l" "_" (func $l._ (type $t2)))  # Storage set (initialize) in Soroban
  (func $__unnamed_1 (export "__unnamed_1") (type $t3) (result i64) # THIS IS THE CONSTRUCTOR
    (local $l0 i64)
    (drop
      (call $x._ # this is a call to print in soroban
        (local.tee $l0
          (i64.or
            (i64.shl
              (i64.extend_i32_u
                (i32.const 1024))
              (i64.const 32))
            (i64.const 4)))
        (i64.const 77309411332)
        (local.get $l0)
        (i64.const 4)))
    (i64.const 2))
  (func $decrement (export "decrement") (type $t4) (param $p0 i64) (result i64)
    (local $l1 i64)
    (local.set $l1
      (call $l.1
        (i64.const 0)
        (i64.const 2)))
    (block $B0
      (br_if $B0
        (i32.const 0))
      (return
        (i64.or
          (i64.shl
            (local.get $l1)
            (i64.const 8))
          (i64.const 6))))
    (drop
      (call $x._
        (local.tee $l1
          (i64.or
            (i64.shl
              (i64.extend_i32_u
                (i32.const 1042))
              (i64.const 32))
            (i64.const 4)))
        (i64.const 64424509444)
        (local.get $l1)
        (i64.const 4)))
    (unreachable))
  (func $init (export "init") (type $t3) (result i64)
    (drop
      (call $l._ # this is a call to `storage_set` in soroban
        (i64.const 0)
        (i64.const 0)
        (i64.const 2)))
    (i64.const 2))
  (memory $memory (export "memory") 2)
  (global $__stack_pointer (mut i32) (i32.const 1048576))
  (data $.rodata (i32.const 1024) "Constructor calledmath overflow,\0a"))

U see that the constructor is named unnamed and it contains some logic for doing the print in Soroban. U see the function init doing some logic to initialize data (it is supposed to call storage_initializer, but llvm optimized this and inlined the function call in place not allocate another stack frame)

What needs to be done is two things:
1- function unnamed to be called __constructor, so the the soroban env can recognize it is indeed the constructor and call it upon deployment.
2- insert a call to storage_initalizer before executing the logic.

Does that answer your question?

@tareknaser
Copy link
Contributor

Thanks for mentioning wasm2wat—it really helps with debugging.

U see that the constructor is named unnamed and it contains some logic for doing the print in Soroban. U see the function init doing some logic to initialize data

So, it's safe to say that both __unnamed_1 and storage_initializertogether form the constructor, which we should re-export as __constructor?

Also, is the function __unnamed_1 a standard function that’s exported with the same name each time? Meaning, can I do..

let constructor = binary.module.get_function("__unnamed_1").unwrap();

?

function unnamed to be called __constructor, so the the soroban env can recognize it is indeed the constructor and call it upon deployment.

I'm not sure I fully understand this part. If a user defines their own version of __constructor and we prepend __unnamed_1 and storage_initializer to their instructions, then the Soroban VM wouldn’t be able to call the constructor upon deployment. This is because the developer’s constructor might have arguments that they would need to manually pass.

Something related that I found while looking into how this is implemented in rs-soroban-sdk is that we’re currently using register_contract_wasm here. I think we should make use of register_contract_wasm_with_constructor, but I’m not sure how that would work in Solang.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants