Closed howlbot-integration[bot] closed 3 months ago
The PoC does not function as described.
The exchange rate has decreased from 1 basket per RToken (after Alice's mint) to 0.5 baskets per RToken (after Bob's mint).
The exchange rate has not decreased -- it is 1:1 between basketsNeeded and totalSupply.
This results in amtRToken = 1 * 1 / 1 = 1, minting 1 RToken to Bob and updating basketsNeeded to 2.
And _mint will update totalSupply to 2.
The exchange rate has decreased from 1 basket per RToken (after Alice's mint) to 0.5 baskets per RToken (after Bob's mint).
The exchange rate is still basketsNeeded / totalSupply = 2 / 2 = 1
thereksfour marked the issue as unsatisfactory: Invalid
I'll make sure to clarify and it might help. Even if it is not immediately evident in the simplistic arithmetic of basketsNeeded / totalSupply
. The change in effective backing is the real issue and can lead to an inflated supply without appropriate collateral backing
Initial State and Exchange Rate Definition:
The initial state has totalSupply = 0
and basketsNeeded = 0
. The exchange rate is defined as basketsNeeded / totalSupply
. The protocol starts in a state where no tokens are minted and no collateral is required.
Effect of the First Mint:
_scaleUp()
is called with amtBaskets = 1
, totalSupply = 0
, and basketsNeeded = 0
.totalSupply
is zero, the function mints amtRToken = amtBaskets = 1
and updates basketsNeeded
to 1.totalSupply
is also 1 (because _mint()
will increase the total supply by amtRToken
), and basketsNeeded
is 1.1 basket per RToken
, calculated as basketsNeeded / totalSupply = 1 / 1 = 1
.Effect of the Second Mint:
_scaleUp()
is called with amtBaskets = 1
, totalSupply = 1
, and basketsNeeded = 1
.amtRToken = amtBaskets.muluDivu(totalSupply, basketsNeeded)
, which simplifies to 1 * 1 / 1 = 1
.amtRToken = 1
is minted for Bob, and basketsNeeded
is updated to 2.totalSupply
becomes 2 (again, _mint()
updates this), and basketsNeeded
is 2.1 basket per RToken
, calculated as basketsNeeded / totalSupply = 2 / 2 = 1
.I believe the confusion stems from a misunderstanding of how the exchange rate evolves when the protocol's state changes from zero to non-zero supply:
Issue with Zero Initial State:
When starting from a zero supply state, the first mint operation does not consider an exchange rate but instead sets an implicit one by directly using the amtBaskets
value. This is problematic because subsequent mints use a different formula (amtBaskets.muluDivu(totalSupply, basketsNeeded)
), which relies on the assumption that an initial exchange rate already exists.
Decreasing Effective Exchange Rate:
The effective exchange rate for new minters changes relative to the initial state. If you consider the sequence:
The effective backing per RToken decreases after the first mint. Bob’s mint introduces a change in the system's collateralization ratio because it does not account for additional collateral proportional to the new total supply — the system ends up with a 1:1 ratio only when retrospectively recalculated, not in effective terms.
I believe this is a widespread way of setting initial exchange rates and don't see any vulnerabilities in it.
Lines of code
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/RToken.sol#L483-L497 https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/RToken.sol#L89-L110
Vulnerability details
The
issue()
andissueTo()
functions allow users to mint new RTokens by depositing the required collateral baskets. The contract tracks the number of collateral baskets needed (basketsNeeded
) to fully collateralize the RToken supply and calculates the exchange rate asbasketsNeeded / totalSupply
.There is an issue in the
_scaleUp()
function, which is called byissueTo()
to mint new tokens and update thebasketsNeeded
value. The problem arises when minting tokens from a zero supply state. In this case, the function mintsamtBaskets
worth of RTokens instead of using the current exchange rate, allowing the effective exchange rate to decrease on subsequent mints.Impact
Proof of Concept
The RToken contract is deployed with an initial zero supply and zero
basketsNeeded
.Alice calls
issueTo(alice, 1)
, minting 1 RToken to her address. The_scaleUp()
function is called withamtBaskets = 1
,totalSupply = 0
, andbasketsNeeded = 0
. SincetotalSupply
is zero, the function mintsamtRToken = amtBaskets = 1
to Alice and updatesbasketsNeeded
to 1.Bob calls
issueTo(bob, 1)
, minting 1 RToken to his address. The_scaleUp()
function is called withamtBaskets = 1
,totalSupply = 1
, andbasketsNeeded = 1
. The function calculatesamtRToken
as follows:This results in
amtRToken = 1 * 1 / 1 = 1
, minting 1 RToken to Bob and updatingbasketsNeeded
to 2.The exchange rate has decreased from 1 basket per RToken (after Alice's mint) to 0.5 baskets per RToken (after Bob's mint).
Tools Used
Manual review
Recommended Mitigation Steps
The
_scaleUp()
function should establish a non-zero initialbasketsNeeded
value when minting from a zero supply state.Assessed type
Math