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
401 changes: 401 additions & 0 deletions .github/workflows/contracts-ci.yml

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ target
# Local settings
.soroban
.stellar

# Build artifacts (WASM binaries) — not committed; regenerated from source
artifacts/*.wasm
artifacts/*.sha256

# Keep build-info.json and deployment records in version control
!artifacts/build-info.json
!deployments/*.json
1 change: 1 addition & 0 deletions contracts/artifacts/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This directory stores build artifacts.
1 change: 1 addition & 0 deletions contracts/deployments/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This directory stores deployment records.
4 changes: 4 additions & 0 deletions contracts/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.82.0"
targets = ["wasm32-unknown-unknown"]
components = ["rustfmt", "clippy"]
120 changes: 120 additions & 0 deletions contracts/scripts/build-verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# build-verify.sh — Deterministic build and artifact verification for the escrow contract.
#
# Outputs:
# contracts/artifacts/escrow.wasm — optimised WASM binary
# contracts/artifacts/escrow.wasm.sha256 — SHA-256 hash of the binary
# contracts/artifacts/build-info.json — build provenance record
#
# Exit codes:
# 0 — success
# 1 — build or verification failure

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONTRACTS_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ARTIFACTS_DIR="$CONTRACTS_ROOT/artifacts"
WASM_RELEASE_PATH="$CONTRACTS_ROOT/contracts/escrow/target/wasm32-unknown-unknown/release/escrow.wasm"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log() { echo "[build-verify] $*"; }
fail() { echo "[build-verify] ERROR: $*" >&2; exit 1; }

# ---------------------------------------------------------------------------
# Toolchain validation
# ---------------------------------------------------------------------------
log "Validating Rust toolchain..."
RUST_VERSION=$(rustc --version 2>/dev/null) \
|| fail "rustc not found. Install Rust from https://rustup.rs"
log " rustc: $RUST_VERSION"

CARGO_VERSION=$(cargo --version 2>/dev/null) \
|| fail "cargo not found."
log " cargo: $CARGO_VERSION"

rustup target list --installed 2>/dev/null | grep -q "wasm32-unknown-unknown" \
|| fail "wasm32-unknown-unknown target not installed. Run: rustup target add wasm32-unknown-unknown"

# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
log "Building escrow contract (release, wasm32-unknown-unknown)..."
cd "$CONTRACTS_ROOT"

# Use RUSTFLAGS for reproducibility: no embedded paths, no timestamps
RUSTFLAGS="-C strip=symbols" \
cargo build \
--manifest-path "$CONTRACTS_ROOT/Cargo.toml" \
--package escrow \
--release \
--target wasm32-unknown-unknown \
--locked \
|| fail "cargo build failed"

[ -f "$WASM_RELEASE_PATH" ] || fail "WASM artifact not found at $WASM_RELEASE_PATH"

# ---------------------------------------------------------------------------
# Artifact staging
# ---------------------------------------------------------------------------
mkdir -p "$ARTIFACTS_DIR"
cp "$WASM_RELEASE_PATH" "$ARTIFACTS_DIR/escrow.wasm"
WASM_SIZE=$(wc -c < "$ARTIFACTS_DIR/escrow.wasm")
log "WASM artifact staged: $ARTIFACTS_DIR/escrow.wasm ($WASM_SIZE bytes)"

# ---------------------------------------------------------------------------
# Hash computation
# ---------------------------------------------------------------------------
log "Computing SHA-256 hash..."
if command -v sha256sum >/dev/null 2>&1; then
WASM_HASH=$(sha256sum "$ARTIFACTS_DIR/escrow.wasm" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
WASM_HASH=$(shasum -a 256 "$ARTIFACTS_DIR/escrow.wasm" | awk '{print $1}')
else
fail "Neither sha256sum nor shasum found. Cannot compute artifact hash."
fi

echo "$WASM_HASH escrow.wasm" > "$ARTIFACTS_DIR/escrow.wasm.sha256"
log " WASM hash: $WASM_HASH"

# ---------------------------------------------------------------------------
# Build provenance record
# ---------------------------------------------------------------------------
GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
GIT_TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
GIT_DIRTY=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SOROBAN_SDK_VERSION=$(grep -A1 'name = "soroban-sdk"' "$CONTRACTS_ROOT/Cargo.lock" \
| grep version | head -1 | sed 's/.*"\(.*\)".*/\1/' || echo "unknown")

cat > "$ARTIFACTS_DIR/build-info.json" <<EOF
{
"contract": "escrow",
"build_timestamp": "$BUILD_TIMESTAMP",
"git_commit": "$GIT_COMMIT",
"git_tag": "$GIT_TAG",
"git_dirty": $GIT_DIRTY,
"rust_version": "$RUST_VERSION",
"cargo_version": "$CARGO_VERSION",
"soroban_sdk_version": "$SOROBAN_SDK_VERSION",
"wasm_hash_sha256": "$WASM_HASH",
"wasm_size_bytes": $WASM_SIZE,
"toolchain_file": "contracts/rust-toolchain.toml"
}
EOF

log "Build provenance saved: $ARTIFACTS_DIR/build-info.json"

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
log ""
log "Build complete."
log " Artifact : $ARTIFACTS_DIR/escrow.wasm"
log " Hash : $WASM_HASH"
log " Commit : $GIT_COMMIT"
log ""
log "To deploy to Testnet: ./scripts/deploy-testnet.sh"
log "To deploy to Mainnet: ./scripts/deploy-mainnet.sh"
237 changes: 237 additions & 0 deletions contracts/scripts/deploy-mainnet.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# deploy-mainnet.sh — Deploy the escrow contract to Stellar Mainnet.
#
# MAINNET DEPLOYMENT REQUIRES EXPLICIT MANUAL APPROVAL.
# This script will not proceed without the operator typing "CONFIRM MAINNET DEPLOY".
# In CI, set MAINNET_DEPLOY_CONFIRMED=yes to bypass the interactive prompt
# (only after a human approval gate step has passed).
#
# Prerequisites:
# - Run ./scripts/build-verify.sh first (or set SKIP_BUILD=1)
# - stellar CLI available in PATH
# - MAINNET_DEPLOYER_SECRET set in environment (isolated from Testnet credentials)
#
# Environment variables:
# MAINNET_DEPLOYER_SECRET — Stellar secret key (required, never commit)
# MAINNET_DEPLOY_CONFIRMED — Set to "yes" to bypass interactive confirmation (CI only)
# SKIP_BUILD — Set to 1 to skip build
# SOROBAN_RPC_URL_MAINNET — Override RPC (default: Mainnet Horizon Soroban RPC)
# INIT_ADMIN / INIT_CLIENT / INIT_FREELANCER / INIT_ARBITER / INIT_TOKEN
# — Initialization parameters (optional)
#
# Outputs:
# contracts/deployments/mainnet-<timestamp>.json — immutable deployment record

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONTRACTS_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ARTIFACTS_DIR="$CONTRACTS_ROOT/artifacts"
DEPLOYMENTS_DIR="$CONTRACTS_ROOT/deployments"
NETWORK="mainnet"
SOROBAN_RPC_URL="${SOROBAN_RPC_URL_MAINNET:-https://soroban-mainnet.stellar.org}"
NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"

# ---------------------------------------------------------------------------
log() { echo "[deploy-mainnet] $*"; }
fail() { echo "[deploy-mainnet] ERROR: $*" >&2; exit 1; }

# ---------------------------------------------------------------------------
# Hard guard: reject if RPC points to Testnet
# ---------------------------------------------------------------------------
if echo "$SOROBAN_RPC_URL" | grep -qi "testnet"; then
fail "SOROBAN_RPC_URL appears to point to Testnet. Aborting Mainnet deployment."
fi

# ---------------------------------------------------------------------------
# Secrets validation
# ---------------------------------------------------------------------------
[ -n "${MAINNET_DEPLOYER_SECRET:-}" ] \
|| fail "MAINNET_DEPLOYER_SECRET is not set."

# Ensure Testnet key is not reused for Mainnet
if [ -n "${TESTNET_DEPLOYER_SECRET:-}" ] && \
[ "$MAINNET_DEPLOYER_SECRET" = "$TESTNET_DEPLOYER_SECRET" ]; then
fail "MAINNET_DEPLOYER_SECRET must not be the same as TESTNET_DEPLOYER_SECRET."
fi

# ---------------------------------------------------------------------------
# Manual approval gate
# ---------------------------------------------------------------------------
if [ "${MAINNET_DEPLOY_CONFIRMED:-}" != "yes" ]; then
echo ""
echo " ╔══════════════════════════════════════════════════════════╗"
echo " ║ *** MAINNET DEPLOYMENT WARNING *** ║"
echo " ║ You are about to deploy to the Stellar Mainnet. ║"
echo " ║ This action incurs real transaction fees and is ║"
echo " ║ irreversible once the contract is deployed. ║"
echo " ╚══════════════════════════════════════════════════════════╝"
echo ""
printf " Type exactly 'CONFIRM MAINNET DEPLOY' to proceed: "
read -r CONFIRMATION
if [ "$CONFIRMATION" != "CONFIRM MAINNET DEPLOY" ]; then
fail "Confirmation not received. Aborting."
fi
fi

log "Mainnet deployment confirmed. Proceeding..."

# ---------------------------------------------------------------------------
# Build step
# ---------------------------------------------------------------------------
if [ "${SKIP_BUILD:-0}" != "1" ]; then
log "Running build-verify.sh..."
bash "$SCRIPT_DIR/build-verify.sh"
fi

WASM_PATH="$ARTIFACTS_DIR/escrow.wasm"
HASH_FILE="$ARTIFACTS_DIR/escrow.wasm.sha256"
BUILD_INFO="$ARTIFACTS_DIR/build-info.json"

[ -f "$WASM_PATH" ] || fail "Artifact not found: $WASM_PATH"
[ -f "$HASH_FILE" ] || fail "Hash file not found: $HASH_FILE"
[ -f "$BUILD_INFO" ] || fail "Build info not found: $BUILD_INFO"

WASM_HASH=$(awk '{print $1}' "$HASH_FILE")
log "WASM hash: $WASM_HASH"

# ---------------------------------------------------------------------------
# Tooling check
# ---------------------------------------------------------------------------
command -v stellar >/dev/null 2>&1 \
|| fail "'stellar' CLI not found. Install: https://github.com/stellar/stellar-cli"

# ---------------------------------------------------------------------------
# Network validation
# ---------------------------------------------------------------------------
log "Validating network connectivity to $SOROBAN_RPC_URL ..."
curl -sf --max-time 10 "$SOROBAN_RPC_URL" -o /dev/null \
|| fail "Cannot reach Mainnet RPC endpoint $SOROBAN_RPC_URL"
log "Network check passed."

DEPLOYER_ADDRESS=$(stellar keys address --secret-key "$MAINNET_DEPLOYER_SECRET" 2>/dev/null) \
|| fail "Could not derive public key from MAINNET_DEPLOYER_SECRET."
log "Deployer address: $DEPLOYER_ADDRESS"

# ---------------------------------------------------------------------------
# Upload WASM
# ---------------------------------------------------------------------------
log "Uploading WASM to Mainnet..."
UPLOAD_OUTPUT=$(stellar contract upload \
--wasm "$WASM_PATH" \
--source "$MAINNET_DEPLOYER_SECRET" \
--rpc-url "$SOROBAN_RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
2>&1) || fail "WASM upload failed:\n$UPLOAD_OUTPUT"

WASM_ID=$(echo "$UPLOAD_OUTPUT" | grep -Eo '[0-9a-fA-F]{64}' | tail -1 || true)
log "WASM ID (on-chain): ${WASM_ID:-<parse failed>}"

# ---------------------------------------------------------------------------
# Deploy contract
# ---------------------------------------------------------------------------
log "Deploying contract instance to Mainnet..."
DEPLOY_OUTPUT=$(stellar contract deploy \
--wasm-hash "${WASM_ID:-}" \
--source "$MAINNET_DEPLOYER_SECRET" \
--rpc-url "$SOROBAN_RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
2>&1) || {
log "wasm-hash deploy failed, falling back to --wasm ..."
DEPLOY_OUTPUT=$(stellar contract deploy \
--wasm "$WASM_PATH" \
--source "$MAINNET_DEPLOYER_SECRET" \
--rpc-url "$SOROBAN_RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
2>&1) || fail "Contract deploy failed:\n$DEPLOY_OUTPUT"
}

CONTRACT_ID=$(echo "$DEPLOY_OUTPUT" | grep -Eo 'C[A-Z0-9]{55}' | head -1 || true)
DEPLOY_TX_HASH=$(echo "$DEPLOY_OUTPUT" | grep -Eo '[0-9a-fA-F]{64}' | head -1 || true)

[ -n "$CONTRACT_ID" ] \
|| fail "Could not parse contract ID from deploy output:\n$DEPLOY_OUTPUT"

log "Contract ID: $CONTRACT_ID"
log "Deploy TX : ${DEPLOY_TX_HASH:-<not captured>}"

# ---------------------------------------------------------------------------
# Optional: initialize contract
# ---------------------------------------------------------------------------
INIT_RECORD="null"
if [ -n "${INIT_ADMIN:-}" ] && [ -n "${INIT_CLIENT:-}" ] && \
[ -n "${INIT_FREELANCER:-}" ] && [ -n "${INIT_ARBITER:-}" ] && \
[ -n "${INIT_TOKEN:-}" ]; then
log "Calling initialize() on deployed contract..."
INIT_OUTPUT=$(stellar contract invoke \
--id "$CONTRACT_ID" \
--source "$MAINNET_DEPLOYER_SECRET" \
--rpc-url "$SOROBAN_RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
-- initialize \
--admin "$INIT_ADMIN" \
--client "$INIT_CLIENT" \
--freelancer "$INIT_FREELANCER" \
--arbiter "$INIT_ARBITER" \
--token "$INIT_TOKEN" \
--milestones '[]' \
2>&1) || fail "initialize() call failed:\n$INIT_OUTPUT"
log "initialize() succeeded."

INIT_RECORD=$(cat <<JSONEOF
{
"admin": "$INIT_ADMIN",
"client": "$INIT_CLIENT",
"freelancer": "$INIT_FREELANCER",
"arbiter": "$INIT_ARBITER",
"token": "$INIT_TOKEN"
}
JSONEOF
)
fi

# ---------------------------------------------------------------------------
# Deployment record
# ---------------------------------------------------------------------------
mkdir -p "$DEPLOYMENTS_DIR"
DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
RECORD_FILE="$DEPLOYMENTS_DIR/mainnet-$(date -u +"%Y%m%dT%H%M%SZ").json"

GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
GIT_TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
RUST_VERSION=$(rustc --version 2>/dev/null || echo "unknown")
SOROBAN_SDK_VERSION=$(grep -A1 'name = "soroban-sdk"' "$CONTRACTS_ROOT/Cargo.lock" \
| grep version | head -1 | sed 's/.*"\(.*\)".*/\1/' || echo "unknown")

cat > "$RECORD_FILE" <<EOF
{
"schema_version": "1",
"network": "$NETWORK",
"network_passphrase": "$NETWORK_PASSPHRASE",
"rpc_url": "$SOROBAN_RPC_URL",
"contract_id": "$CONTRACT_ID",
"wasm_hash_sha256": "$WASM_HASH",
"wasm_id_on_chain": "${WASM_ID:-}",
"deploy_tx_hash": "${DEPLOY_TX_HASH:-}",
"deployer_address": "$DEPLOYER_ADDRESS",
"deployment_timestamp": "$DEPLOY_TIMESTAMP",
"git_commit": "$GIT_COMMIT",
"git_tag": "$GIT_TAG",
"rust_version": "$RUST_VERSION",
"soroban_sdk_version": "$SOROBAN_SDK_VERSION",
"initialization_params": $INIT_RECORD
}
EOF

log ""
log "Deployment record saved: $RECORD_FILE"
log ""
log "===== MAINNET DEPLOYMENT COMPLETE ====="
log " Network : $NETWORK"
log " Contract ID: $CONTRACT_ID"
log " WASM hash : $WASM_HASH"
log " TX hash : ${DEPLOY_TX_HASH:-<not captured>}"
log " Timestamp : $DEPLOY_TIMESTAMP"
log " Record : $RECORD_FILE"
log ""
log "Verify with: ./scripts/verify-deployment.sh $RECORD_FILE"
Loading