diff --git a/.gitmodules b/.gitmodules index e2c1eca..254c1db 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,7 +9,7 @@ url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable [submodule "lib/bread-token-v2"] path = lib/bread-token-v2 - url = git@github.com:BreadchainCoop/bread-token-v2.git + url = https://github.com/BreadchainCoop/bread-token-v2.git [submodule "lib/openzeppelin-foundry-upgrades"] path = lib/openzeppelin-foundry-upgrades url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades diff --git a/README.md b/README.md index 5ab48d6..e5234bf 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ Contributions to this repo are expected to adhere to the [Biconomy Solidity Styl ## Usage +### Clone +```shell +$ git clone git@github.com:BreadchainCoop/breadchain.git --recursive +``` ### Build ```shell @@ -32,10 +36,18 @@ $ forge snapshot ``` ### Test +For validating that tests are working run + +```shell +$ forge test --fork-url "https://rpc.gnosis.gateway.fm" -vvvv --fuzz-runs 1 +``` + +For a full run of tests run the following command. Note that the command may take upwards of 8 minutes to execute. ```shell $ forge test --fork-url "https://rpc.gnosis.gateway.fm" -vvvv ``` + ### Deploy ```shell @@ -55,4 +67,7 @@ forge script script/deploy/DeployYieldDistributor.s.sol:DeployYieldDistributor - 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)" --rpc-url $RPC_URL --sender ` -The proxy admin address is configured to be the Breadchain multisig at address `0x918dEf5d593F46735f74F9E2B280Fe51AF3A99ad` and the Yield Distributor proxy address is `0xeE95A62b749d8a2520E0128D9b3aCa241269024b` \ No newline at end of file +The proxy admin address is configured to be the Breadchain multisig at address `0x918dEf5d593F46735f74F9E2B280Fe51AF3A99ad` and the Yield Distributor proxy address is `0xeE95A62b749d8a2520E0128D9b3aCa241269024b` + + +The development of this project was enabled by a [grant](https://gov.powerpool.finance/t/approved-grant-for-breadchain-cooperative-integrations/2007) from [Powerpool](https://powerpool.finance/). \ No newline at end of file diff --git a/docs/Bread.md b/docs/Bread.md new file mode 100644 index 0000000..17f38aa --- /dev/null +++ b/docs/Bread.md @@ -0,0 +1,89 @@ +## What is BREAD? + +BREAD is the community currency for the Breadchain ecosystem which exists on Gnosis Chain. All BREAD is created through the [Bread Crowdstaking Application](https://www.notion.so/Crowdstaking-Application-9f233bc2fb1e419ebeb58a2809b21658?pvs=21) which anyone with xDAI on Gnosis Chain is able to use to have some BREAD for themselves. + +```mermaid +classDiagram +class Bread { + <> ERC20VotesUpgradeable + <> OwnableUpgradeable + <> IBread + + <> SafeERC20 for IERC20 + + + address yieldClaimer + + IWXDAI wxDai + + ISXDAI sexyDai + + + __constructor__() + + initialize() + + setYieldClaimer() + + mint() šŸ’° + + burn() + + claimYield() + + rescueToken() + + yieldAccrued() šŸ” + # _yieldAccrued() šŸ” + # _nativeTransfer() + + transfer() + + transferFrom() +} + +Bread <|-- ERC20VotesUpgradeable : Inheritance +Bread <|-- OwnableUpgradeable : Inheritance +Bread <|-- IBread : Inheritance +Bread .. SafeERC20 : uses IERC20 +``` + +The Crowdstaking Application is a smart contract on Gnosis Chain that accepts a userā€™s xDAI and turns it into sDAI. In exchange, stakers receive BREAD tokens, minted at a 1-to-1 ratio with the collateralized xDAI. + +All of the interest earned on the sDAI is helps fund the collective and its various member projects based on a monthly vote from BREAD holders. The Crowdstaking Application functions as a fundraising engine for the Breadchain Cooperative, while the BREAD token acts as a local currency within the ecosystem, promoting financial sustainability. + +Additionally, BREAD holders are able to to vote on how the yield generated from the sDAI is distributed among the projects part of the Breadchain Network every month. + +[Gnosis Chain Deployment](https://gnosisscan.io/token/0xa555d5344f6fb6c65da19e403cb4c1ec4a1a5ee3) + +## Technical Breakdown +### Minting +```solidity +1 function mint(address receiver) external payable { +2 // ... validation snippets +3 wxDai.deposit{value: val}(); +4 IERC20(address(wxDai)).safeIncreaseAllowance(address(sexyDai), val); +5 sexyDai.deposit(val, address(this)); +6 +7 _mint(receiver, val); +8 +9 // ... delegation snippet +10 } +``` +On line 3 the native currency (xDai) of Gnosis Chain gets converted to an ERC20 representation ([wxDai](https://gnosisscan.io/address/0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d)). This is done because in order to lock the xDai. + +[sDai](https://gnosisscan.io/address/0xaf204776c7245bF4147c2612BF6e5972Ee483701) is an automatic mechanism that allows a user to recieve yield from the [Dai Savings Rate](https://blog.makerdao.com/why-the-dai-savings-rate-is-a-game-changer-for-the-defi-ecosystem-and-beyond/) by depositing and locking wxDai. This is how BREAD generates yield. +On line 4 , the Bread contract allows the [sDai contract](https://gnosisscan.io/address/0xaf204776c7245bF4147c2612BF6e5972Ee483701) to take xDai from itself. + +On line 5 , the wxDai is deposited and turned into sDai, which is in possession by the BREAD contract. + +On line 7 , BREAD is minted to the reciever as a voucher for their deposit. This enables the reciever to redeem their BREAD for the amount of xDai that was deposited. + +Lets look at another snippet from the BREAD contract , which is used to calculate how much yield is available for the Breadchain federation. + +```solidity +1 function _yieldAccrued() internal view returns (uint256) { +2 uint256 bal = IERC20(address(sexyDai)).balanceOf(address(this)); +3 uint256 assets = sexyDai.convertToAssets(bal); +4 uint256 supply = totalSupply(); +5 return assets > supply ? assets - supply : 0; +6 } +``` +On line 2 , the `bal` variable represents the sDai balance of the Bread contract. This represents how much xDai is locked and earning yield. + +In line 3 use the `bal` variable to determine how much xDai the Bread contract is eligible for by burning sDai, and we store that in the `assets` variable. This represents the original xDai locked and any rewards earned. + +On line 4 we determine how much BREAD is in circulation, and store that in the `supply` variable. This variable represents how much xDai the BREAD contract "owes" to BREAD holders, as they may redeem their BREAD for xDai at a 1:1 ratio. + +To understand how much yield the Bread contract has , we first ascertain that by burning all sDai we have enough xDai for BREAD redemptions. While the state is unreachable, this validation is present for safety reasons. Once we are certain of that , the `assets - supply` subtraction in line 5 represents the **total liquid xDai available for claim to the Bread contract** minus **the total xDai "owed" to BREAD holders**. Thus, what is left over is what can be allocated to the Breadchain projects. + +Click [here](https://docs.soliditylang.org/en/v0.8.27/types.html#ternary-operator) for a reference on the ternary operator that is used here. + + diff --git a/docs/Breadchain.md b/docs/Breadchain.md new file mode 100644 index 0000000..12bee57 --- /dev/null +++ b/docs/Breadchain.md @@ -0,0 +1,14 @@ +## About Breadchain Cooperative +Breadchain Cooperative is a **collective federation of decentralized cooperative projects** looking to advance a **progressive vision for blockchain** and its effect on society. We aim to do this by building and utilizing what we call *solidarity primitives* - development tools which help to forge solidarity between individuals and collectives. + +The first *solidarity primitive* created by the Breadchain Cooperative is the **BREAD** community token created through theĀ [Bread Crowdstaking Application](https://app.breadchain.xyz/).Ā  + +The primary infrastructure being built at Breadchain is to ask ourselvesĀ ***what if progressives had their own [community currency](https://www.notion.so/What-is-BREAD-f3335ccc7b5142bca578c6d2f2b563d3?pvs=21) and [credit union](https://www.notion.so/Yield-Governance-d3ac44ac679c4756a18e27e5ec0696d5?pvs=21)? Where would we decide to put resources towards to build a post-capitalist political economy?*** + +## BREAD as a Community Currency +### Consistent value +BREAD is linked to xDAI which is a stablecoin with equal value to USD. So $1 = 1 $BREAD. +### Built on solidarity +A solidarity primitive is a building block for solidarity through code. Build with $BREAD to have a tech stack with values. +### Fund the future +Earnings from the minting of BREAD go to supporting a co-operative of [post-capitalist web3 projects](https://breadchain.notion.site/e92f4f3dfb64402aada35a90bf2712af). \ No newline at end of file diff --git a/docs/ButteredBread.md b/docs/ButteredBread.md new file mode 100644 index 0000000..d2db6b2 --- /dev/null +++ b/docs/ButteredBread.md @@ -0,0 +1,97 @@ +```mermaid +classDiagram + class ButteredBread { + <> IButteredBread + <> ERC20VotesUpgradeable + <> OwnableUpgradeable + + +static uint256 FIXED_POINT_PERCENT + +IERC20Votes bread + +mapping(address => bool) allowlistedLPs + +mapping(address => uint256) scalingFactors + #mapping(address => mapping(address => LPData)) _accountToLPData + + +__constructor__() + +initialize() + +accountToLPBalance() + +syncDelegation() + +deposit() + +withdraw() + +modifyAllowList() + +modifyScalingFactor() + +transfer() + +transferFrom() + +delegate() + #_deposit() + #_withdraw() + #_modifyScalingFactor() + #_syncDelegation() + #_syncVotingWeight() + } + + ButteredBread --|> IButteredBread : Inherits + ButteredBread --|> ERC20VotesUpgradeable : Inherits + ButteredBread --|> OwnableUpgradeable : Inherits +``` +# Buttered Bread + +The ButteredBread contract is designed to enhance liquidity provision and governance participation within the Breadchain ecosystem. At its core, it allows users to deposit Liquidity Pool (LP) tokens, referred to as "Butter," and in return, mint ButteredBread tokens. These ButteredBread tokens represent a scaled version of the deposited LP tokens, with the scaling factor determined for each supported liquidity pool. This mechanism incentivizes users to provide liquidity to specific pools by offering voting power. + +A key feature of the ButteredBread contract is its integration with the BREAD token's governance system. While ButteredBread tokens themselves are non-transferable, they inherit the voting power and delegation mechanics of the underlying BREAD token. This is achieved through a unique synchronization process where the ButteredBread contract mirrors the delegation choices made by users in the BREAD token contract. Additionally, the contract allows for dynamic adjustment of scaling factors, enabling the protocol to fine-tune incentives for different liquidity pools over time. + +## Deposits + +```mermaid +sequenceDiagram + actor User + participant BB as ButteredBread + participant LP as Liquidity Pool Token + + BB->>LP: deposit(amount) + BB->>LP: approve(address) approve BB to take tokens + User->>BB: deposit(lp, amount) + activate BB + BB->>BB: Check if LP is allowlisted + BB->>LP: transferFrom(user, this, amount) + BB->>BB: Update user's LP balance + BB->>BB: Calculate ButteredBread to mint + BB->>BB: Mint ButteredBread tokens + BB->>BB: Sync delegation + BB-->>User: Deposit complete + deactivate BB +``` +1. User calls deposit function with LP address and amount. +2. The contract checks if the LP is allowlisted. +3. It transfers LP tokens from the user to the contract. +4. Updates the user's LP balance in the contract's storage. +5. Calculates the amount of ButteredBread tokens to mint based on the scaling factor. +6. Mints the calculated amount of ButteredBread tokens to the user. +7. Syncs the user's delegation based on their delegation in the $BREAD contract + +## Withdrawals +```mermaid +sequenceDiagram + actor User + participant BB as ButteredBread + participant LP as Liquidity Pool Token + + User->>BB: withdraw(lp, amount) + activate BB + BB->>BB: Check if LP is allowlisted + BB->>BB: Check if user has sufficient balance + BB->>BB: Sync delegation + BB->>BB: Sync voting weight + BB->>BB: Update user's LP balance + BB->>BB: Calculate ButteredBread to burn + BB->>BB: Burn ButteredBread tokens + BB->>LP: transfer(user, amount) + BB-->>User: Withdrawal complete + deactivate BB +``` + +1. User calls `withdraw` function with LP address and amount. +2. The contract checks if the LP is allowlisted. +3. It checks if the user has sufficient balance to withdraw. +6. Updates the user's LP balance in the contract's storage. +8. Burns the calculated amount of ButteredBread tokens from the user. +9. Transfers the LP tokens back to the user. \ No newline at end of file diff --git a/docs/YieldDistributor.md b/docs/YieldDistributor.md new file mode 100644 index 0000000..55b26ec --- /dev/null +++ b/docs/YieldDistributor.md @@ -0,0 +1,245 @@ +```mermaid +classDiagram + class YieldDistributor { + <> IYieldDistributor + <> OwnableUpgradeable + + +Bread BREAD + +uint256 PRECISION + +uint256 cycleLength + +uint256 maxPoints + +uint256 minRequiredVotingPower + +uint256 lastClaimedBlockNumber + +uint256 currentVotes + +address projects + +address queuedProjectsForAddition + +address queuedProjectsForRemoval + +uint256 projectDistributions + +mapping(address => uint256) accountLastVoted + #mapping(address => null) voterDistributions + +uint256 yieldFixedSplitDivisor + +ERC20VotesUpgradeable BUTTERED_BREAD + + +__constructor__() + +initialize() + +getCurrentVotingDistribution() + +getCurrentVotingPower() + +getVotingPowerForPeriod() + +resolveYieldDistribution() + +distributeYield() + +castVote() + #_castVote() + #_updateBreadchainProjects() + +queueProjectAddition() + +queueProjectRemoval() + +setMinRequiredVotingPower() + +setMaxPoints() + +setCycleLength() + +setyieldFixedSplitDivisor() + +setButteredBread() + } + + YieldDistributor --|> IYieldDistributor : Inherits + YieldDistributor --|> OwnableUpgradeable : Inherits + +``` + +# YieldDistributor +The YieldDistributor contract is a smart contract designed to manage and distribute yield (in the form of $BREAD tokens) to eligible member projects within the Breadchain ecosystem. This contract implements a voting mechanism that allows token holders to influence the distribution of yield across various projects. By leveraging both $BREAD and $BUTTERED_BREAD tokens, the system creates a dual-token voting power structure, encouraging active participation and long-term commitment from community members. + +At its core, the YieldDistributor contract enables a democratic and transparent process for allocating resources within the Breadchain network. It features a cyclical distribution system, where token holders can cast votes using their voting power, which is calculated based on their token holdings over time. The contract supports dynamic project management, allowing for the addition and removal of eligible projects, and incorporates both fixed and voted components in the yield distribution to ensure a balance between equity and direct democracy. + +# Voting +The `castVote` function allows users to participate in the yield distribution process by allocating their voting power to different projects. Here's a detailed explanation: + +```mermaid +sequenceDiagram + actor User + participant YD as YieldDistributor + participant BREAD as BREAD Token + participant BB as BUTTERED_BREAD Token + + User->>YD: castVote(_points) + activate YD + YD->>YD: getCurrentVotingPower(msg.sender) + activate YD + YD->>YD: getVotingPowerForPeriod(BREAD, ...) + YD->>BREAD: numCheckpoints(account) + YD->>BREAD: checkpoints(account, index) + YD->>YD: getVotingPowerForPeriod(BUTTERED_BREAD, ...) + YD->>BB: numCheckpoints(account) + YD->>BB: checkpoints(account, index) + deactivate YD + + alt _currentVotingPower < minRequiredVotingPower + YD-->>User: revert BelowMinRequiredVotingPower + else _currentVotingPower >= minRequiredVotingPower + YD->>YD: _castVote(msg.sender, _points, _currentVotingPower) + end + deactivate YD +``` + + +1. The function takes an array of `_points` as input, representing the user's vote allocation for each project. These points allow for a dynamic and flexible voting system: + + - Each project can be assigned a number of points, up to `maxPoints`. + - The total number of points allocated across all projects can vary. + - The actual voting power distributed to each project is calculated proportionally based on the points allocated. + - This system allows users to express their preferences with fine-grained control, as they can allocate any number of points (up to `maxPoints`) to each project. + - For example, if there are two projects and `maxPoints` is 100, a user could vote [100, 50], [1, 2], or any other combination, providing a wide range of possible distributions within the precision of the points system. + +2. It first calculates the user's current voting power using `getCurrentVotingPower`. + +3. It checks if the user has sufficient voting power to participate. If not, it reverts. + +4. If the user has enough voting power, it calls the internal `_castVote` function. + +The internal `_castVote` function does the heavy lifting: + +```mermaid +sequenceDiagram + participant YD as YieldDistributor + participant Storage as Contract Storage + + activate YD + YD->>YD: Check _points.length == projects.length + YD->>YD: Calculate _totalPoints + YD->>YD: Check each point <= maxPoints + YD->>YD: Check _totalPoints > 0 + + YD->>Storage: Read accountLastVoted[_account] + YD->>Storage: Read lastClaimedBlockNumber + YD->>YD: Calculate _hasVotedInCycle + + alt !_hasVotedInCycle + YD->>Storage: Delete voterDistributions[_account] + YD->>Storage: Update currentVotes += _votingPower + end + + loop For each project + alt !_hasVotedInCycle + YD->>Storage: Initialize _voterDistributions[i] = 0 + else + YD->>Storage: Update projectDistributions[i] + end + + YD->>YD: Calculate _currentProjectDistribution + YD->>Storage: Update projectDistributions[i] + YD->>Storage: Update _voterDistributions[i] + end + + YD->>Storage: Update accountLastVoted[_account] + YD->>YD: Emit BreadHolderVoted event + deactivate YD +``` + +Here's what this function does: + +1. It checks if the number of points matches the number of projects. + +2. It calculates the total points and ensures they don't exceed the maximum allowed per project. + +3. It checks if the user has already voted in this cycle. + +4. If it's a new vote in the cycle, it resets the user's previous votes and adds their voting power to the current total votes. + +5. For each project: + + - If it's a new vote, it initializes the user's distribution for that project. + + - If it's an update to an existing vote, it subtracts the user's previous distribution. + + - It calculates the new distribution based on the points allocated and the user's voting power. + + - It updates both the overall project distribution and the user's personal distribution. + +6. It updates the last voted block number for the user. + +7. Finally, it emits an event with the voting details. + + +Key points: +- The function allows users to update their votes within a cycle. +- It uses precision calculations to ensure accurate distribution of voting power. +- It maintains both global project distributions and individual user distributions. +- The voting power is based on the user's token holdings (both BREAD and BUTTERED_BREAD) over a specific period. + +## Voting power +```mermaid +sequenceDiagram + actor User + participant YD as YieldDistributor + participant BREAD as BREAD Token + participant BB as BUTTERED_BREAD Token + + User->>YD: getCurrentVotingPower(account) + activate YD + YD->>YD: Calculate period start and end + YD->>YD: getVotingPowerForPeriod(BREAD, start, end, account) + activate YD + YD->>BREAD: numCheckpoints(account) + alt numCheckpoints == 0 + YD-->>YD: Return 0 + else numCheckpoints > 0 + YD->>BREAD: checkpoints(account, 0) + alt first checkpoint > end + YD-->>YD: Return 0 + else first checkpoint <= end + loop Find latest checkpoint within interval + YD->>BREAD: checkpoints(account, index) + end + YD->>YD: Calculate initial voting power + loop Process remaining checkpoints + YD->>BREAD: checkpoints(account, index) + YD->>YD: Update total voting power + alt checkpoint <= start + YD->>YD: Adjust for interval start + YD-->>YD: Break loop + end + end + end + end + YD-->>YD: Return BREAD voting power + deactivate YD + + YD->>YD: getVotingPowerForPeriod(BUTTERED_BREAD, start, end, account) + activate YD + Note over YD: Same process as BREAD + YD-->>YD: Return BUTTERED_BREAD voting power + deactivate YD + + YD->>YD: Sum BREAD and BUTTERED_BREAD voting power + YD-->>User: Return total voting power + deactivate YD +``` +Now, let's break down the getVotingPowerForPeriod function step by step: +1. Input Validation: + - Check if the start time is before the end time. + - Ensure the end time is not after the current block. +2. Initial Checkpoint Check: + - Get the total number of checkpoints for the account. + - If there are no checkpoints, return 0 voting power. +3. Boundary Checks: + - If the first checkpoint is after the end of the interval, return 0 voting power. +4. Find Relevant Checkpoints: + - Start from the latest checkpoint and move backwards. + - Find the most recent checkpoint that is within or before the end of the interval. +5. Initialize Voting Power Calculation: + - Set the initial voting power based on the latest relevant checkpoint. + - Calculate the duration from this checkpoint to the end of the interval. +6. Process Earlier Checkpoints: + - Iterate through earlier checkpoints, moving backwards in time. + - For each checkpoint: + - Calculate the voting power for the sub-interval between checkpoints. + - Add this to the total voting power. + - If the checkpoint is before or at the start of the interval: + - Adjust the voting power to exclude time before the interval start. + - Break the loop as we've covered the entire interval. +7. Return Total Voting Power: + - The function returns the accumulated voting power over the specified interval. +Key Points: + - The function uses a checkpoint system to track voting power changes over time. + - It calculates voting power as a product of token balance and time held within the specified interval. + - The calculation is done separately for both BREAD and BUTTERED_BREAD tokens, then summed for the total voting power. + - This method allows for accurate representation of voting power even with balance changes during the period. + - This approach ensures that voting power is proportional to both the amount of tokens held and the duration of holding within the specified period, promoting long-term engagement and preventing last-minute large token transfers from disproportionately influencing votes. \ No newline at end of file diff --git a/src/YieldDistributor.sol b/src/YieldDistributor.sol index 8128b57..9071064 100644 --- a/src/YieldDistributor.sol +++ b/src/YieldDistributor.sol @@ -195,7 +195,7 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable { } /** - * @notice Distribute $BREAD yield to projects based on cast votes + * @notice Distribute $BREAD yield to projects based on cast votes, may leave some dust */ function distributeYield() public { (bool _resolved,) = resolveYieldDistribution(); @@ -236,7 +236,7 @@ contract YieldDistributor is IYieldDistributor, OwnableUpgradeable { /** * @notice Internal function for casting votes for a specified user * @param _account Address of user to cast votes for - * @param _points Basis points for calculating the amount of votes cast + * @param _points Points for calculating the amount of votes cast * @param _votingPower Amount of voting power being cast */ function _castVote(address _account, uint256[] calldata _points, uint256 _votingPower) internal {