@@ -1479,6 +1479,93 @@ fn asset_hold_prevents_spend_over_free() {
14791479 } ) ;
14801480}
14811481
1482+ #[ test]
1483+ fn recover_funds_is_atomic_when_release_fails ( ) {
1484+ new_test_ext ( ) . execute_with ( || {
1485+ System :: set_block_number ( 1 ) ;
1486+
1487+ let protected = alice ( ) ; // high-security from genesis, guardian = bob
1488+ let guardian = bob ( ) ;
1489+ let asset_operator = charlie ( ) ;
1490+ let recipient = eve ( ) ;
1491+ let asset_id: u32 = 4_343 ;
1492+ let amount: Balance = 1_000 ;
1493+
1494+ // Create an asset and block the guardian's asset account so it cannot receive funds.
1495+ create_asset ( asset_id, asset_operator. clone ( ) , Some ( 10_000 ) ) ;
1496+ assert_ok ! ( pallet_assets:: Pallet :: <Test >:: mint(
1497+ RuntimeOrigin :: signed( asset_operator. clone( ) ) ,
1498+ codec:: Compact ( asset_id) ,
1499+ protected. clone( ) ,
1500+ 10_000 ,
1501+ ) ) ;
1502+ assert_ok ! ( pallet_assets:: Pallet :: <Test >:: mint(
1503+ RuntimeOrigin :: signed( asset_operator. clone( ) ) ,
1504+ codec:: Compact ( asset_id) ,
1505+ guardian. clone( ) ,
1506+ 1 ,
1507+ ) ) ;
1508+ assert_ok ! ( pallet_assets:: Pallet :: <Test >:: block(
1509+ RuntimeOrigin :: signed( asset_operator. clone( ) ) ,
1510+ codec:: Compact ( asset_id) ,
1511+ guardian. clone( ) ,
1512+ ) ) ;
1513+
1514+ let asset_call: RuntimeCall = pallet_assets:: Call :: < Test > :: transfer_keep_alive {
1515+ id : codec:: Compact ( asset_id) ,
1516+ target : recipient. clone ( ) ,
1517+ amount,
1518+ }
1519+ . into ( ) ;
1520+ let tx_id = calculate_tx_id :: < Test > ( protected. clone ( ) , & asset_call) ;
1521+
1522+ assert_ok ! ( ReversibleTransfers :: schedule_asset_transfer(
1523+ RuntimeOrigin :: signed( protected. clone( ) ) ,
1524+ asset_id,
1525+ recipient,
1526+ amount,
1527+ ) ) ;
1528+
1529+ let hold_before = asset_holds ( asset_id, & protected) ;
1530+ let issuance_before =
1531+ <pallet_assets:: Pallet < Test > as AssetsInspect < _ > >:: total_issuance ( asset_id) ;
1532+ assert_eq ! ( hold_before, amount) ;
1533+ assert_eq ! ( ReversibleTransfers :: pending_dispatches( tx_id) . unwrap( ) . amount, amount) ;
1534+
1535+ // Recovery cannot deliver to the blocked guardian, so the release must roll back fully:
1536+ // no fee is burned and the pending transfer is preserved for retry.
1537+ assert_ok ! ( ReversibleTransfers :: recover_funds(
1538+ RuntimeOrigin :: signed( guardian. clone( ) ) ,
1539+ protected. clone( ) ,
1540+ ) ) ;
1541+ System :: assert_has_event ( Event :: TransferRecoveryFailed { tx_id } . into ( ) ) ;
1542+ assert_eq ! ( asset_holds( asset_id, & protected) , hold_before) ;
1543+ assert_eq ! (
1544+ <pallet_assets:: Pallet <Test > as AssetsInspect <_>>:: total_issuance( asset_id) ,
1545+ issuance_before
1546+ ) ;
1547+ let pending_after = ReversibleTransfers :: pending_dispatches ( tx_id) . unwrap ( ) ;
1548+ assert_eq ! ( pending_after. amount, asset_holds( asset_id, & protected) ) ;
1549+ assert ! ( ReversibleTransfers :: pending_transfers_by_sender( & protected) . contains( & tx_id) ) ;
1550+
1551+ // Repeated recovery stays idempotent: the hold is never partially burned.
1552+ assert_ok ! ( Balances :: transfer_keep_alive(
1553+ RuntimeOrigin :: signed( asset_operator) ,
1554+ protected. clone( ) ,
1555+ 100 ,
1556+ ) ) ;
1557+ assert_ok ! ( ReversibleTransfers :: recover_funds(
1558+ RuntimeOrigin :: signed( guardian) ,
1559+ protected. clone( ) ,
1560+ ) ) ;
1561+ assert_eq ! ( asset_holds( asset_id, & protected) , hold_before) ;
1562+ assert_eq ! (
1563+ <pallet_assets:: Pallet <Test > as AssetsInspect <_>>:: total_issuance( asset_id) ,
1564+ issuance_before
1565+ ) ;
1566+ } ) ;
1567+ }
1568+
14821569#[ test]
14831570fn schedule_transfer_with_error_short_delay ( ) {
14841571 new_test_ext ( ) . execute_with ( || {
0 commit comments