Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ wasmparser = "=0.116.1"

# NB: When updating, also update the version in rs-soroban-env dev-dependencies
[workspace.dependencies.stellar-xdr]
version = "=27.0.0"
git = "https://github.com/stellar/rs-stellar-xdr"
rev = "a90de0c0d6eba8be668e9074cfb7b285ac0ecd34"
version = "=28.0.0"
# SPIKE: rs-stellar-xdr#552 (CAP-0084). Fork-hosted branch p28-cap-0084.
# Productionize -> stellar/rs-stellar-xdr merged SHA (or crates.io 28.0.0) once #552 lands.
git = "https://github.com/sisuresh/rs-stellar-xdr"
rev = "3d2ad659a3a285aae9543069ee18cb6245a427ce"
default-features = false
features = ["cap_0085_executable_ref"]
features = ["cap_0084_muxed_contract", "cap_0085_executable_ref"]

[workspace.dependencies.wasmi]
package = "soroban-wasmi"
Expand Down
5 changes: 4 additions & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,10 @@ allow-git = []

[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = ["stellar"]
# TEMP CAP-0084 SPIKE: "sisuresh" allows the rs-stellar-xdr#552 fork-rev pin
# (git+https://github.com/sisuresh/rs-stellar-xdr?rev=cf7fdedf7ca95fccf45c82e6829d14637c9b5e4c). Revert when the
# stellar-xdr dependency re-points to an accepted stellar/rs-stellar-xdr rev.
github = ["stellar", "sisuresh"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be removed

# 1 or more gitlab.com organizations to allow git sources for
# gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
Expand Down
4 changes: 3 additions & 1 deletion soroban-env-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ sha2 = "0.10.8"
hex-literal = "0.4.1"
hmac = "0.12.1"
# NB: We'll need to pin this again after switching the Core to the new env version.
ed25519-dalek = {version = ">=2.0.0", features = ["rand_core"] }
# ed25519-dalek 3.0.0 (2026-07-06) bumped its rand_core past the rand 0.8.5 /
# rand_chacha 0.3.1 pinned below, breaking `ChaCha20Rng: CryptoRng`. Cap <3.0.0.
ed25519-dalek = {version = ">=2.0.0, <3.0.0", features = ["rand_core"] }
# NB: this must match the same rand version used by ed25519-dalek above
rand = "0.8.5"
# NB: this must match the same rand_chacha version used by ed25519-dalek above
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
" 0 begin": "cpu:0, mem:0, prngs:-/-, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
" 1 call get_address_from_muxed_address(MuxedAddress(obj#1))": "cpu:442, mem:80, objs:-/1@a53ac37b",
" 2 ret get_address_from_muxed_address -> Ok(Address(obj#3))": "cpu:1110, mem:160, objs:-/2@4b6987b0",
" 3 call get_id_from_muxed_address(MuxedAddress(obj#1))": "",
" 4 ret get_id_from_muxed_address -> Ok(U64(456))": "cpu:1230",
" 5 end": "cpu:1338, mem:160, prngs:-/-, objs:-/2@4b6987b0, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-"
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
" 0 begin": "cpu:14285, mem:0, prngs:-/9b4a753, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
" 1 end": "cpu:14895, mem:80, prngs:-/9b4a753, objs:-/1@d3ad1bcd, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-"
}
4 changes: 4 additions & 0 deletions soroban-env-host/src/builtin_contracts/base_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,10 @@ impl TryFromVal<Host, ScAddress> for MuxedAddress {
let obj = env.add_host_object(MuxedScAddress(addr.clone()))?;
MuxedAddress::try_from_val(env, &obj)
}
ScAddress::MuxedContract(_) => {
let obj = env.add_host_object(MuxedScAddress(addr.clone()))?;
MuxedAddress::try_from_val(env, &obj)
}
_ => {
let obj = env.add_host_object(addr.clone())?;
MuxedAddress::try_from_val(env, &obj)
Expand Down
20 changes: 20 additions & 0 deletions soroban-env-host/src/builtin_contracts/testutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@ pub(crate) fn contract_id_to_address(host: &Host, contract_id: [u8; 32]) -> Addr
.unwrap()
}

// CAP-0084: build a muxed contract address (`ScAddress::MuxedContract`) wrapping
// the given contract id and multiplexing id, mirroring `TestSigner::muxed_address`
// for accounts.
pub(crate) fn muxed_contract_address(
host: &Host,
contract_id: [u8; 32],
mux_id: u64,
) -> MuxedAddress {
use soroban_env_common::xdr::MuxedContract;
let sc_address = ScAddress::MuxedContract(MuxedContract {
id: mux_id,
contract_id: ContractId(Hash(contract_id)),
});
MuxedAddress::try_from_val(
host,
&host.add_host_object(MuxedScAddress(sc_address)).unwrap(),
)
.unwrap()
}

#[derive(Clone)]
pub(crate) enum TestSigner {
AccountInvoker(AccountId),
Expand Down
7 changes: 7 additions & 0 deletions soroban-env-host/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ fn display_address(addr: &ScAddress, f: &mut std::fmt::Formatter<'_>) -> std::fm
};
write!(f, "{}", strkey)
}
// CAP-0084: muxed contracts have no canonical strkey form yet, so this
// is a diagnostic-only rendering. In practice muxed contract addresses
// are de-muxed before they reach events, making this arm unreachable.
ScAddress::MuxedContract(muxed_contract) => {
let strkey = stellar_strkey::Contract(muxed_contract.contract_id.0 .0);
write!(f, "{}:{}", strkey, muxed_contract.id)
}
// Note, that claimable balance and liquidity pool types can't normally
// appear in host, so we have the proper rendering for these here just
// for consistency (similar to e.g. non-representable ScVal types).
Expand Down
4 changes: 4 additions & 0 deletions soroban-env-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3756,6 +3756,9 @@ impl VmCallerEnv for Host {
)));
Ok(address)
}
ScAddress::MuxedContract(muxed_contract) => Ok(ScAddress::Contract(
muxed_contract.contract_id.metered_clone(self)?,
)),
_ => Err(self.err(
ScErrorType::Object,
ScErrorCode::InternalError,
Expand All @@ -3773,6 +3776,7 @@ impl VmCallerEnv for Host {
) -> Result<U64Val, Self::Error> {
let mux_id = self.visit_obj(muxed_address, |addr: &MuxedScAddress| match &addr.0 {
ScAddress::MuxedAccount(muxed_account) => Ok(muxed_account.id),
ScAddress::MuxedContract(muxed_contract) => Ok(muxed_contract.id),
_ => Err(self.err(
ScErrorType::Object,
ScErrorCode::InternalError,
Expand Down
3 changes: 3 additions & 0 deletions soroban-env-host/src/host/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,9 @@ impl Host {
ScAddress::MuxedAccount(_) => Ok(self
.add_host_object(MuxedScAddress(addr.metered_clone(self)?))?
.into()),
ScAddress::MuxedContract(_) => Ok(self
.add_host_object(MuxedScAddress(addr.metered_clone(self)?))?
.into()),
_ => Err(self.err(
ScErrorType::Object,
ScErrorCode::UnexpectedType,
Expand Down
1 change: 1 addition & 0 deletions soroban-env-host/src/host_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ impl HostObjectType for MuxedScAddress {
fn inject(self, host: &Host) -> Result<HostObject, HostError> {
match &self.0 {
xdr::ScAddress::MuxedAccount(_) => Ok(HostObject::MuxedAddress(self)),
xdr::ScAddress::MuxedContract(_) => Ok(HostObject::MuxedAddress(self)),
_ => Err(host.err(
ScErrorType::Object,
ScErrorCode::InvalidInput,
Expand Down
25 changes: 25 additions & 0 deletions soroban-env-host/src/test/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ fn test_muxed_address_to_components_conversion() {
assert_eq!(mux_id_val, ScVal::U64(123));
}

// CAP-0084: `get_address_from_muxed_address` / `get_id_from_muxed_address` on a
// muxed contract return the underlying contract address and the mux id.
#[test]
fn test_muxed_contract_to_components_conversion() {
use crate::xdr::MuxedContract;
let host = observe_host!(Host::test_host());
let muxed_address_obj = host
.add_host_object(MuxedScAddress(ScAddress::MuxedContract(MuxedContract {
id: 456,
contract_id: ContractId(Hash([20; 32])),
})))
.unwrap();
let address = host
.get_address_from_muxed_address(muxed_address_obj)
.unwrap();
let mux_id = host.get_id_from_muxed_address(muxed_address_obj).unwrap();
let address_val = host.from_host_val(address.into()).unwrap();
let mux_id_val = host.from_host_val(mux_id.into()).unwrap();
assert_eq!(
address_val,
ScVal::Address(ScAddress::Contract(ContractId(Hash([20; 32]))))
);
assert_eq!(mux_id_val, ScVal::U64(456));
}

#[test]
fn test_invalid_muxed_address_object_conversions() {
let host = observe_host!(Host::test_host());
Expand Down
136 changes: 136 additions & 0 deletions soroban-env-host/src/test/stellar_asset_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,142 @@ fn test_cap_67_transfer_with_muxed_accounts() {
);
}

#[test]
fn test_cap_84_transfer_with_muxed_contracts() {
use crate::builtin_contracts::testutils::muxed_contract_address;

let test = StellarAssetContractTest::setup(function_name!());
// Enable invocation metering to get the events to reset automatically on
// every contract call.
test.host.enable_invocation_metering();
let contract = test.default_stellar_asset_contract();

let user = TestSigner::account(&test.user_key);
test.create_default_account(&user);
test.create_default_trustline(&user);

// The destination is a contract; contracts hold SAC balances without a
// trustline. The de-muxed contract address is what the event/balance use.
let dst_contract_id = generate_bytes_array(&test.host);
let dst_address = contract_id_to_address(&test.host, dst_contract_id);

contract
.mint(
&TestSigner::account(&test.issuer_key),
user.address(&test.host),
100_000_000,
)
.unwrap();

let transfer_symbol = Symbol::try_from_small_str("transfer").unwrap().to_val();
let token_name = contract.name().unwrap();

// Transfer from a (non-muxed) account to a muxed contract destination.
contract
.transfer_muxed(
&user,
muxed_contract_address(&test.host, dst_contract_id, 0xCAFE),
9_999_999,
)
.unwrap();

assert_eq!(
test.host.get_contract_events().unwrap().0,
vec![contract.test_event(
test_vec![
&test.host,
transfer_symbol,
user.address(&test.host),
// `to` topic is the de-muxed contract address.
dst_address.clone(),
&token_name,
],
test_map![
&test.host,
("amount", 9_999_999_i128),
("to_muxed_id", 0xCAFE_u64)
]
.into()
)]
);
// Balance credits the underlying contract.
assert_eq!(contract.balance(dst_address.clone()).unwrap(), 9_999_999);
assert_eq!(
contract.balance(user.address(&test.host)).unwrap(),
90_000_001
);

// A plain (non-muxed) contract destination emits no `to_muxed_id`.
contract.transfer(&user, dst_address.clone(), 1).unwrap();
assert_eq!(
test.host.get_contract_events().unwrap().0,
vec![contract.test_event(
test_vec![
&test.host,
transfer_symbol,
user.address(&test.host),
dst_address.clone(),
&token_name,
],
1_i128.try_into_val(&test.host).unwrap()
)]
);
assert_eq!(contract.balance(dst_address).unwrap(), 10_000_000);
}

#[test]
fn test_cap_84_issuer_transfer_to_muxed_contract_emits_mint() {
use crate::builtin_contracts::testutils::muxed_contract_address;

// CAP-84 scopes muxed contract support to `transfer` only; the SAC `mint`
// function does not accept a muxed contract destination. The `mint` *event*
// is unchanged, however: an issuer-source `transfer` to a muxed contract
// destination still emits a `mint` event carrying `to_muxed_id`, following
// CAP-67's issuer semantics.
let test = StellarAssetContractTest::setup(function_name!());
test.host.enable_invocation_metering();
// The issuer is the default SAC administrator.
let issuer = TestSigner::account(&test.issuer_key);
let contract = test.default_stellar_asset_contract();

let dst_contract_id = generate_bytes_array(&test.host);
let dst_address = contract_id_to_address(&test.host, dst_contract_id);

let mint_symbol = Symbol::try_from_small_str("mint").unwrap().to_val();
let token_name = contract.name().unwrap();

// Transfer from the issuer to a muxed contract destination. Because the
// source is the issuer, this emits a `mint` event rather than a `transfer`.
contract
.transfer_muxed(
&issuer,
muxed_contract_address(&test.host, dst_contract_id, 7_654_321),
100_000_000,
)
.unwrap();

assert_eq!(
test.host.get_contract_events().unwrap().0,
vec![contract.test_event(
test_vec![
&test.host,
mint_symbol,
// `to` topic is the de-muxed contract address.
dst_address.clone(),
&token_name,
],
test_map![
&test.host,
("amount", 100_000_000_i128),
("to_muxed_id", 7_654_321_u64)
]
.into()
)]
);
// Balance credits the underlying contract.
assert_eq!(contract.balance(dst_address).unwrap(), 100_000_000);
}

#[test]
fn test_native_self_transfer() {
let test = StellarAssetContractTest::setup(function_name!());
Expand Down
Loading
Loading