Summary
Add revocation event emission with actor, reason, and timestamp — tax_attribute's revoke_attribute silently detaches a tag from a token with no event at all, unlike every other mutating function in the contract.
Social Media Link
Let's collaborate on Discord. And ensure to star our repo.
Problem Statement
Confirmed in stellar-core/compliance-engine/contracts/tax_attribute/src/lib.rs and stellar-core/compliance-engine/contracts/tax_attribute/src/events.rs:
-
revoke_attribute emits nothing on success: lib.rs:220-265 authorizes the caller, loads the TaxAttributeTag, checks caller == admin || caller == attribute.issuing_authority, removes the tag_id from TokenAttributes(token_id), and returns Ok(()) — there is no env.events().publish(...) call anywhere in this function, unlike add_issuer and remove_issuer which both publish Symbol::short("iss_add")/("iss_rem") events (lib.rs:88-119, 121-155).
-
No actor is recorded anywhere: the function receives caller: Address but discards it after the authorization check — there is no persistent or event-logged trace of who revoked the attribute.
-
No reason field exists on the revocation path at all: revoke_attribute's signature (lib.rs:220-225) is (env, caller, token_id, tag_id) — there is no parameter to capture why the attribute was revoked (e.g. regulatory change, issuer error, fraud finding).
-
No timestamp is captured for the revocation itself: TaxAttributeTag (lib.rs:14-25) has attached_at: u64 for creation but nothing equivalent for revocation — once removed from TokenAttributes, the original Attribute(tag_id) entry in persistent storage is left untouched (not deleted), so there is stale data with no marker of when or why it stopped applying to the token.
-
events.rs only defines initialization-related events: Initialized and ReinitializationAttempted (events.rs:3-13) — there is no AttributeRevokedEvent type at all, despite Event::IssuerAdded/Event::IssuerRemoved being declared as an enum in lib.rs:36-40 that is itself never actually published either (dead code — add_issuer/remove_issuer publish raw tuples via env.events().publish((Symbol::short(...),), ...) instead of using the Event enum).
-
get_attributes_for_token cannot distinguish "never attached" from "revoked": since revoke_attribute only removes the tag_id from the token's index (lib.rs:255-260) without marking the underlying Attribute(tag_id) record as revoked, any code path that still holds a reference to the old tag_id and calls get_attribute directly (if such a function existed) would see no indication of revocation.
-
Off-chain auditors and the carbon_asset/regulatory_checks contracts have no signal to react to a revocation: since no event fires, downstream indexers or compliance dashboards cannot detect a tax attribute being pulled from a token without polling get_attributes_for_token before and after and diffing results.
Required Changes
-
Add AttributeRevokedEvent { token_id: u32, tag_id: String, revoked_by: Address, reason: String, timestamp: u64 } to events.rs, following the existing contractevent pattern used by Initialized/ReinitializationAttempted.
-
Add a reason: String parameter to revoke_attribute's signature: revoke_attribute(env, caller: Address, token_id: u32, tag_id: String, reason: String) -> Result<(), ContractError>.
-
Inside revoke_attribute, after successfully removing the tag_id from TokenAttributes(token_id), publish AttributeRevokedEvent with caller as revoked_by, the passed reason, and env.ledger().timestamp().
-
Add a corresponding emit_attribute_revoked_event(env, token_id, tag_id, revoked_by, reason) helper function to events.rs, matching the style of emit_initialized_event/emit_reinitialization_attempted_event.
-
Remove the unused Event enum (IssuerAdded/IssuerRemoved) from lib.rs or actually wire add_issuer/remove_issuer to publish it via .publish(&env) instead of raw Symbol::short tuples, for internal consistency — flag as a related but separate cleanup if out of scope for this issue.
-
Add unit tests: revoking an attribute emits AttributeRevokedEvent with the correct token_id, tag_id, revoked_by, reason, and timestamp; revocation by the admin vs. the original issuing authority both emit the event correctly with the respective caller as revoked_by.
Acceptance Criteria
revoke_attribute accepts a reason: String parameter.
- A successful revocation emits
AttributeRevokedEvent containing the token ID, tag ID, caller address, reason, and ledger timestamp.
- Both admin-initiated and issuer-initiated revocations correctly attribute
revoked_by to the actual caller.
- Existing revocation authorization logic (admin or original issuer only) is unchanged.
- Existing
AttributeNotFound/AttributeNotAttached/NotAuthorized error paths continue to behave as before.
- Test coverage includes event emission verification for both authorized-caller paths.
Directory to Work on:
stellar-core/compliance-engine/
Summary
Add revocation event emission with actor, reason, and timestamp —
tax_attribute'srevoke_attributesilently detaches a tag from a token with no event at all, unlike every other mutating function in the contract.Social Media Link
Let's collaborate on Discord. And ensure to star our repo.
Problem Statement
Confirmed in
stellar-core/compliance-engine/contracts/tax_attribute/src/lib.rsandstellar-core/compliance-engine/contracts/tax_attribute/src/events.rs:revoke_attributeemits nothing on success:lib.rs:220-265authorizes the caller, loads theTaxAttributeTag, checkscaller == admin || caller == attribute.issuing_authority, removes thetag_idfromTokenAttributes(token_id), and returnsOk(())— there is noenv.events().publish(...)call anywhere in this function, unlikeadd_issuerandremove_issuerwhich both publishSymbol::short("iss_add")/("iss_rem")events (lib.rs:88-119,121-155).No actor is recorded anywhere: the function receives
caller: Addressbut discards it after the authorization check — there is no persistent or event-logged trace of who revoked the attribute.No reason field exists on the revocation path at all:
revoke_attribute's signature (lib.rs:220-225) is(env, caller, token_id, tag_id)— there is no parameter to capture why the attribute was revoked (e.g. regulatory change, issuer error, fraud finding).No timestamp is captured for the revocation itself:
TaxAttributeTag(lib.rs:14-25) hasattached_at: u64for creation but nothing equivalent for revocation — once removed fromTokenAttributes, the originalAttribute(tag_id)entry in persistent storage is left untouched (not deleted), so there is stale data with no marker of when or why it stopped applying to the token.events.rsonly defines initialization-related events:InitializedandReinitializationAttempted(events.rs:3-13) — there is noAttributeRevokedEventtype at all, despiteEvent::IssuerAdded/Event::IssuerRemovedbeing declared as an enum inlib.rs:36-40that is itself never actually published either (dead code —add_issuer/remove_issuerpublish raw tuples viaenv.events().publish((Symbol::short(...),), ...)instead of using theEventenum).get_attributes_for_tokencannot distinguish "never attached" from "revoked": sincerevoke_attributeonly removes thetag_idfrom the token's index (lib.rs:255-260) without marking the underlyingAttribute(tag_id)record as revoked, any code path that still holds a reference to the oldtag_idand callsget_attributedirectly (if such a function existed) would see no indication of revocation.Off-chain auditors and the
carbon_asset/regulatory_checkscontracts have no signal to react to a revocation: since no event fires, downstream indexers or compliance dashboards cannot detect a tax attribute being pulled from a token without pollingget_attributes_for_tokenbefore and after and diffing results.Required Changes
Add
AttributeRevokedEvent { token_id: u32, tag_id: String, revoked_by: Address, reason: String, timestamp: u64 }toevents.rs, following the existingcontracteventpattern used byInitialized/ReinitializationAttempted.Add a
reason: Stringparameter torevoke_attribute's signature:revoke_attribute(env, caller: Address, token_id: u32, tag_id: String, reason: String) -> Result<(), ContractError>.Inside
revoke_attribute, after successfully removing thetag_idfromTokenAttributes(token_id), publishAttributeRevokedEventwithcallerasrevoked_by, the passedreason, andenv.ledger().timestamp().Add a corresponding
emit_attribute_revoked_event(env, token_id, tag_id, revoked_by, reason)helper function toevents.rs, matching the style ofemit_initialized_event/emit_reinitialization_attempted_event.Remove the unused
Eventenum (IssuerAdded/IssuerRemoved) fromlib.rsor actually wireadd_issuer/remove_issuerto publish it via.publish(&env)instead of rawSymbol::shorttuples, for internal consistency — flag as a related but separate cleanup if out of scope for this issue.Add unit tests: revoking an attribute emits
AttributeRevokedEventwith the correcttoken_id,tag_id,revoked_by,reason, andtimestamp; revocation by the admin vs. the original issuing authority both emit the event correctly with the respective caller asrevoked_by.Acceptance Criteria
revoke_attributeaccepts areason: Stringparameter.AttributeRevokedEventcontaining the token ID, tag ID, caller address, reason, and ledger timestamp.revoked_byto the actual caller.AttributeNotFound/AttributeNotAttached/NotAuthorizederror paths continue to behave as before.Directory to Work on:
stellar-core/compliance-engine/