diff --git a/src/Hyperfund.sol b/src/Hyperfund.sol index 1e726b1..1c4c539 100644 --- a/src/Hyperfund.sol +++ b/src/Hyperfund.sol @@ -16,7 +16,7 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade uint256 public hypercertUnits; // erc20 token allowlist, 0 means the token is not allowed - // negative multiplier means the total amount of hypercert units is smaller than the amount of tokens it represents and rounding is applied + // negative multiplier means the total amount of Hypercert units is smaller than the amount of tokens it represents and rounding is applied mapping(address token => int256 multiplier) public tokenMultipliers; // allowlist for non-financial contributions, 0 means the contributor is not allowed @@ -55,10 +55,10 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade } /// @notice Initialize the contract, to be called by proxy - /// @notice NOTE: after deployment of proxy, the hypercert owner must approve the proxy contract to split and burn fractions + /// @notice NOTE: after deployment of proxy, the Hypercert owner must approve the proxy contract to split and burn fractions /// by calling hypercertMinter.setApprovalForAll(address(proxy), true) - /// @param _hypercertMinter The address of the hypercert minter contract - /// @param _hypercertTypeId The id of the hypercert type + /// @param _hypercertMinter The address of the Hypercert minter contract + /// @param _hypercertTypeId The id of the Hypercert type /// @param _admin The address that will have the DEFAULT_ADMIN_ROLE /// @param _manager The address that will have the MANAGER_ROLE /// @param _pauser The address that will have the PAUSER_ROLE @@ -88,15 +88,21 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} - /// @notice set the multiplier for an allowlisted token, 0 means the token is not allowed + // ADMIN FUNCTIONS + + /// @notice Set the multiplier for an allowlisted token, 0 means the token is not allowed, only callable by a manager /// @param _token address of the token - /// @param _multiplier multiplier for the token, negative means the total amount of hypercert units is smaller - /// than the amount of tokens it represents and rounding is applied + /// @param _multiplier multiplier for the token, negative multiplier means the total amount of Hypercert units is + /// smaller than the amount of tokens it represents and rounding is applied function allowlistToken(address _token, int256 _multiplier) external onlyRole(MANAGER_ROLE) { tokenMultipliers[_token] = _multiplier; emit TokenAllowlisted(_token, _multiplier); } + /// @notice Withdraw funds from the hyperfund, only callable by a manager + /// @param _token address of the token to withdraw, address(0) for native token + /// @param _amount amount of the token to withdraw + /// @param _to address to send the funds to function withdrawFunds(address _token, uint256 _amount, address _to) external onlyRole(MANAGER_ROLE) { if (_token == address(0)) { payable(_to).transfer(_amount); @@ -106,15 +112,48 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade emit FundsWithdrawn(_token, _amount, _to); } + /// @notice Issue a Hypercert fraction for a non-financial contribution, only callable by a manager + /// @param _contributor address of the contributor to receive the Hypercert fraction + /// @param _units amount of units to register as a non-financial contribution + function nonfinancialContribution(address _contributor, uint256 _units) external whenNotPaused onlyRole(MANAGER_ROLE) { + uint256 availableSupply = hypercertMinter.unitsOf(hypercertId); + require(availableSupply >= _units, AmountExceedsAvailableSupply(availableSupply)); + _nonfinancialContribution(_contributor, _units); + } + + /// @notice Issue Hypercert fractions for non-financial contributions, only callable by a manager + /// @param _contributors array of addresses of the contributors + /// @param _units array of amounts of units to register as non-financial contributions + function nonFinancialContributions(address[] calldata _contributors, uint256[] calldata _units) + external + onlyRole(MANAGER_ROLE) + { + require(_contributors.length == _units.length, ArrayLengthsMismatch()); + uint256 totalUnits = 0; + for (uint256 i = 0; i < _units.length; i++) { + totalUnits += _units[i]; + } + uint256 availableSupply = hypercertMinter.unitsOf(hypercertId); + require(availableSupply >= totalUnits, AmountExceedsAvailableSupply(availableSupply)); + + for (uint256 i = 0; i < _contributors.length; i++) { + _nonfinancialContribution(_contributors[i], _units[i]); + } + } + + /// @notice pause the hyperfund, only callable by a pauser function pause() external onlyRole(PAUSER_ROLE) { _pause(); } + /// @notice unpause the hyperfund, only callable by a pauser function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } - /// @notice send funds to the hyperfund and receive a hypercert fraction + // USER FUNCTIONS + + /// @notice Send funds to the hyperfund and receive a Hypercert fraction /// @param _token address of the token to send, must be allowlisted. address(0) for native token /// @param _amount amount of the token to send function fund(address _token, uint256 _amount) external payable whenNotPaused { @@ -132,42 +171,17 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade emit Funded(_token, _amount); } - function nonfinancialContribution(address _contributor, uint256 _units) - external - whenNotPaused - onlyRole(MANAGER_ROLE) - { - uint256 availableSupply = hypercertMinter.unitsOf(hypercertId); - require(availableSupply >= _units, AmountExceedsAvailableSupply(availableSupply)); - _nonfinancialContribution(_contributor, _units); - } - - function nonFinancialContributions(address[] calldata _contributors, uint256[] calldata _units) - external - onlyRole(MANAGER_ROLE) - { - require(_contributors.length == _units.length, ArrayLengthsMismatch()); - uint256 totalUnits = 0; - for (uint256 i = 0; i < _units.length; i++) { - totalUnits += _units[i]; - } - uint256 availableSupply = hypercertMinter.unitsOf(hypercertId); - require(availableSupply >= totalUnits, AmountExceedsAvailableSupply(availableSupply)); - - for (uint256 i = 0; i < _contributors.length; i++) { - _nonfinancialContribution(_contributors[i], _units[i]); - } - } - - /// @notice redeem a hypercert fraction for the corresponding amount of tokens - /// NOTE: sender must first approve the hyperfund to burn the hypercert fraction, by calling hypercertMinter.setApprovalForAll(address(this), true) - /// @param _fractionId id of the hypercert fraction + /// @notice Redeem a Hypercert fraction for the corresponding amount of tokens. User must have previously contributed + /// enough non-financial units + /// NOTE: sender must first approve the hyperfund to burn the Hypercert fraction, by calling hypercertMinter.setApprovalForAll(address(this), true) + /// @param _fractionId id of the Hypercert fraction /// @param _token address of the token to redeem, must be allowlisted. address(0) for native token function redeem(uint256 _fractionId, address _token) external whenNotPaused { require(nonfinancialContributions[msg.sender] != 0, NotAllowlisted()); require(hypercertMinter.ownerOf(_fractionId) == msg.sender, Unauthorized()); require(_isFraction(_fractionId), NotFractionOfThisHypercert(hypercertTypeId)); uint256 units = hypercertMinter.unitsOf(_fractionId); + require(units != 0, InvalidAmount()); uint256 tokenAmount = _unitsToTokenAmount(_token, units); if (_token == address(0)) { (bool success,) = payable(msg.sender).call{value: tokenAmount}(""); @@ -180,6 +194,11 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade emit FractionRedeemed(_fractionId, _token, tokenAmount); } + // INTERNAL FUNCTIONS + + /// @notice Mint a Hypercert fraction for a non-financial contributor and register the amount of non-financial units contributed + /// @param _contributor address of the contributor to receive the Hypercert fraction + /// @param _units amount of units to register as a non-financial contribution function _nonfinancialContribution(address _contributor, uint256 _units) internal { require(_contributor != address(0), InvalidAddress()); require(_units != 0, InvalidAmount()); @@ -188,13 +207,20 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade emit NonfinancialContribution(_contributor, _units); } - function _mintFraction(address account, uint256 units) internal { + /// @notice Split a fraction of the Hypercert for a contributor + /// @param _contributor address of the contributor to receive the Hypercert fraction + /// @param _units amount of units to mint + function _mintFraction(address _contributor, uint256 _units) internal { uint256[] memory newallocations = new uint256[](2); - newallocations[0] = hypercertMinter.unitsOf(hypercertId) - units; - newallocations[1] = units; - hypercertMinter.splitFraction(account, hypercertId, newallocations); + newallocations[0] = hypercertMinter.unitsOf(hypercertId) - _units; + newallocations[1] = _units; + hypercertMinter.splitFraction(_contributor, hypercertId, newallocations); } + /// @notice Convert token amount to Hypercert units using the token's multiplier + /// @param _token address of the token + /// @param _amount amount of the token + /// @return units amount of Hypercert units function _tokenAmountToUnits(address _token, uint256 _amount) internal view returns (uint256 units) { int256 multiplier = tokenMultipliers[_token]; if (multiplier > 0) { @@ -204,6 +230,10 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade } } + /// @notice Convert Hypercert units to token amount using the token's multiplier + /// @param _token address of the token + /// @param _units amount of Hypercert units + /// @return amount amount of the token function _unitsToTokenAmount(address _token, uint256 _units) internal view returns (uint256 amount) { int256 multiplier = tokenMultipliers[_token]; if (multiplier > 0) { @@ -213,6 +243,8 @@ contract Hyperfund is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgrade } } + /// @notice Check if a Hypercert belongs to the correct Hypercert type + /// @param _fractionId id of the Hypercert fraction function _isFraction(uint256 _fractionId) internal view returns (bool) { return _fractionId & TYPE_MASK == hypercertTypeId; } diff --git a/src/HyperfundFactory.sol b/src/HyperfundFactory.sol index 321a7a5..8f75491 100644 --- a/src/HyperfundFactory.sol +++ b/src/HyperfundFactory.sol @@ -21,7 +21,6 @@ contract HyperfundFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable error AlreadyDeployed(); error NotOwnerOfHypercert(); - // Event to emit when a new Hyperfund is created event HyperfundCreated( address indexed hyperfundAddress, uint256 indexed hypercertId, @@ -31,7 +30,6 @@ contract HyperfundFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable address upgrader ); - // Event to emit when a new Hyperstaker is created event HyperstakerCreated( address indexed hyperstakerAddress, uint256 indexed hypercertId, @@ -41,7 +39,6 @@ contract HyperfundFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable address upgrader ); - /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -55,7 +52,13 @@ contract HyperfundFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - // Function to create a new Hyperfund + /// @notice Create a new Hyperfund + /// @param hypercertTypeId id of the Hypercert type to create the Hyperfund for + /// @param admin address of the admin of the Hyperfund + /// @param manager address of the manager of the Hyperfund + /// @param pauser address of the pauser of the Hyperfund + /// @param upgrader address of the upgrader of the Hyperfund + /// @return address of the new Hyperfund function createHyperfund(uint256 hypercertTypeId, address admin, address manager, address pauser, address upgrader) external returns (address) @@ -79,7 +82,13 @@ contract HyperfundFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable return newHyperfund; } - // Function to create a new Hyperstaker + /// @notice Create a new Hyperstaker + /// @param hypercertTypeId id of the Hypercert type to create the Hyperstaker for + /// @param admin address of the admin of the Hyperstaker + /// @param manager address of the manager of the Hyperstaker + /// @param pauser address of the pauser of the Hyperstaker + /// @param upgrader address of the upgrader of the Hyperstaker + /// @return address of the new Hyperstaker function createHyperstaker( uint256 hypercertTypeId, address admin, @@ -105,6 +114,14 @@ contract HyperfundFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable return newHyperstaker; } + /// @notice Create a new Hyperfund and Hyperstaker + /// @param hypercertTypeId id of the Hypercert type to create the Hyperfund and Hyperstaker for + /// @param admin address of the admin of the Hyperfund and Hyperstaker + /// @param manager address of the manager of the Hyperfund and Hyperstaker + /// @param pauser address of the pauser of the Hyperfund and Hyperstaker + /// @param upgrader address of the upgrader of the Hyperfund and Hyperstaker + /// @return hyperfund address of the new Hyperfund + /// @return hyperstaker address of the new Hyperstaker function createProject(uint256 hypercertTypeId, address admin, address manager, address pauser, address upgrader) external returns (address hyperfund, address hyperstaker) diff --git a/src/Hyperstaker.sol b/src/Hyperstaker.sol index 3385323..dd9830e 100644 --- a/src/Hyperstaker.sol +++ b/src/Hyperstaker.sol @@ -94,6 +94,11 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} + // ADMIN FUNCTIONS + + /// @notice Set the reward for the current round, this ends the current round and starts a new one. Only callable by a manager + /// @param _rewardToken address of the reward token + /// @param _rewardAmount amount of the reward for the current round function setReward(address _rewardToken, uint256 _rewardAmount) external payable onlyRole(MANAGER_ROLE) { Round storage currentRound = rounds[rounds.length - 1]; currentRound.totalRewards = _rewardAmount; @@ -112,6 +117,20 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra emit RewardSet(_rewardToken, _rewardAmount); } + /// @notice Pause the contract, only callable by a pauser + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @notice Unpause the contract, only callable by a pauser + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + + // USER FUNCTIONS + + /// @notice Stake a Hypercert, this will transfer the Hypercert from the user to the contract + /// @param _hypercertId id of the Hypercert to stake function stake(uint256 _hypercertId) external whenNotPaused { require(hypercertMinter.unitsOf(_hypercertId) != 0, NoUnitsInHypercert()); uint256 hypercertTypeId_ = _getHypercertTypeId(_hypercertId); @@ -123,6 +142,9 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra hypercertMinter.safeTransferFrom(msg.sender, address(this), _hypercertId, 1, ""); } + /// @notice Unstake a Hypercert, this will transfer the Hypercert from the contract to the user and delete all + ///stake information + /// @param _hypercertId id of the Hypercert to unstake function unstake(uint256 _hypercertId) external whenNotPaused { require(stakes[_hypercertId].stakingStartTime != 0, NotStaked()); address staker = stakes[_hypercertId].staker; @@ -132,6 +154,9 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra hypercertMinter.safeTransferFrom(address(this), msg.sender, _hypercertId, 1, ""); } + /// @notice Claim a reward eligable by a staked Hypercert for a given round + /// @param _hypercertId id of the Hypercert to claim the reward for + /// @param _roundId id of the round to claim the reward for function claimReward(uint256 _hypercertId, uint256 _roundId) external whenNotPaused { require(stakes[_hypercertId].stakingStartTime != 0, NotStaked()); address staker = stakes[_hypercertId].staker; @@ -152,6 +177,12 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra } } + // VIEW FUNCTIONS + + /// @notice Calculate the reward for a staked Hypercert for a given round + /// @param _hypercertId id of the Hypercert to calculate the reward for + /// @param _roundId id of the round to calculate the reward for + /// @return amount of the reward eligable for the staked Hypercert for the given round function calculateReward(uint256 _hypercertId, uint256 _roundId) public view returns (uint256) { Round memory round = rounds[_roundId]; require(round.endTime != 0, RoundNotSet()); @@ -163,30 +194,40 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra round.totalRewards * hypercertMinter.unitsOf(_hypercertId) * stakeDuration / (totalUnits * round.duration); } - function pause() external onlyRole(PAUSER_ROLE) { - _pause(); - } - - function unpause() external onlyRole(PAUSER_ROLE) { - _unpause(); + /// @notice Check if a staked Hypercert had already claimed a reward for a given round + /// @param _hypercertId id of the Hypercert to check + /// @param _roundId id of the round to check + /// @return true if the staked Hypercert had claimed a reward for the given round, false otherwise + function isRoundClaimed(uint256 _hypercertId, uint256 _roundId) public view returns (bool) { + return (stakes[_hypercertId].claimed & (1 << _roundId)) != 0; } + /// @notice Get the stake information for a staked Hypercert + /// @param _hypercertId id of the Hypercert to get the stake information for + /// @return stake information for the staked Hypercert (staking start time, staker address, claimed rounds bitmap) function getStakeInfo(uint256 _hypercertId) external view returns (Stake memory) { return stakes[_hypercertId]; } + /// @notice Get the round information for a given round + /// @param _roundId id of the round to get the information for + /// @return round information for the given round (start time, end time, duration, total rewards, reward token) function getRoundInfo(uint256 _roundId) external view returns (Round memory) { return rounds[_roundId]; } + // INTERNAL FUNCTIONS + + /// @notice Get the hypercert type id for a given hypercert id + /// @param _hypercertId id of the Hypercert to get the type id for + /// @return hypercert type id for the given hypercert id function _getHypercertTypeId(uint256 _hypercertId) internal pure returns (uint256) { return _hypercertId & TYPE_MASK; } - function isRoundClaimed(uint256 _hypercertId, uint256 _roundId) public view returns (bool) { - return (stakes[_hypercertId].claimed & (1 << _roundId)) != 0; - } - + /// @notice Set a round as claimed for a staked Hypercert + /// @param _hypercertId id of the Hypercert to set the round as claimed for + /// @param _roundId id of the round to set as claimed function _setRoundClaimed(uint256 _hypercertId, uint256 _roundId) internal { stakes[_hypercertId].claimed |= (1 << _roundId); }