Closed sherlock-admin closed 1 year ago
I'm not sure if this assumption (required for re-entrancy) is valid:
Ether or ERC20 tokens will be transferred to the user depending on the vault's underlying assets. The transfer will effectively pass the control back to the user.
We use payable(address).transfer(xxx)
to transfer Ether which does not forward sufficient gas to make any smart contract calls so that does not transfer control.
https://github.com/notional-finance/contracts-v2/blob/bcb145c725d90e65f99505d44275e62f37398264/contracts/internal/balances/protocols/GenericToken.sol#L23-L31
ERC777 transfers may transfer control to the user but ERC20 tokens generally do not.
This bug report and similar one in #104 depend on tokens that transfer control to the user in order to initiate re-entrancy.
While that is a vulnerability, currently none of the assets listed by Notional pass control to the user on transfer. I think the auditor has made some good points about checks-effects and account state in their reports so some credit is due to that. This issue and the linked issue (104) are more detailed versions of re-entrancy attacks so I would consider them separately than issue #12.
xiaoming90
high
Malicious User Can Steal All Assets From A Vault When Exiting The Vault By Performing A Re-Entrancy Attack
Summary
An attacker can perform a re-entrancy attack against the vault to drain all its assets by exploiting the vulnerable exit vault function as the checks-effects-interactions pattern is not adhered to.
Vulnerability Detail
If the
VaultConfiguration.ALLOW_REENTRANCY
setting of a vault is set toTrue
, theVaultAccountAction.exitVault
function allows re-entrancy. Line 183 within theVaultAccountAction.exitVault
function will set thereentrancyStatus
back to_NOT_ENTERED
to allow re-entrancy.Assume that a vault allows re-entrancy and Bob (attacker) is trying to exit the vault post-maturity after the vault has settled by calling the
VaultAccountAction.exitVault
function. Bob has 100 vault shares, so in the storage, Bob'svaultAccount.vaultShares = 100
at this point.At Line 186, the
VaultAccountLib.getVaultAccount
function is called to load the user's vault account data from the storage and load them onto thevaultAccount
variable on memory. The key point to note is thatvaultAccount
variable is stored in memory.At Line 192, the
vaultAccount.settleVaultAccount
function will settle Bob's vault account and set thevaultAccount.vaultShares
to0
in the memory.At Line 204, the
vaultConfig.redeemWithDebtRepayment
function will be called to redeem all strategy tokens, and any profits will be sent back to the user. Within thevaultConfig.redeemWithDebtRepayment
function, Ether or ERC20 tokens will be transferred to the user depending on the vault's underlying assets. The transfer will effectively pass the control back to the user. At this point, thevaultAccount.vaultShares
is0
in the memory, but thevaultAccount.vaultShares
is still100
in the storage because thevaultAccount.setVaultAccount
has not been triggered yet to write the information stored in the memory back to the storage.Bob re-enters the
VaultAccountAction.exitVault
function again, and at Line 186, theVaultAccountLib.getVaultAccount
function is called again to load the user's vault account data from the storage and load them onto thevaultAccount
variable on memory. Note that thevaultAccount.vaultShares
is still100
in storage at this point. Therefore, in this context, thevaultAccount.vaultShares
in the memory will be100,
and the vault will redeem the 100 vault shares again and transfer the profits to Bob again.Bob repeats the above steps multiple times until all the assets in the vault are drained.
At the end of the
VaultAccountAction.exitVault
function at Line 247, thevaultAccount
in memory is finally written back to the storage via thevaultAccount.setVaultAccount
function.A checks-effects-interactions pattern is a pattern to avoid a re-entrancy attack. Note that in terms of the checks-effects-interactions pattern:
Notice that over here the classic checks-effects-interactions pattern is not followed because the "effects" step occurred at the end, after the "interactions" step. Thus, a re-entrancy attack is possible over here.
https://github.com/sherlock-audit/2022-09-notional/blob/main/contracts-v2/contracts/external/actions/VaultAccountAction.sol#L169
Additional Note About Vault Account Data
All vault account data are stored inside a storage slot within the LibStorage. At the start of the function, the function will attempt to load the vault account data from the storage slot within the LibStorage via the
VaultAccount.getVaultAccount
function , and load them onto a state variable in memory (usually calledvaultAccount
). When logic within a function is executed, all the changes are made against thevaultAccount
state variable in the memory. At the end of the function, the function will write the data fromvaultAccount
state variable in the memory back onto the storage slot in the LibStorage via theVaultAccount.setVaultAccount
function.https://github.com/sherlock-audit/2022-09-notional/blob/main/contracts-v2/contracts/internal/vaults/VaultAccount.sol#L39
https://github.com/sherlock-audit/2022-09-notional/blob/main/contracts-v2/contracts/internal/vaults/VaultAccount.sol#L54
Impact
All assets within the vault can be drained by the attacker, thus leaving other vault shareholders with nothing.
Code Snippet
https://github.com/sherlock-audit/2022-09-notional/blob/main/contracts-v2/contracts/external/actions/VaultAccountAction.sol#L169
Tool used
Manual Review
Recommendation
It is recommended to adhere to the checks-effects-interactions pattern to prevent re-entrancy attacks. Write the vault account data in the memory back to the storage (effects) before performing a transfer (interactions).
If possible, remove the option of allowing re-entrancy as this is a significant attack vector that an attacker always exploits. Redesign the system so that re-entrancy is not needed.
Duplicate of #12