diff --git a/src/FeeCalculator.sol b/src/FeeCalculator.sol index 304260c..e030052 100644 --- a/src/FeeCalculator.sol +++ b/src/FeeCalculator.sol @@ -11,7 +11,6 @@ import {SD59x18, sd, intoUint256} from "@prb/math/src/SD59x18.sol"; import {IFeeCalculator, FeeDistribution} from "./interfaces/IFeeCalculator.sol"; import "./interfaces/IPool.sol"; -import {VintageData, ITCO2} from "./interfaces/ITCO2.sol"; /// @title FeeCalculator /// @author Neutral Labs Inc. & Toucan Protocol @@ -164,11 +163,12 @@ contract FeeCalculator is IFeeCalculator, Ownable { { require(depositAmount > 0, "depositAmount must be > 0"); - uint256 feeAmount = getDepositFee(depositAmount, getProjectSupply(pool, tco2), getTotalSupply(pool)); - - require(feeAmount <= depositAmount, "Fee must be lower or equal to deposit amount"); - require(feeAmount > 0, "Fee must be greater than 0"); - feeDistribution = calculateFeeShares(feeAmount); + feeDistribution = calculateFee( + getTotalSupply(pool), + IPool(pool).totalPerProjectSupply(tco2), + depositAmount, + getDepositFee + ); } /// @notice Calculates the fee shares and recipients based on the total fee. @@ -211,13 +211,63 @@ contract FeeCalculator is IFeeCalculator, Ownable { address tco2 = tco2s[0]; uint256 redemptionAmount = redemptionAmounts[0]; - require(redemptionAmount > 0, "redemptionAmount must be > 0"); + feeDistribution = calculateFee( + getTotalSupply(pool), + IPool(pool).totalPerProjectSupply(tco2), + redemptionAmount, + getRedemptionFee + ); + } - uint256 feeAmount = getRedemptionFee(redemptionAmount, getProjectSupply(pool, tco2), getTotalSupply(pool)); + /// @notice Calculates the deposit fee for a given amount of an ERC1155 project. + /// @param pool The address of the pool. + /// @param erc1155 The address of the ERC1155 project + /// @param tokenId The tokenId of the vintage. + /// @param depositAmount The amount to be deposited. + /// @return feeDistribution How the fee is meant to be + /// distributed among the fee recipients. + function calculateDepositFees(address pool, address erc1155, uint256 tokenId, uint256 depositAmount) + external + view + override + returns (FeeDistribution memory feeDistribution) + { + require(depositAmount > 0, "depositAmount must be > 0"); - require(feeAmount <= redemptionAmount, "Fee must be lower or equal to redemption amount"); - require(feeAmount > 0, "Fee must be greater than 0"); - feeDistribution = calculateFeeShares(feeAmount); + feeDistribution = calculateFee( + getTotalSupply(pool), + IPool(pool).totalPerProjectSupply(erc1155, tokenId), + depositAmount, + getDepositFee + ); + } + + /// @notice Calculates the redemption fees for a given amount on ERC1155 projects. + /// @param pool The address of the pool. + /// @param erc1155s The addresses of the ERC1155 projects. + /// @param tokenIds The tokenIds of the project vintages. + /// @param redemptionAmounts The amounts to be redeemed. + /// @return feeDistribution How the fee is meant to be + /// distributed among the fee recipients. + function calculateRedemptionFees( + address pool, + address[] calldata erc1155s, + uint256[] calldata tokenIds, + uint256[] calldata redemptionAmounts + ) external view override returns (FeeDistribution memory feeDistribution) { + require(erc1155s.length == tokenIds.length, "erc1155s/tokenIds length mismatch"); + require(erc1155s.length == redemptionAmounts.length, "erc1155s/redemptionAmounts length mismatch"); + require(erc1155s.length == 1, "only one"); + address erc1155 = erc1155s[0]; + uint256 tokenId = tokenIds[0]; + uint256 redemptionAmount = redemptionAmounts[0]; + + feeDistribution = calculateFee( + getTotalSupply(pool), + IPool(pool).totalPerProjectSupply(erc1155, tokenId), + redemptionAmount, + getRedemptionFee + ); } /// @notice Gets the total supply of a given pool. @@ -228,15 +278,6 @@ contract FeeCalculator is IFeeCalculator, Ownable { return totalSupply; } - /// @notice Gets the total supply of a project in the pool. - /// @param pool The address of the pool. - /// @return The total supply of the pool. - function getProjectSupply(address pool, address tco2) private view returns (uint256) { - VintageData memory vData = ITCO2(tco2).getVintageData(); - uint256 projectSupply = IPool(pool).totalPerProjectTCO2Supply(vData.projectTokenId); - return projectSupply; - } - /// @notice Calculates the ratios for deposit fee calculation. /// @param amount The amount to be deposited. /// @param current The current balance of the pool. @@ -349,6 +390,22 @@ contract FeeCalculator is IFeeCalculator, Ownable { return intoUint256(feeSD); } + function calculateFee( + uint256 totalPoolSupply, + uint256 projectSupply, + uint256 requestedAmount, + function(uint256, uint256, uint256) view returns (uint256) calculator + ) internal view returns (FeeDistribution memory) { + require(requestedAmount > 0, "requested amount must be > 0"); + + uint256 feeAmount = calculator(requestedAmount, projectSupply, totalPoolSupply); + + require(feeAmount <= requestedAmount, "Fee must be lower or equal to requested amount"); + require(feeAmount > 0, "Fee must be greater than 0"); + + return calculateFeeShares(feeAmount); + } + /// @notice Returns the current fee setup. /// @return recipients shares The fee recipients and their share of the total fee. function getFeeSetup() external view returns (address[] memory recipients, uint256[] memory shares) { diff --git a/src/interfaces/IFeeCalculator.sol b/src/interfaces/IFeeCalculator.sol index 1f1c3ef..419a023 100644 --- a/src/interfaces/IFeeCalculator.sol +++ b/src/interfaces/IFeeCalculator.sol @@ -20,10 +20,11 @@ interface IFeeCalculator { /// @param depositAmount The amount to be deposited. /// @return feeDistribution How the fee is meant to be /// distributed among the fee recipients. - function calculateDepositFees(address pool, address tco2, uint256 depositAmount) - external - view - returns (FeeDistribution memory feeDistribution); + function calculateDepositFees( + address pool, + address tco2, + uint256 depositAmount + ) external view returns (FeeDistribution memory feeDistribution); /// @notice Calculates the redemption fees for a given amount. /// @param pool The address of the pool. @@ -31,8 +32,23 @@ interface IFeeCalculator { /// @param redemptionAmounts The amounts to be redeemed. /// @return feeDistribution How the fee is meant to be /// distributed among the fee recipients. - function calculateRedemptionFees(address pool, address[] calldata tco2s, uint256[] calldata redemptionAmounts) - external - view - returns (FeeDistribution memory feeDistribution); + function calculateRedemptionFees( + address pool, + address[] calldata tco2s, + uint256[] calldata redemptionAmounts + ) external view returns (FeeDistribution memory feeDistribution); + + function calculateDepositFees( + address pool, + address erc1155, + uint256 tokenId, + uint256 depositAmount + ) external view returns (FeeDistribution memory feeDistribution); + + function calculateRedemptionFees( + address pool, + address[] calldata erc1155s, + uint256[] calldata tokenIds, + uint256[] calldata redemptionAmounts + ) external view returns (FeeDistribution memory feeDistribution); } diff --git a/src/interfaces/IPool.sol b/src/interfaces/IPool.sol index 4ddc6f6..3332bc3 100644 --- a/src/interfaces/IPool.sol +++ b/src/interfaces/IPool.sol @@ -16,8 +16,19 @@ interface IPool { /// @notice Exposes the total TCO2 supply of a project in a pool, /// tracked as the aggregation of deposit, redemmption and bridge actions - /// @param projectTokenId The token id of the project as it's tracked - /// in the CarbonProjects contract + /// @param tco2 The TCO2 address of the project /// @return supply Current supply of a project in the pool - function totalPerProjectTCO2Supply(uint256 projectTokenId) external view returns (uint256 supply); + function totalPerProjectSupply( + address tco2 + ) external view returns (uint256 supply); + + /// @notice Exposes the total TCO2 supply of a project in a pool, + /// tracked as the aggregation of deposit, redemmption and bridge actions + /// @param erc1155 The ERC1155 address of the project + /// @param tokenId The token id of the project + /// @return supply Current supply of a project in the pool + function totalPerProjectSupply( + address erc1155, + uint256 tokenId + ) external view returns (uint256 supply); } diff --git a/test/FeeCalculator.t.sol b/test/FeeCalculator/AbstractFeeCalculator.t.sol similarity index 84% rename from test/FeeCalculator.t.sol rename to test/FeeCalculator/AbstractFeeCalculator.t.sol index 8daf281..7ad29c6 100644 --- a/test/FeeCalculator.t.sol +++ b/test/FeeCalculator/AbstractFeeCalculator.t.sol @@ -6,13 +6,13 @@ pragma solidity ^0.8.13; import {Test, console2} from "forge-std/Test.sol"; -import {FeeCalculator} from "../src/FeeCalculator.sol"; -import {FeeDistribution} from "../src/interfaces/IFeeCalculator.sol"; +import {FeeCalculator} from "../../src/FeeCalculator.sol"; +import {FeeDistribution} from "../../src/interfaces/IFeeCalculator.sol"; import {SD59x18, sd, intoUint256 as sdIntoUint256} from "@prb/math/src/SD59x18.sol"; import {UD60x18, ud, intoUint256} from "@prb/math/src/UD60x18.sol"; -import "./TestUtilities.sol"; +import "../TestUtilities.sol"; -contract FeeCalculatorTest is Test { +abstract contract AbstractFeeCalculatorTest is Test { using TestUtilities for uint256[]; UD60x18 private one = ud(1e18); @@ -36,6 +36,10 @@ contract FeeCalculatorTest is Test { feeCalculator.feeSetup(recipients, feeShares); } + function setProjectSupply(address token, uint256 supply) internal virtual; + function calculateDepositFees(address pool, address token, uint256 amount) internal view virtual returns (FeeDistribution memory); + function calculateRedemptionFees(address pool, address[] memory tokens, uint256[] memory amounts) internal view virtual returns (FeeDistribution memory); + function testFeeSetupEmpty() public { address[] memory recipients = new address[](0); uint256[] memory feeShares = new uint256[](0); @@ -70,11 +74,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -95,11 +99,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -118,13 +122,13 @@ contract FeeCalculatorTest is Test { uint256[] memory redemptionAmounts = new uint256[](1); redemptionAmounts[0] = redemptionAmount; - // Set up mock pool + // Set up mock pools mockPool.setTotalSupply(1e6 * 1e18); - mockPool.setProjectSupply(1, 1 * 1e18); + setProjectSupply(address(mockToken), 1 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -146,11 +150,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e6 * 1e18); - mockPool.setProjectSupply(1, 1e6 * 1e18); + setProjectSupply(address(mockToken), 1e6 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -168,7 +172,7 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); address[] memory _recipients = new address[](2); _recipients[0] = feeRecipient1; @@ -180,7 +184,7 @@ contract FeeCalculatorTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -201,7 +205,7 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); address[] memory _recipients = new address[](2); _recipients[0] = feeRecipient1; @@ -213,7 +217,7 @@ contract FeeCalculatorTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -232,11 +236,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(53461 * 1e18); - mockPool.setProjectSupply(1, 15462 * 1e18); + setProjectSupply(address(mockToken), 15462 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -252,11 +256,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act vm.expectRevert("Fee must be greater than 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_DepositOfHundredWei_ShouldThrowError() public { @@ -269,11 +273,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act vm.expectRevert("Fee must be greater than 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_DepositOfHundredThousandsPartOfOne_NonzeroFee() public { @@ -283,11 +287,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -303,11 +307,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -328,7 +332,7 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); address[] memory _recipients = new address[](5); _recipients[0] = feeRecipient1; @@ -346,7 +350,7 @@ contract FeeCalculatorTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -376,7 +380,7 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); address[] memory _recipients = new address[](5); _recipients[0] = feeRecipient1; @@ -394,7 +398,7 @@ contract FeeCalculatorTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -419,11 +423,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(100 * 1e6 * 1e18); - mockPool.setProjectSupply(1, 1e6 * 1e18); + setProjectSupply(address(mockToken), 1e6 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -439,11 +443,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act vm.expectRevert("depositAmount must be > 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_CurrentGreaterThanTotal_ExceptionShouldBeThrown() public { @@ -453,13 +457,13 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 1500 * 1e18); + setProjectSupply(address(mockToken), 1500 * 1e18); // Act vm.expectRevert( "The total volume in the pool must be greater than or equal to the volume for an individual asset" ); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateRedemptionFees_CurrentGreaterThanTotal_ExceptionShouldBeThrown() public { @@ -473,13 +477,13 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 1500 * 1e18); + setProjectSupply(address(mockToken), 1500 * 1e18); // Act & Assert vm.expectRevert( "The total volume in the pool must be greater than or equal to the volume for an individual asset" ); - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); } function testCalculateRedemptionFees_AmountGreaterThanCurrent_ExceptionShouldBeThrown() public { @@ -493,11 +497,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act vm.expectRevert("The amount to be redeemed cannot exceed the current balance of the pool"); - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); } function testCalculateRedemptionFees_ZeroRedemption_ExceptionShouldBeThrown() public { @@ -511,11 +515,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act & Assert - vm.expectRevert("redemptionAmount must be > 0"); - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + vm.expectRevert("requested amount must be > 0"); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); } function testCalculateDepositFees_EmptyPool_FeeCappedAt10Percent() public { @@ -525,11 +529,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(0); - mockPool.setProjectSupply(1, 0); + setProjectSupply(address(mockToken), 0); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -545,11 +549,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1); - mockPool.setProjectSupply(1, 0); + setProjectSupply(address(mockToken), 0); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -569,11 +573,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act - vm.expectRevert("redemptionAmount must be > 0"); - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + vm.expectRevert("requested amount must be > 0"); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); } function testCalculateRedemptionFees_TotalEqualCurrent_FeeCappedAt10Percent() public { @@ -587,11 +591,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000); - mockPool.setProjectSupply(1, 1000); + setProjectSupply(address(mockToken), 1000); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -607,11 +611,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000); - mockPool.setProjectSupply(1, 1000); + setProjectSupply(address(mockToken), 1000); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -627,11 +631,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000); - mockPool.setProjectSupply(1, 999); + setProjectSupply(address(mockToken), 999); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -647,11 +651,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 0); + setProjectSupply(address(mockToken), 0); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -672,11 +676,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool uint256 supply = 100000 * 1e18; mockPool.setTotalSupply(100000 * 1e18); - mockPool.setProjectSupply(1, supply - 1); + setProjectSupply(address(mockToken), supply - 1); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -696,11 +700,11 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(56636794628913227180683983236); - mockPool.setProjectSupply(1, 55661911070827884041095553095); + setProjectSupply(address(mockToken), 55661911070827884041095553095); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -840,12 +844,12 @@ contract FeeCalculatorTest is Test { // Arrange // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); feeCalculator.setDepositFeeScale(0.09 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 100 * 1e18); + calculateDepositFees(address(mockPool), address(mockToken), 100 * 1e18); uint256[] memory fees = feeDistribution.shares; // Assert @@ -856,12 +860,12 @@ contract FeeCalculatorTest is Test { // Arrange // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); feeCalculator.setDepositFeeRatioScale(0.2 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 100 * 1e18); + calculateDepositFees(address(mockPool), address(mockToken), 100 * 1e18); uint256[] memory fees = feeDistribution.shares; // Assert @@ -872,12 +876,12 @@ contract FeeCalculatorTest is Test { // Arrange // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 1000 * 1e18); + setProjectSupply(address(mockToken), 1000 * 1e18); feeCalculator.setSingleAssetDepositRelativeFee(0.67 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 100 * 1e18); + calculateDepositFees(address(mockPool), address(mockToken), 100 * 1e18); uint256[] memory fees = feeDistribution.shares; // Assert @@ -893,12 +897,12 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); feeCalculator.setRedemptionFeeScale(0.4 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); uint256[] memory fees = feeDistribution.shares; // Assert @@ -914,12 +918,12 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); feeCalculator.setRedemptionFeeShift(0.5 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); uint256[] memory fees = feeDistribution.shares; // Assert @@ -935,12 +939,12 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 1000 * 1e18); + setProjectSupply(address(mockToken), 1000 * 1e18); feeCalculator.setSingleAssetRedemptionRelativeFee(0.83 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); uint256[] memory fees = feeDistribution.shares; assertEq(fees[0], 83 * 1e18); @@ -957,12 +961,12 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(56636794628913227180683983236); - mockPool.setProjectSupply(1, 55661911070827884041095553095); + setProjectSupply(address(mockToken), 55661911070827884041095553095); feeCalculator.setDustAssetRedemptionRelativeFee(0.91 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); + calculateRedemptionFees(address(mockPool), tco2s, redemptionAmounts); uint256[] memory fees = feeDistribution.shares; // Assert diff --git a/test/FeeCalculator/FeeCalculatorERC1155.t.sol b/test/FeeCalculator/FeeCalculatorERC1155.t.sol new file mode 100644 index 0000000..1f49b9a --- /dev/null +++ b/test/FeeCalculator/FeeCalculatorERC1155.t.sol @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import "./AbstractFeeCalculator.t.sol"; + +contract FeeCalculatorERC1155Test is AbstractFeeCalculatorTest { + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setERC1155Supply(address(token), 1, supply); + } + + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + 1, + amount + ); + } + + function calculateRedemptionFees( + address pool, + address[] memory tokens, + uint256[] memory amounts + ) internal view override returns (FeeDistribution memory) { + uint256[] memory tokenIds = new uint256[](tokens.length); + tokenIds[0] = 1; + return + feeCalculator.calculateRedemptionFees( + address(pool), + tokens, + tokenIds, + amounts + ); + } +} diff --git a/test/FeeCalculator/FeeCalculatorTCO2.t.sol b/test/FeeCalculator/FeeCalculatorTCO2.t.sol new file mode 100644 index 0000000..b6ac179 --- /dev/null +++ b/test/FeeCalculator/FeeCalculatorTCO2.t.sol @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import "./AbstractFeeCalculator.t.sol"; + +contract FeeCalculatorTCO2Test is AbstractFeeCalculatorTest { + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setTCO2Supply(address(token), supply); + } + + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + amount + ); + } + + function calculateRedemptionFees( + address pool, + address[] memory tokens, + uint256[] memory amounts + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateRedemptionFees( + address(pool), + tokens, + amounts + ); + } +} diff --git a/test/FeeCalculatorFuzzy/AbstractFeeCalculator.fuzzy.t.sol b/test/FeeCalculatorFuzzy/AbstractFeeCalculator.fuzzy.t.sol new file mode 100644 index 0000000..a6971ca --- /dev/null +++ b/test/FeeCalculatorFuzzy/AbstractFeeCalculator.fuzzy.t.sol @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {FeeCalculator} from "../../src/FeeCalculator.sol"; +import {FeeDistribution} from "../../src/interfaces/IFeeCalculator.sol"; +import {SD59x18, sd, intoUint256 as sdIntoUint256} from "@prb/math/src/SD59x18.sol"; +import {UD60x18, ud, intoUint256} from "@prb/math/src/UD60x18.sol"; +import "../TestUtilities.sol"; + +abstract contract AbstractFeeCalculatorTestFuzzy is Test { + using TestUtilities for uint256[]; + + FeeCalculator public feeCalculator; + MockPool public mockPool; + MockToken public mockToken; + address public feeRecipient = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; + + function setUp() public { + feeCalculator = new FeeCalculator(); + mockPool = new MockPool(); + mockToken = new MockToken(); + address[] memory recipients = new address[](1); + recipients[0] = feeRecipient; + uint256[] memory feeShares = new uint256[](1); + feeShares[0] = 100; + feeCalculator.feeSetup(recipients, feeShares); + } + + function setProjectSupply(address token, uint256 supply) internal virtual; + function calculateDepositFees(address pool, address token, uint256 amount) internal view virtual returns (FeeDistribution memory); + function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption( + uint8 numberOfRedemptions, + uint128 _redemptionAmount, + uint128 _current, + uint128 _total + ) public virtual; + function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + uint8 numberOfDeposits, + uint256 depositAmount, + uint256 current, + uint256 total + ) public virtual; + + function testCalculateDepositFees_FuzzyExtremelySmallDepositsToLargePool_ShouldThrowError(uint256 depositAmount) + public + { + vm.assume(depositAmount <= 1e-14 * 1e18); + vm.assume(depositAmount >= 10); + + //Note! This is a bug, where a very small deposit to a very large pool + //causes a == b because of precision limited by ratioDenominator in FeeCalculator + + // Arrange + // Set up your test data + + // Set up mock pool + mockPool.setTotalSupply(1e12 * 1e18); + setProjectSupply(address(mockToken), 1e9 * 1e18); + + vm.expectRevert("Fee must be greater than 0"); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + } + + function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoOneChunkFeesGreaterOrEqualToOneRedemption( + uint128 _redemptionAmount, + uint128 _current, + uint128 _total + ) public { + //just a sanity check + testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption( + 1, _redemptionAmount, _current, _total + ); + } + + + function testCalculateDepositFeesFuzzy_DepositDividedIntoOneChunkFeesGreaterOrEqualToOneDeposit( + uint256 depositAmount, + uint256 current, + uint256 total + ) public { + //just a sanity check + testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + 1, depositAmount, current, total + ); + } + + + function testFeeSetupFuzzy(address[] memory recipients, uint8 firstShare) public { + vm.assume(recipients.length <= 100); + vm.assume(recipients.length > 1); //at least two recipients + vm.assume(firstShare <= 100); + vm.assume(firstShare > 0); + + uint256[] memory feeShares = new uint256[](recipients.length); + + uint256 shareLeft = 100 - firstShare; + feeShares[0] = firstShare; + uint256 equalShare = shareLeft / (recipients.length - 1); + uint256 leftShare = shareLeft % (recipients.length - 1); + + for (uint256 i = 1; i < recipients.length; i++) { + feeShares[i] = equalShare; + } + feeShares[recipients.length - 1] += leftShare; //last one gets additional share + feeCalculator.feeSetup(recipients, feeShares); + + uint256 depositAmount = 100 * 1e18; + // Set up mock pool + mockPool.setTotalSupply(200 * 1e18); + setProjectSupply(address(mockToken), 100 * 1e18); + + // Act + FeeDistribution memory feeDistribution = + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + address[] memory gotRecipients = feeDistribution.recipients; + uint256[] memory fees = feeDistribution.shares; + + // Assert + assertEq(gotRecipients.length, recipients.length); + for (uint256 i = 0; i < recipients.length; i++) { + assertEq(gotRecipients[i], recipients[i]); + } + + assertEq(fees.sumOf(), 11526003792614720250); + + assertApproxEqAbs(fees[0], 11526003792614720250 * uint256(firstShare) / 100, recipients.length - 1 + 1); //first fee might get the rest from division + + for (uint256 i = 1; i < recipients.length - 1; i++) { + assertApproxEqAbs(fees[i], 11526003792614720250 * equalShare / 100, 1); + } + assertApproxEqAbs(fees[recipients.length - 1], 11526003792614720250 * (equalShare + leftShare) / 100, 1); + } +} diff --git a/test/FeeCalculatorFuzzy/FeeCalculatorERC1155.fuzzy.t.sol b/test/FeeCalculatorFuzzy/FeeCalculatorERC1155.fuzzy.t.sol new file mode 100644 index 0000000..9359faf --- /dev/null +++ b/test/FeeCalculatorFuzzy/FeeCalculatorERC1155.fuzzy.t.sol @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import "./AbstractFeeCalculator.fuzzy.t.sol"; + +contract FeeCalculatorERC1155TestFuzzy is AbstractFeeCalculatorTestFuzzy { + using TestUtilities for uint256[]; + + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setERC1155Supply(address(token), 1, supply); + } + + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + 1, + amount + ); + } + + + function testCalculateDepositFeesFuzzy(uint256 depositAmount, uint256 current, uint256 total) public { + //vm.assume(depositAmount > 0); + //vm.assume(total > 0); + //vm.assume(current > 0); + vm.assume(total >= current); + vm.assume(depositAmount < 1e20 * 1e18); + vm.assume(depositAmount > 0); + vm.assume(total < 1e20 * 1e18); + + // Arrange + // Set up your test data + + // Set up mock pool + mockPool.setTotalSupply(total); + mockPool.setTCO2Supply(address(mockToken), current); + + // Act + try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 1, depositAmount) {} + catch Error(string memory reason) { + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" + ); + } + } + + function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption( + uint8 numberOfRedemptions, + uint128 _redemptionAmount, + uint128 _current, + uint128 _total + ) public override { + vm.assume(0 < numberOfRedemptions); + vm.assume(_total >= _current); + vm.assume(_redemptionAmount <= _current); + vm.assume(_redemptionAmount < 1e20 * 1e18); + vm.assume(_total < 1e20 * 1e18); + vm.assume(_redemptionAmount > 1e-6 * 1e18); + vm.assume(_current > 1e12); + + uint256 redemptionAmount = _redemptionAmount; + uint256 current = _current; + uint256 total = _total; + + SD59x18 dustAssetRedemptionRelativeFee = sd(0.3 * 1e18); + + // Arrange + // Set up your test data + + // Set up mock pool + mockPool.setTotalSupply(total); + setProjectSupply(address(mockToken), current); + uint256 oneTimeFee = 0; + bool oneTimeRedemptionFailed = false; + uint256 multipleTimesRedemptionFailedCount = 0; + + address[] memory tco2s = new address[](1); + tco2s[0] = address(mockToken); + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + uint256[] memory redemptionAmounts = new uint256[](1); + redemptionAmounts[0] = redemptionAmount; + + // Act + try feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, tokenIds, redemptionAmounts) returns ( + FeeDistribution memory feeDistribution + ) { + oneTimeFee = feeDistribution.shares.sumOf(); + } catch Error(string memory reason) { + oneTimeRedemptionFailed = true; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" + ); + } + + /// @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; + + for (uint256 i = 0; i < numberOfRedemptions; i++) { + uint256 redemption = equalRedemption + (i == 0 ? restRedemption : 0); + redemptionAmounts[0] = redemption; + try feeCalculator.calculateRedemptionFees(address(mockPool), tco2s, tokenIds, redemptionAmounts) returns ( + FeeDistribution memory feeDistribution + ) { + feeFromDividedRedemptions += feeDistribution.shares.sumOf(); + total -= redemption; + current -= redemption; + mockPool.setTotalSupply(total); + setProjectSupply(address(mockToken), current); + } catch Error(string memory reason) { + multipleTimesRedemptionFailedCount++; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" + ); + } + } + + // @dev we allow for 0.1% error + assertGe(1001 * feeFromDividedRedemptions / 1000, oneTimeFee); + } + + + function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + uint8 numberOfDeposits, + uint256 depositAmount, + uint256 current, + uint256 total + ) public override { + vm.assume(0 < numberOfDeposits); + vm.assume(total >= current); + + vm.assume(depositAmount < 1e20 * 1e18); + vm.assume(total < 1e20 * 1e18); + + vm.assume(depositAmount > 1e-6 * 1e18); + + // Arrange + // Set up your test data + bool oneTimeDepositFailed = false; + uint256 multipleTimesDepositFailedCount = 0; + // Set up mock pool + mockPool.setTotalSupply(total); + setProjectSupply(address(mockToken), current); + + uint256 oneTimeFee = 0; + + // Act + try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 1, depositAmount) returns ( + FeeDistribution memory feeDistribution + ) { + oneTimeFee = feeDistribution.shares.sumOf(); + } catch Error(string memory reason) { + oneTimeDepositFailed = true; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" + ); + } + + uint256 equalDeposit = depositAmount / numberOfDeposits; + uint256 restDeposit = depositAmount % numberOfDeposits; + uint256 feeFromDividedDeposits = 0; + + for (uint256 i = 0; i < numberOfDeposits; i++) { + uint256 deposit = equalDeposit + (i == 0 ? restDeposit : 0); + + try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 1, deposit) returns ( + FeeDistribution memory feeDistribution + ) { + feeFromDividedDeposits += feeDistribution.shares.sumOf(); + total += deposit; + current += deposit; + mockPool.setTotalSupply(total); + setProjectSupply(address(mockToken), current); + } catch Error(string memory reason) { + multipleTimesDepositFailedCount++; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" + ); + } + } + + // Assert + if (multipleTimesDepositFailedCount == 0 && !oneTimeDepositFailed) { + uint256 maximumAllowedErrorPercentage = (numberOfDeposits <= 1) ? 0 : 2; + if ( + oneTimeFee + feeFromDividedDeposits > 1e-8 * 1e18 // we skip assertion for extremely small fees (basically zero fees) because of numerical errors + ) { + assertGe((maximumAllowedErrorPercentage + 100) * feeFromDividedDeposits / 100, oneTimeFee); + } //we add 1% tolerance for numerical errors + } + } +} diff --git a/test/FeeCalculator.fuzzy.t.sol b/test/FeeCalculatorFuzzy/FeeCalculatorTCO2.fuzzy.t.sol similarity index 58% rename from test/FeeCalculator.fuzzy.t.sol rename to test/FeeCalculatorFuzzy/FeeCalculatorTCO2.fuzzy.t.sol index 17f0c29..669209a 100644 --- a/test/FeeCalculator.fuzzy.t.sol +++ b/test/FeeCalculatorFuzzy/FeeCalculatorTCO2.fuzzy.t.sol @@ -5,56 +5,29 @@ // If you encounter a vulnerability or an issue, please contact pragma solidity ^0.8.13; -import {Test, console2} from "forge-std/Test.sol"; -import {FeeCalculator} from "../src/FeeCalculator.sol"; -import {FeeDistribution} from "../src/interfaces/IFeeCalculator.sol"; -import {SD59x18, sd, intoUint256 as sdIntoUint256} from "@prb/math/src/SD59x18.sol"; -import {UD60x18, ud, intoUint256} from "@prb/math/src/UD60x18.sol"; -import "./TestUtilities.sol"; +import "./AbstractFeeCalculator.fuzzy.t.sol"; -contract FeeCalculatorTestFuzzy is Test { +contract FeeCalculatorTCO2TestFuzzy is AbstractFeeCalculatorTestFuzzy { using TestUtilities for uint256[]; - - FeeCalculator public feeCalculator; - MockPool public mockPool; - MockToken public mockToken; - address public feeRecipient = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; - - function setUp() public { - feeCalculator = new FeeCalculator(); - mockPool = new MockPool(); - mockToken = new MockToken(); - address[] memory recipients = new address[](1); - recipients[0] = feeRecipient; - uint256[] memory feeShares = new uint256[](1); - feeShares[0] = 100; - feeCalculator.feeSetup(recipients, feeShares); + + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setTCO2Supply(address(token), supply); } - function testCalculateDepositFees_FuzzyExtremelySmallDepositsToLargePool_ShouldThrowError(uint256 depositAmount) - public - { - vm.assume(depositAmount <= 1e-14 * 1e18); - vm.assume(depositAmount >= 10); - - //Note! This is a bug, where a very small deposit to a very large pool - //causes a == b because of precision limited by ratioDenominator in FeeCalculator - - // Arrange - // Set up your test data - - // Set up mock pool - mockPool.setTotalSupply(1e12 * 1e18); - mockPool.setProjectSupply(1, 1e9 * 1e18); - - vm.expectRevert("Fee must be greater than 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + amount + ); } function testCalculateDepositFeesFuzzy(uint256 depositAmount, uint256 current, uint256 total) public { - //vm.assume(depositAmount > 0); - //vm.assume(total > 0); - //vm.assume(current > 0); vm.assume(total >= current); vm.assume(depositAmount < 1e20 * 1e18); vm.assume(depositAmount > 0); @@ -65,36 +38,25 @@ contract FeeCalculatorTestFuzzy is Test { // Set up mock pool mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + mockPool.setTCO2Supply(address(mockToken), current); // Act try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount) {} catch Error(string memory reason) { assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" ); } } - function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoOneChunkFeesGreaterOrEqualToOneRedemption( - uint128 _redemptionAmount, - uint128 _current, - uint128 _total - ) public { - //just a sanity check - testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption( - 1, _redemptionAmount, _current, _total - ); - } - function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption( uint8 numberOfRedemptions, uint128 _redemptionAmount, uint128 _current, uint128 _total - ) public { + ) public override { vm.assume(0 < numberOfRedemptions); vm.assume(_total >= _current); vm.assume(_redemptionAmount <= _current); @@ -114,7 +76,7 @@ contract FeeCalculatorTestFuzzy is Test { // Set up mock pool mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + setProjectSupply(address(mockToken), current); uint256 oneTimeFee = 0; bool oneTimeRedemptionFailed = false; uint256 multipleTimesRedemptionFailedCount = 0; @@ -133,8 +95,8 @@ contract FeeCalculatorTestFuzzy is Test { oneTimeRedemptionFailed = true; assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" ); } @@ -158,13 +120,13 @@ contract FeeCalculatorTestFuzzy is Test { total -= redemption; current -= redemption; mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + setProjectSupply(address(mockToken), current); } catch Error(string memory reason) { multipleTimesRedemptionFailedCount++; assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" ); } } @@ -173,23 +135,13 @@ contract FeeCalculatorTestFuzzy is Test { assertGe(1001 * feeFromDividedRedemptions / 1000, oneTimeFee); } - function testCalculateDepositFeesFuzzy_DepositDividedIntoOneChunkFeesGreaterOrEqualToOneDeposit( - uint256 depositAmount, - uint256 current, - uint256 total - ) public { - //just a sanity check - testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( - 1, depositAmount, current, total - ); - } function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( uint8 numberOfDeposits, uint256 depositAmount, uint256 current, uint256 total - ) public { + ) public override { vm.assume(0 < numberOfDeposits); vm.assume(total >= current); @@ -204,7 +156,7 @@ contract FeeCalculatorTestFuzzy is Test { uint256 multipleTimesDepositFailedCount = 0; // Set up mock pool mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + setProjectSupply(address(mockToken), current); uint256 oneTimeFee = 0; @@ -217,8 +169,8 @@ contract FeeCalculatorTestFuzzy is Test { oneTimeDepositFailed = true; assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" ); } @@ -236,13 +188,13 @@ contract FeeCalculatorTestFuzzy is Test { total += deposit; current += deposit; mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + setProjectSupply(address(mockToken), current); } catch Error(string memory reason) { multipleTimesDepositFailedCount++; assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount'" ); } } @@ -257,50 +209,4 @@ contract FeeCalculatorTestFuzzy is Test { } //we add 1% tolerance for numerical errors } } - - function testFeeSetupFuzzy(address[] memory recipients, uint8 firstShare) public { - vm.assume(recipients.length <= 100); - vm.assume(recipients.length > 1); //at least two recipients - vm.assume(firstShare <= 100); - vm.assume(firstShare > 0); - - uint256[] memory feeShares = new uint256[](recipients.length); - - uint256 shareLeft = 100 - firstShare; - feeShares[0] = firstShare; - uint256 equalShare = shareLeft / (recipients.length - 1); - uint256 leftShare = shareLeft % (recipients.length - 1); - - for (uint256 i = 1; i < recipients.length; i++) { - feeShares[i] = equalShare; - } - feeShares[recipients.length - 1] += leftShare; //last one gets additional share - feeCalculator.feeSetup(recipients, feeShares); - - uint256 depositAmount = 100 * 1e18; - // Set up mock pool - mockPool.setTotalSupply(200 * 1e18); - mockPool.setProjectSupply(1, 100 * 1e18); - - // Act - FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); - address[] memory gotRecipients = feeDistribution.recipients; - uint256[] memory fees = feeDistribution.shares; - - // Assert - assertEq(gotRecipients.length, recipients.length); - for (uint256 i = 0; i < recipients.length; i++) { - assertEq(gotRecipients[i], recipients[i]); - } - - assertEq(fees.sumOf(), 11526003792614720250); - - assertApproxEqAbs(fees[0], 11526003792614720250 * uint256(firstShare) / 100, recipients.length - 1 + 1); //first fee might get the rest from division - - for (uint256 i = 1; i < recipients.length - 1; i++) { - assertApproxEqAbs(fees[i], 11526003792614720250 * equalShare / 100, 1); - } - assertApproxEqAbs(fees[recipients.length - 1], 11526003792614720250 * (equalShare + leftShare) / 100, 1); - } } diff --git a/test/FeeCalculatorLaunchParams.t.sol b/test/FeeCalculatorLaunchParams/AbstractFeeCalculatorLaunchParams.t.sol similarity index 83% rename from test/FeeCalculatorLaunchParams.t.sol rename to test/FeeCalculatorLaunchParams/AbstractFeeCalculatorLaunchParams.t.sol index d1f19f1..4e6f032 100644 --- a/test/FeeCalculatorLaunchParams.t.sol +++ b/test/FeeCalculatorLaunchParams/AbstractFeeCalculatorLaunchParams.t.sol @@ -6,11 +6,11 @@ pragma solidity ^0.8.13; import {Test, console2} from "forge-std/Test.sol"; -import {FeeCalculator} from "../src/FeeCalculator.sol"; -import {FeeDistribution} from "../src/interfaces/IFeeCalculator.sol"; -import "./TestUtilities.sol"; +import {FeeCalculator} from "../../src/FeeCalculator.sol"; +import {FeeDistribution} from "../../src/interfaces/IFeeCalculator.sol"; +import "../TestUtilities.sol"; -contract FeeCalculatorLaunchParamsTest is Test { +abstract contract AbstractFeeCalculatorLaunchParamsTest is Test { using TestUtilities for uint256[]; FeeCalculator public feeCalculator; @@ -32,6 +32,9 @@ contract FeeCalculatorLaunchParamsTest is Test { feeCalculator.setDepositFeeRatioScale(1.25 * 1e18); } + function setProjectSupply(address token, uint256 supply) internal virtual; + function calculateDepositFees(address pool, address token, uint256 amount) internal view virtual returns (FeeDistribution memory); + function testCalculateDepositFeesNormalCase() public { // Arrange // Set up your test data @@ -39,11 +42,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -62,7 +65,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); address[] memory _recipients = new address[](2); _recipients[0] = feeRecipient1; @@ -74,7 +77,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -95,7 +98,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); address[] memory _recipients = new address[](2); _recipients[0] = feeRecipient1; @@ -107,7 +110,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -126,11 +129,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(53461 * 1e18); - mockPool.setProjectSupply(1, 15462 * 1e18); + setProjectSupply(address(mockToken), 15462 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -147,11 +150,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act vm.expectRevert("Fee must be greater than 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_DepositOfHundredWei_ShouldThrowError() public { @@ -164,11 +167,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act vm.expectRevert("Fee must be greater than 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_DepositOfHundredThousandsPartOfOne_NonzeroFee() public { @@ -178,11 +181,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -199,11 +202,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -225,7 +228,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); address[] memory _recipients = new address[](5); _recipients[0] = feeRecipient1; @@ -243,7 +246,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -274,7 +277,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1e5 * 1e18); - mockPool.setProjectSupply(1, 1e4 * 1e18); + setProjectSupply(address(mockToken), 1e4 * 1e18); address[] memory _recipients = new address[](5); _recipients[0] = feeRecipient1; @@ -292,7 +295,7 @@ contract FeeCalculatorLaunchParamsTest is Test { // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -318,11 +321,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(100 * 1e6 * 1e18); - mockPool.setProjectSupply(1, 1e6 * 1e18); + setProjectSupply(address(mockToken), 1e6 * 1e18); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -339,11 +342,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 500 * 1e18); + setProjectSupply(address(mockToken), 500 * 1e18); // Act vm.expectRevert("depositAmount must be > 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_CurrentGreaterThanTotal_ExceptionShouldBeThrown() public { @@ -353,13 +356,13 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 1500 * 1e18); + setProjectSupply(address(mockToken), 1500 * 1e18); // Act vm.expectRevert( "The total volume in the pool must be greater than or equal to the volume for an individual asset" ); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_EmptyPool_FeeCappedAt10Percent() public { @@ -369,11 +372,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(0); - mockPool.setProjectSupply(1, 0); + setProjectSupply(address(mockToken), 0); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -389,11 +392,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1); - mockPool.setProjectSupply(1, 0); + setProjectSupply(address(mockToken), 0); // Act vm.expectRevert("Deposit outside range"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_TotalEqualCurrent_FeeCappedAt10Percent() public { @@ -403,11 +406,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000); - mockPool.setProjectSupply(1, 1000); + setProjectSupply(address(mockToken), 1000); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; @@ -423,11 +426,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000); - mockPool.setProjectSupply(1, 999); + setProjectSupply(address(mockToken), 999); // Act vm.expectRevert("Deposit outside range"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); } function testCalculateDepositFees_ZeroCurrent_NormalFees() public { @@ -437,11 +440,11 @@ contract FeeCalculatorLaunchParamsTest is Test { // Set up mock pool mockPool.setTotalSupply(1000 * 1e18); - mockPool.setProjectSupply(1, 0); + setProjectSupply(address(mockToken), 0); // Act FeeDistribution memory feeDistribution = - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); address[] memory recipients = feeDistribution.recipients; uint256[] memory fees = feeDistribution.shares; diff --git a/test/FeeCalculatorLaunchParams/FeeCalculatorLaunchParamsERC1155.t.sol b/test/FeeCalculatorLaunchParams/FeeCalculatorLaunchParamsERC1155.t.sol new file mode 100644 index 0000000..0d3bb9a --- /dev/null +++ b/test/FeeCalculatorLaunchParams/FeeCalculatorLaunchParamsERC1155.t.sol @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import "./AbstractFeeCalculatorLaunchParams.t.sol"; + +contract FeeCalculatorLaunchParamsERC1155Test is + AbstractFeeCalculatorLaunchParamsTest +{ + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setERC1155Supply(address(token), 1, supply); + } + + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + 1, + amount + ); + } +} diff --git a/test/FeeCalculatorLaunchParams/FeeCalculatorLaunchParamsTCO2.t.sol b/test/FeeCalculatorLaunchParams/FeeCalculatorLaunchParamsTCO2.t.sol new file mode 100644 index 0000000..e854bb8 --- /dev/null +++ b/test/FeeCalculatorLaunchParams/FeeCalculatorLaunchParamsTCO2.t.sol @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import "./AbstractFeeCalculatorLaunchParams.t.sol"; + +contract FeeCalculatorLaunchParamsTCO2Test is + AbstractFeeCalculatorLaunchParamsTest +{ + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setTCO2Supply(address(token), supply); + } + + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + amount + ); + } +} diff --git a/test/FeeCalculatorLaunchParamsFuzzy/AbstractFeeCalculatorLaunchParams.fuzzy.t.sol b/test/FeeCalculatorLaunchParamsFuzzy/AbstractFeeCalculatorLaunchParams.fuzzy.t.sol new file mode 100644 index 0000000..ba8104d --- /dev/null +++ b/test/FeeCalculatorLaunchParamsFuzzy/AbstractFeeCalculatorLaunchParams.fuzzy.t.sol @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {FeeCalculator} from "../../src/FeeCalculator.sol"; +import {FeeDistribution} from "../../src/interfaces/IFeeCalculator.sol"; +import "../TestUtilities.sol"; +import "forge-std/console.sol"; + +abstract contract AbstractFeeCalculatorLaunchParamsTestFuzzy is Test { + using TestUtilities for uint256[]; + + FeeCalculator public feeCalculator; + MockPool public mockPool; + MockToken public mockToken; + address public feeRecipient = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; + + function setUp() public { + feeCalculator = new FeeCalculator(); + mockPool = new MockPool(); + mockToken = new MockToken(); + address[] memory recipients = new address[](1); + recipients[0] = feeRecipient; + uint256[] memory feeShares = new uint256[](1); + feeShares[0] = 100; + feeCalculator.feeSetup(recipients, feeShares); + // set up fee calculator launch params + feeCalculator.setDepositFeeScale(0.15 * 1e18); + feeCalculator.setDepositFeeRatioScale(1.25 * 1e18); + } + + function setProjectSupply(address token, uint256 supply) internal virtual; + function calculateDepositFees(address pool, address token, uint256 amount) internal view virtual returns (FeeDistribution memory); + function testCalculateDepositFeesFuzzy(uint256 depositAmount, uint256 current, uint256 total) public virtual; + function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + uint8 numberOfDeposits, + uint256 depositAmount, + uint256 current, + uint256 total + ) public virtual; + + function testCalculateDepositFees_FuzzyExtremelySmallDepositsToLargePool_ShouldThrowError(uint256 depositAmount) + public + { + vm.assume(depositAmount <= 1e-14 * 1e18); + vm.assume(depositAmount >= 10); + + //Note! This is a bug, where a very small deposit to a very large pool + //causes a == b because of precision limited by ratioDenominator in FeeCalculator + + // Arrange + // Set up your test data + + // Set up mock pool + mockPool.setTotalSupply(1e12 * 1e18); + setProjectSupply(address(mockToken), 1e9 * 1e18); + + vm.expectRevert("Fee must be greater than 0"); + calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + } + + + function testCalculateDepositFeesFuzzy_DepositDividedIntoOneChunkFeesGreaterOrEqualToOneDeposit( + uint256 depositAmount, + uint256 current, + uint256 total + ) public { + //just a sanity check + testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + 1, depositAmount, current, total + ); + } +} diff --git a/test/FeeCalculatorLaunchParamsFuzzy/FeeCalculatorLaunchParamsERC1155.fuzzy.t.sol b/test/FeeCalculatorLaunchParamsFuzzy/FeeCalculatorLaunchParamsERC1155.fuzzy.t.sol new file mode 100644 index 0000000..9193dc5 --- /dev/null +++ b/test/FeeCalculatorLaunchParamsFuzzy/FeeCalculatorLaunchParamsERC1155.fuzzy.t.sol @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2024 Toucan Protocol +// +// SPDX-License-Identifier: UNLICENSED + +// If you encounter a vulnerability or an issue, please contact +pragma solidity ^0.8.13; + +import "./AbstractFeeCalculatorLaunchParams.fuzzy.t.sol"; + +contract FeeCalculatorLaunchParamsERC1155TestFuzzy is AbstractFeeCalculatorLaunchParamsTestFuzzy { + using TestUtilities for uint256[]; + + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setERC1155Supply(address(token), 1, supply); + } + + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + 1, + amount + ); + } + + function testCalculateDepositFeesFuzzy(uint256 depositAmount, uint256 current, uint256 total) public override { + vm.assume(total >= current); + vm.assume(depositAmount < 1e20 * 1e18); + vm.assume(depositAmount > 0); + vm.assume(total < 1e20 * 1e18); + + // Arrange + // Set up your test data + + // Set up mock pool + mockPool.setTotalSupply(total); + setProjectSupply(address(mockToken), current); + + // Act + try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 1, depositAmount) {} + catch Error(string memory reason) { + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)) + || keccak256(bytes("Deposit outside range")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount' or 'Deposit outside range'" + ); + } + } + + function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + uint8 numberOfDeposits, + uint256 depositAmount, + uint256 current, + uint256 total + ) public override { + vm.assume(0 < numberOfDeposits); + vm.assume(total >= current); + + vm.assume(depositAmount < 1e20 * 1e18); + vm.assume(total < 1e20 * 1e18); + + vm.assume(depositAmount > 1e-6 * 1e18); + + // Arrange + // Set up your test data + bool oneTimeDepositFailed = false; + uint256 multipleTimesDepositFailedCount = 0; + // Set up mock pool + mockPool.setTotalSupply(total); + setProjectSupply(address(mockToken), current); + + uint256 oneTimeFee = 0; + + // Act + try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 1, depositAmount) returns ( + FeeDistribution memory feeDistribution + ) { + oneTimeFee = feeDistribution.shares.sumOf(); + } catch Error(string memory reason) { + oneTimeDepositFailed = true; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)) + || keccak256(bytes("Deposit outside range")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount' or 'Deposit outside range'" + ); + } + + uint256 equalDeposit = depositAmount / numberOfDeposits; + uint256 restDeposit = depositAmount % numberOfDeposits; + uint256 feeFromDividedDeposits = 0; + + for (uint256 i = 0; i < numberOfDeposits; i++) { + uint256 deposit = equalDeposit + (i == 0 ? restDeposit : 0); + + try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), 1, deposit) returns ( + FeeDistribution memory feeDistribution + ) { + feeFromDividedDeposits += feeDistribution.shares.sumOf(); + total += deposit; + current += deposit; + mockPool.setTotalSupply(total); + setProjectSupply(address(mockToken), current); + } catch Error(string memory reason) { + multipleTimesDepositFailedCount++; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)) + || keccak256(bytes("Deposit outside range")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount' or 'Deposit outside range'" + ); + } + } + + // Assert + if (multipleTimesDepositFailedCount == 0 && !oneTimeDepositFailed) { + uint256 maximumAllowedErrorPercentage = (numberOfDeposits <= 1) ? 0 : 2; + if ( + oneTimeFee + feeFromDividedDeposits > 1e-8 * 1e18 // we skip assertion for extremely small fees (basically zero fees) because of numerical errors + ) { + assertGe((maximumAllowedErrorPercentage + 100) * feeFromDividedDeposits / 100, oneTimeFee); + } //we add 1% tolerance for numerical errors + } + } +} diff --git a/test/FeeCalculatorLaunchParams.fuzzy.t.sol b/test/FeeCalculatorLaunchParamsFuzzy/FeeCalculatorLaunchParamsTCO2.fuzzy.t.sol similarity index 60% rename from test/FeeCalculatorLaunchParams.fuzzy.t.sol rename to test/FeeCalculatorLaunchParamsFuzzy/FeeCalculatorLaunchParamsTCO2.fuzzy.t.sol index 8705be6..d6c2db4 100644 --- a/test/FeeCalculatorLaunchParams.fuzzy.t.sol +++ b/test/FeeCalculatorLaunchParamsFuzzy/FeeCalculatorLaunchParamsTCO2.fuzzy.t.sol @@ -5,57 +5,30 @@ // If you encounter a vulnerability or an issue, please contact pragma solidity ^0.8.13; -import {Test, console2} from "forge-std/Test.sol"; -import {FeeCalculator} from "../src/FeeCalculator.sol"; -import {FeeDistribution} from "../src/interfaces/IFeeCalculator.sol"; -import "./TestUtilities.sol"; +import "./AbstractFeeCalculatorLaunchParams.fuzzy.t.sol"; -contract FeeCalculatorLaunchParamsTestFuzzy is Test { +contract FeeCalculatorLaunchParamsTCO2TestFuzzy is AbstractFeeCalculatorLaunchParamsTestFuzzy { using TestUtilities for uint256[]; - FeeCalculator public feeCalculator; - MockPool public mockPool; - MockToken public mockToken; - address public feeRecipient = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; - - function setUp() public { - feeCalculator = new FeeCalculator(); - mockPool = new MockPool(); - mockToken = new MockToken(); - address[] memory recipients = new address[](1); - recipients[0] = feeRecipient; - uint256[] memory feeShares = new uint256[](1); - feeShares[0] = 100; - feeCalculator.feeSetup(recipients, feeShares); - // set up fee calculator launch params - feeCalculator.setDepositFeeScale(0.15 * 1e18); - feeCalculator.setDepositFeeRatioScale(1.25 * 1e18); + function setProjectSupply(address token, uint256 supply) internal override { + mockPool.setTCO2Supply(address(token), supply); } - function testCalculateDepositFees_FuzzyExtremelySmallDepositsToLargePool_ShouldThrowError(uint256 depositAmount) - public - { - vm.assume(depositAmount <= 1e-14 * 1e18); - vm.assume(depositAmount >= 10); - - //Note! This is a bug, where a very small deposit to a very large pool - //causes a == b because of precision limited by ratioDenominator in FeeCalculator - - // Arrange - // Set up your test data - - // Set up mock pool - mockPool.setTotalSupply(1e12 * 1e18); - mockPool.setProjectSupply(1, 1e9 * 1e18); - - vm.expectRevert("Fee must be greater than 0"); - feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount); + function calculateDepositFees( + address pool, + address token, + uint256 amount + ) internal view override returns (FeeDistribution memory) { + return + feeCalculator.calculateDepositFees( + address(pool), + address(token), + amount + ); } - function testCalculateDepositFeesFuzzy(uint256 depositAmount, uint256 current, uint256 total) public { - //vm.assume(depositAmount > 0); - //vm.assume(total > 0); - //vm.assume(current > 0); + + function testCalculateDepositFeesFuzzy(uint256 depositAmount, uint256 current, uint256 total) public override { vm.assume(total >= current); vm.assume(depositAmount < 1e20 * 1e18); vm.assume(depositAmount > 0); @@ -66,37 +39,26 @@ contract FeeCalculatorLaunchParamsTestFuzzy is Test { // Set up mock pool mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + setProjectSupply(address(mockToken), current); // Act try feeCalculator.calculateDepositFees(address(mockPool), address(mockToken), depositAmount) {} catch Error(string memory reason) { assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)) || keccak256(bytes("Deposit outside range")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount' or 'Deposit outside range'" + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount' or 'Deposit outside range'" ); } } - function testCalculateDepositFeesFuzzy_DepositDividedIntoOneChunkFeesGreaterOrEqualToOneDeposit( - uint256 depositAmount, - uint256 current, - uint256 total - ) public { - //just a sanity check - testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( - 1, depositAmount, current, total - ); - } - function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( uint8 numberOfDeposits, uint256 depositAmount, uint256 current, uint256 total - ) public { + ) public override { vm.assume(0 < numberOfDeposits); vm.assume(total >= current); @@ -111,7 +73,7 @@ contract FeeCalculatorLaunchParamsTestFuzzy is Test { uint256 multipleTimesDepositFailedCount = 0; // Set up mock pool mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + setProjectSupply(address(mockToken), current); uint256 oneTimeFee = 0; @@ -124,9 +86,9 @@ contract FeeCalculatorLaunchParamsTestFuzzy is Test { oneTimeDepositFailed = true; assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)) || keccak256(bytes("Deposit outside range")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount' or 'Deposit outside range'" + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount' or 'Deposit outside range'" ); } @@ -144,14 +106,14 @@ contract FeeCalculatorLaunchParamsTestFuzzy is Test { total += deposit; current += deposit; mockPool.setTotalSupply(total); - mockPool.setProjectSupply(1, current); + setProjectSupply(address(mockToken), current); } catch Error(string memory reason) { multipleTimesDepositFailedCount++; assertTrue( keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) - || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to requested amount")) == keccak256(bytes(reason)) || keccak256(bytes("Deposit outside range")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount' or 'Deposit outside range'" + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to requested amount' or 'Deposit outside range'" ); } } diff --git a/test/TestUtilities.sol b/test/TestUtilities.sol index 0f402fa..dffba88 100644 --- a/test/TestUtilities.sol +++ b/test/TestUtilities.sol @@ -21,7 +21,9 @@ library TestUtilities { contract MockPool is IERC20 { uint256 private _totalSupply; - mapping(uint256 => uint256) private _totalPerProjectSupply; + mapping(address => uint256) private _totalPerTCO2Supply; + mapping(address => mapping(uint256 => uint256)) + private _totalPerERC1155TokenSupply; function totalSupply() external view returns (uint256) { return _totalSupply; @@ -31,19 +33,35 @@ contract MockPool is IERC20 { return _totalSupply; } - function totalPerProjectTCO2Supply(uint256 projectTokenId) external view returns (uint256) { - return _totalPerProjectSupply[projectTokenId]; + function totalPerProjectSupply( + address tco2 + ) external view returns (uint256) { + return _totalPerTCO2Supply[tco2]; + } + + function totalPerProjectSupply( + address erc1155, + uint256 tokenId + ) external view returns (uint256) { + return _totalPerERC1155TokenSupply[erc1155][tokenId]; } function setTotalSupply(uint256 ts) public { _totalSupply = ts; } - function setProjectSupply(uint256 projectTokenId, uint256 ts) public { - _totalPerProjectSupply[projectTokenId] = ts; + function setTCO2Supply(address tco2, uint256 ts) public { + _totalPerTCO2Supply[tco2] = ts; + } + + function setERC1155Supply(address erc1155, uint256 tokenId, uint256 ts) public { + _totalPerERC1155TokenSupply[erc1155][tokenId] = ts; } - function allowance(address, address) external pure override returns (uint256) { + function allowance( + address, + address + ) external pure override returns (uint256) { return 0; } @@ -55,7 +73,11 @@ contract MockPool is IERC20 { return true; } - function transferFrom(address, address, uint256) external pure override returns (bool) { + function transferFrom( + address, + address, + uint256 + ) external pure override returns (bool) { return true; } @@ -67,7 +89,10 @@ contract MockPool is IERC20 { contract MockToken is IERC20 { mapping(address => uint256) public override balanceOf; - function allowance(address, address) external pure override returns (uint256) { + function allowance( + address, + address + ) external pure override returns (uint256) { return 0; } @@ -79,7 +104,11 @@ contract MockToken is IERC20 { return true; } - function transferFrom(address, address, uint256) external pure override returns (bool) { + function transferFrom( + address, + address, + uint256 + ) external pure override returns (bool) { return true; } @@ -88,19 +117,20 @@ contract MockToken is IERC20 { } function getVintageData() external pure returns (VintageData memory) { - return VintageData({ - name: "test", - startTime: 0, - endTime: 0, - projectTokenId: 1, - totalVintageQuantity: 0, - isCorsiaCompliant: false, - isCCPcompliant: false, - coBenefits: "", - correspAdjustment: "", - additionalCertification: "", - uri: "", - registry: "" - }); + return + VintageData({ + name: "test", + startTime: 0, + endTime: 0, + projectTokenId: 1, + totalVintageQuantity: 0, + isCorsiaCompliant: false, + isCCPcompliant: false, + coBenefits: "", + correspAdjustment: "", + additionalCertification: "", + uri: "", + registry: "" + }); } }