Skip to content

Commit

Permalink
Apply style guide (#93)
Browse files Browse the repository at this point in the history
* Update README to include style guide and some details about the project

* Refactor to make more consistent with style guide and run formatter

* Rename events to be consistent with style guide

* Revert changes to IERC20Votes interface
  • Loading branch information
bagelface authored Sep 18, 2024
1 parent a658246 commit 5bbb335
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 166 deletions.
49 changes: 11 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
## Foundry
# Breadchain
Breadchain smart contracts power [Breadchain's governance application](https://app.breadchain.xyz/governance).

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
To learn more check out the [Breadchain wiki](https://breadchain.notion.site/4d496b311b984bd9841ef9c192b9c1c7).

Foundry consists of:
## Contributing
Join in on the conversation in our [Discord](https://discord.com/invite/zmNqsHRHDa).

- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
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.

## Documentation

https://book.getfoundry.sh/
### 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 @@ -21,12 +19,6 @@ https://book.getfoundry.sh/
$ forge build
```

### Test

```shell
$ forge test
```

### Format

```shell
Expand All @@ -39,12 +31,6 @@ $ forge fmt
$ forge snapshot
```

### Anvil

```shell
$ anvil
```

### Test

```shell
Expand All @@ -56,29 +42,16 @@ $ forge test --fork-url "https://rpc.gnosis.gateway.fm" -vvvv
forge script script/deploy/DeployYieldDistributor.s.sol:DeployYieldDistributor --rpc-url "https://rpc.gnosis.gateway.fm" --broadcast --private-key <pk>
```

### Cast

```shell
$ cast <subcommand>
```

### Help

```shell
$ forge --help
$ anvil --help
$ cast --help
```

## Validate Upgrade Safety
## 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
### 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>`

Expand Down
35 changes: 19 additions & 16 deletions src/ButteredBread.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/a
import {ERC20VotesUpgradeable} from
"openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

import {IButteredBread} from "src/interfaces/IButteredBread.sol";
import {IERC20Votes} from "src/interfaces/IERC20Votes.sol";

Expand All @@ -16,18 +17,19 @@ import {IERC20Votes} from "src/interfaces/IERC20Votes.sol";
* @custom:coauthor @daopunk
* @custom:coauthor @bagelface
*/
contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBread {
contract ButteredBread is IButteredBread, ERC20VotesUpgradeable, OwnableUpgradeable {
/// @notice Value used for calculating the precision of scaling factors
uint256 public constant FIXED_POINT_PERCENT = 100;

IERC20Votes public BREAD;

/// @notice `IERC20Votes` contract used for powering `ButteredBread` voting
IERC20Votes public bread;
/// @notice Access control for Breadchain sanctioned liquidity pools
mapping(address lp => bool allowed) public allowlistedLPs;
/// @notice How much ButteredBread should be minted for a Liquidity Pool token (Butter)
mapping(address lp => uint256 factor) public scalingFactors;
/// @notice Butter balance by account and Liquidity Pool token deposited
mapping(address account => mapping(address lp => LPData)) internal _accountToLPData;

/// @dev Applied to functions to only allow access for sanctioned liquidity pools
modifier onlyAllowed(address _lp) {
if (!allowlistedLPs[_lp]) revert NotAllowListed();
_;
Expand All @@ -38,10 +40,10 @@ contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBr
_disableInitializers();
}

/// @param _initData See IButteredBread
/// @param _initData See `IButteredBread`
function initialize(InitData calldata _initData) external initializer {
if (_initData.liquidityPools.length != _initData.scalingFactors.length) revert InvalidValue();
BREAD = IERC20Votes(_initData.breadToken);
bread = IERC20Votes(_initData.breadToken);

__Ownable_init(msg.sender);
__ERC20_init(_initData.name, _initData.symbol);
Expand All @@ -53,15 +55,16 @@ contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBr
}

/**
* @notice Return token balance of account for a specified LP
* @param _account Voting account
* @param _lp Liquidity Pool token
* @return _lpBalance Balance of LP tokens for an account by LP type
* @return _lpBalance Balance of LP tokens for an account by LP address
*/
function accountToLPBalance(address _account, address _lp) external view returns (uint256 _lpBalance) {
_lpBalance = _accountToLPData[_account][_lp].balance;
}

/// @notice Sync this delegation with user delegate selection on BREAD
/// @notice Sync this delegation with user delegate selection on $BREAD
function syncDelegation() external {
_syncDelegation(msg.sender);
}
Expand Down Expand Up @@ -91,7 +94,7 @@ contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBr
* @param _allowed Sanction status of LP token
*/
function modifyAllowList(address _lp, bool _allowed) external onlyOwner {
if (scalingFactors[_lp] < FIXED_POINT_PERCENT) revert Unset();
if (scalingFactors[_lp] == 0) revert UnsetVariable();
allowlistedLPs[_lp] = _allowed;
}

Expand All @@ -105,17 +108,17 @@ contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBr
_modifyScalingFactor(_lp, _factor, _holders);
}

/// @notice ButteredBread tokens are non-transferable
/// @notice `ButteredBread` tokens are non-transferable
function transfer(address, uint256) public virtual override returns (bool) {
revert NonTransferable();
}

/// @notice ButteredBread tokens are non-transferable
/// @notice `ButteredBread` tokens are non-transferable
function transferFrom(address, address, uint256) public virtual override returns (bool) {
revert NonTransferable();
}

/// @notice ButteredBread delegation is determined by the BREAD token
/// @notice `ButteredBread` delegation is determined by `BreadToken`
function delegate(address) public virtual override {
revert NonDelegatable();
}
Expand All @@ -131,7 +134,7 @@ contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBr
_mint(_account, _amount * currentScalingFactor / FIXED_POINT_PERCENT);
_syncDelegation(_account);

emit AddButter(_account, _lp, _amount);
emit ButterAdded(_account, _lp, _amount);
}

/// @notice Withdraw LP tokens and burn ButteredBread with corresponding LP scaling factor
Expand All @@ -146,7 +149,7 @@ contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBr
_burn(_account, _amount * scalingFactors[_lp] / FIXED_POINT_PERCENT);
IERC20(_lp).transfer(_account, _amount);

emit RemoveButter(_account, _lp, _amount);
emit ButterRemoved(_account, _lp, _amount);
}

function _modifyScalingFactor(address _lp, uint256 _factor, address[] calldata _holders) internal {
Expand All @@ -158,9 +161,9 @@ contract ButteredBread is ERC20VotesUpgradeable, OwnableUpgradeable, IButteredBr
}
}

/// @notice Sync this delegation with delegate selection on BREAD
/// @notice Sync this delegation with delegate selection on $BREAD
function _syncDelegation(address _account) internal {
_delegate(_account, BREAD.delegates(_account));
_delegate(_account, bread.delegates(_account));
if (this.delegates(_account) == address(0)) _delegate(_account, _account);
}

Expand Down
87 changes: 30 additions & 57 deletions src/YieldDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {ERC20VotesUpgradeable} from
"openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import {Bread} from "bread-token/src/Bread.sol";

import {IYieldDistributor} from "src/interfaces/IYieldDistributor.sol";

/**
* @title Breadchain Yield Distributor
* @notice Distribute $BREAD yield to eligible member projects based on a voted distribution
Expand All @@ -18,66 +20,34 @@ import {Bread} from "bread-token/src/Bread.sol";
* @custom:coauthor kassandra.eth
* @custom:coauthor theblockchainsocialist.eth
*/
contract YieldDistributor is OwnableUpgradeable {
// @notice The error emitted when attempting to add a project that is already in the `projects` array
error AlreadyMemberProject();
// @notice The error emitted when a user attempts to vote without the minimum required voting power
error BelowMinRequiredVotingPower();
// @notice The error emitted when attempting to calculate voting power for a period that has not yet ended
error EndAfterCurrentBlock();
// @notice The error emitted when attempting to vote with a point value greater than `pointsMax`
error ExceedsMaxPoints();
// @notice The error emitted when attempting to vote with an incorrect number of projects
error IncorrectNumberOfProjects();
// @notice The error emitted when attempting to instantiate a variable with a zero value
error MustBeGreaterThanZero();
// @notice The error emitted when attempting to add or remove a project that is already queued for addition or removal
error ProjectAlreadyQueued();
// @notice The error emitted when attempting to remove a project that is not in the `projects` array
error ProjectNotFound();
// @notice The error emitted when attempting to calculate voting power for a period with a start block greater than the end block
error StartMustBeBeforeEnd();
// @notice The error emitted when attempting to distribute yield when access conditions are not met
error YieldNotResolved();
// @notice The error emitted if a user with zero points attempts to cast votes
error ZeroVotePoints();

// @notice The event emitted when an account casts a vote
event BreadHolderVoted(address indexed account, uint256[] points, address[] projects);
// @notice The event emitted when a project is added as eligibile for yield distribution
event ProjectAdded(address project);
// @notice The event emitted when a project is removed as eligibile for yield distribution
event ProjectRemoved(address project);
// @notice The event emitted when yield is distributed
event YieldDistributed(uint256 yield, uint256 totalVotes, uint256[] projectDistributions);

// @notice The address of the $BREAD token contract
contract YieldDistributor is IYieldDistributor, OwnableUpgradeable {
/// @notice The address of the $BREAD token contract
Bread public BREAD;
// @notice The precision to use for calculations
/// @notice The precision to use for calculations
uint256 public PRECISION;
// @notice The minimum number of blocks between yield distributions
/// @notice The minimum number of blocks between yield distributions
uint256 public cycleLength;
// @notice The maximum number of points a voter can allocate to a project
/// @notice The maximum number of points a voter can allocate to a project
uint256 public maxPoints;
// @notice The minimum required voting power participants must have to cast a vote
/// @notice The minimum required voting power participants must have to cast a vote
uint256 public minRequiredVotingPower;
// @notice The block number of the last yield distribution
/// @notice The block number of the last yield distribution
uint256 public lastClaimedBlockNumber;
// @notice The total number of votes cast in the current cycle
/// @notice The total number of votes cast in the current cycle
uint256 public currentVotes;
// @notice Array of projects eligible for yield distribution
/// @notice Array of projects eligible for yield distribution
address[] public projects;
// @notice Array of projects queued for addition to the next cycle
/// @notice Array of projects queued for addition to the next cycle
address[] public queuedProjectsForAddition;
// @notice Array of projects queued for removal from the next cycle
/// @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 projects by voters in the current cycle
uint256[] public projectDistributions;
// @notice The last block number in which a specified account cast a vote
/// @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 projects by voters in the current cycle
mapping(address => uint256[]) voterDistributions;
// @notice How much of the yield is divided equally among projects
/// @notice How much of the yield is divided equally among projects
uint256 public yieldFixedSplitDivisor;

/// @custom:oz-upgrades-unsafe-allow constructor
Expand Down Expand Up @@ -147,34 +117,34 @@ contract YieldDistributor is OwnableUpgradeable {
if (_start >= _end) revert StartMustBeBeforeEnd();
if (_end > block.number) revert EndAfterCurrentBlock();

// Initialized as the checkpoint count, but later used to track checkpoint index
/// Initialized as the checkpoint count, but later used to track checkpoint index
uint32 _currentCheckpointIndex = _sourceContract.numCheckpoints(_account);
if (_currentCheckpointIndex == 0) return 0;

// No voting power if the first checkpoint is after the end of the interval
/// No voting power if the first checkpoint is after the end of the interval
Checkpoints.Checkpoint208 memory _currentCheckpoint = _sourceContract.checkpoints(_account, 0);
if (_currentCheckpoint._key > _end) return 0;

// Find the latest checkpoint that is within the interval
/// Find the latest checkpoint that is within the interval
do {
--_currentCheckpointIndex;
_currentCheckpoint = _sourceContract.checkpoints(_account, _currentCheckpointIndex);
} while (_currentCheckpoint._key > _end);

// Initialize voting power with the latest checkpoint thats within the interval (or nearest to it)
/// Initialize voting power with the latest checkpoint thats within the interval (or nearest to it)
uint48 _latestKey = _currentCheckpoint._key < _start ? uint48(_start) : _currentCheckpoint._key;
uint256 _totalVotingPower = _currentCheckpoint._value * (_end - _latestKey);

if (_latestKey == _start) return _totalVotingPower;

for (uint32 i = _currentCheckpointIndex; i > 0;) {
// Latest checkpoint voting power is calculated when initializing `_totalVotingPower`, so we pre-decrement the index here
/// Latest checkpoint voting power is calculated when initializing `_totalVotingPower`, so we pre-decrement the index here
_currentCheckpoint = _sourceContract.checkpoints(_account, --i);

// Add voting power for the sub-interval to the total
/// Add voting power for the sub-interval to the total
_totalVotingPower += _currentCheckpoint._value * (_latestKey - _currentCheckpoint._key);

// At the start of the interval, deduct voting power accrued before the interval and return the total
/// At the start of the interval, deduct voting power accrued before the interval and return the total
if (_currentCheckpoint._key <= _start) {
_totalVotingPower -= _currentCheckpoint._value * (_start - _currentCheckpoint._key);
break;
Expand All @@ -196,10 +166,13 @@ contract YieldDistributor is OwnableUpgradeable {
function resolveYieldDistribution() public view returns (bool, bytes memory) {
uint256 _available_yield = BREAD.balanceOf(address(this)) + BREAD.yieldAccrued();
if (
currentVotes == 0 // No votes were cast
|| block.number < lastClaimedBlockNumber + cycleLength // Already claimed this cycle
|| _available_yield / yieldFixedSplitDivisor < projects.length // Yield is insufficient
/// No votes were cast
/// Already claimed this cycle
currentVotes == 0 || block.number < lastClaimedBlockNumber + cycleLength
|| _available_yield / yieldFixedSplitDivisor < projects.length
) {
/// Yield is insufficient

return (false, new bytes(0));
} else {
return (true, abi.encodePacked(this.distributeYield.selector));
Expand Down
Loading

0 comments on commit 5bbb335

Please sign in to comment.