diff --git a/contracts/protocol/Sector3DAO.sol b/contracts/protocol/Sector3DAO.sol index 316ff31..e6fefcc 100644 --- a/contracts/protocol/Sector3DAO.sol +++ b/contracts/protocol/Sector3DAO.sol @@ -39,11 +39,13 @@ contract Sector3DAO { */ Sector3DAOPriority[] public priorities; + event PriorityDeployed(Sector3DAOPriority priority); + constructor(string memory name_, string memory purpose_, address token_) { name = name_; purpose = purpose_; token = token_; - owner = tx.origin; + owner = msg.sender; } /** @@ -78,10 +80,11 @@ contract Sector3DAO { token = token_; } - function deployPriority(string calldata title, address rewardToken, uint16 epochDurationInDays, uint256 epochBudget, address gatingNFT) public returns (Sector3DAOPriority) { + function deployPriority(string calldata title, address rewardToken, uint16 epochDurationInDays, uint256 epochBudget, address gatingNFT, uint16 coolingWindowDurationInDays) public returns (Sector3DAOPriority) { require(msg.sender == owner, "You aren't the owner"); - Sector3DAOPriority priority = new Sector3DAOPriority(address(this), title, rewardToken, epochDurationInDays, epochBudget, gatingNFT); + Sector3DAOPriority priority = new Sector3DAOPriority(address(this), title, rewardToken, epochDurationInDays, epochBudget, gatingNFT, coolingWindowDurationInDays); priorities.push(priority); + emit PriorityDeployed(priority); return priority; } diff --git a/contracts/protocol/Sector3DAOFactory.sol b/contracts/protocol/Sector3DAOFactory.sol index df77333..26c8716 100644 --- a/contracts/protocol/Sector3DAOFactory.sol +++ b/contracts/protocol/Sector3DAOFactory.sol @@ -5,40 +5,45 @@ import './Sector3DAO.sol'; contract Sector3DAOFactory { - address public owner; + address public immutable owner; address[] public daos; + event DAODeployed(address dao); + constructor() { owner = msg.sender; } - function setOwner(address owner_) public { + modifier onlyOwner() { require(msg.sender == owner, "You aren't the owner"); - owner = owner_; + _; } function getDAOs() public view returns (address[] memory) { return daos; } - function deployDAO(string calldata name, string calldata purpose, address token) public returns (address) { + function deployDAO(string calldata name, string calldata purpose, address token) public returns (address) { Sector3DAO dao = new Sector3DAO(name, purpose, token); daos.push(address(dao)); return address(dao); } - function removeDAO(address dao) public { - require(msg.sender == owner, "You aren't the owner"); - address[] memory daosAfterRemoval = new address[](daos.length - 1); - uint16 daosIndex = 0; - for (uint16 i = 0; i < daosAfterRemoval.length; i++) { - if (dao == daos[daosIndex]) { - daosIndex++; + function removeDAO(address dao) public onlyOwner { + uint256 indexToRemove = daos.length; + + for (uint256 i = 0; i < daos.length; i++) { + if (dao == daos[i]) { + indexToRemove = i; + break; } - daosAfterRemoval[i] = daos[daosIndex]; - daosIndex++; } - daos = daosAfterRemoval; + + require(indexToRemove < daos.length, "DAO not found"); + + // Use the "delete-and-swap" technique to save gas. + daos[indexToRemove] = daos[daos.length - 1]; + daos.pop(); } } diff --git a/contracts/protocol/Sector3DAOPriority.sol b/contracts/protocol/Sector3DAOPriority.sol index edb31cf..5ccb2ef 100644 --- a/contracts/protocol/Sector3DAOPriority.sol +++ b/contracts/protocol/Sector3DAOPriority.sol @@ -9,6 +9,13 @@ import "./Structs.sol"; contract Sector3DAOPriority is IPriority { using SafeERC20 for IERC20; + + struct Rating { + uint16 contributionIndex; + address rater; + uint8 rating; + } + address public immutable dao; string public title; IERC20 public immutable rewardToken; @@ -17,19 +24,24 @@ contract Sector3DAOPriority is IPriority { uint256 public immutable epochBudget; IERC721 public immutable gatingNFT; Contribution[] contributions; + Rating[] ratings; mapping(uint16 => mapping(address => bool)) claims; uint256 public claimsBalance; + uint16 public immutable coolingWindowDuration; event ContributionAdded(Contribution contribution); event RewardClaimed(uint16 epochIndex, address contributor, uint256 amount); + event ContributionRated(uint16 contributionIndex, address rater, uint8 rating); + event RatingUpdated(uint16 contributionIndex, address rater, uint8 newRating); error EpochNotYetEnded(); error EpochNotYetFunded(); error NoRewardForEpoch(); error RewardAlreadyClaimed(); error NoGatingNFTOwnership(); + error CoolingWindowNotEnded(); - constructor(address dao_, string memory title_, address rewardToken_, uint16 epochDurationInDays, uint256 epochBudget_, address gatingNFT_) { + constructor(address dao_, string memory title_, address rewardToken_, uint16 epochDurationInDays, uint256 epochBudget_, address gatingNFT_, uint16 coolingWindowDurationInDays) { dao = dao_; title = title_; rewardToken = IERC20(rewardToken_); @@ -37,6 +49,7 @@ contract Sector3DAOPriority is IPriority { epochDuration = epochDurationInDays; epochBudget = epochBudget_; gatingNFT = IERC721(gatingNFT_); + coolingWindowDuration = coolingWindowDurationInDays; } /** @@ -101,6 +114,9 @@ contract Sector3DAOPriority is IPriority { if (epochIndex >= getEpochIndex()) { revert EpochNotYetEnded(); } + if (block.timestamp < getCoolingWindowEndTime(epochIndex)) { + revert CoolingWindowNotEnded(); + } uint256 epochReward = getEpochReward(epochIndex, msg.sender); if (epochReward == 0) { revert NoRewardForEpoch(); @@ -185,4 +201,64 @@ contract Sector3DAOPriority is IPriority { return totalFundingReceived >= totalBudget; } } + + function rateContribution(uint16 contributionIndex, uint8 rating) public { + require(contributionIndex < contributions.length, "Invalid contribution index"); + require(rating >= 1 && rating <= 5, "Invalid rating value"); + + uint16 ratingIndex = findRatingIndex(contributionIndex, msg.sender); + + if (ratingIndex < ratings.length) { + ratings[ratingIndex].rating = rating; + emit RatingUpdated(contributionIndex, msg.sender, rating); + } else { + Rating memory newRating = Rating({ + contributionIndex: contributionIndex, + rater: msg.sender, + rating: rating + }); + ratings.push(newRating); + emit ContributionRated(contributionIndex, msg.sender, rating); + } + } + + function findRatingIndex(uint16 contributionIndex, address rater) internal view returns (uint16) { + for (uint16 i = 0; i < ratings.length; i++) { + if (ratings[i].contributionIndex == contributionIndex && ratings[i].rater == rater) { + return i; + } + } + return uint16(ratings.length); + } + + function getContributionRating(uint16 contributionIndex) public view returns (uint8) { + require(contributionIndex < contributions.length, "Invalid contribution index"); + + uint16 totalRatings = 0; + uint16 sumRatings = 0; + + for (uint16 i = 0; i < ratings.length; i++) { + if (ratings[i].contributionIndex == contributionIndex) { + totalRatings++; + sumRatings += ratings[i].rating; + } + } + + if (totalRatings == 0) { + return 0; + } else { + return uint8(sumRatings / totalRatings); + } + } + + /** + * @notice Gets the end time of the cooling window for an epoch. + * @dev This should be implemented based on the requirements of the DAO. + * @param epochIndex The index of the epoch. + */ + function getCoolingWindowEndTime(uint16 epochIndex) public view returns (uint256) { + uint256 epochEndTime = startTime + (epochIndex + 1) * epochDuration * 1 days; + uint256 localCoolingWindowDuration = 3 * 1 days; + return epochEndTime + localCoolingWindowDuration; + } }