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
39 changes: 26 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ concurrency:

jobs:
fmt:
name: Formatting
name: Formatting (${{ matrix.crate }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
crate: [intent_settlement, proof_registry]
defaults:
run:
working-directory: intent_settlement
working-directory: ${{ matrix.crate }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
Expand All @@ -28,17 +32,18 @@ jobs:
run: cargo fmt --all -- --check

contract:
name: Contract (${{ matrix.toolchain }})
name: Contract (${{ matrix.crate }} / ${{ matrix.toolchain }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
crate: [intent_settlement, proof_registry]
# stable — tracks latest stable Rust
# 1.78 — MSRV declared in README ("Rust 1.78+")
toolchain: [stable, "1.78"]
defaults:
run:
working-directory: intent_settlement
working-directory: ${{ matrix.crate }}
steps:
- uses: actions/checkout@v4

Expand All @@ -50,12 +55,12 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: intent_settlement
workspaces: ${{ matrix.crate }}
components: rustfmt, clippy

- uses: Swatinem/rust-cache@v2
with:
workspaces: intent_settlement
workspaces: ${{ matrix.crate }}
# Separate cache per toolchain to avoid cross-contamination
key: ${{ matrix.toolchain }}

Expand All @@ -74,11 +79,15 @@ jobs:
run: cargo test

wasm-size:
name: Wasm size budget
name: Wasm size budget (${{ matrix.crate }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
crate: [intent_settlement, proof_registry]
defaults:
run:
working-directory: intent_settlement
working-directory: ${{ matrix.crate }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
Expand All @@ -87,7 +96,7 @@ jobs:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
workspaces: intent_settlement
workspaces: ${{ matrix.crate }}
- name: Build wasm
run: cargo build --target wasm32-unknown-unknown --release
- name: Check wasm size
Expand All @@ -98,7 +107,7 @@ jobs:
MAX_BYTES=58982
WASM=$(ls target/wasm32-unknown-unknown/release/*.wasm | head -n1)
SIZE=$(wc -c < "$WASM")
echo "### Wasm size report" >> "$GITHUB_STEP_SUMMARY"
echo "### Wasm size report (${{ matrix.crate }})" >> "$GITHUB_STEP_SUMMARY"
echo "| File | Size (bytes) | Budget (bytes) | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "| ---- | -----------: | -------------: | ------ |" >> "$GITHUB_STEP_SUMMARY"
if [ "$SIZE" -le "$MAX_BYTES" ]; then
Expand Down Expand Up @@ -132,18 +141,22 @@ jobs:
run: PROPTEST_CASES=256 cargo test --features testutils -- bond_conservation

audit:
name: Dependency audit
name: Dependency audit (${{ matrix.crate }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
crate: [intent_settlement, proof_registry]
defaults:
run:
working-directory: intent_settlement
working-directory: ${{ matrix.crate }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: intent_settlement
workspaces: ${{ matrix.crate }}
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Audit dependencies
Expand Down
110 changes: 110 additions & 0 deletions intent_settlement/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ mod test;
#[cfg(test)]
mod proptest_bond;

#[cfg(test)]
mod proptest_fill;

// ─── Constants ────────────────────────────────────────────────────────────────

const INTENT_EXPIRY: u64 = 1800; // 30 minutes
Expand Down Expand Up @@ -82,6 +85,15 @@ pub enum DataKey {
/// timestamp at which `accept_fee_recipient` may execute it (issue #30,
/// timelock added by #115): `(Address, u64)`.
PendingFeeRecipient,
/// Proposed-but-not-yet-accepted new admin plus the ledger timestamp at
/// which `accept_admin_transfer` may execute it: `(Address, u64)`.
PendingAdmin,
/// Proposed-but-not-yet-accepted dst token addition plus the ledger
/// timestamp at which `execute_add_dst_token` may execute it: `u64`.
PendingDstTokenAdd(Address),
/// Proposed-but-not-yet-accepted dst token removal plus the ledger
/// timestamp at which `execute_remove_dst_token` may execute it: `u64`.
PendingDstTokenRemove(Address),
BondToken, // USDC address for bonds
Intent(BytesN<32>), // intent_id -> IntentRecord
Solver(Address), // address -> SolverRecord
Expand Down Expand Up @@ -524,6 +536,29 @@ impl IntentSettlement {
);
}

/// Admin-only: cancel a pending fee recipient proposal and emit a
/// `fee_recipient_proposal_cancelled` event. Allows the admin to withdraw a
/// mistaken proposal without simultaneously replacing it with a new one (#210).
/// Calling `accept_fee_recipient` after cancellation fails with
/// `NoPendingFeeRecipient`, exactly as if no proposal had been made.
pub fn cancel_pending_fee_recipient(env: Env) {
env.current_contract_address().require_auth();
let pending = env
.storage()
.instance()
.get::<_, (Address, u64)>(&DataKey::PendingFeeRecipient)
.unwrap_or_else(|| panic_with_error!(&env, Error::NoPendingFeeRecipient));

env.storage()
.instance()
.remove(&DataKey::PendingFeeRecipient);

env.events().publish(
(Symbol::new(&env, "fee_recipient_proposal_cancelled"),),
pending.0,
);
}

/// Admin-only: propose transferring the admin role to a new address. A
/// `admin_transfer_proposed` event fires immediately for off-chain
/// monitors (#116); the transfer itself only takes effect once
Expand Down Expand Up @@ -580,6 +615,33 @@ impl IntentSettlement {
.publish((Symbol::new(&env, "admin_transferred"),), new_admin);
}

/// Admin-only: cancel a pending admin transfer proposal and emit an
/// `admin_transfer_proposal_cancelled` event. Allows the current admin to
/// withdraw a mistaken proposal without simultaneously replacing it with a
/// new one (#210). Calling `accept_admin_transfer` after cancellation fails
/// with `NoPendingAdminTransfer`, exactly as if no proposal had been made.
pub fn cancel_pending_admin_transfer(env: Env) {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized));
admin.require_auth();

let pending = env
.storage()
.instance()
.get::<_, (Address, u64)>(&DataKey::PendingAdmin)
.unwrap_or_else(|| panic_with_error!(&env, Error::NoPendingAdminTransfer));

env.storage().instance().remove(&DataKey::PendingAdmin);

env.events().publish(
(Symbol::new(&env, "admin_transfer_proposal_cancelled"),),
pending.0,
);
}

// ── Protocol Config ───────────────────────────────────────────────────────

/// Read the effective protocol config. Falls back to compile-time defaults
Expand Down Expand Up @@ -699,6 +761,30 @@ impl IntentSettlement {
.publish((Symbol::new(&env, "dst_token_allowed"),), token);
}

/// Admin-only: cancel a pending dst_token_add proposal for the given token
/// and emit a `dst_token_add_proposal_cancelled` event. Allows the admin to
/// withdraw a mistaken proposal without simultaneously replacing it with a
/// new one (#210). Calling `execute_add_dst_token` after cancellation fails
/// with `NoPendingDstTokenChange`, exactly as if no proposal had been made.
pub fn cancel_pending_dst_token_add(env: Env, token: Address) {
Self::require_admin(&env);

let _eta: u64 = env
.storage()
.instance()
.get(&DataKey::PendingDstTokenAdd(token.clone()))
.unwrap_or_else(|| panic_with_error!(&env, Error::NoPendingDstTokenChange));

env.storage()
.instance()
.remove(&DataKey::PendingDstTokenAdd(token.clone()));

env.events().publish(
(Symbol::new(&env, "dst_token_add_proposal_cancelled"),),
token,
);
}

/// Admin-only: propose disallowing a dst_token. Fires a
/// `dst_token_remove_proposed` event immediately (#116); the token stays
/// allowed until `execute_remove_dst_token` is called after the timelock
Expand Down Expand Up @@ -742,6 +828,30 @@ impl IntentSettlement {
.publish((Symbol::new(&env, "dst_token_disallowed"),), token);
}

/// Admin-only: cancel a pending dst_token_remove proposal for the given token
/// and emit a `dst_token_remove_proposal_cancelled` event. Allows the admin to
/// withdraw a mistaken proposal without simultaneously replacing it with a
/// new one (#210). Calling `execute_remove_dst_token` after cancellation fails
/// with `NoPendingDstTokenChange`, exactly as if no proposal had been made.
pub fn cancel_pending_dst_token_remove(env: Env, token: Address) {
Self::require_admin(&env);

let _eta: u64 = env
.storage()
.instance()
.get(&DataKey::PendingDstTokenRemove(token.clone()))
.unwrap_or_else(|| panic_with_error!(&env, Error::NoPendingDstTokenChange));

env.storage()
.instance()
.remove(&DataKey::PendingDstTokenRemove(token.clone()));

env.events().publish(
(Symbol::new(&env, "dst_token_remove_proposal_cancelled"),),
token,
);
}

/// Returns `true` if `token` is on the dst_token allowlist.
/// Does not check whether allowlist enforcement is currently active.
pub fn is_dst_token_allowed(env: Env, token: Address) -> bool {
Expand Down
Loading