Skip to content

Commit

Permalink
Voting Multipliers (#100)
Browse files Browse the repository at this point in the history
* chore: init

* chore: init of Imultiplier @secbajor

* chore: implementing interfaces from #71

* chore: init permanftmul impl

* chore: tmp commit

* fix: reverting auto changes to yd

* fix: changing to interface

* fix: fixing imports

* chore: init derived interfaces

* fix: changing variable name"

* fix: removing ref to external, impl in implemtations

* chore: first multiplier

* chore: Adding voting multiplier handling and integration to yd

* fix: amending functionality to handle no multiplier for voter properly

* chore: adding unit and fuzzy tests

* chore: adding basic NFTMultiplier implementation, deploy script and config with tests

 Changes to be committed:

* fix: updating wrong documentation

* fix: reducing scope of pr

* fix: order of operations clarification

* chore: adding gas benchmark tests for mutipliers

* rename: using PRECISION instead of hardcoded 1e18 for clarity"

* chore: removing external calls by declaring public

* chore: renaming outdating convention

* chore: including prefix for function param and fixing up previous commit rename

* chore: renaming constant to fit camel case convention

* chore: refactoring constant to snake case

* style: underscore for internal vars"

* docs: updating authors

* chore: removing voting multiplier gas snapshot testing

* style: internal / function-scoped variables should have underscore

* perf: optimizing init variable validation

* Revert "perf: optimizing init variable validation"

This reverts commit de0309d.

* chore: introducing offchain indexing of multipliers to be bundled with voting function to prevent on-chain iteriations

* fmt: removing underscore from natspec comment

* fmt: moving func closer to similiar func and documenting func

* fmt: removing line break

* fmt: removing line break

* fmt: removing line break

* fmt: removing line break

* fmt: removing line break

* fmt: removing line break

* chore: fixing checking of minimum voting power to include voting multipliers

* chore: unclear why this is needed as it should have been merged from dev, reintroducing

* chore: attempting to revert changes from upstream

* chore: attempting to remove more unclear merge issues

---------

Co-authored-by: Ron Turetzky <[email protected]>
  • Loading branch information
RonTuretzky and Ron Turetzky authored Jan 2, 2025
1 parent 59eeba7 commit 4da5fa7
Show file tree
Hide file tree
Showing 17 changed files with 6,054 additions and 7 deletions.
64 changes: 64 additions & 0 deletions script/deploy/DeployNFTMultiplier.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Script} from "forge-std/Script.sol";
import {NFTMultiplier} from "src/multipliers/NFTMultiplier.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {console} from "forge-std/console.sol";

contract DeployNFTMultiplier is Script {
function run() external {
uint256 deployerPrivateKey;
address nftContractAddress;
uint256 initialMultiplyingFactor;
uint256 validUntilBlock;

string memory configPath = "deploy_config.json";
string memory jsonData;
// Try to read the JSON file, if it doesn't exist or can't be read, catch the error
try vm.readFile(configPath) returns (string memory data) {
jsonData = data;
} catch {
console.log("Config file not found or couldn't be read. Falling back to environment variables.");
jsonData = "";
}

if (bytes(jsonData).length > 0) {
// Read from JSON if file exists
deployerPrivateKey = vm.parseJsonUint(jsonData, ".deployerPrivateKey");
nftContractAddress = vm.parseJsonAddress(jsonData, ".nftContractAddress");
initialMultiplyingFactor = vm.parseJsonUint(jsonData, ".initialMultiplyingFactor");
validUntilBlock = vm.parseJsonUint(jsonData, ".validUntilBlock");
} else {
// Fall back to environment variables
deployerPrivateKey = vm.envUint("PRIVATE_KEY");
nftContractAddress = vm.envAddress("NFT_CONTRACT_ADDRESS");
initialMultiplyingFactor = vm.envUint("INITIAL_MULTIPLYING_FACTOR");
validUntilBlock = vm.envUint("VALID_UNTIL_BLOCK");
}

// Check if all required variables are set
require(deployerPrivateKey != 0, "Deployer private key not set");
require(nftContractAddress != address(0), "NFT contract address not set");
require(initialMultiplyingFactor != 0, "Initial multiplying factor not set");
require(validUntilBlock != 0, "Valid until block not set");

vm.startBroadcast(deployerPrivateKey);

NFTMultiplier implementation = new NFTMultiplier();

bytes memory initData = abi.encodeWithSelector(
NFTMultiplier.initialize.selector, IERC721(nftContractAddress), initialMultiplyingFactor, validUntilBlock
);

TransparentUpgradeableProxy proxy =
new TransparentUpgradeableProxy(address(implementation), vm.addr(deployerPrivateKey), initData);

NFTMultiplier nftMultiplier = NFTMultiplier(address(proxy));

vm.stopBroadcast();

console.log("NFTMultiplier deployed at:", address(nftMultiplier));
}
}
6 changes: 6 additions & 0 deletions script/deploy/config/deployNFTMul.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"deployerPrivateKey": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"nftContractAddress": "0x1234567890123456789012345678901234567890",
"initialMultiplyingFactor": 100,
"validUntilBlock": 1000000
}
105 changes: 105 additions & 0 deletions src/VotingMultipliers.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {IVotingMultipliers, IMultiplier} from "src/interfaces/IVotingMultipliers.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

/// @title VotingMultipliers
/// @notice A contract for managing voting multipliers
/// @dev Implements IVotingMultipliers interface
contract VotingMultipliers is OwnableUpgradeable, IVotingMultipliers {
/// @notice Array of allowlisted multiplier contracts
IMultiplier[] public allowlistedMultipliers;

/// @notice Adds a multiplier to the allowlist
/// @param _multiplier The multiplier contract to be added
function addMultiplier(IMultiplier _multiplier) external onlyOwner {
// Check if the multiplier is already allowlisted
for (uint256 i = 0; i < allowlistedMultipliers.length; i++) {
if (allowlistedMultipliers[i] == _multiplier) {
revert MultiplierAlreadyAllowlisted();
}
}
allowlistedMultipliers.push(_multiplier);
emit MultiplierAdded(_multiplier);
}

/// @notice Removes a multiplier from the allowlist
/// @param _multiplier The multiplier contract to be removed
function removeMultiplier(IMultiplier _multiplier) external onlyOwner {
bool isallowlisted = false;
for (uint256 i = 0; i < allowlistedMultipliers.length; i++) {
if (allowlistedMultipliers[i] == _multiplier) {
allowlistedMultipliers[i] = allowlistedMultipliers[allowlistedMultipliers.length - 1];
allowlistedMultipliers.pop();
isallowlisted = true;
emit MultiplierRemoved(_multiplier);
break;
}
}
if (!isallowlisted) {
revert MultiplierNotAllowlisted();
}
}

/// @notice Gets the indexes of valid multipliers for a user
/// @param _user The address of the user
/// @return uint256[] Array of valid multiplier indexes
function getValidMultiplierIndexes(address _user) public view returns (uint256[] memory) {
uint256[] memory validIndexes = new uint256[](allowlistedMultipliers.length);
uint256 count = 0;

for (uint256 i = 0; i < allowlistedMultipliers.length; i++) {
if (
block.number <= allowlistedMultipliers[i].validUntil(_user)
&& allowlistedMultipliers[i].getMultiplyingFactor(_user) > 0
) {
validIndexes[count] = i;
count++;
}
}

// Create correctly sized array
uint256[] memory result = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
result[i] = validIndexes[i];
}
return result;
}

/// @notice Calculates the total multiplier for a given user using specific multiplier indexes
/// @param _user The address of the user
/// @param _multiplierIndexes Array of multiplier indexes to use
/// @return The total multiplier value for the user
function getTotalMultipliers(address _user, uint256[] calldata _multiplierIndexes) public view returns (uint256) {
uint256 _totalMultiplier = 0;

for (uint256 i = 0; i < _multiplierIndexes.length; i++) {
uint256 index = _multiplierIndexes[i];
if (index >= allowlistedMultipliers.length) {
revert InvalidMultiplierIndex();
}

IMultiplier multiplier = allowlistedMultipliers[index];
if (block.number <= multiplier.validUntil(_user)) {
_totalMultiplier += multiplier.getMultiplyingFactor(_user);
}
}
return _totalMultiplier;
}

/// @notice Calculates the total multiplier for a given user
/// @param _user The address of the _user
/// @return The total multiplier value for the _user
/// @dev This function is intended for frontend and testing purposes
function getTotalMultipliers(address _user) public view returns (uint256) {
uint256 _totalMultiplier = 0;
for (uint256 i = 0; i < allowlistedMultipliers.length; i++) {
IMultiplier multiplier = allowlistedMultipliers[i];
if (block.number <= multiplier.validUntil(_user)) {
_totalMultiplier += multiplier.getMultiplyingFactor(_user);
}
}
return _totalMultiplier;
}
}
21 changes: 18 additions & 3 deletions src/YieldDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ 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
* @notice Distribute $BREAD yield to eligible member projects based on a voted distribution
* @author Breadchain Collective
* @custom:coauthor @RonTuretzky
* @custom:coauthor postcapitalistcrypto.eth
* @custom:coauthor bagelface.eth
* @custom:coauthor prosalads.eth
* @custom:coauthor kassandra.eth
* @custom:coauthor theblockchainsocialist.eth
* @custom:coauthor github.com/daopunk
*/
contract YieldDistributor is IYieldDistributor, OwnableUpgradeable {
contract YieldDistributor is IYieldDistributor, OwnableUpgradeable, VotingMultipliers {
/// @notice The address of the $BREAD token contract
Bread public BREAD;
/// @notice The precision to use for calculations
Expand Down Expand Up @@ -135,7 +137,7 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable {
uint256 _start,
uint256 _end,
address _account
) external view returns (uint256) {
) public view returns (uint256) {
if (_start >= _end) revert StartMustBeBeforeEnd();
if (_end > block.number) revert EndAfterCurrentBlock();

Expand Down Expand Up @@ -227,6 +229,19 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable {
_castVote(msg.sender, _points, _currentVotingPower);
}

/**
* @notice Cast votes for the distribution of $BREAD yield with multipliers
* @param _points List of points as integers for each project
* @param _multiplierIndices List of indices of multipliers to use for each project
*/
function castVoteWithMultipliers(uint256[] calldata _points, uint256[] calldata _multiplierIndices) public {
uint256 _currentVotingPower = getCurrentVotingPower(msg.sender);
uint256 multiplier = getTotalMultipliers(msg.sender, _multiplierIndices);
_currentVotingPower = multiplier == 0 ? _currentVotingPower : (_currentVotingPower * multiplier) / PRECISION;
if (_currentVotingPower < minRequiredVotingPower) revert BelowMinRequiredVotingPower();
_castVote(msg.sender, _points, _currentVotingPower);
}

/**
* @notice Internal function for casting votes for a specified user
* @param _account Address of user to cast votes for
Expand Down
35 changes: 35 additions & 0 deletions src/interfaces/IVotingMultipliers.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
pragma solidity ^0.8.22;

import {IMultiplier} from "./multipliers/IMultiplier.sol";

/// @title IVotingMultipliers
/// @notice Interface for the VotingMultipliers contract
/// @dev This interface defines the structure and functions for managing voting multipliers
interface IVotingMultipliers {
/// @notice Thrown when attempting to add a multiplier that is already allowlisted
error MultiplierAlreadyAllowlisted();
/// @notice Thrown when attempting to remove a multiplier that is not allowlisted
error MultiplierNotAllowlisted();
/// @notice Thrown when an invalid multiplier index is provided
error InvalidMultiplierIndex();
/// @notice Emitted when a new multiplier is added to the allowlist
/// @param multiplier The address of the added multiplier
event MultiplierAdded(IMultiplier indexed multiplier);
/// @notice Emitted when a multiplier is removed from the allowlist
/// @param multiplier The address of the removed multiplier
event MultiplierRemoved(IMultiplier indexed multiplier);
/// @notice Returns the multiplier at the specified index in the allowlist
/// @param index The index of the multiplier in the allowlist
/// @return The multiplier contract at the specified index
function allowlistedMultipliers(uint256 index) external view returns (IMultiplier);
/// @notice Calculates the total multiplier for a given _user
/// @param __user The address of the _user
/// @return The total multiplier value for the _user
function getTotalMultipliers(address __user) external view returns (uint256);
/// @notice Adds a multiplier to the allowlist
/// @param _multiplier The multiplier contract to be added
function addMultiplier(IMultiplier _multiplier) external;
/// @notice Removes a multiplier from the allowlist
/// @param _multiplier The multiplier contract to be removed
function removeMultiplier(IMultiplier _multiplier) external;
}
12 changes: 12 additions & 0 deletions src/interfaces/multipliers/ICrossChainProveableMultiplier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {IProveableMultiplier} from "src/interfaces/multipliers/IProveableMultiplier.sol";

/// @title Cross-Chain Proveable Multiplier Interface
/// @notice Interface for contracts that provide a cross-chain proveable multiplying factor
interface ICrossChainProveableMultiplier is IProveableMultiplier {
/// @notice Get the address of the bridge contract
/// @return The address of the contract used for cross-chain communication
function bridge() external view returns (address);
}
18 changes: 18 additions & 0 deletions src/interfaces/multipliers/IDynamicNFTMultiplier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {INFTMultiplier} from "src/interfaces/multipliers/INFTMultiplier.sol";
/// @title Dynamic NFT Multiplier Interface
/// @notice Interface for contracts that provide a dynamic multiplying factor for _users based on NFT ownership
/// @dev Extends the INFTMultiplier interface with dynamic multiplier functionality
interface IDynamicNFTMultiplier is INFTMultiplier {
/// @notice Get the multiplying factor for a _user
/// @param _user The address of the _user
/// @return The multiplying factor for the _user
function _userToFactor(address _user) external view returns (uint256);

/// @notice Get the validity period for a _user's factor
/// @param _user The address of the _user
/// @return The timestamp until which the _user's factor is valid
function _userToValidity(address _user) external view returns (uint256);
}
10 changes: 10 additions & 0 deletions src/interfaces/multipliers/IMultiplier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IMultiplier {
/// @notice Returns the voting multiplier for `_user`.
function getMultiplyingFactor(address _user) external view returns (uint256);

/// @notice Returns the validity period of the multiplier for `_user`.
function validUntil(address _user) external view returns (uint256);
}
19 changes: 19 additions & 0 deletions src/interfaces/multipliers/INFTMultiplier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IMultiplier} from "src/interfaces/multipliers/IMultiplier.sol";

/// @title NFT Multiplier Interface
/// @notice Interface for contracts that provide multiplying factors based on NFT ownership
/// @dev Extends the IMultiplier interface with NFT-specific functionality
interface INFTMultiplier is IMultiplier {
/// @notice Get the address of the NFT contract
/// @return The address of the NFT contract used for checking ownership
function NFT_ADDRESS() external view returns (IERC721);

/// @notice Check if a _user owns an NFT
/// @param _user The address of the _user to check
/// @return True if the _user owns at least one NFT, false otherwise
function hasNFT(address _user) external view returns (bool);
}
12 changes: 12 additions & 0 deletions src/interfaces/multipliers/IOffChainProveableMultiplier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {IProveableMultiplier} from "src/interfaces/multipliers/IProveableMultiplier.sol";

/// @title Off-Chain Proveable Multiplier Interface
/// @notice Interface for contracts that provide an off-chain proveable multiplying factor
interface IOffChainProveableMultiplier is IProveableMultiplier {
/// @notice Get the address of the pull oracle
/// @return The address of the oracle used for off-chain data verification
function oracle() external view returns (address);
}
11 changes: 11 additions & 0 deletions src/interfaces/multipliers/IOnChainProveableMultiplier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {IProveableMultiplier} from "src/interfaces/multipliers/IProveableMultiplier.sol";
/// @title On-Chain Proveable Multiplier Interface
/// @notice Interface for contracts that provide an on-chain proveable multiplying factor
interface IOnChainProveableMultiplier is IProveableMultiplier {
/// @notice Get the address of the activity contract
/// @return The address of the contract used for verifying on-chain activities
function activityContract() external view returns (address);
}
12 changes: 12 additions & 0 deletions src/interfaces/multipliers/IProveableMultiplier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IDynamicNFTMultiplier} from "src/interfaces/multipliers/IDynamicNFTMultiplier.sol";
/// @title Proveable Multiplier Interface
/// @notice Interface for contracts that provide a proveable multiplying factor based on _user activities
interface IProveableMultiplier is IERC721, IDynamicNFTMultiplier {
/// @notice Submit activities to potentially earn or upgrade an NFT
/// @param data Encoded data representing the activities
function submitActivities(bytes calldata data) external;
}
Loading

0 comments on commit 4da5fa7

Please sign in to comment.