Skip to content

Implement VotingStreakMultiplier #129

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

Merged
merged 38 commits into from
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
526986a
Add Voting streak multiplier contract
Sep 23, 2024
9e57b1b
Make VotingStreakMultiplier an IMultiplier
Mar 20, 2025
0ee81a2
Update YieldDistributor to call voting streak multiplier `onVoteCase`
Mar 20, 2025
7f56c11
check if the multiplier is valid
Apr 24, 2025
e74b804
update comment
Apr 24, 2025
4250340
add internal function
Apr 24, 2025
9042f17
upgradeable contract wont call constructor
Apr 24, 2025
bc484ca
votingstreakmultiplier is not an erc721 contract
Apr 24, 2025
7813141
Add update function to voting multipliers; remove code from YD
Apr 27, 2025
9a2a440
cleanup and i guess we dont need ivotingstreakmultiplier
Apr 27, 2025
68f1efc
f
Apr 27, 2025
32eab1c
f
Apr 27, 2025
cad7e3d
add natspec and update mock multiplier
Apr 28, 2025
3a761f1
add no-op updateMultiplierFactor functions
Apr 28, 2025
7fb1e7e
pass user to updateMultiplyingFactor
May 7, 2025
b1def3b
make compiler happy
May 7, 2025
08f183d
add basic multiplier streak test
bagelface May 13, 2025
6babba8
rough but passing tests
May 20, 2025
5217f57
test cleanup
May 21, 2025
1717f2e
remove extra events
May 21, 2025
48508e5
f
May 21, 2025
c6cd5a5
remove unused function
May 23, 2025
fe64fc6
rewrite with fuzz test
May 23, 2025
5376053
move tests to yd existing suite
May 23, 2025
3c86cca
upgrade check
May 26, 2025
8e70f68
Revert "upgrade check"
May 27, 2025
f470adf
redo upgrade safety check; update readme for clarity
May 27, 2025
5d67dc3
pr review feedback
May 31, 2025
097e147
total multiplier should start at base 100% ; individual multipliers s…
May 31, 2025
b1e3882
resolve compiler warning
May 31, 2025
fd16164
rename to max multiplier increments
May 31, 2025
c63332b
be clear about what value multiplierIncrement should be
May 31, 2025
a41ce0f
make voting boost multipliers like 102%
Jun 1, 2025
d95dbac
all working with separate multiplying factor and multiplier increment
Jun 1, 2025
4961315
remove separate multiplying factor, use only increment
Jun 2, 2025
b6c6182
rename function to calculateVotingMultipliers
Jun 4, 2025
7e7d15b
pr review feedback
Jun 6, 2025
f17990d
move base multiplier to a constants file
Jun 6, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ cache
out
broadcast
.env
.DS_Store
46 changes: 31 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Breadchain

Breadchain smart contracts power [Breadchain's governance application](https://app.breadchain.xyz/governance).

To learn more check out the [Breadchain wiki](https://breadchain.notion.site/4d496b311b984bd9841ef9c192b9c1c7).

## Contributing

Join in on the conversation in our [Discord](https://discord.com/invite/zmNqsHRHDa).

If you have skills (both technical and non-technical) that you believe would benefit our mission, you can fill out [this Google Form](https://forms.gle/UU4FmHq4CZbiEKPc6). Expect to hear from a member of our team shortly regarding any potential opportunities for collaboration.

### Style Guide

Contributions to this repo are expected to adhere to the [Biconomy Solidity Style Guide](https://github.com/bcnmy/biconomy-solidity-style-guide).

## Usage
Expand All @@ -31,28 +34,41 @@ $ forge fmt
$ forge snapshot
```

### Test
### Test

```shell
```shell
$ forge test --fork-url "https://rpc.gnosis.gateway.fm" -vvvv
```

### Deploy

```shell
forge script script/deploy/DeployYieldDistributor.s.sol:DeployYieldDistributor --rpc-url "https://rpc.gnosis.gateway.fm" --broadcast --private-key <pk>
```

## Upgrading
### Validate Upgrade Safety
1. Checkout to the deployed implementation commit
2. Copy "YieldDistributor.sol" to `test/upgrades/<version>/YieldDistributor.sol`
3. Checkout to upgrade candidate version (A version that is strictly higher than the version in the previous step)
4. Update the version in the options object of the `script/upgrades/ValidateUpgrade.s.sol` script
5. Run `forge clean && forge build && forge script script/upgrades/ValidateUpgrade.s.sol`
6. If script is runs successfully, proceed, otherwise address errors produced by the script until no errors are produced.

### Test Upgrade with Calldata Locally
1. Amend the `data` variable in `script/upgrades/UpgradeYieldDistributor.s.sol` to match desired data
2. run `forge clean && forge build && forge script script/upgrades/UpgradeYieldDistributor.s.sol --sig "run(address)" <proxy_address> --rpc-url $RPC_URL --sender <proxy_admin>`

The proxy admin address is configured to be the Breadchain multisig at address `0x918dEf5d593F46735f74F9E2B280Fe51AF3A99ad` and the Yield Distributor proxy address is `0xeE95A62b749d8a2520E0128D9b3aCa241269024b`

### Validate Upgrade Safety

Before upgrading to a new version of our deployed contracts, it is necessary to run the upgrade safety validation check. This ensures that upgrading won't break existing functionality or corrupt the contract's state.

1. Checkout the deployed implementation commit (usually the `dev` branch)
2. Determine the new version targeted by the upgrade; it should be strictly higher than the latest deployed version [tag](https://github.com/BreadchainCoop/breadchain/tags)
3. Flatten YieldDistributor and ButteredBread (this will output a single .sol file with all dependencies inlined for comparison by the upgrade script):

```
forge flatten src/YieldDistributor.sol > test/upgrades/<new_version>/YieldDistributor.sol
forge flatten src/ButteredBread.sol > test/upgrades/<new_version>/ButteredBread.sol
```

4. Checkout the upgrade branch
5. Update the version in the options object of the `script/upgrades/ValidateUpgrade.s.sol` script
6. Run `forge clean && forge build && forge script script/upgrades/ValidateUpgrade.s.sol`
7. If script is runs successfully, proceed, otherwise address errors produced by the script until no errors are produced.

### Test Upgrade with Calldata Locally

1. Amend the `data` variable in `script/upgrades/UpgradeYieldDistributor.s.sol` to match desired data
2. run `forge clean && forge build && forge script script/upgrades/UpgradeYieldDistributor.s.sol --sig "run(address)" <proxy_address> --rpc-url $RPC_URL --sender <proxy_admin>`

The proxy admin address is configured to be the Breadchain multisig at address `0x918dEf5d593F46735f74F9E2B280Fe51AF3A99ad` and the Yield Distributor proxy address is `0xeE95A62b749d8a2520E0128D9b3aCa241269024b`
2 changes: 1 addition & 1 deletion script/upgrades/ValidateUpgrade.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ contract DeployYieldDistributor is Script {
function run() external {
vm.startBroadcast();
Options memory opts;
opts.referenceContract = "v1.0.1/YieldDistributor.sol:YieldDistributor";
opts.referenceContract = "v1.0.4/YieldDistributor.sol:YieldDistributor";
Upgrades.validateUpgrade("YieldDistributor.sol:YieldDistributor", opts);
vm.stopBroadcast();
}
Expand Down
30 changes: 23 additions & 7 deletions src/VotingMultipliers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.22;

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

/// @title VotingMultipliers
/// @notice A contract for managing voting multipliers
Expand All @@ -11,6 +12,11 @@ contract VotingMultipliers is Ownable2StepUpgradeable, IVotingMultipliers {
/// @notice Array of allowlisted multiplier contracts
IMultiplier[] public allowlistedMultipliers;

/// @notice Initializes the contract
function initialize() public initializer {
__Ownable_init(msg.sender);
}

/// @notice Adds a multiplier to the allowlist
/// @param _multiplier The multiplier contract to be added
function addMultiplier(IMultiplier _multiplier) external onlyOwner {
Expand Down Expand Up @@ -68,11 +74,12 @@ contract VotingMultipliers is Ownable2StepUpgradeable, IVotingMultipliers {
}

/// @notice Calculates the total multiplier for a given user using specific multiplier indexes
/// @notice Performs the updateMultiplyingFactor function for each multiplier to ensure the multiplier is up to date
/// @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;
function calculateTotalMultipliers(address _user, uint256[] calldata _multiplierIndexes) public returns (uint256) {
uint256 _totalMultiplier = MultiplierConstants.BASE_MULTIPLIER;

for (uint256 i = 0; i < _multiplierIndexes.length; i++) {
uint256 index = _multiplierIndexes[i];
Expand All @@ -81,25 +88,34 @@ contract VotingMultipliers is Ownable2StepUpgradeable, IVotingMultipliers {
}

IMultiplier multiplier = allowlistedMultipliers[index];
multiplier.updateMultiplyingFactor(_user);
if (block.number <= multiplier.validUntil(_user)) {
_totalMultiplier += multiplier.getMultiplyingFactor(_user);
uint256 factor = multiplier.getMultiplyingFactor(_user);
if (factor > MultiplierConstants.BASE_MULTIPLIER) {
// Add only the bonus amount to the total
_totalMultiplier += (factor - MultiplierConstants.BASE_MULTIPLIER);
}
}
}
return _totalMultiplier;
return Math.max(_totalMultiplier, MultiplierConstants.BASE_MULTIPLIER);
}

/// @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;
uint256 _totalMultiplier = MultiplierConstants.BASE_MULTIPLIER;
for (uint256 i = 0; i < allowlistedMultipliers.length; i++) {
IMultiplier multiplier = allowlistedMultipliers[i];
if (block.number <= multiplier.validUntil(_user)) {
_totalMultiplier += multiplier.getMultiplyingFactor(_user);
uint256 factor = multiplier.getMultiplyingFactor(_user);
if (factor > MultiplierConstants.BASE_MULTIPLIER) {
// Add only the bonus amount to the total
_totalMultiplier += (factor - MultiplierConstants.BASE_MULTIPLIER);
}
}
}
return _totalMultiplier;
return Math.max(_totalMultiplier, MultiplierConstants.BASE_MULTIPLIER);
}
}
8 changes: 4 additions & 4 deletions src/YieldDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ contract YieldDistributor is IYieldDistributor, Ownable2StepUpgradeable, VotingM
address[] public queuedProjectsForAddition;
/// @notice Array of projects queued for removal from the next cycle
address[] public queuedProjectsForRemoval;
/// @notice The voting power allocated to projects by voters in the current cycle
/// @notice The voting power allocated to each project by voters in the current cycle
uint256[] public projectDistributions;
/// @notice The last block number in which a specified account cast a vote
mapping(address => uint256) public accountLastVoted;
/// @notice The voting power allocated to projects by voters in the current cycle
/// @notice The voting power allocated to each project by a specific voter in the current cycle
mapping(address => uint256[]) voterDistributions;
/// @notice How much of the yield is divided equally among projects
uint256 public yieldFixedSplitDivisor;
Expand All @@ -72,7 +72,7 @@ contract YieldDistributor is IYieldDistributor, Ownable2StepUpgradeable, VotingM
uint256 _lastClaimedBlockNumber,
address[] memory _projects
) public initializer {
__Ownable_init(msg.sender);
VotingMultipliers.initialize();
if (
_bread == address(0) || _butteredBread == address(0) || _precision == 0 || _minRequiredVotingPower == 0
|| _maxPoints == 0 || _cycleLength == 0 || _yieldFixedSplitDivisor == 0 || _lastClaimedBlockNumber == 0
Expand Down Expand Up @@ -236,7 +236,7 @@ contract YieldDistributor is IYieldDistributor, Ownable2StepUpgradeable, VotingM
*/
function castVoteWithMultipliers(uint256[] calldata _points, uint256[] calldata _multiplierIndices) public {
uint256 _currentVotingPower = getCurrentVotingPower(msg.sender);
uint256 multiplier = getTotalMultipliers(msg.sender, _multiplierIndices);
uint256 multiplier = calculateTotalMultipliers(msg.sender, _multiplierIndices);
_currentVotingPower = multiplier == 0 ? _currentVotingPower : (_currentVotingPower * multiplier) / PRECISION;
if (_currentVotingPower < minRequiredVotingPower) revert BelowMinRequiredVotingPower();
_castVote(msg.sender, _points, _currentVotingPower);
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/IVotingMultipliers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ interface IVotingMultipliers {
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
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/multipliers/IDynamicNFTMultiplier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ 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);
function _userToValidUntil(address _user) external view returns (uint256);
}
8 changes: 7 additions & 1 deletion src/interfaces/multipliers/IMultiplier.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {MultiplierConstants} from "src/libraries/MultiplierConstants.sol";

interface IMultiplier {
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually might be a good idea to just add BASE_MULTIPLIER constant here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i had this instinct as well, but found that solidity prevents variables from being set in interfaces. cursor suggested i create a library directory and define it, like MultiplierConstants, and import it from there. Do you think that's a good solution ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

did this in a separate commit so its clearer f17990d - wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

oh weird I didnt know that. I do think this makes things a little easier to work with so that the value doesn't need to be explicitly defined each time

/// @notice Returns the voting multiplier for `_user`.
/// @notice Updates the multiplying factor for a specific user
/// @param _user The address of the user to update the multiplying factor for
function updateMultiplyingFactor(address _user) external;

/// @notice Returns the multiplying factor for `_user`.
function getMultiplyingFactor(address _user) external view returns (uint256);

/// @notice Returns the validity period of the multiplier for `_user`.
Expand Down
7 changes: 7 additions & 0 deletions src/libraries/MultiplierConstants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

library MultiplierConstants {
/// @notice The base multiplier value (100% in fixed-point representation)
uint256 public constant BASE_MULTIPLIER = 1e18;
}
7 changes: 3 additions & 4 deletions src/multipliers/NFTMultiplier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@ contract NFTMultiplier is INFTMultiplier, Initializable, Ownable2StepUpgradeable
return validUntilBlock;
}

/// @notice Update the multiplying factor
/// @param _newMultiplyingFactor New multiplying factor (in basis points)
function updateMultiplyingFactor(uint256 _newMultiplyingFactor) external onlyOwner {
multiplyingFactor = _newMultiplyingFactor;
/// @notice Updates the multiplying factor for a specific user
function updateMultiplyingFactor(address /* _user */ ) external pure {
return;
}

/// @notice Update the valid until block
Expand Down
5 changes: 5 additions & 0 deletions src/multipliers/PermanentNFTMultiplier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ contract PermanentNFTMultiplier is INFTMultiplier {
function hasNFT(address _user) public view override returns (bool) {
return NFT_ADDRESS.balanceOf(_user) > 0;
}

/// @notice Updates the multiplying factor for a specific user
function updateMultiplyingFactor(address /* _user */ ) external pure override {
return;
}
}
Loading