Skip to content

Commit ca456ae

Browse files
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>
1 parent 41b93d3 commit ca456ae

File tree

9 files changed

+349
-10
lines changed

9 files changed

+349
-10
lines changed

src/codegen/dispatch/soroban.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub fn function_dispatch(
3434
let function = match &cfg.function_no {
3535
ASTFunction::SolidityFunction(no) => &ns.functions[*no],
3636

37-
// untill this stage, we have processed constructor and storage_initializer, so all that is left is the solidity functions
37+
// until this stage, we have processed constructor and storage_initializer, so all that is left is the solidity functions
3838
_ => unreachable!(),
3939
};
4040

@@ -105,6 +105,18 @@ pub fn function_dispatch(
105105
args: vec![],
106106
};
107107
wrapper_cfg.add(&mut vartab, placeholder);
108+
109+
// check if constructor exists. If it does, we need to call it
110+
if let ASTFunction::SolidityFunction(cfg_no) = cfg.function_no {
111+
// add a call to the constructor
112+
let placeholder = Instr::Call {
113+
res: call_returns.clone(),
114+
call: InternalCallTy::Static { cfg_no },
115+
return_tys: vec![],
116+
args: decoded.clone(),
117+
};
118+
wrapper_cfg.add(&mut vartab, placeholder);
119+
};
108120
}
109121

110122
if wrapper_cfg.name != "__constructor" {
@@ -113,7 +125,6 @@ pub fn function_dispatch(
113125
_ => unreachable!(),
114126
};
115127

116-
// add a call to the storage initializer
117128
let placeholder = Instr::Call {
118129
res: call_returns,
119130
call: InternalCallTy::Static { cfg_no },

src/codegen/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ impl From<inkwell::OptimizationLevel> for OptimizationLevel {
9898
pub enum HostFunctions {
9999
PutContractData,
100100
GetContractData,
101+
HasContractData,
101102
ExtendContractDataTtl,
102103
ExtendCurrentContractInstanceAndCodeTtl,
103104
LogFromLinearMemory,
@@ -129,6 +130,7 @@ impl HostFunctions {
129130
match self {
130131
HostFunctions::PutContractData => "l._",
131132
HostFunctions::GetContractData => "l.1",
133+
HostFunctions::HasContractData => "l.0",
132134
HostFunctions::ExtendContractDataTtl => "l.7",
133135
HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => "l.8",
134136
HostFunctions::LogFromLinearMemory => "x._",

src/emit/binary.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,10 @@ impl<'a> Binary<'a> {
885885

886886
/// Return the llvm type for a variable holding the type, not the type itself
887887
pub(crate) fn llvm_var_ty(&self, ty: &Type) -> BasicTypeEnum<'a> {
888+
if self.ns.target == Target::Soroban {
889+
return self.llvm_type(ty);
890+
}
891+
888892
let llvm_ty = self.llvm_type(ty);
889893
match ty.deref_memory() {
890894
Type::Struct(_)

src/emit/soroban/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ impl HostFunctions {
3434
.context
3535
.i64_type()
3636
.fn_type(&[ty.into(), ty.into()], false),
37+
HostFunctions::HasContractData => bin
38+
.context
39+
.i64_type()
40+
.fn_type(&[ty.into(), ty.into()], false),
3741
// https://github.com/stellar/stellar-protocol/blob/2fdc77302715bc4a31a784aef1a797d466965024/core/cap-0046-03.md#ledger-host-functions-mod-l
3842
// ;; 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.
3943
// (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 {
334338
let host_functions = [
335339
HostFunctions::PutContractData,
336340
HostFunctions::GetContractData,
341+
HostFunctions::HasContractData,
337342
HostFunctions::ExtendContractDataTtl,
338343
HostFunctions::ExtendCurrentContractInstanceAndCodeTtl,
339344
HostFunctions::LogFromLinearMemory,

src/emit/soroban/target.rs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
5656
*slot
5757
};
5858

59-
let ret = call!(
60-
HostFunctions::GetContractData.name(),
59+
// === Call HasContractData ===
60+
let has_data_val = call!(
61+
HostFunctions::HasContractData.name(),
6162
&[
6263
slot.into(),
6364
bin.context.i64_type().const_int(storage_type, false).into(),
@@ -68,7 +69,49 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
6869
.unwrap()
6970
.into_int_value();
7071

71-
ret.into()
72+
// === Use helper to check if it's true ===
73+
let condition = is_val_true(bin, has_data_val);
74+
75+
// === Prepare blocks ===
76+
let parent = function;
77+
let then_bb = bin.context.append_basic_block(parent, "has_data");
78+
let else_bb = bin.context.append_basic_block(parent, "no_data");
79+
let merge_bb = bin.context.append_basic_block(parent, "merge");
80+
81+
bin.builder
82+
.build_conditional_branch(condition, then_bb, else_bb)
83+
.unwrap();
84+
85+
// === THEN block: call GetContractData ===
86+
bin.builder.position_at_end(then_bb);
87+
let value_from_contract = call!(
88+
HostFunctions::GetContractData.name(),
89+
&[
90+
slot.into(),
91+
bin.context.i64_type().const_int(storage_type, false).into(),
92+
]
93+
)
94+
.try_as_basic_value()
95+
.left()
96+
.unwrap();
97+
bin.builder.build_unconditional_branch(merge_bb).unwrap();
98+
let then_value = value_from_contract;
99+
100+
// === ELSE block: return default ===
101+
bin.builder.position_at_end(else_bb);
102+
let default_value = type_to_tagged_zero_val(bin, ty);
103+
104+
bin.builder.build_unconditional_branch(merge_bb).unwrap();
105+
106+
// === MERGE block with phi node ===
107+
bin.builder.position_at_end(merge_bb);
108+
let phi = bin
109+
.builder
110+
.build_phi(bin.context.i64_type(), "storage_result")
111+
.unwrap();
112+
phi.add_incoming(&[(&then_value, then_bb), (&default_value, else_bb)]);
113+
114+
phi.as_basic_value()
72115
}
73116

74117
/// Recursively store a type to storage
@@ -222,7 +265,8 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
222265
};
223266

224267
// push the slot to the vector
225-
bin.builder
268+
let res = bin
269+
.builder
226270
.build_call(
227271
bin.module
228272
.get_function(HostFunctions::VecPushBack.name())
@@ -237,20 +281,21 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
237281
.into_int_value();
238282

239283
// push the index to the vector
240-
bin.builder
284+
let res = bin
285+
.builder
241286
.build_call(
242287
bin.module
243288
.get_function(HostFunctions::VecPushBack.name())
244289
.unwrap(),
245-
&[vec_new.as_basic_value_enum().into(), index.into()],
290+
&[res.as_basic_value_enum().into(), index.into()],
246291
"push",
247292
)
248293
.unwrap()
249294
.try_as_basic_value()
250295
.left()
251296
.unwrap()
252297
.into_int_value();
253-
vec_new
298+
res
254299
}
255300

256301
fn storage_push(
@@ -728,3 +773,47 @@ fn encode_value<'a>(value: IntValue<'a>, shift: u64, add: u64, bin: &'a Binary)
728773
)
729774
.unwrap()
730775
}
776+
777+
fn is_val_true<'ctx>(bin: &Binary<'ctx>, val: IntValue<'ctx>) -> IntValue<'ctx> {
778+
let tag_mask = bin.context.i64_type().const_int(0xff, false);
779+
let tag_true = bin.context.i64_type().const_int(1, false);
780+
781+
let tag = bin
782+
.builder
783+
.build_and(val, tag_mask, "val_tag")
784+
.expect("build_and failed");
785+
786+
bin.builder
787+
.build_int_compare(inkwell::IntPredicate::EQ, tag, tag_true, "is_val_true")
788+
.expect("build_int_compare failed")
789+
}
790+
791+
/// Returns a Val representing a default zero value with the correct Soroban Tag.
792+
pub fn type_to_tagged_zero_val<'ctx>(bin: &Binary<'ctx>, ty: &Type) -> IntValue<'ctx> {
793+
let context = &bin.context;
794+
let i64_type = context.i64_type();
795+
796+
// Tag definitions from CAP-0046
797+
let tag = match ty {
798+
Type::Bool => 0, // Tag::False
799+
Type::Uint(32) => 4, // Tag::U32Val
800+
Type::Int(32) => 5, // Tag::I32Val
801+
Type::Uint(64) => 6, // Tag::U64Small
802+
Type::Int(64) => 7, // Tag::I64Small
803+
Type::Uint(128) => 10, // Tag::U128Small
804+
Type::Int(128) => 11, // Tag::I128Small
805+
Type::Uint(256) => 12, // Tag::U256Small
806+
Type::Int(256) => 13, // Tag::I256Small
807+
Type::String => 73, // Tag::StringObject
808+
Type::Address(_) => 77, // Tag::AddressObject
809+
Type::Void => 2, // Tag::Void
810+
_ => {
811+
// Fallback to Void for unsupported types
812+
2 // Tag::Void
813+
}
814+
};
815+
816+
// All zero body + tag in lower 8 bits
817+
let tag_val: u64 = tag;
818+
i64_type.const_int(tag_val, false)
819+
}

tests/soroban.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use solang::sema::ast::Namespace;
99
use solang::sema::diagnostics::Diagnostics;
1010
use solang::{compile, Target};
1111
use soroban_sdk::testutils::Logs;
12-
use soroban_sdk::{vec, Address, Env, Symbol, Val};
12+
use soroban_sdk::{vec, Address, ConstructorArgs, Env, Symbol, Val};
1313
use std::ffi::OsStr;
1414

1515
// TODO: register accounts, related balances, events, etc.
@@ -132,6 +132,19 @@ impl SorobanEnv {
132132

133133
addr
134134
}
135+
136+
pub fn deploy_contract_with_args<A>(&mut self, src: &str, args: A) -> Address
137+
where
138+
A: ConstructorArgs,
139+
{
140+
let wasm = build_wasm(src).0;
141+
142+
let addr = self.env.register(wasm.as_slice(), args);
143+
144+
self.contracts.push(addr.clone());
145+
146+
addr
147+
}
135148
}
136149

137150
impl Default for SorobanEnv {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
use crate::SorobanEnv;
4+
use indexmap::Equivalent;
5+
use soroban_sdk::{testutils::Address as _, Address, FromVal, IntoVal, String, Val};
6+
7+
#[test]
8+
fn constructor_profile_test() {
9+
let mut runtime = SorobanEnv::new();
10+
11+
let user = Address::generate(&runtime.env);
12+
let name = String::from_str(&runtime.env, "Alice");
13+
let age: Val = 30_u32.into_val(&runtime.env);
14+
15+
let contract_src = r#"
16+
contract profile {
17+
address public user;
18+
string public name;
19+
uint32 public age;
20+
21+
constructor(address _user, string memory _name, uint32 _age) {
22+
user = _user;
23+
name = _name;
24+
age = _age;
25+
}
26+
}
27+
"#;
28+
29+
let addr = runtime.deploy_contract_with_args(contract_src, (user.clone(), name.clone(), age));
30+
31+
let user_ret = runtime.invoke_contract(&addr, "user", vec![]);
32+
let name_ret = runtime.invoke_contract(&addr, "name", vec![]);
33+
let age_ret = runtime.invoke_contract(&addr, "age", vec![]);
34+
35+
let expected_user = Address::from_val(&runtime.env, &user_ret);
36+
assert!(expected_user.equivalent(&user));
37+
38+
let expected_name = String::from_val(&runtime.env, &name_ret);
39+
assert!(expected_name.equivalent(&name));
40+
41+
let expected_age: u32 = FromVal::from_val(&runtime.env, &age_ret);
42+
assert_eq!(expected_age, 30);
43+
}

tests/soroban_testcases/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// SPDX-License-Identifier: Apache-2.0
22
mod auth;
3+
mod constructor;
34
mod cross_contract_calls;
45
mod mappings;
56
mod math;
67
mod print;
78
mod storage;
9+
mod token;
810
mod ttl;

0 commit comments

Comments
 (0)