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
42 changes: 41 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ permissions:

env:
CARGO_TERM_COLOR: always
WASM_MAX_BYTES: 262144

jobs:
check-and-test:
Expand Down Expand Up @@ -40,6 +41,45 @@ jobs:
- name: Run workspace unit and integration tests
run: cargo test --all --verbose

- name: Build WASM contracts
- name: Build WASM contracts in release mode
run: |
cargo build --release --target wasm32-unknown-unknown

- name: Report WASM artifact sizes
run: |
set -euo pipefail

shopt -s nullglob
wasm_artifacts=(target/wasm32-unknown-unknown/release/*.wasm)

if (( ${#wasm_artifacts[@]} == 0 )); then
echo "No WASM artifacts found in target/wasm32-unknown-unknown/release" >&2
exit 1
fi

printf '%-64s %12s %12s\n' "Artifact" "Bytes" "KiB"
for artifact in "${wasm_artifacts[@]}"; do
size_bytes=$(stat -c%s "$artifact")
size_kib=$(awk -v bytes="$size_bytes" 'BEGIN { printf "%.2f", bytes / 1024 }')
printf '%-64s %12d %12s\n' "$artifact" "$size_bytes" "$size_kib"
done

- name: Enforce WASM artifact size limit
run: |
set -euo pipefail

shopt -s nullglob
wasm_artifacts=(target/wasm32-unknown-unknown/release/*.wasm)

if (( ${#wasm_artifacts[@]} == 0 )); then
echo "No WASM artifacts found in target/wasm32-unknown-unknown/release" >&2
exit 1
fi

for artifact in "${wasm_artifacts[@]}"; do
size_bytes=$(stat -c%s "$artifact")
if (( size_bytes > WASM_MAX_BYTES )); then
echo "${artifact} is ${size_bytes} bytes, exceeding the ${WASM_MAX_BYTES}-byte limit" >&2
exit 1
fi
done
18 changes: 17 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,28 @@ cargo clippy --all-targets --all-features -- -D warnings
# 3. Run all unit and integration tests across all workspace contracts
cargo test --all --verbose

# 4. Build release WASM binaries for target wasm32-unknown-unknown
# 4. Audit dependency vulnerabilities against the RustSec advisory database
cargo audit

# 5. Build release WASM binaries for target wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown
```

---

## πŸ” Dependency Vulnerability Triage

CI runs `cargo audit` on every Pull Request targeting `dev` and every push to `dev`. When an advisory is reported:

1. Confirm the affected crate, version range, and advisory details in the RustSec database.
2. Prefer upgrading the vulnerable dependency or its direct parent dependency in the same Pull Request.
3. If no patched version is available, document the impact analysis, affected code paths, and mitigation plan in the Pull Request before requesting review.
4. Do not add advisory ignores unless the advisory is demonstrably unreachable or a maintainer approves a temporary exception with a tracked follow-up issue.

Run `cargo install cargo-audit --locked` once locally if the `cargo audit` command is unavailable.

---

## πŸ› οΈ Local Development Setup

### 1. Requirements
Expand Down
6 changes: 6 additions & 0 deletions contracts/invoice-token/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@ pub enum Error {
InvalidExpiration = 10,
/// Contract is paused and the requested operation is temporarily disabled.
Paused = 11,
/// Address parameter is zero (null).
InvalidAddress = 12,
/// Account is frozen and cannot perform transfers.
AccountFrozen = 13,
/// Account is not frozen and cannot be unfrozen.
AccountNotFrozen = 14,
}
12 changes: 12 additions & 0 deletions contracts/invoice-token/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,15 @@ pub fn paused_updated_event(env: &Env, old_value: bool, new_value: bool) {
(old_value, new_value),
);
}

/// Emit account frozen event.
pub fn account_frozen_event(env: &Env, account: &Address) {
env.events()
.publish((Symbol::new(env, "account_frozen"),), account);
}

/// Emit account unfrozen event.
pub fn account_unfrozen_event(env: &Env, account: &Address) {
env.events()
.publish((Symbol::new(env, "account_unfrozen"),), account);
}
55 changes: 55 additions & 0 deletions contracts/invoice-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ impl InvoiceToken {
if meta.transfer_locked && from != meta.admin {
return Err(Error::TransferLocked);
}
// Check if from account is frozen
if storage::is_frozen(&env, &from) {
return Err(Error::AccountFrozen);
}
let from_balance = storage::get_balance(&env, &from);
if from_balance < amount {
return Err(Error::InsufficientBalance);
Expand Down Expand Up @@ -149,6 +153,10 @@ impl InvoiceToken {
if meta.transfer_locked && from != meta.admin {
return Err(Error::TransferLocked);
}
// Check if from account is frozen
if storage::is_frozen(&env, &from) {
return Err(Error::AccountFrozen);
}
let ledger = env.ledger().sequence();
let allow = storage::get_allowance_data(&env, &from, &spender)
.ok_or(Error::InsufficientAllowance)?;
Expand Down Expand Up @@ -324,6 +332,53 @@ impl InvoiceToken {
let meta = storage::get_metadata(&env).ok_or(Error::NotInit)?;
Ok(meta.paused)
}

// ---------- Account Freeze/Unfreeze ----------

/// Freeze an account, preventing it from transferring tokens. Admin only.
pub fn freeze_account(env: Env, account: Address) -> Result<(), Error> {
// Validate address parameters
Self::validate_address(&account)?;

let meta = storage::get_metadata(&env).ok_or(Error::NotInit)?;
meta.admin.require_auth();

// Check if account is already frozen
if storage::is_frozen(&env, &account) {
return Err(Error::AccountFrozen);
}

storage::freeze_account(&env, &account);
events::account_frozen_event(&env, &account);
Ok(())
}

/// Unfreeze an account, allowing it to transfer tokens again. Admin only.
pub fn unfreeze_account(env: Env, account: Address) -> Result<(), Error> {
// Validate address parameters
Self::validate_address(&account)?;

let meta = storage::get_metadata(&env).ok_or(Error::NotInit)?;
meta.admin.require_auth();

// Check if account is not frozen
if !storage::is_frozen(&env, &account) {
return Err(Error::AccountNotFrozen);
}

storage::unfreeze_account(&env, &account);
events::account_unfrozen_event(&env, &account);
Ok(())
}

/// Check if an account is frozen.
pub fn is_frozen(env: Env, account: Address) -> Result<bool, Error> {
// Validate address parameters
Self::validate_address(&account)?;

storage::get_metadata(&env).ok_or(Error::NotInit)?;
Ok(storage::is_frozen(&env, &account))
}
}

#[cfg(test)]
Expand Down
22 changes: 22 additions & 0 deletions contracts/invoice-token/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,25 @@ pub fn get_allowance_data(
.persistent()
.get(&StorageKey::Allowance(from.clone(), spender.clone()))
}

/// Check if an account is frozen.
pub fn is_frozen(env: &soroban_sdk::Env, addr: &Address) -> bool {
env.storage()
.persistent()
.get(&StorageKey::Frozen(addr.clone()))
.unwrap_or(false)
}

/// Freeze an account.
pub fn freeze_account(env: &soroban_sdk::Env, addr: &Address) {
env.storage()
.persistent()
.set(&StorageKey::Frozen(addr.clone()), &true);
}

/// Unfreeze an account.
pub fn unfreeze_account(env: &soroban_sdk::Env, addr: &Address) {
env.storage()
.persistent()
.remove(&StorageKey::Frozen(addr.clone()));
}
Loading
Loading