diff --git a/src/VotingMultipliers.sol b/src/VotingMultipliers.sol new file mode 100644 index 0000000..ec15446 --- /dev/null +++ b/src/VotingMultipliers.sol @@ -0,0 +1,63 @@ +// VotingMultipliers.sol +pragma solidity ^0.8.22; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./IMultiplier.sol"; +import "../interfaces/IBread.sol"; + +contract VotingMultipliers is Initializable, OwnableUpgradeable { + IBread public BREAD; + IMultiplier[] public whitelistedMultipliers; + IMultiplier[] public queuedMultipliersForAddition; + IMultiplier[] public queuedMultipliersForRemoval; + + event MultiplierAdded(IMultiplier indexed multiplier); + event MultiplierRemoved(IMultiplier indexed multiplier); + + function initialize(IBread _bread) public initializer { + __Ownable_init(); + BREAD = _bread; + } + + function getTotalMultipliers(address user) external view returns (uint256) { + uint256 totalMultiplier = 1e18; // Start with 100% (no multiplier) + for (uint256 i = 0; i < whitelistedMultipliers.length; i++) { + IMultiplier multiplier = whitelistedMultipliers[i]; + if (block.number <= multiplier.validUntil(user)) { + totalMultiplier += multiplier.getMultiplyingFactor(user); + } + } + return totalMultiplier; + } + + function queueMultiplierAddition(IMultiplier _multiplier) external onlyOwner { + queuedMultipliersForAddition.push(_multiplier); + } + + function queueMultiplierRemoval(IMultiplier _multiplier) external onlyOwner { + queuedMultipliersForRemoval.push(_multiplier); + } + + function updateMultipliers() external onlyOwner { + // Add queued multipliers + for (uint256 i = 0; i < queuedMultipliersForAddition.length; i++) { + whitelistedMultipliers.push(queuedMultipliersForAddition[i]); + emit MultiplierAdded(queuedMultipliersForAddition[i]); + } + delete queuedMultipliersForAddition; + + // Remove queued multipliers + for (uint256 i = 0; i < queuedMultipliersForRemoval.length; i++) { + for (uint256 j = 0; j < whitelistedMultipliers.length; j++) { + if (whitelistedMultipliers[j] == queuedMultipliersForRemoval[i]) { + whitelistedMultipliers[j] = whitelistedMultipliers[whitelistedMultipliers.length - 1]; + whitelistedMultipliers.pop(); + emit MultiplierRemoved(queuedMultipliersForRemoval[i]); + break; + } + } + } + delete queuedMultipliersForRemoval; + } +} \ No newline at end of file diff --git a/src/YieldDistributor.sol b/src/YieldDistributor.sol index 8128b57..5009480 100644 --- a/src/YieldDistributor.sol +++ b/src/YieldDistributor.sol @@ -9,6 +9,7 @@ import {ERC20VotesUpgradeable} from import {Bread} from "bread-token/src/Bread.sol"; import {IYieldDistributor} from "src/interfaces/IYieldDistributor.sol"; +import {VotingMultipliers} from "src/VotingMultipliers.sol"; /** * @title Breadchain Yield Distributor @@ -51,6 +52,8 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable { uint256 public yieldFixedSplitDivisor; /// @notice The address of the `ButteredBread` token contract ERC20VotesUpgradeable public BUTTERED_BREAD; + /// @notice The address of the `VotingMultipliers` contract + VotingMultipliers public votingMultipliers; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -66,7 +69,8 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable { uint256 _cycleLength, uint256 _yieldFixedSplitDivisor, uint256 _lastClaimedBlockNumber, - address[] memory _projects + address[] memory _projects, + VotingMultipliers _votingMultipliers ) public initializer { __Ownable_init(msg.sender); if ( @@ -85,6 +89,7 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable { cycleLength = _cycleLength; yieldFixedSplitDivisor = _yieldFixedSplitDivisor; lastClaimedBlockNumber = _lastClaimedBlockNumber; + votingMultipliers = _votingMultipliers; projectDistributions = new uint256[](_projects.length); projects = new address[](_projects.length); @@ -249,18 +254,21 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable { } if (_totalPoints == 0) revert ZeroVotePoints(); + uint256 multiplier = votingMultipliers.getTotalMultipliers(_account); + uint256 adjustedVotingPower = (_votingPower * multiplier) / 1e18; + bool _hasVotedInCycle = accountLastVoted[_account] > lastClaimedBlockNumber; uint256[] storage _voterDistributions = voterDistributions[_account]; if (!_hasVotedInCycle) { delete voterDistributions[_account]; - currentVotes += _votingPower; + currentVotes += adjustedVotingPower; } for (uint256 i; i < _points.length; ++i) { if (!_hasVotedInCycle) _voterDistributions.push(0); else projectDistributions[i] -= _voterDistributions[i]; - uint256 _currentProjectDistribution = ((_points[i] * _votingPower * PRECISION) / _totalPoints) / PRECISION; + uint256 _currentProjectDistribution = ((_points[i] * adjustedVotingPower * PRECISION) / _totalPoints) / PRECISION; projectDistributions[i] += _currentProjectDistribution; _voterDistributions[i] = _currentProjectDistribution; } diff --git a/src/interfaces/IMultiplier.sol b/src/interfaces/IMultiplier.sol new file mode 100644 index 0000000..f99d452 --- /dev/null +++ b/src/interfaces/IMultiplier.sol @@ -0,0 +1,82 @@ +// IMultiplier.sol +pragma solidity ^0.8.22; + +interface IMultiplier { + function getMultiplyingFactor(address user) external view returns (uint256); + function validUntil(address user) external view returns (uint256); +} + +// INFTMultiplier.sol +pragma solidity ^0.8.22; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "./IMultiplier.sol"; + +interface INFTMultiplier is IMultiplier { + function NFTAddress() external view returns (IERC721); + function hasNFT(address user) external view returns (bool); +} + +// PermanentNFTMultiplier.sol +pragma solidity ^0.8.22; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "./INFTMultiplier.sol"; + +contract PermanentNFTMultiplier is INFTMultiplier { + IERC721 public immutable NFTAddress; + uint256 public immutable multiplyingFactor; + uint256 public constant validity = type(uint256).max; + + constructor(IERC721 _nftAddress, uint256 _multiplyingFactor) { + NFTAddress = _nftAddress; + multiplyingFactor = _multiplyingFactor; + } + + function getMultiplyingFactor(address user) external view override returns (uint256) { + return hasNFT(user) ? multiplyingFactor : 0; + } + + function validUntil(address) external pure override returns (uint256) { + return validity; + } + + function hasNFT(address user) public view override returns (bool) { + return NFTAddress.balanceOf(user) > 0; + } +} + +// DynamicNFTMultiplier.sol +pragma solidity ^0.8.22; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "./INFTMultiplier.sol"; + +contract DynamicNFTMultiplier is INFTMultiplier { + IERC721 public immutable NFTAddress; + mapping(address => uint256) public userToFactor; + mapping(address => uint256) public userToValidity; + + constructor(IERC721 _nftAddress) { + NFTAddress = _nftAddress; + } + + function getMultiplyingFactor(address user) external view override returns (uint256) { + return hasNFT(user) ? userToFactor[user] : 0; + } + + function validUntil(address user) external view override returns (uint256) { + return userToValidity[user]; + } + + function hasNFT(address user) public view override returns (bool) { + return NFTAddress.balanceOf(user) > 0; + } + + function setUserFactor(address user, uint256 factor, uint256 validity) external { + // Add appropriate access control + require(hasNFT(user), "User does not have the required NFT"); + userToFactor[user] = factor; + userToValidity[user] = validity; + } +} \ No newline at end of file