diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 649210bc..9d01c313 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -49,18 +49,17 @@ contract Depository is IErrors, Ownable { uint256 sold; } - // OLA interface + // OLA token address address public immutable ola; - // Treasury interface + // Treasury address address public treasury; - // Tokenomics interface + // Tokenomics address address public tokenomics; // Mapping of user address => list of bonds mapping(address => Bond[]) public mapUserBonds; // Map of token address => bond products they are present mapping(address => Product[]) public mapTokenProducts; - - // TODO later fix government / manager + constructor(address _ola, address _treasury, address _tokenomics) { ola = _ola; treasury = _treasury; @@ -122,6 +121,9 @@ contract Depository is IErrors, Ownable { mapUserBonds[user].push(Bond(payout, uint256(block.timestamp), expiry, productId, false)); emit CreateBond(productId, payout, tokenAmount); + // Take into account this bond in current epoch + ITokenomics(tokenomics).usedBond(payout); + // Transfer tokens to the depository product.token.safeTransferFrom(msg.sender, address(this), tokenAmount); // Approve treasury for the specified token amount @@ -203,6 +205,10 @@ contract Depository is IErrors, Ownable { /// @return productId New bond product Id. function create(IERC20 token, uint256 supply, uint256 vesting) external onlyOwner returns (uint256 productId) { // Create a new product. + if(!ITokenomics(tokenomics).allowedNewBond(supply)) { + // fixed later to correct revert + revert AmountLowerThan(ITokenomics(tokenomics).getBondLeft(), supply); + } productId = mapTokenProducts[address(token)].length; Product memory product = Product(token, supply, vesting, uint256(block.timestamp + vesting), 0, 0); mapTokenProducts[address(token)].push(product); diff --git a/contracts/Dispenser.sol b/contracts/Dispenser.sol new file mode 100644 index 00000000..6c9bf430 --- /dev/null +++ b/contracts/Dispenser.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./governance/VotingEscrow.sol"; +import "./interfaces/IErrors.sol"; +import "./interfaces/IStructs.sol"; +import "./interfaces/ITreasury.sol"; +import "./interfaces/ITokenomics.sol"; + + +/// @title Dispenser - Smart contract for rewards +/// @author AL +contract Dispenser is IErrors, IStructs, Ownable, Pausable, ReentrancyGuard { + using SafeERC20 for IERC20; + + event VotingEscrowUpdated(address ve); + event TreasuryUpdated(address treasury); + event TokenomicsUpdated(address tokenomics); + + // OLA token address + address public immutable ola; + // Voting Escrow address + address public ve; + // Treasury address + address public treasury; + // Tokenomics address + address public tokenomics; + // Mapping of owner of component / agent address => reward amount + mapping(address => uint256) public mapOwnerRewards; + // Mapping of staker address => reward amount + mapping(address => uint256) public mapStakerRewards; + + constructor(address _ola, address _ve, address _treasury, address _tokenomics) { + ola = _ola; + ve = _ve; + treasury = _treasury; + tokenomics = _tokenomics; + } + + // Only treasury has a privilege to manipulate a dispenser + modifier onlyTreasury() { + if (treasury != msg.sender) { + revert ManagerOnly(msg.sender, treasury); + } + _; + } + + // Only voting escrow has a privilege to manipulate a dispenser + modifier onlyVotingEscrow() { + if (ve != msg.sender) { + revert ManagerOnly(msg.sender, ve); + } + _; + } + + function changeVotingEscrow(address newVE) external onlyOwner { + ve = newVE; + emit VotingEscrowUpdated(newVE); + } + + function changeTreasury(address newTreasury) external onlyOwner { + treasury = newTreasury; + emit TreasuryUpdated(newTreasury); + } + + function changeTokenomics(address newTokenomics) external onlyOwner { + tokenomics = newTokenomics; + emit TokenomicsUpdated(newTokenomics); + } + + /// @dev Distributes rewards between component and agent owners. + function _distributeOwnerRewards(uint256 componentReward, uint256 agentReward) internal { + uint256 componentRewardLeft = componentReward; + uint256 agentRewardLeft = agentReward; + + // Get components owners and their UCFc-s + (address[] memory profitableComponents, uint256[] memory ucfcs) = + ITokenomics(tokenomics).getProfitableComponents(); + + uint256 numComponents = profitableComponents.length; + uint256 sumProfits; + if (numComponents > 0) { + // Calculate overall profits of UCFc-s + for (uint256 i = 0; i < numComponents; ++i) { + sumProfits += ucfcs[i]; + } + + // Calculate reward per component owner + for (uint256 i = 0; i < numComponents; ++i) { + uint256 rewardPerComponent = componentReward * ucfcs[i] / sumProfits; + // If there is a rounding error, floor to the correct value + if (rewardPerComponent > componentRewardLeft) { + rewardPerComponent = componentRewardLeft; + } + componentRewardLeft -= rewardPerComponent; + mapOwnerRewards[profitableComponents[i]] += rewardPerComponent; + } + } + + // Get components owners and their UCFa-s + (address[] memory profitableAgents, uint256[] memory ucfas) = ITokenomics(tokenomics).getProfitableAgents(); + uint256 numAgents = profitableAgents.length; + if (numAgents > 0) { + // Calculate overall profits of UCFa-s + sumProfits = 0; + for (uint256 i = 0; i < numAgents; ++i) { + sumProfits += ucfas[i]; + } + + uint256 rewardPerAgent; + for (uint256 i = 0; i < numAgents; ++i) { + rewardPerAgent = agentReward * ucfas[i] / sumProfits; + // If there is a rounding error, floor to the correct value + if (rewardPerAgent > agentRewardLeft) { + rewardPerAgent = agentRewardLeft; + } + agentRewardLeft -= rewardPerAgent; + mapOwnerRewards[profitableAgents[i]] += rewardPerAgent; + } + } + } + + /// @dev Distributes rewards between stakers. + function _distributeStakerRewards(uint256 stakerReward) internal { + VotingEscrow veContract = VotingEscrow(ve); + address[] memory accounts = veContract.getLockAccounts(); + + // Get the overall amount of rewards for stakers + uint256 rewardLeft = stakerReward; + + // Iterate over staker addresses and distribute + uint256 numAccounts = accounts.length; + uint256 supply = veContract.totalSupply(); + if (supply > 0) { + for (uint256 i = 0; i < numAccounts; ++i) { + uint256 balance = veContract.balanceOf(accounts[i]); + // Reward for this specific staker + uint256 reward = stakerReward * balance / supply; + + // If there is a rounding error, floor to the correct value + if (reward > rewardLeft) { + reward = rewardLeft; + } + rewardLeft -= reward; + mapStakerRewards[accounts[i]] += reward; + } + } + } + + /// @dev Distributes rewards. + function distributeRewards( + uint256 stakerReward, + uint256 componentReward, + uint256 agentReward + ) external onlyTreasury whenNotPaused + { + // Distribute rewards between component and agent owners + _distributeOwnerRewards(componentReward, agentReward); + + // Distribute rewards for stakers + _distributeStakerRewards(stakerReward); + } + + /// @dev Withdraws rewards for owners of components / agents. + function withdrawOwnerRewards() external nonReentrant { + uint256 balance = mapOwnerRewards[msg.sender]; + if (balance > 0) { + mapOwnerRewards[msg.sender] = 0; + IERC20(ola).safeTransfer(msg.sender, balance); + } + } + + /// @dev Withdraws rewards for stakers. + /// @param account Account address. + /// @return balance Reward balance. + function withdrawStakingRewards(address account) external onlyVotingEscrow returns (uint256 balance) { + balance = mapStakerRewards[account]; + if (balance > 0) { + mapStakerRewards[account] = 0; + IERC20(ola).safeTransfer(ve, balance); + } + } + + /// @dev Gets the paused state. + /// @return True, if paused. + function isPaused() external returns (bool) { + return paused(); + } +} diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 6c1392eb..edc4d44a 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -3,40 +3,36 @@ pragma solidity ^0.8.4; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./ComponentRegistry.sol"; -import "./AgentRegistry.sol"; -import "./ServiceRegistry.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; +import "./interfaces/IService.sol"; import "./interfaces/ITreasury.sol"; import "./interfaces/IErrors.sol"; -// Uniswapv2 +import "./interfaces/IStructs.sol"; import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; -import "@uniswap/lib/contracts/libraries/FixedPoint.sol"; + /// @title Tokenomics - Smart contract for store/interface for key tokenomics params /// @author AL -contract Tokenomics is IErrors, Ownable { +contract Tokenomics is IErrors, IStructs, Ownable { using FixedPoint for *; event TreasuryUpdated(address treasury); - - struct PointEcomonics { - FixedPoint.uq112x112 ucf; - FixedPoint.uq112x112 usf; - FixedPoint.uq112x112 df; // x > 1.0 - uint256 ts; // timestamp - uint256 blk; // block - bool _exist; // ready or not - } + event DepositoryUpdated(address depository); // OLA token address address public immutable ola; // Treasury contract address address public treasury; + // Depository contract address + address public depository; + bytes4 private constant FUNC_SELECTOR = bytes4(keccak256("kLast()")); // is pair or pure ERC20? uint256 public immutable epochLen; // epoch len in blk // source: https://github.com/compound-finance/open-oracle/blob/d0a0d0301bff08457d9dfc5861080d3124d079cd/contracts/Uniswap/UniswapLib.sol#L27 // 2^(112 - log2(1e18)) uint256 public constant MAGIC_DENOMINATOR = 5192296858534816; + uint256 public constant INITIAL_DF = (110 * 10**18) / 100; // 10% with 18 decimals + uint256 public maxBond = 2000000 * 10**18; // 2M OLA with 18 decimals // Epsilon subject to rounding error uint256 public constant E13 = 10**13; // Maximum precision number to be considered @@ -51,7 +47,20 @@ contract Tokenomics is IErrors, Ownable { FixedPoint.uq112x112 public gamma = FixedPoint.fraction(1, 1); // Total service revenue per epoch: sum(r(s)) - uint256 public totalServiceETHRevenue; + uint256 public totalServiceRevenueETH; + + // Staking parameters with multiplying by 100 + // treasuryFraction + componentFraction + agentFraction + stakerFraction = 100% + uint256 public treasuryFraction = 0; + uint256 public stakerFraction = 50; + uint256 public componentFraction = 33; + uint256 public agentFraction = 17; + + //Discount Factor v2 + //Bond(t) + uint256 private _bondPerEpoch; + // MaxBond(e) - sum(BondingProgram) + uint256 private _bondLeft = maxBond; // Component Registry address public immutable componentRegistry; @@ -62,17 +71,30 @@ contract Tokenomics is IErrors, Ownable { // Mapping of epoch => point mapping(uint256 => PointEcomonics) public mapEpochEconomics; + // Set of UCFc(epoch) + uint256[] private _ucfcs; + // Set of UCFa(epoch) + uint256[] private _ucfas; + // Set of profitable components in current epoch + address[] private _profitableComponents; + // Set of profitable agents in current epoch + address[] private _profitableAgents; // Set of protocol-owned services in current epoch uint256[] private _protocolServiceIds; // Map of service Ids and their amounts in current epoch mapping(uint256 => uint256) private _mapServiceAmounts; mapping(uint256 => uint256) private _mapServiceIndexes; + // TODO sync address constants with other contracts + address public constant ETH_TOKEN_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + // TODO later fix government / manager - constructor(address _ola, address _treasury, uint256 _epochLen, address _componentRegistry, address _agentRegistry, address payable _serviceRegistry) + constructor(address _ola, address _treasury, address _depository, uint256 _epochLen, address _componentRegistry, address _agentRegistry, + address payable _serviceRegistry) { ola = _ola; treasury = _treasury; + depository = _depository; epochLen = _epochLen; componentRegistry = _componentRegistry; agentRegistry = _agentRegistry; @@ -87,11 +109,26 @@ contract Tokenomics is IErrors, Ownable { _; } + // Only the manager has a privilege to manipulate a tokenomics + modifier onlyDepository() { + if (depository != msg.sender) { + revert ManagerOnly(msg.sender, depository); + } + _; + } + + /// @dev Changes treasury address. function changeTreasury(address newTreasury) external onlyOwner { treasury = newTreasury; emit TreasuryUpdated(newTreasury); } + /// @dev Changes treasury address. + function changeDepository(address newDepository) external onlyOwner { + depository = newDepository; + emit DepositoryUpdated(newDepository); + } + /// @dev Gets curretn epoch number. function getEpoch() public view returns (uint256 epoch) { epoch = block.number / epochLen; @@ -104,18 +141,69 @@ contract Tokenomics is IErrors, Ownable { /// @param _gammaNumerator Numerator for gamma value. /// @param _gammaDenominator Denominator for gamma value. /// @param _maxDF Maximum interest rate in %, 18 decimals. - function changeParameters( + /// @param _maxBond MaxBond OLA, 18 decimals + function changeTokenomicsParameters( uint256 _alphaNumerator, uint256 _alphaDenominator, uint256 _beta, uint256 _gammaNumerator, uint256 _gammaDenominator, - uint256 _maxDF + uint256 _maxDF, + uint256 _maxBond ) external onlyOwner { alpha = FixedPoint.fraction(_alphaNumerator, _alphaDenominator); beta = _beta; gamma = FixedPoint.fraction(_gammaNumerator, _gammaDenominator); maxDF = _maxDF + E13; + // take into account the change during the epoch + if(_maxBond > maxBond) { + uint256 delta = _maxBond - maxBond; + _bondLeft += delta; + } + if(_maxBond < maxBond) { + uint256 delta = maxBond - _maxBond; + if(delta < _bondLeft) { + _bondLeft -= delta; + } else { + _bondLeft = 0; + } + } + maxBond = _maxBond; + } + + /// @dev Sets staking parameters in fractions of distributed rewards. + /// @param _stakerFraction Fraction for stakers. + /// @param _componentFraction Fraction for component owners. + function changeRewardFraction( + uint256 _treasuryFraction, + uint256 _stakerFraction, + uint256 _componentFraction, + uint256 _agentFraction + ) external onlyOwner { + // Check that the sum of fractions is 100% + if (_treasuryFraction + _stakerFraction + _componentFraction + _agentFraction != 100) { + revert WrongAmount(_treasuryFraction + _stakerFraction + _componentFraction + _agentFraction, 100); + } + + treasuryFraction = _treasuryFraction; + stakerFraction = _stakerFraction; + componentFraction = _componentFraction; + agentFraction = _agentFraction; + } + + /// @dev take into account the bonding program in this epoch. + /// @dev programs exceeding the limit in the epoch are not allowed + function allowedNewBond(uint256 amount) external onlyDepository returns (bool) { + if(_bondLeft >= amount) { + _bondLeft -= amount; + return true; + } + return false; + } + + /// @dev take into account materialization OLA per Depository.deposit() for currents program + function usedBond(uint256 payout) external onlyDepository { + _bondPerEpoch += payout; } /// @dev Tracks the deposit token amount during the epoch. @@ -125,7 +213,7 @@ contract Tokenomics is IErrors, Ownable { uint256 numServices = serviceIds.length; for (uint256 i = 0; i < numServices; ++i) { // Check for the service Id existance - if (!ServiceRegistry(serviceRegistry).exists(serviceIds[i])) { + if (!IService(serviceRegistry).exists(serviceIds[i])) { revert ServiceDoesNotExist(serviceIds[i]); } @@ -137,7 +225,7 @@ contract Tokenomics is IErrors, Ownable { _mapServiceAmounts[serviceIds[i]] += amounts[i]; // Increase also the total service revenue - totalServiceETHRevenue += amounts[i]; + totalServiceRevenueETH += amounts[i]; } } @@ -160,25 +248,31 @@ contract Tokenomics is IErrors, Ownable { return success; } - function _calculateUCFc() private view returns (FixedPoint.uq112x112 memory ucfc) { - ComponentRegistry cRegistry = ComponentRegistry(componentRegistry); - uint256 numComponents = cRegistry.totalSupply(); + function _calculateUCFc() private returns (FixedPoint.uq112x112 memory ucfc) { + uint256 numComponents = IERC721Enumerable(componentRegistry).totalSupply(); uint256 numProfitableComponents; uint256 numServices = _protocolServiceIds.length; - // Array of sum(UCFc(epoch)) - uint256[] memory ucfcs = new uint256[](numServices); + + // Clear the previous epoch profitable set of components + delete _profitableComponents; + delete _ucfcs; + + // Allocate set of UCFc for the current epoch number of services + _ucfcs = new uint256[](numServices); // Array of cardinality of components in a specific profitable service: |Cs(epoch)| uint256[] memory ucfcsNum = new uint256[](numServices); + // Loop over components for (uint256 i = 0; i < numComponents; ++i) { - (, uint256[] memory serviceIds) = ServiceRegistry(serviceRegistry).getServiceIdsCreatedWithComponentId(cRegistry.tokenByIndex(i)); + uint256 componentId = IERC721Enumerable(componentRegistry).tokenByIndex(i); + (, uint256[] memory serviceIds) = IService(serviceRegistry).getServiceIdsCreatedWithComponentId(componentId); bool profitable = false; // Loop over services that include the component i for (uint256 j = 0; j < serviceIds.length; ++j) { uint256 revenue = _mapServiceAmounts[serviceIds[j]]; if (revenue > 0) { // Add cit(c, s) * r(s) for component j to add to UCFc(epoch) - ucfcs[_mapServiceIndexes[serviceIds[j]]] += _mapServiceAmounts[serviceIds[j]]; + _ucfcs[_mapServiceIndexes[serviceIds[j]]] += _mapServiceAmounts[serviceIds[j]]; // Increase |Cs(epoch)| ucfcsNum[_mapServiceIndexes[serviceIds[j]]]++; profitable = true; @@ -186,19 +280,24 @@ contract Tokenomics is IErrors, Ownable { } // If at least one service has profitable component, increase the component cardinality: Cref(epoch-1) if (profitable) { + // Add address of a profitable component owner + address owner = IERC721Enumerable(componentRegistry).ownerOf(componentId); + _profitableComponents.push(owner); + // Increase the profitable component number ++numProfitableComponents; } } + uint256 denominator; // Calculate total UCFc for (uint256 i = 0; i < numServices; ++i) { denominator = ucfcsNum[_mapServiceIndexes[_protocolServiceIds[i]]]; if(denominator > 0) { // avoid exception div by zero - ucfc = _add(ucfc, FixedPoint.fraction(ucfcs[_mapServiceIndexes[_protocolServiceIds[i]]], denominator)); + ucfc = _add(ucfc, FixedPoint.fraction(_ucfcs[_mapServiceIndexes[_protocolServiceIds[i]]], denominator)); } } - ucfc = ucfc.muluq(FixedPoint.fraction(1, totalServiceETHRevenue)); + ucfc = ucfc.muluq(FixedPoint.fraction(1, totalServiceRevenueETH)); denominator = numServices * numComponents; if(denominator > 0) { // avoid exception div by zero @@ -208,25 +307,31 @@ contract Tokenomics is IErrors, Ownable { } } - function _calculateUCFa() private view returns (FixedPoint.uq112x112 memory ucfa) { - AgentRegistry aRegistry = AgentRegistry(agentRegistry); - uint256 numAgents = aRegistry.totalSupply(); + function _calculateUCFa() private returns (FixedPoint.uq112x112 memory ucfa) { + uint256 numAgents = IERC721Enumerable(agentRegistry).totalSupply(); uint256 numProfitableAgents; uint256 numServices = _protocolServiceIds.length; - // Array of sum(UCFa(epoch)) - uint256[] memory ucfas = new uint256[](numServices); + + // Clear the previous epoch profitable set of agents + delete _profitableAgents; + delete _ucfas; + + // Allocate set of UCFa for the current epoch number of services + _ucfas = new uint256[](numServices); // Array of cardinality of components in a specific profitable service: |As(epoch)| uint256[] memory ucfasNum = new uint256[](numServices); + // Loop over agents for (uint256 i = 0; i < numAgents; ++i) { - (, uint256[] memory serviceIds) = ServiceRegistry(serviceRegistry).getServiceIdsCreatedWithAgentId(aRegistry.tokenByIndex(i)); + uint256 agentId = IERC721Enumerable(agentRegistry).tokenByIndex(i); + (, uint256[] memory serviceIds) = IService(serviceRegistry).getServiceIdsCreatedWithAgentId(agentId); bool profitable = false; // Loop over services that include the agent i for (uint256 j = 0; j < serviceIds.length; ++j) { uint256 revenue = _mapServiceAmounts[serviceIds[j]]; if (revenue > 0) { // Add cit(c, s) * r(s) for component j to add to UCFa(epoch) - ucfas[_mapServiceIndexes[serviceIds[j]]] += _mapServiceAmounts[serviceIds[j]]; + _ucfas[_mapServiceIndexes[serviceIds[j]]] += _mapServiceAmounts[serviceIds[j]]; // Increase |As(epoch)| ucfasNum[_mapServiceIndexes[serviceIds[j]]]++; profitable = true; @@ -234,19 +339,24 @@ contract Tokenomics is IErrors, Ownable { } // If at least one service has profitable component, increase the component cardinality: Cref(epoch-1) if (profitable) { + // Add address of a profitable component owner + address owner = IERC721Enumerable(agentRegistry).ownerOf(agentId); + _profitableAgents.push(owner); + // Increase a profitable agent number ++numProfitableAgents; } } + uint256 denominator; // Calculate total UCFa for (uint256 i = 0; i < numServices; ++i) { denominator = ucfasNum[_mapServiceIndexes[_protocolServiceIds[i]]]; if(denominator > 0) { // avoid div by zero - ucfa = _add(ucfa, FixedPoint.fraction(ucfas[_mapServiceIndexes[_protocolServiceIds[i]]], denominator)); + ucfa = _add(ucfa, FixedPoint.fraction(_ucfas[_mapServiceIndexes[_protocolServiceIds[i]]], denominator)); } } - ucfa = ucfa.muluq(FixedPoint.fraction(1, totalServiceETHRevenue)); + ucfa = ucfa.muluq(FixedPoint.fraction(1, totalServiceRevenueETH)); denominator = numServices * numAgents; if(denominator > 0) { // avoid div by zero @@ -306,16 +416,19 @@ contract Tokenomics is IErrors, Ownable { delete _mapServiceIndexes[_protocolServiceIds[i]]; } delete _protocolServiceIds; - totalServiceETHRevenue = 0; + totalServiceRevenueETH = 0; + // clean bonding data + _bondLeft = maxBond; + _bondPerEpoch = 0; } /// @notice Record global data to checkpoint, any can do it /// @dev Checked point exist or not - function checkpoint() external { + function checkpoint() external onlyTreasury { uint256 epoch = getEpoch(); PointEcomonics memory lastPoint = mapEpochEconomics[epoch]; // if not exist - if(!lastPoint._exist) { + if(!lastPoint.exists) { _checkpoint(epoch); } } @@ -328,10 +441,11 @@ contract Tokenomics is IErrors, Ownable { FixedPoint.uq112x112 memory _dcm; // df = 1/(1 + iterest_rate) by documantation, reverse_df = 1/df >= 1.0. FixedPoint.uq112x112 memory _df; + // Calculate UCF, USF // TODO Look for optimization possibilities - // Calculate total UCFc - if (totalServiceETHRevenue > 0) { + if (totalServiceRevenueETH > 0) { + // Calculate total UCFc FixedPoint.uq112x112 memory _ucfc = _calculateUCFc(); // Calculate total UCFa @@ -350,9 +464,9 @@ contract Tokenomics is IErrors, Ownable { for (uint256 i = 0; i < numServices; ++i) { usf += _mapServiceAmounts[_protocolServiceIds[i]]; } - uint256 denominator = totalServiceETHRevenue * ServiceRegistry(serviceRegistry).totalSupply(); + uint256 denominator = totalServiceRevenueETH * IERC721Enumerable(serviceRegistry).totalSupply(); if(denominator > 0) { - // _usf = usf / ServiceRegistry(serviceRegistry).totalSupply(); + // _usf = usf / IERC721Enumerable(serviceRegistry).totalSupply(); _usf = FixedPoint.fraction(usf, denominator); } //_dcm = (_ucf + _usf) / 2; @@ -362,9 +476,10 @@ contract Tokenomics is IErrors, Ownable { } } - // ToDO :: df/iterest rate + uint256 totalRewardOLA = _getExchangeAmountOLA(ETH_TOKEN_ADDRESS, totalServiceRevenueETH); _df = _calculateDFv1(_dcm); - PointEcomonics memory newPoint = PointEcomonics({ucf: _ucf, usf: _usf, df: _df, ts: block.timestamp, blk: block.number, _exist: true }); + PointEcomonics memory newPoint = PointEcomonics(_ucf, _usf, _df, treasuryFraction, stakerFraction, + componentFraction, agentFraction, totalRewardOLA, block.timestamp, block.number, true); mapEpochEconomics[epoch] = newPoint; _clearEpochData(); @@ -375,16 +490,27 @@ contract Tokenomics is IErrors, Ownable { /// @param tokenAmount Token amount. /// @param _epoch epoch number /// @return resAmount Resulting amount of OLA tokens. - function calculatePayoutFromLP(address token, uint256 tokenAmount, uint _epoch) external + function calculatePayoutFromLP(address token, uint256 tokenAmount, uint _epoch) external view returns (uint256 resAmount) { - PointEcomonics memory _PE = mapEpochEconomics[_epoch]; - if (!_PE._exist) { - _checkpoint(_epoch); - _PE = mapEpochEconomics[_epoch]; + uint256 df; + PointEcomonics memory _PE; + // avoid start checkpoint from calculatePayoutFromLP + uint256 _epochC = _epoch + 1; + for (uint256 i = _epochC; i > 0; i--) { + _PE = mapEpochEconomics[i-1]; + // if current point undefined, so calculatePayoutFromLP called before mined tx(checkpoint) + if(_PE.exists) { + df = uint256(_PE.df._x / MAGIC_DENOMINATOR); + break; + } + } + if(df > 0) { + resAmount = _calculatePayoutFromLP(token, tokenAmount, df); + } else { + // if df undefined in points + resAmount = _calculatePayoutFromLP(token, tokenAmount, INITIAL_DF); } - uint256 df = uint256(_PE.df._x / MAGIC_DENOMINATOR); - resAmount = _calculatePayoutFromLP(token, tokenAmount, df); } /// @dev Calculates the amount of OLA tokens based on LP (see the doc for explanation of price computation). @@ -464,28 +590,59 @@ contract Tokenomics is IErrors, Ownable { _PE = mapEpochEconomics[_epoch]; } - // decode a uq112x112 into a uint with 18 decimals of precision, 0 if not exist + /// @dev Get last epoch Point. + function getLastPoint() external view returns (PointEcomonics memory _PE) { + uint256 epoch = getEpoch(); + _PE = mapEpochEconomics[epoch]; + } + + // decode a uq112x112 into a uint with 18 decimals of precision (cycle into the past), INITIAL_DF if not exist function getDF(uint256 _epoch) public view returns (uint256 df) { - PointEcomonics memory _PE = mapEpochEconomics[_epoch]; - if (_PE._exist) { - // https://github.com/compound-finance/open-oracle/blob/d0a0d0301bff08457d9dfc5861080d3124d079cd/contracts/Uniswap/UniswapLib.sol#L27 - // a/b is encoded as (a << 112) / b or (a * 2^112) / b - df = uint256(_PE.df._x / MAGIC_DENOMINATOR); // 2^(112 - log2(1e18)) - } else { - df = 0; + PointEcomonics memory _PE; + uint256 _epochC = _epoch + 1; + for (uint256 i = _epochC; i > 0; i--) { + _PE = mapEpochEconomics[i-1]; + // if current point undefined, so getDF called before mined tx(checkpoint) + if(_PE.exists) { + // https://github.com/compound-finance/open-oracle/blob/d0a0d0301bff08457d9dfc5861080d3124d079cd/contracts/Uniswap/UniswapLib.sol#L27 + // a/b is encoded as (a << 112) / b or (a * 2^112) / b + df = uint256(_PE.df._x / MAGIC_DENOMINATOR); + break; + } + } + if (df == 0) { + df = INITIAL_DF; } } - // decode a uq112x112 into a uint with 18 decimals of precision, re-calc if not exist - function getDFForEpoch(uint256 _epoch) external returns (uint256 df) { - PointEcomonics memory _PE = mapEpochEconomics[_epoch]; - if (!_PE._exist) { - _checkpoint(_epoch); - _PE = mapEpochEconomics[_epoch]; - } - // https://github.com/compound-finance/open-oracle/blob/d0a0d0301bff08457d9dfc5861080d3124d079cd/contracts/Uniswap/UniswapLib.sol#L27 - // a/b is encoded as (a << 112) / b or (a * 2^112) / b - df = uint256(_PE.df._x / MAGIC_DENOMINATOR); // 2^(112 - log2(1e18)) + /// @dev Gets exchange rate for OLA. + /// @param token Token address to be exchanged for OLA. + /// @param tokenAmount Token amount. + /// @return amountOLA Amount of OLA tokens. + function _getExchangeAmountOLA(address token, uint256 tokenAmount) private pure returns (uint256 amountOLA) { + // TODO Exchange rate is a stub for now + amountOLA = tokenAmount; + } + + function getProfitableComponents() external view + returns (address[] memory profitableComponents, uint256[] memory ucfcs) + { + profitableComponents = _profitableComponents; + ucfcs = _ucfcs; + } + + function getProfitableAgents() external view + returns (address[] memory profitableAgents, uint256[] memory ucfas) + { + profitableAgents = _profitableAgents; + ucfas = _ucfas; } + function getBondLeft() external view returns (uint256 bondLeft) { + bondLeft = _bondLeft; + } + + function getBondCurrentEpoch() external view returns (uint256 bondPerEpoch) { + bondPerEpoch = _bondPerEpoch; + } } diff --git a/contracts/Treasury.sol b/contracts/Treasury.sol index 6bd61022..6f134332 100644 --- a/contracts/Treasury.sol +++ b/contracts/Treasury.sol @@ -4,13 +4,15 @@ pragma solidity ^0.8.4; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "./interfaces/IDispenser.sol"; import "./interfaces/IErrors.sol"; import "./interfaces/IOLA.sol"; import "./interfaces/ITokenomics.sol"; +import "./interfaces/IStructs.sol"; /// @title Treasury - Smart contract for managing OLA Treasury /// @author AL -contract Treasury is IErrors, Ownable, ReentrancyGuard { +contract Treasury is IErrors, IStructs, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; event DepositFromDepository(address token, uint256 tokenAmount, uint256 olaMintAmount); @@ -19,8 +21,12 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { event TokenReserves(address token, uint256 reserves); event EnableToken(address token); event DisableToken(address token); - event TreasuryManagerUpdated(address manager); + event TreasuryUpdated(address treasury); + event TokenomicsUpdated(address tokenomics); event DepositoryUpdated(address depository); + event DispenserUpdated(address dispenser); + event TransferToDispenser(uint256 amount); + event TransferToProtocol(uint256 amount); enum TokenState { NonExistent, @@ -37,10 +43,12 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { // OLA token address address public immutable ola; - // Tokenomics contract address - address public tokenomics; // Depository address address public depository; + // Dispenser contract address + address public dispenser; + // Tokenomics contract address + address public tokenomics; // Set of registered tokens address[] public tokenRegistry; // Token address => token info @@ -49,14 +57,15 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { // https://developer.kyber.network/docs/DappsGuide#contract-example address public constant ETH_TOKEN_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); // well-know representation ETH as address - constructor(address _ola, address _depository, address _tokenomics) { + constructor(address _ola, address _depository, address _tokenomics, address _dispenser) { if (_ola == address(0)) { revert ZeroAddress(); } ola = _ola; mapTokens[ETH_TOKEN_ADDRESS].state = TokenState.Enabled; - tokenomics = _tokenomics; depository = _depository; + dispenser = _dispenser; + tokenomics = _tokenomics; } // Only the depository has a privilege to control some actions of a treasury @@ -67,6 +76,13 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { _; } + modifier onlyDispenser() { + if (dispenser != msg.sender) { + revert ManagerOnly(msg.sender, dispenser); + } + _; + } + /// @dev Changes the depository address. /// @param newDepository Address of a new depository. function changeDepository(address newDepository) external onlyOwner { @@ -74,6 +90,18 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { emit DepositoryUpdated(newDepository); } + /// @dev Changes dispenser address. + /// @param newDispenser Address of a new dispenser. + function changeDispenser(address newDispenser) external onlyOwner { + dispenser = newDispenser; + emit DispenserUpdated(newDispenser); + } + + function changeTokenomics(address newTokenomics) external onlyOwner { + tokenomics = newTokenomics; + emit TokenomicsUpdated(newTokenomics); + } + /// @dev Allows approved address to deposit an asset for OLA. /// @param tokenAmount Token amount to get OLA for. /// @param token Token address. @@ -192,4 +220,76 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { function isEnabled(address token) public view returns (bool enabled) { enabled = (mapTokens[token].state == TokenState.Enabled); } + + /// @dev Sends OLA funds to dispenser. + /// @param amount Amount of OLA. + function _sendFundsToDispenser(uint256 amount) internal { + if (amount > 0) { + // Check current OLA balance + uint256 balance = IOLA(ola).balanceOf(address(this)); + + // If the balance is insufficient, mint the difference + // TODO Check if minting is not causing the inflation go beyond the limits, and refuse if that's the case + // TODO or allocate OLA tokens differently as by means suggested (breaking up LPs etc) + if (amount > balance) { + balance = amount - balance; + IOLA(ola).mint(address(this), balance); + } + + // Transfer funds to the dispenser + IERC20(ola).safeTransfer(dispenser, amount); + + emit TransferToDispenser(amount); + } + } + + /// @dev Sends (mints) funds to itself + /// @param amount OLA amount. + function _sendFundsToProtocol(uint256 amount) internal { + if (amount > 0) { + IOLA(ola).mint(address(this), amount); + emit TransferToProtocol(amount); + } + } + + /// @dev Starts a new epoch. + function allocateRewards() external onlyOwner returns (bool) { + // Gets the latest economical point of epoch + PointEcomonics memory point = ITokenomics(tokenomics).getLastPoint(); + // If the point exists, it was already started and there is no need to continue + if (point.exists) { + return false; + } + + // Process the epoch data + ITokenomics(tokenomics).checkpoint(); + point = ITokenomics(tokenomics).getLastPoint(); + + // Request OLA funds from treasury for the last epoch + uint256 amountOLA = point.totalRewardOLA; + // Get OLA amount that has to stay as a reward in Treasury + uint256 protocolReward = amountOLA * point.treasuryFraction / 100; + uint256 stakerReward = amountOLA * point.stakerFraction / 100; + uint256 componentReward = amountOLA * point.componentFraction / 100; + uint256 agentReward = amountOLA * point.agentFraction / 100; + + // Protocol reward must be lower than the overall reward + if (amountOLA < protocolReward) { + revert AmountLowerThan(amountOLA, protocolReward); + } + amountOLA -= protocolReward; + + // Send funds to protocol + _sendFundsToProtocol(protocolReward); + + if (!IDispenser(dispenser).isPaused()) { + // Send funds to dispenser + _sendFundsToDispenser(amountOLA); + // Distribute rewards + if (amountOLA > 0) { + IDispenser(dispenser).distributeRewards(stakerReward, componentReward, agentReward); + } + } + return true; + } } diff --git a/contracts/governance/VotingEscrow.sol b/contracts/governance/VotingEscrow.sol index 8bc8ac09..390724bf 100644 --- a/contracts/governance/VotingEscrow.sol +++ b/contracts/governance/VotingEscrow.sol @@ -5,8 +5,10 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/governance/utils/IVotes.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "./ERC20VotesCustom.sol"; +import "../interfaces/IDispenser.sol"; /** @title Voting Escrow @@ -60,6 +62,8 @@ struct LockedBalance { /// @notice This token supports the ERC20 interface specifications except for transfers. contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { + using SafeERC20 for IERC20; + enum DepositType { DEPOSIT_FOR_TYPE, CREATE_LOCK_TYPE, @@ -74,17 +78,28 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { DepositType depositType, uint256 ts ); + event Withdraw(address indexed provider, uint256 value, uint256 ts); event Supply(uint256 prevSupply, uint256 supply); + event DispenserUpdated(address dispenser); uint256 internal constant WEEK = 1 weeks; uint256 internal constant MAXTIME = 4 * 365 * 86400; int128 internal constant iMAXTIME = 4 * 365 * 86400; uint256 internal constant MULTIPLIER = 1 ether; + // Token address address immutable public token; + // Dispenser address + address public dispenser; + // Total token supply uint256 public supply; + // Mapping of account address => LockedBalance mapping(address => LockedBalance) public locked; + // Mapping Id => account address + mapping(address => uint256) private _mapAccountIds; + // Set of locking accounts + address[] private _accounts; uint256 public epoch; mapping(uint256 => Point) public pointHistory; // epoch -> unsigned point @@ -111,7 +126,7 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { /// @param _name Token name /// @param _symbol Token symbol /// @param _version Contract version - required for Aragon compatibility - constructor(address tokenAddr, string memory _name, string memory _symbol, string memory _version) + constructor(address tokenAddr, string memory _name, string memory _symbol, string memory _version, address _dispenser) { token = tokenAddr; pointHistory[0].blk = block.number; @@ -125,6 +140,14 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { if (decimals > 255) { revert Overflow(uint256(decimals), 255); } + dispenser = _dispenser; + } + + /// @dev Changes dispenser address. + /// @param newDispenser Address of a new dispenser. + function changeDispenser(address newDispenser) external onlyOwner { + dispenser = newDispenser; + emit DispenserUpdated(newDispenser); } /// @dev Set an external contract to check for approved smart contract wallets @@ -350,9 +373,7 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { address from = msg.sender; if (_value != 0) { - if (!ERC20(token).transferFrom(from, address(this), _value)) { - revert TransferFailed(token, from, address(this), _value); - } + IERC20(token).safeTransferFrom(from, address(this), _value); } emit Deposit(_addr, _value, _locked.end, depositType, block.timestamp); @@ -406,6 +427,11 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { } _depositFor(msg.sender, _value, unlockTime, _locked, DepositType.CREATE_LOCK_TYPE); + + // Add to the map for subsequent cleaning during the withdraw + uint256 id = _accounts.length; + _mapAccountIds[msg.sender] = id; + _accounts.push(msg.sender); } /// @dev Deposit `_value` additional tokens for `msg.sender` without modifying the unlock time @@ -470,10 +496,22 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { // Both can have >= 0 amount _checkpoint(msg.sender, _locked, LockedBalance(0,0)); - assert(ERC20(token).transfer(msg.sender, value)); + // Return value from staking + value += IDispenser(dispenser).withdrawStakingRewards(msg.sender); + + // Clean up the account information + uint256 id = _mapAccountIds[msg.sender]; + uint256 numAccounts = _accounts.length; + _accounts[id] = _accounts[numAccounts - 1]; + address addr = _accounts[id]; + _accounts.pop(); + _mapAccountIds[addr] = id; + _mapAccountIds[msg.sender] = 0; emit Withdraw(msg.sender, value, block.timestamp); emit Supply(supplyBefore, supplyBefore - value); + + IERC20(token).safeTransfer(msg.sender, value); } /// @dev Binary search to estimate timestamp for block number @@ -633,8 +671,8 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { /// @dev Calculate total voting power at some point in the past. /// @param blockNumber Block number to calculate the total voting power at. - /// @return supply Total voting power. - function getPastTotalSupply(uint256 blockNumber) public view override returns (uint256 supply) { + /// @return Total voting power. + function getPastTotalSupply(uint256 blockNumber) public view override returns (uint256) { if (blockNumber > block.number) { revert WrongBlockNumber(blockNumber, block.number); } @@ -656,4 +694,9 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { // Now dt contains info on how far are we beyond point return supplyLockedAt(point, point.ts + dt); } + + /// @dev Gets the set of current locking accounts + function getLockAccounts() external view returns (address[] memory accounts) { + accounts = _accounts; + } } diff --git a/contracts/interfaces/IDispenser.sol b/contracts/interfaces/IDispenser.sol new file mode 100644 index 00000000..975b111e --- /dev/null +++ b/contracts/interfaces/IDispenser.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @dev Interface for dispenser management. +interface IDispenser { + /// @dev Distributes rewards. + function distributeRewards( + uint256 stakerReward, + uint256 componentReward, + uint256 agentReward + ) external; + + /// @dev Withdraws rewards for stakers. + /// @param account Account address. + /// @return balance Reward balance. + function withdrawStakingRewards(address account) external returns (uint256 balance); + + /// @dev Gets the paused state. + /// @return True, if paused. + function isPaused() external returns (bool); +} diff --git a/contracts/interfaces/IService.sol b/contracts/interfaces/IService.sol index 763fd520..de7ad62f 100644 --- a/contracts/interfaces/IService.sol +++ b/contracts/interfaces/IService.sol @@ -96,4 +96,23 @@ interface IService is IStructs { /// @param serviceId Correspondent service Id. /// @return success True, if function executed successfully. function destroy(address owner, uint256 serviceId) external returns (bool success); + + /// @dev Checks if the service Id exists. + /// @param serviceId Service Id. + /// @return true if the service exists, false otherwise. + function exists(uint256 serviceId) external view returns (bool); + + /// @dev Gets the set of service Ids that contain specified agent Id. + /// @param agentId Agent Id. + /// @return numServiceIds Number of service Ids. + /// @return serviceIds Set of service Ids. + function getServiceIdsCreatedWithAgentId(uint256 agentId) external view + returns (uint256 numServiceIds, uint256[] memory serviceIds); + + /// @dev Gets the set of service Ids that contain specified component Id (through the agent Id). + /// @param componentId Component Id. + /// @return numServiceIds Number of service Ids. + /// @return serviceIds Set of service Ids. + function getServiceIdsCreatedWithComponentId(uint256 componentId) external view + returns (uint256 numServiceIds, uint256[] memory serviceIds); } diff --git a/contracts/interfaces/IStructs.sol b/contracts/interfaces/IStructs.sol index 6aa2f4a6..045434fa 100644 --- a/contracts/interfaces/IStructs.sol +++ b/contracts/interfaces/IStructs.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; +import "@uniswap/lib/contracts/libraries/FixedPoint.sol"; + /// @dev IPFS multihash. interface IStructs { // Canonical agent Id parameters @@ -20,4 +22,18 @@ interface IStructs { // Length of the hash is 32 bytes, or 0x20 in hex uint8 size; } + + struct PointEcomonics { + FixedPoint.uq112x112 ucf; + FixedPoint.uq112x112 usf; + FixedPoint.uq112x112 df; // x > 1.0 + uint256 treasuryFraction; + uint256 stakerFraction; + uint256 componentFraction; + uint256 agentFraction; + uint256 totalRewardOLA; + uint256 ts; // timestamp + uint256 blk; // block + bool exists; // ready or not + } } diff --git a/contracts/interfaces/ITokenomics.sol b/contracts/interfaces/ITokenomics.sol index 151d0c6e..ecb8c19a 100644 --- a/contracts/interfaces/ITokenomics.sol +++ b/contracts/interfaces/ITokenomics.sol @@ -1,11 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; +import "./IStructs.sol"; + /// @dev Interface for tokenomics management. -interface ITokenomics { +interface ITokenomics is IStructs { + function epochLen() external view returns (uint256); function getDF(uint256 epoch) external view returns (uint256 df); function getEpochLen() external view returns (uint256); + function getLastPoint() external view returns (PointEcomonics memory _PE); function calculatePayoutFromLP(address token, uint256 tokenAmount, uint _epoch) external returns (uint256 resAmount); function trackServicesETHRevenue(uint256[] memory serviceIds, uint256[] memory amounts) external; + function checkpoint() external; + function getExchangeAmountOLA(address token, uint256 tokenAmount) external returns (uint256 amount); + function getProfitableComponents() external view returns (address[] memory profitableComponents, uint256[] memory ucfcs); + function getProfitableAgents() external view returns (address[] memory profitableAgents, uint256[] memory ucfcs); + function usedBond(uint256 payout) external; + function allowedNewBond(uint256 amount) external returns(bool); + function getBondLeft() external view returns (uint256 bondLeft); + function getBondCurrentEpoch() external view returns (uint256 bondPerEpoch); } diff --git a/contracts/interfaces/ITreasury.sol b/contracts/interfaces/ITreasury.sol index 9c072601..85be3fb1 100644 --- a/contracts/interfaces/ITreasury.sol +++ b/contracts/interfaces/ITreasury.sol @@ -26,4 +26,10 @@ interface ITreasury { /// @param token Token address. /// @return enabled True is token is enabled. function isEnabled(address token) external view returns (bool enabled); + + /// @dev Requests OLA funds from treasury. + function requestFunds(uint256 amount) external; + + /// @dev Starts a new epoch. + function allocateRewards() external returns (bool); } diff --git a/deploy/contracts.js b/deploy/contracts.js index 92a110df..e3c2aa97 100644 --- a/deploy/contracts.js +++ b/deploy/contracts.js @@ -126,7 +126,7 @@ module.exports = async () => { console.log("OLA token deployed to", token.address); const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - const escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1"); + const escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1", AddressZero); await escrow.deployed(); console.log("Voting Escrow deployed to", escrow.address); diff --git a/hardhat.config.js b/hardhat.config.js index c9e3d370..ae772247 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -8,13 +8,19 @@ require("hardhat-contract-sizer"); /** * @type import('hardhat/config').HardhatUserConfig */ +const accounts = { + mnemonic: "test test test test test test test test test test test junk", + accountsBalance: "100000000000000000000000000", +}; + module.exports = { networks: { ganache: { url: "http://localhost:8545", }, hardhat: { - allowUnlimitedContractSize: true + allowUnlimitedContractSize: true, + accounts }, }, solidity: { diff --git a/scripts/deploy.js b/scripts/deploy.js index 3704810d..da9f1026 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -126,7 +126,7 @@ async function main() { console.log("OLA token deployed to", token.address); const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - const escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1"); + const escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1", AddressZero); await escrow.deployed(); console.log("Voting Escrow deployed to", escrow.address); diff --git a/scripts/uni-adjust/adjust-point.sh b/scripts/uni-adjust/adjust-point.sh index 27e39e52..2c0d0281 100755 --- a/scripts/uni-adjust/adjust-point.sh +++ b/scripts/uni-adjust/adjust-point.sh @@ -25,19 +25,321 @@ case "$(uname -s)" in esac FILE="./node_modules/@uniswap/lib/contracts/libraries/FixedPoint.sol" +rm -rf $FILE case "$(uname -s)" in Darwin) echo 'Mac OS X' - sed -i.bu "s/uint112(-1)/type(uint112).max/g" $FILE - sed -i.bu "s/uint224(-1)/type(uint224).max/g" $FILE - sed -i.bu "s/uint144(-1)/type(uint144).max/g" $FILE - ;; +cat << 'EOF' > ./node_modules/@uniswap/lib/contracts/libraries/FixedPoint.sol +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.4.0; + +import './FullMath.sol'; +import './Babylonian.sol'; +import './BitMath.sol'; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) +library FixedPoint { + // range: [0, 2**112 - 1] + // resolution: 1 / 2**112 + struct uq112x112 { + uint224 _x; + } + + // range: [0, 2**144 - 1] + // resolution: 1 / 2**112 + struct uq144x112 { + uint256 _x; + } + + uint8 public constant RESOLUTION = 112; + uint256 public constant Q112 = 0x10000000000000000000000000000; // 2**112 + uint256 private constant Q224 = 0x100000000000000000000000000000000000000000000000000000000; // 2**224 + uint256 private constant LOWER_MASK = 0xffffffffffffffffffffffffffff; // decimal of UQ*x112 (lower 112 bits) + + // encode a uint112 as a UQ112x112 + function encode(uint112 x) internal pure returns (uq112x112 memory) { + return uq112x112(uint224(x) << RESOLUTION); + } + + // encodes a uint144 as a UQ144x112 + function encode144(uint144 x) internal pure returns (uq144x112 memory) { + return uq144x112(uint256(x) << RESOLUTION); + } + + // decode a UQ112x112 into a uint112 by truncating after the radix point + function decode(uq112x112 memory self) internal pure returns (uint112) { + return uint112(self._x >> RESOLUTION); + } + + // decode a UQ144x112 into a uint144 by truncating after the radix point + function decode144(uq144x112 memory self) internal pure returns (uint144) { + return uint144(self._x >> RESOLUTION); + } + + // multiply a UQ112x112 by a uint, returning a UQ144x112 + // reverts on overflow + function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) { + unchecked { + uint256 z = 0; + require(y == 0 || (z = self._x * y) / y == self._x, 'FixedPoint::mul: overflow'); + return uq144x112(z); + } + } + + // multiply a UQ112x112 by an int and decode, returning an int + // reverts on overflow + function muli(uq112x112 memory self, int256 y) internal pure returns (int256) { + unchecked { + uint256 z = FullMath.mulDiv(self._x, uint256(y < 0 ? -y : y), Q112); + require(z < 2**255, 'FixedPoint::muli: overflow'); + return y < 0 ? -int256(z) : int256(z); + } + } + + // multiply a UQ112x112 by a UQ112x112, returning a UQ112x112 + // lossy + function muluq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { + if (self._x == 0 || other._x == 0) { + return uq112x112(0); + } + unchecked { + uint112 upper_self = uint112(self._x >> RESOLUTION); // * 2^0 + uint112 lower_self = uint112(self._x & LOWER_MASK); // * 2^-112 + uint112 upper_other = uint112(other._x >> RESOLUTION); // * 2^0 + uint112 lower_other = uint112(other._x & LOWER_MASK); // * 2^-112 + + // partial products + uint224 upper = uint224(upper_self) * upper_other; // * 2^0 + uint224 lower = uint224(lower_self) * lower_other; // * 2^-224 + uint224 uppers_lowero = uint224(upper_self) * lower_other; // * 2^-112 + uint224 uppero_lowers = uint224(upper_other) * lower_self; // * 2^-112 + // so the bit shift does not overflow + require(upper <= type(uint112).max, 'FixedPoint::muluq: upper overflow'); + + // this cannot exceed 256 bits, all values are 224 bits + uint256 sum = uint256(upper << RESOLUTION) + uppers_lowero + uppero_lowers + (lower >> RESOLUTION); + + // so the cast does not overflow + require(sum <= type(uint224).max, 'FixedPoint::muluq: sum overflow'); + return uq112x112(uint224(sum)); + } + } + + // divide a UQ112x112 by a UQ112x112, returning a UQ112x112 + function divuq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { + require(other._x > 0, 'FixedPoint::divuq: division by zero'); + if (self._x == other._x) { + return uq112x112(uint224(Q112)); + } + if (self._x <= type(uint144).max) { + uint256 value = (uint256(self._x) << RESOLUTION) / other._x; + require(value <= type(uint224).max, 'FixedPoint::divuq: overflow'); + return uq112x112(uint224(value)); + } + unchecked { + uint256 result = FullMath.mulDiv(Q112, self._x, other._x); + require(result <= type(uint224).max, 'FixedPoint::divuq: overflow'); + return uq112x112(uint224(result)); + } + } + + // returns a UQ112x112 which represents the ratio of the numerator to the denominator + // can be lossy + function fraction(uint256 numerator, uint256 denominator) internal pure returns (uq112x112 memory) { + require(denominator > 0, 'FixedPoint::fraction: division by zero'); + if (numerator == 0) return FixedPoint.uq112x112(0); + unchecked { + if (numerator <= type(uint144).max) { + uint256 result = (numerator << RESOLUTION) / denominator; + require(result <= type(uint224).max, 'FixedPoint::fraction: overflow'); + return uq112x112(uint224(result)); + } else { + uint256 result = FullMath.mulDiv(numerator, Q112, denominator); + require(result <= type(uint224).max, 'FixedPoint::fraction: overflow'); + return uq112x112(uint224(result)); + } + } + } + + // take the reciprocal of a UQ112x112 + // reverts on overflow + // lossy + function reciprocal(uq112x112 memory self) internal pure returns (uq112x112 memory) { + require(self._x != 0, 'FixedPoint::reciprocal: reciprocal of zero'); + require(self._x != 1, 'FixedPoint::reciprocal: overflow'); + return uq112x112(uint224(Q224 / self._x)); + } + + // square root of a UQ112x112 + // lossy between 0/1 and 40 bits + function sqrt(uq112x112 memory self) internal pure returns (uq112x112 memory) { + if (self._x <= type(uint144).max) { + return uq112x112(uint224(Babylonian.sqrt(uint256(self._x) << 112))); + } + uint8 safeShiftBits = 255 - BitMath.mostSignificantBit(self._x); + safeShiftBits -= safeShiftBits % 2; + return uq112x112(uint224(Babylonian.sqrt(uint256(self._x) << safeShiftBits) << ((112 - safeShiftBits) / 2))); + } +} +EOF + ;; Linux) echo 'Linux' - sed -i "s/uint112(-1)/type(uint112).max/g" $FILE - sed -i "s/uint224(-1)/type(uint224).max/g" $FILE - sed -i "s/uint144(-1)/type(uint144).max/g" $FILE +cat << EOF > ./node_modules/@uniswap/lib/contracts/libraries/FixedPoint.sol +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.4.0; + +import './FullMath.sol'; +import './Babylonian.sol'; +import './BitMath.sol'; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) +library FixedPoint { + // range: [0, 2**112 - 1] + // resolution: 1 / 2**112 + struct uq112x112 { + uint224 _x; + } + + // range: [0, 2**144 - 1] + // resolution: 1 / 2**112 + struct uq144x112 { + uint256 _x; + } + + uint8 public constant RESOLUTION = 112; + uint256 public constant Q112 = 0x10000000000000000000000000000; // 2**112 + uint256 private constant Q224 = 0x100000000000000000000000000000000000000000000000000000000; // 2**224 + uint256 private constant LOWER_MASK = 0xffffffffffffffffffffffffffff; // decimal of UQ*x112 (lower 112 bits) + + // encode a uint112 as a UQ112x112 + function encode(uint112 x) internal pure returns (uq112x112 memory) { + return uq112x112(uint224(x) << RESOLUTION); + } + + // encodes a uint144 as a UQ144x112 + function encode144(uint144 x) internal pure returns (uq144x112 memory) { + return uq144x112(uint256(x) << RESOLUTION); + } + + // decode a UQ112x112 into a uint112 by truncating after the radix point + function decode(uq112x112 memory self) internal pure returns (uint112) { + return uint112(self._x >> RESOLUTION); + } + + // decode a UQ144x112 into a uint144 by truncating after the radix point + function decode144(uq144x112 memory self) internal pure returns (uint144) { + return uint144(self._x >> RESOLUTION); + } + + // multiply a UQ112x112 by a uint, returning a UQ144x112 + // reverts on overflow + function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) { + unchecked { + uint256 z = 0; + require(y == 0 || (z = self._x * y) / y == self._x, 'FixedPoint::mul: overflow'); + return uq144x112(z); + } + } + + // multiply a UQ112x112 by an int and decode, returning an int + // reverts on overflow + function muli(uq112x112 memory self, int256 y) internal pure returns (int256) { + unchecked { + uint256 z = FullMath.mulDiv(self._x, uint256(y < 0 ? -y : y), Q112); + require(z < 2**255, 'FixedPoint::muli: overflow'); + return y < 0 ? -int256(z) : int256(z); + } + } + + // multiply a UQ112x112 by a UQ112x112, returning a UQ112x112 + // lossy + function muluq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { + if (self._x == 0 || other._x == 0) { + return uq112x112(0); + } + unchecked { + uint112 upper_self = uint112(self._x >> RESOLUTION); // * 2^0 + uint112 lower_self = uint112(self._x & LOWER_MASK); // * 2^-112 + uint112 upper_other = uint112(other._x >> RESOLUTION); // * 2^0 + uint112 lower_other = uint112(other._x & LOWER_MASK); // * 2^-112 + + // partial products + uint224 upper = uint224(upper_self) * upper_other; // * 2^0 + uint224 lower = uint224(lower_self) * lower_other; // * 2^-224 + uint224 uppers_lowero = uint224(upper_self) * lower_other; // * 2^-112 + uint224 uppero_lowers = uint224(upper_other) * lower_self; // * 2^-112 + + // so the bit shift does not overflow + require(upper <= type(uint112).max, 'FixedPoint::muluq: upper overflow'); + + // this cannot exceed 256 bits, all values are 224 bits + uint256 sum = uint256(upper << RESOLUTION) + uppers_lowero + uppero_lowers + (lower >> RESOLUTION); + + // so the cast does not overflow + require(sum <= type(uint224).max, 'FixedPoint::muluq: sum overflow'); + return uq112x112(uint224(sum)); + } + } + + // divide a UQ112x112 by a UQ112x112, returning a UQ112x112 + function divuq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { + require(other._x > 0, 'FixedPoint::divuq: division by zero'); + if (self._x == other._x) { + return uq112x112(uint224(Q112)); + } + if (self._x <= type(uint144).max) { + uint256 value = (uint256(self._x) << RESOLUTION) / other._x; + require(value <= type(uint224).max, 'FixedPoint::divuq: overflow'); + return uq112x112(uint224(value)); + } + unchecked { + uint256 result = FullMath.mulDiv(Q112, self._x, other._x); + require(result <= type(uint224).max, 'FixedPoint::divuq: overflow'); + return uq112x112(uint224(result)); + } + } + + // returns a UQ112x112 which represents the ratio of the numerator to the denominator + // can be lossy + function fraction(uint256 numerator, uint256 denominator) internal pure returns (uq112x112 memory) { + require(denominator > 0, 'FixedPoint::fraction: division by zero'); + if (numerator == 0) return FixedPoint.uq112x112(0); + unchecked { + if (numerator <= type(uint144).max) { + uint256 result = (numerator << RESOLUTION) / denominator; + require(result <= type(uint224).max, 'FixedPoint::fraction: overflow'); + return uq112x112(uint224(result)); + } else { + uint256 result = FullMath.mulDiv(numerator, Q112, denominator); + require(result <= type(uint224).max, 'FixedPoint::fraction: overflow'); + return uq112x112(uint224(result)); + } + } + } + + // take the reciprocal of a UQ112x112 + // reverts on overflow + // lossy + function reciprocal(uq112x112 memory self) internal pure returns (uq112x112 memory) { + require(self._x != 0, 'FixedPoint::reciprocal: reciprocal of zero'); + require(self._x != 1, 'FixedPoint::reciprocal: overflow'); + return uq112x112(uint224(Q224 / self._x)); + } + + // square root of a UQ112x112 + // lossy between 0/1 and 40 bits + function sqrt(uq112x112 memory self) internal pure returns (uq112x112 memory) { + if (self._x <= type(uint144).max) { + return uq112x112(uint224(Babylonian.sqrt(uint256(self._x) << 112))); + } + uint8 safeShiftBits = 255 - BitMath.mostSignificantBit(self._x); + safeShiftBits -= safeShiftBits % 2; + return uq112x112(uint224(Babylonian.sqrt(uint256(self._x) << safeShiftBits) << ((112 - safeShiftBits) / 2))); + } +} +EOF ;; *) @@ -46,19 +348,130 @@ case "$(uname -s)" in esac FILE="./node_modules/@uniswap/lib/contracts/libraries/FullMath.sol" +rm -rf $FILE case "$(uname -s)" in Darwin) echo 'Mac OS X' - sed -i.bu "s/uint256(-1)/type(uint256).max/g" $FILE - sed -i.bu "s/-d/(~d+1)/g" $FILE - sed -i.bu "s/(-pow2)/(~pow2+1)/g" $FILE +cat << 'EOF' >> ./node_modules/@uniswap/lib/contracts/libraries/FullMath.sol +// SPDX-License-Identifier: CC-BY-4.0 +pragma solidity >=0.4.0; + +// taken from https://medium.com/coinmonks/math-in-solidity-part-3-percents-and-proportions-4db014e080b1 +// license is CC-BY-4.0 +library FullMath { + function fullMul(uint256 x, uint256 y) internal pure returns (uint256 l, uint256 h) { + uint256 mm = mulmod(x, y, type(uint256).max); + unchecked { + l = x * y; + h = mm - l; + if (mm < l) h -= 1; + } + } + + function fullDiv( + uint256 l, + uint256 h, + uint256 d + ) private pure returns (uint256) { + uint256 pow2 = d & (~d+1); + unchecked { + d /= pow2; + l /= pow2; + l += h * ((~pow2+1) / pow2 + 1); + uint256 r = 1; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + return l * r; + } + } + + function mulDiv( + uint256 x, + uint256 y, + uint256 d + ) internal pure returns (uint256) { + (uint256 l, uint256 h) = fullMul(x, y); + + uint256 mm = mulmod(x, y, d); + unchecked { + if (mm > l) h -= 1; + l -= mm; + + if (h == 0) return l / d; + } + require(h < d, 'FullMath: FULLDIV_OVERFLOW'); + return fullDiv(l, h, d); + } +} +EOF ;; Linux) echo 'Linux' - sed -i.bu "s/uint256(-1)/type(uint256).max/g" $FILE - sed -i.bu "s/-d/(~d+1)/g" $FILE - sed -i.bu "s/(-pow2)/(~pow2+1)/g" $FILE +cat << 'EOF' >> ./node_modules/@uniswap/lib/contracts/libraries/FullMath.sol +// SPDX-License-Identifier: CC-BY-4.0 +pragma solidity >=0.4.0; + +// taken from https://medium.com/coinmonks/math-in-solidity-part-3-percents-and-proportions-4db014e080b1 +// license is CC-BY-4.0 +library FullMath { + function fullMul(uint256 x, uint256 y) internal pure returns (uint256 l, uint256 h) { + uint256 mm = mulmod(x, y, type(uint256).max); + unchecked { + l = x * y; + h = mm - l; + if (mm < l) h -= 1; + } + } + + function fullDiv( + uint256 l, + uint256 h, + uint256 d + ) private pure returns (uint256) { + uint256 pow2 = d & (~d+1); + unchecked { + d /= pow2; + l /= pow2; + l += h * ((~pow2+1) / pow2 + 1); + uint256 r = 1; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + return l * r; + } + } + + function mulDiv( + uint256 x, + uint256 y, + uint256 d + ) internal pure returns (uint256) { + (uint256 l, uint256 h) = fullMul(x, y); + + uint256 mm = mulmod(x, y, d); + unchecked { + if (mm > l) h -= 1; + l -= mm; + + if (h == 0) return l / d; + } + require(h < d, 'FullMath: FULLDIV_OVERFLOW'); + return fullDiv(l, h, d); + } +} +EOF ;; *) diff --git a/test/integration/GovernanceControl.js b/test/integration/GovernanceControl.js index ece1e49e..4ec58c13 100644 --- a/test/integration/GovernanceControl.js +++ b/test/integration/GovernanceControl.js @@ -39,8 +39,9 @@ describe("Governance integration", function () { token = await Token.deploy(); await token.deployed(); + // Dispenser address is irrelevant in these tests, so its contract is passed as a zero address const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1"); + escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1", addressZero); await escrow.deployed(); signers = await ethers.getSigners(); diff --git a/test/integration/TokenomicsLoop.js b/test/integration/TokenomicsLoop.js index b208d3ad..f887657d 100644 --- a/test/integration/TokenomicsLoop.js +++ b/test/integration/TokenomicsLoop.js @@ -13,6 +13,8 @@ describe("Tokenomics integration", async () => { let olaFactory; let depositoryFactory; let tokenomicsFactory; + let veFactory; + let dispenserFactory; let componentRegistry; let agentRegistry; let serviceRegistry; @@ -25,10 +27,10 @@ describe("Tokenomics integration", async () => { let treasury; let treasuryFactory; let tokenomics; + let ve; + let dispenser; let epochLen = 100; - let supply = "10000000000000000000000"; // 10,000 - let vesting = 60 * 60 *24; let timeToConclusion = 60 * 60 * 24; let conclusion; @@ -46,14 +48,15 @@ describe("Tokenomics integration", async () => { const maxThreshold = 1; const name = "service name"; const description = "service description"; - const regServiceRevenue = 100000; + const milETHBalance = ethers.utils.parseEther("1000000"); + const regServiceRevenue = milETHBalance; const agentId = 1; const agentParams = [1, regBond]; const serviceId = 1; const payload = "0x"; const magicDenominator = 5192296858534816; const E18 = 10**18; - const delta = 1.0 / 10**13; + const delta = 1.0 / 10**10; let signers; @@ -61,13 +64,15 @@ describe("Tokenomics integration", async () => { * Everything in this block is only run once before all tests. * This is the home for setup methods */ - before(async () => { + beforeEach(async () => { signers = await ethers.getSigners(); olaFactory = await ethers.getContractFactory("OLA"); erc20Token = await ethers.getContractFactory("ERC20Token"); depositoryFactory = await ethers.getContractFactory("Depository"); treasuryFactory = await ethers.getContractFactory("Treasury"); tokenomicsFactory = await ethers.getContractFactory("Tokenomics"); + dispenserFactory = await ethers.getContractFactory("Dispenser"); + veFactory = await ethers.getContractFactory("VotingEscrow"); const ComponentRegistry = await ethers.getContractFactory("ComponentRegistry"); componentRegistry = await ComponentRegistry.deploy("agent components", "MECHCOMP", @@ -95,22 +100,26 @@ describe("Tokenomics integration", async () => { const GnosisSafeMultisig = await ethers.getContractFactory("GnosisSafeMultisig"); gnosisSafeMultisig = await GnosisSafeMultisig.deploy(gnosisSafeL2.address, gnosisSafeProxyFactory.address); await gnosisSafeMultisig.deployed(); - }); - beforeEach(async () => { const deployer = signers[0]; dai = await erc20Token.deploy(); ola = await olaFactory.deploy(); // Correct treasury address is missing here, it will be defined just one line below - tokenomics = await tokenomicsFactory.deploy(ola.address, deployer.address, epochLen, componentRegistry.address, + tokenomics = await tokenomicsFactory.deploy(ola.address, deployer.address, deployer.address, epochLen, componentRegistry.address, agentRegistry.address, serviceRegistry.address); // Correct depository address is missing here, it will be defined just one line below - treasury = await treasuryFactory.deploy(ola.address, deployer.address, tokenomics.address); + treasury = await treasuryFactory.deploy(ola.address, deployer.address, tokenomics.address, deployer.address); // Change to the correct treasury address await tokenomics.changeTreasury(treasury.address); // Change to the correct depository address depository = await depositoryFactory.deploy(ola.address, treasury.address, tokenomics.address); await treasury.changeDepository(depository.address); + await tokenomics.changeDepository(depository.address); + + ve = await veFactory.deploy(ola.address, "Governance OLA", "veOLA", "0.1", deployer.address); + dispenser = await dispenserFactory.deploy(ola.address, ve.address, treasury.address, tokenomics.address); + await ve.changeDispenser(dispenser.address); + await treasury.changeDispenser(dispenser.address); // Airdrop from the deployer :) await dai.mint(deployer.address, initialMint); @@ -150,7 +159,7 @@ describe("Tokenomics integration", async () => { await treasury.depositETHFromService(1, {value: regServiceRevenue}); // Calculate current epoch parameters - await tokenomics.checkpoint(); + await treasury.allocateRewards(); // Get the information from tokenomics point const epoch = await tokenomics.getEpoch(); @@ -158,11 +167,50 @@ describe("Tokenomics integration", async () => { // Checking the values with delta rounding error const ucf = Number(point.ucf / magicDenominator) * 1.0 / E18; + expect(Math.abs(ucf - 0.5)).to.lessThan(delta); + const usf = Number(point.usf / magicDenominator) * 1.0 / E18; - expect(ucf).to.greaterThanOrEqual(0.5); - expect(ucf).to.lessThan(0.5 + delta); + expect(Math.abs(usf - 1.0)).to.lessThan(delta); + }); + + it("Dispenser for an agent owner", async () => { + const mechManager = signers[3]; + const serviceManager = signers[4]; + const owner = signers[5]; + const ownerAddress = owner.address; + const operator = signers[6].address; + const agentInstance = signers[7].address; + + // Create one agent + await agentRegistry.changeManager(mechManager.address); + await agentRegistry.connect(mechManager).create(ownerAddress, ownerAddress, agentHash, description, []); + + // Create one service + await serviceRegistry.changeManager(serviceManager.address); + await serviceRegistry.connect(serviceManager).createService(ownerAddress, name, description, configHash, [agentId], + [agentParams], maxThreshold); + + // Register agent instances + await serviceRegistry.connect(serviceManager).activateRegistration(ownerAddress, serviceId, {value: regDeposit}); + await serviceRegistry.connect(serviceManager).registerAgents(operator, serviceId, [agentInstance], [agentId], {value: regBond}); + + // Deploy the service + await serviceRegistry.changeMultisigPermission(gnosisSafeMultisig.address, true); + await serviceRegistry.connect(serviceManager).deploy(ownerAddress, serviceId, gnosisSafeMultisig.address, payload); + + // Send deposits from a service + await treasury.depositETHFromService(1, {value: regServiceRevenue}); + + // Calculate current epoch parameters + await treasury.allocateRewards(); + + // Get owner rewards + await dispenser.connect(owner).withdrawOwnerRewards(); + const balance = await ola.balanceOf(ownerAddress); - expect(usf).to.greaterThanOrEqual(1); - expect(usf).to.lessThan(1 + delta); + // Check the received reward + const agentFraction = await tokenomics.agentFraction(); + const expectedReward = regServiceRevenue * agentFraction / 100; + expect(Number(balance)).to.equal(expectedReward); }); }); diff --git a/test/unit/governance/Governance.js b/test/unit/governance/Governance.js index 4509fb17..8358bf4b 100644 --- a/test/unit/governance/Governance.js +++ b/test/unit/governance/Governance.js @@ -36,8 +36,9 @@ describe("Governance unit", function () { token = await Token.deploy(); await token.deployed(); + // Dispenser address is irrelevant in these tests, so its contract is passed as a zero address const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1"); + escrow = await VotingEscrow.deploy(token.address, "Governance OLA", "veOLA", "0.1", AddressZero); await escrow.deployed(); signers = await ethers.getSigners(); diff --git a/test/unit/governance/VotingEscrow.js b/test/unit/governance/VotingEscrow.js index 7525994b..cea18a54 100644 --- a/test/unit/governance/VotingEscrow.js +++ b/test/unit/governance/VotingEscrow.js @@ -6,12 +6,14 @@ const { ethers } = require("hardhat"); describe("VotingEscrow", function () { let token; let ve; + let dispenser; let signers; const initialMint = "1000000000000000000000000"; // 1000000 const oneWeek = 7 * 86400; const oneETHBalance = ethers.utils.parseEther("1"); const twoETHBalance = ethers.utils.parseEther("2"); const tenETHBalance = ethers.utils.parseEther("10"); + const AddressZero = "0x" + "0".repeat(40); beforeEach(async function () { const Token = await ethers.getContractFactory("OLA"); @@ -22,8 +24,14 @@ describe("VotingEscrow", function () { await token.mint(signers[0].address, initialMint); const VE = await ethers.getContractFactory("VotingEscrow"); - ve = await VE.deploy(token.address, "name", "symbol", "0.1"); + ve = await VE.deploy(token.address, "name", "symbol", "0.1", signers[0].address); await ve.deployed(); + + // Tokenomics and Treasury contract addresses are irrelevant for these tests + const Dispenser = await ethers.getContractFactory("Dispenser"); + dispenser = await Dispenser.deploy(token.address, ve.address, AddressZero, AddressZero); + await dispenser.deployed(); + await ve.changeDispenser(dispenser.address); }); context("Locks", async function () { diff --git a/test/unit/tokenomics/depository/Depository.js b/test/unit/tokenomics/depository/Depository.js index d7965116..6ba1c57f 100644 --- a/test/unit/tokenomics/depository/Depository.js +++ b/test/unit/tokenomics/depository/Depository.js @@ -2,12 +2,13 @@ const { ethers, network } = require("hardhat"); const { expect } = require("chai"); -describe("Bond Depository LP", async () => { +describe("Depository LP", async () => { const LARGE_APPROVAL = "100000000000000000000000000000000"; // const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; // Initial mint for ola and DAI (40,000) const initialMint = "40000000000000000000000"; // Increase timestamp by amount determined by `offset` + const AddressZero = "0x" + "0".repeat(40); let deployer, alice, bob; let erc20Token; @@ -68,15 +69,16 @@ describe("Bond Depository LP", async () => { dai = await erc20Token.deploy(); ola = await olaFactory.deploy(); // Correct treasury address is missing here, it will be defined just one line below - tokenomics = await tokenomicsFactory.deploy(ola.address, deployer.address, epochLen, componentRegistry.address, + tokenomics = await tokenomicsFactory.deploy(ola.address, deployer.address, deployer.address, epochLen, componentRegistry.address, agentRegistry.address, serviceRegistry.address); // Correct depository address is missing here, it will be defined just one line below - treasury = await treasuryFactory.deploy(ola.address, deployer.address, tokenomics.address); + treasury = await treasuryFactory.deploy(ola.address, deployer.address, tokenomics.address, AddressZero); // Change to the correct treasury address await tokenomics.changeTreasury(treasury.address); // Change to the correct depository address depository = await depositoryFactory.deploy(ola.address, treasury.address, tokenomics.address); await treasury.changeDepository(depository.address); + await tokenomics.changeDepository(depository.address); // Airdrop from the deployer :) await dai.mint(deployer.address, initialMint); diff --git a/test/unit/tokenomics/treasury/Treasury.js b/test/unit/tokenomics/treasury/Treasury.js index 3b92063f..42410796 100644 --- a/test/unit/tokenomics/treasury/Treasury.js +++ b/test/unit/tokenomics/treasury/Treasury.js @@ -7,6 +7,7 @@ describe("Treasury", async () => { // const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; // Initial mint for Frax and DAI (10,000,000) const initialMint = "10000000000000000000000000"; + const AddressZero = "0x" + "0".repeat(40); let deployer; let erc20Token; @@ -57,10 +58,11 @@ describe("Treasury", async () => { lpToken = await erc20Token.deploy(); ola = await olaFactory.deploy(); // Correct treasury address is missing here, it will be defined just one line below - tokenomics = await tokenomicsFactory.deploy(ola.address, deployer.address, epochLen, componentRegistry.address, + tokenomics = await tokenomicsFactory.deploy(ola.address, deployer.address, deployer.address, epochLen, componentRegistry.address, agentRegistry.address, serviceRegistry.address); // Depository contract is irrelevant here, so we are using a deployer's address - treasury = await treasuryFactory.deploy(ola.address, deployer.address, tokenomics.address); + // Dispenser address is irrelevant in these tests, so its contract is passed as a zero address + treasury = await treasuryFactory.deploy(ola.address, deployer.address, tokenomics.address, AddressZero); // Change to the correct treasury address await tokenomics.changeTreasury(treasury.address);