Explorar o código

Soroban counter.sol example (#1645)

This PR aims to make Solang support a simple counter.sol example on
Soroban, where a storage variable is instatiated, modified and
retrieved. The counter contract is only limited to `uint64` data types,
and only supports `instance` soroban storage.

This can be considered a "skeleton" for supporting more data and storage
types, as well as more host function invokations.

- [x] Support Soroban storage function calls `put_contract_data`,
`get_contract_data` and `has_contract_data`
- [x] Implement a wrapper `init` for `storage_initializer` 
- [x] Implement wrappers for public functions
- [x] Insert decoding/encoding instructions into the wrapper functions 
- [x] Soroban doesn't have function return codes. This needs to be
handled all over emit
- [x] Add integration tests and MockVm tests

---------

Signed-off-by: salaheldinsoliman <salaheldin_sameh@aucegypt.edu>
salaheldinsoliman hai 1 ano
pai
achega
399c199923

+ 42 - 0
.github/workflows/test.yml

@@ -306,6 +306,48 @@ jobs:
       with:
        name: anchor-tests
        path: ./target/*.profraw
+  
+  soroban:
+    name: Soroban Integration test
+    runs-on: solang-ubuntu-latest
+    container: ghcr.io/hyperledger/solang-llvm:ci-7
+    needs: linux-x86-64
+    steps:
+    - name: Checkout sources
+      uses: actions/checkout@v3
+    - uses: actions/setup-node@v3
+      with:
+        node-version: '16'
+    - uses: dtolnay/rust-toolchain@1.74.0
+    - uses: actions/download-artifact@v3
+      with:
+        name: solang-linux-x86-64
+        path: bin
+    - name: Solang Compiler
+      run: |
+        chmod 755 ./bin/solang
+        echo "$(pwd)/bin" >> $GITHUB_PATH
+    
+    - name: Install Soroban
+      run: cargo install --locked soroban-cli --version 21.0.0-rc.1
+    - name: Add cargo install location to PATH
+      run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
+    - run: npm install
+      working-directory: ./integration/soroban
+    - name: Build Solang contracts
+      run: npm run build
+      working-directory: ./integration/soroban
+    - name: Setup Soroban enivronment
+      run: npm run setup
+      working-directory: ./integration/soroban
+    - name: Deploy and test contracts
+      run: npm run test
+      working-directory: ./integration/soroban
+    - name: Upload test coverage files
+      uses: actions/upload-artifact@v3.1.0
+      with:
+        name: soroban-tests
+        path: ./target/*.profraw
 
   solana:
     name: Solana Integration test

+ 8 - 0
integration/soroban/.gitignore

@@ -0,0 +1,8 @@
+*.js
+*.so
+*.key
+*.json
+!tsconfig.json
+!package.json
+node_modules
+package-lock.json

+ 13 - 0
integration/soroban/counter.sol

@@ -0,0 +1,13 @@
+contract counter {
+    uint64 public count = 10;
+
+    function increment() public returns (uint64) {
+        count += 1;
+        return count;
+    }
+
+    function decrement() public returns (uint64) {
+        count -= 1;
+        return count;
+    }
+}

+ 55 - 0
integration/soroban/counter.spec.js

@@ -0,0 +1,55 @@
+import * as StellarSdk from '@stellar/stellar-sdk';
+import { readFileSync } from 'fs';
+import { expect } from 'chai';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { call_contract_function } from './test_helpers.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const dirname = path.dirname(__filename);
+
+describe('Counter', () => {
+  let keypair;
+  const server = new StellarSdk.SorobanRpc.Server(
+    "https://soroban-testnet.stellar.org:443",
+  );
+
+  let contractAddr;
+  let contract;
+  before(async () => {
+
+    console.log('Setting up counter contract tests...');
+
+    // read secret from file
+    const secret = readFileSync('alice.txt', 'utf8').trim();
+    keypair = StellarSdk.Keypair.fromSecret(secret);
+
+    let contractIdFile = path.join(dirname, '.soroban', 'contract-ids', 'counter.txt');
+    // read contract address from file
+    contractAddr = readFileSync(contractIdFile, 'utf8').trim().toString();
+
+    // load contract
+    contract = new StellarSdk.Contract(contractAddr);
+
+    // initialize the contract
+    await call_contract_function("init", server, keypair, contract);
+
+  });
+
+  it('get correct initial counter', async () => {
+    // get the count
+    let count = await call_contract_function("count", server, keypair, contract);
+    expect(count.toString()).eq("10");
+  });
+
+  it('increment counter', async () => {
+    // increment the counter
+    await call_contract_function("increment", server, keypair, contract);
+
+    // get the count
+    let count = await call_contract_function("count", server, keypair, contract);
+    expect(count.toString()).eq("11");
+  });
+});
+
+

+ 23 - 0
integration/soroban/package.json

@@ -0,0 +1,23 @@
+{
+    "type": "module",
+    "dependencies": {
+      "@stellar/stellar-sdk": "^12.0.1",
+      "chai": "^5.1.1",
+      "dotenv": "^16.4.5",
+      "mocha": "^10.4.0"
+    },
+    "scripts": {
+      "build": "solang compile *.sol --target soroban",
+      "setup": "node setup.js",
+      "test": "mocha *.spec.js --timeout 20000"
+    },
+    "devDependencies": {
+      "@eslint/js": "^9.4.0",
+      "@types/mocha": "^10.0.6",
+      "eslint": "^9.4.0",
+      "expect": "^29.7.0",
+      "globals": "^15.4.0",
+      "typescript": "^5.4.5"
+    }
+  }
+  

+ 63 - 0
integration/soroban/setup.js

@@ -0,0 +1,63 @@
+
+import 'dotenv/config';
+import { mkdirSync, readdirSync} from 'fs';
+import { execSync } from 'child_process';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+console.log("###################### Initializing ########################");
+
+// Get dirname (equivalent to the Bash version)
+const __filename = fileURLToPath(import.meta.url);
+const dirname = path.dirname(__filename);
+
+// variable for later setting pinned version of soroban in "$(dirname/target/bin/soroban)"
+const soroban = "soroban"
+
+// Function to execute and log shell commands
+function exe(command) {
+  console.log(command);
+  execSync(command, { stdio: 'inherit' });
+}
+
+function generate_alice() {
+  exe(`${soroban} keys generate alice --network testnet`);
+
+  // get the secret key of alice and put it in alice.txt
+  exe(`${soroban} keys show alice > alice.txt`);
+}
+
+
+function filenameNoExtension(filename) {
+  return path.basename(filename, path.extname(filename));
+}
+
+function deploy(wasm) {
+
+  let contractId = path.join(dirname, '.soroban', 'contract-ids', filenameNoExtension(wasm) + '.txt');
+
+  exe(`(${soroban} contract deploy --wasm ${wasm} --ignore-checks --source-account alice --network testnet) > ${contractId}`);
+}
+
+function deploy_all() {
+  const contractsDir = path.join(dirname, '.soroban', 'contract-ids');
+  mkdirSync(contractsDir, { recursive: true });
+
+  const wasmFiles = readdirSync(`${dirname}`).filter(file => file.endsWith('.wasm'));
+
+  wasmFiles.forEach(wasmFile => {
+    deploy(path.join(dirname, wasmFile));
+  });
+}
+
+function add_testnet() {
+
+  exe(`${soroban} network add \
+    --global testnet \
+    --rpc-url https://soroban-testnet.stellar.org:443 \
+    --network-passphrase "Test SDF Network ; September 2015"`);
+}
+
+add_testnet();
+generate_alice();
+deploy_all();

+ 53 - 0
integration/soroban/test_helpers.js

@@ -0,0 +1,53 @@
+import * as StellarSdk from '@stellar/stellar-sdk';
+
+
+
+export async function call_contract_function(method, server, keypair, contract) {
+
+    let res;
+    let builtTransaction = new StellarSdk.TransactionBuilder(await server.getAccount(keypair.publicKey()), {
+      fee: StellarSdk.BASE_FEE,
+      networkPassphrase: StellarSdk.Networks.TESTNET,
+    }).addOperation(contract.call(method)).setTimeout(30).build();
+  
+    let preparedTransaction = await server.prepareTransaction(builtTransaction);
+  
+    // Sign the transaction with the source account's keypair.
+    preparedTransaction.sign(keypair);
+  
+    try {
+      let sendResponse = await server.sendTransaction(preparedTransaction);
+      if (sendResponse.status === "PENDING") {
+        let getResponse = await server.getTransaction(sendResponse.hash);
+        // Poll `getTransaction` until the status is not "NOT_FOUND"
+        while (getResponse.status === "NOT_FOUND") {
+          console.log("Waiting for transaction confirmation...");
+          // See if the transaction is complete
+          getResponse = await server.getTransaction(sendResponse.hash);
+          // Wait one second
+          await new Promise((resolve) => setTimeout(resolve, 1000));
+        }
+  
+        if (getResponse.status === "SUCCESS") {
+          // Make sure the transaction's resultMetaXDR is not empty
+          if (!getResponse.resultMetaXdr) {
+            throw "Empty resultMetaXDR in getTransaction response";
+          }
+          // Find the return value from the contract and return it
+          let transactionMeta = getResponse.resultMetaXdr;
+          let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
+          console.log(`Transaction result: ${returnValue.value()}`);
+          res = returnValue.value();
+        } else {
+          throw `Transaction failed: ${getResponse.resultXdr}`;
+        }
+      } else {
+        throw sendResponse.errorResultXdr;
+      }
+    } catch (err) {
+      // Catch and report any errors we've thrown
+      console.log("Sending transaction failed");
+      console.log(err);
+    }
+    return res;
+}

+ 15 - 1
src/bin/cli/mod.rs

@@ -321,6 +321,15 @@ pub struct CompilePackage {
     #[arg(name = "VERSION", help = "specify contracts version", long = "version", num_args = 1, value_parser = ValueParser::new(parse_version))]
     #[serde(default, deserialize_with = "deserialize_version")]
     pub version: Option<String>,
+
+    #[arg(
+        name = "SOROBAN-VERSION",
+        help = "specify soroban contracts pre-release number",
+        short = 's',
+        long = "soroban-version",
+        num_args = 1
+    )]
+    pub soroban_version: Option<u64>,
 }
 
 #[derive(Args, Deserialize, Debug, PartialEq)]
@@ -545,7 +554,11 @@ pub fn imports_arg<T: PackageTrait>(package: &T) -> FileResolver {
     resolver
 }
 
-pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Options {
+pub fn options_arg(
+    debug: &DebugFeatures,
+    optimizations: &Optimizations,
+    compiler_inputs: &CompilePackage,
+) -> Options {
     let opt_level = if let Some(level) = &optimizations.opt_level {
         match level.as_str() {
             "none" => OptimizationLevel::None,
@@ -574,6 +587,7 @@ pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Opti
         } else {
             None
         }),
+        soroban_version: compiler_inputs.soroban_version,
     }
 }
 

+ 15 - 3
src/bin/cli/test.rs

@@ -101,7 +101,17 @@ mod tests {
 
         let default_optimize: cli::Optimizations = toml::from_str("").unwrap();
 
-        let opt = options_arg(&default_debug, &default_optimize);
+        let compiler_package = cli::CompilePackage {
+            input: Some(vec![PathBuf::from("flipper.sol")]),
+            contracts: Some(vec!["flipper".to_owned()]),
+            import_path: Some(vec![]),
+            import_map: Some(vec![]),
+            authors: None,
+            version: Some("0.1.0".to_string()),
+            soroban_version: None,
+        };
+
+        let opt = options_arg(&default_debug, &default_optimize, &compiler_package);
 
         assert_eq!(opt, Options::default());
 
@@ -185,7 +195,8 @@ mod tests {
                     import_path: Some(vec![]),
                     import_map: Some(vec![]),
                     authors: None,
-                    version: Some("0.1.0".to_string())
+                    version: Some("0.1.0".to_string()),
+                    soroban_version: None
                 },
                 compiler_output: cli::CompilerOutput {
                     emit: None,
@@ -239,7 +250,8 @@ mod tests {
                     import_path: Some(vec![]),
                     import_map: Some(vec![]),
                     authors: Some(vec!["not_sesa".to_owned()]),
-                    version: Some("0.1.0".to_string())
+                    version: Some("0.1.0".to_string()),
+                    soroban_version: None
                 },
                 compiler_output: cli::CompilerOutput {
                     emit: None,

+ 7 - 1
src/bin/solang.rs

@@ -171,7 +171,13 @@ fn compile(compile_args: &Compile) {
 
     let mut resolver = imports_arg(&compile_args.package);
 
-    let opt = options_arg(&compile_args.debug_features, &compile_args.optimizations);
+    let compile_package = &compile_args.package;
+
+    let opt = options_arg(
+        &compile_args.debug_features,
+        &compile_args.optimizations,
+        compile_package,
+    );
 
     let mut namespaces = Vec::new();
 

+ 3 - 2
src/codegen/dispatch/mod.rs

@@ -5,10 +5,11 @@ use crate::{sema::ast::Namespace, Target};
 
 pub(crate) mod polkadot;
 pub(super) mod solana;
+pub(super) mod soroban;
 
 pub(super) fn function_dispatch(
     contract_no: usize,
-    all_cfg: &[ControlFlowGraph],
+    all_cfg: &mut [ControlFlowGraph],
     ns: &mut Namespace,
     opt: &Options,
 ) -> Vec<ControlFlowGraph> {
@@ -17,6 +18,6 @@ pub(super) fn function_dispatch(
         Target::Polkadot { .. } | Target::EVM => {
             polkadot::function_dispatch(contract_no, all_cfg, ns, opt)
         }
-        Target::Soroban => vec![],
+        Target::Soroban => soroban::function_dispatch(contract_no, all_cfg, ns, opt),
     }
 }

+ 141 - 0
src/codegen/dispatch/soroban.rs

@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use num_bigint::BigInt;
+use solang_parser::pt::{self};
+
+use crate::sema::ast;
+use crate::{
+    codegen::{
+        cfg::{ASTFunction, ControlFlowGraph, Instr, InternalCallTy},
+        vartable::Vartable,
+        Expression, Options,
+    },
+    sema::ast::{Namespace, Type},
+};
+
+pub fn function_dispatch(
+    contract_no: usize,
+    all_cfg: &mut [ControlFlowGraph],
+    ns: &mut Namespace,
+    _opt: &Options,
+) -> Vec<ControlFlowGraph> {
+    // For each function in all_cfg, we will generate a wrapper function that will call the function
+    // The wrapper function will call abi_encode to encode the arguments, and then call the function
+
+    let mut wrapper_cfgs = Vec::new();
+
+    for cfg in all_cfg.iter_mut() {
+        let function = match &cfg.function_no {
+            ASTFunction::SolidityFunction(no) => &ns.functions[*no],
+            _ => continue,
+        };
+
+        let wrapper_name = {
+            if cfg.public {
+                if function.mangled_name_contracts.contains(&contract_no) {
+                    function.mangled_name.clone()
+                } else {
+                    function.id.name.clone()
+                }
+            } else {
+                continue;
+            }
+        };
+
+        let mut wrapper_cfg = ControlFlowGraph::new(wrapper_name.to_string(), ASTFunction::None);
+
+        wrapper_cfg.params = function.params.clone();
+
+        let param = ast::Parameter::new_default(Type::Uint(64));
+        wrapper_cfg.returns = vec![param].into();
+        wrapper_cfg.public = true;
+
+        let mut vartab = Vartable::from_symbol_table(&function.symtable, ns.next_id);
+
+        let mut value = Vec::new();
+        let mut return_tys = Vec::new();
+
+        let mut call_returns = Vec::new();
+        for arg in function.returns.iter() {
+            let new = vartab.temp_anonymous(&arg.ty);
+            value.push(Expression::Variable {
+                loc: arg.loc,
+                ty: arg.ty.clone(),
+                var_no: new,
+            });
+            return_tys.push(arg.ty.clone());
+            call_returns.push(new);
+        }
+
+        let cfg_no = match cfg.function_no {
+            ASTFunction::SolidityFunction(no) => no,
+            _ => 0,
+        };
+        let placeholder = Instr::Call {
+            res: call_returns,
+            call: InternalCallTy::Static { cfg_no },
+            return_tys,
+            args: function
+                .params
+                .iter()
+                .enumerate()
+                .map(|(i, p)| Expression::ShiftRight {
+                    loc: pt::Loc::Codegen,
+                    ty: Type::Uint(64),
+                    left: Expression::FunctionArg {
+                        loc: p.loc,
+                        ty: p.ty.clone(),
+                        arg_no: i,
+                    }
+                    .into(),
+                    right: Expression::NumberLiteral {
+                        loc: pt::Loc::Codegen,
+                        ty: Type::Uint(64),
+                        value: BigInt::from(8_u64),
+                    }
+                    .into(),
+
+                    signed: false,
+                })
+                .collect(),
+        };
+
+        wrapper_cfg.add(&mut vartab, placeholder);
+
+        // set the msb 8 bits of the return value to 6, the return value is 64 bits.
+        // FIXME: this assumes that the solidity function always returns one value.
+        let shifted = Expression::ShiftLeft {
+            loc: pt::Loc::Codegen,
+            ty: Type::Uint(64),
+            left: value[0].clone().into(),
+            right: Expression::NumberLiteral {
+                loc: pt::Loc::Codegen,
+                ty: Type::Uint(64),
+                value: BigInt::from(8_u64),
+            }
+            .into(),
+        };
+
+        let tag = Expression::NumberLiteral {
+            loc: pt::Loc::Codegen,
+            ty: Type::Uint(64),
+            value: BigInt::from(6_u64),
+        };
+
+        let added = Expression::Add {
+            loc: pt::Loc::Codegen,
+            ty: Type::Uint(64),
+            overflowing: false,
+            left: shifted.into(),
+            right: tag.into(),
+        };
+
+        wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![added] });
+
+        vartab.finalize(ns, &mut wrapper_cfg);
+        cfg.public = false;
+        wrapper_cfgs.push(wrapper_cfg);
+    }
+
+    wrapper_cfgs
+}

+ 3 - 1
src/codegen/mod.rs

@@ -107,6 +107,7 @@ pub struct Options {
     pub log_prints: bool,
     #[cfg(feature = "wasm_opt")]
     pub wasm_opt: Option<OptimizationPasses>,
+    pub soroban_version: Option<u64>,
 }
 
 impl Default for Options {
@@ -123,6 +124,7 @@ impl Default for Options {
             log_prints: true,
             #[cfg(feature = "wasm_opt")]
             wasm_opt: None,
+            soroban_version: None,
         }
     }
 }
@@ -247,7 +249,7 @@ fn contract(contract_no: usize, ns: &mut Namespace, opt: &Options) {
             ns.contracts[contract_no].default_constructor = Some((func, cfg_no));
         }
 
-        for mut dispatch_cfg in function_dispatch(contract_no, &all_cfg, ns, opt) {
+        for mut dispatch_cfg in function_dispatch(contract_no, &mut all_cfg, ns, opt) {
             optimize_and_check_cfg(&mut dispatch_cfg, ns, ASTFunction::None, opt);
             all_cfg.push(dispatch_cfg);
         }

+ 1 - 1
src/emit/binary.rs

@@ -1227,7 +1227,7 @@ impl<'a> Binary<'a> {
         ns: &Namespace,
         code: PanicCode,
     ) -> (PointerValue<'a>, IntValue<'a>) {
-        if ns.target == Target::Solana {
+        if ns.target == Target::Solana || ns.target == Target::Soroban {
             return (
                 self.context
                     .i8_type()

+ 46 - 41
src/emit/instructions.rs

@@ -461,7 +461,8 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>(
                 .map(|p| expression(target, bin, p, &w.vars, function, ns).into())
                 .collect::<Vec<BasicMetadataValueEnum>>();
 
-            if !res.is_empty() {
+            // Soroban doesn't write return values to imported memory
+            if !res.is_empty() && ns.target != Target::Soroban {
                 for v in f.returns.iter() {
                     parms.push(if ns.target == Target::Solana {
                         bin.build_alloca(function, bin.llvm_var_ty(&v.ty, ns), v.name_as_str())
@@ -484,54 +485,58 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>(
                 .build_call(bin.functions[cfg_no], &parms, "")
                 .unwrap()
                 .try_as_basic_value()
-                .left()
-                .unwrap();
-
-            let success = bin
-                .builder
-                .build_int_compare(
-                    IntPredicate::EQ,
-                    ret.into_int_value(),
-                    bin.return_values[&ReturnCode::Success],
-                    "success",
-                )
-                .unwrap();
+                .left();
 
-            let success_block = bin.context.append_basic_block(function, "success");
-            let bail_block = bin.context.append_basic_block(function, "bail");
-            bin.builder
-                .build_conditional_branch(success, success_block, bail_block)
-                .unwrap();
+            // Soroban doesn't have return codes, and only returns a single i64 value
+            if ns.target != Target::Soroban {
+                let success = bin
+                    .builder
+                    .build_int_compare(
+                        IntPredicate::EQ,
+                        ret.unwrap().into_int_value(),
+                        bin.return_values[&ReturnCode::Success],
+                        "success",
+                    )
+                    .unwrap();
 
-            bin.builder.position_at_end(bail_block);
+                let success_block = bin.context.append_basic_block(function, "success");
+                let bail_block = bin.context.append_basic_block(function, "bail");
+                bin.builder
+                    .build_conditional_branch(success, success_block, bail_block)
+                    .unwrap();
 
-            bin.builder.build_return(Some(&ret)).unwrap();
-            bin.builder.position_at_end(success_block);
+                bin.builder.position_at_end(bail_block);
 
-            if !res.is_empty() {
-                for (i, v) in f.returns.iter().enumerate() {
-                    let load_ty = bin.llvm_var_ty(&v.ty, ns);
-                    let val = bin
-                        .builder
-                        .build_load(
-                            load_ty,
-                            parms[args.len() + i].into_pointer_value(),
-                            v.name_as_str(),
-                        )
-                        .unwrap();
-                    let dest = w.vars[&res[i]].value;
+                bin.builder.build_return(Some(&ret.unwrap())).unwrap();
+                bin.builder.position_at_end(success_block);
 
-                    if dest.is_pointer_value()
-                        && !(v.ty.is_reference_type(ns)
-                            || matches!(v.ty, Type::ExternalFunction { .. }))
-                    {
-                        bin.builder
-                            .build_store(dest.into_pointer_value(), val)
+                if !res.is_empty() {
+                    for (i, v) in f.returns.iter().enumerate() {
+                        let load_ty = bin.llvm_var_ty(&v.ty, ns);
+                        let val = bin
+                            .builder
+                            .build_load(
+                                load_ty,
+                                parms[args.len() + i].into_pointer_value(),
+                                v.name_as_str(),
+                            )
                             .unwrap();
-                    } else {
-                        w.vars.get_mut(&res[i]).unwrap().value = val;
+                        let dest = w.vars[&res[i]].value;
+
+                        if dest.is_pointer_value()
+                            && !(v.ty.is_reference_type(ns)
+                                || matches!(v.ty, Type::ExternalFunction { .. }))
+                        {
+                            bin.builder
+                                .build_store(dest.into_pointer_value(), val)
+                                .unwrap();
+                        } else {
+                            w.vars.get_mut(&res[i]).unwrap().value = val;
+                        }
                     }
                 }
+            } else if let Some(value) = ret {
+                w.vars.get_mut(&res[0]).unwrap().value = value;
             }
         }
         Instr::Call {

+ 120 - 38
src/emit/soroban/mod.rs

@@ -1,13 +1,13 @@
 // SPDX-License-Identifier: Apache-2.0
 
 pub(super) mod target;
-use crate::codegen::cfg::ControlFlowGraph;
-use crate::emit::cfg::emit_cfg;
-use crate::{
-    codegen::{cfg::ASTFunction, Options},
-    emit::Binary,
-    sema::ast,
+use crate::codegen::{
+    cfg::{ASTFunction, ControlFlowGraph},
+    Options, STORAGE_INITIALIZER,
 };
+
+use crate::emit::cfg::emit_cfg;
+use crate::{emit::Binary, sema::ast};
 use inkwell::{
     context::Context,
     module::{Linkage, Module},
@@ -16,8 +16,12 @@ use soroban_sdk::xdr::{
     DepthLimitedWrite, ScEnvMetaEntry, ScSpecEntry, ScSpecFunctionInputV0, ScSpecFunctionV0,
     ScSpecTypeDef, StringM, WriteXdr,
 };
+use std::ffi::CString;
+use std::sync;
 
-const SOROBAN_ENV_INTERFACE_VERSION: u64 = 85899345977;
+const SOROBAN_ENV_INTERFACE_VERSION: u64 = 90194313216;
+pub const PUT_CONTRACT_DATA: &str = "l._";
+pub const GET_CONTRACT_DATA: &str = "l.1";
 
 pub struct SorobanTarget;
 
@@ -41,8 +45,21 @@ impl SorobanTarget {
             None,
         );
 
-        Self::emit_functions_with_spec(contract, &mut binary, ns, context, contract_no);
-        Self::emit_env_meta_entries(context, &mut binary);
+        let mut export_list = Vec::new();
+        Self::declare_externals(&mut binary);
+        Self::emit_functions_with_spec(
+            contract,
+            &mut binary,
+            ns,
+            context,
+            contract_no,
+            &mut export_list,
+        );
+        binary.internalize(export_list.as_slice());
+
+        Self::emit_initializer(&mut binary, ns);
+
+        Self::emit_env_meta_entries(context, &mut binary, opt);
 
         binary
     }
@@ -54,7 +71,8 @@ impl SorobanTarget {
         binary: &mut Binary<'a>,
         ns: &'a ast::Namespace,
         context: &'a Context,
-        contract_no: usize,
+        _contract_no: usize,
+        export_list: &mut Vec<&'a str>,
     ) {
         let mut defines = Vec::new();
 
@@ -68,41 +86,30 @@ impl SorobanTarget {
             // For each function, determine the name and the linkage
             // Soroban has no dispatcher, so all externally addressable functions are exported and should be named the same as the original function name in the source code.
             // If there are duplicate function names, then the function name in the source is mangled to include the signature.
-            let default_constructor = ns.default_constructor(contract_no);
-            let name = {
-                if cfg.public {
-                    let f = match &cfg.function_no {
-                        ASTFunction::SolidityFunction(no) | ASTFunction::YulFunction(no) => {
-                            &ns.functions[*no]
-                        }
-                        _ => &default_constructor,
-                    };
-
-                    if f.mangled_name_contracts.contains(&contract_no) {
-                        &f.mangled_name
-                    } else {
-                        &f.id.name
-                    }
-                } else {
-                    &cfg.name
-                }
-            };
 
-            Self::emit_function_spec_entry(context, cfg, name.clone(), binary);
+            // if func is a default constructor, then the function name is the contract name
 
             let linkage = if cfg.public {
+                let name = if cfg.name.contains("::") {
+                    // get the third part of the name which is the function name
+                    cfg.name.split("::").collect::<Vec<&str>>()[2]
+                } else {
+                    &cfg.name
+                };
+                Self::emit_function_spec_entry(context, cfg, name.to_string(), binary);
+                export_list.push(name);
                 Linkage::External
             } else {
                 Linkage::Internal
             };
 
-            let func_decl = if let Some(func) = binary.module.get_function(name) {
+            let func_decl = if let Some(func) = binary.module.get_function(&cfg.name) {
                 // must not have a body yet
                 assert_eq!(func.get_first_basic_block(), None);
 
                 func
             } else {
-                binary.module.add_function(name, ftype, Some(linkage))
+                binary.module.add_function(&cfg.name, ftype, Some(linkage))
             };
 
             binary.functions.insert(cfg_no, func_decl);
@@ -110,14 +117,21 @@ impl SorobanTarget {
             defines.push((func_decl, cfg));
         }
 
+        let init_type = context.i64_type().fn_type(&[], false);
+        binary
+            .module
+            .add_function("storage_initializer", init_type, None);
+
         for (func_decl, cfg) in defines {
             emit_cfg(&mut SorobanTarget, binary, contract, cfg, func_decl, ns);
         }
     }
 
-    fn emit_env_meta_entries<'a>(context: &'a Context, binary: &mut Binary<'a>) {
+    fn emit_env_meta_entries<'a>(context: &'a Context, binary: &mut Binary<'a>, opt: &'a Options) {
         let mut meta = DepthLimitedWrite::new(Vec::new(), 10);
-        ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(SOROBAN_ENV_INTERFACE_VERSION)
+        let soroban_env_interface_version =
+            opt.soroban_version.unwrap_or(SOROBAN_ENV_INTERFACE_VERSION);
+        ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(soroban_env_interface_version)
             .write_xdr(&mut meta)
             .expect("writing env meta interface version to xdr");
         Self::add_custom_section(context, &binary.module, "contractenvmetav0", meta.inner);
@@ -125,12 +139,12 @@ impl SorobanTarget {
 
     fn emit_function_spec_entry<'a>(
         context: &'a Context,
-        cfg: &'a ControlFlowGraph,
+        cfg: &ControlFlowGraph,
         name: String,
         binary: &mut Binary<'a>,
     ) {
         if cfg.public && !cfg.is_placeholder() {
-            // TODO: Emit custom type spec entries.
+            // TODO: Emit custom type spec entries
             let mut spec = DepthLimitedWrite::new(Vec::new(), 10);
             ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
                 name: name
@@ -148,7 +162,7 @@ impl SorobanTarget {
                             .unwrap_or_else(|| i.to_string())
                             .try_into()
                             .expect("function input name exceeds limit"),
-                        type_: ScSpecTypeDef::U32, // TODO: Map type.
+                        type_: ScSpecTypeDef::U64, // TODO: Map type.
                         doc: StringM::default(),   // TODO: Add doc.
                     })
                     .collect::<Vec<_>>()
@@ -157,7 +171,20 @@ impl SorobanTarget {
                 outputs: cfg
                     .returns
                     .iter()
-                    .map(|_| ScSpecTypeDef::U32) // TODO: Map type.
+                    .map(|return_type| {
+                        let ty = return_type.ty.clone();
+                        match ty {
+                            ast::Type::Uint(32) => ScSpecTypeDef::U32,
+                            ast::Type::Uint(64) => ScSpecTypeDef::U64,
+                            ast::Type::Int(_) => ScSpecTypeDef::I32,
+                            ast::Type::Bool => ScSpecTypeDef::Bool,
+                            ast::Type::Address(_) => ScSpecTypeDef::Address,
+                            ast::Type::Bytes(_) => ScSpecTypeDef::Bytes,
+                            ast::Type::String => ScSpecTypeDef::String,
+                            ast::Type::Void => ScSpecTypeDef::Void,
+                            _ => panic!("unsupported return type {:?}", ty),
+                        }
+                    }) // TODO: Map type.
                     .collect::<Vec<_>>()
                     .try_into()
                     .expect("function output count exceeds limit"),
@@ -192,4 +219,59 @@ impl SorobanTarget {
             )
             .expect("adding spec as metadata");
     }
+
+    fn declare_externals(binary: &mut Binary) {
+        let ty = binary.context.i64_type();
+        let function_ty_1 = binary
+            .context
+            .i64_type()
+            .fn_type(&[ty.into(), ty.into(), ty.into()], false);
+        let function_ty = binary
+            .context
+            .i64_type()
+            .fn_type(&[ty.into(), ty.into()], false);
+
+        binary
+            .module
+            .add_function(PUT_CONTRACT_DATA, function_ty_1, Some(Linkage::External));
+        binary
+            .module
+            .add_function(GET_CONTRACT_DATA, function_ty, Some(Linkage::External));
+    }
+
+    fn emit_initializer(binary: &mut Binary, _ns: &ast::Namespace) {
+        let mut cfg = ControlFlowGraph::new("init".to_string(), ASTFunction::None);
+
+        cfg.public = true;
+        let void_param = ast::Parameter::new_default(ast::Type::Void);
+        cfg.returns = sync::Arc::new(vec![void_param]);
+
+        Self::emit_function_spec_entry(binary.context, &cfg, "init".to_string(), binary);
+
+        let function_name = CString::new(STORAGE_INITIALIZER).unwrap();
+        let mut storage_initializers = binary
+            .functions
+            .values()
+            .filter(|f: &&inkwell::values::FunctionValue| f.get_name() == function_name.as_c_str());
+        let storage_initializer = *storage_initializers
+            .next()
+            .expect("storage initializer is always present");
+        assert!(storage_initializers.next().is_none());
+
+        let void_type = binary.context.i64_type().fn_type(&[], false);
+        let init = binary
+            .module
+            .add_function("init", void_type, Some(Linkage::External));
+        let entry = binary.context.append_basic_block(init, "entry");
+
+        binary.builder.position_at_end(entry);
+        binary
+            .builder
+            .build_call(storage_initializer, &[], "storage_initializer")
+            .unwrap();
+
+        // return zero
+        let zero_val = binary.context.i64_type().const_int(2, false);
+        binary.builder.build_return(Some(&zero_val)).unwrap();
+    }
 }

+ 51 - 10
src/emit/soroban/target.rs

@@ -3,17 +3,22 @@
 use crate::codegen::cfg::HashTy;
 use crate::codegen::Expression;
 use crate::emit::binary::Binary;
-use crate::emit::soroban::SorobanTarget;
+use crate::emit::soroban::{SorobanTarget, GET_CONTRACT_DATA, PUT_CONTRACT_DATA};
 use crate::emit::ContractArgs;
 use crate::emit::{TargetRuntime, Variable};
+use crate::emit_context;
 use crate::sema::ast;
 use crate::sema::ast::CallTy;
 use crate::sema::ast::{Function, Namespace, Type};
+
 use inkwell::types::{BasicTypeEnum, IntType};
 use inkwell::values::{
-    ArrayValue, BasicMetadataValueEnum, BasicValueEnum, FunctionValue, IntValue, PointerValue,
+    ArrayValue, BasicMetadataValueEnum, BasicValue, BasicValueEnum, FunctionValue, IntValue,
+    PointerValue,
 };
+
 use solang_parser::pt::Loc;
+
 use std::collections::HashMap;
 
 // TODO: Implement TargetRuntime for SorobanTarget.
@@ -21,12 +26,12 @@ use std::collections::HashMap;
 impl<'a> TargetRuntime<'a> for SorobanTarget {
     fn get_storage_int(
         &self,
-        bin: &Binary<'a>,
+        binary: &Binary<'a>,
         function: FunctionValue,
         slot: PointerValue<'a>,
         ty: IntType<'a>,
     ) -> IntValue<'a> {
-        unimplemented!()
+        todo!()
     }
 
     fn storage_load(
@@ -37,7 +42,23 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
         function: FunctionValue<'a>,
         ns: &ast::Namespace,
     ) -> BasicValueEnum<'a> {
-        unimplemented!()
+        emit_context!(binary);
+        let ret = call!(
+            GET_CONTRACT_DATA,
+            &[
+                slot.as_basic_value_enum()
+                    .into_int_value()
+                    .const_cast(binary.context.i64_type(), false)
+                    .into(),
+                i64_const!(2).into()
+            ]
+        )
+        .try_as_basic_value()
+        .left()
+        .unwrap()
+        .into_int_value();
+
+        ret.into()
     }
 
     /// Recursively store a type to storage
@@ -51,7 +72,28 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
         function: FunctionValue<'a>,
         ns: &ast::Namespace,
     ) {
-        unimplemented!()
+        emit_context!(binary);
+        let function_value = binary.module.get_function(PUT_CONTRACT_DATA).unwrap();
+
+        let value = binary
+            .builder
+            .build_call(
+                function_value,
+                &[
+                    slot.as_basic_value_enum()
+                        .into_int_value()
+                        .const_cast(binary.context.i64_type(), false)
+                        .into(),
+                    dest.into(),
+                    binary.context.i64_type().const_int(2, false).into(),
+                ],
+                PUT_CONTRACT_DATA,
+            )
+            .unwrap()
+            .try_as_basic_value()
+            .left()
+            .unwrap()
+            .into_int_value();
     }
 
     /// Recursively clear storage. The default implementation is for slot-based storage
@@ -193,9 +235,8 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
     }
 
     /// Prints a string
-    fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {
-        unimplemented!()
-    }
+    /// TODO: Implement this function, with a call to the `log` function in the Soroban runtime.
+    fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {}
 
     /// Return success without any result
     fn return_empty_abi(&self, bin: &Binary) {
@@ -209,7 +250,7 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
 
     /// Return failure without any result
     fn assert_failure(&self, bin: &Binary, data: PointerValue, length: IntValue) {
-        unimplemented!()
+        bin.builder.build_unreachable().unwrap();
     }
 
     fn builtin_function(

+ 1 - 1
src/emit/storage.rs

@@ -5,7 +5,7 @@ use crate::sema::ast::{Namespace, Type};
 use inkwell::types::BasicTypeEnum;
 use inkwell::values::{ArrayValue, BasicValueEnum, FunctionValue, IntValue, PointerValue};
 
-/// This trait species the methods for managing storage on slot based environments
+/// This trait specifies the methods for managing storage on slot based environments
 pub(super) trait StorageSlot {
     fn set_storage(
         &self,

+ 66 - 1
src/linker/soroban_wasm.rs

@@ -5,6 +5,14 @@ use std::fs::File;
 use std::io::Read;
 use std::io::Write;
 use tempfile::tempdir;
+use wasm_encoder::{
+    ConstExpr, EntityType, GlobalSection, GlobalType, ImportSection, MemoryType, Module,
+    RawSection, ValType,
+};
+use wasmparser::{Global, Import, Parser, Payload::*, SectionLimited, TypeRef};
+
+use crate::emit::soroban::GET_CONTRACT_DATA;
+use crate::emit::soroban::PUT_CONTRACT_DATA;
 
 pub fn link(input: &[u8], name: &str) -> Vec<u8> {
     let dir = tempdir().expect("failed to create temp directory for linking");
@@ -50,5 +58,62 @@ pub fn link(input: &[u8], name: &str) -> Vec<u8> {
         .read_to_end(&mut output)
         .expect("failed to read output file");
 
-    output
+    //output
+    generate_module(&output)
+}
+
+fn generate_module(input: &[u8]) -> Vec<u8> {
+    let mut module = Module::new();
+    for payload in Parser::new(0).parse_all(input).map(|s| s.unwrap()) {
+        match payload {
+            ImportSection(s) => generate_import_section(s, &mut module),
+            GlobalSection(s) => generate_global_section(s, &mut module),
+            ModuleSection { .. } | ComponentSection { .. } => panic!("nested WASM module"),
+            _ => {
+                if let Some((id, range)) = payload.as_section() {
+                    module.section(&RawSection {
+                        id,
+                        data: &input[range],
+                    });
+                }
+            }
+        }
+    }
+    module.finish()
+}
+
+/// Resolve all pallet contracts runtime imports
+fn generate_import_section(section: SectionLimited<Import>, module: &mut Module) {
+    let mut imports = ImportSection::new();
+    for import in section.into_iter().map(|import| import.unwrap()) {
+        let import_type = match import.ty {
+            TypeRef::Func(n) => EntityType::Function(n),
+            TypeRef::Memory(m) => EntityType::Memory(MemoryType {
+                maximum: m.maximum,
+                minimum: m.initial,
+                memory64: m.memory64,
+                shared: m.shared,
+            }),
+            _ => panic!("unexpected WASM import section {:?}", import),
+        };
+        let module_name = match import.name {
+            GET_CONTRACT_DATA | PUT_CONTRACT_DATA => "l",
+            _ => panic!("got func {:?}", import),
+        };
+        // parse the import name to all string after the the first dot
+        let import_name = import.name.split('.').nth(1).unwrap();
+        imports.import(module_name, import_name, import_type);
+    }
+    module.section(&imports);
+}
+
+/// Set the stack pointer to 64k (this is the only global)
+fn generate_global_section(_section: SectionLimited<Global>, module: &mut Module) {
+    let mut globals = GlobalSection::new();
+    let global_type = GlobalType {
+        val_type: ValType::I32,
+        mutable: true,
+    };
+    globals.global(global_type, &ConstExpr::i32_const(1048576));
+    module.section(&globals);
 }

+ 2 - 1
src/sema/contracts.rs

@@ -1,7 +1,8 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use super::{
-    annotions_not_allowed, ast,
+    annotions_not_allowed,
+    ast::{self},
     diagnostics::Diagnostics,
     expression::{compatible_mutability, ExprContext},
     functions, statements,

+ 1 - 0
tests/soroban.rs

@@ -31,6 +31,7 @@ pub fn build_solidity(src: &str) -> SorobanEnv {
             log_prints: true,
             #[cfg(feature = "wasm_opt")]
             wasm_opt: Some(contract_build::OptimizationPasses::Z),
+            soroban_version: Some(85899345977),
             ..Default::default()
         },
         std::vec!["unknown".to_string()],

+ 22 - 25
tests/soroban_testcases/math.rs

@@ -1,11 +1,11 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use crate::build_solidity;
-use soroban_sdk::Val;
+use soroban_sdk::{IntoVal, Val};
 
 #[test]
 fn math() {
-    let env = build_solidity(
+    let runtime = build_solidity(
         r#"contract math {
         function max(uint64 a, uint64 b) public returns (uint64) {
             if (a > b) {
@@ -17,13 +17,14 @@ fn math() {
     }"#,
     );
 
-    let addr = env.contracts.last().unwrap();
-    let res = env.invoke_contract(
-        addr,
-        "max",
-        vec![*Val::from_u32(4).as_val(), *Val::from_u32(5).as_val()],
-    );
-    assert!(Val::from_u32(5).as_val().shallow_eq(&res))
+    let arg: Val = 5_u64.into_val(&runtime.env);
+    let arg2: Val = 4_u64.into_val(&runtime.env);
+
+    let addr = runtime.contracts.last().unwrap();
+    let res = runtime.invoke_contract(addr, "max", vec![arg, arg2]);
+
+    let expected: Val = 5_u64.into_val(&runtime.env);
+    assert!(expected.shallow_eq(&res));
 }
 
 #[test]
@@ -58,21 +59,17 @@ fn math_same_name() {
     );
 
     let addr = src.contracts.last().unwrap();
-    let res = src.invoke_contract(
-        addr,
-        "max_uint64_uint64",
-        vec![*Val::from_u32(4).as_val(), *Val::from_u32(5).as_val()],
-    );
-    assert!(Val::from_u32(5).as_val().shallow_eq(&res));
 
-    let res = src.invoke_contract(
-        addr,
-        "max_uint64_uint64_uint64",
-        vec![
-            *Val::from_u32(4).as_val(),
-            *Val::from_u32(5).as_val(),
-            *Val::from_u32(6).as_val(),
-        ],
-    );
-    assert!(Val::from_u32(6).as_val().shallow_eq(&res));
+    let arg1: Val = 5_u64.into_val(&src.env);
+    let arg2: Val = 4_u64.into_val(&src.env);
+    let res = src.invoke_contract(addr, "max_uint64_uint64", vec![arg1, arg2]);
+    let expected: Val = 5_u64.into_val(&src.env);
+    assert!(expected.shallow_eq(&res));
+
+    let arg1: Val = 5_u64.into_val(&src.env);
+    let arg2: Val = 4_u64.into_val(&src.env);
+    let arg3: Val = 6_u64.into_val(&src.env);
+    let res = src.invoke_contract(addr, "max_uint64_uint64_uint64", vec![arg1, arg2, arg3]);
+    let expected: Val = 6_u64.into_val(&src.env);
+    assert!(expected.shallow_eq(&res));
 }

+ 1 - 0
tests/soroban_testcases/mod.rs

@@ -1,2 +1,3 @@
 // SPDX-License-Identifier: Apache-2.0
 mod math;
+mod storage;

+ 41 - 0
tests/soroban_testcases/storage.rs

@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::build_solidity;
+use soroban_sdk::{IntoVal, Val};
+
+#[test]
+fn counter() {
+    let src = build_solidity(
+        r#"contract counter {
+            uint64 public count = 10;
+        
+            function increment() public returns (uint64){
+                count += 1;
+                return count;
+            }
+        
+            function decrement() public returns (uint64){
+                count -= 1;
+                return count;
+            }
+        }"#,
+    );
+
+    let addr = src.contracts.last().unwrap();
+
+    let _res = src.invoke_contract(addr, "init", vec![]);
+
+    let res = src.invoke_contract(addr, "count", vec![]);
+    let expected: Val = 10_u64.into_val(&src.env);
+    assert!(expected.shallow_eq(&res));
+
+    src.invoke_contract(addr, "increment", vec![]);
+    let res = src.invoke_contract(addr, "count", vec![]);
+    let expected: Val = 11_u64.into_val(&src.env);
+    assert!(expected.shallow_eq(&res));
+
+    src.invoke_contract(addr, "decrement", vec![]);
+    let res = src.invoke_contract(addr, "count", vec![]);
+    let expected: Val = 10_u64.into_val(&src.env);
+    assert!(expected.shallow_eq(&res));
+}

+ 1 - 0
tests/undefined_variable_detection.rs

@@ -27,6 +27,7 @@ fn parse_and_codegen(src: &'static str) -> Namespace {
         log_prints: true,
         #[cfg(feature = "wasm_opt")]
         wasm_opt: None,
+        soroban_version: None,
     };
 
     codegen(&mut ns, &opt);