@@ -19,8 +19,9 @@ use solana_program::{
1919 pubkey:: Pubkey ,
2020 sysvar:: Sysvar ,
2121} ;
22- use solend_sdk:: state:: { Obligation , PositionKind } ;
22+ use solend_sdk:: state:: { HasRewardEnded , Obligation , PositionKind } ;
2323use solend_sdk:: { error:: LendingError , instruction:: reward_vault_authority_seeds} ;
24+ use spl_associated_token_account:: get_associated_token_address_with_program_id;
2425
2526use super :: {
2627 check_and_unpack_pool_reward_accounts, unpack_token_account, Bumps ,
@@ -29,6 +30,8 @@ use super::{
2930
3031/// Use [Self::from_unchecked_iter] to validate the accounts.
3132struct ClaimUserReward < ' a , ' info > {
33+ /// ✅ is_signer
34+ perhaps_payer_info : Option < & ' a AccountInfo < ' info > > ,
3235 /// ✅ belongs to this program
3336 /// ✅ unpacks
3437 /// ✅ matches `lending_market_info`
@@ -37,7 +40,7 @@ struct ClaimUserReward<'a, 'info> {
3740 /// ✅ belongs to the token program
3841 /// ✅ is writable
3942 /// ✅ matches `reward_mint_info`
40- /// ✅ owned by the obligation owner
43+ /// ✅ is obligation owner's ATA for the reward mint
4144 obligation_owner_token_account_info : & ' a AccountInfo < ' info > ,
4245 /// ✅ belongs to this program
4346 /// ✅ unpacks
@@ -88,11 +91,22 @@ pub(crate) fn process(
8891 ) ?;
8992 let reserve_key = accounts. reserve . key ( ) ;
9093
94+ // AUDIT:
95+ // > ClaimUserReward doesn’t check if the Obligation is stale.
96+ // > This can cause problems for borrow rewards, because the obligation's liability_shares will
97+ // > be stale.
98+ if matches ! ( position_kind, PositionKind :: Borrow )
99+ && accounts. obligation . last_update . is_stale ( clock. slot ) ?
100+ {
101+ msg ! ( "obligation is stale and must be refreshed in the current slot" ) ;
102+ return Err ( LendingError :: ObligationStale . into ( ) ) ;
103+ }
104+
91105 // 1.
92106
93107 let pool_reward_manager = accounts. reserve . pool_reward_manager_mut ( position_kind) ;
94108
95- if let Some ( user_reward_manager) = accounts
109+ if let Some ( ( _ , user_reward_manager) ) = accounts
96110 . obligation
97111 . user_reward_managers
98112 . find_mut ( reserve_key, position_kind)
@@ -106,12 +120,23 @@ pub(crate) fn process(
106120
107121 // 2.
108122
109- let total_reward_amount = user_reward_manager. claim_rewards (
123+ let ( has_ended , total_reward_amount) = user_reward_manager. claim_rewards (
110124 pool_reward_manager,
111125 * accounts. reward_token_vault_info . key ,
112126 clock,
113127 ) ?;
114128
129+ // AUDIT:
130+ // > ClaimUserReward on Suilend can only be called permissionlessly if the reward period is
131+ // > fully elapsed.
132+ let payer_matches_obligation_owner = accounts
133+ . perhaps_payer_info
134+ . map_or ( false , |payer| payer. key == & accounts. obligation . owner ) ;
135+ if !matches ! ( has_ended, HasRewardEnded :: Yes ) && !payer_matches_obligation_owner {
136+ msg ! ( "User reward manager has not ended, but payer does not match obligation owner" ) ;
137+ return Err ( LendingError :: InvalidSigner . into ( ) ) ;
138+ }
139+
115140 // 3.
116141
117142 if total_reward_amount > 0 {
@@ -204,6 +229,7 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
204229 let reward_token_vault_info = next_account_info ( iter) ?;
205230 let lending_market_info = next_account_info ( iter) ?;
206231 let token_program_info = next_account_info ( iter) ?;
232+ let perhaps_payer_info = next_account_info ( iter) . ok ( ) ;
207233
208234 let ( _, reserve) = check_and_unpack_pool_reward_accounts (
209235 program_id,
@@ -218,6 +244,11 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
218244 } ,
219245 ) ?;
220246
247+ if perhaps_payer_info. map ( |a| !a. is_signer ) . unwrap_or ( false ) {
248+ msg ! ( "Payer account must be a signer" ) ;
249+ return Err ( LendingError :: InvalidSigner . into ( ) ) ;
250+ }
251+
221252 if obligation_info. owner != program_id {
222253 msg ! ( "Obligation provided is not owned by the lending program" ) ;
223254 return Err ( LendingError :: InvalidAccountOwner . into ( ) ) ;
@@ -230,19 +261,29 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
230261 return Err ( LendingError :: InvalidAccountInput . into ( ) ) ;
231262 }
232263
264+ // AUDIT:
265+ // > In ClaimUserReward, because this is a permissionless instruction, we recommend
266+ // > validating that obligation_owner_token_account_info is an associated token account
267+ // > (ATA), rather than only a token account owned by the obligation owner.
268+ // > Allowing arbitrary token accounts would require indexing each one, adding unnecessary
269+ // > complexity and risk.
270+ let expected_ata = get_associated_token_address_with_program_id (
271+ & obligation. owner ,
272+ reward_mint_info. key ,
273+ token_program_info. key ,
274+ ) ;
275+ if expected_ata != * obligation_owner_token_account_info. key {
276+ msg ! ( "Token account for collecting rewards must be ATA" ) ;
277+ return Err ( LendingError :: InvalidAccountInput . into ( ) ) ;
278+ }
279+
233280 if obligation_owner_token_account_info. owner != token_program_info. key {
234281 msg ! ( "Obligation owner token account provided must be owned by the token program" ) ;
235282 return Err ( LendingError :: InvalidTokenOwner . into ( ) ) ;
236283 }
237284 let obligation_owner_token_account =
238285 unpack_token_account ( & obligation_owner_token_account_info. data . borrow ( ) ) ?;
239286
240- if obligation_owner_token_account. owner != obligation. owner {
241- msg ! (
242- "Obligation owner token account owner does not match the obligation owner provided"
243- ) ;
244- return Err ( LendingError :: InvalidAccountInput . into ( ) ) ;
245- }
246287 if obligation_owner_token_account. mint != * reward_mint_info. key {
247288 msg ! ( "Obligation owner token account mint does not match the reward mint provided" ) ;
248289 return Err ( LendingError :: InvalidAccountInput . into ( ) ) ;
@@ -283,6 +324,7 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
283324 }
284325
285326 Ok ( Self {
327+ perhaps_payer_info,
286328 obligation_info,
287329 obligation_owner_token_account_info,
288330 _reserve_info : reserve_info,
0 commit comments