@@ -259,10 +259,11 @@ impl<
259259 if outstanding_credit. peek ( ) . saturating_sub ( refund_balance) >= minimum_balance {
260260 outstanding_credit. extract ( refund_balance)
261261 } else {
262- // If refunding would leave less than ED, we refund ED to ensure the
263- // OnUnbalanced handler receives at least ED when this trader is dropped.
264- // This prevents dust amounts that can't be properly handled.
265- outstanding_credit. extract ( minimum_balance)
262+ // Keep at least ED in outstanding credit for the OnUnbalanced drop
263+ // handler. Refund only the surplus above ED (zero if outstanding < ED).
264+ let keep = minimum_balance. min ( outstanding_credit. peek ( ) ) ;
265+ let refund_amount = outstanding_credit. peek ( ) . saturating_sub ( keep) ;
266+ outstanding_credit. extract ( refund_amount)
266267 }
267268 } ) ?;
268269 // Subtract the refunded weight from existing weight.
@@ -1060,6 +1061,149 @@ mod test_trader {
10601061 _ => panic ! ( "Expected fungible asset" ) ,
10611062 }
10621063 }
1064+
1065+ #[ test]
1066+ fn take_first_asset_trader_refund_keeps_ed_for_drop_handler ( ) {
1067+ // Regression test: when refunding would leave less than ED in outstanding
1068+ // credit, `refund_weight` must refund `outstanding - ED` and keep ED for the
1069+ // `OnUnbalanced` drop handler.
1070+ use core:: cell:: Cell ;
1071+
1072+ const ED : u128 = 10 ;
1073+ const BUY_FEE : u128 = 15 ;
1074+ const REFUND_FEE : u128 = 10 ;
1075+
1076+ type TestAccountId = u32 ;
1077+ type TestAssetId = Location ;
1078+ type TestBalance = u128 ;
1079+
1080+ struct TestAssets ;
1081+ impl MatchesFungibles < TestAssetId , TestBalance > for TestAssets {
1082+ fn matches_fungibles ( a : & Asset ) -> Result < ( TestAssetId , TestBalance ) , Error > {
1083+ match a {
1084+ Asset { fun : Fungible ( amount) , id : AssetId ( _) } => {
1085+ Ok ( ( Location :: new ( 0 , [ GeneralIndex ( 1 ) ] ) , * amount) )
1086+ } ,
1087+ _ => Err ( Error :: AssetNotHandled ) ,
1088+ }
1089+ }
1090+ }
1091+ impl fungibles:: Inspect < TestAccountId > for TestAssets {
1092+ type AssetId = TestAssetId ;
1093+ type Balance = TestBalance ;
1094+ fn total_issuance ( _: Self :: AssetId ) -> Self :: Balance {
1095+ 0
1096+ }
1097+ fn minimum_balance ( _: Self :: AssetId ) -> Self :: Balance {
1098+ ED
1099+ }
1100+ fn balance ( _: Self :: AssetId , _: & TestAccountId ) -> Self :: Balance {
1101+ 0
1102+ }
1103+ fn total_balance ( _: Self :: AssetId , _: & TestAccountId ) -> Self :: Balance {
1104+ 0
1105+ }
1106+ fn reducible_balance (
1107+ _: Self :: AssetId ,
1108+ _: & TestAccountId ,
1109+ _: Preservation ,
1110+ _: Fortitude ,
1111+ ) -> Self :: Balance {
1112+ 0
1113+ }
1114+ fn can_deposit (
1115+ _: Self :: AssetId ,
1116+ _: & TestAccountId ,
1117+ _: Self :: Balance ,
1118+ _: Provenance ,
1119+ ) -> DepositConsequence {
1120+ DepositConsequence :: Success
1121+ }
1122+ fn can_withdraw (
1123+ _: Self :: AssetId ,
1124+ _: & TestAccountId ,
1125+ _: Self :: Balance ,
1126+ ) -> WithdrawConsequence < Self :: Balance > {
1127+ WithdrawConsequence :: Success
1128+ }
1129+ fn asset_exists ( _: Self :: AssetId ) -> bool {
1130+ true
1131+ }
1132+ }
1133+ impl fungibles:: Mutate < TestAccountId > for TestAssets { }
1134+ impl fungibles:: Balanced < TestAccountId > for TestAssets {
1135+ type OnDropCredit = fungibles:: DecreaseIssuance < TestAccountId , Self > ;
1136+ type OnDropDebt = fungibles:: IncreaseIssuance < TestAccountId , Self > ;
1137+ }
1138+ impl fungibles:: Unbalanced < TestAccountId > for TestAssets {
1139+ fn handle_dust ( _: fungibles:: Dust < TestAccountId , Self > ) { }
1140+ fn write_balance (
1141+ _: Self :: AssetId ,
1142+ _: & TestAccountId ,
1143+ _: Self :: Balance ,
1144+ ) -> Result < Option < Self :: Balance > , DispatchError > {
1145+ Ok ( None )
1146+ }
1147+ fn set_total_issuance ( _: Self :: AssetId , _: Self :: Balance ) { }
1148+ }
1149+
1150+ // Charge BUY_FEE on the first call (during `buy_weight`), REFUND_FEE on the
1151+ // next (during `refund_weight`).
1152+ thread_local ! {
1153+ static CALLS : Cell <u32 > = const { Cell :: new( 0 ) } ;
1154+ static HANDLER_RECEIVED : Cell <u128 > = const { Cell :: new( 0 ) } ;
1155+ }
1156+ struct FeeCharger ;
1157+ impl ChargeWeightInFungibles < TestAccountId , TestAssets > for FeeCharger {
1158+ fn charge_weight_in_fungibles (
1159+ _: <TestAssets as fungibles:: Inspect < TestAccountId > >:: AssetId ,
1160+ _: Weight ,
1161+ ) -> Result < <TestAssets as fungibles:: Inspect < TestAccountId > >:: Balance , XcmError > {
1162+ let n = CALLS . with ( |c| {
1163+ let v = c. get ( ) ;
1164+ c. set ( v + 1 ) ;
1165+ v
1166+ } ) ;
1167+ Ok ( if n == 0 { BUY_FEE } else { REFUND_FEE } )
1168+ }
1169+ }
1170+
1171+ struct HandleFees ;
1172+ impl OnUnbalancedT < fungibles:: Credit < TestAccountId , TestAssets > > for HandleFees {
1173+ fn on_unbalanced ( credit : fungibles:: Credit < TestAccountId , TestAssets > ) {
1174+ HANDLER_RECEIVED . with ( |h| h. set ( credit. peek ( ) ) ) ;
1175+ }
1176+ }
1177+
1178+ type Trader =
1179+ TakeFirstAssetTrader < TestAccountId , FeeCharger , TestAssets , TestAssets , HandleFees > ;
1180+ let mut trader = <Trader as WeightTrader >:: new ( ) ;
1181+ let ctx = XcmContext { origin : None , message_id : XcmHash :: default ( ) , topic : None } ;
1182+
1183+ // `buy_weight` populates outstanding_credit with BUY_FEE = 15.
1184+ let payment = asset_to_holding ( ( Here , BUY_FEE ) . into ( ) ) ;
1185+ assert_ok ! ( trader. buy_weight( Weight :: from_parts( 1_000 , 1_000 ) , payment, & ctx) ) ;
1186+
1187+ // `refund_weight` would compute refund=10, but outstanding(15) - 10 = 5 < ED(10),
1188+ // so it must refund only `outstanding - ED` = 5 and keep ED for the handler.
1189+ let refund = trader
1190+ . refund_weight ( Weight :: from_parts ( 500 , 500 ) , & ctx)
1191+ . expect ( "non-zero refund" ) ;
1192+ let refund_assets: Vec < Asset > = refund. fungible_assets_iter ( ) . collect ( ) ;
1193+ assert_eq ! ( refund_assets. len( ) , 1 ) ;
1194+ match & refund_assets[ 0 ] {
1195+ Asset { fun : Fungible ( amount) , .. } => {
1196+ assert_eq ! ( * amount, BUY_FEE - ED , "refund must be `outstanding - ED`, not ED" )
1197+ } ,
1198+ _ => panic ! ( "expected fungible refund" ) ,
1199+ }
1200+
1201+ // Drop the trader; the handler must receive at least ED.
1202+ drop ( trader) ;
1203+ HANDLER_RECEIVED . with ( |h| {
1204+ assert_eq ! ( h. get( ) , ED , "OnUnbalanced drop handler must receive at least ED" )
1205+ } ) ;
1206+ }
10631207}
10641208
10651209/// Implementation of `xcm_builder::EnsureDelivery` which helps to ensure delivery to the
0 commit comments