Skip to content

Commit

Permalink
Decoupled pool management from council management
Browse files Browse the repository at this point in the history
  • Loading branch information
sembrestels committed Sep 7, 2024
1 parent b16df71 commit 881a9e0
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 91 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CouncilHaus: A Decentralized Grant Allocation System [![Coverage Status](https://coveralls.io/repos/github/BlossomLabs/councilhaus/badge.svg?branch=master)](https://coveralls.io/github/BlossomLabs/councilhaus?branch=master)
# CouncilHaus: A Decentralized Grant Allocation System [![Coverage Status](https://coveralls.io/repos/github/BlossomLabs/councilhaus/badge.svg?branch=master&dummy=unused)](https://coveralls.io/github/BlossomLabs/councilhaus?branch=master)

## What is Council?

Expand Down
191 changes: 122 additions & 69 deletions contracts/councilhaus/contracts/Council.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,39 @@
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;

// Import required OpenZeppelin contracts
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {GDAv1Forwarder, PoolConfig} from "./interfaces/GDAv1Forwarder.sol";
import {ISuperfluidPool} from "./interfaces/ISuperfluidPool.sol";
// Import custom contracts
import {PoolManager} from "./PoolManager.sol";
import {NonTransferableToken} from "./NonTransferableToken.sol";

contract Council is NonTransferableToken, AccessControl {
// Define the Allocation struct to represent budget allocations
struct Allocation {
address[] accounts;
uint128[] amounts;
}

error PoolCreationFailed();
/**
* @title Council
* @dev A contract for managing a council with voting power, budget allocation, and grantee management
* Inherits from NonTransferableToken, AccessControl, and PoolManager
*/
contract Council is NonTransferableToken, AccessControl, PoolManager {
// Custom error definitions
error InvalidMaxAllocations();
error FlowRateMustBePositive();
error CouncilMemberAlreadyAdded();
error CouncilMemberNotFound();
error TooManyAllocations();
error GranteesAndAmountsMustBeEqualLength();
error ArraysLengthMismatch();
error GranteeAlreadyAdded();
error GranteeNotFound();
error AmountMustBeGreaterThanZero();
error TotalAllocatedExceedsBalance();

// Event definitions
event MaxAllocationsPerMemberSet(uint8 maxAllocationsPerMember);
event FlowRateSet(int96 flowRate);
event CouncilMemberAdded(address member, uint256 votingPower);
Expand All @@ -33,46 +45,32 @@ contract Council is NonTransferableToken, AccessControl {
event BudgetExecuted();
event Withdrawn(address token, address account, uint256 amount);

struct Allocation {
address[] grantees;
uint128[] amounts;
}

modifier updatePoolUnits(address _member) {
Allocation memory _allocation = _allocations[_member];
for (uint256 i = 0; i < _allocation.grantees.length; i++) {
pool.updateMemberUnits(_allocation.grantees[i], pool.getUnits(_allocation.grantees[i]) - _allocation.amounts[i]);
}
_;
_allocation = _allocations[_member];
for (uint256 i = 0; i < _allocation.grantees.length; i++) {
pool.updateMemberUnits(_allocation.grantees[i], pool.getUnits(_allocation.grantees[i]) + _allocation.amounts[i]);
}
}

GDAv1Forwarder public immutable gdav1Forwarder;

// Constants
uint8 public constant MAX_ALLOCATIONS_PER_MEMBER = 10;
bytes32 public constant MEMBER_MANAGER_ROLE = keccak256("MEMBER_MANAGER_ROLE");
bytes32 public constant GRANTEE_MANAGER_ROLE = keccak256("GRANTEE_MANAGER_ROLE");

ISuperfluidPool public pool;
// State variables
address public gdav1Forwarder;
uint8 public maxAllocationsPerMember;
mapping(address => bool) internal grantees; // grantees[grantee] = true if the grantee is a valid grantee, false otherwise
mapping(address => uint256) internal _allocatedBy; // _allocatedBy[member] = amount allocated by the member
mapping(address => Allocation) internal _allocations; // _allocations[member] = { grantees: [grantee1, grantee2, ...], amounts: [amount1, amount2, ...] }
uint256 public totalAllocated;

constructor(string memory _name, string memory _symbol, address _distributionToken, address _gdav1Forwarder)
/**
* @dev Constructor to initialize the Council contract
* @param _name Name of the non-transferable token
* @param _symbol Symbol of the non-transferable token
* @param _distributionToken Address of the token used for distribution
* @param _gdav1Forwarder Address of the GDAv1Forwarder contract
*/
constructor(
string memory _name,
string memory _symbol,
address _distributionToken,
address _gdav1Forwarder
)
NonTransferableToken(_name, _symbol)
PoolManager(_distributionToken, _gdav1Forwarder)
{
gdav1Forwarder = GDAv1Forwarder(_gdav1Forwarder);
(bool _success, address _pool) = GDAv1Forwarder(_gdav1Forwarder).createPool(_distributionToken, address(this), PoolConfig({
transferabilityForUnitsOwner: false,
distributionFromAnyAddress: false
}));
if (!_success) revert PoolCreationFailed();
pool = ISuperfluidPool(_pool);
gdav1Forwarder = _gdav1Forwarder;
maxAllocationsPerMember = MAX_ALLOCATIONS_PER_MEMBER;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MEMBER_MANAGER_ROLE, msg.sender);
Expand All @@ -81,72 +79,127 @@ contract Council is NonTransferableToken, AccessControl {
emit MaxAllocationsPerMemberSet(MAX_ALLOCATIONS_PER_MEMBER);
}

function setMaxAllocationsPerMember(uint8 _maxAllocationsPerMember) public onlyRole(DEFAULT_ADMIN_ROLE) {
if (_maxAllocationsPerMember <= 0 || _maxAllocationsPerMember > MAX_ALLOCATIONS_PER_MEMBER) revert InvalidMaxAllocations();
/**
* @dev Set the maximum number of allocations per member
* @param _maxAllocationsPerMember New maximum allocations per member
*/
function setMaxAllocationsPerMember(
uint8 _maxAllocationsPerMember
) public onlyRole(DEFAULT_ADMIN_ROLE) {
if (
_maxAllocationsPerMember <= 0 ||
_maxAllocationsPerMember > MAX_ALLOCATIONS_PER_MEMBER
) revert InvalidMaxAllocations();
maxAllocationsPerMember = _maxAllocationsPerMember;
emit MaxAllocationsPerMemberSet(_maxAllocationsPerMember);
}

function addCouncilMember(address _member, uint256 _votingPower) public onlyRole(MEMBER_MANAGER_ROLE) {
/**
* @notice Add a new council member
* @param _member Address of the new council member
* @param _votingPower Voting power of the new council member
*/
function addCouncilMember(
address _member,
uint256 _votingPower
) public onlyRole(MEMBER_MANAGER_ROLE) {
if (balanceOf(_member) > 0) revert CouncilMemberAlreadyAdded();
if (_votingPower == 0) revert AmountMustBeGreaterThanZero();
_mint(_member, _votingPower);
emit CouncilMemberAdded(_member, _votingPower);
}

function removeCouncilMember(address _member) public onlyRole(MEMBER_MANAGER_ROLE) updatePoolUnits(_member) {
/**
* @notice Remove a council member
* @param _member Address of the council member to remove
*/
function removeCouncilMember(
address _member
) public onlyRole(MEMBER_MANAGER_ROLE) {
if (balanceOf(_member) == 0) revert CouncilMemberNotFound();
_burn(_member, balanceOf(_member));
delete _allocations[_member];
_setAllocation(
_member,
Allocation({accounts: new address[](0), amounts: new uint128[](0)})
);
emit CouncilMemberRemoved(_member);
}

function addGrantee(string memory _name, address _grantee) public onlyRole(GRANTEE_MANAGER_ROLE) {
if (grantees[_grantee]) revert GranteeAlreadyAdded();
grantees[_grantee] = true;
/**
* @notice Add a new grantee
* @param _name Name of the grantee
* @param _grantee Address of the grantee
*/
function addGrantee(
string memory _name,
address _grantee
) public onlyRole(GRANTEE_MANAGER_ROLE) {
if (isGrantee(_grantee)) revert GranteeAlreadyAdded();
_addGrantee(_grantee);
emit GranteeAdded(_name, _grantee);
}

function removeGrantee(address _grantee) public onlyRole(GRANTEE_MANAGER_ROLE) {
if (!grantees[_grantee]) revert GranteeNotFound();
grantees[_grantee] = false;
pool.updateMemberUnits(_grantee, 0);
/**
* @notice Remove a grantee
* @param _grantee Address of the grantee to remove
*/
function removeGrantee(
address _grantee
) public onlyRole(GRANTEE_MANAGER_ROLE) {
if (!isGrantee(_grantee)) revert GranteeNotFound();
_removeGrantee(_grantee);
emit GranteeRemoved(_grantee);
}

function allocateBudget(Allocation memory _allocation) public updatePoolUnits(msg.sender) {
/**
* @notice Allocate budget for grantees
* @param _allocation Allocation struct containing grantee addresses and amounts
*/
function allocateBudget(Allocation memory _allocation) public {
uint256 balance = balanceOf(msg.sender);
if (balance == 0) revert CouncilMemberNotFound();
if (_allocation.grantees.length > maxAllocationsPerMember) revert TooManyAllocations();
if (_allocation.grantees.length != _allocation.amounts.length) revert GranteesAndAmountsMustBeEqualLength();
if (_allocation.accounts.length > maxAllocationsPerMember)
revert TooManyAllocations();
if (_allocation.accounts.length != _allocation.amounts.length)
revert ArraysLengthMismatch();
uint256 _totalAllocatedBySender = 0;
for (uint256 i = 0; i < _allocation.grantees.length; i++) {
if (!grantees[_allocation.grantees[i]]) revert GranteeNotFound();
if (_allocation.amounts[i] == 0) revert AmountMustBeGreaterThanZero();
for (uint256 i = 0; i < _allocation.accounts.length; i++) {
if (!isGrantee(_allocation.accounts[i])) revert GranteeNotFound();
if (_allocation.amounts[i] == 0)
revert AmountMustBeGreaterThanZero();
_totalAllocatedBySender += _allocation.amounts[i];
}
if (_totalAllocatedBySender > balance) revert TotalAllocatedExceedsBalance();
_allocations[msg.sender] = _allocation;
_allocatedBy[msg.sender] = _totalAllocatedBySender;
totalAllocated += _totalAllocatedBySender;
if (_totalAllocatedBySender > balance)
revert TotalAllocatedExceedsBalance();
_setAllocation(msg.sender, _allocation);
emit BudgetAllocated(msg.sender, _allocation);
}

/**
* @notice Withdraw tokens from the contract
* @param _token Address of the token to withdraw
*/
function withdraw(address _token) public onlyRole(DEFAULT_ADMIN_ROLE) {
uint256 balance = IERC20(_token).balanceOf(address(this));
IERC20(_token).transfer(msg.sender, balance);
emit Withdrawn(_token, msg.sender, balance);
}

function getAllocation(address _member) public view returns (Allocation memory) {
return _allocations[_member];
}

function isGrantee(address _grantee) public view returns (bool) {
return grantees[_grantee];
}

function distributionToken() public view returns (address) {
return address(pool.superToken());
/**
* @notice Get the allocation details for a council member
* @param _member Address of the council member
* @return allocation Allocation struct for the member
* @return sum Total amount allocated by the member
* @return balance Voting power balance of the member
*/
function getAllocation(
address _member
)
public
view
returns (Allocation memory allocation, uint256 sum, uint256 balance)
{
(allocation, sum) = _getAllocation(_member);
return (allocation, sum, balanceOf(_member));
}
}
2 changes: 1 addition & 1 deletion contracts/councilhaus/contracts/NonTransferableToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract NonTransferableToken is ERC20 {
abstract contract NonTransferableToken is ERC20 {

error CantTransferToken();
error CantApproveToken();
Expand Down
Loading

0 comments on commit 881a9e0

Please sign in to comment.