IFeeDistributor Interface¶
The IFeeDistributor interface defines the standard for smart contracts responsible for distributing collected fees or revenues to multiple recipients based on predefined percentages or shares. This is a critical component for managing revenue sharing in various decentralized applications within the Gemforce ecosystem.
Overview¶
IFeeDistributor provides:
- Configurable Shares: Define distribution percentages for multiple recipients.
- Automated Distribution: Functions to trigger and manage fee distribution.
- Recipient Management: Add, update, and remove recipients with their shares.
- Event Logging: Comprehensive event tracking for all distribution activities.
Key Features¶
Distribution Management¶
- Manual Trigger: Allow authorized parties to initiate distribution.
- Automated Trigger (Optional): Can be integrated with external mechanisms for time-based or volume-based distribution.
- Share Calculation: Accurately calculate each recipient's share.
Recipient and Share Management¶
- Add Recipient: Onboard new recipients with their respective shares.
- Update Share: Adjust existing recipient shares.
- Remove Recipient: Remove recipients from the distribution list.
Fund Handling¶
- Multi-Token Support: Can be designed to distribute various ERC20 tokens or native currency (ETH).
- Residual Handling: Mechanisms to manage any remaining dust or undistributed funds.
Interface Definition¶
interface IFeeDistributor {
// Events
event FundsReceived(
address indexed token,
uint256 amount,
address indexed sender
);
event FeesDistributed(
address indexed token,
uint256 totalAmount,
uint256 leftOverAmount,
address indexed distributor
);
event RecipientAdded(
address indexed recipient,
uint256 share,
address indexed manager
);
event RecipientUpdated(
address indexed recipient,
uint256 oldShare,
uint256 newShare,
address indexed manager
);
event RecipientRemoved(
address indexed recipient,
uint256 share,
address indexed manager
);
event FeeCollectorUpdated(
address indexed oldCollector,
address indexed newCollector
);
// Structs
struct Recipient {
address addr;
uint256 share; // Basis points (e.g., 100 = 1%)
uint256 lastDistributedAmount; // Amount last sent to this recipient
}
// Core Functions
function distributeFees(address token) external returns (uint256 distributedAmount);
function updateRecipient(address recipientAddr, uint256 newShare) external;
function addRecipient(address recipientAddr, uint256 share) external;
function removeRecipient(address recipientAddr) external;
function setFeeCollector(address collector) external;
// View Functions
function getTotalShare() external view returns (uint256);
function getRecipientShare(address recipientAddr) external view returns (uint256);
function getRecipientCount() external view returns (uint256);
function getRecipientAddress(uint256 index) external view returns (address);
function getFeeCollector() external view returns (address);
function getBalance(address token) external view returns (uint256);
function getRecipientDetails(address recipientAddr) external view returns (Recipient memory);
}
Core Functions¶
distributeFees()¶
Initiates the distribution of collected fees for a specific token to all registered recipients based on their shares.
Parameters:
- token: The address of the ERC20 token to distribute. If address(0) is used, it refers to the native blockchain currency (e.g., ETH).
Returns:
- uint256: The total amount of the token that was successfully distributed.
Usage:
// Distribute ETH
feeDistributor.distributeFees(address(0));
// Distribute DAI (ERC20 token)
feeDistributor.distributeFees(DAI_TOKEN_ADDRESS);
addRecipient()¶
Adds a new address to the list of fee recipients with a specified share. Shares are typically defined in basis points (e.g., 100 = 1%).
Parameters:
- recipientAddr: The address of the new recipient.
- share: The share percentage of the recipient in basis points.
Access Control:
- Typically restricted to the contract owner or an authorized feeCollector.
updateRecipient()¶
Updates the share percentage of an existing recipient.
Parameters:
- recipientAddr: The address of the recipient whose share is to be updated.
- newShare: The new share percentage for the recipient.
removeRecipient()¶
Removes an address from the list of fee recipients.
Parameters:
- recipientAddr: The address of the recipient to be removed.
Implementation Example¶
Basic FeeDistributor Contract¶
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract FeeDistributor is IFeeDistributor, Ownable {
using SafeMath for uint256;
uint256 public constant TOTAL_BASIS_POINTS = 10000; // 100%
// Mapping from recipient address to their share (in basis points)
mapping(address => uint256) private _recipientShares;
// Array to maintain order and iterate over recipients
address[] private _recipients;
// Total sum of all recipient shares
uint256 private _totalShares;
address private _feeCollector;
constructor() {
_feeCollector = msg.sender;
}
modifier onlyFeeCollector() {
require(msg.sender == _feeCollector, "Only fee collector can call this function");
_;
}
receive() external payable {
emit FundsReceived(address(0), msg.value, msg.sender);
}
// Handles ERC20 transfers directly to the contract
function onERC20Received(address, uint256 amount) external returns (bool) {
emit FundsReceived(msg.sender, amount, tx.origin); // Assume msg.sender is the ERC20 token contract
return true;
}
function distributeFees(address token) external override returns (uint256 distributedAmount) {
require(_recipients.length > 0, "No recipients to distribute to");
require(_totalShares <= TOTAL_BASIS_POINTS, "Invalid total shares configuration");
uint256 contractBalance;
if (token == address(0)) {
contractBalance = address(this).balance;
} else {
contractBalance = IERC20(token).balanceOf(address(this));
}
require(contractBalance > 0, "No funds to distribute");
distributedAmount = 0;
uint256 remainingAmount = contractBalance;
for (uint256 i = 0; i < _recipients.length; i++) {
address recipientAddr = _recipients[i];
uint256 share = _recipientShares[recipientAddr];
// Calculate amount for current recipient
uint256 amountToSend = contractBalance.mul(share).div(TOTAL_BASIS_POINTS);
if (amountToSend > 0) {
if (token == address(0)) {
// Send ETH
payable(recipientAddr).transfer(amountToSend);
} else {
// Send ERC20
IERC20(token).transfer(recipientAddr, amountToSend);
}
distributedAmount = distributedAmount.add(amountToSend);
remainingAmount = remainingAmount.sub(amountToSend);
}
}
emit FeesDistributed(token, distributedAmount, remainingAmount, msg.sender);
}
function addRecipient(address recipientAddr, uint256 share) external override onlyFeeCollector {
require(recipientAddr != address(0), "Cannot add zero address");
require(_recipientShares[recipientAddr] == 0, "Recipient already exists");
require(_totalShares.add(share) <= TOTAL_BASIS_POINTS, "Total shares exceed 100%");
_recipientShares[recipientAddr] = share;
_recipients.push(recipientAddr);
_totalShares = _totalShares.add(share);
emit RecipientAdded(recipientAddr, share, msg.sender);
}
function updateRecipient(address recipientAddr, uint256 newShare) external override onlyFeeCollector {
require(recipientAddr != address(0), "Cannot update zero address");
require(_recipientShares[recipientAddr] != 0, "Recipient does not exist");
uint256 oldShare = _recipientShares[recipientAddr];
require(_totalShares.sub(oldShare).add(newShare) <= TOTAL_BASIS_POINTS, "Total shares exceed 100%");
_recipientShares[recipientAddr] = newShare;
_totalShares = _totalShares.sub(oldShare).add(newShare);
emit RecipientUpdated(recipientAddr, oldShare, newShare, msg.sender);
}
function removeRecipient(address recipientAddr) external override onlyFeeCollector {
require(recipientAddr != address(0), "Cannot remove zero address");
uint256 shareToRemove = _recipientShares[recipientAddr];
require(shareToRemove > 0, "Recipient does not exist");
delete _recipientShares[recipientAddr];
_totalShares = _totalShares.sub(shareToRemove);
// Remove from dynamic array by swapping with last element and popping
for (uint256 i = 0; i < _recipients.length; i++) {
if (_recipients[i] == recipientAddr) {
_recipients[i] = _recipients[_recipients.length - 1];
_recipients.pop();
break;
}
}
emit RecipientRemoved(recipientAddr, shareToRemove, msg.sender);
}
function setFeeCollector(address collector) external override onlyOwner {
require(collector != address(0), "New fee collector cannot be the zero address");
emit FeeCollectorUpdated(_feeCollector, collector);
_feeCollector = collector;
}
function getTotalShare() external view override returns (uint256) {
return _totalShares;
}
function getRecipientShare(address recipientAddr) external view override returns (uint256) {
return _recipientShares[recipientAddr];
}
function getRecipientCount() external view override returns (uint256) {
return _recipients.length;
}
function getRecipientAddress(uint256 index) external view override returns (address) {
require(index < _recipients.length, "Index out of bounds");
return _recipients[index];
}
function getFeeCollector() external view override returns (address) {
return _feeCollector;
}
function getBalance(address token) external view override returns (uint256) {
if (token == address(0)) {
return address(this).balance;
} else {
return IERC20(token).balanceOf(address(this));
}
}
function getRecipientDetails(address recipientAddr) external view override returns (Recipient memory) {
return Recipient({
addr: recipientAddr,
share: _recipientShares[recipientAddr],
lastDistributedAmount: 0 // This would require more complex state tracking
});
}
}
Security Considerations¶
Access Control¶
onlyOwner: Critical administrative functions (like changing the fee collector) should be restricted to the contract owner.onlyFeeCollector: Functions modifying recipient lists or shares should be restricted to an authorized fee collector role.
Fund Handling¶
- Reentrancy: Implement reentrancy guards for
distributeFeesif external calls are made that could re-enter the contract (though direct token transfers are less prone to this). - Exact Amounts: Ensure precise calculations to avoid sending unintended amounts or leaving dust. Using SafeMath is crucial.
- Eth/Token Differences: Correctly handle native currency (ETH) transfers vs. ERC20 token transfers.
- Dust Management: Consider a mechanism to handle small remaining amounts (dust) if total shares don't perfectly sum up to
TOTAL_BASIS_POINTSor if precision issues arise.
Configuration¶
- Share Validation: Prevent total shares from exceeding
TOTAL_BASIS_POINTS(100%) to avoid potential over-distribution. - Zero Address Checks: Always validate non-zero addresses for recipients and collectors.
Best Practices¶
Shares Configuration¶
- Basis Points: Use basis points (e.g., 10,000 for 100%) for share representation to avoid floating-point errors common in Solidity.
- Sum to 100%: Ensure that the sum of all recipient shares equals
TOTAL_BASIS_POINTSto guarantee full distribution and no residual funds left in the contract (unless intentional).
Modularity¶
- Separation of Concerns: Keep fee distribution logic separate from other core application logic.
- Upgradeable: Design the contract to be upgradeable if shares or recipients might change frequently or if new distribution logic is anticipated.
Gas Efficiency¶
- Array Iteration: Be mindful of gas costs for iterating over a large number of recipients; large arrays can become expensive. Consider a pull-based mechanism for recipients to claim their funds if recipient count is very high.
Integration Examples¶
Frontend Integration (React/Web3Modal)¶
import { ethers } from 'ethers';
import feeDistributorABI from './FeeDistributor.json'; // ABI of the FeeDistributor contract
const FEE_DISTRIBUTOR_ADDRESS = "0x..."; // Deploy address of your FeeDistributor contract
const getProvider = () => new ethers.providers.Web3Provider(window.ethereum);
const getSigner = () => getProvider().getSigner();
const getFeeDistributorContract = () => new ethers.Contract(FEE_DISTRIBUTOR_ADDRESS, feeDistributorABI, getSigner());
async function handleDistributeFees(tokenAddress: string) {
try {
const feeDistributor = getFeeDistributorContract();
const tx = await feeDistributor.distributeFees(tokenAddress);
await tx.wait();
alert("Fees distributed successfully!");
} catch (error) {
console.error("Error distributing fees:", error);
alert("Failed to distribute fees.");
}
}
async function handleAddRecipient(recipientAddress: string, share: number) {
try {
const feeDistributor = getFeeDistributorContract();
// Assuming share is in basis points, e.g., 2500 for 25%
const tx = await feeDistributor.addRecipient(recipientAddress, share);
await tx.wait();
alert("Recipient added successfully!");
} catch (error) {
console.error("Error adding recipient:", error);
alert("Failed to add recipient.");
}
}
// Example usage in a React component
// <button onClick={() => handleDistributeFees("0x0000000000000000000000000000000000000000")}>Distribute ETH Fees</button>
// <button onClick={() => handleAddRecipient("0xRecipientAddress", 1000)}>Add 10% Recipient</button>
Backend Integration (Node.js/Ethers.js)¶
const { ethers } = require("ethers");
const FeeDistributorABI = require("./FeeDistributor.json").abi;
const provider = new ethers.JsonRpcProvider("YOUR_RPC_URL");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
const feeDistributorAddress = "0x..."; // Your deployed FeeDistributor contract address
const feeDistributorContract = new ethers.Contract(feeDistributorAddress, FeeDistributorABI, wallet);
async function performFeeDistribution(tokenAddress) {
try {
console.log(`Initiating fee distribution for ${tokenAddress === ethers.ZeroAddress ? "ETH" : "ERC20 token"}...`);
const tx = await feeDistributorContract.distributeFees(tokenAddress);
await tx.wait();
console.log(`Distribution transaction successful: ${tx.hash}`);
} catch (error) {
console.error("Error during fee distribution:", error);
throw error;
}
}
async function addNewRecipient(address, share) {
try {
console.log(`Adding new recipient ${address} with share ${share}...`);
const tx = await feeDistributorContract.addRecipient(address, share);
await tx.wait();
console.log(`Recipient added transaction successful: ${tx.hash}`);
} catch (error) {
console.error("Error adding new recipient:", error);
throw error;
}
}
// Example usage
// performFeeDistribution(ethers.ZeroAddress); // Distribute ETH
// addNewRecipient("0xNewRecipientAddress", 500); // Add a recipient with 5% share
Related Documentation¶
Standards Compliance¶
- ERC20: Compatible with ERC20 tokens for distribution.
- Ownable: Utilizes OpenZeppelin's
Ownablefor administrative access control.