Open sherlock-admin opened 9 months ago
1 comment(s) were left on this issue during the judging contest.
takarez commented:
valid: high(2)
The protocol team fixed this issue in PR/commit https://github.com/arcadia-finance/accounts-v2/pull/174.
Fix looks good. AccountV1.sol will now transfer all assets prior to valuing them.
The Lead Senior Watson signed off on the fix.
0xadrii
high
Caching Uniswap position liquidity allows borrowing using undercollateralized Uni positions
Summary
It is possible to fake the amount of liquidity held in a Uniswap V3 position, making the protocol believe the Uniswap position has more liquidity than the actual liquidity deposited in the position. This makes it possible to borrow using undercollateralized Uniswap positions.
Vulnerability Detail
When depositing into an account, the
deposit()
function is called, which calls the internal_deposit()
function. Depositing is performed in two steps:batchProcessDeposit()
function is called. This function checks if the deposited assets can be priced, and in case that a creditor is set, it also updates the exposures and underlying assets for the creditor.For Uniswap positions (and assuming that a creditor is set), calling
batchProcessDeposit()
will internally trigger theUniswapV3AM.processDirectDeposit()
:The Uniswap position will then be added to the protocol using the internal
_addAsset()
function. One of the most important actions performed inside this function is to store the liquidity that the Uniswap position has in that moment. Such liquidity is obtained from directly querying the NonfungiblePositionManager contract:As the snippet shows, the liquidity is stored in a mapping because “Since liquidity of a position can be increased by a non-owner, the max exposure checks could otherwise be circumvented.”. From this point forward, and until the Uniswap position is withdrawn from the account, the collateral value (i.e the amount that the position is worth) will be computed utilizing the
_getPosition()
internal function, which will read the cached liquidity value stored in theassetToLiquidity[assetId]
mapping, rather than directly consulting the NonFungibleManager contract. This way, the position won’t be able to surpass the max exposures:However, storing the liquidity leads to an attack vector that allows Uniswap positions’ liquidity to be comlpetely withdrawn while making the protocol believe that the Uniswap position is still full.
As mentioned in the beginning of the report, the deposit process is done in two steps: processing assets in the registry and transferring the actual assets to the account. Because processing assets in the registry is the step where the Uniswap position’s liquidity is cached, a malicious depositor can use an ERC777 hook in the transferring process to withdraw the liquidity in the Uniswap position.
The following steps show how the attack could be performed:
tokensToSend()
hook.deposit()
function with twoassetAddresses
to be deposited: the first asset must be an ERC777 token, and the second asset must be the Uniswap position.IRegistry(registry).batchProcessDeposit()
will then execute. This is the first of the two steps taking place to deposit assets, where the liquidity from the Uniswap position will be fetched from the NonFungiblePositionManager and stored in theassetToLiquidity[assetId]
mapping.tokensToSend()
hook in our malicious contract. At this point, our contract is still the owner of the Uniswap position (the Uniswap position won’t be transferred until the ERC777 transfer finishes), so the liquidity in the Uniswap position can be decreased inside the hook triggered in the malicious contract. This leaves the Uniswap position with a smaller liquidity amount than the one stored in thebatchProcessDeposit()
step, making the protocol believe that the liquidity stored in the position is the one that the position had prior to starting the attack.Proof of Concept
This proof of concept show show the previous attack can be performed so that the liquidity in the uniswap position is 0, while the collateral value for the account is far greater than 0.
Create a
ERC777Mock.sol
file inlib/accounts-v2/test/utils/mocks/tokens
and paste the code found in this github gist.Import the ERC777Mock and change the MockOracles, MockERC20 and Rates structs in
lib/accounts-v2/test/utils/Types.sol
to add an additionaltoken777ToUsd
,token777
of type ERC777Mock and token777ToUsd rate:Replace the contents inside
lib/accounts-v2/test/fuzz/Fuzz.t.sol
for the code found in this github gist.Next step is to replace the file found in
lending-v2/test/fuzz/Fuzz.t.sol
for the code found in this github gist.Create a
PocUniswap.t.sol
file inlending-v2/test/fuzz/LendingPool/PocUniswap.t.sol
and paste the following code snippet into it:Execute the following command being inside the
lending-v2
folder:forge test --mt testVuln_borrowUsingUndercollateralizedUniswapPosition -vvvvv
.NOTE: It is possible that you find issues related to code not being found. This is because the Uniswap V3 deployment uses foundry’s
vm.getCode()
and we are importing the deployment file from theaccounts-v2
repo to thelending-v2
repo, which makes foundry throw some errors. To fix this, just compile the contracts in theaccounts-v2
repo and copy the missing folders from theaccounts-v2/out
generated folder into thelending-v2/out
folder.Impact
High. The protocol will always believe that there is liquidity deposited in the Uniswap position while in reality the position is empty. This allows for undercollateralized borrows, essentially enabling the protocol to be drained if the attack is performed utilizing several uniswap positions.
Code Snippet
https://github.com/sherlock-audit/2023-12-arcadia/blob/main/accounts-v2/src/asset-modules/UniswapV3/UniswapV3AM.sol#L107
https://github.com/sherlock-audit/2023-12-arcadia/blob/main/accounts-v2/src/accounts/AccountV1.sol#L844
https://github.com/sherlock-audit/2023-12-arcadia/blob/main/accounts-v2/src/accounts/AccountV1.sol#L855
Tool used
Manual Review
Recommendation
There are several ways to mitigate this issue. One possible option is to perform the transfer of assets when depositing at the same time that the asset is processed, instead of first processing the assets (and storing the Uniswap liquidity) and then transferring them. Another option is to perform a liquidity check after depositing the Uniswap position, ensuring that the liquidity stored in the assetToLiquidity[assetId] mapping and the one returned by the NonFungiblePositionManager are the same.