Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
rustflags = ["-D", "warnings"]
70 changes: 15 additions & 55 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,69 +1,29 @@
name: Veritix Pay CI
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
branches: [main]

jobs:
build-and-test:
name: Build, Test, and Format
contract-ci:
runs-on: ubuntu-latest

# Set the working directory for all run steps in this job
defaults:
run:
working-directory: veritixpay/contract/token

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.84.0"
# wasm32-unknown-unknown is needed for `cargo test` and clippy.
# `stellar contract build` uses wasm32v1-none internally and manages
# that target itself; we do not need to install it here.
targets: wasm32-unknown-unknown
components: rustfmt, clippy

- name: Cache Cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
workspaces: "veritixpay/contract/token -> target"

targets: wasm32-unknown-unknown,wasm32v1-none
- name: Install Stellar CLI
run: cargo install --locked stellar-cli

- name: Check for orphaned source files
run: |
LIB=src/lib.rs
EXIT=0
for f in src/*.rs; do
mod=$(basename "$f" .rs)
[ "$mod" = "lib" ] && continue
if ! grep -qE "^\s*(pub\s+)?mod\s+${mod}\s*;" "$LIB"; then
echo "ORPHAN: $f is not declared in lib.rs"
EXIT=1
fi
done
exit $EXIT

- name: Check Formatting
# Uses cargo fmt directly, or make fmt if it passes the args in your Makefile
run: cargo fmt -- --check

- name: Run Clippy
run: cargo clippy --lib -- -D warnings

- name: Compile-only check (locked)
run: cargo check --locked

- name: Build Contract
run: cargo install stellar-cli --locked
- name: Preflight
run: make preflight
- name: Build
run: make build

- name: Run Tests
- name: Test
run: make test
- name: Clippy
run: cargo clippy --all -- -D warnings
- name: Rustfmt
run: cargo fmt --all --check
50 changes: 49 additions & 1 deletion veritixpay/contract/token/src/admin_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#[cfg(test)]
mod admin_test {
continue
use soroban_sdk::{testutils::Address as _, testutils::Events as _, Address, Env, String};

use crate::admin::{has_admin, read_admin, transfer_admin, write_admin};
Expand Down Expand Up @@ -143,4 +142,53 @@ continue
transfer_admin(&env, admin.clone());
assert_eq!(read_admin(&env), admin);
}

#[test]
fn test_admin_info_tracks_admin_rotation() {
let env = setup_env();
let (_admin, client) = create_initialized_client(&env);
let new_admin = Address::generate(&env);
let before = client.admin_info();
assert_eq!(before.paused, false);
client.set_admin(&new_admin);
let after = client.admin_info();
assert_eq!(after.admin, new_admin);
assert_eq!(after.paused, false);
}

#[test]
fn test_freeze_batch_and_unfreeze_batch() {
let env = setup_env();
let (_admin, client) = create_initialized_client(&env);
let a = Address::generate(&env);
let b = Address::generate(&env);
let mut targets = soroban_sdk::Vec::new(&env);
targets.push_back(a.clone());
targets.push_back(b.clone());
client.freeze_batch(&client.admin(), &targets);
assert!(client.is_frozen(&a));
assert!(client.is_frozen(&b));
client.unfreeze_batch(&client.admin(), &targets);
assert!(!client.is_frozen(&a));
assert!(!client.is_frozen(&b));
}

#[test]
fn test_clawback_batch_reduces_balances_and_supply() {
let env = setup_env();
let (_admin, client) = create_initialized_client(&env);
let admin = client.admin();
let a = Address::generate(&env);
let b = Address::generate(&env);
client.mint(&admin, &a, &1000);
client.mint(&admin, &b, &1000);
let mut targets = soroban_sdk::Vec::new(&env);
targets.push_back((a.clone(), 200));
targets.push_back((b.clone(), 300));
let before_supply = client.total_supply();
client.clawback_batch(&admin, &targets);
assert_eq!(client.balance(&a), 800);
assert_eq!(client.balance(&b), 700);
assert_eq!(client.total_supply(), before_supply - 500);
}
}
53 changes: 52 additions & 1 deletion veritixpay/contract/token/src/allowance.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::storage_types::{
AllowanceDataKey, AllowanceValue, DataKey, ALLOWANCE_BUMP_AMOUNT, ALLOWANCE_LIFETIME_THRESHOLD,
PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD,
};
use crate::validation::{require_current_or_future_ledger, require_non_negative_amount};
use soroban_sdk::{Address, Env};
use soroban_sdk::{Address, Env, Vec};

pub fn read_allowance(e: &Env, from: Address, spender: Address) -> AllowanceValue {
let key = DataKey::Allowance(AllowanceDataKey {
Expand Down Expand Up @@ -56,17 +57,67 @@ pub fn write_allowance(
spender: spender.clone(),
});

let index_key = DataKey::SpenderAllowances(spender.clone());
let mut spenders_from: Vec<Address> = e
.storage()
.persistent()
.get(&index_key)
.unwrap_or_else(|| Vec::new(e));

if amount == 0 {
e.storage().persistent().remove(&key);
let mut updated = Vec::new(e);
for i in 0..spenders_from.len() {
let addr = spenders_from.get(i).unwrap();
if addr != from {
updated.push_back(addr);
}
}
e.storage().persistent().set(&index_key, &updated);
// Keep spender index alive for long-lived delegated payment lookups.
e.storage().persistent().extend_ttl(
&index_key,
PERSISTENT_LIFETIME_THRESHOLD,
PERSISTENT_BUMP_AMOUNT,
);
} else {
let mut exists = false;
for i in 0..spenders_from.len() {
if spenders_from.get(i).unwrap() == from {
exists = true;
break;
}
}
if !exists {
spenders_from.push_back(from.clone());
e.storage().persistent().set(&index_key, &spenders_from);
// Keep spender index alive for long-lived delegated payment lookups.
e.storage().persistent().extend_ttl(
&index_key,
PERSISTENT_LIFETIME_THRESHOLD,
PERSISTENT_BUMP_AMOUNT,
);
}
let allowance = AllowanceValue {
amount,
expiration_ledger,
};
e.storage().persistent().set(&key, &allowance);
e.storage().persistent().extend_ttl(
&key,
ALLOWANCE_LIFETIME_THRESHOLD,
ALLOWANCE_BUMP_AMOUNT,
);
}
}

pub fn get_allowances_for_spender(e: &Env, spender: Address) -> Vec<Address> {
e.storage()
.persistent()
.get(&DataKey::SpenderAllowances(spender))
.unwrap_or_else(|| Vec::new(e))
}

pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) {
let allowance = read_allowance(e, from.clone(), spender.clone());

Expand Down
20 changes: 19 additions & 1 deletion veritixpay/contract/token/src/allowance_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
mod allowance_tests {
use soroban_sdk::{Address, Env};

use crate::allowance::{read_allowance, spend_allowance, write_allowance};
use crate::allowance::{get_allowances_for_spender, read_allowance, spend_allowance, write_allowance};
use crate::contract::VeritixToken;

fn setup_env() -> (Env, Address) {
Expand Down Expand Up @@ -108,4 +108,22 @@ mod allowance_tests {
assert_eq!(a.amount, 0);
});
}

#[test]
fn test_allowances_for_spender_index_adds_and_removes_from() {
let (e, contract_id) = setup_env();
let from = Address::generate(&e);
let spender = Address::generate(&e);
e.as_contract(&contract_id, || {
let expiry = e.ledger().sequence() + 100;
write_allowance(&e, from.clone(), spender.clone(), 500, expiry);
let indexed = get_allowances_for_spender(&e, spender.clone());
assert_eq!(indexed.len(), 1);
assert_eq!(indexed.get(0).unwrap(), from.clone());

write_allowance(&e, from.clone(), spender.clone(), 0, expiry);
let indexed_after = get_allowances_for_spender(&e, spender);
assert_eq!(indexed_after.len(), 0);
});
}
}
48 changes: 48 additions & 0 deletions veritixpay/contract/token/src/batch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::admin::check_admin;
use crate::balance::{decrease_supply, spend_balance};
use crate::freeze::{freeze_account, unfreeze_account};
use crate::validation::require_positive_amount;
use soroban_sdk::{symbol_short, Address, Env, Vec};

const MAX_BATCH_TARGETS: u32 = 50;

pub fn clawback_batch(e: &Env, admin: Address, targets: Vec<(Address, i128)>) {
check_admin(e, &admin);
if targets.len() > MAX_BATCH_TARGETS {
panic!("batch too large");
}
for i in 0..targets.len() {
let (from, amount) = targets.get(i).unwrap();
require_positive_amount(amount);
spend_balance(e, from.clone(), amount);
decrease_supply(e, amount);
e.events()
.publish((symbol_short!("clawback"), admin.clone(), from), amount);
}
}

pub fn freeze_batch(e: &Env, admin: Address, targets: Vec<Address>) {
check_admin(e, &admin);
if targets.len() > MAX_BATCH_TARGETS {
panic!("batch too large");
}
for i in 0..targets.len() {
let target = targets.get(i).unwrap();
freeze_account(e, admin.clone(), target);
}
e.events()
.publish((symbol_short!("batch_frz"), admin), targets.len());
}

pub fn unfreeze_batch(e: &Env, admin: Address, targets: Vec<Address>) {
check_admin(e, &admin);
if targets.len() > MAX_BATCH_TARGETS {
panic!("batch too large");
}
for i in 0..targets.len() {
let target = targets.get(i).unwrap();
unfreeze_account(e, admin.clone(), target);
}
e.events()
.publish((symbol_short!("batch_unf"), admin), targets.len());
}
Loading