diff --git a/contracts/vc-vault-factory/README.md b/contracts/vc-vault-factory/README.md index 4492dd0..b7c4faf 100644 --- a/contracts/vc-vault-factory/README.md +++ b/contracts/vc-vault-factory/README.md @@ -27,7 +27,7 @@ Soroban smart contract that deploys and tracks **single-tenant `vc-vault` instan | `deploy_sponsored(deployer, owner, did_uri, salt)` | `deployer` | Deploy a vault on behalf of `owner`. The deployer signs and pays; the vault belongs to `owner` from creation. Anyone can be a deployer — no whitelist. | | `is_vault(vault_address)` | none | Return `true` if `vault_address` was deployed by this factory. Used by `receive_push` inside vaults to validate transfer sources. | -`salt` is a `BytesN<32>` chosen by the caller. Internally the factory derives the actual deploy salt as `keccak256(user_salt ‖ owner_bytes)`, which makes vault addresses deterministic per `(owner, salt)` pair and prevents frontrunning. +`salt` is a `BytesN<32>` chosen by the caller. Internally the factory derives the actual deploy salt as `keccak256(user_salt ‖ XDR(owner))`, which makes vault addresses deterministic per `(owner, salt)` pair and prevents frontrunning. The vault constructor called by `deploy_v2` receives `(owner, contract_admin, did_uri, factory_address)`. The factory address is stored inside each vault so it can call `is_vault` during `receive_push`. @@ -59,10 +59,12 @@ Persistent entries for deployed vault addresses are extended on `set_deployed` a Vault addresses are deterministic and unique per `(owner, salt)` pair: ``` -deploy_salt = keccak256(user_salt || owner_address_bytes) -vault_address = hash(factory_address || deploy_salt) +deploy_salt = keccak256( user_salt (32 bytes) || XDR(owner) ) +vault_address = hash( factory_address || deploy_salt ) ``` +`XDR(owner)` is the canonical XDR serialization of the owner `Address` (i.e. `Address.toXDR(env)` on-chain / the equivalent ScAddress XDR encoding off-chain) — **not** its StrKey display string. A client precomputing a vault address must hash the raw XDR bytes of the owner address, not the `"G..."`/`"C..."` text. + Two different owners using the same user salt get different vault addresses. The same owner using different salts also gets different addresses. This means a vault address can be pre-computed client-side before submitting a transaction. --- diff --git a/contracts/vc-vault-factory/src/contract.rs b/contracts/vc-vault-factory/src/contract.rs index 2733bd3..034fca6 100644 --- a/contracts/vc-vault-factory/src/contract.rs +++ b/contracts/vc-vault-factory/src/contract.rs @@ -1,5 +1,6 @@ use soroban_sdk::{ - contract, contractclient, contractimpl, Address, Bytes, BytesN, Env, IntoVal, String, + contract, contractclient, contractimpl, xdr::ToXdr, Address, Bytes, BytesN, Env, IntoVal, + String, }; use crate::{events, storage}; @@ -32,12 +33,18 @@ impl VaultFactoryContract { } fn derive_salt(e: &Env, user_salt: BytesN<32>, owner: &Address) -> BytesN<32> { - // Salt = keccak256(user_salt || owner_bytes) — prevents frontrunning. - let mut owner_bytes: [u8; 56] = [0; 56]; - owner.to_string().copy_into_slice(&mut owner_bytes); - let mut salt_bytes: Bytes = user_salt.into_val(e); - salt_bytes.extend_from_array(&owner_bytes); - e.crypto().keccak256(&salt_bytes).into() + // deploy_salt = keccak256( user_salt(32 bytes) || XDR(owner) ) + // + // Mixing the owner into the salt binds the deterministic vault address to a + // specific owner (so a sponsored deploy lands on the same address the owner + // would compute, and two owners can't collide on one address). The owner is + // serialized via its canonical XDR form rather than to_string(): XDR is the + // stable wire encoding an off-chain client reproduces directly, and it + // matches the preimage documented in the README. to_string() is a display + // (StrKey) encoding and ties determinism to a fixed 56-byte assumption. + let mut preimage: Bytes = user_salt.into_val(e); + preimage.append(&owner.clone().to_xdr(e)); + e.crypto().keccak256(&preimage).into() } fn deploy_vault(e: &Env, owner: &Address, did_uri: String, user_salt: BytesN<32>) -> Address {