From 13230fc755ecd20e62aac777469eef73fa4109cd Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 13 Apr 2022 21:43:13 +0100 Subject: [PATCH 1/8] feat and refator: introducing dispenser contract --- contracts/Depository.sol | 9 +- contracts/Dispenser.sol | 111 +++++++ contracts/Tokenomics.sol | 142 +++++++-- contracts/Treasury.sol | 52 +++- contracts/interfaces/IStructs.sol | 15 + contracts/interfaces/ITokenomics.sol | 10 +- contracts/interfaces/ITreasury.sol | 3 + hardhat.config.js | 8 +- scripts/uni-adjust/adjust-point.sh | 439 ++++++++++++++++++++++++++- test/integration/TokenomicsLoop.js | 13 +- 10 files changed, 739 insertions(+), 63 deletions(-) create mode 100644 contracts/Dispenser.sol diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 649210bc..09ee1399 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; diff --git a/contracts/Dispenser.sol b/contracts/Dispenser.sol new file mode 100644 index 00000000..86b75826 --- /dev/null +++ b/contracts/Dispenser.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./interfaces/IErrors.sol"; +import "./interfaces/IStructs.sol"; +import "./interfaces/ITreasury.sol"; +import "./interfaces/ITokenomics.sol"; + + +/// @title Bond Depository - Smart contract for OLA Bond Depository +/// @author AL +contract Depository is IErrors, IStructs, Ownable, 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 => revenue amount + mapping(address => uint256) public mapOwnersRevenue; + + constructor(address _ola, address _ve, address _treasury, address _tokenomics) { + ola = _ola; + ve = _ve; + treasury = _treasury; + tokenomics = _tokenomics; + } + + function changeVeotingEscrow(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 Starts a new epoch. + function startNewEpoch() external onlyOwner { + // 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) { + // Process the epoch data + ITokenomics(tokenomics).checkpoint(); + + // Request OLA funds from treasury for the last epoch + uint256 amountOLA = point.totalRevenue; + ITreasury(treasury).requestFunds(amountOLA); + + // Distribute rewards information between component and agent owners + uint256 componentReward = point.componentFraction * amountOLA / 100; + uint256 agentReward = point.agentFraction * amountOLA / 100; + + // Iterating over components + address[] memory profitableComponents = ITokenomics(tokenomics).getProfitableComponents(); + uint256 numComponents = profitableComponents.length; + if (numComponents > 0) { + uint256 rewardPerComponent = componentReward / numComponents; + for (uint256 i = 0; i < numComponents; ++i) { + mapOwnersRevenue[profitableComponents[i]] += rewardPerComponent; + } + } + + // Iterating over agents + address[] memory profitableAgents = ITokenomics(tokenomics).getProfitableAgents(); + uint256 numAgents = profitableAgents.length; + if (numAgents > 0) { + uint256 rewardPerAgent = agentReward / numAgents; + for (uint256 i = 0; i < numAgents; ++i) { + mapOwnersRevenue[profitableAgents[i]] += rewardPerAgent; + } + } + } + } + + /// @dev Withdraws rewards for owners of components / agents. + /// @param account Account address. + function withdrawOwnerReward(address account) external nonReentrant { + uint256 balance = mapOwnersRevenue[account]; + if (balance > 0) { + mapOwnersRevenue[account] = 0; + IERC20(ola).safeTransferFrom(address(this), account, balance); + } + } + + /// @dev Withdraws rewards for stakers. + /// @param account Account address. + function withdrawStakingReward(address account) external nonReentrant { + + } +} diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 6c1392eb..c15f018e 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -8,30 +8,23 @@ import "./AgentRegistry.sol"; import "./ServiceRegistry.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 - } - // OLA token address address public immutable ola; // Treasury contract address address public treasury; + // Dispenser contract address + address public dispenser; + 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 @@ -51,7 +44,13 @@ 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 + // componentFraction + agentFraction + stakerFraction = 100% + uint256 public stakerFraction = 50; + uint256 public componentFraction = 33; + uint256 public agentFraction = 17; // Component Registry address public immutable componentRegistry; @@ -62,12 +61,19 @@ contract Tokenomics is IErrors, Ownable { // Mapping of epoch => point mapping(uint256 => PointEcomonics) public mapEpochEconomics; + // 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) { @@ -87,11 +93,25 @@ contract Tokenomics is IErrors, Ownable { _; } + modifier onlyDispenser() { + if (dispenser != msg.sender) { + revert ManagerOnly(msg.sender, dispenser); + } + _; + } + + /// @dev Changes treasury address. function changeTreasury(address newTreasury) external onlyOwner { treasury = newTreasury; emit TreasuryUpdated(newTreasury); } + /// @dev Changes dispenser address. + function changeDispenser(address newDispenser) external onlyOwner { + dispenser = newDispenser; + emit TreasuryUpdated(newDispenser); + } + /// @dev Gets curretn epoch number. function getEpoch() public view returns (uint256 epoch) { epoch = block.number / epochLen; @@ -104,7 +124,7 @@ 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( + function changeTokenomicsParameters( uint256 _alphaNumerator, uint256 _alphaDenominator, uint256 _beta, @@ -118,6 +138,24 @@ contract Tokenomics is IErrors, Ownable { maxDF = _maxDF + E13; } + /// @dev Sets staking parameters in fractions of distributed rewards. + /// @param _stakerFraction Fraction for stakers. + /// @param _componentFraction Fraction for component owners. + function changeStakingParameters( + uint256 _stakerFraction, + uint256 _componentFraction, + uint256 _agentFraction + ) external onlyOwner { + // Check that the sum of fractions is 100% + if (_stakerFraction + _componentFraction + _agentFraction != 100) { + revert AmountLowerThan(_stakerFraction + _componentFraction + _agentFraction, 100); + } + + stakerFraction = _stakerFraction; + componentFraction = _componentFraction; + agentFraction = _agentFraction; + } + /// @dev Tracks the deposit token amount during the epoch. function trackServicesETHRevenue(uint256[] memory serviceIds, uint256[] memory amounts) public onlyTreasury { @@ -137,7 +175,7 @@ contract Tokenomics is IErrors, Ownable { _mapServiceAmounts[serviceIds[i]] += amounts[i]; // Increase also the total service revenue - totalServiceETHRevenue += amounts[i]; + totalServiceRevenueETH += amounts[i]; } } @@ -160,7 +198,7 @@ contract Tokenomics is IErrors, Ownable { return success; } - function _calculateUCFc() private view returns (FixedPoint.uq112x112 memory ucfc) { + function _calculateUCFc() private returns (FixedPoint.uq112x112 memory ucfc) { ComponentRegistry cRegistry = ComponentRegistry(componentRegistry); uint256 numComponents = cRegistry.totalSupply(); uint256 numProfitableComponents; @@ -169,6 +207,10 @@ contract Tokenomics is IErrors, Ownable { uint256[] memory ucfcs = new uint256[](numServices); // Array of cardinality of components in a specific profitable service: |Cs(epoch)| uint256[] memory ucfcsNum = new uint256[](numServices); + + // Clear the previous epoch profitable set of components + delete _profitableComponents; + // Loop over components for (uint256 i = 0; i < numComponents; ++i) { (, uint256[] memory serviceIds) = ServiceRegistry(serviceRegistry).getServiceIdsCreatedWithComponentId(cRegistry.tokenByIndex(i)); @@ -186,9 +228,14 @@ 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 = cRegistry.ownerOf(i); + _profitableComponents.push(owner); + // Increase the profitable component number ++numProfitableComponents; } } + uint256 denominator; // Calculate total UCFc for (uint256 i = 0; i < numServices; ++i) { @@ -198,7 +245,7 @@ contract Tokenomics is IErrors, Ownable { 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,7 +255,7 @@ contract Tokenomics is IErrors, Ownable { } } - function _calculateUCFa() private view returns (FixedPoint.uq112x112 memory ucfa) { + function _calculateUCFa() private returns (FixedPoint.uq112x112 memory ucfa) { AgentRegistry aRegistry = AgentRegistry(agentRegistry); uint256 numAgents = aRegistry.totalSupply(); uint256 numProfitableAgents; @@ -217,6 +264,10 @@ contract Tokenomics is IErrors, Ownable { uint256[] memory ucfas = new uint256[](numServices); // Array of cardinality of components in a specific profitable service: |As(epoch)| uint256[] memory ucfasNum = new uint256[](numServices); + + // Clear the previous epoch profitable set of agents + delete _profitableAgents; + // Loop over agents for (uint256 i = 0; i < numAgents; ++i) { (, uint256[] memory serviceIds) = ServiceRegistry(serviceRegistry).getServiceIdsCreatedWithAgentId(aRegistry.tokenByIndex(i)); @@ -234,9 +285,14 @@ 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 = aRegistry.ownerOf(i); + _profitableAgents.push(owner); + // Increase a profitable agent number ++numProfitableAgents; } } + uint256 denominator; // Calculate total UCFa for (uint256 i = 0; i < numServices; ++i) { @@ -246,7 +302,7 @@ contract Tokenomics is IErrors, Ownable { 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 +362,16 @@ contract Tokenomics is IErrors, Ownable { delete _mapServiceIndexes[_protocolServiceIds[i]]; } delete _protocolServiceIds; - totalServiceETHRevenue = 0; + totalServiceRevenueETH = 0; } /// @notice Record global data to checkpoint, any can do it /// @dev Checked point exist or not - function checkpoint() external { + function checkpoint() external onlyDispenser { uint256 epoch = getEpoch(); PointEcomonics memory lastPoint = mapEpochEconomics[epoch]; // if not exist - if(!lastPoint._exist) { + if(!lastPoint.exists) { _checkpoint(epoch); } } @@ -328,10 +384,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,7 +407,7 @@ 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 * ServiceRegistry(serviceRegistry).totalSupply(); if(denominator > 0) { // _usf = usf / ServiceRegistry(serviceRegistry).totalSupply(); _usf = FixedPoint.fraction(usf, denominator); @@ -362,9 +419,10 @@ contract Tokenomics is IErrors, Ownable { } } - // ToDO :: df/iterest rate + uint256 totalServiceRevenueOLA = _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, stakerFraction, componentFraction, + agentFraction, totalServiceRevenueOLA, block.timestamp, block.number, true); mapEpochEconomics[epoch] = newPoint; _clearEpochData(); @@ -379,7 +437,7 @@ contract Tokenomics is IErrors, Ownable { returns (uint256 resAmount) { PointEcomonics memory _PE = mapEpochEconomics[_epoch]; - if (!_PE._exist) { + if (!_PE.exists) { _checkpoint(_epoch); _PE = mapEpochEconomics[_epoch]; } @@ -464,10 +522,16 @@ contract Tokenomics is IErrors, Ownable { _PE = mapEpochEconomics[_epoch]; } + /// @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, 0 if not exist function getDF(uint256 _epoch) public view returns (uint256 df) { PointEcomonics memory _PE = mapEpochEconomics[_epoch]; - if (_PE._exist) { + 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); // 2^(112 - log2(1e18)) @@ -477,9 +541,9 @@ contract Tokenomics is IErrors, Ownable { } // decode a uq112x112 into a uint with 18 decimals of precision, re-calc if not exist - function getDFForEpoch(uint256 _epoch) external returns (uint256 df) { + function getDFForEpoch(uint256 _epoch) external onlyDispenser returns (uint256 df) { PointEcomonics memory _PE = mapEpochEconomics[_epoch]; - if (!_PE._exist) { + if (!_PE.exists) { _checkpoint(_epoch); _PE = mapEpochEconomics[_epoch]; } @@ -488,4 +552,20 @@ contract Tokenomics is IErrors, Ownable { 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 returns (uint256 amountOLA) { + // TODO Exchange rate is a stub for now + amountOLA = tokenAmount; + } + + function getProfitableComponents() external view returns (address[] memory profitableComponents) { + profitableComponents = _profitableComponents; + } + + function getProfitableAgents() external view returns (address[] memory profitableAgents) { + profitableAgents = _profitableAgents; + } } diff --git a/contracts/Treasury.sol b/contracts/Treasury.sol index 6bd61022..fd277799 100644 --- a/contracts/Treasury.sol +++ b/contracts/Treasury.sol @@ -19,8 +19,10 @@ 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); enum TokenState { NonExistent, @@ -37,10 +39,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 +53,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 _dispenser, address _tokenomics) { 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 +72,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 +86,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 +216,22 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { function isEnabled(address token) public view returns (bool enabled) { enabled = (mapTokens[token].state == TokenState.Enabled); } + + /// @dev Requests OLA funds from treasury. + /// @param amount Amount of OLA. + function requestFunds(uint256 amount) external onlyDispenser { + // 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); + } } diff --git a/contracts/interfaces/IStructs.sol b/contracts/interfaces/IStructs.sol index 6aa2f4a6..2cac5de6 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,17 @@ 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 stakerFraction; + uint256 componentFraction; + uint256 agentFraction; + uint256 totalRevenue; + 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..4b0f1c51 100644 --- a/contracts/interfaces/ITokenomics.sol +++ b/contracts/interfaces/ITokenomics.sol @@ -1,11 +1,19 @@ // 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); + function getProfitableAgents() external view returns (address[] memory profitableAgents); } diff --git a/contracts/interfaces/ITreasury.sol b/contracts/interfaces/ITreasury.sol index 9c072601..804db139 100644 --- a/contracts/interfaces/ITreasury.sol +++ b/contracts/interfaces/ITreasury.sol @@ -26,4 +26,7 @@ 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; } 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/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/TokenomicsLoop.js b/test/integration/TokenomicsLoop.js index b208d3ad..91b65cd1 100644 --- a/test/integration/TokenomicsLoop.js +++ b/test/integration/TokenomicsLoop.js @@ -46,14 +46,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; @@ -158,11 +159,9 @@ describe("Tokenomics integration", async () => { // Checking the values with delta rounding error const ucf = Number(point.ucf / magicDenominator) * 1.0 / E18; - 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(ucf - 0.5)).to.lessThan(delta); - expect(usf).to.greaterThanOrEqual(1); - expect(usf).to.lessThan(1 + delta); + const usf = Number(point.usf / magicDenominator) * 1.0 / E18; + expect(Math.abs(usf - 1.0)).to.lessThan(delta); }); }); From 03f8c715c709f3bc0a79993d4b1cc411c857cebd Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 14 Apr 2022 16:51:40 +0100 Subject: [PATCH 2/8] feat: initial version of rewards functionality --- contracts/Dispenser.sol | 160 +++++++++++++++++++------- contracts/Tokenomics.sol | 120 +++++++++---------- contracts/Treasury.sol | 76 +++++++++--- contracts/governance/VotingEscrow.sol | 57 +++++++-- contracts/interfaces/IDispenser.sol | 18 +++ contracts/interfaces/IService.sol | 19 +++ contracts/interfaces/IStructs.sol | 3 +- contracts/interfaces/ITokenomics.sol | 4 +- 8 files changed, 329 insertions(+), 128 deletions(-) create mode 100644 contracts/interfaces/IDispenser.sol diff --git a/contracts/Dispenser.sol b/contracts/Dispenser.sol index 86b75826..5bda8c22 100644 --- a/contracts/Dispenser.sol +++ b/contracts/Dispenser.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/access/Ownable.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"; @@ -13,7 +14,7 @@ import "./interfaces/ITokenomics.sol"; /// @title Bond Depository - Smart contract for OLA Bond Depository /// @author AL -contract Depository is IErrors, IStructs, Ownable, ReentrancyGuard { +contract Dispenser is IErrors, IStructs, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; event VotingEscrowUpdated(address ve); @@ -28,8 +29,10 @@ contract Depository is IErrors, IStructs, Ownable, ReentrancyGuard { address public treasury; // Tokenomics address address public tokenomics; - // Mapping of owner of component / agent address => revenue amount - mapping(address => uint256) public mapOwnersRevenue; + // 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; @@ -38,7 +41,23 @@ contract Depository is IErrors, IStructs, Ownable, ReentrancyGuard { tokenomics = _tokenomics; } - function changeVeotingEscrow(address newVE) external onlyOwner { + // 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); } @@ -53,59 +72,120 @@ contract Depository is IErrors, IStructs, Ownable, ReentrancyGuard { emit TokenomicsUpdated(newTokenomics); } - /// @dev Starts a new epoch. - function startNewEpoch() external onlyOwner { - // 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) { - // Process the epoch data - ITokenomics(tokenomics).checkpoint(); - - // Request OLA funds from treasury for the last epoch - uint256 amountOLA = point.totalRevenue; - ITreasury(treasury).requestFunds(amountOLA); - - // Distribute rewards information between component and agent owners - uint256 componentReward = point.componentFraction * amountOLA / 100; - uint256 agentReward = point.agentFraction * amountOLA / 100; - - // Iterating over components - address[] memory profitableComponents = ITokenomics(tokenomics).getProfitableComponents(); - uint256 numComponents = profitableComponents.length; - if (numComponents > 0) { - uint256 rewardPerComponent = componentReward / numComponents; - for (uint256 i = 0; i < numComponents; ++i) { - mapOwnersRevenue[profitableComponents[i]] += rewardPerComponent; + /// @dev Distributes rewards between component and agent owners. + function _distributeOwnerRewards(uint256 componentFraction, uint256 agentFraction, uint256 amountOLA) internal { + uint256 componentReward = componentFraction * amountOLA / 100; + uint256 agentReward = agentFraction * amountOLA / 100; + 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]; } - // Iterating over agents - address[] memory profitableAgents = ITokenomics(tokenomics).getProfitableAgents(); - uint256 numAgents = profitableAgents.length; - if (numAgents > 0) { - uint256 rewardPerAgent = agentReward / numAgents; - for (uint256 i = 0; i < numAgents; ++i) { - mapOwnersRevenue[profitableAgents[i]] += rewardPerAgent; + 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 stakerFraction, uint256 amountOLA) internal { + VotingEscrow veContract = VotingEscrow(ve); + address[] memory accounts = veContract.getLockAccounts(); + + // Get the overall amount of rewards for stakers + uint256 rewardLeft = stakerFraction * amountOLA / 100 ; + + // 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 = amountOLA * 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 componentFraction, + uint256 agentFraction, + uint256 stakerFraction, + uint256 amountOLA + ) external onlyTreasury + { + // Distribute rewards between component and agent owners + _distributeOwnerRewards(componentFraction, agentFraction, amountOLA); + + // Distribute rewards for stakers + _distributeStakerRewards(stakerFraction, amountOLA); + } + /// @dev Withdraws rewards for owners of components / agents. /// @param account Account address. function withdrawOwnerReward(address account) external nonReentrant { - uint256 balance = mapOwnersRevenue[account]; + uint256 balance = mapOwnerRewards[account]; if (balance > 0) { - mapOwnersRevenue[account] = 0; + mapOwnerRewards[account] = 0; IERC20(ola).safeTransferFrom(address(this), account, balance); } } /// @dev Withdraws rewards for stakers. /// @param account Account address. - function withdrawStakingReward(address account) external nonReentrant { - + /// @return balance Reward balance. + function withdrawStakingReward(address account) external onlyVotingEscrow returns (uint256 balance) { + balance = mapStakerRewards[account]; + if (balance > 0) { + mapStakerRewards[account] = 0; + IERC20(ola).safeTransferFrom(address(this), ve, balance); + } } } diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index c15f018e..d848677d 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -3,9 +3,8 @@ 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"; import "./interfaces/IStructs.sol"; @@ -22,8 +21,6 @@ contract Tokenomics is IErrors, IStructs, Ownable { address public immutable ola; // Treasury contract address address public treasury; - // Dispenser contract address - address public dispenser; bytes4 private constant FUNC_SELECTOR = bytes4(keccak256("kLast()")); // is pair or pure ERC20? uint256 public immutable epochLen; // epoch len in blk @@ -47,8 +44,9 @@ contract Tokenomics is IErrors, IStructs, Ownable { uint256 public totalServiceRevenueETH; // Staking parameters with multiplying by 100 - // componentFraction + agentFraction + stakerFraction = 100% - uint256 public stakerFraction = 50; + // treasuryFraction + componentFraction + agentFraction + stakerFraction = 100% + uint256 public treasuryFraction = 10; + uint256 public stakerFraction = 40; uint256 public componentFraction = 33; uint256 public agentFraction = 17; @@ -61,6 +59,10 @@ contract Tokenomics is IErrors, IStructs, 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 @@ -75,7 +77,8 @@ contract Tokenomics is IErrors, IStructs, Ownable { 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, uint256 _epochLen, address _componentRegistry, address _agentRegistry, + address payable _serviceRegistry) { ola = _ola; treasury = _treasury; @@ -93,25 +96,12 @@ contract Tokenomics is IErrors, IStructs, Ownable { _; } - modifier onlyDispenser() { - if (dispenser != msg.sender) { - revert ManagerOnly(msg.sender, dispenser); - } - _; - } - /// @dev Changes treasury address. function changeTreasury(address newTreasury) external onlyOwner { treasury = newTreasury; emit TreasuryUpdated(newTreasury); } - /// @dev Changes dispenser address. - function changeDispenser(address newDispenser) external onlyOwner { - dispenser = newDispenser; - emit TreasuryUpdated(newDispenser); - } - /// @dev Gets curretn epoch number. function getEpoch() public view returns (uint256 epoch) { epoch = block.number / epochLen; @@ -142,15 +132,17 @@ contract Tokenomics is IErrors, IStructs, Ownable { /// @param _stakerFraction Fraction for stakers. /// @param _componentFraction Fraction for component owners. function changeStakingParameters( + uint256 _treasuryFraction, uint256 _stakerFraction, uint256 _componentFraction, uint256 _agentFraction ) external onlyOwner { // Check that the sum of fractions is 100% - if (_stakerFraction + _componentFraction + _agentFraction != 100) { - revert AmountLowerThan(_stakerFraction + _componentFraction + _agentFraction, 100); + if (_treasuryFraction + _stakerFraction + _componentFraction + _agentFraction != 100) { + revert WrongAmount(_treasuryFraction + _stakerFraction + _componentFraction + _agentFraction, 100); } + treasuryFraction = _treasuryFraction; stakerFraction = _stakerFraction; componentFraction = _componentFraction; agentFraction = _agentFraction; @@ -163,7 +155,7 @@ contract Tokenomics is IErrors, IStructs, 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]); } @@ -199,28 +191,30 @@ contract Tokenomics is IErrors, IStructs, Ownable { } function _calculateUCFc() private returns (FixedPoint.uq112x112 memory ucfc) { - ComponentRegistry cRegistry = ComponentRegistry(componentRegistry); - uint256 numComponents = cRegistry.totalSupply(); + uint256 numComponents = IERC721Enumerable(componentRegistry).totalSupply(); uint256 numProfitableComponents; uint256 numServices = _protocolServiceIds.length; - // Array of sum(UCFc(epoch)) - uint256[] memory ucfcs = new uint256[](numServices); - // Array of cardinality of components in a specific profitable service: |Cs(epoch)| - uint256[] memory ucfcsNum = 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; @@ -229,7 +223,7 @@ contract Tokenomics is IErrors, IStructs, 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 = cRegistry.ownerOf(i); + address owner = IERC721Enumerable(componentRegistry).ownerOf(i); _profitableComponents.push(owner); // Increase the profitable component number ++numProfitableComponents; @@ -242,7 +236,7 @@ contract Tokenomics is IErrors, IStructs, Ownable { 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, totalServiceRevenueETH)); @@ -256,28 +250,30 @@ contract Tokenomics is IErrors, IStructs, Ownable { } function _calculateUCFa() private returns (FixedPoint.uq112x112 memory ucfa) { - AgentRegistry aRegistry = AgentRegistry(agentRegistry); - uint256 numAgents = aRegistry.totalSupply(); + uint256 numAgents = IERC721Enumerable(agentRegistry).totalSupply(); uint256 numProfitableAgents; uint256 numServices = _protocolServiceIds.length; - // Array of sum(UCFa(epoch)) - uint256[] memory ucfas = new uint256[](numServices); - // Array of cardinality of components in a specific profitable service: |As(epoch)| - uint256[] memory ucfasNum = 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; @@ -286,7 +282,7 @@ contract Tokenomics is IErrors, IStructs, 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 = aRegistry.ownerOf(i); + address owner = IERC721Enumerable(agentRegistry).ownerOf(i); _profitableAgents.push(owner); // Increase a profitable agent number ++numProfitableAgents; @@ -299,7 +295,7 @@ contract Tokenomics is IErrors, IStructs, Ownable { 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, totalServiceRevenueETH)); @@ -367,7 +363,7 @@ contract Tokenomics is IErrors, IStructs, Ownable { /// @notice Record global data to checkpoint, any can do it /// @dev Checked point exist or not - function checkpoint() external onlyDispenser { + function checkpoint() external onlyTreasury { uint256 epoch = getEpoch(); PointEcomonics memory lastPoint = mapEpochEconomics[epoch]; // if not exist @@ -407,9 +403,9 @@ contract Tokenomics is IErrors, IStructs, Ownable { for (uint256 i = 0; i < numServices; ++i) { usf += _mapServiceAmounts[_protocolServiceIds[i]]; } - uint256 denominator = totalServiceRevenueETH * 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; @@ -419,10 +415,10 @@ contract Tokenomics is IErrors, IStructs, Ownable { } } - uint256 totalServiceRevenueOLA = _getExchangeAmountOLA(ETH_TOKEN_ADDRESS, totalServiceRevenueETH); + uint256 totalRewardOLA = _getExchangeAmountOLA(ETH_TOKEN_ADDRESS, totalServiceRevenueETH); _df = _calculateDFv1(_dcm); - PointEcomonics memory newPoint = PointEcomonics(_ucf, _usf, _df, stakerFraction, componentFraction, - agentFraction, totalServiceRevenueOLA, block.timestamp, block.number, true); + PointEcomonics memory newPoint = PointEcomonics(_ucf, _usf, _df, treasuryFraction, stakerFraction, + componentFraction, agentFraction, totalRewardOLA, block.timestamp, block.number, true); mapEpochEconomics[epoch] = newPoint; _clearEpochData(); @@ -540,32 +536,26 @@ contract Tokenomics is IErrors, IStructs, Ownable { } } - // decode a uq112x112 into a uint with 18 decimals of precision, re-calc if not exist - function getDFForEpoch(uint256 _epoch) external onlyDispenser returns (uint256 df) { - PointEcomonics memory _PE = mapEpochEconomics[_epoch]; - if (!_PE.exists) { - _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 returns (uint256 amountOLA) { + 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) { + function getProfitableComponents() external view + returns (address[] memory profitableComponents, uint256[] memory ucfcs) + { profitableComponents = _profitableComponents; + ucfcs = _ucfcs; } - function getProfitableAgents() external view returns (address[] memory profitableAgents) { + function getProfitableAgents() external view + returns (address[] memory profitableAgents, uint256[] memory ucfas) + { profitableAgents = _profitableAgents; + ucfas = _ucfas; } } diff --git a/contracts/Treasury.sol b/contracts/Treasury.sol index fd277799..4b79c56b 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); @@ -23,6 +25,8 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { event TokenomicsUpdated(address tokenomics); event DepositoryUpdated(address depository); event DispenserUpdated(address dispenser); + event TransferToDispenser(uint256 amount); + event TransferToProtocol(uint256 amount); enum TokenState { NonExistent, @@ -217,21 +221,67 @@ contract Treasury is IErrors, Ownable, ReentrancyGuard { enabled = (mapTokens[token].state == TokenState.Enabled); } - /// @dev Requests OLA funds from treasury. + /// @dev Sends OLA funds to dispenser. /// @param amount Amount of OLA. - function requestFunds(uint256 amount) external onlyDispenser { - // Check current OLA balance - uint256 balance = IOLA(ola).balanceOf(address(this)); + 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); - // 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); + 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 { + // Gets the latest economical point of epoch + PointEcomonics memory point = ITokenomics(tokenomics).getLastPoint(); - // Transfer funds to the dispenser - IERC20(ola).safeTransfer(dispenser, amount); + // If the point exists, it was already started and there is no need to continue + if (!point.exists) { + // Process the epoch data + ITokenomics(tokenomics).checkpoint(); + + // 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; + + // Protocol reward must be lower than the overall reward + if (amountOLA < protocolReward) { + revert AmountLowerThan(amountOLA, protocolReward); + } + amountOLA -= protocolReward; + + // Send funds to dispenser and protocol + _sendFundsToDispenser(amountOLA); + _sendFundsToProtocol(protocolReward); + + // Distribute rewards + if (amountOLA > 0) { + IDispenser(dispenser).distributeRewards(point.componentFraction, point.agentFraction, + point.stakerFraction, amountOLA); + } + } } } diff --git a/contracts/governance/VotingEscrow.sol b/contracts/governance/VotingEscrow.sol index 8bc8ac09..9921538b 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).withdrawStakingReward(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..c5ed29d5 --- /dev/null +++ b/contracts/interfaces/IDispenser.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @dev Interface for dispenser management. +interface IDispenser { + /// @dev Distributes rewards. + function distributeRewards( + uint256 componentFraction, + uint256 agentFraction, + uint256 stakerFraction, + uint256 amountOLA + ) external; + + /// @dev Withdraws rewards for stakers. + /// @param account Account address. + /// @return balance Reward balance. + function withdrawStakingReward(address account) external returns (uint256 balance); +} 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 2cac5de6..045434fa 100644 --- a/contracts/interfaces/IStructs.sol +++ b/contracts/interfaces/IStructs.sol @@ -27,10 +27,11 @@ interface IStructs { FixedPoint.uq112x112 ucf; FixedPoint.uq112x112 usf; FixedPoint.uq112x112 df; // x > 1.0 + uint256 treasuryFraction; uint256 stakerFraction; uint256 componentFraction; uint256 agentFraction; - uint256 totalRevenue; + 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 4b0f1c51..3c7888d6 100644 --- a/contracts/interfaces/ITokenomics.sol +++ b/contracts/interfaces/ITokenomics.sol @@ -14,6 +14,6 @@ interface ITokenomics is IStructs { 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); - function getProfitableAgents() external view returns (address[] memory profitableAgents); + function getProfitableComponents() external view returns (address[] memory profitableComponents, uint256[] memory ucfcs); + function getProfitableAgents() external view returns (address[] memory profitableAgents, uint256[] memory ucfcs); } From 764bd0b8e3053d3dabc046bbf86a8167b0b215c0 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 14 Apr 2022 17:44:12 +0100 Subject: [PATCH 3/8] feat: dispenser is pausable --- contracts/Dispenser.sol | 11 +++++++++-- contracts/Treasury.sol | 15 +++++++++------ contracts/interfaces/IDispenser.sol | 4 ++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/contracts/Dispenser.sol b/contracts/Dispenser.sol index 5bda8c22..e25ed86a 100644 --- a/contracts/Dispenser.sol +++ b/contracts/Dispenser.sol @@ -2,6 +2,7 @@ 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"; @@ -14,7 +15,7 @@ import "./interfaces/ITokenomics.sol"; /// @title Bond Depository - Smart contract for OLA Bond Depository /// @author AL -contract Dispenser is IErrors, IStructs, Ownable, ReentrancyGuard { +contract Dispenser is IErrors, IStructs, Ownable, Pausable, ReentrancyGuard { using SafeERC20 for IERC20; event VotingEscrowUpdated(address ve); @@ -159,7 +160,7 @@ contract Dispenser is IErrors, IStructs, Ownable, ReentrancyGuard { uint256 agentFraction, uint256 stakerFraction, uint256 amountOLA - ) external onlyTreasury + ) external onlyTreasury whenNotPaused { // Distribute rewards between component and agent owners _distributeOwnerRewards(componentFraction, agentFraction, amountOLA); @@ -188,4 +189,10 @@ contract Dispenser is IErrors, IStructs, Ownable, ReentrancyGuard { IERC20(ola).safeTransferFrom(address(this), ve, balance); } } + + /// @dev Gets the paused state. + /// @return True, if paused. + function isPaused() external returns (bool) { + return paused(); + } } diff --git a/contracts/Treasury.sol b/contracts/Treasury.sol index 4b79c56b..835afcd8 100644 --- a/contracts/Treasury.sol +++ b/contracts/Treasury.sol @@ -273,14 +273,17 @@ contract Treasury is IErrors, IStructs, Ownable, ReentrancyGuard { } amountOLA -= protocolReward; - // Send funds to dispenser and protocol - _sendFundsToDispenser(amountOLA); + // Send funds to protocol _sendFundsToProtocol(protocolReward); - // Distribute rewards - if (amountOLA > 0) { - IDispenser(dispenser).distributeRewards(point.componentFraction, point.agentFraction, - point.stakerFraction, amountOLA); + if (!IDispenser(dispenser).isPaused()) { + // Send funds to dispenser + _sendFundsToDispenser(amountOLA); + // Distribute rewards + if (amountOLA > 0) { + IDispenser(dispenser).distributeRewards(point.componentFraction, point.agentFraction, + point.stakerFraction, amountOLA); + } } } } diff --git a/contracts/interfaces/IDispenser.sol b/contracts/interfaces/IDispenser.sol index c5ed29d5..c647d6e9 100644 --- a/contracts/interfaces/IDispenser.sol +++ b/contracts/interfaces/IDispenser.sol @@ -15,4 +15,8 @@ interface IDispenser { /// @param account Account address. /// @return balance Reward balance. function withdrawStakingReward(address account) external returns (uint256 balance); + + /// @dev Gets the paused state. + /// @return True, if paused. + function isPaused() external returns (bool); } From 2b05a50a3d0cbd7361057f6051bbb64d835269b0 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 14 Apr 2022 20:46:26 +0100 Subject: [PATCH 4/8] fix: fixed all current tests --- contracts/Tokenomics.sol | 4 ++-- contracts/Treasury.sol | 2 +- test/integration/GovernanceControl.js | 3 ++- test/integration/TokenomicsLoop.js | 15 +++++++++++++-- test/unit/governance/Governance.js | 3 ++- test/unit/governance/VotingEscrow.js | 10 +++++++++- test/unit/tokenomics/depository/Depository.js | 5 +++-- test/unit/tokenomics/treasury/Treasury.js | 4 +++- 8 files changed, 35 insertions(+), 11 deletions(-) diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index d848677d..102a4ec4 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -223,7 +223,7 @@ contract Tokenomics is IErrors, IStructs, 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(i); + address owner = IERC721Enumerable(componentRegistry).ownerOf(componentId); _profitableComponents.push(owner); // Increase the profitable component number ++numProfitableComponents; @@ -282,7 +282,7 @@ contract Tokenomics is IErrors, IStructs, 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(i); + address owner = IERC721Enumerable(agentRegistry).ownerOf(agentId); _profitableAgents.push(owner); // Increase a profitable agent number ++numProfitableAgents; diff --git a/contracts/Treasury.sol b/contracts/Treasury.sol index 835afcd8..6a012168 100644 --- a/contracts/Treasury.sol +++ b/contracts/Treasury.sol @@ -57,7 +57,7 @@ contract Treasury is IErrors, IStructs, 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 _dispenser, address _tokenomics) { + constructor(address _ola, address _depository, address _tokenomics, address _dispenser) { if (_ola == address(0)) { revert ZeroAddress(); } 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 91b65cd1..b5ef1623 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,6 +27,8 @@ describe("Tokenomics integration", async () => { let treasury; let treasuryFactory; let tokenomics; + let ve; + let dispenser; let epochLen = 100; let supply = "10000000000000000000000"; // 10,000 @@ -69,6 +73,8 @@ describe("Tokenomics integration", async () => { 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", @@ -106,13 +112,18 @@ describe("Tokenomics integration", async () => { tokenomics = await tokenomicsFactory.deploy(ola.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); + 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); await ola.mint(deployer.address, initialMint); @@ -151,7 +162,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(); 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..06b6e2bd 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; @@ -71,7 +72,7 @@ describe("Bond Depository LP", async () => { tokenomics = await tokenomicsFactory.deploy(ola.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 diff --git a/test/unit/tokenomics/treasury/Treasury.js b/test/unit/tokenomics/treasury/Treasury.js index 3b92063f..e2571d59 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; @@ -60,7 +61,8 @@ describe("Treasury", async () => { tokenomics = await tokenomicsFactory.deploy(ola.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); From b0591d55b413f92f416524d1c59782a722a065a3 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 14 Apr 2022 21:47:12 +0100 Subject: [PATCH 5/8] fix and test: fixing algos and adding new dispenser integration test --- contracts/Dispenser.sol | 34 +++++++++---------- contracts/Treasury.sol | 7 ++-- contracts/governance/VotingEscrow.sol | 2 +- contracts/interfaces/IDispenser.sol | 9 +++-- deploy/contracts.js | 2 +- scripts/deploy.js | 2 +- test/integration/TokenomicsLoop.js | 47 ++++++++++++++++++++++++--- 7 files changed, 69 insertions(+), 34 deletions(-) diff --git a/contracts/Dispenser.sol b/contracts/Dispenser.sol index e25ed86a..37ffb5d8 100644 --- a/contracts/Dispenser.sol +++ b/contracts/Dispenser.sol @@ -74,9 +74,7 @@ contract Dispenser is IErrors, IStructs, Ownable, Pausable, ReentrancyGuard { } /// @dev Distributes rewards between component and agent owners. - function _distributeOwnerRewards(uint256 componentFraction, uint256 agentFraction, uint256 amountOLA) internal { - uint256 componentReward = componentFraction * amountOLA / 100; - uint256 agentReward = agentFraction * amountOLA / 100; + function _distributeOwnerRewards(uint256 componentReward, uint256 agentReward) internal { uint256 componentRewardLeft = componentReward; uint256 agentRewardLeft = agentReward; @@ -128,12 +126,12 @@ contract Dispenser is IErrors, IStructs, Ownable, Pausable, ReentrancyGuard { } /// @dev Distributes rewards between stakers. - function _distributeStakerRewards(uint256 stakerFraction, uint256 amountOLA) internal { + function _distributeStakerRewards(uint256 stakerReward) internal { VotingEscrow veContract = VotingEscrow(ve); address[] memory accounts = veContract.getLockAccounts(); // Get the overall amount of rewards for stakers - uint256 rewardLeft = stakerFraction * amountOLA / 100 ; + uint256 rewardLeft = stakerReward; // Iterate over staker addresses and distribute uint256 numAccounts = accounts.length; @@ -142,7 +140,7 @@ contract Dispenser is IErrors, IStructs, Ownable, Pausable, ReentrancyGuard { for (uint256 i = 0; i < numAccounts; ++i) { uint256 balance = veContract.balanceOf(accounts[i]); // Reward for this specific staker - uint256 reward = amountOLA * balance / supply; + uint256 reward = stakerReward * balance / supply; // If there is a rounding error, floor to the correct value if (reward > rewardLeft) { @@ -156,37 +154,35 @@ contract Dispenser is IErrors, IStructs, Ownable, Pausable, ReentrancyGuard { /// @dev Distributes rewards. function distributeRewards( - uint256 componentFraction, - uint256 agentFraction, - uint256 stakerFraction, - uint256 amountOLA + uint256 stakerReward, + uint256 componentReward, + uint256 agentReward ) external onlyTreasury whenNotPaused { // Distribute rewards between component and agent owners - _distributeOwnerRewards(componentFraction, agentFraction, amountOLA); + _distributeOwnerRewards(componentReward, agentReward); // Distribute rewards for stakers - _distributeStakerRewards(stakerFraction, amountOLA); + _distributeStakerRewards(stakerReward); } /// @dev Withdraws rewards for owners of components / agents. - /// @param account Account address. - function withdrawOwnerReward(address account) external nonReentrant { - uint256 balance = mapOwnerRewards[account]; + function withdrawOwnerRewards() external nonReentrant { + uint256 balance = mapOwnerRewards[msg.sender]; if (balance > 0) { - mapOwnerRewards[account] = 0; - IERC20(ola).safeTransferFrom(address(this), account, balance); + mapOwnerRewards[msg.sender] = 0; + IERC20(ola).safeTransfer(msg.sender, balance); } } /// @dev Withdraws rewards for stakers. /// @param account Account address. /// @return balance Reward balance. - function withdrawStakingReward(address account) external onlyVotingEscrow returns (uint256 balance) { + function withdrawStakingRewards(address account) external onlyVotingEscrow returns (uint256 balance) { balance = mapStakerRewards[account]; if (balance > 0) { mapStakerRewards[account] = 0; - IERC20(ola).safeTransferFrom(address(this), ve, balance); + IERC20(ola).safeTransfer(ve, balance); } } diff --git a/contracts/Treasury.sol b/contracts/Treasury.sol index 6a012168..73275550 100644 --- a/contracts/Treasury.sol +++ b/contracts/Treasury.sol @@ -261,11 +261,15 @@ contract Treasury is IErrors, IStructs, Ownable, ReentrancyGuard { if (!point.exists) { // 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) { @@ -281,8 +285,7 @@ contract Treasury is IErrors, IStructs, Ownable, ReentrancyGuard { _sendFundsToDispenser(amountOLA); // Distribute rewards if (amountOLA > 0) { - IDispenser(dispenser).distributeRewards(point.componentFraction, point.agentFraction, - point.stakerFraction, amountOLA); + IDispenser(dispenser).distributeRewards(stakerReward, componentReward, agentReward); } } } diff --git a/contracts/governance/VotingEscrow.sol b/contracts/governance/VotingEscrow.sol index 9921538b..390724bf 100644 --- a/contracts/governance/VotingEscrow.sol +++ b/contracts/governance/VotingEscrow.sol @@ -497,7 +497,7 @@ contract VotingEscrow is Ownable, ReentrancyGuard, ERC20VotesCustom { _checkpoint(msg.sender, _locked, LockedBalance(0,0)); // Return value from staking - value += IDispenser(dispenser).withdrawStakingReward(msg.sender); + value += IDispenser(dispenser).withdrawStakingRewards(msg.sender); // Clean up the account information uint256 id = _mapAccountIds[msg.sender]; diff --git a/contracts/interfaces/IDispenser.sol b/contracts/interfaces/IDispenser.sol index c647d6e9..975b111e 100644 --- a/contracts/interfaces/IDispenser.sol +++ b/contracts/interfaces/IDispenser.sol @@ -5,16 +5,15 @@ pragma solidity ^0.8.4; interface IDispenser { /// @dev Distributes rewards. function distributeRewards( - uint256 componentFraction, - uint256 agentFraction, - uint256 stakerFraction, - uint256 amountOLA + uint256 stakerReward, + uint256 componentReward, + uint256 agentReward ) external; /// @dev Withdraws rewards for stakers. /// @param account Account address. /// @return balance Reward balance. - function withdrawStakingReward(address account) external returns (uint256 balance); + function withdrawStakingRewards(address account) external returns (uint256 balance); /// @dev Gets the paused state. /// @return True, if paused. 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/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/test/integration/TokenomicsLoop.js b/test/integration/TokenomicsLoop.js index b5ef1623..1deaf251 100644 --- a/test/integration/TokenomicsLoop.js +++ b/test/integration/TokenomicsLoop.js @@ -31,8 +31,6 @@ describe("Tokenomics integration", async () => { let dispenser; let epochLen = 100; - let supply = "10000000000000000000000"; // 10,000 - let vesting = 60 * 60 *24; let timeToConclusion = 60 * 60 * 24; let conclusion; @@ -66,7 +64,7 @@ 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"); @@ -102,9 +100,7 @@ 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(); @@ -175,4 +171,45 @@ describe("Tokenomics integration", async () => { const usf = Number(point.usf / magicDenominator) * 1.0 / E18; 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); + + // Check the received reward + const agentFraction = await tokenomics.agentFraction(); + const expectedReward = regServiceRevenue * agentFraction / 100; + expect(Number(balance)).to.equal(expectedReward); + }); }); From d60c77e0d5f04b8266e498c3ed06f6025629c141 Mon Sep 17 00:00:00 2001 From: AL Date: Fri, 15 Apr 2022 03:49:58 -0500 Subject: [PATCH 6/8] fixed and refactor: fixed initial fraction, fixed description, naming, avoid nested block --- contracts/Dispenser.sol | 2 +- contracts/Tokenomics.sol | 6 +-- contracts/Treasury.sol | 64 +++++++++++++++--------------- contracts/interfaces/ITreasury.sol | 3 ++ 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/contracts/Dispenser.sol b/contracts/Dispenser.sol index 37ffb5d8..6c9bf430 100644 --- a/contracts/Dispenser.sol +++ b/contracts/Dispenser.sol @@ -13,7 +13,7 @@ import "./interfaces/ITreasury.sol"; import "./interfaces/ITokenomics.sol"; -/// @title Bond Depository - Smart contract for OLA Bond Depository +/// @title Dispenser - Smart contract for rewards /// @author AL contract Dispenser is IErrors, IStructs, Ownable, Pausable, ReentrancyGuard { using SafeERC20 for IERC20; diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 102a4ec4..7700a230 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -45,8 +45,8 @@ contract Tokenomics is IErrors, IStructs, Ownable { // Staking parameters with multiplying by 100 // treasuryFraction + componentFraction + agentFraction + stakerFraction = 100% - uint256 public treasuryFraction = 10; - uint256 public stakerFraction = 40; + uint256 public treasuryFraction = 0; + uint256 public stakerFraction = 50; uint256 public componentFraction = 33; uint256 public agentFraction = 17; @@ -131,7 +131,7 @@ contract Tokenomics is IErrors, IStructs, Ownable { /// @dev Sets staking parameters in fractions of distributed rewards. /// @param _stakerFraction Fraction for stakers. /// @param _componentFraction Fraction for component owners. - function changeStakingParameters( + function changeRewardFraction( uint256 _treasuryFraction, uint256 _stakerFraction, uint256 _componentFraction, diff --git a/contracts/Treasury.sol b/contracts/Treasury.sol index 73275550..6f134332 100644 --- a/contracts/Treasury.sol +++ b/contracts/Treasury.sol @@ -253,41 +253,43 @@ contract Treasury is IErrors, IStructs, Ownable, ReentrancyGuard { } /// @dev Starts a new epoch. - function allocateRewards() external onlyOwner { + 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) { - // 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); - } + 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/interfaces/ITreasury.sol b/contracts/interfaces/ITreasury.sol index 804db139..85be3fb1 100644 --- a/contracts/interfaces/ITreasury.sol +++ b/contracts/interfaces/ITreasury.sol @@ -29,4 +29,7 @@ interface ITreasury { /// @dev Requests OLA funds from treasury. function requestFunds(uint256 amount) external; + + /// @dev Starts a new epoch. + function allocateRewards() external returns (bool); } From e40eb3f005a093d58ef1608393d54c0cb4e10cae Mon Sep 17 00:00:00 2001 From: AL Date: Fri, 15 Apr 2022 05:22:42 -0500 Subject: [PATCH 7/8] feat: fix calculatePayoutFromLP according to logic checkout --- contracts/Tokenomics.sol | 49 ++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 7700a230..d9e36aaa 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -27,6 +27,7 @@ contract Tokenomics is IErrors, IStructs, Ownable { // 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 // Epsilon subject to rounding error uint256 public constant E13 = 10**13; // Maximum precision number to be considered @@ -429,16 +430,27 @@ contract Tokenomics is IErrors, IStructs, 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.exists) { - _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). @@ -524,15 +536,22 @@ contract Tokenomics is IErrors, IStructs, Ownable { _PE = mapEpochEconomics[epoch]; } - // decode a uq112x112 into a uint with 18 decimals of precision, 0 if not exist + // 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.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); // 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; } } From 74464aa0259a348be0bc20f3b5f7f73de86195db Mon Sep 17 00:00:00 2001 From: AL Date: Fri, 15 Apr 2022 06:58:30 -0500 Subject: [PATCH 8/8] feat: pre-implementation v2 design. BR(t) not taken into account yet --- contracts/Depository.sol | 7 ++ contracts/Tokenomics.sol | 72 ++++++++++++++++++- contracts/interfaces/ITokenomics.sol | 4 ++ test/integration/TokenomicsLoop.js | 3 +- test/unit/tokenomics/depository/Depository.js | 3 +- test/unit/tokenomics/treasury/Treasury.js | 2 +- 6 files changed, 86 insertions(+), 5 deletions(-) diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 09ee1399..9d01c313 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -121,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 @@ -202,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/Tokenomics.sol b/contracts/Tokenomics.sol index d9e36aaa..edc4d44a 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -10,17 +10,21 @@ import "./interfaces/IErrors.sol"; import "./interfaces/IStructs.sol"; import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; + /// @title Tokenomics - Smart contract for store/interface for key tokenomics params /// @author AL contract Tokenomics is IErrors, IStructs, Ownable { using FixedPoint for *; event TreasuryUpdated(address treasury); + 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 @@ -28,6 +32,7 @@ contract Tokenomics is IErrors, IStructs, Ownable { // 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,6 +56,12 @@ contract Tokenomics is IErrors, IStructs, Ownable { 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; // Agent Registry @@ -78,11 +89,12 @@ contract Tokenomics is IErrors, IStructs, Ownable { address public constant ETH_TOKEN_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); // TODO later fix government / manager - constructor(address _ola, address _treasury, uint256 _epochLen, address _componentRegistry, address _agentRegistry, + 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; @@ -97,12 +109,26 @@ contract Tokenomics is IErrors, IStructs, 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; @@ -115,18 +141,34 @@ contract Tokenomics is IErrors, IStructs, Ownable { /// @param _gammaNumerator Numerator for gamma value. /// @param _gammaDenominator Denominator for gamma value. /// @param _maxDF Maximum interest rate in %, 18 decimals. + /// @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. @@ -149,6 +191,21 @@ contract Tokenomics is IErrors, IStructs, Ownable { 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. function trackServicesETHRevenue(uint256[] memory serviceIds, uint256[] memory amounts) public onlyTreasury { @@ -360,6 +417,9 @@ contract Tokenomics is IErrors, IStructs, Ownable { } delete _protocolServiceIds; totalServiceRevenueETH = 0; + // clean bonding data + _bondLeft = maxBond; + _bondPerEpoch = 0; } /// @notice Record global data to checkpoint, any can do it @@ -577,4 +637,12 @@ contract Tokenomics is IErrors, IStructs, Ownable { 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/interfaces/ITokenomics.sol b/contracts/interfaces/ITokenomics.sol index 3c7888d6..ecb8c19a 100644 --- a/contracts/interfaces/ITokenomics.sol +++ b/contracts/interfaces/ITokenomics.sol @@ -16,4 +16,8 @@ interface ITokenomics is IStructs { 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/test/integration/TokenomicsLoop.js b/test/integration/TokenomicsLoop.js index 1deaf251..f887657d 100644 --- a/test/integration/TokenomicsLoop.js +++ b/test/integration/TokenomicsLoop.js @@ -105,7 +105,7 @@ describe("Tokenomics integration", 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, deployer.address); @@ -114,6 +114,7 @@ describe("Tokenomics integration", async () => { // 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); diff --git a/test/unit/tokenomics/depository/Depository.js b/test/unit/tokenomics/depository/Depository.js index 06b6e2bd..6ba1c57f 100644 --- a/test/unit/tokenomics/depository/Depository.js +++ b/test/unit/tokenomics/depository/Depository.js @@ -69,7 +69,7 @@ describe("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, AddressZero); @@ -78,6 +78,7 @@ describe("Depository LP", async () => { // 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 e2571d59..42410796 100644 --- a/test/unit/tokenomics/treasury/Treasury.js +++ b/test/unit/tokenomics/treasury/Treasury.js @@ -58,7 +58,7 @@ 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 // Dispenser address is irrelevant in these tests, so its contract is passed as a zero address