Skip to content
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

fix: pool domination attack #17

Merged
merged 6 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/FeeCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator {
SD59x18 private redemptionFeeShift = sd(0.1 * 1e18); //-log10(0+0.1)=1 -> 10^-1
SD59x18 private redemptionFeeConstant = redemptionFeeScale * (one + redemptionFeeShift).log10(); //0.0413926851582251=log10(1+0.1)
SD59x18 private singleAssetRedemptionRelativeFee = sd(0.1 * 1e18);
SD59x18 private dustAssetRedemptionRelativeFee = sd(0.3 * 1e18);

address[] private _recipients;
uint256[] private _shares;
Expand Down Expand Up @@ -216,9 +217,19 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator {
SD59x18 i_b = tb * (db + redemptionFeeShift).log10();
SD59x18 fee_float = redemptionFeeScale * (i_b - i_a) + redemptionFeeConstant * amount_float;

require(fee_float > zero, "Fee must be greater than 0");
/*
@dev
The fee becomes negative if the amount is too small in comparison to the pool's size.
In such cases, we apply the dustAssetRedemptionRelativeFee, which is currently set at 30%.
This represents the maximum fee for the redemption function.
This measure protects against scenarios where the sum of multiple extremely small redemptions could deplete the pool at a discounted rate.

Case exists only if asset pool domination is > 90% and amount is ~1e-18 of that asset in the pool
*/
if (fee_float < zero) {
return intoUint256(amount_float * dustAssetRedemptionRelativeFee);
}

uint256 fee = intoUint256(fee_float);
return fee;
return intoUint256(fee_float);
}
}
87 changes: 49 additions & 38 deletions test/FeeCalculator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import {FeeCalculator} from "../src/FeeCalculator.sol";
import {SD59x18, sd, intoUint256 as sdIntoUint256} from "@prb/math/src/SD59x18.sol";
import {UD60x18, ud, intoUint256} from "@prb/math/src/UD60x18.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract MockPool is IERC20 {
Expand Down Expand Up @@ -177,35 +179,6 @@ contract FeeCalculatorTest is Test {
assertEq(fees[0], redemptionAmount / 10);
}

function testCalculateRedemptionFees_AlmostFullMonopolization_ZeroFees() public {
// Arrange
// Set up your test data
uint256 redemptionAmount = 1 * 1e18;

// Set up mock pool
mockPool.setTotalSupply(1e6 * 1e18 + 1);
mockToken.setTokenBalance(address(mockPool), 1e6 * 1e18);

// Act
vm.expectRevert("Fee must be greater than 0");
feeCalculator.calculateRedemptionFees(address(mockToken), address(mockPool), redemptionAmount);
}

function testCalculateRedemptionFees_CurrentSlightLessThanTotal_AmountSuperSmall_ShouldResultInException() public {
//this test was producing negative redemption fees before rounding extremely small negative redemption fees to zero
// Arrange
// Set up your test data
uint256 redemptionAmount = 186843141273221600445448244614; //1.868e29

// Set up mock pool
mockPool.setTotalSupply(11102230246251565404236316680908203126); //1.11e37
mockToken.setTokenBalance(address(mockPool), 11102230246251565403820829061134812052); //1.11e37

// Act
vm.expectRevert("Fee must be greater than 0");
feeCalculator.calculateRedemptionFees(address(mockToken), address(mockPool), redemptionAmount);
}

function testCalculateDepositFeesNormalCase_TwoFeeRecipientsSplitEqually() public {
// Arrange
// Set up your test data
Expand Down Expand Up @@ -619,6 +592,43 @@ contract FeeCalculatorTest is Test {
assertEq(fees[0], 737254938220315128);
}

function testCalculateRedemptionFees_HugeTotalLargeCurrentSmallDeposit_FeeCappedAt30Percent() public {
// Arrange
// Set up your test data
uint256 depositAmount = 10000 * 1e18;

// Set up mock pool
uint256 supply = 100000 * 1e18;
mockPool.setTotalSupply(100000 * 1e18);
mockToken.setTokenBalance(address(mockPool), supply - 1);

// Act
(address[] memory recipients, uint256[] memory fees) =
feeCalculator.calculateRedemptionFees(address(mockToken), address(mockPool), depositAmount);

// Assert
assertEq(recipients[0], feeRecipient);
assertEq(fees[0], depositAmount * 30 / 100);
}

function testCalculateRedemptionFees_NegativeFeeValue_FeeCappedAt30Percent() public {
// Arrange
// Set up your test data
uint256 depositAmount = 2323662174650;

// Set up mock pool
mockPool.setTotalSupply(56636794628913227180683983236);
mockToken.setTokenBalance(address(mockPool), 55661911070827884041095553095);

// Act
(address[] memory recipients, uint256[] memory fees) =
feeCalculator.calculateRedemptionFees(address(mockToken), address(mockPool), depositAmount);

// Assert
assertEq(recipients[0], feeRecipient);
assertEq(fees[0], depositAmount * 30 / 100);
}

function testCalculateDepositFeesFuzzy(uint256 depositAmount, uint256 current, uint256 total) public {
//vm.assume(depositAmount > 0);
//vm.assume(total > 0);
Expand Down Expand Up @@ -679,6 +689,8 @@ contract FeeCalculatorTest is Test {
uint256 current = _current;
uint256 total = _total;

SD59x18 dustAssetRedemptionRelativeFee = sd(0.3 * 1e18);

// Arrange
// Set up your test data

Expand Down Expand Up @@ -706,6 +718,12 @@ contract FeeCalculatorTest is Test {
);
}

/// @dev if we fail at the first try, we do not want to test the rest of the function
vm.assume(oneTimeRedemptionFailed == false);
/// @dev This prevents the case when the fee is so small that it is being calculated using dustAssetRedemptionRelativeFee
/// @dev we don not want to test this case
vm.assume(oneTimeFee != sdIntoUint256(sd(int256(redemptionAmount)) * dustAssetRedemptionRelativeFee));

uint256 equalRedemption = redemptionAmount / numberOfRedemptions;
uint256 restRedemption = redemptionAmount % numberOfRedemptions;
uint256 feeFromDividedRedemptions = 0;
Expand All @@ -730,15 +748,8 @@ contract FeeCalculatorTest is Test {
}
}

// Assert
if (multipleTimesRedemptionFailedCount == 0 && !oneTimeRedemptionFailed) {
uint256 maximumAllowedErrorPercentage = (numberOfRedemptions <= 1) ? 0 : 1;
if (
oneTimeFee + feeFromDividedRedemptions > 1e-8 * 1e18 // we skip assertion for extremely small fees (basically zero fees) because of numerical errors
) {
assertGe((maximumAllowedErrorPercentage + 100) * feeFromDividedRedemptions / 100, oneTimeFee);
} //we add 1% tolerance for numerical errors
}
// @dev we allow for 0.1% error
assertGe(1001 * feeFromDividedRedemptions / 1000, oneTimeFee);
}

function testCalculateDepositFeesFuzzy_DepositDividedIntoOneChunkFeesGreaterOrEqualToOneDeposit(
Expand Down