Open code423n4 opened 1 year ago
Would like to get some sponsor comments on this once prior to final review.
0xean marked the issue as satisfactory
tmattimore marked the issue as sponsor confirmed
We think its real.
other potential mitigation:
Will discuss more and decide on mitigation path with team.
We think its real.
other potential mitigation:
- governance level norm of excluding erc777 as collateral. Can't fully enforce though, so not a full mitigation.
Will discuss more and decide on mitigation path with team.
Thanks @tmattimore - I am going to downgrade to M due to the external requirements needed for it to become a reality. If I may ask, What is the hesitancy to simply introduce standard reentrancy modifiers? Its not critical to the audit in any way, just more of my own curiosity.
0xean changed the severity to 2 (Med Risk)
0xean marked the issue as primary issue
We think its real. other potential mitigation:
- governance level norm of excluding erc777 as collateral. Can't fully enforce though, so not a full mitigation.
Will discuss more and decide on mitigation path with team.
What is the hesitancy to simply introduce standard reentrancy modifiers? Its not critical to the audit in any way, just more of my own curiosity.
@0xean we would need a global mutex in order to prevent the attack noted here, which means lots of gas-inefficient external calls. The classic OZ modifier wouldn't be enough.
Lines of code
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L439-L514 https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BackingManager.sol#L105-L150
Vulnerability details
Impact
Function
redeem()
redeems RToken for basket collateral and it updatedbasketsNeeded
and transfers users basket ERC20 from BackingManager to user address. it loops through tokens and transfer them to caller and if one of tokens were ERC777 or any other 3rd party protocol token with hook, attacker can perform reentrancy attack during token transfers. Attacker can cause multiple impacts by choosing the reentrancy function:redeem()
again and bypass "bounding each withdrawal by the prorata share when protocol is under-collateralized" because tokens balance of BackingManager is not updated yet.BackingManager.manageTokens()
and becausebasketsNeeded
gets decreased and basket tokens balances of BasketManager are not updated, code would detect those tokens as excess funds and would distribute them between RSR stakers and RToken holders and some of RToken deposits would get transferred to RSR holders as rewards.Proof of Concept
This is
redeem()
code:As you can see code calculates withdrawal amount of each basket erc20 tokens by calling
basketHandler.quote()
and then bounds each withdrawal by the prorata share of token balance, in case protocol is under-collateralized. and then code updatesbasketsNeeded
and in the end transfers the tokens. if one of those tokens were ERC777 then that token would call receiver hook function in token transfer. there may be other 3rd party protocol tokens that calls registered hook functions during the token transfer. as reserve protocol is permission less and tries to work with all tokens so the external call in the token transfer can call hook functions. attacker can use this hook and perform reentrancy attack. This isfullyCollateralized()
code in BasketHandler:As you can see it calculates baskets that can be held by backingManager tokens balance and needed baskets by RToken contract and by comparing them determines that if RToken is fully collateralized or not. if RToken is fully collateralized then
BackingManager.manageTokens()
would callhandoutExcessAssets()
and would distributes extra funds between RToken holders and RSR stakers. the root cause of the issue is that during tokens transfers inredeem()
not all the basket tokens balance of the BackingManager updates once and if one has hook function which calls attacker contract then attacker can use this updated token balance of the contract and perform his reentrancy attack. attacker can call different functions for reentrancy. these are two scenarios: ** scenario #1: attacker callredeem()
again and bypass prorata share bound check when protocol is under-collaterialized:SOME_ERC777
,USDT
] with quantity [1, 1] are in the basket right now and basket nonce is BasketNonce1.SOME_ERC777
balance and 100KUSDT
balance.basketsNeeded
in RToken is 150K and RToken supply is 150K and attacker address Attacker1 has 30k RToken. battery charge allows for attacker to withdraw 30K tokens in one block.SOME_ERC777
token to get called during transfers.redeem()
to redeem 15K RToken and code would updatedbasketsNeeded
to 135K and code would bounds withdrawal by prorata shares of balance of the BackingManager because protocol is under-collateralized and code would calculated withdrawal amouns as 15KSOME_ERC777
tokens and 10KUSDT
tokens (instead of 15KUSDT
tokens) for withdraws.SOME_ERC777
tokens first to attacker address and attacker contract would get called during the hook function and nowbasketsNeeded
is 135K and total RTokens is 135K and BackingManager balance is 185KSOME_ERC777
and 100KUSDT
(USDT
is not yet transferred). then attacker contract can callredeem()
again for the remaining 15K RTokens.SOME_ERC777
and 11.1KUSDT
(USDT_balance rtokenAmount / totalSupply = 100K 15K / 135K) and it would burn 15K RToken form caller and the new value of totalSupply of RTokens would be 120K andbasketsNeeded
would be 120K too. then code would transfers 15KSOME_ERC777
and 11.1KUSDT
for attacker address.redeem()
would transfer 10KUSDT
to attacker in the rest of the execution. attacker would receive 30KSOME_ERC777
and 21.1KUSDT
tokens for 15K redeemed RToken but attacker should have get (100 * 30K / 150K = 20K
) 20KUSDT
tokens because of the bound each withdrawal by the prorata share, in case we're currently under-collateralized.charge/2
amount of RToken in each block and stole other users funds when protocol is under collaterlized.** scenario #2: attacker can call
BackingManager.manageTokens()
for reentrancy call:SOME_ERC777
,USDT
] with quantity [1, 1] are in the basket right now and basket nonce is BasketNonce1.SOME_ERC777
balance and 150KUSDT
balance.basketsNeeded
in RToken is 150K and RToken supply is 150K and attacker address Attacker1 has 30k RToken. battery charge allows for attacker to withdraw 30K tokens in one block.SOME_ERC777
token to get called during transfers.redeem()
to redeem 30K RToken and code would updatedbasketsNeeded
to 120K and burn 30K RToken and code would calculated withdrawal amounts as 30KSOME_ERC777
tokens and 30KUSDT
tokens for withdraws.SOME_ERC777
tokens first to attacker address and attacker contract would get called during the hook function and nowbasketsNeeded
is 120K and total RTokens is 120K and BackingManager balance is 170KSOME_ERC777
and 150KUSDT
(USDT
is not yet transferred). then attacker contract can callBackingManager.manageTokens()
.manageTokens()
would calculated baskets can held by BackingManager and it would be higher than 150K andbasketsNeeded
would be 130K and code would consider 60KSOME_ERC777
and 30KUSDT
tokens as revenue and try to distribute it between RSR stakers and RToken holders. code would mint 30K RTokens and would distribute it.redeem()
would transfer 30KUSDT
to attacker address in rest of the execution.Tools Used
VIM
Recommended Mitigation Steps
prevent reading reentrancy attack by central reentrancy guard or by one main proxy interface contract that has reentrancy guard. or create contract state (similar to basket nonce) which changes after each interaction and check for contracts states change during the call. (start and end of the call)