pragma solidity ^0.5.16; // Inheritance import "./Owned.sol"; import "./SelfDestructible.sol"; import "./LimitedSetup.sol"; // Libraries import "./SafeDecimalMath.sol"; // Internal references import "./interfaces/IFeePool.sol"; // https://docs.oikos.cash/contracts/FeePoolState contract FeePoolState is Owned, SelfDestructible, LimitedSetup { using SafeMath for uint; using SafeDecimalMath for uint; /* ========== STATE VARIABLES ========== */ uint8 public constant FEE_PERIOD_LENGTH = 6; address public feePool; // The IssuanceData activity that's happened in a fee period. struct IssuanceData { uint debtPercentage; uint debtEntryIndex; } // The IssuanceData activity that's happened in a fee period. mapping(address => IssuanceData[FEE_PERIOD_LENGTH]) public accountIssuanceLedger; constructor(address _owner, IFeePool _feePool) public Owned(_owner) SelfDestructible() LimitedSetup(6 weeks) { feePool = address(_feePool); } /* ========== SETTERS ========== */ /** * @notice set the FeePool contract as it is the only authority to be able to call * appendAccountIssuanceRecord with the onlyFeePool modifer * @dev Must be set by owner when FeePool logic is upgraded */ function setFeePool(IFeePool _feePool) external onlyOwner { feePool = address(_feePool); } /* ========== VIEWS ========== */ /** * @notice Get an accounts issuanceData for * @param account users account * @param index Index in the array to retrieve. Upto FEE_PERIOD_LENGTH */ function getAccountsDebtEntry(address account, uint index) public view returns (uint debtPercentage, uint debtEntryIndex) { require(index < FEE_PERIOD_LENGTH, "index exceeds the FEE_PERIOD_LENGTH"); debtPercentage = accountIssuanceLedger[account][index].debtPercentage; debtEntryIndex = accountIssuanceLedger[account][index].debtEntryIndex; } /** * @notice Find the oldest debtEntryIndex for the corresponding closingDebtIndex * @param account users account * @param closingDebtIndex the last periods debt index on close */ function applicableIssuanceData(address account, uint closingDebtIndex) external view returns (uint, uint) { IssuanceData[FEE_PERIOD_LENGTH] memory issuanceData = accountIssuanceLedger[account]; // We want to use the user's debtEntryIndex at when the period closed // Find the oldest debtEntryIndex for the corresponding closingDebtIndex for (uint i = 0; i < FEE_PERIOD_LENGTH; i++) { if (closingDebtIndex >= issuanceData[i].debtEntryIndex) { return (issuanceData[i].debtPercentage, issuanceData[i].debtEntryIndex); } } } /* ========== MUTATIVE FUNCTIONS ========== */ /** * @notice Logs an accounts issuance data in the current fee period which is then stored historically * @param account Message.Senders account address * @param debtRatio Debt of this account as a percentage of the global debt. * @param debtEntryIndex The index in the global debt ledger. oikos.oikosState().issuanceData(account) * @param currentPeriodStartDebtIndex The startingDebtIndex of the current fee period * @dev onlyFeePool to call me on oikos.issue() & oikos.burn() calls to store the locked OKS * per fee period so we know to allocate the correct proportions of fees and rewards per period accountIssuanceLedger[account][0] has the latest locked amount for the current period. This can be update as many time accountIssuanceLedger[account][1-2] has the last locked amount for a previous period they minted or burned */ function appendAccountIssuanceRecord( address account, uint debtRatio, uint debtEntryIndex, uint currentPeriodStartDebtIndex ) external onlyFeePool { // Is the current debtEntryIndex within this fee period if (accountIssuanceLedger[account][0].debtEntryIndex < currentPeriodStartDebtIndex) { // If its older then shift the previous IssuanceData entries periods down to make room for the new one. issuanceDataIndexOrder(account); } // Always store the latest IssuanceData entry at [0] accountIssuanceLedger[account][0].debtPercentage = debtRatio; accountIssuanceLedger[account][0].debtEntryIndex = debtEntryIndex; } /** * @notice Pushes down the entire array of debt ratios per fee period */ function issuanceDataIndexOrder(address account) private { for (uint i = FEE_PERIOD_LENGTH - 2; i < FEE_PERIOD_LENGTH; i--) { uint next = i + 1; accountIssuanceLedger[account][next].debtPercentage = accountIssuanceLedger[account][i].debtPercentage; accountIssuanceLedger[account][next].debtEntryIndex = accountIssuanceLedger[account][i].debtEntryIndex; } } /** * @notice Import issuer data from oikosState.issuerData on FeePeriodClose() block # * @dev Only callable by the contract owner, and only for 6 weeks after deployment. * @param accounts Array of issuing addresses * @param ratios Array of debt ratios * @param periodToInsert The Fee Period to insert the historical records into * @param feePeriodCloseIndex An accounts debtEntryIndex is valid when within the fee peroid, * since the input ratio will be an average of the pervious periods it just needs to be * > recentFeePeriods[periodToInsert].startingDebtIndex * < recentFeePeriods[periodToInsert - 1].startingDebtIndex */ function importIssuerData( address[] calldata accounts, uint[] calldata ratios, uint periodToInsert, uint feePeriodCloseIndex ) external onlyOwner onlyDuringSetup { require(accounts.length == ratios.length, "Length mismatch"); for (uint i = 0; i < accounts.length; i++) { accountIssuanceLedger[accounts[i]][periodToInsert].debtPercentage = ratios[i]; accountIssuanceLedger[accounts[i]][periodToInsert].debtEntryIndex = feePeriodCloseIndex; emit IssuanceDebtRatioEntry(accounts[i], ratios[i], feePeriodCloseIndex); } } /* ========== MODIFIERS ========== */ modifier onlyFeePool { require(msg.sender == address(feePool), "Only the FeePool contract can perform this action"); _; } /* ========== Events ========== */ event IssuanceDebtRatioEntry(address indexed account, uint debtRatio, uint feePeriodCloseIndex); }