@@ -30,6 +30,8 @@ import {
3030 ExtensionType ,
3131 getExtensionData ,
3232 createApproveCheckedInstruction ,
33+ createThawAccountInstruction ,
34+ createFreezeAccountInstruction ,
3335 AccountState ,
3436} from '@solana/spl-token' ;
3537import { randomInt } from 'crypto' ;
@@ -596,19 +598,32 @@ class EarnTest<V extends Variant = Variant.New> {
596598 public async mintM ( to : PublicKey , amount : BN ) {
597599 const toATA : PublicKey = await this . getATA ( this . mMint . publicKey , to ) ;
598600
601+ // Check if the account is frozen, and if so, thaw it temporarily for minting
602+ const accountInfo = await getAccount ( this . provider . connection , toATA , undefined , TOKEN_2022_PROGRAM_ID ) ;
603+ const wasFrozen = accountInfo . isFrozen ;
604+
605+ if ( wasFrozen ) {
606+ await this . thawTokenAccount ( toATA ) ;
607+ }
608+
599609 const mintToInstruction = createMintToCheckedInstruction (
600610 this . mMint . publicKey ,
601611 toATA ,
602612 this . mMintAuthority . publicKey ,
603613 BigInt ( amount . toString ( ) ) ,
604614 6 ,
605- [ this . admin ] ,
615+ [ ] ,
606616 TOKEN_2022_PROGRAM_ID ,
607617 ) ;
608618
609619 let tx = new Transaction ( ) ;
610620 tx . add ( mintToInstruction ) ;
611- await this . provider . sendAndConfirm ! ( tx , [ this . admin ] ) ;
621+ await this . provider . sendAndConfirm ! ( tx , [ this . mMintAuthority ] ) ;
622+
623+ // Re-freeze the account if it was originally frozen
624+ if ( wasFrozen ) {
625+ await this . freezeTokenAccount ( toATA ) ;
626+ }
612627 }
613628
614629 public async getTokenBalance ( tokenAccount : PublicKey ) {
@@ -666,6 +681,32 @@ class EarnTest<V extends Variant = Variant.New> {
666681 return { sourceATA } ;
667682 }
668683
684+ public async thawTokenAccount ( tokenAccount : PublicKey ) {
685+ // For testing purposes, we'll directly manipulate the account state using the SVM
686+ // In a real scenario, only the freeze authority (global account) can thaw the account
687+ const accountInfo = this . svm . getAccount ( tokenAccount ) ! ;
688+
689+ // Token account state is at offset 108 for Token2022 accounts
690+ // AccountState: Uninitialized = 0, Initialized = 1, Frozen = 2
691+ // We set it to Initialized (1) to thaw it
692+ accountInfo . data [ 108 ] = 1 ;
693+
694+ this . svm . setAccount ( tokenAccount , accountInfo ) ;
695+ }
696+
697+ public async freezeTokenAccount ( tokenAccount : PublicKey ) {
698+ // For testing purposes, we'll directly manipulate the account state using the SVM
699+ // In a real scenario, only the freeze authority (global account) can freeze the account
700+ const accountInfo = this . svm . getAccount ( tokenAccount ) ! ;
701+
702+ // Token account state is at offset 108 for Token2022 accounts
703+ // AccountState: Uninitialized = 0, Initialized = 1, Frozen = 2
704+ // We set it to Frozen (2) to freeze it
705+ accountInfo . data [ 108 ] = 2 ;
706+
707+ this . svm . setAccount ( tokenAccount , accountInfo ) ;
708+ }
709+
669710 // general SVM cheat functions
670711 public warp ( seconds : BN , increment : boolean ) {
671712 const clock = this . svm . getClock ( ) ;
@@ -1902,5 +1943,281 @@ for (const variant of VARIANTS) {
19021943 await $ . expectTokenAccountState ( earnerTwoATA , AccountState . Frozen ) ;
19031944 } ) ;
19041945 } ) ;
1946+
1947+ describe ( 'recover_m unit tests' , ( ) => {
1948+ beforeEach ( async ( ) => {
1949+ // Initialize the program
1950+ await $ . initializeEarn ( initialIndex ) ;
1951+ } ) ;
1952+
1953+ test ( 'source_token_account not frozen - reverts' , async ( ) => {
1954+ // Create source and destination token accounts
1955+ const sourceAccount = await $ . getATA ( $ . mMint . publicKey , $ . earnerOne . publicKey ) ;
1956+ const destinationAccount = await $ . getATA ( $ . mMint . publicKey , $ . admin . publicKey ) ;
1957+
1958+ // Mint some tokens to the source account
1959+ await $ . mintM ( $ . earnerOne . publicKey , new BN ( 1000 ) ) ;
1960+
1961+ // Source account should be frozen by default due to M mint configuration
1962+ // Thaw the source account to make it invalid for recover_m
1963+ await $ . thawTokenAccount ( sourceAccount ) ;
1964+
1965+ // Attempt to recover from unfrozen source account should fail
1966+ await $ . expectAnchorError (
1967+ $ . earn . methods
1968+ . recoverM ( null )
1969+ . accountsPartial ( {
1970+ admin : $ . admin . publicKey ,
1971+ sourceTokenAccount : sourceAccount ,
1972+ destinationTokenAccount : destinationAccount ,
1973+ } )
1974+ . signers ( [ $ . admin ] )
1975+ . rpc ( ) ,
1976+ 'InvalidAccount' ,
1977+ ) ;
1978+ } ) ;
1979+
1980+ test ( 'm_mint doesnt match stored value in global account - reverts' , async ( ) => {
1981+ // Create a different mint
1982+ const wrongMint = new Keypair ( ) ;
1983+ await $ . createMint ( wrongMint , $ . mMintAuthority . publicKey ) ;
1984+
1985+ // Create source and destination token accounts for the wrong mint
1986+ const sourceAccount = await $ . getATA ( wrongMint . publicKey , $ . earnerOne . publicKey ) ;
1987+ const destinationAccount = await $ . getATA ( wrongMint . publicKey , $ . admin . publicKey ) ;
1988+
1989+ // Attempt to recover with wrong mint should fail
1990+ await $ . expectSystemError (
1991+ $ . earn . methods
1992+ . recoverM ( null )
1993+ . accountsPartial ( {
1994+ admin : $ . admin . publicKey ,
1995+ mMint : wrongMint . publicKey ,
1996+ sourceTokenAccount : sourceAccount ,
1997+ destinationTokenAccount : destinationAccount ,
1998+ } )
1999+ . signers ( [ $ . admin ] )
2000+ . rpc ( ) ,
2001+ ) ;
2002+ } ) ;
2003+
2004+ test ( 'token accounts are for the wrong mint - reverts' , async ( ) => {
2005+ // Create a different mint
2006+ const wrongMint = new Keypair ( ) ;
2007+ await $ . createMint ( wrongMint , $ . admin . publicKey ) ;
2008+
2009+ // Create source and destination token accounts for the wrong mint
2010+ const sourceAccount = await $ . getATA ( wrongMint . publicKey , $ . earnerOne . publicKey ) ;
2011+ const destinationAccount = await $ . getATA ( wrongMint . publicKey , $ . admin . publicKey ) ;
2012+
2013+ // Attempt to recover with token accounts for wrong mint should fail
2014+ await $ . expectAnchorError (
2015+ $ . earn . methods
2016+ . recoverM ( null )
2017+ . accountsPartial ( {
2018+ admin : $ . admin . publicKey ,
2019+ sourceTokenAccount : sourceAccount ,
2020+ destinationTokenAccount : destinationAccount ,
2021+ } )
2022+ . signers ( [ $ . admin ] )
2023+ . rpc ( ) ,
2024+ 'InvalidAccount' ,
2025+ ) ;
2026+ } ) ;
2027+
2028+ test ( 'amount is more than source token balance - reverts' , async ( ) => {
2029+ // Create source and destination token accounts
2030+ const sourceAccount = await $ . getATA ( $ . mMint . publicKey , $ . earnerOne . publicKey ) ;
2031+ const destinationAccount = await $ . getATA ( $ . mMint . publicKey , $ . admin . publicKey ) ;
2032+
2033+ // Mint some tokens to the source account
2034+ const sourceBalance = new BN ( 1000 ) ;
2035+ await $ . mintM ( $ . earnerOne . publicKey , sourceBalance ) ;
2036+
2037+ // Attempt to recover more than available balance
2038+ const excessiveAmount = sourceBalance . add ( new BN ( 500 ) ) ;
2039+ await $ . expectSystemError (
2040+ $ . earn . methods
2041+ . recoverM ( excessiveAmount )
2042+ . accountsPartial ( {
2043+ admin : $ . admin . publicKey ,
2044+ sourceTokenAccount : sourceAccount ,
2045+ destinationTokenAccount : destinationAccount ,
2046+ } )
2047+ . signers ( [ $ . admin ] )
2048+ . rpc ( ) ,
2049+ ) ;
2050+ } ) ;
2051+
2052+ test ( 'amount is None, transfers full balance - success' , async ( ) => {
2053+ // Create source and destination token accounts
2054+ const sourceAccount = await $ . getATA ( $ . mMint . publicKey , $ . earnerOne . publicKey ) ;
2055+ const destinationAccount = await $ . getATA ( $ . mMint . publicKey , $ . admin . publicKey ) ;
2056+
2057+ // Mint some tokens to the source account
2058+ const sourceBalance = new BN ( 1000 ) ;
2059+ await $ . mintM ( $ . earnerOne . publicKey , sourceBalance ) ;
2060+
2061+ // Get initial balances
2062+ const initialSourceBalance = await $ . getTokenBalance ( sourceAccount ) ;
2063+ const initialDestBalance = await $ . getTokenBalance ( destinationAccount ) ;
2064+
2065+ // Execute recover_m with no amount (should transfer full balance)
2066+ await $ . earn . methods
2067+ . recoverM ( null )
2068+ . accountsPartial ( {
2069+ admin : $ . admin . publicKey ,
2070+ sourceTokenAccount : sourceAccount ,
2071+ destinationTokenAccount : destinationAccount ,
2072+ } )
2073+ . signers ( [ $ . admin ] )
2074+ . rpc ( ) ;
2075+
2076+ // Verify balances after recovery
2077+ await $ . expectTokenBalance ( sourceAccount , new BN ( 0 ) ) ;
2078+ await $ . expectTokenBalance ( destinationAccount , initialDestBalance . add ( initialSourceBalance ) ) ;
2079+
2080+ // Verify account states
2081+ await $ . expectTokenAccountState ( sourceAccount , AccountState . Frozen ) ;
2082+ await $ . expectTokenAccountState ( destinationAccount , AccountState . Initialized ) ;
2083+ } ) ;
2084+
2085+ test ( 'amount is less than or equal to source_token_balance - success' , async ( ) => {
2086+ // Create source and destination token accounts
2087+ const sourceAccount = await $ . getATA ( $ . mMint . publicKey , $ . earnerOne . publicKey ) ;
2088+ const destinationAccount = await $ . getATA ( $ . mMint . publicKey , $ . admin . publicKey ) ;
2089+
2090+ // Mint some tokens to the source account
2091+ const sourceBalance = new BN ( 1000 ) ;
2092+ await $ . mintM ( $ . earnerOne . publicKey , sourceBalance ) ;
2093+
2094+ // Get initial balances
2095+ const initialSourceBalance = await $ . getTokenBalance ( sourceAccount ) ;
2096+ const initialDestBalance = await $ . getTokenBalance ( destinationAccount ) ;
2097+
2098+ // Execute recover_m with partial amount
2099+ const transferAmount = new BN ( 600 ) ;
2100+ await $ . earn . methods
2101+ . recoverM ( transferAmount )
2102+ . accountsPartial ( {
2103+ admin : $ . admin . publicKey ,
2104+ sourceTokenAccount : sourceAccount ,
2105+ destinationTokenAccount : destinationAccount ,
2106+ } )
2107+ . signers ( [ $ . admin ] )
2108+ . rpc ( ) ;
2109+
2110+ // Verify balances after recovery
2111+ const expectedSourceBalance = initialSourceBalance . sub ( transferAmount ) ;
2112+ const expectedDestBalance = initialDestBalance . add ( transferAmount ) ;
2113+ await $ . expectTokenBalance ( sourceAccount , expectedSourceBalance ) ;
2114+ await $ . expectTokenBalance ( destinationAccount , expectedDestBalance ) ;
2115+
2116+ // Verify account states
2117+ await $ . expectTokenAccountState ( sourceAccount , AccountState . Frozen ) ;
2118+ await $ . expectTokenAccountState ( destinationAccount , AccountState . Initialized ) ;
2119+ } ) ;
2120+
2121+ test ( 'destination token account already thawed - success' , async ( ) => {
2122+ // Create source and destination token accounts
2123+ const sourceAccount = await $ . getATA ( $ . mMint . publicKey , $ . earnerOne . publicKey ) ;
2124+ const destinationAccount = await $ . getATA ( $ . mMint . publicKey , $ . admin . publicKey ) ;
2125+
2126+ // Mint some tokens to the source account
2127+ const sourceBalance = new BN ( 1000 ) ;
2128+ await $ . mintM ( $ . earnerOne . publicKey , sourceBalance ) ;
2129+
2130+ // Thaw destination account
2131+ await $ . thawTokenAccount ( destinationAccount ) ;
2132+
2133+ // Get initial balances
2134+ const initialSourceBalance = await $ . getTokenBalance ( sourceAccount ) ;
2135+ const initialDestBalance = await $ . getTokenBalance ( destinationAccount ) ;
2136+
2137+ // Execute recover_m
2138+ const transferAmount = new BN ( 600 ) ;
2139+ await $ . earn . methods
2140+ . recoverM ( transferAmount )
2141+ . accountsPartial ( {
2142+ admin : $ . admin . publicKey ,
2143+ sourceTokenAccount : sourceAccount ,
2144+ destinationTokenAccount : destinationAccount ,
2145+ } )
2146+ . signers ( [ $ . admin ] )
2147+ . rpc ( ) ;
2148+
2149+ // Verify balances after recovery
2150+ const expectedSourceBalance = initialSourceBalance . sub ( transferAmount ) ;
2151+ const expectedDestBalance = initialDestBalance . add ( transferAmount ) ;
2152+ await $ . expectTokenBalance ( sourceAccount , expectedSourceBalance ) ;
2153+ await $ . expectTokenBalance ( destinationAccount , expectedDestBalance ) ;
2154+
2155+ // Verify account states
2156+ await $ . expectTokenAccountState ( sourceAccount , AccountState . Frozen ) ;
2157+ await $ . expectTokenAccountState ( destinationAccount , AccountState . Initialized ) ;
2158+ } ) ;
2159+
2160+ test ( 'destination token account frozen - success' , async ( ) => {
2161+ // Create source and destination token accounts
2162+ const sourceAccount = await $ . getATA ( $ . mMint . publicKey , $ . earnerOne . publicKey ) ;
2163+ const destinationAccount = await $ . getATA ( $ . mMint . publicKey , $ . admin . publicKey ) ;
2164+
2165+ // Mint some tokens to the source account
2166+ const sourceBalance = new BN ( 1000 ) ;
2167+ await $ . mintM ( $ . earnerOne . publicKey , sourceBalance ) ;
2168+
2169+ // Destination account should be frozen by default due to M mint configuration
2170+ await $ . expectTokenAccountState ( destinationAccount , AccountState . Frozen ) ;
2171+
2172+ // Get initial balances
2173+ const initialSourceBalance = await $ . getTokenBalance ( sourceAccount ) ;
2174+ const initialDestBalance = await $ . getTokenBalance ( destinationAccount ) ;
2175+
2176+ // Execute recover_m
2177+ const transferAmount = new BN ( 600 ) ;
2178+ await $ . earn . methods
2179+ . recoverM ( transferAmount )
2180+ . accountsPartial ( {
2181+ admin : $ . admin . publicKey ,
2182+ sourceTokenAccount : sourceAccount ,
2183+ destinationTokenAccount : destinationAccount ,
2184+ } )
2185+ . signers ( [ $ . admin ] )
2186+ . rpc ( ) ;
2187+
2188+ // Verify balances after recovery
2189+ const expectedSourceBalance = initialSourceBalance . sub ( transferAmount ) ;
2190+ const expectedDestBalance = initialDestBalance . add ( transferAmount ) ;
2191+ await $ . expectTokenBalance ( sourceAccount , expectedSourceBalance ) ;
2192+ await $ . expectTokenBalance ( destinationAccount , expectedDestBalance ) ;
2193+
2194+ // Verify account states
2195+ await $ . expectTokenAccountState ( sourceAccount , AccountState . Frozen ) ;
2196+ await $ . expectTokenAccountState ( destinationAccount , AccountState . Initialized ) ;
2197+ } ) ;
2198+
2199+ test ( 'non-admin cannot recover - reverts' , async ( ) => {
2200+ // Create source and destination token accounts
2201+ const sourceAccount = await $ . getATA ( $ . mMint . publicKey , $ . earnerOne . publicKey ) ;
2202+ const destinationAccount = await $ . getATA ( $ . mMint . publicKey , $ . nonAdmin . publicKey ) ;
2203+
2204+ // Mint some tokens to the source account
2205+ await $ . mintM ( $ . earnerOne . publicKey , new BN ( 1000 ) ) ;
2206+
2207+ // Attempt to recover as non-admin should fail
2208+ await $ . expectAnchorError (
2209+ $ . earn . methods
2210+ . recoverM ( null )
2211+ . accountsPartial ( {
2212+ admin : $ . nonAdmin . publicKey ,
2213+ sourceTokenAccount : sourceAccount ,
2214+ destinationTokenAccount : destinationAccount ,
2215+ } )
2216+ . signers ( [ $ . nonAdmin ] )
2217+ . rpc ( ) ,
2218+ 'NotAuthorized' ,
2219+ ) ;
2220+ } ) ;
2221+ } ) ;
19052222 } ) ;
19062223}
0 commit comments