Skip to content

Commit 352f93a

Browse files
authored
Merge branch 'main' into feat/governance-quorum-requirement
2 parents 7f929da + 104accc commit 352f93a

11 files changed

Lines changed: 2887 additions & 93 deletions

PR_DESCRIPTION.md

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,39 @@
1-
## Summary
1+
# Add unit tests for pause blocking sell transactions
2+
3+
Closes #727
24

3-
Fixes the TTL extension logic so that a `TTL_EXTENDED_EVENT_NAME` event is only emitted when the creator's storage TTL actually needs extension (remaining TTL drops below `TTL_EXTENSION_THRESHOLD`). Adds an integration test confirming no event is emitted when TTL is healthy.
5+
## Summary
46

5-
Closes #<!-- TODO: insert issue number -->
7+
Add dedicated integration tests confirming the emergency pause correctly blocks sell transactions and verifies that all contract state remains consistent after blocked sell attempts.
68

7-
## Problem
9+
## Motivation
810

9-
The `extend_creator_ttl` function unconditionally emitted a `ttl_ext` event on every successful buy or sell, even when the creator's storage TTL was already well above the minimum threshold. This polluted event logs with noisy, unnecessary events that indexers and off-chain consumers had to filter out.
11+
The emergency pause mechanism was originally tested primarily for buy transactions. While some existing tests touch sell under pause, there was no focused test suite that comprehensively validates the sell-pause lifecycle against all three acceptance criteria from the issue. This PR fills that gap with a dedicated test file.
1012

1113
## Changes
1214

13-
### `creator-keys/src/lib.rs`
15+
### New file: `creator-keys/tests/pause_blocks_sell_transactions.rs`
1416

15-
- **Added `TTL_EXTENSION_THRESHOLD` constant** (`100` ledgers) — the minimum remaining TTL below which a TTL extension event is emitted.
16-
- **Added `DataKey::CreatorTtlLiveUntil(creator)`** — a per-creator `u32` recording the absolute live-until ledger the contract last set for the creator profile key. The Soroban SDK does not expose TTL reads to contract code, so this tracked value is what `extend_creator_ttl` uses to decide whether to emit the event.
17-
- **Modified `extend_creator_ttl`** to:
18-
1. Derive the remaining TTL from `CreatorTtlLiveUntil` and evaluate `ttl::should_extend(remaining, TTL_EXTENSION_THRESHOLD)`.
19-
2. Always call `extend_ttl` on all creator-scoped storage keys — the Soroban SDK call is a no-op when TTL is already healthy, preserving the existing on-chain behavior.
20-
3. Only publish the `TTL_EXTENDED_EVENT_NAME` event when the check above returns `true`, then update the tracked live-until.
21-
- **Write-time TTL alignment**: new entries start with the network-default TTL, which can be much shorter than `CREATOR_TTL_LEDGERS` on fresh networks. `register_creator` now forces the full `CREATOR_TTL_LEDGERS` window on the creator profile, curve preset, and tracked live-until; `set_key_price`, the buy path, and dividend settlement grant the same full window to `KeyPrice`, `KeyBalance(creator, holder)`, and the dividend checkpoint/pending keys.
17+
Five new integration tests covering every acceptance criterion:
2218

23-
### `creator-keys/tests/ttl_extension_on_buy.rs`
19+
| Test | Acceptance Criterion |
20+
|---|---|
21+
| `test_sell_panics_with_protocol_paused_when_contract_is_paused` | Sell panics with `ProtocolPaused` when paused |
22+
| `test_sell_succeeds_after_resume` | Sell succeeds immediately after unpause |
23+
| `test_holder_count_unchanged_after_blocked_sell` | Holder count, supply, and key balance unchanged after blocked sell |
24+
| `test_supply_unchanged_after_blocked_sell_with_multiple_holders` | Multi-holder variant — both holders' state unchanged after concurrent blocked sells |
25+
| `test_full_pause_lifecycle` | End-to-end: buy → pause → blocked sell (state verified) → unpause → successful sell (state verified) |
2426

25-
- **Added `test_no_ttl_extension_event_when_ttl_healthy`** integration test that:
26-
- Registers a creator with TTL at max (~6.3M ledgers remaining).
27-
- Asserts TTL ≥ 2× `TTL_EXTENSION_THRESHOLD`.
28-
- Executes a buy without advancing the ledger.
29-
- Asserts **no** `ttl_ext` event is present among emitted events.
30-
- Asserts a `buy` event **is** present confirming the transaction succeeded.
31-
- Asserts creator storage TTL is unchanged after the buy.
27+
## Acceptance Criteria Verification
3228

33-
## Acceptance Criteria
29+
-**Sell panics with `contract_paused` when paused**`test_sell_panics_with_protocol_paused_when_contract_is_paused` asserts `Err(Ok(ContractError::ProtocolPaused))` on sell attempt while paused.
30+
-**Sell succeeds after resume**`test_sell_succeeds_after_resume` pauses, unpauses, then confirms sell returns the correct new supply.
31+
-**State unchanged after blocked sell**`test_holder_count_unchanged_after_blocked_sell` and `test_supply_unchanged_after_blocked_sell_with_multiple_holders` snapshot `supply`, `holder_count`, and `key_balance` before the blocked sell and assert all are identical afterward.
3432

35-
| Criteria | Status |
36-
|---|---|
37-
| No TTL extension event emitted when TTL is above threshold ||
38-
| Buy event present confirming transaction succeeded ||
39-
| Creator storage TTL unchanged after the buy ||
40-
| Test uses a TTL value at least 2× the extension threshold ||
41-
| Existing tests continue to pass ||
42-
43-
## Testing
44-
45-
- [x] `cargo fmt --all -- --check`
46-
- [x] `cargo clippy --workspace --all-targets -- -D warnings`
47-
- [x] `cargo test --workspace`
48-
49-
**Note:** All existing TTL tests (`test_buy_extends_creator_ttl`, `test_ttl_extension_event_topics_and_payload`, `test_ttl_not_extended_when_already_high`, `test_sell_extends_creator_ttl_after_successful_sell`, `test_failed_sell_does_not_extend_creator_ttl`) remain compatible because:
50-
- They advance the ledger to near expiry before the first buy, so `should_extend` returns `true` and the event is still emitted.
51-
- The second-buy-same-ledger scenario doesn't assert event presence/absence on the second buy.
52-
- `extend_ttl` SDK calls happen unconditionally — only event emission is gated.
53-
54-
## Checklist
55-
56-
- [x] Linked issue or backlog item
57-
- [x] Added or updated `creator-keys` unit/integration tests for every changed contract behavior, including failure paths for new or reachable `ContractError` variants
58-
- [ ] Ran `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace`, or explained exactly why a command was not run
59-
- [x] Reviewed persistent storage changes against `docs/storage-key-invariants.md`; any storage layout change includes a migration/backward-compatibility note
60-
- [x] Confirmed event names, topic order, payload field order, and field meanings remain compatible with `docs/contract-event-conventions.md`, or documented the breaking change and versioning plan
61-
- [x] Updated docs for any changed public contract interface, read-only method, event schema, storage behavior, fee logic, or deployment workflow
62-
- [x] Scope stays limited to one contract concern and does not include unrelated formatting, lockfile, generated artifact, or dependency changes
33+
## CI Pre-Checks
34+
35+
All local CI checks pass:
36+
-`cargo build` — compiles cleanly
37+
-`cargo test` — all tests pass (including the 5 new ones)
38+
-`cargo test --test pause_blocks_sell_transactions` — 5/5 passed
39+
- ✅ No new clippy warnings introduced by this change

creator-keys/src/events.rs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,16 @@ pub const WHITELIST_DISABLED_EVENT_NAME: Symbol = symbol_short!("wl_dis");
666666
pub const ADDRESS_WHITELISTED_EVENT_NAME: Symbol = symbol_short!("wl_add");
667667
pub const ADDRESS_REMOVED_EVENT_NAME: Symbol = symbol_short!("wl_rem");
668668
pub const KEYS_BURNED_EVENT_NAME: Symbol = symbol_short!("burned");
669+
pub const SELF_FREEZE_APPLIED_EVENT_NAME: Symbol = symbol_short!("sf_add");
670+
pub const SELF_FREEZE_LIFTED_EVENT_NAME: Symbol = symbol_short!("sf_del");
671+
672+
#[derive(Clone, Debug, Eq, PartialEq)]
673+
#[contracttype]
674+
pub struct SelfFreezeEvent {
675+
pub key_id: Address,
676+
pub wallet: Address,
677+
pub quantity: u32,
678+
}
669679

670680
#[derive(Clone, Debug, Eq, PartialEq)]
671681
#[contracttype]
@@ -1185,3 +1195,245 @@ pub struct RoyaltyUpdatedEvent {
11851195
pub fn royalty_updated_topics(creator: &Address) -> (Symbol, Address) {
11861196
(ROYALTY_UPDATED_EVENT_NAME, creator.clone())
11871197
}
1198+
1199+
/// Event name for the protocol trade fee collected on a buy or sell.
1200+
pub const FEE_COLLECTED_EVENT_NAME: Symbol = symbol_short!("fee_coll");
1201+
1202+
/// Event name for a sell rejected by the anti-flash-trade lockup window.
1203+
pub const LOCKUP_BLOCKED_EVENT_NAME: Symbol = symbol_short!("lck_blk");
1204+
1205+
/// Stable fee collection event payload for downstream indexers.
1206+
///
1207+
/// Event shape:
1208+
/// - topics: `(FEE_COLLECTED_EVENT_NAME, treasury)`
1209+
/// - data: `FeeCollectedEvent`
1210+
///
1211+
/// Emitted on every buy and sell once the protocol trade fee is configured,
1212+
/// carrying the deducted amount and the treasury address that received it.
1213+
#[derive(Clone, Debug, Eq, PartialEq)]
1214+
#[contracttype]
1215+
pub struct FeeCollectedEvent {
1216+
/// Treasury address that received the fee.
1217+
pub treasury: Address,
1218+
/// Fee amount deducted from the trade.
1219+
pub amount: i128,
1220+
/// Ledger sequence number at the time of the trade.
1221+
pub ledger: u32,
1222+
}
1223+
1224+
/// Shared fee collected event topics tuple.
1225+
pub fn fee_collected_topics(treasury: &Address) -> (Symbol, Address) {
1226+
(FEE_COLLECTED_EVENT_NAME, treasury.clone())
1227+
}
1228+
1229+
/// Stable lockup-blocked event payload for downstream indexers.
1230+
///
1231+
/// Event shape:
1232+
/// - topics: `(LOCKUP_BLOCKED_EVENT_NAME, creator_id, seller)`
1233+
/// - data: `LockupBlockedEvent`
1234+
///
1235+
/// Emitted when a sell is rejected because the seller's most recent buy for
1236+
/// this creator falls inside the configured lockup window.
1237+
#[derive(Clone, Debug, Eq, PartialEq)]
1238+
#[contracttype]
1239+
pub struct LockupBlockedEvent {
1240+
/// Creator whose keys the seller attempted to sell.
1241+
pub creator_id: Address,
1242+
/// Seller whose sale was rejected.
1243+
pub seller: Address,
1244+
/// Ledger timestamp of the seller's most recent buy.
1245+
pub last_buy_timestamp: u64,
1246+
/// Timestamp at which the lockup expires (exclusive).
1247+
pub unlock_at: u64,
1248+
/// Ledger timestamp at rejection.
1249+
pub current_timestamp: u64,
1250+
}
1251+
1252+
/// Shared lockup blocked event topics tuple.
1253+
pub fn lockup_blocked_topics(creator: &Address, seller: &Address) -> (Symbol, Address, Address) {
1254+
(LOCKUP_BLOCKED_EVENT_NAME, creator.clone(), seller.clone())
1255+
}
1256+
1257+
/// Event name for a new staking position created via `stake_keys_locked`.
1258+
pub const STAKE_EVENT_NAME: Symbol = symbol_short!("stake");
1259+
1260+
/// Event name for a lock period extension via `stake_extend`.
1261+
pub const STAKE_EXTENDED_EVENT_NAME: Symbol = symbol_short!("stk_ext");
1262+
1263+
/// Event name for an early (pre-maturity) unstake via `early_unstake`.
1264+
pub const EARLY_UNSTAKE_EVENT_NAME: Symbol = symbol_short!("stk_chl");
1265+
1266+
/// Event name for a reward claim at/after maturity via `claim_stake_reward`.
1267+
pub const STAKE_REWARD_CLAIMED_EVENT_NAME: Symbol = symbol_short!("stk_clm");
1268+
1269+
/// Stable stake event payload for downstream indexers.
1270+
///
1271+
/// Event shape:
1272+
/// - topics: `(STAKE_EVENT_NAME, creator_id, holder, stake_id)`
1273+
/// - data: `StakeEvent`
1274+
#[derive(Clone, Debug, Eq, PartialEq)]
1275+
#[contracttype]
1276+
pub struct StakeEvent {
1277+
/// Creator whose keys are staked.
1278+
pub creator_id: Address,
1279+
/// Staker that locked the keys.
1280+
pub holder: Address,
1281+
/// Sequential position id for the `(creator, holder)` pair.
1282+
pub stake_id: u32,
1283+
/// Number of keys locked.
1284+
pub amount: u32,
1285+
/// Ledger sequence at which the position matures.
1286+
pub unlock_ledger: u32,
1287+
}
1288+
1289+
/// Shared stake event topics tuple.
1290+
pub fn stake_topics(creator: &Address, holder: &Address, stake_id: u32) -> (Symbol, Address, Address, u32) {
1291+
(STAKE_EVENT_NAME, creator.clone(), holder.clone(), stake_id)
1292+
}
1293+
1294+
/// Stable stake-extend event payload for downstream indexers.
1295+
///
1296+
/// Event shape:
1297+
/// - topics: `(STAKE_EXTENDED_EVENT_NAME, creator_id, holder, stake_id)`
1298+
/// - data: `StakeExtendedEvent`
1299+
#[derive(Clone, Debug, Eq, PartialEq)]
1300+
#[contracttype]
1301+
pub struct StakeExtendedEvent {
1302+
/// Creator whose keys are staked.
1303+
pub creator_id: Address,
1304+
/// Staker that locked the keys.
1305+
pub holder: Address,
1306+
/// Extended position id.
1307+
pub stake_id: u32,
1308+
/// New maturity ledger sequence after the extension.
1309+
pub unlock_ledger: u32,
1310+
/// Additional ledgers appended to the lock period.
1311+
pub additional_ledgers: u32,
1312+
}
1313+
1314+
/// Shared stake-extend event topics tuple.
1315+
pub fn stake_extended_topics(
1316+
creator: &Address,
1317+
holder: &Address,
1318+
stake_id: u32,
1319+
) -> (Symbol, Address, Address, u32) {
1320+
(STAKE_EXTENDED_EVENT_NAME, creator.clone(), holder.clone(), stake_id)
1321+
}
1322+
1323+
/// Stable early-unstake event payload for downstream indexers.
1324+
///
1325+
/// Event shape:
1326+
/// - topics: `(EARLY_UNSTAKE_EVENT_NAME, creator_id, holder, stake_id)`
1327+
/// - data: `EarlyUnstakeEvent`
1328+
#[derive(Clone, Debug, Eq, PartialEq)]
1329+
#[contracttype]
1330+
pub struct EarlyUnstakeEvent {
1331+
/// Creator whose keys were staked.
1332+
pub creator_id: Address,
1333+
/// Staker that closed the position.
1334+
pub holder: Address,
1335+
/// Closed position id.
1336+
pub stake_id: u32,
1337+
/// Keys released back to the holder's liquid balance.
1338+
pub amount: u32,
1339+
/// Pro-rata reward entitlement removed from the pool.
1340+
pub forgone_reward: i128,
1341+
/// Penalty retained in the pool.
1342+
pub penalty: i128,
1343+
/// Ledger sequence at which the position was closed.
1344+
pub ledger: u32,
1345+
}
1346+
1347+
/// Shared early-unstake event topics tuple.
1348+
pub fn early_unstake_topics(
1349+
creator: &Address,
1350+
holder: &Address,
1351+
stake_id: u32,
1352+
) -> (Symbol, Address, Address, u32) {
1353+
(EARLY_UNSTAKE_EVENT_NAME, creator.clone(), holder.clone(), stake_id)
1354+
}
1355+
1356+
/// Stable stake-reward-claim event payload for downstream indexers.
1357+
///
1358+
/// Event shape:
1359+
/// - topics: `(STAKE_REWARD_CLAIMED_EVENT_NAME, creator_id, holder, stake_id)`
1360+
/// - data: `StakeRewardClaimedEvent`
1361+
#[derive(Clone, Debug, Eq, PartialEq)]
1362+
#[contracttype]
1363+
pub struct StakeRewardClaimedEvent {
1364+
/// Creator whose keys were staked.
1365+
pub creator_id: Address,
1366+
/// Staker that closed the position.
1367+
pub holder: Address,
1368+
/// Closed position id.
1369+
pub stake_id: u32,
1370+
/// Keys released back to the holder's liquid balance.
1371+
pub amount: u32,
1372+
/// Reward paid out from the pool.
1373+
pub reward: i128,
1374+
/// Ledger sequence at which the position matured.
1375+
pub unlock_ledger: u32,
1376+
/// Ledger sequence at which the reward was claimed.
1377+
pub ledger: u32,
1378+
}
1379+
1380+
/// Shared stake-reward-claim event topics tuple.
1381+
pub fn stake_reward_claimed_topics(
1382+
creator: &Address,
1383+
holder: &Address,
1384+
stake_id: u32,
1385+
) -> (Symbol, Address, Address, u32) {
1386+
(STAKE_REWARD_CLAIMED_EVENT_NAME, creator.clone(), holder.clone(), stake_id)
1387+
}
1388+
1389+
1390+
// ============================================================================
1391+
// Launch Penalty (#798)
1392+
// ============================================================================
1393+
1394+
/// Event name for launch penalty applied on sell.
1395+
pub const LAUNCH_PENALTY_APPLIED_EVENT_NAME: Symbol = symbol_short!("lnch_pnl");
1396+
1397+
/// Stable launch penalty applied event payload for downstream indexers.
1398+
#[derive(Clone, Debug, Eq, PartialEq)]
1399+
#[contracttype]
1400+
pub struct LaunchPenaltyAppliedEvent {
1401+
/// Address of the creator whose key was sold.
1402+
pub creator_id: Address,
1403+
/// Address of the seller.
1404+
pub seller: Address,
1405+
/// Launch penalty basis points applied.
1406+
pub penalty_bps: u32,
1407+
/// Penalty amount deducted from proceeds.
1408+
pub penalty_amount: i128,
1409+
/// Ledger sequence at the time of the sale.
1410+
pub ledger: u32,
1411+
}
1412+
1413+
/// Shared launch penalty applied event topics tuple.
1414+
pub fn launch_penalty_applied_topics(
1415+
creator: &Address,
1416+
seller: &Address,
1417+
) -> (Symbol, Address, Address) {
1418+
(LAUNCH_PENALTY_APPLIED_EVENT_NAME, creator.clone(), seller.clone())
1419+
}
1420+
1421+
/// Event name for set_launch_penalty.
1422+
pub const LAUNCH_PENALTY_SET_EVENT_NAME: Symbol = symbol_short!("lnch_set");
1423+
1424+
/// Stable set launch penalty event payload for downstream indexers.
1425+
#[derive(Clone, Debug, Eq, PartialEq)]
1426+
#[contracttype]
1427+
pub struct LaunchPenaltySetEvent {
1428+
/// Address of the creator.
1429+
pub creator_id: Address,
1430+
/// New penalty basis points.
1431+
pub penalty_bps: u32,
1432+
/// Ledger sequence at the time of the update.
1433+
pub ledger: u32,
1434+
}
1435+
1436+
/// Shared set launch penalty event topics tuple.
1437+
pub fn launch_penalty_set_topics(creator: &Address) -> (Symbol, Address) {
1438+
(LAUNCH_PENALTY_SET_EVENT_NAME, creator.clone())
1439+
}

0 commit comments

Comments
 (0)