Эх сурвалжийг харах

Support Soroban Token Contract (#1808)

This PR aims to support a Stellar Asset Contract:
https://developers.stellar.org/docs/tokens/stellar-asset-contract.
This involved:
- Supporting constructors with args
- Bug fix in mappings support
- Accepting strings as args

Signed-off-by: salaheldinsoliman <salaheldin_sameh@aucegypt.edu>
salaheldinsoliman 5 сар өмнө
parent
commit
ca456aef6c

+ 13 - 2
src/codegen/dispatch/soroban.rs

@@ -34,7 +34,7 @@ pub fn function_dispatch(
                 let function = match &cfg.function_no {
                     ASTFunction::SolidityFunction(no) => &ns.functions[*no],
 
-                    // untill this stage, we have processed constructor and storage_initializer, so all that is left is the solidity functions
+                    // until this stage, we have processed constructor and storage_initializer, so all that is left is the solidity functions
                     _ => unreachable!(),
                 };
 
@@ -105,6 +105,18 @@ pub fn function_dispatch(
                 args: vec![],
             };
             wrapper_cfg.add(&mut vartab, placeholder);
+
+            // check if constructor exists. If it does, we need to call it
+            if let ASTFunction::SolidityFunction(cfg_no) = cfg.function_no {
+                // add a call to the constructor
+                let placeholder = Instr::Call {
+                    res: call_returns.clone(),
+                    call: InternalCallTy::Static { cfg_no },
+                    return_tys: vec![],
+                    args: decoded.clone(),
+                };
+                wrapper_cfg.add(&mut vartab, placeholder);
+            };
         }
 
         if wrapper_cfg.name != "__constructor" {
@@ -113,7 +125,6 @@ pub fn function_dispatch(
                 _ => unreachable!(),
             };
 
-            // add a call to the storage initializer
             let placeholder = Instr::Call {
                 res: call_returns,
                 call: InternalCallTy::Static { cfg_no },

+ 2 - 0
src/codegen/mod.rs

@@ -98,6 +98,7 @@ impl From<inkwell::OptimizationLevel> for OptimizationLevel {
 pub enum HostFunctions {
     PutContractData,
     GetContractData,
+    HasContractData,
     ExtendContractDataTtl,
     ExtendCurrentContractInstanceAndCodeTtl,
     LogFromLinearMemory,
@@ -129,6 +130,7 @@ impl HostFunctions {
         match self {
             HostFunctions::PutContractData => "l._",
             HostFunctions::GetContractData => "l.1",
+            HostFunctions::HasContractData => "l.0",
             HostFunctions::ExtendContractDataTtl => "l.7",
             HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => "l.8",
             HostFunctions::LogFromLinearMemory => "x._",

+ 4 - 0
src/emit/binary.rs

@@ -885,6 +885,10 @@ impl<'a> Binary<'a> {
 
     /// Return the llvm type for a variable holding the type, not the type itself
     pub(crate) fn llvm_var_ty(&self, ty: &Type) -> BasicTypeEnum<'a> {
+        if self.ns.target == Target::Soroban {
+            return self.llvm_type(ty);
+        }
+
         let llvm_ty = self.llvm_type(ty);
         match ty.deref_memory() {
             Type::Struct(_)

+ 5 - 0
src/emit/soroban/mod.rs

@@ -34,6 +34,10 @@ impl HostFunctions {
                 .context
                 .i64_type()
                 .fn_type(&[ty.into(), ty.into()], false),
+            HostFunctions::HasContractData => bin
+                .context
+                .i64_type()
+                .fn_type(&[ty.into(), ty.into()], false),
             // https://github.com/stellar/stellar-protocol/blob/2fdc77302715bc4a31a784aef1a797d466965024/core/cap-0046-03.md#ledger-host-functions-mod-l
             // ;; If the entry's TTL is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger.
             // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
@@ -334,6 +338,7 @@ impl SorobanTarget {
         let host_functions = [
             HostFunctions::PutContractData,
             HostFunctions::GetContractData,
+            HostFunctions::HasContractData,
             HostFunctions::ExtendContractDataTtl,
             HostFunctions::ExtendCurrentContractInstanceAndCodeTtl,
             HostFunctions::LogFromLinearMemory,

+ 96 - 7
src/emit/soroban/target.rs

@@ -56,8 +56,9 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             *slot
         };
 
-        let ret = call!(
-            HostFunctions::GetContractData.name(),
+        // === Call HasContractData ===
+        let has_data_val = call!(
+            HostFunctions::HasContractData.name(),
             &[
                 slot.into(),
                 bin.context.i64_type().const_int(storage_type, false).into(),
@@ -68,7 +69,49 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
         .unwrap()
         .into_int_value();
 
-        ret.into()
+        // === Use helper to check if it's true ===
+        let condition = is_val_true(bin, has_data_val);
+
+        // === Prepare blocks ===
+        let parent = function;
+        let then_bb = bin.context.append_basic_block(parent, "has_data");
+        let else_bb = bin.context.append_basic_block(parent, "no_data");
+        let merge_bb = bin.context.append_basic_block(parent, "merge");
+
+        bin.builder
+            .build_conditional_branch(condition, then_bb, else_bb)
+            .unwrap();
+
+        // === THEN block: call GetContractData ===
+        bin.builder.position_at_end(then_bb);
+        let value_from_contract = call!(
+            HostFunctions::GetContractData.name(),
+            &[
+                slot.into(),
+                bin.context.i64_type().const_int(storage_type, false).into(),
+            ]
+        )
+        .try_as_basic_value()
+        .left()
+        .unwrap();
+        bin.builder.build_unconditional_branch(merge_bb).unwrap();
+        let then_value = value_from_contract;
+
+        // === ELSE block: return default ===
+        bin.builder.position_at_end(else_bb);
+        let default_value = type_to_tagged_zero_val(bin, ty);
+
+        bin.builder.build_unconditional_branch(merge_bb).unwrap();
+
+        // === MERGE block with phi node ===
+        bin.builder.position_at_end(merge_bb);
+        let phi = bin
+            .builder
+            .build_phi(bin.context.i64_type(), "storage_result")
+            .unwrap();
+        phi.add_incoming(&[(&then_value, then_bb), (&default_value, else_bb)]);
+
+        phi.as_basic_value()
     }
 
     /// Recursively store a type to storage
@@ -222,7 +265,8 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
         };
 
         // push the slot to the vector
-        bin.builder
+        let res = bin
+            .builder
             .build_call(
                 bin.module
                     .get_function(HostFunctions::VecPushBack.name())
@@ -237,12 +281,13 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             .into_int_value();
 
         // push the index to the vector
-        bin.builder
+        let res = bin
+            .builder
             .build_call(
                 bin.module
                     .get_function(HostFunctions::VecPushBack.name())
                     .unwrap(),
-                &[vec_new.as_basic_value_enum().into(), index.into()],
+                &[res.as_basic_value_enum().into(), index.into()],
                 "push",
             )
             .unwrap()
@@ -250,7 +295,7 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             .left()
             .unwrap()
             .into_int_value();
-        vec_new
+        res
     }
 
     fn storage_push(
@@ -728,3 +773,47 @@ fn encode_value<'a>(value: IntValue<'a>, shift: u64, add: u64, bin: &'a Binary)
         )
         .unwrap()
 }
+
+fn is_val_true<'ctx>(bin: &Binary<'ctx>, val: IntValue<'ctx>) -> IntValue<'ctx> {
+    let tag_mask = bin.context.i64_type().const_int(0xff, false);
+    let tag_true = bin.context.i64_type().const_int(1, false);
+
+    let tag = bin
+        .builder
+        .build_and(val, tag_mask, "val_tag")
+        .expect("build_and failed");
+
+    bin.builder
+        .build_int_compare(inkwell::IntPredicate::EQ, tag, tag_true, "is_val_true")
+        .expect("build_int_compare failed")
+}
+
+/// Returns a Val representing a default zero value with the correct Soroban Tag.
+pub fn type_to_tagged_zero_val<'ctx>(bin: &Binary<'ctx>, ty: &Type) -> IntValue<'ctx> {
+    let context = &bin.context;
+    let i64_type = context.i64_type();
+
+    // Tag definitions from CAP-0046
+    let tag = match ty {
+        Type::Bool => 0,        // Tag::False
+        Type::Uint(32) => 4,    // Tag::U32Val
+        Type::Int(32) => 5,     // Tag::I32Val
+        Type::Uint(64) => 6,    // Tag::U64Small
+        Type::Int(64) => 7,     // Tag::I64Small
+        Type::Uint(128) => 10,  // Tag::U128Small
+        Type::Int(128) => 11,   // Tag::I128Small
+        Type::Uint(256) => 12,  // Tag::U256Small
+        Type::Int(256) => 13,   // Tag::I256Small
+        Type::String => 73,     // Tag::StringObject
+        Type::Address(_) => 77, // Tag::AddressObject
+        Type::Void => 2,        // Tag::Void
+        _ => {
+            // Fallback to Void for unsupported types
+            2 // Tag::Void
+        }
+    };
+
+    // All zero body + tag in lower 8 bits
+    let tag_val: u64 = tag;
+    i64_type.const_int(tag_val, false)
+}

+ 14 - 1
tests/soroban.rs

@@ -9,7 +9,7 @@ use solang::sema::ast::Namespace;
 use solang::sema::diagnostics::Diagnostics;
 use solang::{compile, Target};
 use soroban_sdk::testutils::Logs;
-use soroban_sdk::{vec, Address, Env, Symbol, Val};
+use soroban_sdk::{vec, Address, ConstructorArgs, Env, Symbol, Val};
 use std::ffi::OsStr;
 
 // TODO: register accounts, related balances, events, etc.
@@ -132,6 +132,19 @@ impl SorobanEnv {
 
         addr
     }
+
+    pub fn deploy_contract_with_args<A>(&mut self, src: &str, args: A) -> Address
+    where
+        A: ConstructorArgs,
+    {
+        let wasm = build_wasm(src).0;
+
+        let addr = self.env.register(wasm.as_slice(), args);
+
+        self.contracts.push(addr.clone());
+
+        addr
+    }
 }
 
 impl Default for SorobanEnv {

+ 43 - 0
tests/soroban_testcases/constructor.rs

@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::SorobanEnv;
+use indexmap::Equivalent;
+use soroban_sdk::{testutils::Address as _, Address, FromVal, IntoVal, String, Val};
+
+#[test]
+fn constructor_profile_test() {
+    let mut runtime = SorobanEnv::new();
+
+    let user = Address::generate(&runtime.env);
+    let name = String::from_str(&runtime.env, "Alice");
+    let age: Val = 30_u32.into_val(&runtime.env);
+
+    let contract_src = r#"
+        contract profile {
+            address public user;
+            string public name;
+            uint32 public age;
+
+            constructor(address _user, string memory _name, uint32 _age) {
+                user = _user;
+                name = _name;
+                age = _age;
+            }
+        }
+    "#;
+
+    let addr = runtime.deploy_contract_with_args(contract_src, (user.clone(), name.clone(), age));
+
+    let user_ret = runtime.invoke_contract(&addr, "user", vec![]);
+    let name_ret = runtime.invoke_contract(&addr, "name", vec![]);
+    let age_ret = runtime.invoke_contract(&addr, "age", vec![]);
+
+    let expected_user = Address::from_val(&runtime.env, &user_ret);
+    assert!(expected_user.equivalent(&user));
+
+    let expected_name = String::from_val(&runtime.env, &name_ret);
+    assert!(expected_name.equivalent(&name));
+
+    let expected_age: u32 = FromVal::from_val(&runtime.env, &age_ret);
+    assert_eq!(expected_age, 30);
+}

+ 2 - 0
tests/soroban_testcases/mod.rs

@@ -1,8 +1,10 @@
 // SPDX-License-Identifier: Apache-2.0
 mod auth;
+mod constructor;
 mod cross_contract_calls;
 mod mappings;
 mod math;
 mod print;
 mod storage;
+mod token;
 mod ttl;

+ 170 - 0
tests/soroban_testcases/token.rs

@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::SorobanEnv;
+use soroban_sdk::{testutils::Address as _, Address, IntoVal, Val};
+
+#[test]
+fn token_end_to_end_test() {
+    let mut runtime = SorobanEnv::new();
+
+    let admin = Address::generate(&runtime.env);
+    let name = soroban_sdk::String::from_str(&runtime.env, "Test Token");
+    let symbol = soroban_sdk::String::from_str(&runtime.env, "TTK");
+    let decimals: Val = 18_u32.into_val(&runtime.env);
+
+    let contract_src = r#"
+        contract token {
+            address public admin;
+            uint32 public decimals;
+            string public name;
+            string public symbol;
+
+            constructor(address _admin, string memory _name, string memory _symbol, uint32 _decimals) {
+                admin = _admin;
+                name = _name;
+                symbol = _symbol;
+                decimals = _decimals;
+            }
+
+            mapping(address => int128) public balances;
+            mapping(address => mapping(address => int128)) public allowances;
+
+            function mint(address to, int128 amount) public {
+                require(amount >= 0, "Amount must be non-negative");
+                admin.requireAuth();
+                setBalance(to, balance(to) + amount);
+            }
+
+            function approve(address owner, address spender, int128 amount) public {
+                require(amount >= 0, "Amount must be non-negative");
+                owner.requireAuth();
+                allowances[owner][spender] = amount;
+            }
+
+            function transfer(address from, address to, int128 amount) public {
+                require(amount >= 0, "Amount must be non-negative");
+                from.requireAuth();
+                require(balance(from) >= amount, "Insufficient balance");
+                setBalance(from, balance(from) - amount);
+                setBalance(to, balance(to) + amount);
+            }
+
+            function transfer_from(address spender, address from, address to, int128 amount) public {
+                require(amount >= 0, "Amount must be non-negative");
+                spender.requireAuth();
+                require(balance(from) >= amount, "Insufficient balance");
+                require(allowance(from, spender) >= amount, "Insufficient allowance");
+                setBalance(from, balance(from) - amount);
+                setBalance(to, balance(to) + amount);
+                allowances[from][spender] -= amount;
+            }
+
+            function burn(address from, int128 amount) public {
+                require(amount >= 0, "Amount must be non-negative");
+                require(balance(from) >= amount, "Insufficient balance");
+                from.requireAuth();
+                setBalance(from, balance(from) - amount);
+            }
+
+            function burn_from(address spender, address from, int128 amount) public {
+                require(amount >= 0, "Amount must be non-negative");
+                spender.requireAuth();
+                require(balance(from) >= amount, "Insufficient balance");
+                require(allowance(from, spender) >= amount, "Insufficient allowance");
+                setBalance(from, balance(from) - amount);
+                allowances[from][spender] -= amount;
+            }
+
+            function setBalance(address addr, int128 amount) internal {
+                balances[addr] = amount;
+            }
+
+            function balance(address addr) public view returns (int128) {
+                return balances[addr];
+            }
+
+            function allowance(address owner, address spender) public view returns (int128) {
+                return allowances[owner][spender];
+            }
+        }
+    "#;
+
+    let addr = runtime.deploy_contract_with_args(contract_src, (admin, name, symbol, decimals));
+
+    runtime.env.mock_all_auths();
+
+    let user1 = Address::generate(&runtime.env);
+    let user2 = Address::generate(&runtime.env);
+    let user3 = Address::generate(&runtime.env);
+
+    runtime.invoke_contract(
+        &addr,
+        "mint",
+        vec![
+            user1.clone().into_val(&runtime.env),
+            100_i128.into_val(&runtime.env),
+        ],
+    );
+
+    runtime.invoke_contract(
+        &addr,
+        "transfer",
+        vec![
+            user1.clone().into_val(&runtime.env),
+            user2.clone().into_val(&runtime.env),
+            25_i128.into_val(&runtime.env),
+        ],
+    );
+
+    runtime.invoke_contract(
+        &addr,
+        "approve",
+        vec![
+            user1.clone().into_val(&runtime.env),
+            user3.clone().into_val(&runtime.env),
+            30_i128.into_val(&runtime.env),
+        ],
+    );
+
+    runtime.invoke_contract(
+        &addr,
+        "transfer_from",
+        vec![
+            user3.clone().into_val(&runtime.env),
+            user1.clone().into_val(&runtime.env),
+            user3.clone().into_val(&runtime.env),
+            10_i128.into_val(&runtime.env),
+        ],
+    );
+
+    runtime.invoke_contract(
+        &addr,
+        "burn",
+        vec![
+            user2.clone().into_val(&runtime.env),
+            5_i128.into_val(&runtime.env),
+        ],
+    );
+
+    runtime.invoke_contract(
+        &addr,
+        "burn_from",
+        vec![
+            user3.clone().into_val(&runtime.env),
+            user1.clone().into_val(&runtime.env),
+            15_i128.into_val(&runtime.env),
+        ],
+    );
+
+    let b1 = runtime.invoke_contract(&addr, "balance", vec![user1.into_val(&runtime.env)]);
+    let b2 = runtime.invoke_contract(&addr, "balance", vec![user2.into_val(&runtime.env)]);
+    let b3 = runtime.invoke_contract(&addr, "balance", vec![user3.into_val(&runtime.env)]);
+
+    let expected1: Val = 50_i128.into_val(&runtime.env);
+    let expected2: Val = 20_i128.into_val(&runtime.env);
+    let expected3: Val = 10_i128.into_val(&runtime.env);
+
+    assert!(expected1.shallow_eq(&b1));
+    assert!(expected2.shallow_eq(&b2));
+    assert!(expected3.shallow_eq(&b3));
+}