Skip to content

Commit

Permalink
feat: FlatFeeCalculator
Browse files Browse the repository at this point in the history
  • Loading branch information
kosecki123 committed Apr 23, 2024
1 parent 4609afc commit c2b3738
Show file tree
Hide file tree
Showing 2 changed files with 370 additions and 0 deletions.
189 changes: 189 additions & 0 deletions src/FlatFeeCalculator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: 2024 Toucan Protocol
//
// SPDX-License-Identifier: UNLICENSED

// If you encounter a vulnerability or an issue, please contact <info@neutralx.com>
pragma solidity 0.8.19;

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

import {IFeeCalculator, FeeDistribution} from "./interfaces/IFeeCalculator.sol";
import "./interfaces/IPool.sol";

/// @title FlatFeeCalculator
/// @author Neutral Labs Inc. & Toucan Protocol
/// @notice This contract calculates deposit and redemption fees for a given pool.
/// @dev It implements the IFeeCalculator interface.
contract FlatFeeCalculator is IFeeCalculator, Ownable {
/// @dev Version-related parameters. VERSION keeps track of production
/// releases. VERSION_RELEASE_CANDIDATE keeps track of iterations
/// of a VERSION in our staging environment.
string public constant VERSION = "1.0.0";
uint256 public constant VERSION_RELEASE_CANDIDATE = 1;

uint256 public feeBasisPoints = 300;

address[] private _recipients;
uint256[] private _shares;

event FeeBasisPointsUpdated(uint256 feeBasisPoints);
event FeeSetup(address[] recipients, uint256[] shares);

constructor() Ownable() {}

/// @notice Sets the fee basis points.
/// @dev Can only be called by the current owner.
/// @param _feeBasisPoints The new fee basis points.
function setFeeBasisPoints(uint256 _feeBasisPoints) external onlyOwner {
require(_feeBasisPoints < 10000, "Fee basis points should be less than 10000");

feeBasisPoints = _feeBasisPoints;
emit FeeBasisPointsUpdated(_feeBasisPoints);
}

/// @notice Sets up the fee distribution among recipients.
/// @dev Can only be called by the current owner.
/// @param recipients The addresses of the fee recipients.
/// @param shares The share of the fee each recipient should receive.
function feeSetup(address[] memory recipients, uint256[] memory shares) external onlyOwner {
require(recipients.length == shares.length, "Recipients and shares arrays must have the same length");

uint256 totalShares = 0;
for (uint256 i = 0; i < shares.length; i++) {
totalShares += shares[i];
}
require(totalShares == 100, "Total shares must equal 100");

_recipients = recipients;
_shares = shares;
emit FeeSetup(recipients, shares);
}

/// @notice Calculates the deposit fee for a given amount.
/// @param pool The address of the pool.
/// @param tco2 The address of the TCO2 token.
/// @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
override
returns (FeeDistribution memory feeDistribution)
{
require(depositAmount > 0, "depositAmount must be > 0");

feeDistribution = _calculateFee(
depositAmount
);
}

/// @notice Calculates the fee shares and recipients based on the total fee.
/// @param totalFee The total fee to be distributed.
/// @return feeDistribution The recipients and the amount of fees each
/// recipient should receive.
function calculateFeeShares(uint256 totalFee) internal view returns (FeeDistribution memory feeDistribution) {
uint256 recipientsLength = _recipients.length;
uint256[] memory shares = new uint256[](recipientsLength);

uint256 restFee = totalFee;
for (uint256 i = 0; i < recipientsLength; i++) {
shares[i] = (totalFee * _shares[i]) / 100;
restFee -= shares[i];
}

// If any fee is left, it is distributed to the first recipient.
// This may happen if any of the shares of the fee to be distributed
// has leftover from the division by 100 above.
shares[0] += restFee;

feeDistribution.recipients = _recipients;
feeDistribution.shares = shares;
}

/// @notice Calculates the redemption fees for a given amount.
/// @param pool The address of the pool.
/// @param tco2s The addresses of the TCO2 token.
/// @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
override
returns (FeeDistribution memory feeDistribution)
{
require(tco2s.length == redemptionAmounts.length, "length mismatch");
require(tco2s.length == 1, "only one");

feeDistribution = _calculateFee(
redemptionAmounts[0]
);
}

/// @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");

feeDistribution = _calculateFee(
depositAmount
);
}

/// @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");

feeDistribution = _calculateFee(
redemptionAmounts[0]
);
}

/// @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) {
recipients = _recipients;
shares = _shares;
}

/// @notice Calculates the fee for a given amount.
/// @param requestedAmount The amount to be used for the fee calculation.
/// @return feeDistribution How the fee is meant to be
function _calculateFee(
uint256 requestedAmount
) internal view returns (FeeDistribution memory) {
require(requestedAmount > 0, "requested amount must be > 0");

uint256 feeAmount = requestedAmount * feeBasisPoints / 10000;

require(feeAmount <= requestedAmount, "Fee must be lower or equal to requested amount");
require(feeAmount > 0, "Fee must be greater than 0");

return calculateFeeShares(feeAmount);
}
}
181 changes: 181 additions & 0 deletions test/FlatFeeCalculator/FlatFeeCalculator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: 2024 Toucan Protocol
//
// SPDX-License-Identifier: UNLICENSED

// If you encounter a vulnerability or an issue, please contact <info@neutralx.com>
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 {FlatFeeCalculator} from "../../src/FlatFeeCalculator.sol";

contract FlatFeeCalculatorTest is Test {
FlatFeeCalculator public feeCalculator;
address public feeRecipient = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B;
address public empty = address(0);

function setUp() public {
feeCalculator = new FlatFeeCalculator();
address[] memory recipients = new address[](1);
recipients[0] = feeRecipient;
uint256[] memory feeShares = new uint256[](1);
feeShares[0] = 100;
feeCalculator.feeSetup(recipients, feeShares);
}

function testFeeSetupEmpty() public {
address[] memory recipients = new address[](0);
uint256[] memory feeShares = new uint256[](0);
vm.expectRevert("Total shares must equal 100");
feeCalculator.feeSetup(recipients, feeShares);
}

function testGetFeeSetup() public {
address feeRecipient1 = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B;
address feeRecipient2 = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;

address[] memory recipients = new address[](2);
recipients[0] = feeRecipient1;
recipients[1] = feeRecipient2;
uint256[] memory feeShares = new uint256[](2);
feeShares[0] = 30;
feeShares[1] = 70;

feeCalculator.feeSetup(recipients, feeShares);

(address[] memory _recipients, uint256[] memory _feeShares) = feeCalculator.getFeeSetup();
assertEq(_recipients[0], feeRecipient1);
assertEq(_recipients[1], feeRecipient2);
assertEq(_feeShares[0], 30);
assertEq(_feeShares[1], 70);
}

function testSetFeeBasisPoints(uint256 basisPoints) public {
vm.assume(basisPoints > 0);
vm.assume(basisPoints < 10000);

feeCalculator.setFeeBasisPoints(basisPoints);

assertEq(feeCalculator.feeBasisPoints(), basisPoints);
}

function testSetFeeBasisPoints_OutOfRange_Reverts(uint256 basisPoints) public {
vm.assume(basisPoints >= 10000);

vm.expectRevert("Fee basis points should be less than 10000");
feeCalculator.setFeeBasisPoints(basisPoints);
}

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

// Act
FeeDistribution memory feeDistribution =
feeCalculator.calculateDepositFees(empty, empty, depositAmount);
address[] memory recipients = feeDistribution.recipients;
uint256[] memory fees = feeDistribution.shares;

// Assert
assertEq(feeDistribution.recipients.length, feeDistribution.shares.length, "array length mismatch");
assertEq(recipients[0], feeRecipient);
assertEq(fees[0], 3000000000000000000);
}

function testCalculateRedemptionFeesNormalCase() public {
// Arrange
// Set up your test data
uint256 redemptionAmount = 100 * 1e18;
address[] memory tco2s = new address[](1);
tco2s[0] = empty;
uint256[] memory redemptionAmounts = new uint256[](1);
redemptionAmounts[0] = redemptionAmount;

// Act
FeeDistribution memory feeDistribution = feeCalculator.calculateRedemptionFees(empty, tco2s, redemptionAmounts);
address[] memory recipients = feeDistribution.recipients;
uint256[] memory fees = feeDistribution.shares;

// Assert
assertEq(feeDistribution.recipients.length, feeDistribution.shares.length, "array length mismatch");
assertEq(recipients[0], feeRecipient);
assertEq(fees[0], 3000000000000000000);
}

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

// Act
vm.expectRevert("Fee must be greater than 0");
FeeDistribution memory feeDistribution =
feeCalculator.calculateDepositFees(empty, empty, depositAmount);
}

function testCalculateDepositFee_TCO2(uint256 depositAmount) public {
// Arrange
vm.assume(depositAmount > 100);
vm.assume(depositAmount < 1e18*1e18);
// Act
FeeDistribution memory feeDistribution =
feeCalculator.calculateDepositFees(empty, empty, depositAmount);

uint256 expected = depositAmount * feeCalculator.feeBasisPoints() / 10000;

assertEq(feeDistribution.shares[0], expected);
}

function testCalculateDepositFee_ERC1155(uint256 depositAmount) public {
// Arrange
vm.assume(depositAmount > 100);
vm.assume(depositAmount < 1e18*1e18);
// Act
FeeDistribution memory feeDistribution =
feeCalculator.calculateDepositFees(empty, empty, 0, depositAmount);

uint256 expected = depositAmount * feeCalculator.feeBasisPoints() / 10000;

assertEq(feeDistribution.shares[0], expected);
}

function testCalculateRedemptionAmount_TCO2(uint256 redemptionAmount) public {
// Arrange
vm.assume(redemptionAmount > 100);
vm.assume(redemptionAmount < 1e18*1e18);
// Act
address[] memory tco2s = new address[](1);
tco2s[0] = empty;
uint256[] memory redemptionAmounts = new uint256[](1);
redemptionAmounts[0] = redemptionAmount;

FeeDistribution memory feeDistribution =
feeCalculator.calculateRedemptionFees(empty, tco2s, redemptionAmounts);

uint256 expected = redemptionAmount * feeCalculator.feeBasisPoints() / 10000;

assertEq(feeDistribution.shares[0], expected);
}

function testCalculateRedemptionAmount_ERC1155(uint256 redemptionAmount) public {
// Arrange
vm.assume(redemptionAmount > 100);
vm.assume(redemptionAmount < 1e18*1e18);
// Act
address[] memory erc1155s = new address[](1);
erc1155s[0] = empty;
uint256[] memory tokenIds = new uint256[](1);
tokenIds[0] = 1;
uint256[] memory redemptionAmounts = new uint256[](1);
redemptionAmounts[0] = redemptionAmount;

FeeDistribution memory feeDistribution =
feeCalculator.calculateRedemptionFees(empty, erc1155s, tokenIds, redemptionAmounts);

uint256 expected = redemptionAmount * feeCalculator.feeBasisPoints() / 10000;

assertEq(feeDistribution.shares[0], expected);
}
}

0 comments on commit c2b3738

Please sign in to comment.