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
18 changes: 17 additions & 1 deletion COMEBACKHERE-contracts/contracts/compliance/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![no_std]

use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, Symbol, Vec};
use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, Symbol, Vec,
};

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
Expand Down Expand Up @@ -61,6 +63,20 @@ const MAX_BATCH_SIZE: u32 = 50;

#[contractimpl]
impl ComplianceContract {
/// Replaces this contract's Wasm while preserving its address and storage.
/// The stored admin must authorize the call.
pub fn upgrade(e: Env, new_wasm_hash: BytesN<32>) -> Result<(), ContractError> {
let admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
e.deployer()
.update_current_contract_wasm(new_wasm_hash.clone());
e.events().publish(
(Symbol::new(&e, "upgraded"),),
new_wasm_hash,
);
Ok(())
}

pub fn initialize(e: Env, admin: Address) {
e.storage().instance().set(&DataKey::Admin, &admin);
e.storage().instance().set(&DataKey::Paused, &false);
Expand Down
7 changes: 6 additions & 1 deletion COMEBACKHERE-contracts/contracts/invoice/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use soroban_sdk::{Address, Env, Symbol};
use soroban_sdk::{Address, BytesN, Env, Symbol};

pub fn upgraded(env: &Env, new_wasm_hash: BytesN<32>) {
env.events()
.publish((Symbol::new(env, "upgraded"),), new_wasm_hash);
}

pub fn invoice_created(env: &Env, merchant: &Address, invoice_id: &u64) {
env.events().publish(
Expand Down
28 changes: 26 additions & 2 deletions COMEBACKHERE-contracts/contracts/invoice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
mod events;

use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, Address, Env, IntoVal, String, Symbol,
Vec,
contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, IntoVal, String,
Symbol, Vec,
};

/// Maximum length, in bytes, allowed for the optional `reference` field on an invoice.
const MAX_REFERENCE_LEN: u32 = 64;

/// Maximum number of invoice IDs accepted by a single batch operation.
const MAX_BATCH_SIZE: u32 = 50;

/// Minimum invoice amount, in stroops (10,000,000 stroops == 1 USDC given 7 decimals).
const MIN_AMOUNT_USDC: i128 = 10_000_000;

Expand Down Expand Up @@ -38,6 +41,8 @@ pub enum ContractError {
/// (e.g. `mark_paids` called on an invoice that is `RefundRequested`,
/// `Released`, `Cancelled`, or `Expired`).
InvalidStateTransition = 18,
/// A batch operation was called with more than `MAX_BATCH_SIZE` invoice IDs.
BatchTooLarge = 19,
}

#[contracttype]
Expand Down Expand Up @@ -116,6 +121,17 @@ pub struct InvoiceContract;

#[contractimpl]
impl InvoiceContract {
/// Replaces this contract's Wasm while preserving its address and storage.
/// The stored admin must authorize the call.
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), ContractError> {
let contract_admin = admin(&env);
contract_admin.require_auth();
env.deployer()
.update_current_contract_wasm(new_wasm_hash.clone());
events::upgraded(&env, new_wasm_hash);
Ok(())
}

/// Initialises the contract, setting the admin address and default configuration.
///
/// # Parameters
Expand Down Expand Up @@ -335,6 +351,7 @@ impl InvoiceContract {
/// # Errors
/// - [`ContractError::ContractPaused`] if the contract is currently paused.
/// - [`ContractError::InvoiceNotFound`] if any ID in the batch does not exist.
/// - [`ContractError::BatchTooLarge`] if `invoice_ids` has more than `MAX_BATCH_SIZE` IDs.
/// - [`ContractError::InvalidStateTransition`] if any invoice is `RefundRequested`,
/// `Released`, `Cancelled`, or `Expired` — a payment confirmation must never
/// silently override a refund already in progress or a closed invoice.
Expand All @@ -345,6 +362,9 @@ impl InvoiceContract {
/// Emits `invoice_paid(invoice_id)` for each successfully marked invoice.
pub fn mark_paids(env: Env, invoice_ids: Vec<u64>) -> Result<(), ContractError> {
check_not_paused(&env)?;
if invoice_ids.len() > MAX_BATCH_SIZE {
return Err(ContractError::BatchTooLarge);
}

// Resolve compliance contract once; if set, every invoice
// customer must be allowed.
Expand Down Expand Up @@ -573,11 +593,15 @@ impl InvoiceContract {
/// # Errors
/// - [`ContractError::ContractPaused`] if the contract is currently paused.
/// - [`ContractError::InvoiceNotFound`] if any ID in the batch does not exist.
/// - [`ContractError::BatchTooLarge`] if `invoice_ids` has more than `MAX_BATCH_SIZE` IDs.
///
/// # Events
/// Emits `invoice_expired(invoice_id)` for each invoice that transitions to `Expired`.
pub fn batch_expire(env: Env, invoice_ids: Vec<u64>) -> Result<(), ContractError> {
check_not_paused(&env)?;
if invoice_ids.len() > MAX_BATCH_SIZE {
return Err(ContractError::BatchTooLarge);
}
let now = env.ledger().timestamp();
for id in invoice_ids.iter() {
let mut invoice = env
Expand Down
38 changes: 37 additions & 1 deletion COMEBACKHERE-contracts/contracts/treasury/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
extern crate std;

use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, Address, Env, IntoVal, Symbol, Vec,
contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, IntoVal, Symbol,
Vec,
};

/// Status of a settlement proposal within the Treasury contract.
Expand Down Expand Up @@ -83,6 +84,8 @@ pub enum TreasuryError {
/// `daily_withdraw_limit` and the withdrawal would push cumulative
/// withdrawals for the current 24h window above that limit.
DailyLimitExceeded = 13,
/// An upgrade was requested while a settlement was partially executed.
UpgradeInProgress = 14,
}

/// Storage keys for Treasury contract instance state.
Expand Down Expand Up @@ -133,6 +136,39 @@ pub struct TreasuryContract;

#[contractimpl]
impl TreasuryContract {
/// Replaces this contract's Wasm while preserving its address and storage.
/// The stored admin must authorize the call. Upgrades are rejected while any
/// settlement is in the partially executed state.
pub fn upgrade(e: Env, new_wasm_hash: BytesN<32>) -> Result<(), TreasuryError> {
let admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap();
Self::check_admin(&e, &admin)?;

let next_settlement_id: u64 = e
.storage()
.instance()
.get(&DataKey::NextSettlementId)
.unwrap_or(1u64);
for settlement_id in 1..next_settlement_id {
if let Some(settlement) = e
.storage()
.instance()
.get::<DataKey, Settlement>(&DataKey::Settlement(settlement_id))
{
if settlement.status == SettlementStatus::PartiallyExecuted {
return Err(TreasuryError::UpgradeInProgress);
}
}
}

e.deployer()
.update_current_contract_wasm(new_wasm_hash.clone());
e.events().publish(
(Symbol::new(&e, "upgraded"),),
new_wasm_hash,
);
Ok(())
}

pub fn initialize(
e: Env,
signers: Vec<(Address, u64)>,
Expand Down
18 changes: 7 additions & 11 deletions abis/compliance.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,27 @@
"contract": "compliance",
"version": "1.0.0",
"functions": [
"upgrade",
"initialize",
"is_allowed",
"get_address_status",
"allow_address",
"block_address",
"allow_address_until",
"batch_allow_addresses",
"transfer_admin",
"accept_admin",
"clear_address",
"pause",
"unpause"
],
"events": [
"address_allowed",
"address_blocked",
"address_cleared",
"address_allowed_until",
"admin_transfer_initiated",
"admin_transferred",
"contract_paused",
"contract_unpaused"
],
"events": ["upgraded", "address_allowed", "address_blocked", "address_allowed_until", "compliance_batch_processed", "accept_admin", "address_cleared", "contract_paused", "contract_unpaused"],
"errors": {
"1": "Unauthorized",
"2": "ContractPaused",
"3": "AlreadyInitialized"
"3": "AlreadyInitialized",
"4": "AddressNotFound",
"5": "PastExpiry",
"6": "BatchTooLarge"
}
}
3 changes: 2 additions & 1 deletion abis/invoice.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"contract": "invoice",
"version": "1.1.0",
"functions": [
"upgrade",
"initialize",
"create_invoice",
"get_invoice",
Expand All @@ -20,5 +21,5 @@
"set_grace_window",
"get_grace_window"
],
"events": ["invoice_created", "invoice_paid", "invoice_expired", "invoice_cancelled", "invoice_refund_req", "escrow_released", "contract_paused", "contract_unpaused", "dispute_raised"]
"events": ["upgraded", "invoice_created", "invoice_paid", "invoice_expired", "invoice_cancelled", "invoice_refund_req", "escrow_released", "contract_paused", "contract_unpaused", "dispute_raised"]
}
18 changes: 7 additions & 11 deletions abis/treasury.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
"contract": "treasury",
"version": "1.1.0",
"functions": [
"upgrade",
"initialize",
"set_signer",
"rotate_signer",
"propose_settlement",
"approve_settlement",
"execute_settlement",
Expand All @@ -13,24 +15,18 @@
"unpause",
"get_threshold",
"update_threshold",
"get_total_signer_weight",
"raise_dispute",
"resolve_dispute",
"update_settlement_merchant",
"get_settlement",
"deposit",
"set_daily_withdraw_limit",
"get_daily_withdraw_limit",
"withdraw",
"add_token_to_allowlist",
"remove_token_from_allowlist"
],
"errors": {
"1": "ContractPaused",
"2": "NotPending",
"3": "InsufficientApprovals",
"4": "DisputeNotFound",
"5": "DisputeAlreadyRaised",
"6": "DisputeNotRaised",
"7": "AlreadyVoted",
"8": "UnauthorizedSigner",
"9": "ThresholdNotMet"
}
"events": ["upgraded", "signer_rotated", "balance", "contract_paused", "contract_unpaused", "threshold_updated", "merchant_updated", "daily_withdraw_limit_set"],
"threshold": "2-of-3"
}
35 changes: 28 additions & 7 deletions docs/MAINNET_DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,8 @@ A `SYSTEM` contract event is emitted automatically on upgrade with:
- `data = []`

Backend services that monitor contract events can use this to detect upgrades.
Each protocol contract also emits an application-level `upgraded` event whose
data is the uploaded Wasm hash.

### Upgrade Procedure

Expand Down Expand Up @@ -442,12 +444,23 @@ Backend services that monitor contract events can use this to detect upgrades.
5. **Invoke the upgrade function** through the standard ceremony process:

```sh
stellar contract invoke \
--id <CONTRACT_ID> \
--source-account <ADMIN_KEY> \
--network mainnet \
-- upgrade \
--new_wasm_hash <NEW_WASM_HASH>
# Invoice
INVOICE_WASM_HASH=$(stellar contract upload --source-account "$ADMIN_KEY" \
--wasm target/wasm32-unknown-unknown/release/comebackhere_invoice.wasm --network mainnet)
stellar contract invoke --id "$INVOICE_CONTRACT_ID" --source-account "$ADMIN_KEY" \
--network mainnet -- upgrade --new_wasm_hash "$INVOICE_WASM_HASH"

# Treasury
TREASURY_WASM_HASH=$(stellar contract upload --source-account "$ADMIN_KEY" \
--wasm target/wasm32-unknown-unknown/release/comebackhere_treasury.wasm --network mainnet)
stellar contract invoke --id "$TREASURY_CONTRACT_ID" --source-account "$ADMIN_KEY" \
--network mainnet -- upgrade --new_wasm_hash "$TREASURY_WASM_HASH"

# Compliance
COMPLIANCE_WASM_HASH=$(stellar contract upload --source-account "$ADMIN_KEY" \
--wasm target/wasm32-unknown-unknown/release/comebackhere_compliance.wasm --network mainnet)
stellar contract invoke --id "$COMPLIANCE_CONTRACT_ID" --source-account "$ADMIN_KEY" \
--network mainnet -- upgrade --new_wasm_hash "$COMPLIANCE_WASM_HASH"
```

6. **Verify** the upgrade by querying contract state and running the
Expand All @@ -460,7 +473,15 @@ if a new contract was deployed rather than upgraded in-place.

### Upgrade Authorization

Each contract enforces admin authorization in its `upgrade` function:
Each contract's `upgrade(new_wasm_hash)` entrypoint loads the stored admin and
requires that address to authorize the call. This is a single-key on-chain
authorization; the treasury signer threshold does not gate contract upgrades.
The multi-sig process described above is currently an off-chain governance
control. A compromised admin key could bypass that process, so maintainers
should consider moving upgrade authorization to the treasury multisig in a
future change before relying on these entrypoints for mainnet governance.

The on-chain authorization pattern is:

```rust
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
Expand Down
4 changes: 4 additions & 0 deletions docs/contract-interaction-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ soroban contract invoke \
--invoice_ids '[1]'
```

`mark_paids` and `batch_expire` accept at most 50 invoice IDs per call. Larger
jobs must be split into chunks of 50 or fewer; a larger batch fails with
`ContractError::BatchTooLarge` before any invoice is changed.

---

### Raise a dispute
Expand Down
2 changes: 2 additions & 0 deletions docs/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Defined in `COMEBACKHERE-contracts/contracts/invoice/src/lib.rs`. Shares some va
| 16 | `Overflow` | An internal counter (invoice ID, or `created_at + grace_window`) would overflow `u64`. | Practically unreachable outside of adversarial ledger state; not user-actionable. |
| 17 | `AddressBlocked` | `mark_paids` was called for a customer that the configured compliance contract reports as not allowed. | Confirm the customer's compliance status with `ComplianceContract.is_allowed` before retrying. |
| 18 | `InvalidStateTransition` | `mark_paids` was called on an invoice in `RefundRequested`, `Released`, `Cancelled`, or `Expired` status — see [ARCHITECTURE.md § Invoice state machine](../ARCHITECTURE.md#invoice-state-machine) for the full legal-transition diagram. | Fetch the current status with `get_invoice_status` first. A refund already in progress must not be overridden by a stale payment confirmation. |
| 19 | `BatchTooLarge` | `mark_paids` or `batch_expire` was called with more than 50 invoice IDs. | Split the input into batches of 50 or fewer and submit multiple calls. |

---

Expand Down Expand Up @@ -121,6 +122,7 @@ Defined in `COMEBACKHERE-contracts/contracts/treasury/src/lib.rs`.
| 6 | `InvalidThreshold` | `update_threshold` was called with a threshold of 0. | Pass a positive `u32` threshold; the multi-sig cannot function with zero required weight. |
| 7 | `DuplicateSigner` | `initialize` was called with the same signer address appearing more than once in the `signers` list. | Ensure every `(address, weight)` pair in the `signers` vector is unique before calling `initialize`. |
| 8 | `InvalidWeightSum` | `initialize` was called with a `threshold` greater than the sum of all signer weights. | Lower the threshold or add signers with sufficient weight so that `sum(weights) ≥ threshold`. |
| 14 | `UpgradeInProgress` | `upgrade` was called while at least one settlement is in `PartiallyExecuted` status. | Allow the settlement to reach a safe terminal state before retrying the upgrade. |

---

Expand Down
Loading