Missing account reload after cross program invocation
Description
Accounts that can be modified by cross program invocations (CPI) must be explicitly deserialized again after the CPI is performed. withdraw_stake
invokes the token program to transfer tokens from the custody account to the user withdrawal account. After the CPI, the amount
from the custody account is passed to validate
without refreshing the information read from the account. The amount passed to validate
is therefore the amount before the tokens are transferred.
transfer(
CpiContext::from(&*ctx.accounts).with_signer(&[&[
AUTHORITY_SEED.as_bytes(),
ctx.accounts.stake_account_positions.key().as_ref(),
&[stake_account_metadata.authority_bump],
]]),
amount,
)?;
if utils::risk::validate(
stake_account_positions,
!!! stake_account_custody.amount,
unvested_balance,
current_epoch,
config.unlocking_duration,
)
Impact
This mistake makes the call to validate
occurring after the transfer effectively useless. Fortunately the issue is not exploitable, as the function performs a call to validate
immediately before the token transfer, using a computed amount corresponding to the amount after the transfer.
Recommendations
Currently, the call to validate
after transfer
does not have any effect; it cannot cause a transaction to revert, since the condition for a revert would have already been caught by the call to validate
before the transfer. Therefore removing the call to validate
after transfer
is a valid remediation.
Alternatively, we recommend calling inserting a call to .reload()
immediately after the call to transfer
.
Remediation
TBD: Pending client feedback.