Skip to content

feat: added sui support - #633

Draft
frolvanya wants to merge 2 commits into
mainfrom
feat/sui
Draft

feat: added sui support#633
frolvanya wants to merge 2 commits into
mainfrom
feat/sui

Conversation

@frolvanya

Copy link
Copy Markdown
Contributor

Adds the Sui side of the Omni Bridge: a Move package mirroring the
Aptos/Starknet contracts, plus the NEAR-side type wiring and CI.

What's inside

  • sui/ Move package:
    • init_transfer — send tokens Sui → NEAR: locks native coins in bridge
      custody (or burns bridge-deployed ones), collects an optional SUI
      native fee, and emits an InitTransfer event for the NEAR MPC to read
      (no Wormhole).
    • fin_transfer — receive tokens NEAR → Sui: verifies the NEAR MPC
      secp256k1 signature over the borsh payload, replay-protected by a
      destination-nonce bitmap, then unlocks or mints to the recipient.
    • deploy_token / log_metadata — token registration in both
      directions. Sui can't create coins at runtime (one-time-witness rule),
      so NEAR-originated tokens are published from sui/token_template/ and
      bound to the MPC-signed metadata payload (zero-supply TreasuryCap +
      surrendered UpgradeCap + metadata equality checks).
    • Role-based admin (Admin/Pauser/MetadataAdmin), pause flags, and a
      version-gated shared state with migrate for Sui's package-upgrade
      model.
  • Sui-specific wire format: coins are types, not addresses, so the 32-byte
    token id is keccak256(canonical coin type string); payload layout is
    byte-identical to Aptos. Chain id = 14 (ChainKind::Sui), decimals
    clamped to 9.
  • NEAR-side wiring in omni-types/omni-bridge: ChainKind::Sui,
    OmniAddress::Sui(H256), token prefix, native-token constant, tests.
    (Event parsers / mpc-prover dispatch are follow-ups, blocked on near/mpc
    adding Sui read support.)
  • CI: sui.yaml (build + test) and Aptos/Sui release artifacts in
    update-contracts.yaml.

Testing

  • 86 Move unit tests, including byte-exact borsh layout checks and real
    secp256k1 signature vectors (positive and negative), generated offline
    against an independent encoder implementation.
  • Full e2e smoke test on a local Sui network: publish → initialize →
    log_metadata → init_transfer (lock) → signed fin_transfer (unlock) →
    replay correctly rejected → template publish → deploy_token →
    fin_transfer mint → init_transfer burn. Lock/unlock and mint/burn paths
    both verified on-chain, and event token_address values matched the
    offline keccak ids exactly.
  • A test version is deployed on Sui testnet (initialized with a test MPC
    key — not for production use):

See sui/README.md for the trust model, the documented deploy_token
front-running trade-off, and the deployment guide in sui/deployment.md.

@frolvanya frolvanya left a comment

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.

A bit more information regarding deployment, testing and multisig:

Installation

https://docs.sui.io/getting-started/onboarding/sui-install#quick-install

$ curl -sSfL https://raw.githubusercontent.com/Mystenlabs/suiup/main/install.sh | sh
$ suiup install sui@testnet # or mainnet
$ suiup default set sui@testnet # or mainnet

Some usefull initial commands:

$ sui client active-address
$ sui client faucet # or https://faucet.suilearn.io/
$ sui client balance

Package deployment

  1. First we need to publish package:
$ sui client publish
  1. Then call initialize:
sui client call --package <Package address> --module omni_bridge --function initialize --args <BridgeState> <NEAR MPC derived address (20 bytes)> 14

Note

Both package address and bridge state could be found after executing first command (sui client publish)
The 14 is the ChainKind::Sui discriminant; a wrong value can be corrected later with set_chain_id (admin).

Calling methods

$ sui client ptb --split-coins gas "[1000000]" --assign pay --move-call 0x2::coin::zero "<0x2::sui::SUI>" --assign zero_fee --move-call <Package address>::omni_bridge::init_transfer "<0x2::sui::SUI>" @<BridgeState> pay.0 0 zero_fee '"near:frolik.testnet"' "vector[]"

# https://testnet.suivision.xyz/txblock/HcrYCissQ4qS1bF5DXa5zmejYiHVwupPWMuq1t9eYiot?tab=Overview

Multisig

https://docs.sui.io/develop/transactions/transaction-auth/multisig

/// Sui coins are types, not addresses: for tokens this carries
/// `keccak256(canonical coin type string)`; for accounts, the native
/// 32-byte Sui address.
pub type SuiAddress = H256;

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.

Sui treats coins differently as in Aptos, I decided to keep H256 for more similarity between move-related chains and store hash as an address, but maybe it's more transparent to use bounded string here

Comment thread sui/sources/utils.move
signature: &vector<u8>,
expected_address: &vector<u8>,
) {
assert!(signature.length() == 65, E_INVALID_SIGNATURE_LENGTH);

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.

FYI, signature of this method is a bit different from Aptos, since we pass v as a last byte

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class Sui support to Omni Bridge by introducing a new Sui Move contract package (plus a per-token template package), extensive Move unit tests for payload/signature/bridge flows, NEAR-side type wiring for ChainKind::Sui / OmniAddress::Sui, and CI workflows to build/test and package artifacts.

Changes:

  • Introduce sui/ Move package implementing bridge flows (init_transfer, fin_transfer, deploy_token, metadata logging/updates) and shared utilities (borsh encoders, signature verification, coin identity helpers).
  • Add comprehensive Sui Move unit tests validating borsh layouts, secp256k1 signature recovery, nonce bitmap replay protection, and deploy/mint/burn + lock/unlock scenarios.
  • Wire Sui into NEAR-side enums/types/tests and extend CI to build/test Sui and include Sui/Aptos artifacts in contract update workflow.

Reviewed changes

Copilot reviewed 24 out of 26 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
sui/token_template/sources/template_coin.move Per-token Move template used to publish bridged NEAR-originated coins on Sui.
sui/token_template/Move.toml Move manifest for the token template package.
sui/token_template/Move.lock Locked Sui framework dependencies for the token template package.
sui/token_template/.gitignore Ignore build artifacts for the token template package.
sui/tests/utils_tests.move Unit tests for signature verification, decimals clamping, and coin identity helpers.
sui/tests/test_coin.move Test-only coin helper to create real TreasuryCap/CoinMetadata fixtures for deploy flows.
sui/tests/omni_bridge_tests.move End-to-end unit tests for init/fin transfer, replay protection, deploy_token, metadata updates, roles/pause/version gating.
sui/tests/bridge_types_tests.move Byte-exact tests for Sui-side borsh layouts of metadata/transfer payloads.
sui/tests/borsh_tests.move Unit tests for borsh helper encoders (byte vec + string).
sui/sources/utils.move Core utilities: secp256k1 “Ethereum-style” signature verification + type-id/token-id helpers.
sui/sources/omni_bridge.move Main Sui bridge contract: shared state, roles, pause flags, transfers, deploy_token, events, and views.
sui/sources/bridge_types.move Cross-chain payload structs and borsh serialization mirroring sibling chains.
sui/sources/borsh.move Borsh sequence encoders (u32-LE length prefix) used by payload encoding.
sui/README.md Sui-specific documentation: trust model, token identity, deploy flows, residual risks, and testing guide.
sui/Move.toml Move manifest for the main OmniBridge Sui package.
sui/Move.lock Locked Sui framework dependencies for the main Sui package.
sui/CLAUDE.md Developer documentation for architecture, invariants, and module layout of the Sui contract.
sui/.gitignore Ignore build artifacts and local Published.toml to keep signature tests stable.
near/omni-types/src/tests/lib_test.rs Extend enum-stability and native-token tests to cover ChainKind::Sui / OmniAddress::Sui.
near/omni-types/src/lib.rs Add ChainKind::Sui, OmniAddress::Sui(H256), and native SUI token-id constant wiring.
near/omni-tests/src/omni_token.rs Include Sui factory address in omni-token integration test routing.
near/omni-tests/src/helpers.rs Add helper returning a canonical test Sui factory OmniAddress.
near/omni-bridge/src/lib.rs Teach origin-chain detection to recognize “sui” prefixes.
.github/workflows/update-contracts.yaml Add Aptos/Sui CLI install + build steps and package Sui/Aptos artifacts for releases.
.github/workflows/sui.yaml New CI workflow to install Sui CLI and run build/tests for sui/ + build token template.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sui/sources/utils.move
Comment on lines +50 to +56
let mut sig = *signature;
let v = &mut sig[64];
if (*v >= 27) {
*v = *v - 27;
};

let compressed = ecdsa_k1::secp256k1_ecrecover(&sig, message_bytes, 0);

@frolvanya frolvanya Jul 6, 2026

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.

Technically Aptos has the same "flaw":

let recovery_id = if (v >= 27) { v - 27 }
else { v };

and starknet has the loosest check:
fn _verify_borsh_signature(
ref self: ContractState, borsh_bytes: @ByteArray, signature: Signature,
) {
let message_hash_le = compute_keccak_byte_array(borsh_bytes);
let message_hash = reverse_u256_bytes(message_hash_le);
let sig = signature_from_vrs(signature.v, signature.r, signature.s);
verify_eth_signature(message_hash, sig, self.omni_bridge_derived_address.read());
}

But I'm not sure how critical is that
cc @karim-en

@frolvanya
frolvanya changed the base branch from feat/bumped-rust-version-contracts to main July 21, 2026 22:24
@frolvanya

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Pull request overview

Adds the Sui side of the Omni Bridge: a Move package (sui/) mirroring the Aptos/Starknet contracts, plus a frozen per-token token_template, the NEAR-side type wiring for ChainKind::Sui / OmniAddress::Sui, and CI (sui.yaml + release-artifact steps). The NEAR→Sui (inbound-to-Sui) path is complete and signature-authorized; the Sui→NEAR prover dispatch is explicitly deferred as a follow-up (blocked on near/mpc adding Sui read support).

I cross-checked the payload encoding, signature scheme, and enum wiring against the Aptos sibling and the NEAR encode_hashable path. Everything lines up.

Changes:

  • New sui/ Move package: init_transfer (lock/burn + optional SUI fee), fin_transfer (signature-verified unlock/mint, nonce-bitmap replay protection), deploy_token (binds a pre-published TreasuryCap to a signed MetadataPayload), log_metadata(_registry), role/pause/version gating, migrate.
  • Borsh payload encoders byte-identical to Aptos; secp256k1 verify_eth_signature adapted to Sui's ecdsa_k1 (raw message, keccak internal via flag 0).
  • NEAR wiring: ChainKind::Sui = 14 appended, TryFrom<u8> arm, all exhaustive is_evm/utxo/svm_chain + OmniAddress arms, get_token_origin_chain prefix, native-token constant, tests.
  • CI: sui.yaml (build/test + template build), Aptos/Sui artifacts in update-contracts.yaml.

Reviewed changes

Per-file summary
File Description
near/omni-types/src/lib.rs ChainKind::Sui/OmniAddress::Sui variants + all arms; native SUI token-id constant
near/omni-types/src/tests/lib_test.rs Enum-stability, from_str/display, native-token coverage for Sui
near/omni-bridge/src/lib.rs get_token_origin_chain recognizes sui prefix
near/omni-tests/src/{helpers,omni_token}.rs Sui factory address + integration-test routing
sui/sources/omni_bridge.move Main contract: state, roles, pause, transfers, deploy_token, events, views
sui/sources/bridge_types.move MetadataPayload/TransferMessagePayload + borsh encoders
sui/sources/borsh.move u32-LE length-prefixed sequence encoders
sui/sources/utils.move secp256k1 eth-sig verify, decimals clamp (9), coin-type id helpers
sui/tests/*.move 86 unit tests incl. byte-exact borsh + real secp256k1 vectors
sui/token_template/** Frozen per-token coin template for deploy_token
sui/{CLAUDE,README}.md, .gitignore, Move.{toml,lock} Docs + manifests
.github/workflows/sui.yaml New Sui build/test CI
.github/workflows/update-contracts.yaml Aptos/Sui CLI install + release artifacts

Findings

Verified and correct:

  • Payload byte-layout matches NEAR. transfer_message_to_borsh reproduces TransferMessagePayloadV1/full exactly: prefix, u64 dest_nonce, u8 origin_chain, u64 origin_nonce, dual interleaved chain_id tag before token_address/recipient (32B each), u128 amount, tagged Option<String> fee_recipient, untagged message. The OmniAddress::Sui enum tag = 14 = the configured chain_id, so the two interleaved bytes agree with NEAR's borsh output. Empty-message → no message bytes matches NEAR's V1 branch.
  • Signature scheme is consistent with the fleet. secp256k1_ecrecover(sig, msg, 0) hashes internally with keccak256, so recovering over keccak256(borsh) matches the NEAR MPC keccak256(borsh) signing and Aptos's keccak256(message_bytes) recover. fin_transfer<T>/deploy_token<T> verify before any mint/unlock, and the payload binds T (via token_address<T>()), recipient, amount, nonces, and chain_id.
  • Replay + gating. Nonce checked-and-marked before signature verify (a failed verify aborts and reverts the mark); assert_version on every entry; initialize/setters admin-gated; set_near_bridge_derived_address/set_chain_id admin-only; last-admin guard; deploy_token binding checks (zero supply, v1 UpgradeCap for the defining package then made immutable, metadata equality).
  • Decimals contract. DeployToken propagates both decimals = min(origin,9) and origin_decimals, and the bound CoinMetadata decimals must equal the clamped value.

Non-blocking (follow-ups / suggestions):

  • near/omni-types/src/lib.rs:995 + near/omni-types/src/tests/lib_test.rs — the native-SUI token-id constant (0x669638…df700c) is only asserted against itself (the test hardcodes the same bytes the function returns), so a wrong keccak256(b"00…02::sui::SUI") wouldn't be caught. I couldn't compute keccak256 in this environment to confirm the value. Not a fund-safety issue (the Sui contract derives token_address<SUI>() at runtime, and a wrong NEAR constant only misroutes native SUL and is admin-recoverable), but worth an independent derivation check.
  • near/omni-prover/mpc-omni-prover/src/lib.rs:155 — the _ => arm in verify_callback treats any non-Strk/non-Aptos chain as EVM. With ChainKind::Sui now existing, a future Sui prover instance would silently be parsed as an EVM log rather than rejected. Not reachable today (the inbound Sui prover is deliberately deferred and configuring such an instance is an admin action), but when the Sui→NEAR path lands, add an explicit Sui arm (or explicit reject) so Sui isn't misclassified as EVM.

✅ Approved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants