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
4 changes: 2 additions & 2 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ pub enum Error {
AlreadyEndorsed = 23,
/// The contract is paused; write operations are temporarily disabled.
ContractPaused = 24,
/// The claim type exceeds 64 characters or contains invalid characters (only alphanumeric and underscores allowed).
InvalidClaimType = 25,
/// Subject is not on the issuer's whitelist and the issuer has whitelist mode enabled.
SubjectNotWhitelisted = 25,
}
52 changes: 52 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,52 @@ impl TrustLinkContract {
Ok(())
}

/// Enable or disable whitelist mode for the calling issuer.
///
/// When enabled, `create_attestation` will reject any subject not present
/// in the issuer's whitelist. Disabled by default.
///
/// # Errors
/// - [`Error::Unauthorized`] — `issuer` is not a registered issuer.
pub fn set_whitelist_enabled(env: Env, issuer: Address, enabled: bool) -> Result<(), Error> {
issuer.require_auth();
Validation::require_issuer(&env, &issuer)?;
Storage::set_whitelist_enabled(&env, &issuer, enabled);
Ok(())
}

/// Add `subject` to the calling issuer's whitelist.
///
/// # Errors
/// - [`Error::Unauthorized`] — `issuer` is not a registered issuer.
pub fn add_to_whitelist(env: Env, issuer: Address, subject: Address) -> Result<(), Error> {
issuer.require_auth();
Validation::require_issuer(&env, &issuer)?;
Storage::add_subject_to_whitelist(&env, &issuer, &subject);
Ok(())
}

/// Remove `subject` from the calling issuer's whitelist.
///
/// # Errors
/// - [`Error::Unauthorized`] — `issuer` is not a registered issuer.
pub fn remove_from_whitelist(env: Env, issuer: Address, subject: Address) -> Result<(), Error> {
issuer.require_auth();
Validation::require_issuer(&env, &issuer)?;
Storage::remove_subject_from_whitelist(&env, &issuer, &subject);
Ok(())
}

/// Return `true` if `subject` is on `issuer`'s whitelist.
pub fn is_whitelisted(env: Env, issuer: Address, subject: Address) -> bool {
Storage::is_subject_whitelisted(&env, &issuer, &subject)
}

/// Return `true` if whitelist mode is enabled for `issuer`.
pub fn is_whitelist_enabled(env: Env, issuer: Address) -> bool {
Storage::is_whitelist_enabled(&env, &issuer)
}

/// Update the trust tier of an already-registered issuer.
///
/// # Errors
Expand Down Expand Up @@ -488,6 +534,12 @@ impl TrustLinkContract {
return Err(Error::Unauthorized);
}

if Storage::is_whitelist_enabled(&env, &issuer)
&& !Storage::is_subject_whitelisted(&env, &issuer, &subject)
{
return Err(Error::SubjectNotWhitelisted);
}

let timestamp = env.ledger().timestamp();
let attestation_id = Attestation::generate_id(env, &issuer, &subject, &claim_type, timestamp);

Expand Down
59 changes: 31 additions & 28 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ pub enum StorageKey {
AuditLog(String),
/// Global pause flag — when present and true, write operations are disabled.
Paused,
/// An attestation request submitted by a subject, keyed by request ID.
AttestationRequest(String),
/// Ordered list of pending request IDs for an issuer address.
IssuerRequests(Address),
/// Whitelist enabled flag per issuer — when true, only whitelisted subjects are accepted.
WhitelistEnabled(Address),
/// Presence flag for a whitelisted subject under a specific issuer.
SubjectWhitelist(Address, Address),
}

const DAY_IN_LEDGERS: u32 = 17280;
Expand Down Expand Up @@ -498,42 +498,45 @@ impl Storage {
env.storage().instance().extend_ttl(ttl, ttl);
}

/// Persist an attestation request and refresh its TTL.
pub fn set_attestation_request(env: &Env, request: &AttestationRequest) {
let key = StorageKey::AttestationRequest(request.id.clone());
let ttl = get_ttl_lifetime(env);
env.storage().persistent().set(&key, request);
env.storage().persistent().extend_ttl(&key, ttl, ttl);
}

/// Retrieve an attestation request by ID.
/// Return `true` if the issuer has whitelist mode enabled.
///
/// # Errors
/// - [`Error::NotFound`] — no request with that ID exists.
pub fn get_attestation_request(env: &Env, id: &String) -> Result<AttestationRequest, Error> {
/// Defaults to `false` (disabled) when the key is absent.
pub fn is_whitelist_enabled(env: &Env, issuer: &Address) -> bool {
env.storage()
.persistent()
.get(&StorageKey::AttestationRequest(id.clone()))
.ok_or(Error::NotFound)
.get(&StorageKey::WhitelistEnabled(issuer.clone()))
.unwrap_or(false)
}

/// Enable or disable whitelist mode for `issuer`.
pub fn set_whitelist_enabled(env: &Env, issuer: &Address, enabled: bool) {
let key = StorageKey::WhitelistEnabled(issuer.clone());
let ttl = get_ttl_lifetime(env);
env.storage().persistent().set(&key, &enabled);
env.storage().persistent().extend_ttl(&key, ttl, ttl);
}

/// Return the ordered list of request IDs for `issuer`, or an empty [`Vec`] if none.
pub fn get_issuer_requests(env: &Env, issuer: &Address) -> Vec<String> {
/// Return `true` if `subject` is whitelisted under `issuer`.
pub fn is_subject_whitelisted(env: &Env, issuer: &Address, subject: &Address) -> bool {
env.storage()
.persistent()
.get(&StorageKey::IssuerRequests(issuer.clone()))
.unwrap_or(Vec::new(env))
.has(&StorageKey::SubjectWhitelist(issuer.clone(), subject.clone()))
}

/// Append `request_id` to `issuer`'s request index and refresh TTL.
pub fn add_issuer_request(env: &Env, issuer: &Address, request_id: &String) {
let key = StorageKey::IssuerRequests(issuer.clone());
/// Add `subject` to `issuer`'s whitelist.
pub fn add_subject_to_whitelist(env: &Env, issuer: &Address, subject: &Address) {
let key = StorageKey::SubjectWhitelist(issuer.clone(), subject.clone());
let ttl = get_ttl_lifetime(env);
let mut requests = Self::get_issuer_requests(env, issuer);
requests.push_back(request_id.clone());
env.storage().persistent().set(&key, &requests);
env.storage().persistent().set(&key, &true);
env.storage().persistent().extend_ttl(&key, ttl, ttl);
}

/// Remove `subject` from `issuer`'s whitelist.
pub fn remove_subject_from_whitelist(env: &Env, issuer: &Address, subject: &Address) {
env.storage()
.persistent()
.remove(&StorageKey::SubjectWhitelist(issuer.clone(), subject.clone()));
}
}

/// Return a paginated window of `values` starting at index `start` for up to
Expand Down
107 changes: 107 additions & 0 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3238,3 +3238,110 @@ fn test_claim_type_with_special_chars_rejected() {
let claim_type = String::from_str(&env, "KYC@PASSED!");
client.create_attestation(&issuer, &subject, &claim_type, &None, &None, &None);
}

// ── subject whitelist tests ───────────────────────────────────────────────────

#[test]
fn test_whitelist_disabled_by_default() {
let env = Env::default();
env.mock_all_auths();
let (_, issuer, client) = setup(&env);
assert!(!client.is_whitelist_enabled(&issuer));
}

#[test]
fn test_attestation_succeeds_without_whitelist() {
let env = Env::default();
env.mock_all_auths();
let (_, issuer, client) = setup(&env);
let subject = Address::generate(&env);
let claim_type = String::from_str(&env, "KYC_PASSED");
// whitelist disabled — any subject is accepted
let id = client.create_attestation(&issuer, &subject, &claim_type, &None, &None, &None);
assert!(!id.is_empty());
}

#[test]
#[should_panic]
fn test_attestation_rejected_when_whitelist_enabled_and_subject_not_listed() {
let env = Env::default();
env.mock_all_auths();
let (_, issuer, client) = setup(&env);
let subject = Address::generate(&env);
let claim_type = String::from_str(&env, "KYC_PASSED");

client.set_whitelist_enabled(&issuer, &true);
// subject not added — should panic with SubjectNotWhitelisted
client.create_attestation(&issuer, &subject, &claim_type, &None, &None, &None);
}

#[test]
fn test_attestation_succeeds_when_subject_is_whitelisted() {
let env = Env::default();
env.mock_all_auths();
let (_, issuer, client) = setup(&env);
let subject = Address::generate(&env);
let claim_type = String::from_str(&env, "KYC_PASSED");

client.set_whitelist_enabled(&issuer, &true);
client.add_to_whitelist(&issuer, &subject);

let id = client.create_attestation(&issuer, &subject, &claim_type, &None, &None, &None);
assert!(!id.is_empty());
}

#[test]
fn test_add_and_remove_from_whitelist() {
let env = Env::default();
env.mock_all_auths();
let (_, issuer, client) = setup(&env);
let subject = Address::generate(&env);

assert!(!client.is_whitelisted(&issuer, &subject));

client.add_to_whitelist(&issuer, &subject);
assert!(client.is_whitelisted(&issuer, &subject));

client.remove_from_whitelist(&issuer, &subject);
assert!(!client.is_whitelisted(&issuer, &subject));
}

#[test]
fn test_issuer_controls_own_whitelist_independently() {
let env = Env::default();
env.mock_all_auths();
let (admin, issuer1, client) = setup(&env);
let issuer2 = Address::generate(&env);
client.register_issuer(&admin, &issuer2);

let subject = Address::generate(&env);
let claim_type = String::from_str(&env, "KYC_PASSED");

// issuer1 enables whitelist and adds subject; issuer2 does not
client.set_whitelist_enabled(&issuer1, &true);
client.add_to_whitelist(&issuer1, &subject);

// issuer1 can attest
let id1 = client.create_attestation(&issuer1, &subject, &claim_type, &None, &None, &None);
assert!(!id1.is_empty());

// issuer2 has no whitelist enabled — can also attest freely
let id2 = client.create_attestation(&issuer2, &subject, &claim_type, &None, &None, &None);
assert!(!id2.is_empty());
}

#[test]
#[should_panic]
fn test_whitelist_check_before_storage_write() {
// Verifies rejection happens before any storage write by checking
// that a failed attestation leaves no attestation record.
let env = Env::default();
env.mock_all_auths();
let (_, issuer, client) = setup(&env);
let subject = Address::generate(&env);
let claim_type = String::from_str(&env, "KYC_PASSED");

client.set_whitelist_enabled(&issuer, &true);
// This must panic — no attestation should be stored
client.create_attestation(&issuer, &subject, &claim_type, &None, &None, &None);
}
Loading