@@ -4860,7 +4860,16 @@ mod aave_atoken {
48604860 use super :: * ;
48614861 use hydradx_runtime:: DCA ;
48624862
4863- const PATH_TO_SNAPSHOT : & str = "dca-snapshot/SNAPSHOT" ;
4863+ // Snapshot at block 12309734, produced with:
4864+ //
4865+ // ./target/release/scraper save-storage --slim \
4866+ // --uri wss://hydration.dotters.network \
4867+ // --at 0x5bfd245dd5612800f2291cef550a5e3f76b569e6de4d52d1c6cb122e674d838e \
4868+ // --pallet Omnipool Stableswap AssetRegistry EVM DynamicFees EmaOracle \
4869+ // MultiTransactionPayment Tokens Balances EVMAccounts Ethereum \
4870+ // EVMChainId HSM Router System Aura Timestamp DCA \
4871+ // --path integration-tests/dca-snapshot
4872+ const PATH_TO_SNAPSHOT : & str = "dca-snapshot/SNAPSHOT_12309734" ;
48644873
48654874 //Ignored as snapshot too big
48664875 //To verify locally, download snapshot with command `./target/release/scraper save-storage --uri wss://paseo-rpc.play.hydration.cloud --at 0x3db005212a4ae320a2808c6813880b583dacbf7df60b0314420e88f4f2dfe989`
@@ -4885,6 +4894,62 @@ mod aave_atoken {
48854894 assert ! ( schedule. is_some( ) ) ;
48864895 } ) ;
48874896 }
4897+
4898+ use frame_support:: traits:: OnInitialize ;
4899+
4900+ #[ test]
4901+ fn dca_should_succeed_after_retry_when_insufficient_balance_error_due_to_off_by_one_error ( ) {
4902+ TestNet :: reset ( ) ;
4903+
4904+ hydra_live_ext ( PATH_TO_SNAPSHOT ) . execute_with ( || {
4905+ //Arrange
4906+ assert_eq ! ( hydradx_runtime:: System :: block_number( ) , 12309734 ) ;
4907+ let schedule_id = 30972 ;
4908+ assert ! ( DCA :: schedules( schedule_id) . is_some( ) ) ;
4909+
4910+ //Act 1: first attempt fails with InsufficientBalance and is retried
4911+ DCA :: on_initialize ( 12309735 ) ;
4912+ assert_trade_failed_with_omnipool_insufficient_balance ( schedule_id) ;
4913+ assert_eq ! (
4914+ DCA :: retries_on_error( schedule_id) ,
4915+ 1 ,
4916+ "first attempt should have been retried"
4917+ ) ;
4918+ let retry_block =
4919+ DCA :: schedule_execution_block ( schedule_id) . expect ( "schedule should be replanned to a retry block" ) ;
4920+
4921+ //Act 2: simulate elapsed time so aave liquidity index drifts (rounding boundary moves)
4922+ let now = hydradx_runtime:: Timestamp :: get ( ) ;
4923+ hydradx_runtime:: Timestamp :: set_timestamp ( now + 240_000 ) ;
4924+
4925+ //Act 3: run DCA at the retry block
4926+ DCA :: on_initialize ( retry_block) ;
4927+
4928+ //Assert: schedule still alive and retry counter reset (= the trade succeeded)
4929+ assert ! ( DCA :: schedules( schedule_id) . is_some( ) , "schedule must survive retry" ) ;
4930+ assert_eq ! (
4931+ DCA :: retries_on_error( schedule_id) ,
4932+ 0 ,
4933+ "retry should have succeeded and reset the counter"
4934+ ) ;
4935+ } ) ;
4936+ }
4937+
4938+ fn assert_trade_failed_with_omnipool_insufficient_balance ( schedule_id : u32 ) {
4939+ let expected: sp_runtime:: DispatchError = pallet_omnipool:: Error :: < Runtime > :: InsufficientBalance . into ( ) ;
4940+ let events = last_hydra_events ( 20 ) ;
4941+ let found = events. iter ( ) . any ( |e| {
4942+ matches ! (
4943+ e,
4944+ RuntimeEvent :: DCA ( pallet_dca:: Event :: TradeFailed { id, error, .. } )
4945+ if * id == schedule_id && * error == expected
4946+ )
4947+ } ) ;
4948+ assert ! (
4949+ found,
4950+ "expected TradeFailed event with omnipool::InsufficientBalance for schedule {schedule_id}"
4951+ ) ;
4952+ }
48884953}
48894954
48904955fn create_xyk_pool_with_amounts ( asset_a : u32 , amount_a : u128 , asset_b : u32 , amount_b : u128 ) {
0 commit comments