Open c4-bot-9 opened 7 months ago
0xEVom marked the issue as duplicate of #309
0xEVom marked the issue as sufficient quality report
jhsagd76 marked the issue as satisfactory
jhsagd76 changed the severity to 2 (Med Risk)
jhsagd76 marked the issue as not a duplicate
jhsagd76 marked the issue as primary issue
jhsagd76 changed the severity to 3 (High Risk)
jhsagd76 marked the issue as selected for report
Lines of code
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L454-L473 https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1223-L1241
Vulnerability details
Issue Description
The
onERC721Received
function is invoked whenever the vault contract receives a Uniswap V3 position ERC721 token. This can happen either when an owner creates a new position or when a transformation occurs.For this issue, we'll focus on the second case, specifically when a position is going through a transformation, which creates a new position token. In such a case, we have
tokenId != oldTokenId
, and the else block is run, as shown below:We should note that the
_cleanupLoan
function does return the old position token to the owner:The issue that can occur is that the
_cleanupLoan
is invoked before the_updateAndCheckCollateral
call. So, a malicious owner can use theonERC721Received
callback when receiving the old token to call theborrow
function, which makes changes toloans[tokenId].debtShares
and calls_updateAndCheckCollateral
. When the call resumes, theV3Vault.onERC721Received
function will call_updateAndCheckCollateral
again, resulting in incorrect accounting of internal token configs debt shares (tokenConfigs[token0].totalDebtShares
&tokenConfigs[token1].totalDebtShares
) and potentially impacting the vault borrowing process negatively.Proof of Concept
Let's use the following scenario to demonstrate the issue:
Before starting, we suppose the following states:
tokenConfigs[token0].totalDebtShares = 10000
tokenConfigs[token1].totalDebtShares = 15000
Bob has previously deposited a UniswapV3 position (which uses token0 and token1) with
tokenId = 12
and borrowedloans[tokenId = 12].debtShares = 1000
debt shares.Bob calls the
transform
function to change the range of his position using the AutoRange transformer, which mints a new ERC721 tokentokenId = 20
for the newly arranged position and sends it to the vault.Upon receiving the new token, the
V3Vault.onERC721Received
function is triggered. As we're in transformation mode and the token ID is different, the second else block above will be executed.V3Vault.onERC721Received
will copy loan debt shares to the new token, so we'll haveloans[tokenId = 20].debtShares = 1000
.Then
V3Vault.onERC721Received
will invoke the_cleanupLoan
function to clear the data of the old loan and transfer the old position tokentokenId = 12
back to Bob.5.1.
_cleanupLoan
will also call_updateAndCheckCollateral
function to changeoldShares = 1000 --> newShares = 0
(remove old token shares), resulting in:tokenConfigs[token0].totalDebtShares = 10000 - 1000 = 9000
tokenConfigs[token1].totalDebtShares = 15000 - 1000 = 14000
Bob, upon receiving the old position token, will also use the ERC721
onERC721Received
callback to call theborrow
function. He will borrow 200 debt shares against his new position tokentokenId = 20
.6.1. The
borrow
function will update the token debt shares fromloans[tokenId = 20].debtShares = 1000
to:loans[tokenId = 20].debtShares = 1000 + 200 = 1200
(assuming the position is healthy).6.2. The
borrow
function will also invoke the_updateAndCheckCollateral
function to changeoldShares = 1000 --> newShares = 1200
fortokenId = 20
, resulting in:tokenConfigs[token0].totalDebtShares = 9000 + 200 = 9200
tokenConfigs[token1].totalDebtShares = 14000 + 200 = 14200
Bob's borrow call ends, and the
V3Vault.onERC721Received
call resumes._updateAndCheckCollateral
gets called again, changingoldShares = 0 --> newShares = 1200
(as the borrow call changed the token debt shares), resulting in:tokenConfigs[token0].totalDebtShares = 9200 + 1200 = 10400
tokenConfigs[token1].totalDebtShares = 14200 + 1200 = 15400
Now, let's assess what Bob managed to achieve by taking a normal/honest transformation process (without using the
onERC721Received
callback) and then a borrow operation scenario:Normally, when
V3Vault.onERC721Received
is called, it shouldn't change the internal token configs debt shares (tokenConfigs[token0].totalDebtShares
&tokenConfigs[token1].totalDebtShares
). After a normalV3Vault.onERC721Received
, we should still have:tokenConfigs[token0].totalDebtShares = 10000
tokenConfigs[token1].totalDebtShares = 15000
Then, when Bob borrows 200 debt shares against the new token, we should get:
tokenConfigs[token0].totalDebtShares = 10000 + 200 = 10200
tokenConfigs[token1].totalDebtShares = 15000 + 200 = 15200
We observe that by using the
onERC721Received
callback, Bob managed to increase the internal token configs debt shares (tokenConfigs[token0].totalDebtShares
&tokenConfigs[token1].totalDebtShares
) by 200 debt shares more than expected.This means that Bob, by using this attack, has manipulated the internal token configs debt shares, making the vault believe it has 200 additional debt shares. Bob can repeat this attack multiple times until he approaches the limit represented by
collateralValueLimitFactorX32
andcollateralValueLimitFactorX32
multiplied by the amount of asset lent as shown below:Then, when other borrowers try to call the
borrow
function, it will revert because_updateAndCheckCollateral
will trigger theCollateralValueLimit
error, thinking there is too much debt already. However, this is not the case, as the internal token configs debt shares have been manipulated (increased) by an attacker (Bob).This attack is irreversible because there is no way to correct the internal token configs debt shares (
tokenConfigs[token0].totalDebtShares
&tokenConfigs[token1].totalDebtShares
), and the vault will remain in that state, not allowing users to borrow, resulting in no interest being accrued and leading to financial losses for the lenders and the protocol.Impact
A malicious attacker could use the AutoRange transformation process to manipulate the internal token configs debt shares, potentially resulting in:
Tools Used
Manual review, VS Code
Recommended Mitigation
The simplest way to address this issue is to ensure that the
onERC721Received
function follows the Correctness by Construction (CEI) pattern, as follows:Assessed type
Context