diff --git a/integration/soroban/.gitignore b/integration/soroban/.gitignore index d33bf9529..ee0ea4517 100644 --- a/integration/soroban/.gitignore +++ b/integration/soroban/.gitignore @@ -6,3 +6,5 @@ !package.json node_modules package-lock.json +*.txt +*.toml diff --git a/integration/soroban/runtime_error.sol b/integration/soroban/runtime_error.sol new file mode 100644 index 000000000..6e6853062 --- /dev/null +++ b/integration/soroban/runtime_error.sol @@ -0,0 +1,9 @@ +contract Error { + uint64 count = 1; + + /// @notice Calling this function twice will cause an overflow + function decrement() public returns (uint64){ + count -= 1; + return count; + } +} \ No newline at end of file diff --git a/integration/soroban/test_helpers.js b/integration/soroban/test_helpers.js index ebafc8f81..1421a6add 100644 --- a/integration/soroban/test_helpers.js +++ b/integration/soroban/test_helpers.js @@ -1,53 +1,64 @@ import * as StellarSdk from '@stellar/stellar-sdk'; - - export async function call_contract_function(method, server, keypair, contract) { + let res = null; - 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(); + 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); + + 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..."); + // Wait one second + await new Promise((resolve) => setTimeout(resolve, 1000)); + // See if the transaction is complete + getResponse = await server.getTransaction(sendResponse.hash); + } + + if (getResponse.status === "SUCCESS") { + // Ensure the transaction's resultMetaXDR is not empty + if (!getResponse.resultMetaXdr) { + throw "Empty resultMetaXDR in getTransaction response"; + } + // Extract and return the return value from the contract + 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 if (sendResponse.status === "FAILED") { + // Handle expected failure and return the error message + if (sendResponse.errorResultXdr) { + const errorXdr = StellarSdk.xdr.TransactionResult.fromXDR(sendResponse.errorResultXdr, 'base64'); + const errorRes = errorXdr.result().results()[0].tr().invokeHostFunctionResult().code().value; + console.log(`Transaction error: ${errorRes}`); + res = errorRes; + } else { + throw "Transaction failed but no errorResultXdr found"; + } } else { - throw `Transaction failed: ${getResponse.resultXdr}`; + throw sendResponse.errorResultXdr; } - } else { - throw sendResponse.errorResultXdr; - } } catch (err) { - // Catch and report any errors we've thrown - console.log("Sending transaction failed"); - console.log(err); + // Return the error as a string instead of failing the test + console.log("Transaction processing failed"); + console.log(err); + res = err.toString(); } + return res; -} \ No newline at end of file +} diff --git a/src/codegen/dispatch/soroban.rs b/src/codegen/dispatch/soroban.rs index 94959edd3..83c2fd4cd 100644 --- a/src/codegen/dispatch/soroban.rs +++ b/src/codegen/dispatch/soroban.rs @@ -102,35 +102,47 @@ pub fn function_dispatch( 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 { + // TODO: support multiple returns + if value.len() == 1 { + // 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), - value: BigInt::from(8_u64), - } - .into(), - }; + 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 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(), - }; + 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] }); + } else { + // Return 2 as numberliteral. 2 is the soroban Void type encoded. + let two = Expression::NumberLiteral { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + value: BigInt::from(2_u64), + }; - wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![added] }); + wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![two] }); + } vartab.finalize(ns, &mut wrapper_cfg); cfg.public = false; diff --git a/src/codegen/expression.rs b/src/codegen/expression.rs index c70818301..6ed45ced0 100644 --- a/src/codegen/expression.rs +++ b/src/codegen/expression.rs @@ -939,7 +939,23 @@ pub fn expression( expr }; - cfg.add(vartab, Instr::Print { expr: to_print }); + let res = if let Expression::AllocDynamicBytes { + loc, + ty, + size: _, + initializer: Some(initializer), + } = &to_print + { + Expression::BytesLiteral { + loc: *loc, + ty: ty.clone(), + value: initializer.to_vec(), + } + } else { + to_print + }; + + cfg.add(vartab, Instr::Print { expr: res }); } Expression::Poison diff --git a/src/emit/expression.rs b/src/emit/expression.rs index 600cfee51..696ecc928 100644 --- a/src/emit/expression.rs +++ b/src/emit/expression.rs @@ -126,7 +126,33 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>( s.into() } - Expression::BytesLiteral { value: bs, .. } => { + Expression::BytesLiteral { value: bs, ty, .. } => { + // If the type of a BytesLiteral is a String, embedd the bytes in the binary. + if ty == &Type::String { + let data = bin.emit_global_string("const_string", bs, true); + + // A constant string, or array, is represented by a struct with two fields: a pointer to the data, and its length. + let ty = bin.context.struct_type( + &[ + bin.llvm_type(&Type::Bytes(bs.len() as u8), ns) + .ptr_type(AddressSpace::default()) + .into(), + bin.context.i64_type().into(), + ], + false, + ); + + return ty + .const_named_struct(&[ + data.into(), + bin.context + .i64_type() + .const_int(bs.len() as u64, false) + .into(), + ]) + .into(); + } + let ty = bin.context.custom_width_int_type((bs.len() * 8) as u32); // hex"11223344" should become i32 0x11223344 diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 51191ea37..3b81b5373 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -22,6 +22,7 @@ use std::sync; const SOROBAN_ENV_INTERFACE_VERSION: u64 = 90194313216; pub const PUT_CONTRACT_DATA: &str = "l._"; pub const GET_CONTRACT_DATA: &str = "l.1"; +pub const LOG_FROM_LINEAR_MEMORY: &str = "x._"; pub struct SorobanTarget; @@ -231,12 +232,23 @@ impl SorobanTarget { .i64_type() .fn_type(&[ty.into(), ty.into()], false); + let log_function_ty = binary + .context + .i64_type() + .fn_type(&[ty.into(), ty.into(), 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)); + + binary.module.add_function( + LOG_FROM_LINEAR_MEMORY, + log_function_ty, + Some(Linkage::External), + ); } fn emit_initializer(binary: &mut Binary, _ns: &ast::Namespace) { diff --git a/src/emit/soroban/target.rs b/src/emit/soroban/target.rs index cc50591f7..76dd8a398 100644 --- a/src/emit/soroban/target.rs +++ b/src/emit/soroban/target.rs @@ -3,7 +3,9 @@ use crate::codegen::cfg::HashTy; use crate::codegen::Expression; use crate::emit::binary::Binary; -use crate::emit::soroban::{SorobanTarget, GET_CONTRACT_DATA, PUT_CONTRACT_DATA}; +use crate::emit::soroban::{ + SorobanTarget, GET_CONTRACT_DATA, LOG_FROM_LINEAR_MEMORY, PUT_CONTRACT_DATA, +}; use crate::emit::ContractArgs; use crate::emit::{TargetRuntime, Variable}; use crate::emit_context; @@ -236,7 +238,65 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { /// Prints a string /// TODO: Implement this function, with a call to the `log` function in the Soroban runtime. - fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {} + fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) { + if string.is_const() && length.is_const() { + let msg_pos = bin + .builder + .build_ptr_to_int(string, bin.context.i64_type(), "msg_pos") + .unwrap(); + let msg_pos = msg_pos.const_cast(bin.context.i64_type(), false); + + let length = length.const_cast(bin.context.i64_type(), false); + + let eight = bin.context.i64_type().const_int(8, false); + let four = bin.context.i64_type().const_int(4, false); + let zero = bin.context.i64_type().const_int(0, false); + let thirty_two = bin.context.i64_type().const_int(32, false); + + // XDR encode msg_pos and length + let msg_pos_encoded = bin + .builder + .build_left_shift(msg_pos, thirty_two, "temp") + .unwrap(); + let msg_pos_encoded = bin + .builder + .build_int_add(msg_pos_encoded, four, "msg_pos_encoded") + .unwrap(); + + let length_encoded = bin + .builder + .build_left_shift(length, thirty_two, "temp") + .unwrap(); + let length_encoded = bin + .builder + .build_int_add(length_encoded, four, "length_encoded") + .unwrap(); + + let zero_encoded = bin.builder.build_left_shift(zero, eight, "temp").unwrap(); + + let eight_encoded = bin.builder.build_left_shift(eight, eight, "temp").unwrap(); + let eight_encoded = bin + .builder + .build_int_add(eight_encoded, four, "eight_encoded") + .unwrap(); + + let call_res = bin + .builder + .build_call( + bin.module.get_function(LOG_FROM_LINEAR_MEMORY).unwrap(), + &[ + msg_pos_encoded.into(), + length_encoded.into(), + msg_pos_encoded.into(), + four.into(), + ], + "log", + ) + .unwrap(); + } else { + todo!("Dynamic String printing is not yet supported") + } + } /// Return success without any result fn return_empty_abi(&self, bin: &Binary) { diff --git a/src/lib.rs b/src/lib.rs index 46abbf7e3..aac266aba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,12 +95,11 @@ impl Target { /// Size of a pointer in bits pub fn ptr_size(&self) -> u16 { - if *self == Target::Solana { + match *self { // Solana is BPF, which is 64 bit - 64 - } else { + Target::Solana => 64, // All others are WebAssembly in 32 bit mode - 32 + _ => 32, } } diff --git a/src/linker/soroban_wasm.rs b/src/linker/soroban_wasm.rs index 0a6be8bc6..6208a6947 100644 --- a/src/linker/soroban_wasm.rs +++ b/src/linker/soroban_wasm.rs @@ -11,8 +11,7 @@ use wasm_encoder::{ }; use wasmparser::{Global, Import, Parser, Payload::*, SectionLimited, TypeRef}; -use crate::emit::soroban::GET_CONTRACT_DATA; -use crate::emit::soroban::PUT_CONTRACT_DATA; +use crate::emit::soroban::{GET_CONTRACT_DATA, LOG_FROM_LINEAR_MEMORY, PUT_CONTRACT_DATA}; pub fn link(input: &[u8], name: &str) -> Vec { let dir = tempdir().expect("failed to create temp directory for linking"); @@ -82,7 +81,7 @@ fn generate_module(input: &[u8]) -> Vec { module.finish() } -/// Resolve all pallet contracts runtime imports +/// Resolve all soroban contracts runtime imports fn generate_import_section(section: SectionLimited, module: &mut Module) { let mut imports = ImportSection::new(); for import in section.into_iter().map(|import| import.unwrap()) { @@ -98,6 +97,7 @@ fn generate_import_section(section: SectionLimited, module: &mut Module) }; let module_name = match import.name { GET_CONTRACT_DATA | PUT_CONTRACT_DATA => "l", + LOG_FROM_LINEAR_MEMORY => "x", _ => panic!("got func {:?}", import), }; // parse the import name to all string after the the first dot diff --git a/src/sema/namespace.rs b/src/sema/namespace.rs index 0b78408b3..d56577f30 100644 --- a/src/sema/namespace.rs +++ b/src/sema/namespace.rs @@ -41,7 +41,7 @@ impl Namespace { value_length, } => (address_length, value_length), Target::Solana => (32, 8), - Target::Soroban => (32, 8), + Target::Soroban => (32, 64), }; let mut ns = Namespace { diff --git a/tests/soroban.rs b/tests/soroban.rs index 841a8dc8d..fee9e43e8 100644 --- a/tests/soroban.rs +++ b/tests/soroban.rs @@ -6,6 +6,7 @@ pub mod soroban_testcases; use solang::codegen::Options; use solang::file_resolver::FileResolver; use solang::{compile, Target}; +use soroban_sdk::testutils::Logs; use soroban_sdk::{vec, Address, Env, Symbol, Val}; use std::ffi::OsStr; @@ -27,7 +28,7 @@ pub fn build_solidity(src: &str) -> SorobanEnv { target, &Options { opt_level: opt.into(), - log_runtime_errors: false, + log_runtime_errors: true, log_prints: true, #[cfg(feature = "wasm_opt")] wasm_opt: Some(contract_build::OptimizationPasses::Z), @@ -74,6 +75,26 @@ impl SorobanEnv { println!("args_soroban: {:?}", args_soroban); self.env.invoke_contract(addr, &func, args_soroban) } + + /// Invoke a contract and expect an error. Returns the logs. + pub fn invoke_contract_expect_error( + &self, + addr: &Address, + function_name: &str, + args: Vec, + ) -> Vec { + let func = Symbol::new(&self.env, function_name); + let mut args_soroban = vec![&self.env]; + for arg in args { + args_soroban.push_back(arg) + } + + let _ = self + .env + .try_invoke_contract::(addr, &func, args_soroban); + + self.env.logs().all() + } } impl Default for SorobanEnv { diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index abe0ca498..080ab8938 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -1,3 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 mod math; +mod print; mod storage; diff --git a/tests/soroban_testcases/print.rs b/tests/soroban_testcases/print.rs new file mode 100644 index 000000000..8dc919906 --- /dev/null +++ b/tests/soroban_testcases/print.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::build_solidity; +use soroban_sdk::testutils::Logs; + +#[test] +fn log_runtime_error() { + let src = build_solidity( + r#"contract counter { + uint64 public count = 1; + + function decrement() public returns (uint64){ + count -= 1; + return count; + } + }"#, + ); + + let addr = src.contracts.last().unwrap(); + + let _res = src.invoke_contract(addr, "init", vec![]); + + src.invoke_contract(addr, "decrement", vec![]); + + let logs = src.invoke_contract_expect_error(addr, "decrement", vec![]); + + assert!(logs[0].contains("runtime_error: math overflow in test.sol:5:17-27")); +} + +#[test] +fn print() { + let src = build_solidity( + r#"contract Printer { + + function print() public { + print("Hello, World!"); + } + }"#, + ); + + let addr = src.contracts.last().unwrap(); + + let _res = src.invoke_contract(addr, "init", vec![]); + + src.invoke_contract(addr, "print", vec![]); + + let logs = src.env.logs().all(); + + assert!(logs[0].contains("Hello, World!")); +} + +#[test] +fn print_then_runtime_error() { + let src = build_solidity( + r#"contract counter { + uint64 public count = 1; + + function decrement() public returns (uint64){ + print("Second call will FAIL!"); + count -= 1; + return count; + } + }"#, + ); + + let addr = src.contracts.last().unwrap(); + + let _res = src.invoke_contract(addr, "init", vec![]); + + src.invoke_contract(addr, "decrement", vec![]); + + let logs = src.invoke_contract_expect_error(addr, "decrement", vec![]); + + assert!(logs[0].contains("Second call will FAIL!")); + assert!(logs[1].contains("Second call will FAIL!")); + assert!(logs[2].contains("runtime_error: math overflow in test.sol:6:17-27")); +}