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
7 changes: 7 additions & 0 deletions contracts/EVENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ assert_eq!(name, Symbol::new(&env, "asset_registered"));
| `contract_paused` | `admin` | `timestamp` | `pause_contract` |
| `contract_unpaused` | `admin` | `timestamp` | `unpause_contract` |

### Branches

| Event (topic 0) | Topic 1 | Data | Emitted by |
|---|---|---|---|
| `branch_registered` | `branch_id` | `admin`, `timestamp` | `register_branch` |
| `asset_assigned_to_branch` | `branch_id`, `asset_id` | `caller`, `timestamp` | `assign_asset_to_branch` |

### Tokenization

| Event | Topic 1 | Data | Emitted by |
Expand Down
35 changes: 28 additions & 7 deletions contracts/assetsup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,28 @@ permanently bricks administration. A two-step transfer is tracked in [SC-48].
> [SC-42]; it is the highest-severity item in the workspace.

Reads: `get_asset`, `get_asset_info`, `batch_get_asset_info`,
`get_assets_by_owner`, `check_asset_exists`, `get_total_asset_count`,
`get_admin`, `is_paused`, `is_authorized_registrar`, `get_contract_metadata`,
`get_asset_audit_logs`.
`get_assets_by_owner`, `get_asset_transfer_history`, `check_asset_exists`,
`get_total_asset_count`, `get_admin`, `is_paused`, `is_authorized_registrar`,
`get_contract_metadata`, `get_asset_audit_logs`.

### Branches

| Entrypoint | Auth |
|---|---|
| `register_branch` | admin |
| `assign_asset_to_branch` | admin or branch admin |
| `get_branch`, `get_branch_assets` | — |

`assign_asset_to_branch` rejects an unknown branch (`BranchNotFound`) and an
unknown asset (`AssetNotFound`), so an asset can never be pointed at a stale or
misspelled branch id.

### Transfer history

`get_asset_transfer_history` returns the asset's append-only list of ownership
transfers (oldest first), recorded on every `transfer_asset_ownership` call. The
on-chain audit log and its reconciliation with the off-chain `audit-logs`
service are documented in [`src/audit.rs`](src/audit.rs).

### Tokenization

Expand Down Expand Up @@ -192,6 +211,7 @@ threshold.
| `("admin_chg",)` | `update_admin` |
| `("c_pause",)` | `pause_contract` |
| `("c_unpause",)` | `unpause_contract` |
| `("branch_registered",)`, `("asset_assigned_to_branch",)` | `register_branch`, `assign_asset_to_branch` |
| `("lease_new",)`, `("lease_ret",)`, `("lease_can",)`, `("lease_exp",)` | lease lifecycle |
| `lease_terminated_early` | `return_leased_asset` when returned before the end date |

Expand All @@ -201,10 +221,11 @@ for the workspace-wide event catalogue.

## Errors

`Error`, defined in [`src/error.rs`](src/error.rs), codes 1–46, grouped by
concern (registry 1–9, tokenization 10–20, voting 21–25, dividends 26–27,
detokenization 28–29, valuation 30, holders 31, math 32–33, contract state
34–35, validation 36–39, leasing 40–46).
`Error`, defined in [`src/error.rs`](src/error.rs), follows the workspace
allocation in [`../ERRORS.md`](../ERRORS.md): shared codes 1–99 mean the same
thing in every contract, and `assetsup`-specific codes occupy 100–199 (registry
100–119, tokenization 120–139, voting 140–149, dividends 150–159, detokenization
160–169, validation 170–179, leasing and insurance 180–199).

Note that most entrypoints return `Result<_, Error>`, but `handle_error` panics
with the error instead of returning it in some paths, so callers see a trap
Expand Down
16 changes: 16 additions & 0 deletions contracts/assetsup/src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub enum DataKey {
Asset(BytesN<32>),
OwnerRegistry(Address),
AssetCounter,
/// Append-only, oldest-first list of ownership transfers for one asset.
TransferHistory(BytesN<32>),
}

#[contracttype]
Expand Down Expand Up @@ -36,4 +38,18 @@ pub struct AssetInfo {
pub status: AssetStatus,
}

/// One entry in an asset's on-chain transfer history.
///
/// Recorded on every successful [`crate::AssetUpContract::transfer_asset_ownership`]
/// call, oldest first, so integrators can reconstruct the chain of custody
/// without replaying events. `from` is the owner that transferred the asset and
/// `to` is the new owner.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TransferRecord {
pub from: Address,
pub to: Address,
pub timestamp: u64,
}

// Note: Contract methods implemented in lib.rs
37 changes: 37 additions & 0 deletions contracts/assetsup/src/audit.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
//! On-chain audit log and its reconciliation contract with the off-chain
//! `audit-logs` service.
//!
//! ## What is written on-chain
//!
//! Every significant registry action appends an [`AuditEntry`] to the asset's
//! log. The entry is intentionally minimal and self-describing:
//!
//! | Field | Meaning | Reconciliation key |
//! |---|---|---|
//! | `timestamp` | Ledger close time of the action | Part of the composite key |
//! | `action` | Stable machine token, e.g. `ASSET_REGISTERED` | Part of the composite key |
//! | `actor` | Address that triggered the action | Part of the composite key |
//! | `details` | Human-readable context | Not used for matching |
//!
//! ## Reconciliation model
//!
//! The on-chain log is the **source of truth**. The backend `audit-logs`
//! service keeps a richer record and must reconcile against the chain as
//! follows:
//!
//! 1. Index [`crate::events`] for the same action; each event carries the
//! transaction hash and ledger sequence that the on-chain entry also falls
//! under. Store that transaction hash on the off-chain record.
//! 2. Treat `(asset_id, action, actor, timestamp)` as the composite identity of
//! an entry. Appending is idempotent under this key, so replaying a ledger
//! range never produces duplicates.
//! 3. On divergence, the on-chain entry wins: an off-chain record with no
//! matching entry is an orphan and must be flagged, and an on-chain entry
//! with no off-chain record must be backfilled from the chain.
//! 4. `details` is display-only. Never match or reconcile on it, because its
//! wording may change between contract versions.
//!
//! Actions are append-only and are never mutated or deleted on-chain, so the
//! log may be replayed from genesis at any time to rebuild the off-chain
//! index.

use soroban_sdk::{contracttype, Address, BytesN, Env, String, Vec};

#[contracttype]
Expand Down
24 changes: 24 additions & 0 deletions contracts/assetsup/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ pub enum Error {
/// No expiry reminder is due: the policy is inactive, already expired, or
/// still outside its pre-expiry reminder window.
ExpiryReminderNotDue = 187,
/// No insurance policy is stored under this id.
PolicyNotFound = 188,
/// No insurance claim is stored under this id.
ClaimNotFound = 189,
/// The policy is not in a state that permits this action.
InvalidPolicyStatus = 190,
/// The claim is not in a state that permits this action.
InvalidClaimStatus = 191,
/// The policy terms are unusable (bad coverage, premium, or dates).
InvalidPolicyTerms = 192,
/// The claim amount is zero, negative, or above the policy coverage.
InvalidClaimAmount = 193,
/// A policy with this id already exists.
PolicyAlreadyExists = 194,
/// A claim with this id already exists.
ClaimAlreadyExists = 195,
}

pub fn handle_error(env: &Env, error: Error) -> ! {
Expand Down Expand Up @@ -256,6 +272,14 @@ mod tests {
Error::LeaseAlreadyStarted as u32,
Error::LeaseNotExpired as u32,
Error::InvalidTimestamps as u32,
Error::PolicyNotFound as u32,
Error::ClaimNotFound as u32,
Error::InvalidPolicyStatus as u32,
Error::InvalidClaimStatus as u32,
Error::InvalidPolicyTerms as u32,
Error::InvalidClaimAmount as u32,
Error::PolicyAlreadyExists as u32,
Error::ClaimAlreadyExists as u32,
Error::ExpiryReminderNotDue as u32,
];

Expand Down
46 changes: 46 additions & 0 deletions contracts/assetsup/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ pub struct AssetRetired {
pub timestamp: u64,
}

/// A branch was registered.
#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BranchRegistered {
#[topic]
pub branch_id: BytesN<32>,
pub admin: Address,
pub timestamp: u64,
}

/// An asset was assigned to a branch.
#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AssetAssignedToBranch {
#[topic]
pub branch_id: BytesN<32>,
#[topic]
pub asset_id: BytesN<32>,
pub caller: Address,
pub timestamp: u64,
}

/// An admin transfer was nominated. The role does **not** move until the
/// proposed address accepts.
#[contractevent]
Expand Down Expand Up @@ -436,6 +458,30 @@ pub fn asset_retired(env: &Env, asset_id: &BytesN<32>, caller: &Address) {
.publish(env);
}

pub fn branch_registered(env: &Env, branch_id: &BytesN<32>, admin: &Address) {
BranchRegistered {
branch_id: branch_id.clone(),
admin: admin.clone(),
timestamp: env.ledger().timestamp(),
}
.publish(env);
}

pub fn asset_assigned_to_branch(
env: &Env,
branch_id: &BytesN<32>,
asset_id: &BytesN<32>,
caller: &Address,
) {
AssetAssignedToBranch {
branch_id: branch_id.clone(),
asset_id: asset_id.clone(),
caller: caller.clone(),
timestamp: env.ledger().timestamp(),
}
.publish(env);
}

pub fn admin_proposed(env: &Env, current_admin: &Address, proposed_admin: &Address) {
AdminProposed {
proposed_admin: proposed_admin.clone(),
Expand Down
Loading
Loading