@@ -19,8 +19,9 @@ use solana_program::{
19
19
pubkey:: Pubkey ,
20
20
sysvar:: Sysvar ,
21
21
} ;
22
- use solend_sdk:: state:: { Obligation , PositionKind } ;
22
+ use solend_sdk:: state:: { HasRewardEnded , Obligation , PositionKind } ;
23
23
use solend_sdk:: { error:: LendingError , instruction:: reward_vault_authority_seeds} ;
24
+ use spl_associated_token_account:: get_associated_token_address_with_program_id;
24
25
25
26
use super :: {
26
27
check_and_unpack_pool_reward_accounts, unpack_token_account, Bumps ,
@@ -29,6 +30,8 @@ use super::{
29
30
30
31
/// Use [Self::from_unchecked_iter] to validate the accounts.
31
32
struct ClaimUserReward < ' a , ' info > {
33
+ /// ✅ is_signer
34
+ perhaps_payer_info : Option < & ' a AccountInfo < ' info > > ,
32
35
/// ✅ belongs to this program
33
36
/// ✅ unpacks
34
37
/// ✅ matches `lending_market_info`
@@ -37,7 +40,7 @@ struct ClaimUserReward<'a, 'info> {
37
40
/// ✅ belongs to the token program
38
41
/// ✅ is writable
39
42
/// ✅ matches `reward_mint_info`
40
- /// ✅ owned by the obligation owner
43
+ /// ✅ is obligation owner's ATA for the reward mint
41
44
obligation_owner_token_account_info : & ' a AccountInfo < ' info > ,
42
45
/// ✅ belongs to this program
43
46
/// ✅ unpacks
@@ -88,11 +91,22 @@ pub(crate) fn process(
88
91
) ?;
89
92
let reserve_key = accounts. reserve . key ( ) ;
90
93
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
+
91
105
// 1.
92
106
93
107
let pool_reward_manager = accounts. reserve . pool_reward_manager_mut ( position_kind) ;
94
108
95
- if let Some ( user_reward_manager) = accounts
109
+ if let Some ( ( _ , user_reward_manager) ) = accounts
96
110
. obligation
97
111
. user_reward_managers
98
112
. find_mut ( reserve_key, position_kind)
@@ -106,12 +120,23 @@ pub(crate) fn process(
106
120
107
121
// 2.
108
122
109
- let total_reward_amount = user_reward_manager. claim_rewards (
123
+ let ( has_ended , total_reward_amount) = user_reward_manager. claim_rewards (
110
124
pool_reward_manager,
111
125
* accounts. reward_token_vault_info . key ,
112
126
clock,
113
127
) ?;
114
128
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
+
115
140
// 3.
116
141
117
142
if total_reward_amount > 0 {
@@ -204,6 +229,7 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
204
229
let reward_token_vault_info = next_account_info ( iter) ?;
205
230
let lending_market_info = next_account_info ( iter) ?;
206
231
let token_program_info = next_account_info ( iter) ?;
232
+ let perhaps_payer_info = next_account_info ( iter) . ok ( ) ;
207
233
208
234
let ( _, reserve) = check_and_unpack_pool_reward_accounts (
209
235
program_id,
@@ -218,6 +244,11 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
218
244
} ,
219
245
) ?;
220
246
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
+
221
252
if obligation_info. owner != program_id {
222
253
msg ! ( "Obligation provided is not owned by the lending program" ) ;
223
254
return Err ( LendingError :: InvalidAccountOwner . into ( ) ) ;
@@ -230,19 +261,29 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
230
261
return Err ( LendingError :: InvalidAccountInput . into ( ) ) ;
231
262
}
232
263
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
+
233
280
if obligation_owner_token_account_info. owner != token_program_info. key {
234
281
msg ! ( "Obligation owner token account provided must be owned by the token program" ) ;
235
282
return Err ( LendingError :: InvalidTokenOwner . into ( ) ) ;
236
283
}
237
284
let obligation_owner_token_account =
238
285
unpack_token_account ( & obligation_owner_token_account_info. data . borrow ( ) ) ?;
239
286
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
- }
246
287
if obligation_owner_token_account. mint != * reward_mint_info. key {
247
288
msg ! ( "Obligation owner token account mint does not match the reward mint provided" ) ;
248
289
return Err ( LendingError :: InvalidAccountInput . into ( ) ) ;
@@ -283,6 +324,7 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
283
324
}
284
325
285
326
Ok ( Self {
327
+ perhaps_payer_info,
286
328
obligation_info,
287
329
obligation_owner_token_account_info,
288
330
_reserve_info : reserve_info,
0 commit comments