Skip to content
Merged
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: 5 additions & 3 deletions contracts/vc-vault-factory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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.

---
Expand Down
21 changes: 14 additions & 7 deletions contracts/vc-vault-factory/src/contract.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
Loading