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
77 changes: 77 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ members = [
"contracts/invoice-amendment",
"contracts/invoice-co-investment",
"contracts/invoice-escrow",
"contracts/invoice-issuer-verification",
"contracts/invoice-maturity",
"contracts/invoice-metadata-registry",
"contracts/invoice-pool",
Expand All @@ -37,7 +38,9 @@ members = [
"contracts/partial-settlement",
"contracts/payment-distributor",
"contracts/secondary-market",
"contracts/secondary-trade-history",
"contracts/staking-rewards",
"contracts/treasury-management",
"contracts/twap-oracle",
"contracts/price-stabilisation",
"contracts/accreditation-renewal",
Expand Down
16 changes: 16 additions & 0 deletions contracts/invoice-issuer-verification/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "invoice-issuer-verification"
version = "0.1.0"
edition = "2021"
# Deployed as WASM, never published to crates.io.
publish = false
description = "StellarSettle invoice issuer verification badge contract"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
22 changes: 22 additions & 0 deletions contracts/invoice-issuer-verification/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Error types for the invoice issuer verification badge contract.

use soroban_sdk::contracterror;

/// Errors that can occur during contract execution.
#[contracterror]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Error {
/// Contract has already been initialized.
AlreadyInit = 1,
/// Contract has not been initialized.
NotInit = 2,
/// Caller is not the admin.
Unauthorized = 3,
/// Address is the zero address (all-zero 32-byte key).
InvalidAddress = 4,
/// expiry_timestamp must be in the future relative to the current ledger time.
InvalidExpiry = 5,
/// No verification record exists for this issuer.
NotVerified = 6,
}
32 changes: 32 additions & 0 deletions contracts/invoice-issuer-verification/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#![allow(deprecated)]
//! Events emitted by the invoice issuer verification badge contract.

use soroban_sdk::{Address, Env, Symbol};

use crate::types::VerificationTier;

/// Emitted when the contract is initialized.
pub fn initialized(env: &Env, admin: &Address) {
let topics = (Symbol::new(env, "initialized"),);
env.events().publish(topics, admin.clone());
}

/// Emitted when an issuer is granted (or re-granted) verification.
/// Topics: (VerificationGranted, issuer); Data: (tier, expiry_timestamp).
pub fn verification_granted(
env: &Env,
issuer: &Address,
tier: VerificationTier,
expiry_timestamp: u64,
) {
let topics = (Symbol::new(env, "VerificationGranted"), issuer.clone());
env.events()
.publish(topics, (tier as u32, expiry_timestamp));
}

/// Emitted when an issuer's verification is revoked.
/// Topics: (VerificationRevoked, issuer); Data: ().
pub fn verification_revoked(env: &Env, issuer: &Address) {
let topics = (Symbol::new(env, "VerificationRevoked"), issuer.clone());
env.events().publish(topics, ());
}
131 changes: 131 additions & 0 deletions contracts/invoice-issuer-verification/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Invoice issuer verification badge contract for StellarSettle.
//!
//! The admin grants a verification tier to an issuer wallet after it has
//! passed off-chain due diligence. The badge, visible via `is_verified`, is
//! a trust signal displayed alongside an issuer's invoices to investors.
//! Verification expires automatically at `expiry_timestamp` and can also be
//! revoked immediately by the admin before that.

#![no_std]

mod errors;
mod events;
mod storage;
mod types;

pub use types::{VerificationRecord, VerificationStatus, VerificationTier};

use soroban_sdk::{contract, contractimpl, Address, Env, String};

use errors::Error;

/// Reject the zero address (all-zero 32-byte Ed25519 key).
fn ensure_non_zero_address(env: &Env, address: &Address) -> Result<(), Error> {
let zero_str = String::from_str(
env,
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
);
let zero = Address::from_string(&zero_str);
if *address == zero {
return Err(Error::InvalidAddress);
}
Ok(())
}

#[contract]
pub struct InvoiceIssuerVerification;

#[contractimpl]
impl InvoiceIssuerVerification {
/// Initialize the contract with the admin allowed to grant/revoke verification.
pub fn initialize(env: Env, admin: Address) -> Result<(), Error> {
ensure_non_zero_address(&env, &admin)?;
if storage::get_admin(&env).is_some() {
return Err(Error::AlreadyInit);
}
storage::set_admin(&env, &admin);
events::initialized(&env, &admin);
Ok(())
}

/// Admin-only: grant (or re-grant, overwriting any existing record)
/// a verification tier to `issuer_wallet`, valid until `expiry_timestamp`.
pub fn grant_verification(
env: Env,
admin: Address,
issuer_wallet: Address,
tier: VerificationTier,
expiry_timestamp: u64,
) -> Result<(), Error> {
let stored_admin = storage::get_admin(&env).ok_or(Error::NotInit)?;
if admin != stored_admin {
return Err(Error::Unauthorized);
}
admin.require_auth();
ensure_non_zero_address(&env, &issuer_wallet)?;

if expiry_timestamp <= env.ledger().timestamp() {
return Err(Error::InvalidExpiry);
}

let record = VerificationRecord {
tier,
expiry_timestamp,
};
storage::set_verification(&env, &issuer_wallet, &record);
events::verification_granted(&env, &issuer_wallet, tier, expiry_timestamp);
Ok(())
}

/// Admin-only: immediately revoke `issuer_wallet`'s verification,
/// regardless of its current expiry. `is_verified` returns false for
/// this wallet right after this call, in the same transaction.
pub fn revoke_verification(
env: Env,
admin: Address,
issuer_wallet: Address,
) -> Result<(), Error> {
let stored_admin = storage::get_admin(&env).ok_or(Error::NotInit)?;
if admin != stored_admin {
return Err(Error::Unauthorized);
}
admin.require_auth();

if storage::get_verification(&env, &issuer_wallet).is_none() {
return Err(Error::NotVerified);
}
storage::remove_verification(&env, &issuer_wallet);
events::verification_revoked(&env, &issuer_wallet);
Ok(())
}

/// View: whether `issuer_wallet` currently holds a non-expired
/// verification, and if so, its tier and expiry. A verification whose
/// `expiry_timestamp` is at or before the current ledger time is
/// treated as not verified without needing a separate expiry call —
/// expiry is read-time, not an action that mutates storage.
pub fn is_verified(env: Env, issuer_wallet: Address) -> VerificationStatus {
match storage::get_verification(&env, &issuer_wallet) {
Some(record) if record.expiry_timestamp > env.ledger().timestamp() => {
VerificationStatus {
verified: true,
tier: record.tier,
expiry_timestamp: record.expiry_timestamp,
}
}
_ => VerificationStatus {
verified: false,
tier: VerificationTier::BasicVerified,
expiry_timestamp: 0,
},
}
}

/// View: return the current admin.
pub fn get_admin(env: Env) -> Result<Address, Error> {
storage::get_admin(&env).ok_or(Error::NotInit)
}
}

#[cfg(test)]
mod test;
Loading