Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/abstract/AbstractDistributionStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";

/// @title AbstractDistributionStrategy
/// @notice Abstract base for distribution strategies that split yield among registry recipients
/// @dev Concrete strategies implement `distribute` to define how yield is allocated
abstract contract AbstractDistributionStrategy is Initializable, IDistributionStrategy, OwnableUpgradeable {
/// @dev Concrete strategies implement `distribute` to define how yield is allocated.
/// Inherits ReentrancyGuardUpgradeable to protect all distribute() implementations.
abstract contract AbstractDistributionStrategy is
Initializable,
IDistributionStrategy,
OwnableUpgradeable,
ReentrancyGuardUpgradeable
{
using SafeERC20 for IERC20;

/// @notice Thrown when a zero address is supplied where a valid address is required
Expand Down Expand Up @@ -96,6 +103,7 @@ abstract contract AbstractDistributionStrategy is Initializable, IDistributionSt
address _distributionManager
) internal onlyInitializing {
__Ownable_init(msg.sender);
__ReentrancyGuard_init();
__AbstractDistributionStrategy_init_unchained(_yieldToken, _recipientRegistry, _distributionManager);
}

Expand Down
6 changes: 4 additions & 2 deletions src/base/BaseDistributionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {AbstractDistributionManager} from "../abstract/AbstractDistributionManag
import {IDistributionStrategy} from "../interfaces/IDistributionStrategy.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";

/// @title BaseDistributionManager
/// @notice Concrete implementation of AbstractDistributionManager that distributes to a single strategy
/// @dev Simple manager that distributes all yield to one configured strategy
contract BaseDistributionManager is AbstractDistributionManager {
contract BaseDistributionManager is AbstractDistributionManager, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;

// ============ EIP-7201 Namespaced Storage ============
Expand Down Expand Up @@ -62,6 +63,7 @@ contract BaseDistributionManager is AbstractDistributionManager {
) external initializer {
// Initialize parent AbstractDistributionManager
__AbstractDistributionManager_init(_cycleManager, _recipientRegistry, _baseToken, _votingModule);
__ReentrancyGuard_init();

// Set the single strategy
if (_strategy != address(0)) {
Expand Down Expand Up @@ -93,7 +95,7 @@ contract BaseDistributionManager is AbstractDistributionManager {
}

/// @notice Claims yield and distributes to the configured strategy
function claimAndDistribute() external override {
function claimAndDistribute() external override nonReentrant {
if (!isDistributionReady()) revert DistributionNotReady();
IDistributionStrategy strategy = _getBaseDistributionManagerStorage().distributionStrategy;
if (address(strategy) == address(0)) revert StrategyNotSet();
Expand Down
19 changes: 13 additions & 6 deletions src/base/MultiStrategyDistributionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {AbstractDistributionManager} from "../abstract/AbstractDistributionManag
import {IDistributionStrategy} from "../interfaces/IDistributionStrategy.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";

/// @title MultiStrategyDistributionManager
/// @notice Concrete implementation of AbstractDistributionManager that distributes to multiple strategies equally
/// @dev Distributes yield equally across all configured strategies
contract MultiStrategyDistributionManager is AbstractDistributionManager {
contract MultiStrategyDistributionManager is AbstractDistributionManager, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;

// ============ EIP-7201 Namespaced Storage ============
Expand Down Expand Up @@ -61,6 +62,7 @@ contract MultiStrategyDistributionManager is AbstractDistributionManager {
) external initializer {
// Initialize parent AbstractDistributionManager
__AbstractDistributionManager_init(_cycleManager, _recipientRegistry, _baseToken, _votingModule);
__ReentrancyGuard_init();

// Store strategies
require(_strategies.length > 0, "No strategies provided");
Expand Down Expand Up @@ -97,10 +99,9 @@ contract MultiStrategyDistributionManager is AbstractDistributionManager {
}

/// @notice Claims yield and distributes equally to all strategies
function claimAndDistribute() external override {
function claimAndDistribute() external override nonReentrant {
if (!isDistributionReady()) revert DistributionNotReady();
MultiStrategyDistributionManagerStorage storage $ = _getMultiStrategyDistributionManagerStorage();
require($.strategies.length > 0, "No strategies configured");

// Get the amount of yield available
uint256 yieldAmount = yieldModule().yieldAccrued();
Expand All @@ -110,14 +111,15 @@ contract MultiStrategyDistributionManager is AbstractDistributionManager {
yieldModule().claimYield(yieldAmount, address(this));
emit YieldClaimed(yieldAmount);

// Calculate amount per strategy (equal distribution)
// Calculate amount per strategy (equal distribution); last strategy absorbs dust
uint256 amountPerStrategy = yieldAmount / $.strategies.length;
uint256 remainder = yieldAmount - (amountPerStrategy * ($.strategies.length - 1));

// Cache storage getter before loop
IERC20 baseToken_ = baseToken();

// Distribute to each strategy
for (uint256 i = 0; i < $.strategies.length; i++) {
// Distribute to each strategy; last one gets the remainder
for (uint256 i = 0; i < $.strategies.length - 1; i++) {
IDistributionStrategy strategy = $.strategies[i];

// Transfer tokens to strategy
Expand All @@ -128,6 +130,11 @@ contract MultiStrategyDistributionManager is AbstractDistributionManager {

emit YieldDistributed(address(strategy), amountPerStrategy);
}
// Last strategy absorbs rounding dust
IDistributionStrategy lastStrategy = $.strategies[$.strategies.length - 1];
baseToken_.safeTransfer(address(lastStrategy), remainder);
lastStrategy.distribute(remainder);
emit YieldDistributed(address(lastStrategy), remainder);
}

/// @notice Gets all configured strategies
Expand Down
14 changes: 11 additions & 3 deletions src/implementation/strategies/EqualDistributionStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ contract EqualDistributionStrategy is AbstractDistributionStrategy {
}

/// @notice Distributes yield equally among all recipients
/// @dev Dust from integer division is left in the contract
/// @dev Distributes the full amount with no dust left in the contract. The last
/// recipient absorbs any rounding remainder (up to N-1 wei where N is recipient count).
/// @param amount The total amount of yield to distribute
function distribute(uint256 amount) external override onlyDistributionManager {
function distribute(uint256 amount) external override onlyDistributionManager nonReentrant {
if (amount == 0) revert ZeroAmount();

address[] memory recipients = recipientRegistry().getRecipients();
Expand All @@ -36,9 +37,16 @@ contract EqualDistributionStrategy is AbstractDistributionStrategy {
uint256 amountPerRecipient = amount / recipients.length;

IERC20 yieldToken_ = yieldToken();
for (uint256 i = 0; i < recipients.length; i++) {
uint256 remainder = amount;
for (uint256 i = 0; i < recipients.length - 1; i++) {
yieldToken_.safeTransfer(recipients[i], amountPerRecipient);
emit Distributed(recipients[i], amountPerRecipient);
remainder -= amountPerRecipient;
}
// Last recipient absorbs any rounding dust
if (remainder > 0) {
yieldToken_.safeTransfer(recipients[recipients.length - 1], remainder);
emit Distributed(recipients[recipients.length - 1], remainder);
}

AbstractDistributionStrategyStorage storage $ = _getAbstractDistributionStrategyStorage();
Expand Down
14 changes: 11 additions & 3 deletions src/implementation/strategies/VotingDistributionStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ contract VotingDistributionStrategy is AbstractDistributionStrategy {
}

/// @notice Distributes yield proportionally based on voting weights
/// @dev Recipients with zero votes receive nothing; dust from rounding is left in the contract
/// @dev Distributes the full amount with no dust left in the contract. The last
/// recipient absorbs any rounding remainder (up to N-1 wei where N is recipient count).
/// @param amount The total amount of yield to distribute
function distribute(uint256 amount) external override onlyDistributionManager {
function distribute(uint256 amount) external override onlyDistributionManager nonReentrant {
if (amount == 0) revert ZeroAmount();

address[] memory recipients = recipientRegistry().getRecipients();
Expand All @@ -89,13 +90,20 @@ contract VotingDistributionStrategy is AbstractDistributionStrategy {
if (totalVotes == 0) revert NoVotes();

IERC20 yieldToken_ = yieldToken();
for (uint256 i = 0; i < recipients.length; i++) {
uint256 remainder = amount;
for (uint256 i = 0; i < recipients.length - 1; i++) {
uint256 recipientShare = (amount * currentVotes[i]) / totalVotes;
remainder -= recipientShare;
if (recipientShare > 0) {
yieldToken_.safeTransfer(recipients[i], recipientShare);
emit Distributed(recipients[i], recipientShare);
}
}
// Last recipient absorbs any rounding dust
if (remainder > 0) {
yieldToken_.safeTransfer(recipients[recipients.length - 1], remainder);
emit Distributed(recipients[recipients.length - 1], remainder);
}

AbstractDistributionStrategyStorage storage $ = _getAbstractDistributionStrategyStorage();
$.distributionId++;
Expand Down
Loading