Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protocol Fee Table #964

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions contracts/domain/BosonErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface BosonErrors {
error InvalidState();
// Two or more array parameters with different lengths
error ArrayLengthMismatch();
// Array elements that are not in ascending order (i.e arr[i-1] > arr[i])
error NonAscendingOrder();

// Reentrancy guard
// Reentrancy guard is active and second call to protocol is made
Expand Down
6 changes: 6 additions & 0 deletions contracts/interfaces/events/IBosonConfigEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ interface IBosonConfigEvents {
event MinDisputePeriodChanged(uint256 minDisputePeriod, address indexed executedBy);
event MaxPremintedVouchersChanged(uint256 maxPremintedVouchers, address indexed executedBy);
event AccessControllerAddressChanged(address indexed accessControllerAddress, address indexed executedBy);
event FeeTableUpdated(
address indexed token,
uint256[] priceRanges,
uint256[] feePercentages,
address indexed executedBy
);
}
41 changes: 38 additions & 3 deletions contracts/interfaces/handlers/IBosonConfigHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IBosonConfigEvents } from "../events/IBosonConfigEvents.sol";
*
* @notice Handles management of configuration within the protocol.
*
* The ERC-165 identifier for this interface is: 0xe27f0773
* The ERC-165 identifier for this interface is: 0xc040bf51
*/
interface IBosonConfigHandler is IBosonConfigEvents, BosonErrors {
/**
Expand Down Expand Up @@ -130,12 +130,47 @@ interface IBosonConfigHandler is IBosonConfigEvents, BosonErrors {
function setProtocolFeePercentage(uint256 _protocolFeePercentage) external;

/**
* @notice Gets the protocol fee percentage.
* @notice Sets the feeTable for a specific token given price ranges and fee tiers for
* the corresponding price ranges.
*
* @return the protocol fee percentage
* Reverts if the number of fee percentages does not match the number of price ranges.
* Reverts if token is Zero address.
*
* @dev Caller must have ADMIN role.
*
* @param _tokenAddress - the address of the token
* @param _priceRanges - array of token price ranges
* @param _feePercentages - array of fee percentages corresponding to each price range
*/
function setProtocolFeeTable(
address _tokenAddress,
uint256[] calldata _priceRanges,
uint256[] calldata _feePercentages
) external;

/**
* @notice Gets the default protocol fee percentage.
*
* @return the default protocol fee percentage
*/
function getProtocolFeePercentage() external view returns (uint256);

/**
* @notice Retrieves the protocol fee percentage for a given exchange token and price.
*
* @dev This function calculates the protocol fee based on the token and price.
* If the token has a custom fee table, it applies the corresponding fee percentage
* for the price range. If the token does not have a custom fee table, it falls back
* to the default protocol fee percentage. If the exchange token is $BOSON,
* this function returns the flatBoson fee
*
* @param _exchangeToken - The address of the token being used for the exchange.
* @param _price - The price of the item or service in the exchange.
*
* @return The protocol fee amount based on the token and the price.
*/
function getProtocolFee(address _exchangeToken, uint256 _price) external view returns (uint256);

/**
* @notice Sets the flat protocol fee for exchanges in $BOSON.
*
Expand Down
2 changes: 1 addition & 1 deletion contracts/protocol/bases/OfferBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents {
if (_offer.buyerCancelPenalty > offerPrice) revert InvalidOfferPenalty();
if (_offer.priceType == PriceType.Static) {
// Calculate and set the protocol fee
uint256 protocolFee = getProtocolFee(_offer.exchangeToken, offerPrice);
uint256 protocolFee = _getProtocolFee(_offer.exchangeToken, offerPrice);

// Calculate the agent fee amount
uint256 agentFeeAmount = (agent.feePercentage * offerPrice) / HUNDRED_PERCENT;
Expand Down
35 changes: 28 additions & 7 deletions contracts/protocol/bases/ProtocolBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EIP712Lib } from "../libs/EIP712Lib.sol";
import { BosonTypes } from "../../domain/BosonTypes.sol";
import { PausableBase } from "./PausableBase.sol";
import { ReentrancyGuardBase } from "./ReentrancyGuardBase.sol";
import { FundsLib } from "../libs/FundsLib.sol";

/**
* @title ProtocolBase
Expand Down Expand Up @@ -687,18 +688,38 @@ abstract contract ProtocolBase is PausableBase, ReentrancyGuardBase, BosonErrors
}

/**
* @notice calculate the protocol fee for a given exchange
* @notice calculate the protocol fee amount for a given exchange
*
* @param _exchangeToken - the token used for the exchange
* @param _price - the price of the exchange
* @return protocolFee - the protocol fee
*/
function getProtocolFee(address _exchangeToken, uint256 _price) internal view returns (uint256 protocolFee) {
// Calculate and set the protocol fee
return
_exchangeToken == protocolAddresses().token
? protocolFees().flatBoson
: (protocolFees().percentage * _price) / HUNDRED_PERCENT;
function _getProtocolFee(address _exchangeToken, uint256 _price) internal view returns (uint256 protocolFee) {
ProtocolLib.ProtocolFees storage fees = protocolFees();
// Check if the exchange token is the Boson token
if (_exchangeToken == protocolAddresses().token) {
// Apply the flatBoson fee if the exchange token is the Boson token
return fees.flatBoson;
}

uint256[] storage priceRanges = fees.tokenPriceRanges[_exchangeToken];
uint256[] storage feePercentages = fees.tokenFeePercentages[_exchangeToken];

// If the token has a custom fee table, calculate based on the price ranges
uint256 priceRangesLength = priceRanges.length;
if (priceRangesLength > 0) {
for (uint256 i; i < priceRangesLength; ++i) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (uint256 i; i < priceRangesLength; ++i) {
for (uint256 i; i < priceRangesLength - 1; ++i) {

I guess it should give the same result saving a few instruction/gas (at least 1 if() assessment)

if (_price <= priceRanges[i]) {
// Apply the fee percentage for the matching price range
return FundsLib.applyPercent(_price, feePercentages[i]);
}
}
// If price exceeds all ranges, use the highest fee percentage
return FundsLib.applyPercent(_price, feePercentages[priceRangesLength - 1]);
}

// If no custom fee table exists, fallback to using the default protocol percentage
return FundsLib.applyPercent(_price, fees.percentage);
}

/**
Expand Down
93 changes: 86 additions & 7 deletions contracts/protocol/facets/ConfigHandlerFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ contract ConfigHandlerFacet is IBosonConfigHandler, ProtocolBase {
*
* @param _addresses - struct of Boson Protocol addresses (Boson Token (ERC-20) contract, treasury, and Voucher contract)
* @param _limits - struct with Boson Protocol limits
* @param _fees - struct of Boson Protocol fees
* @param defaultFeePercentage - efault percentage that will be taken as a fee from the net of a Boson Protocol exchange.
* @param flatBosonFee - flat fee taken for exchanges in $BOSON
* @param buyerEscalationDepositPercentage - buyer escalation deposit percentage
*/
function initialize(
ProtocolLib.ProtocolAddresses calldata _addresses,
ProtocolLib.ProtocolLimits calldata _limits,
ProtocolLib.ProtocolFees calldata _fees
uint256 defaultFeePercentage,
uint256 flatBosonFee,
uint256 buyerEscalationDepositPercentage
) public onlyUninitialized(type(IBosonConfigHandler).interfaceId) {
// Register supported interfaces
DiamondLib.addSupportedInterface(type(IBosonConfigHandler).interfaceId);
Expand All @@ -38,10 +42,10 @@ contract ConfigHandlerFacet is IBosonConfigHandler, ProtocolBase {
setTreasuryAddress(_addresses.treasury);
setVoucherBeaconAddress(_addresses.voucherBeacon);
setPriceDiscoveryAddress(_addresses.priceDiscovery);
setProtocolFeePercentage(_fees.percentage);
setProtocolFeeFlatBoson(_fees.flatBoson);
setProtocolFeePercentage(defaultFeePercentage); // this sets the default fee percentage if fee table is not configured for the exchange token
setProtocolFeeFlatBoson(flatBosonFee);
setMaxEscalationResponsePeriod(_limits.maxEscalationResponsePeriod);
setBuyerEscalationDepositPercentage(_fees.buyerEscalationDepositPercentage);
setBuyerEscalationDepositPercentage(buyerEscalationDepositPercentage);
setMaxTotalOfferFeePercentage(_limits.maxTotalOfferFeePercentage);
setMaxRoyaltyPercentage(_limits.maxRoyaltyPercentage);
setMaxResolutionPeriod(_limits.maxResolutionPeriod);
Expand Down Expand Up @@ -226,14 +230,62 @@ contract ConfigHandlerFacet is IBosonConfigHandler, ProtocolBase {
}

/**
* @notice Gets the protocol fee percentage.
* @notice Sets the feeTable for a specific token given price ranges and fee tiers for
* the corresponding price ranges.
*
* @return the protocol fee percentage
* Reverts if the number of fee percentages does not match the number of price ranges.
* Reverts if token is Zero address.
0xlucian marked this conversation as resolved.
Show resolved Hide resolved
*
* @dev Caller must have ADMIN role.
*
* @param _tokenAddress - the address of the token
* @param _priceRanges - array of token price ranges
* @param _feePercentages - array of fee percentages corresponding to each price range
*/
function setProtocolFeeTable(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I want to "update" a range for an existing token in the fee table, I need to know the existing ranges and values for it.
There is no method to fetch these data. The only way to get the current table is parsing the events.
I would suggest adding a method to fetch the current fee table for a given token to make any update easier

address _tokenAddress,
uint256[] calldata _priceRanges,
uint256[] calldata _feePercentages
) external override onlyRole(ADMIN) nonReentrant {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we allow _tokenAddress to be the Boson Token, we may break the rule saying that Boson Token is a flat fee.
That will become implementation dependent (current implementation will return the flat fee for Boson token, without looking into the fee ranges map).
I think it should worth checking here that the _tokenAddress is NOT the Boson token to avoid any ambiguity

if (_priceRanges.length != _feePercentages.length) revert ArrayLengthMismatch();
// Clear existing price ranges and percentage tiers
delete protocolFees().tokenPriceRanges[_tokenAddress];
delete protocolFees().tokenFeePercentages[_tokenAddress];

if (_priceRanges.length != 0) {
setTokenPriceRanges(_tokenAddress, _priceRanges);
setTokenFeePercentages(_tokenAddress, _feePercentages);
}
emit FeeTableUpdated(_tokenAddress, _priceRanges, _feePercentages, msgSender());
}

/**
* @notice Gets the default protocol fee percentage.
*
* @return the default protocol fee percentage
*/
function getProtocolFeePercentage() external view override returns (uint256) {
return protocolFees().percentage;
}

/**
* @notice Retrieves the protocol fee percentage for a given token and price.
*
* @dev This function calculates the protocol fee based on the token and price.
* If the token has a custom fee table, it applies the corresponding fee percentage
* for the price range. If the token does not have a custom fee table, it falls back
* to the default protocol fee percentage. If the exchange token is BOSON,
* this function returns the flatBoson fee
*
* @param _exchangeToken - The address of the token being used for the exchange.
* @param _price - The price of the item or service in the exchange.
*
* @return The protocol fee amount based on the token and the price.
*/
function getProtocolFee(address _exchangeToken, uint256 _price) external view override returns (uint256) {
return _getProtocolFee(_exchangeToken, _price);
}

/**
* @notice Sets the flat protocol fee for exchanges in $BOSON.
*
Expand Down Expand Up @@ -558,6 +610,33 @@ contract ConfigHandlerFacet is IBosonConfigHandler, ProtocolBase {
return address(DiamondLib.diamondStorage().accessController);
}

/**
* @notice Sets the price ranges for a specific token.
*
* @param _tokenAddress - the address of the token
* @param _priceRanges - array of price ranges for the token
*/
function setTokenPriceRanges(address _tokenAddress, uint256[] calldata _priceRanges) internal {
for (uint256 i = 1; i < _priceRanges.length; ++i) {
if (_priceRanges[i] < _priceRanges[i - 1]) revert NonAscendingOrder();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (_priceRanges[i] < _priceRanges[i - 1]) revert NonAscendingOrder();
if (_priceRanges[i] <= _priceRanges[i - 1]) revert NonAscendingOrder();

I don't think it makes sense to accept 2 successive ranges with the same value

}
protocolFees().tokenPriceRanges[_tokenAddress] = _priceRanges;
}

/**
* @notice Sets the fee percentages for a specific token and price ranges.
*
* @param _tokenAddress - the address of the token
* @param _feePercentages - array of fee percentages corresponding to each price range
*/
function setTokenFeePercentages(address _tokenAddress, uint256[] calldata _feePercentages) internal {
// Set the fee percentages for the token
for (uint256 i; i < _feePercentages.length; ++i) {
checkMaxPercententage(_feePercentages[i]);
}
protocolFees().tokenFeePercentages[_tokenAddress] = _feePercentages;
}

/**
* @notice Checks that supplied value is not 0.
*
Expand Down
2 changes: 1 addition & 1 deletion contracts/protocol/facets/PriceDiscoveryHandlerFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ contract PriceDiscoveryHandlerFacet is IBosonPriceDiscoveryHandler, PriceDiscove

// Calculate fees
address exchangeToken = offer.exchangeToken;
uint256 protocolFeeAmount = getProtocolFee(exchangeToken, actualPrice);
uint256 protocolFeeAmount = _getProtocolFee(exchangeToken, actualPrice);

{
// Calculate royalties
Expand Down
2 changes: 1 addition & 1 deletion contracts/protocol/facets/SequentialCommitHandlerFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ contract SequentialCommitHandlerFacet is IBosonSequentialCommitHandler, PriceDis

{
// Calculate fees
thisExchangeCost.protocolFeeAmount = getProtocolFee(exchangeToken, thisExchangeCost.price);
thisExchangeCost.protocolFeeAmount = _getProtocolFee(exchangeToken, thisExchangeCost.price);

// Calculate royalties
{
Expand Down
6 changes: 5 additions & 1 deletion contracts/protocol/libs/ProtocolLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,16 @@ library ProtocolLib {

// Protocol fees storage
struct ProtocolFees {
// Percentage that will be taken as a fee from the net of a Boson Protocol exchange
// Default percentage that will be taken as a fee from the net of a Boson Protocol exchange.
// This fee is returned if no fee ranges are configured in the fee table for the given asset.
uint256 percentage; // 1.75% = 175, 100% = 10000
// Flat fee taken for exchanges in $BOSON
uint256 flatBoson;
// buyer escalation deposit percentage
uint256 buyerEscalationDepositPercentage;
// Token-specific fee tables
mapping(address => uint256[]) tokenPriceRanges; // Price ranges for each token
mapping(address => uint256[]) tokenFeePercentages; // Fee percentages for each price range
}

// Protocol entities storage
Expand Down
4 changes: 3 additions & 1 deletion scripts/config/facet-deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ function getConfigHandlerInitArgs() {
priceDiscovery: protocolConfig.PRICE_DISCOVERY[network],
},
protocolConfig.limits,
protocolConfig.fees,
protocolConfig.protocolFeePercentage,
protocolConfig.protocolFeeFlatBoson,
protocolConfig.buyerEscalationDepositPercentage,
];
}

Expand Down
8 changes: 3 additions & 5 deletions scripts/config/protocol-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ const { oneWeek, ninetyDays } = require("../../test/util/constants");

module.exports = {
// Protocol configuration params
fees: {
percentage: "50", // 0.5% : 50
flatBoson: "0",
buyerEscalationDepositPercentage: "1000", // 10%
},
protocolFeePercentage: "50", // 0.5% : 50
protocolFeeFlatBoson: "0",
buyerEscalationDepositPercentage: "1000", // 10%,
limits: {
maxExchangesPerBatch: "140",
maxOffersPerGroup: "95",
Expand Down
2 changes: 2 additions & 0 deletions scripts/config/revert-reasons.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exports.RevertReasons = {
INVALID_STATE: "InvalidState",
ARRAY_LENGTH_MISMATCH: "ArrayLengthMismatch",
REENTRANCY_GUARD: "ReentrancyGuard",
NON_ASCENDING_ORDER: "NonAscendingOrder",

// Facet initializer related
ALREADY_INITIALIZED: "AlreadyInitialized",
Expand Down Expand Up @@ -199,6 +200,7 @@ exports.RevertReasons = {
// Config related
FEE_PERCENTAGE_INVALID: "InvalidFeePercentage",
VALUE_ZERO_NOT_ALLOWED: "ValueZeroNotAllowed",
ARRAY_LENGTH_MISSMATCH: "ArrayLengthMissmatch",

// ERC2981 related
ROYALTY_FEE_INVALID: "InvalidRoyaltyFee",
Expand Down
8 changes: 3 additions & 5 deletions scripts/util/estimate-limits.js
Original file line number Diff line number Diff line change
Expand Up @@ -1002,11 +1002,9 @@ async function setupCommonEnvironment() {
maxPremintedVouchers: 100,
},
// Protocol fees
{
percentage: protocolFeePercentage,
flatBoson: protocolFeeFlatBoson,
buyerEscalationDepositPercentage,
},
protocolFeePercentage,
protocolFeeFlatBoson,
buyerEscalationDepositPercentage,
];

const facetNames = [
Expand Down
8 changes: 3 additions & 5 deletions test/example/SnapshotGateTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,9 @@ describe("SnapshotGate", function () {
maxPremintedVouchers: 10000,
},
// Protocol fees
{
percentage: protocolFeePercentage,
flatBoson: protocolFeeFlatBoson,
buyerEscalationDepositPercentage,
},
protocolFeePercentage,
protocolFeeFlatBoson,
buyerEscalationDepositPercentage,
];

const facetNames = [
Expand Down
2 changes: 1 addition & 1 deletion test/integration/01-update-account-roles-addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("[@skip-on-coverage] Update account roles addresses", function () {
diamondAddress: protocolDiamondAddress,
signers: [admin, treasury, buyer, rando, adminDR, treasuryDR, agent],
contractInstances: { accountHandler, offerHandler, exchangeHandler, fundsHandler, disputeHandler },
protocolConfig: [, , { buyerEscalationDepositPercentage }],
protocolConfig: [, , , , buyerEscalationDepositPercentage],
} = await setupTestEnvironment(contracts));

bosonErrors = await getContractAt("BosonErrors", await accountHandler.getAddress());
Expand Down
2 changes: 1 addition & 1 deletion test/integration/02-Upgraded-facet.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("[@skip-on-coverage] After facet upgrade, everything is still operation
({
signers: [admin, treasury, buyer, rando, adminDR, treasuryDR],
contractInstances: { accountHandler, offerHandler, exchangeHandler, fundsHandler, disputeHandler },
protocolConfig: [, , { buyerEscalationDepositPercentage }],
protocolConfig: [, , , , buyerEscalationDepositPercentage],
diamondAddress: protocolDiamondAddress,
} = await setupTestEnvironment(contracts));

Expand Down
Loading
Loading