Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,8 @@ jobs:
CARGO_TERM_COLOR: always
run: |
# --lib: unit tests only (excludes slow WASM integration tests).
# Soroban crates are tested in the dedicated Soroban job with their
# newer toolchain; exclude them from the default 1.86 coverage build.
# Soroban crates are tested in the dedicated Soroban job; exclude them
# from the default workspace coverage build.
cargo llvm-cov nextest --workspace --lib --lcov --output-path coverage.lcov \
--exclude templar-soroban-blend-adapter \
--exclude templar-soroban-governance \
Expand Down
54 changes: 54 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"contract/proxy-oracle/soroban/common",
"contract/proxy-oracle/soroban/integration-tests",
"contract/proxy-oracle/soroban/sep40-adapter-contract",
"contract/vault-counterparty",
"contract/redstone-adapter",
"contract/registry",
"contract/vault/macros",
Expand All @@ -27,6 +28,7 @@ members = [
"contract/vault/soroban",
"contract/vault/soroban/shared-types",
"contract/vault/soroban/blend-adapter",
"contract/vault/soroban/hot-bridge-adapter",
"contract/vault/soroban/governance",
"contract/vault/soroban/share-token",
"fuzz",
Expand Down Expand Up @@ -67,6 +69,7 @@ derive_more = { version = "1", default-features = false, features = [
"is_variant",
] }
getrandom = { version = "0.2", features = ["custom"] }
getset = "0.1.6"
hex = { version = "0.4.3", features = ["serde"] }
hex-literal = "0.4"
itertools = "0.14.0"
Expand Down
31 changes: 31 additions & 0 deletions contract/vault-counterparty/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Vault counterparty spike environment
# Mainnet flow from `just help`:
# 1) just bootstrap
# 2) just bridge-preflight
# 3) perform Stellar -> HOT locker deposit so omni asset mints to COUNTERPARTY_ACCOUNT on NEAR
# 4) just smoke-bridge

# Required for deploy + init + bridge smoke flow
COUNTERPARTY_ACCOUNT=your-counterparty.near
OWNER_ACCOUNT=your-owner.near
CURATOR_ACCOUNT=your-curator.near
NEAR_MARKET=your-market.near

# Stellar destination used by `withdraw_to_stellar`
STELLAR_RECEIVER=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

# HOT omni asset expected to appear on NEAR after Stellar locker deposit completes
OMNI_TOKEN_ID=1100_CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

# Required only for the synthetic funding path (`just fund` / `just smoke-synthetic`)
FUNDER_ACCOUNT=your-funder.near

# Required only when running `just withdraw` / `just smoke-bridge`
# Set this after confirming Stellar locker backing liquidity for the withdraw leg
WITHDRAW_LIQUIDITY_ACK=YES

# Optional overrides (defaults shown)
NETWORK_ID=mainnet
OMNI_CONTRACT=v2_1.omni.hot.tg
STORAGE_DEPOSIT_NEAR=0.25
CALL_GAS=100000000000000
21 changes: 21 additions & 0 deletions contract/vault-counterparty/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
edition.workspace = true
license.workspace = true
name = "templar-vault-counterparty-contract"
repository.workspace = true
version = "0.1.0"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk.workspace = true
bs58 = "0.5"
stellar-xdr = { version = "24.0", default-features = false, features = ["curr", "std"] }

[dev-dependencies]
near-sdk = { workspace = true, features = ["unit-testing"] }
proptest = "1.4"

[lints]
workspace = true
94 changes: 94 additions & 0 deletions contract/vault-counterparty/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Vault Counterparty Spike (NEAR)

This crate is a spike for the HOT Bridge counterparty described in the Stellar <-> NEAR design note.

## What this verifies

- Curator-only methods build the expected outbound calls to the configured `omni_contract`:
- `mt_transfer_call(receiver_id, token_id, amount, msg)` with `msg = "\"Supply\""` for market deposits
- `intents.near mt_withdraw(token, receiver_id, token_ids, amounts, msg)` for HOT withdrawals
- External Intents operations are recorded with an operation id, `Pending` status, and a private
callback that marks them `Succeeded` or `Failed`; retries are allowed only from `Failed`.
- The market-like adapter entrypoint accepts vault supply through `intents.near mt_transfer_call`
with token id `nep245:<omni_contract>:<raw_hot_token_id>` and `msg = "\"Supply\""`.
- HOT asset transfer calls attach exactly `1 yoctoNEAR`; market queue calls attach no deposit.
- `forward_to_market` always uses the preconfigured `near_market` receiver from init and credits
the counterparty's market supply position.
- Curator-only market withdrawal helpers let the counterparty enter, cancel, and execute the
market supply withdrawal queue before returned assets are withdrawn to Stellar.
- `withdraw_to_stellar` always uses the contract's configured `stellar_receiver` and only spends
the counterparty's current Intents balance.
- `token_id` is configured (`omni_token_id`) and runtime methods do not accept token overrides.
- Critical routing config is changed through owner-only two-step updates.
- The Intents contract and bridge refuel account are versioned state, initialized to the production
defaults and changed through owner-only two-step updates.
- `hot_deposit_receiver_hex` is explicit init config. It must come from the verified HOT receiver
bytes instead of local recomputation.
- No runtime callback `msg` input is exposed in this spike (no arbitrary action payloads).
- Security guards in the spike contract:
- Reject `amount == 0`
- Reject empty `token_id` at initialization
- Cap `token_id` and `stellar_receiver` length
- Two-step owner transfer (`propose_owner` -> `accept_owner`)
- One-yocto deposit required for owner role/config operations
- Owner pause blocks forward and HOT withdrawal operations
- Minimum gas budget required before creating HOT withdrawal promises

The tests inspect emitted mock receipts directly, so method names, JSON arg shapes, gas, and deposit are all checked.

## Market exit flow

When funds have been forwarded to the market, they must be brought back to the counterparty before
`withdraw_to_stellar` can bridge them out:

1. `request_market_withdrawal(amount)` enters the counterparty's market supply position into the
withdrawal queue.
2. `execute_market_withdrawal(batch_limit)` advances the market withdrawal queue and transfers
available borrow assets back to the counterparty.
3. `withdraw_to_stellar(amount)` withdraws the returned Intents balance through HOT.

When the vault uses this contract as the Stellar market adapter, the flow is:

1. Vault allocation calls `intents.near mt_transfer_call` into this adapter.
2. The adapter records a vault supply position and calls `intents.near mt_withdraw` to hand the
wrapped HOT asset to `bridge-refuel.hot.tg`.
3. Vault withdrawal calls the adapter's market withdrawal queue methods.
4. Once HOT has returned assets to the adapter's Intents balance, `execute_next_supply_withdrawal_request`
transfers the Intents-wrapped token back to the vault.

## Soroban adapter compatibility

`stellar_receiver` is a plain string provided at init. For this spike, that lets you choose either:

- A real Soroban adapter address (for example from `upstream/feat/soroban-curator-kernel`)
- A mock placeholder receiver during local testing

`near_market`, `omni_token_id`, `omni_contract`, and HOT receiver config are set at init and can be
rotated only through owner-controlled two-step config updates, so curator cannot route to arbitrary
receivers or switch bridged assets at call time.

## Run

```bash
cargo test -p templar-vault-counterparty-contract
```

## Operations

Use the local justfile for deployment, bridge smoke flows, and HOT/Stellar helper operations:

```bash
just -f contract/vault-counterparty/justfile help
just -f contract/vault-counterparty/justfile stellar-deposit <amount>
just -f contract/vault-counterparty/justfile monitor-withdrawal <nonce>
```

If a tool needs `STELLAR_SECRET_KEY` instead of a Stellar CLI identity name, load it from the local
Stellar keystore without printing it:

```bash
source contract/vault-counterparty/scripts/load-stellar-secret-env.sh
```

Do not use funding-bridge scripts for counterparty or HOT bridge operations; this crate owns the
counterparty deployment and Stellar locker helper flow.
Loading
Loading