Common Types and Events
Two things every contract in the protocol shares: a single crate of cross-contract types, and one consistent event schema. Both exist to make the indexer (and any other off-chain consumer) buildable against a frozen, unambiguous contract surface.
The common-types crate
Soroban has no built-in way for one contract to "import" another contract's #[contracttype] definitions — each contract only ships its own compiled WASM plus an XDR spec. Before this crate existed, every contract that needed to decode a cross-contract call result (e.g. the Factory calling Globals::get_global_settings) hand-copied the relevant struct definitions into its own source. Soroban's struct encoding is a map keyed by field name, and it traps at runtime — with no compiler help — the moment a caller's hand-copied struct has a different set of fields than the real one.
That is exactly what happened on testnet: the Factory's hand-mirrored GlobalSettings drifted from the real, growing struct in Globals, and every call that decoded it (including the Factory's own upgrade) began trapping. See Testnet Deployment for the incident and Governance for the upgrade-path rule that came out of it.
common-types now holds the single, canonical definition of every cross-contract type — GlobalSettings, Manager, ManagerCollateral, Pledge/PledgeState, VaultParams — plus #[contractclient] traits (GlobalsClient, CollateralEscrowClient, ComplianceGateClient) so Factory, Vault, and CollateralEscrow all call each other through the same generated client code rather than hand-declared, driftable copies. A CI check (scripts/check-type-drift.sh) guards against a hand-mirrored composite type reappearing anywhere in the workspace.
This crate does not by itself prevent every drift-shaped bug — an upgrade or admin-recovery path that decodes even the shared, correctly-imported composite type is still unsafe, because that type can still grow a field between when a contract was deployed and when it's asked to decode a fresher one. That's why upgrade paths specifically read only primitive-shaped values (is_timelock_admin_required() -> bool, get_timelock() -> Option<Address>) instead. See the Upgrade-path rule in Governance.
Event schema
Every state-changing function across every contract emits exactly one event, generated from that contract's events.rs. All events share the same topic shape:
("trilo", <event_name: Symbol>, <schema_version: u32>, ...any other #[topic] fields)schema_version starts at 1 and increments only on a breaking change to that event's data struct — an indexer can therefore key its parser on (event_name, schema_version) and know exactly what shape to expect, even across a contract upgrade.
globals
RoleUpdated
role
new_address
ProtocolPaused
—
paused
ManagerApproved
manager
—
ManagerRemoved
manager
—
AssetWhitelisted
asset
—
AssetRemoved
asset
—
CollateralAssetWhitelisted
asset
—
CollateralAssetRemoved
asset
—
FeeUpdated
—
new_fee
SettingsUpdated
caller
protocol_fee, min_loan_amount, max_loan_amount
CollateralDeposited
manager
token, amount, total_staked
CollateralWithdrawn
manager
amount, total_staked
CollateralLocked
manager
amount, total_locked
CollateralReleased
manager
amount, total_locked
CollateralSlashed
manager
amount, remaining_staked
ManagerMarkedDelinquent
manager
delinquency_count
ManagerDelinquencyCleared
manager
—
ManagerCreditLimitSet
manager
max_credit
ManagerOutstandingUpdated
manager
outstanding_principal
BorrowerOutstandingUpdated
borrower
outstanding_principal, total_outstanding
ComplianceGateSet
—
gate: Option<Address>
factory
VaultDeployed
vault, manager, borrower
principal, loan_term, interest_rate, leverage_ratio, related_party
WasmHashUpdated
—
old_hash, new_hash
vault
VaultCreated
borrower
principal, loan_term, interest_rate, leverage_ratio, junior_target, senior_target, related_party
InvestorDeposited
investor
tranche, amount, junior_raised, senior_raised
InvestorWithdrew
investor
tranche, amount
JuniorLocked
—
junior_raised, junior_target
VaultFullyFunded
—
total_funded
LoanDisbursed
borrower
amount
PhaseTransition
—
from, to
PaymentReceived
payer
amount, reserve_paid, senior_interest_paid, senior_principal_paid, junior_interest_paid, junior_principal_paid, residual, outstanding_s, outstanding_j, schedule_advanced
YieldClaimed
investor
tranche, amount
CashWithdrawn
borrower
amount
LateFeeApplied
—
fee, fee_senior, fee_junior, total_late_fees, missed_payments
VaultDefaulted
borrower
outstanding_s, outstanding_j, missed_payments
RecoveryDistributed
—
senior_recovery_pool, junior_recovery_pool, manager_residual, borrower_residual
RecoveryClaimed
investor
tranche, amount
RenegotiationProposed
manager
new_interest_rate, new_loan_term
RenegotiationFinalized
—
manager, borrower, new_interest_rate, new_loan_term, new_installment
RenegotiationRejected
caller
—
ProtocolFeeCollected
treasury
fee_amount, source
ProtocolFeeClamped
—
configured_fee, charged_fee
InvestorAllowlisted
investor
—
InvestorRemovedFromAllowlist
investor
—
VaultFundingExpired
—
deadline, pending_refund
VaultApprovalExpired
—
deadline, pending_refund
RefundClaimed
investor
tranche, amount
ReportReceived
—
reported_at
ReportingOverdue
—
last_report_at, checked_at
collateral-escrow
PledgeLocked
borrower
pledge_id, token, amount, controller
PledgeReleased
—
pledge_id, amount
PledgeSeized
to
pledge_id, amount
compliance-allowlist
AllowlistUpdated
who
action, allowed
As of September 2026.
Last updated