-
Notifications
You must be signed in to change notification settings - Fork 8
carrotsmuggler - OOG / unexpected reverts due to incorrect usage of staticcall. #822
Comments
Escalate politely dispute that the severity is high because the transaction that meant to check the reentrancy burn too much gas and can revert and can block withdraw of fund or at least constantly burn all amount of gas and make user lose money in a single transaction, the cost burnt can by minimal, but suppose the user send 10000 transaction, the gas burnt lose add up |
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. |
|
With full respect to senior watson's comment
I have to dispute the statement above, Please review the duplicate issue #837 as well the old balancer reentrancy check version does not cap the staticcall gas limit but the new version add the 10000 gas cap and the balancer team already clearly state that the static call always revert even the reentrancy is not detected
use not capping the gas limit of static call means the user are constantly waste too much gas (let us say for a single call the user waste and lose 0.01 ETH at gas), after 20000 transaction the loss is cumulatively 200 ETH and has no upper limit of loss because the reason above the balancer push a PR fix for this issue specifically, can see the PR balancer/balancer-v2-monorepo#2467 gas is either wasted or transaction revert and block withdraw, which is both really bad for user in long term, so the severity should be high instead of medium
|
The comment by the LSW is wrong. The POC clearly shows 90% of gas is consumed even when no re-entrancy is detected, i.e. for normal usage of the protocol. When there is reentrancy, the entire transaction reverts. If there is no reentrancy, the static call still reverts due to a state change. There is no reentrancy for the situation given in the POC. The |
Thanks @JeffCX and @carrotsmuggler for your explanation. You are correct that the issue will happen irrespective of reentrancy conditions. The |
Hello JeffCX, I believe that the loss of gas might not qualify as a valid high. According to the guidelines in Sherlock's documentation, the OOG matter will be deemed a medium severity issue. It could be considered high only in cases where it results in a complete blockage of all user funds indefinitely. |
Sir, I agree the OOG does not block user withdraw forever but because the static call always revert and waste 63/64 gas when withdraw, the remaining 1 / 64 gas has to be enough to complete the transaction. this means user are force to overpaying 100x more gas to complete the withdraw from balancer vault we can make a analogy: the protocol open a bank, user put 10K fund into the bank, and the user should only pays for 1 USD transaction fee when withdraw the fund, but the bank said, sorry because of a bug, everyone has to pay 100 USD to withdraw the fund, and this 100x cost applies every user in every withdrawal transaction, then the result is the withdraw is not really usable and cause consistent loss of fund this 100x gas applies to all user in every withdrawal transaction that use the balancer vault and the loss of fund from gas has no upper bound, so I think a high severity is still justified. |
Hello, @JeffCX, The Out Of Gas (OOG) situation renders users unable to call a method of the contracts due to insufficient gas. On the other hand, your issue poses a risk where users:
From what I observe, the impact of your issue appears to be a subset of the impact caused by an OOG. Therefore, if the OOG is considered medium, your issue should be equal to or less than medium in severity. I would appreciate it if you could share your opinion on this. |
Yeap, it is a subset of impact caused by OOG, so Users can pay more fees to trigger the function, but as I already shown, every user needs to pay 100x gas more in every withdrawal transaction for balancer vault, so the lose of fund as gas is cumulatively high :) |
I believe that in the scenario of an OOG/DOS, it represents the worst-case scenario for your issue. |
user can pay 100x gas and use the function as long as the remaining 1/64 gas can complete the executions. in my original report the impact I summarized is:
emm as for
as long as user pay a lot of gas (which they should not, transaction can be processed), and if they do not pay that amount of gas, transaction fails and sir, just a friendly reminder my original escalation is the severity should be high instead of medium based on the impact 👍 |
Certainly, I comprehend that you are aiming to elevate the severity level of this issue. However, my stance remains that this issue should be classified as medium due to the following rationale: Let's consider a situation where Alice intends to initiate 2 methods. Method A results in a denial-of-service (DOS) due to an out-of-gas (OOG) scenario, while method B aligns with your described issue.
Consequently, we observe that method A costs Alice 11 ETH as a gas fee without any return, whereas method B costs Alice the same 11 ETH, yet she gains the opportunity to execute it. Hence, we can infer that method A is more susceptible than method B. |
Sir, I don't think the method A and method B example applies in the codebase and in this issue there is only one method for user to withdraw share from the vault I can add more detail to explain how this impact withdraw using top-down approach User can withdraw by calling withdraw in LMPVault.sol and triggers _withdraw the _withdraw calls the method _calcUserWithdrawSharesToBurn this calls LMPDebt._calcUserWithdrawSharesToBurn we need to know the debt value by calling destVault.debtValue this calls this line of code this calls the oracle code uint256 price = _systemRegistry.rootPriceOracle().getPriceInEth(_underlying); then if the dest vault is the balancer vault, balancer reetrancy check is triggered to waste 63 / 64 waste in oracle code so there is no function A and function B call as long as user can withdraw and wants to withdraw share from balancer vault, 100x gas overpayment is required |
I think we can treat this issue same as "transaction missing slippage protection" missing slippage protection is consider a high severity finding, but user may not lose million in one single transaction, the loss depends on user's trading amount the loss amount for individual transaction can be small but there are be a lot of user getting frontrunning and the missing slippage cause consistent leak of value all the above character applies to this finding as well can refer back to my first analogy
but the bank said, sorry because of a bug, everyone has to pay 100 USD to withdraw the fund, and this 100x cost applies every user in every withdrawal transaction, then the result is the withdraw is not really usable and cause consistent loss of fund this 100x gas applies to all user in every withdrawal transaction that use the balancer vault and the loss of fund from gas has no upper bound, so I think a high severity is still justified. |
You are referring to the gas usage here? Putting a limit on the gas is not a task for the protocol, this is a task for the wallet someone is using. As the escalation comment states
Impact is not significant enough for a high severity. Current opinion is to reject escalation and keep issue medium severity. |
sir, please read the report again, the flawed logic in the code charge user 100x gas in every transaction in every withdrawal
the most relevant comments is #822 (comment) and #822 (comment) idk how do state it more clearly, emm if you put money in the bank, you expect to pay 1 USD for withdrawal transaction fee, but every time you have to pay 100 USD withdrawal fee because of the bug this cause loss of fund for every user in every transaction for not only you but every user... |
@JeffCX what are the exact numbers on the withdrawal costs? E.g. if I want to withdraw $10k, how much gas can I expect to pay? If this is a significant amount I can see the argument for
But it's not clear how much this will be assuming current mainnet conditions. |
I write a simpe POC // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor()ERC20("MyToken", "MTK")
{}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
interface ICheckRetrancy {
function checkRentrancy() external;
}
contract RentrancyCheck {
uint256 state = 10;
function checkRentrancy() external {
address(this).staticcall(abi.encodeWithSignature("hihi()"));
}
function hihi() public {
state = 11;
}
}
contract Vault {
address balancerAddr;
bool checkRentrancy;
constructor(bool _checkRentrancy, address _balancerAddr) {
checkRentrancy = _checkRentrancy;
balancerAddr = _balancerAddr;
}
function toggleCheck(bool _state) public {
checkRentrancy = _state;
}
function withdraw(address token, uint256 amount) public {
if(checkRentrancy) {
ICheckRetrancy(balancerAddr).checkRentrancy();
}
IERC20(token).transfer(msg.sender, amount);
}
}
contract CounterTest is Test {
using stdStorage for StdStorage;
StdStorage stdlib;
MockERC20 token;
Vault vault;
RentrancyCheck rentrancyCheck;
address user = vm.addr(5201314);
function setUp() public {
token = new MockERC20();
rentrancyCheck = new RentrancyCheck();
vault = new Vault(false, address(rentrancyCheck));
token.mint(address(vault), 100000000 ether);
vm.deal(user, 100 ether);
// vault.toggleCheck(true);
}
function testPOC() public {
uint256 gas = gasleft();
uint256 amount = 100 ether;
vault.withdraw(address(token), amount);
console.log(gas - gasleft());
}
}
the call is
or
note first we do not check the reentrancy // vault.toggleCheck(true); we run forge test -vvv --match-test "testPOC" --fork-url "https://eth.llamarpc.com" --gas-limit 10000000 the gas cost is 42335
then we uncomment the vault.toggleCheck(true) and check the reentrancy that revert in staticcall vault.toggleCheck(true); we run the same test again, this is the output, as we can see the gas cost surge
then we can use this python scirpt to estimate how much gas is overpaid as lost of fund regular = 42313
overpaid = 9551666
# gas price: 45 gwei -> 0.000000045
cost = 0.000000045 * (overpaid - regular);
print(cost) the cost is 0.427920885 ETH in a single withdraw, assume user lost 0.427 ETH, if 500 user withdraw 20 times each and the total number of transaction is 10000 the lose on gas is 10000 * 0.427 ETH |
note that the more gas limit user set, the more fund user lose in gas but we are interested in what the lowest amount of gas limit user that user can set the pay for withdrawal transaction I did some fuzzing that number is 1800000 unit of gas the command to run the test is forge test -vvv --match-test "testPOC" --fork-url "https://eth.llamarpc.com" --gas-limit 1800000 setting gas limit lower than 1800000 unit of gas is likely to revert in out of gas under this setting, the overpaid transaction cost is 1730089 Running 1 test for test/Counter.t.sol:CounterTest
[PASS] testPOC() (gas: 1733192)
Logs:
1730089 in other words, in each withdrawal for every user, user can lose 0.073 ETH, (1730089 uint of gas * 45 gwei -> 0.000000045 ETH) assume there are 1000 user, each withdraw 10 times, they make 1000 * 10 = 100_00 transaction so the total lost is 100_00 * 0.07 = 700 ETH in reality the gas is more than that because user may use more than 1800000 unit of gas to finalize the withdrawal transaction |
@JeffCX thanks for putting in the effort to make this estimation. But as far as I can see, your estimation doesn't use the actual contracts in scope. But maybe that's irrelevant to make your point. This seems like the key sentence
This is an extra $100-$150 dollars per withdrawal action. This is not a very significant amount in my opinion. I assume an optimized withdrawal transaction will cost between $20-$50. So the difference is not as big. |
the POC is a simplified flow of this it is ok to disagree sir:) |
Result: |
Escalations have been resolved successfully! Escalation status:
|
carrotsmuggler
medium
OOG / unexpected reverts due to incorrect usage of staticcall.
Summary
OOG / unexpected reverts due to incorrect usage of staticcall.
Vulnerability Detail
The function
checkReentrancy
inBalancerUtilities.sol
is used to check if the balancer contract has been re-entered or not. It does this by doing astaticcall
on the pool contract and checking the return value. According to the solidity docs, if a staticcall encounters a state change, it burns up all gas and returns. ThecheckReentrancy
tries to callmanageUserBalance
on the vault contract, and returns if it finds a state change.The issue is that this burns up all the gas sent with the call. According to EIP150, a call gets allocated 63/64 bits of the gas, and the entire 63/64 parts of the gas is burnt up after the staticcall, since the staticcall will always encounter a storage change. This is also highlighted in the balancer monorepo, which has guidelines on how to check re-entrancy here.
This can also be shown with a simple POC.
The oracle is called to get a price. This oracle calls the
checkReentrancy
function and burns up the gas. The gas left is checked before and after this call.The output shows this:
This shows that 96% of the gas sent is burnt up in the oracle call.
Impact
This causes the contract to burn up 63/64 bits of gas in a single check. If there are lots of operations after this call, the call can revert due to running out of gas. This can lead to a DOS of the contract.
Code Snippet
https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/src/libs/BalancerUtilities.sol#L19-L28
Tool used
Foundry
Recommendation
According to the monorepo here, the staticall must be allocated a fixed amount of gas. Change the reentrancy check to the following.
This ensures gas isn't burnt up without reason.
The text was updated successfully, but these errors were encountered: