Skip to content

Commit a6bb60b

Browse files
authored
Merge branch 'main' into issue-891-subscription-unauthorized-revert-tests
2 parents 2eab7a4 + 804aa17 commit a6bb60b

3 files changed

Lines changed: 291 additions & 93 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (2026-06-01)
1+
# (2026-06-02)
22

33

44
### Bug Fixes
@@ -304,6 +304,7 @@
304304
* metrics and alerting ([1f51207](https://github.com/MyFanss/MyFans/commit/1f5120755388c253d3861617a743a931dafa222d))
305305
* **moderation:** add content moderation flags model and admin endpoints ([#353](https://github.com/MyFanss/MyFans/issues/353)) ([227a0ec](https://github.com/MyFanss/MyFans/commit/227a0ec3686f6e3c9a6c747323b79acc6bda0275))
306306
* **myfans-token:** add property tests for allowance, approve, clear_allowance, set_admin, total_supply invariants ([#889](https://github.com/MyFanss/MyFans/issues/889)) ([1316e4b](https://github.com/MyFanss/MyFans/commit/1316e4b48d61fea106b75b16f48d5a3670ffeae1))
307+
* **myfans-token:** add property tests for approve/allowance and sequential transfer invariants ([#889](https://github.com/MyFanss/MyFans/issues/889)) ([e7082b6](https://github.com/MyFanss/MyFans/commit/e7082b66fc6669f5d824c2333b4669c663098671))
307308
* normalize subscription expiry ledger calculations ([#288](https://github.com/MyFanss/MyFans/issues/288)) ([31ccf63](https://github.com/MyFanss/MyFans/commit/31ccf63483e9d83269a1d4baa998a54704210375))
308309
* **observability:** add health check aggregation endpoint ([5003be2](https://github.com/MyFanss/MyFans/commit/5003be2087b17700b053f987a5bf5618bb1cfe14))
309310
* **observability:** define and enforce structured log fields standard ([739961b](https://github.com/MyFanss/MyFans/commit/739961b2575fce917a2ea64ae4630fd4d0f2fc7b))
@@ -338,6 +339,7 @@
338339
* standardize deployed contract env vars with backward-compatible aliases ([e976fe2](https://github.com/MyFanss/MyFans/commit/e976fe2ecad5435e46e6e0ae1b9a39e6ae2ae7b8))
339340
* Subscription List Filter by Status & Sorting ([cc37385](https://github.com/MyFanss/MyFans/commit/cc37385e6ff45abe89b46f4fea01fe1086ba085b))
340341
* **subscription:** add unauthorized caller revert tests ([#891](https://github.com/MyFanss/MyFans/issues/891)) ([7bef01e](https://github.com/MyFanss/MyFans/commit/7bef01ef3602014f2266963125dde0653bc27ad1))
342+
* **subscription:** add unit tests for initialize and admin paths ([#890](https://github.com/MyFanss/MyFans/issues/890)) ([3aad4b5](https://github.com/MyFanss/MyFans/commit/3aad4b549aad2b2161ee03a8b3aa68ee49d9249c))
341343
* **subscription:** add unit tests for initialize and admin paths ([#890](https://github.com/MyFanss/MyFans/issues/890)) ([9b3d0b9](https://github.com/MyFanss/MyFans/commit/9b3d0b944192676a42bca6b4ccbfccba8e7e08a9))
342344
* **subscription:** admin set_fee_bps with bounds and fee_updated event ([5a7c7e2](https://github.com/MyFanss/MyFans/commit/5a7c7e26f5546b4b14ea65e71644765be2346475))
343345
* **subscription:** admin set_fee_recipient with validation and event ([c372cab](https://github.com/MyFanss/MyFans/commit/c372caba4c9716bcdc3fa1e4825f70af8158cd20))

contract/contracts/myfans-token/src/property_tests.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,147 @@ mod props {
529529
}
530530
}
531531
}
532+
533+
// ── approve / allowance invariants ───────────────────────────────────────
534+
535+
proptest! {
536+
/// approve followed by allowance query must return exactly the approved amount
537+
/// (while the ledger is still before expiration_ledger).
538+
#[test]
539+
fn prop_approve_sets_exact_allowance(
540+
amount in 0i128..=1_000_000i128,
541+
expiry_offset in 1u32..=10_000u32,
542+
) {
543+
let env = Env::default();
544+
env.mock_all_auths();
545+
let (client, _) = setup(&env);
546+
547+
let from = Address::generate(&env);
548+
let spender = Address::generate(&env);
549+
let expiry = env.ledger().sequence() + expiry_offset;
550+
551+
client.approve(&from, &spender, &amount, &expiry);
552+
prop_assert_eq!(client.allowance(&from, &spender), amount);
553+
}
554+
555+
/// After transfer_from consumes part of an allowance, the remaining
556+
/// allowance equals (original - spent) and balances are consistent.
557+
#[test]
558+
fn prop_allowance_decreases_by_spent_amount(
559+
mint_amount in 1i128..=1_000_000i128,
560+
allowance_amt in 1i128..=1_000_000i128,
561+
spend_amount in 1i128..=1_000_000i128,
562+
) {
563+
let env = Env::default();
564+
env.mock_all_auths();
565+
let (client, _) = setup(&env);
566+
567+
let owner = Address::generate(&env);
568+
let spender = Address::generate(&env);
569+
let receiver = Address::generate(&env);
570+
571+
let mint = mint_amount.max(allowance_amt).max(spend_amount);
572+
client.mint(&owner, &mint);
573+
client.approve(&owner, &spender, &allowance_amt, &10_000);
574+
575+
if spend_amount <= allowance_amt && spend_amount <= mint {
576+
client.transfer_from(&spender, &owner, &receiver, &spend_amount);
577+
prop_assert_eq!(
578+
client.allowance(&owner, &spender),
579+
allowance_amt - spend_amount,
580+
"remaining allowance must equal original minus spent"
581+
);
582+
}
583+
}
584+
585+
/// clear_allowance must zero the allowance regardless of the prior value.
586+
#[test]
587+
fn prop_clear_allowance_zeroes_it(
588+
amount in 1i128..=1_000_000i128,
589+
expiry_offset in 1u32..=10_000u32,
590+
) {
591+
let env = Env::default();
592+
env.mock_all_auths();
593+
let (client, _) = setup(&env);
594+
595+
let from = Address::generate(&env);
596+
let spender = Address::generate(&env);
597+
let expiry = env.ledger().sequence() + expiry_offset;
598+
599+
client.approve(&from, &spender, &amount, &expiry);
600+
prop_assert_eq!(client.allowance(&from, &spender), amount);
601+
602+
client.clear_allowance(&from, &spender);
603+
prop_assert_eq!(client.allowance(&from, &spender), 0i128);
604+
}
605+
606+
/// Approving with amount=0 is equivalent to clearing the allowance.
607+
#[test]
608+
fn prop_approve_zero_is_same_as_clear(
609+
expiry_offset in 1u32..=10_000u32,
610+
) {
611+
let env = Env::default();
612+
env.mock_all_auths();
613+
let (client, _) = setup(&env);
614+
615+
let from = Address::generate(&env);
616+
let spender = Address::generate(&env);
617+
let expiry = env.ledger().sequence() + expiry_offset;
618+
619+
client.approve(&from, &spender, &0i128, &expiry);
620+
prop_assert_eq!(client.allowance(&from, &spender), 0i128);
621+
}
622+
}
623+
624+
// ── sequential transfer invariants ──────────────────────────────────────
625+
626+
proptest! {
627+
/// Two sequential transfers A→B and B→C conserve total supply and
628+
/// leave each balance consistent with the amounts moved.
629+
#[test]
630+
fn prop_sequential_transfers_conserve_supply(
631+
mint_amount in 2i128..=1_000_000i128,
632+
first_amount in 1i128..=500_000i128,
633+
second_amount in 1i128..=500_000i128,
634+
) {
635+
let env = Env::default();
636+
env.mock_all_auths();
637+
let (client, _) = setup(&env);
638+
639+
let a = Address::generate(&env);
640+
let b = Address::generate(&env);
641+
let c = Address::generate(&env);
642+
643+
client.mint(&a, &mint_amount);
644+
let supply = client.total_supply();
645+
646+
if first_amount <= mint_amount && second_amount <= first_amount {
647+
client.transfer(&a, &b, &first_amount);
648+
client.transfer(&b, &c, &second_amount);
649+
650+
prop_assert_eq!(client.balance(&a), mint_amount - first_amount);
651+
prop_assert_eq!(client.balance(&b), first_amount - second_amount);
652+
prop_assert_eq!(client.balance(&c), second_amount);
653+
prop_assert_eq!(client.total_supply(), supply, "supply unchanged after transfers");
654+
}
655+
}
656+
657+
/// Mint then burn the same amount must leave total supply unchanged.
658+
#[test]
659+
fn prop_mint_then_burn_same_amount_is_noop_on_supply(
660+
amount in 1i128..=1_000_000_000i128,
661+
) {
662+
let env = Env::default();
663+
env.mock_all_auths();
664+
let (client, _) = setup(&env);
665+
666+
let holder = Address::generate(&env);
667+
let supply_before = client.total_supply();
668+
669+
client.mint(&holder, &amount);
670+
client.burn(&holder, &amount);
671+
672+
prop_assert_eq!(client.total_supply(), supply_before);
673+
prop_assert_eq!(client.balance(&holder), 0i128);
674+
}
675+
}

0 commit comments

Comments
 (0)