@@ -1907,6 +1907,28 @@ fn init_legacy_staking() {
19071907 assert_ok ! ( Staking :: initialize_staking( RawOrigin :: Root . into( ) ) ) ;
19081908}
19091909
1910+ /// Submit a root-track referendum (trivial remark proposal) and place its
1911+ /// decision deposit. Returns the referendum index.
1912+ fn submit_remark_referendum ( submitter : & AccountId , depositor : & AccountId ) -> u32 {
1913+ use frame_support:: traits:: { schedule:: DispatchTime , Bounded , StorePreimage } ;
1914+ use hydradx_runtime:: Preimage ;
1915+ let call = hydradx_runtime:: RuntimeCall :: System ( frame_system:: Call :: remark { remark : vec ! [ 1 , 2 , 3 ] } ) ;
1916+ let bounded: Bounded < _ , <Runtime as frame_system:: Config >:: Hashing > = Preimage :: bound ( call) . unwrap ( ) ;
1917+ let now = System :: block_number ( ) ;
1918+ let ref_index = pallet_referenda:: ReferendumCount :: < Runtime > :: get ( ) ;
1919+ assert_ok ! ( Referenda :: submit(
1920+ RuntimeOrigin :: signed( submitter. clone( ) ) ,
1921+ Box :: new( RawOrigin :: Root . into( ) ) ,
1922+ bounded,
1923+ DispatchTime :: At ( now + 100 ) ,
1924+ ) ) ;
1925+ assert_ok ! ( Referenda :: place_decision_deposit(
1926+ RuntimeOrigin :: signed( depositor. clone( ) ) ,
1927+ ref_index,
1928+ ) ) ;
1929+ ref_index
1930+ }
1931+
19101932#[ test]
19111933fn migrate_should_move_legacy_position_into_gigahdx_when_called ( ) {
19121934 TestNet :: reset ( ) ;
@@ -1971,6 +1993,128 @@ fn migrate_should_refuse_when_no_legacy_position() {
19711993 } ) ;
19721994}
19731995
1996+ // Migration must not launder away a conviction commitment: `migrate` is refused
1997+ // while any conviction vote is still registered against the legacy position. The
1998+ // caller must `remove_vote` first (which applies the conviction lock — including
1999+ // to losing votes — while the legacy position still backs it), then migrate.
2000+ #[ test]
2001+ fn migrate_should_be_refused_while_conviction_vote_active_then_succeed_after_removal ( ) {
2002+ TestNet :: reset ( ) ;
2003+ hydra_live_ext ( PATH_TO_SNAPSHOT ) . execute_with ( || {
2004+ let alice: AccountId = ALICE . into ( ) ;
2005+ let bob: AccountId = BOB . into ( ) ;
2006+ assert_ok ! ( Balances :: force_set_balance(
2007+ RawOrigin :: Root . into( ) ,
2008+ alice. clone( ) ,
2009+ 10_000 * UNITS ,
2010+ ) ) ;
2011+ let _ = EVMAccounts :: bind_evm_address ( RuntimeOrigin :: signed ( alice. clone ( ) ) ) ;
2012+ init_legacy_staking ( ) ;
2013+ fund_bob_for_decision_deposit ( ) ;
2014+
2015+ assert_ok ! ( Staking :: stake( RuntimeOrigin :: signed( alice. clone( ) ) , 1_000 * UNITS ) ) ;
2016+
2017+ let ref_index = submit_remark_referendum ( & alice, & bob) ;
2018+ assert_ok ! ( ConvictionVoting :: vote(
2019+ RuntimeOrigin :: signed( alice. clone( ) ) ,
2020+ ref_index,
2021+ AccountVote :: Standard {
2022+ vote: Vote {
2023+ aye: false ,
2024+ conviction: Conviction :: Locked1x ,
2025+ } ,
2026+ balance: 1_000 * UNITS ,
2027+ } ,
2028+ ) ) ;
2029+
2030+ // Refused while the vote is registered (previously this leniently allowed
2031+ // finished votes to survive migration, stranding the losing-vote lock).
2032+ assert_noop ! (
2033+ GigaHdx :: migrate( RuntimeOrigin :: signed( alice. clone( ) ) ) ,
2034+ pallet_staking:: Error :: <Runtime >:: ExistingVotes
2035+ ) ;
2036+ assert ! ( pallet_gigahdx:: Stakes :: <Runtime >:: get( & alice) . is_none( ) ) ;
2037+
2038+ // Remove the vote, then migration succeeds.
2039+ assert_ok ! ( ConvictionVoting :: remove_vote(
2040+ RuntimeOrigin :: signed( alice. clone( ) ) ,
2041+ Some ( 0u16 ) ,
2042+ ref_index,
2043+ ) ) ;
2044+ assert_ok ! ( GigaHdx :: migrate( RuntimeOrigin :: signed( alice. clone( ) ) ) ) ;
2045+ assert ! (
2046+ pallet_gigahdx:: Stakes :: <Runtime >:: get( & alice) . is_some( ) ,
2047+ "gigahdx position opened once the vote is cleared"
2048+ ) ;
2049+ } ) ;
2050+ }
2051+
2052+ // The security goal end-to-end: a *losing* legacy vote gets conviction-locked
2053+ // when removed before migration, and that lock carries into the gigahdx position
2054+ // — it cannot be laundered away by migrating.
2055+ #[ test]
2056+ fn losing_vote_should_be_conviction_locked_before_legacy_migration ( ) {
2057+ TestNet :: reset ( ) ;
2058+ hydra_live_ext ( PATH_TO_SNAPSHOT ) . execute_with ( || {
2059+ let alice: AccountId = ALICE . into ( ) ;
2060+ let bob: AccountId = BOB . into ( ) ;
2061+ assert_ok ! ( Balances :: force_set_balance(
2062+ RawOrigin :: Root . into( ) ,
2063+ alice. clone( ) ,
2064+ 10_000 * UNITS ,
2065+ ) ) ;
2066+ let _ = EVMAccounts :: bind_evm_address ( RuntimeOrigin :: signed ( alice. clone ( ) ) ) ;
2067+ init_legacy_staking ( ) ;
2068+ fund_bob_for_decision_deposit ( ) ;
2069+
2070+ assert_ok ! ( Staking :: stake( RuntimeOrigin :: signed( alice. clone( ) ) , 1_000 * UNITS ) ) ;
2071+
2072+ let ref_index = submit_remark_referendum ( & alice, & bob) ;
2073+ // Vote AYE — the losing side once the referendum is rejected.
2074+ assert_ok ! ( ConvictionVoting :: vote(
2075+ RuntimeOrigin :: signed( alice. clone( ) ) ,
2076+ ref_index,
2077+ AccountVote :: Standard {
2078+ vote: Vote {
2079+ aye: true ,
2080+ conviction: Conviction :: Locked1x ,
2081+ } ,
2082+ balance: 1_000 * UNITS ,
2083+ } ,
2084+ ) ) ;
2085+
2086+ // Force the referendum to Rejected so Alice's AYE is on the losing side.
2087+ // `end = now`, so the conviction-lock window is open at the next remove.
2088+ let now = System :: block_number ( ) ;
2089+ pallet_referenda:: ReferendumInfoFor :: < Runtime > :: insert (
2090+ ref_index,
2091+ pallet_referenda:: ReferendumInfo :: Rejected ( now, None , None ) ,
2092+ ) ;
2093+
2094+ // Remove the losing vote while still a legacy staker → legacy hook applies
2095+ // the conviction lock to the loser.
2096+ assert_ok ! ( ConvictionVoting :: remove_vote(
2097+ RuntimeOrigin :: signed( alice. clone( ) ) ,
2098+ Some ( 0u16 ) ,
2099+ ref_index,
2100+ ) ) ;
2101+ assert_eq ! (
2102+ lock_amount( & alice, * b"pyconvot" ) ,
2103+ 1_000 * UNITS ,
2104+ "losing legacy vote must be conviction-locked on removal"
2105+ ) ;
2106+
2107+ // Migration succeeds and the conviction lock survives — not laundered away.
2108+ assert_ok ! ( GigaHdx :: migrate( RuntimeOrigin :: signed( alice. clone( ) ) ) ) ;
2109+ assert ! ( pallet_gigahdx:: Stakes :: <Runtime >:: get( & alice) . is_some( ) ) ;
2110+ assert_eq ! (
2111+ lock_amount( & alice, * b"pyconvot" ) ,
2112+ 1_000 * UNITS ,
2113+ "conviction lock carries into the migrated gigahdx position"
2114+ ) ;
2115+ } ) ;
2116+ }
2117+
19742118#[ test]
19752119fn legacy_stake_should_refuse_when_gigahdx_lock_present ( ) {
19762120 // Strict policy: HDX already pledged under `ghdxlock` cannot back a legacy
0 commit comments