Platform: Code4rena
Start Date: 04/03/2024
Pot Size: $88,500 USDC
Total HM: 31
Participants: 105
Period: 11 days
Judge: ronnyx2017
Total Solo HM: 7
Id: 342
League: ETH
Rank: 86/105
Findings: 2
Award: $20.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xjuan
Also found by: 0rpse, 0x175, 0xAlix2, 0xBugSlayer, 0xloscar01, Ali-_-Y, Arz, CaeraDenoir, JohnSmith, Ocean_Sky, SpicyMeatball, alix40, ayden, falconhoof, givn, iamandreiski, kinda_very_good, nmirchev8, nnez, novamanbg, stackachu, wangxx2026
17.3162 USDC - $17.32
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L744 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1083
We can see that Uniswap NonfungiblePositionManager.sol uses _mint
when opening a position for a user, instead of _safeMint
(o.e receiver may be a contract, which is not IERC721Receiver
).
However in Revert Lend
we always use safeTransferFrom
when interacting with nft positions, which check whether to
is able to receive NFT (if it is contract, it should implement IERC721Receiver
). If this check is invalid, the whole transaction reverts.
It is possible exploiter to create a contract, which can interact with nfts, but it is not IERC721Receiver
. Example:
contract ExploiterContract{ IERC721 public nftContract; address private owner; contructor(address _nftContract){ owner = msg.sender; nftContract = IERC721(_nftContract); } function setNftContract(address _nftContract) external{ require(msg.sender == owner, "Unauthorized"); nftContract = IERC721(_nftContract); } function executeActionOnNftContract(bytes calldata _data) external { require(msg.sender == owner, "Unauthorized"); // Use low-level call to execute the function on nftContract (bool success, ) = address(nftContract).call(_data); require(success, "Function call failed"); } function executeAnyActionOnAnyContract(address target,bytes calldata _data) external { require(msg.sender == owner, "Unauthorized"); // Use low-level call to execute the function on nftContract (bool success, ) = target.call(_data); require(success, "Function call failed"); } }
This way ExploiterContract
can approve V3Vault
to tranfer NFT position to themself, and open a position, which is impossible to be liquidated, because the following line will always revert, because owner
(to
parameter) is contract, which does not implement IERC721Receiver
:
liquidate -> _cleanupLoan -> nftManager.safeTransferFrom
Impact:
IERC721Receiver
when/if borrower decide to repay loan.safeTransferFrom
is called.Manual Review
transferFrom
instead of safeTransferFrom
when availability of the function is crucial:function _cleanupLoan(uint256 tokenId, uint256 debtExchangeRateX96, uint256 lendExchangeRateX96, address owner) internal { _removeTokenFromOwner(owner, tokenId); _updateAndCheckCollateral(tokenId, debtExchangeRateX96, lendExchangeRateX96, loans[tokenId].debtShares, 0); delete loans[tokenId]; - nonfungiblePositionManager.safeTransferFrom(address(this), owner, tokenId); + nonfungiblePositionManager.transferFrom(address(this), owner, tokenId); emit Remove(tokenId, owner); }
DoS
#0 - c4-pre-sort
2024-03-18T18:41:17Z
0xEVom marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-18T18:41:48Z
0xEVom marked the issue as duplicate of #54
#2 - c4-judge
2024-03-31T16:08:39Z
jhsagd76 marked the issue as satisfactory
🌟 Selected for report: kfx
Also found by: 0x175, 0xAlix2, 0xjuan, AMOW, Aymen0909, CaeraDenoir, Giorgio, JCN, JecikPo, JohnSmith, Norah, SpicyMeatball, alexander_orjustalex, atoko, erosjohn, falconhoof, givn, grearlake, jnforja, kinda_very_good, lanrebayode77, nmirchev8, shaka, web3Tycoon, zxriptor
3.3501 USDC - $3.35
Inside liquidate
function caller must provide debtShares
of the position he is trying to liquidate. The same value is compared with loans[params.tokenId].debtShares
and if it is different, the tx reverts.
The problem is that borrower can front-run every liquidate
attempt by repaying only 1 share, which would make this check fails. Having in mind that mainly shares would have 18 decimals, repaying 1 share could go on forever, or at least reach underwater position
, which would be in cost of protocol reserves.
User A try to liquidate another user with LiquidateParams.debtShares = X:
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L685
Borrower B sees the tx and front-run it calling repay
with shares = X - 1:
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L954
User A transaction revert, because debt.shares
does not match:
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L695-L698
Above steps could continue along...
Manual Review
Remove the whole check:
- if (debtShares != params.debtShares) { - revert DebtChanged(); - }
DoS
#0 - c4-pre-sort
2024-03-22T16:17:15Z
0xEVom marked the issue as duplicate of #222
#1 - c4-pre-sort
2024-03-22T16:17:18Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-03-31T16:06:25Z
jhsagd76 marked the issue as satisfactory