@@ -858,3 +858,82 @@ fn test_upgrade_contract() {
858858 // Version should increment
859859 assert_eq ! ( vault. version( ) , 2 ) ;
860860}
861+
862+ // ─── Invariant Tests (Issue #258) ───────────────────────────────────────────────
863+
864+ #[ test]
865+ fn test_invariant_pool_conservation_and_monotonic_price ( ) {
866+ let env = Env :: default ( ) ;
867+ env. mock_all_auths ( ) ;
868+
869+ let ( vault, _, usdc_sa, admin) = setup_vault ( & env) ;
870+
871+ let user1 = Address :: generate ( & env) ;
872+ let user2 = Address :: generate ( & env) ;
873+ let user3 = Address :: generate ( & env) ;
874+
875+ usdc_sa. mint ( & user1, & 1_000_000_000 ) ;
876+ usdc_sa. mint ( & user2, & 1_000_000_000 ) ;
877+ usdc_sa. mint ( & user3, & 1_000_000_000 ) ;
878+ usdc_sa. mint ( & admin, & 500_000_000 ) ;
879+
880+ let assert_pool_conservation = |v : & YieldVaultClient | {
881+ let shares = v. total_shares ( ) ;
882+ let assets = v. total_assets ( ) ;
883+ let price = v. get_share_price ( ) ;
884+
885+ if shares == 0 {
886+ assert_eq ! ( assets, 0 ) ;
887+ } else {
888+ let reconstructed_assets = ( shares. checked_mul ( price) . unwrap ( ) ) / SCALE ;
889+ let diff = ( assets - reconstructed_assets) . abs ( ) ;
890+ assert ! ( diff <= 1 , "Pool conservation invariant failed. Expected: {}, Got: {}" , assets, reconstructed_assets) ;
891+ }
892+ } ;
893+
894+ // 1. Zero deposits edge case
895+ assert_pool_conservation ( & vault) ;
896+ let mut last_price = vault. get_share_price ( ) ;
897+ assert_eq ! ( last_price, SCALE ) ;
898+
899+ // 2. Initial deposits
900+ vault. deposit ( & user1, & 100_000 ) ;
901+ assert_pool_conservation ( & vault) ;
902+ assert ! ( vault. get_share_price( ) >= last_price, "Price decreased" ) ;
903+ last_price = vault. get_share_price ( ) ;
904+
905+ vault. deposit ( & user2, & 200_000 ) ;
906+ assert_pool_conservation ( & vault) ;
907+ assert ! ( vault. get_share_price( ) >= last_price, "Price decreased" ) ;
908+ last_price = vault. get_share_price ( ) ;
909+
910+ // 3. Yield Accrual (Monotonically non-decreasing price)
911+ vault. accrue_yield ( & 50_000 ) ;
912+ assert_pool_conservation ( & vault) ;
913+ assert ! ( vault. get_share_price( ) >= last_price, "Price decreased" ) ;
914+ last_price = vault. get_share_price ( ) ;
915+
916+ // 4. Partial withdrawal
917+ vault. withdraw ( & user1, & 50_000 ) ;
918+ assert_pool_conservation ( & vault) ;
919+ assert ! ( vault. get_share_price( ) >= last_price, "Price decreased" ) ;
920+ last_price = vault. get_share_price ( ) ;
921+
922+ // 5. Another yield accrual
923+ vault. accrue_yield ( & 20_000 ) ;
924+ assert_pool_conservation ( & vault) ;
925+ assert ! ( vault. get_share_price( ) >= last_price, "Price decreased" ) ;
926+ last_price = vault. get_share_price ( ) ;
927+
928+ // 6. Max deposit cap / large deposit
929+ vault. deposit ( & user3, & 500_000_000 ) ;
930+ assert_pool_conservation ( & vault) ;
931+ assert ! ( vault. get_share_price( ) >= last_price, "Price decreased" ) ;
932+ last_price = vault. get_share_price ( ) ;
933+
934+ // 7. Full withdrawals
935+ let user2_balance = vault. balance ( & user2) ;
936+ vault. withdraw ( & user2, & user2_balance) ;
937+ assert_pool_conservation ( & vault) ;
938+ assert ! ( vault. get_share_price( ) >= last_price, "Price decreased" ) ;
939+ }
0 commit comments