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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,38 @@ jobs:
- name: Build wasm
run: cargo build --target wasm32-unknown-unknown --release

coverage:
name: Code coverage
runs-on: ubuntu-latest
defaults:
run:
working-directory: intent_settlement
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: intent_settlement
- name: Install cargo-tarpaulin
run: cargo install cargo-tarpaulin
- name: Generate coverage report
run: cargo tarpaulin --out Xml --timeout 300 --exclude-files tests
- name: Check coverage threshold
run: |
COVERAGE=$(grep -oP 'line-rate="\K[^"]*' cobertura.xml | head -1)
THRESHOLD=0.70
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
echo "Coverage $COVERAGE is below threshold $THRESHOLD"
exit 1
fi
echo "Coverage $COVERAGE meets minimum threshold of $THRESHOLD"
- name: Upload coverage report
uses: codecov/codecov-action@v3
with:
files: ./intent_settlement/cobertura.xml
fail_ci_if_error: false

audit:
name: Dependency audit
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Soroban smart contracts for [Vortex Protocol](https://github.com/vortex-protocol) — intent-based cross-chain swaps settled on Stellar.**

[![CI](https://github.com/vortex-protocol/vortex-contract/actions/workflows/ci.yml/badge.svg)](https://github.com/vortex-protocol/vortex-contract/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/vortex-protocol/vortex-contracts/branch/main/graph/badge.svg)](https://codecov.io/gh/vortex-protocol/vortex-contracts)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)

This repository holds the on-chain logic that guarantees settlement: intent
Expand Down
137 changes: 137 additions & 0 deletions intent_settlement/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,83 @@ fn pause_does_not_block_slashing_an_already_accepted_intent() {
assert_eq!(c.get_solver(&ctx.solver).unwrap().fills_failed, 1);
}

#[test]
fn pause_blocks_fill_intent() {
let ctx = setup();
let c = ctx.client();
ctx.register_solver();
let id = ctx.submit();
c.accept_intent(&ctx.solver, &id);

c.pause();

let fee = FILL * 5 / 10_000;
ctx.dst_admin().mint(&ctx.solver, &(FILL + fee));
let res = c.try_fill_intent(&ctx.solver, &id, &FILL);
assert_eq!(res, Err(Ok(Error::ContractPaused.into())));
}

#[test]
fn pause_does_not_block_cancel_intent() {
let ctx = setup();
let c = ctx.client();
let id = ctx.submit();

c.pause();
assert!(c.is_paused());

// cancel_intent should succeed even while paused
c.cancel_intent(&ctx.user, &id);
assert!(c.get_intent(&id).unwrap().state == IntentState::Cancelled);
}

#[test]
fn pause_blocks_submit_accept_fill_but_allows_cancel_and_slash() {
let ctx = setup();
let c = ctx.client();
ctx.register_solver();

// Submit and accept before pausing
let id = ctx.submit();
c.accept_intent(&ctx.solver, &id);

// Submit another intent to test that it can't be accepted while paused
let id2 = ctx.submit();

c.pause();
assert!(c.is_paused());

// Test blocked operations
let deadline: Option<u64> = None;
let res = c.try_submit_intent(
&ctx.user,
&String::from_str(&ctx.env, "ethereum"),
&String::from_str(&ctx.env, "0xdef"),
&SRC_AMT,
&ctx.dst_token,
&MIN_DST,
&deadline,
);
assert_eq!(res, Err(Ok(Error::ContractPaused.into())));

let res = c.try_accept_intent(&ctx.solver, &id2);
assert_eq!(res, Err(Ok(Error::ContractPaused.into())));

let fee = FILL * 5 / 10_000;
ctx.dst_admin().mint(&ctx.solver, &(FILL + fee));
let res = c.try_fill_intent(&ctx.solver, &id, &FILL);
assert_eq!(res, Err(Ok(Error::ContractPaused.into())));

// Test allowed operations
let id3 = ctx.submit();
c.cancel_intent(&ctx.user, &id3);
assert!(c.get_intent(&id3).unwrap().state == IntentState::Cancelled);

ctx.pass_time(FILL_WINDOW + 1);
c.slash_solver(&id);
assert_eq!(c.get_solver(&ctx.solver).unwrap().fills_failed, 1);
}

// ─── Solver registration ────────────────────────────────────────────────────────

#[test]
Expand Down Expand Up @@ -610,6 +687,64 @@ fn dst_allowlist_removal_blocks_previously_allowed_token() {
assert_eq!(res, Err(Ok(Error::DstTokenNotAllowed.into())));
}

#[test]
fn dst_allowlist_toggled_mid_lifecycle_does_not_retroactively_affect_open_intent() {
let ctx = setup();
let c = ctx.client();
// Allowlist disabled by default, so any token is accepted
assert!(!c.is_dst_allowlist_enabled());
let id = ctx.submit();

// Enable allowlist without adding the dst_token
c.set_dst_allowlist_enabled(&true);
assert!(!c.is_dst_token_allowed(&ctx.dst_token));

// The already-open intent should still be readable/usable
let intent = c.get_intent(&id).unwrap();
assert!(intent.state == IntentState::Open);

// But new submissions with non-allowed tokens fail
let deadline: Option<u64> = None;
let res = c.try_submit_intent(
&ctx.user,
&String::from_str(&ctx.env, "ethereum"),
&String::from_str(&ctx.env, "0xabc"),
&SRC_AMT,
&ctx.dst_token,
&MIN_DST,
&deadline,
);
assert_eq!(res, Err(Ok(Error::DstTokenNotAllowed.into())));
}

#[test]
fn dst_allowlist_can_be_re_enabled_to_accept_previously_blocked_tokens() {
let ctx = setup();
let c = ctx.client();

// Enable allowlist and block the token
c.set_dst_allowlist_enabled(&true);
assert!(!c.is_dst_token_allowed(&ctx.dst_token));

let deadline: Option<u64> = None;
let res = c.try_submit_intent(
&ctx.user,
&String::from_str(&ctx.env, "ethereum"),
&String::from_str(&ctx.env, "0xabc"),
&SRC_AMT,
&ctx.dst_token,
&MIN_DST,
&deadline,
);
assert_eq!(res, Err(Ok(Error::DstTokenNotAllowed.into())));

// Disable the allowlist
c.set_dst_allowlist_enabled(&false);

// Now submissions with any token succeed again
ctx.submit();
}

#[test]
fn submit_intent_zero_amount_fails() {
let ctx = setup();
Expand Down Expand Up @@ -793,6 +928,8 @@ fn cannot_cancel_someone_elses_intent() {
let stranger = Address::generate(&ctx.env);
let res = ctx.client().try_cancel_intent(&stranger, &id);
assert_eq!(res, Err(Ok(Error::Unauthorized.into())));
// State remains unchanged after rejected cancellation attempt
assert!(ctx.client().get_intent(&id).unwrap().state == IntentState::Open);
}

// ─── Slashing ───────────────────────────────────────────────────────────────────
Expand Down