Files
2022-11-25 15:14:43 +02:00

329 lines
16 KiB
Solidity

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../../common/implementation/Lockable.sol";
import "../../common/implementation/FixedPoint.sol";
import "../../common/implementation/Testable.sol";
import "../../data-verification-mechanism/interfaces/StoreInterface.sol";
import "../../data-verification-mechanism/interfaces/FinderInterface.sol";
import "../../data-verification-mechanism/interfaces/AdministrateeInterface.sol";
import "../../data-verification-mechanism/implementation/Constants.sol";
/**
* @title FeePayer contract.
* @notice Provides fee payment functionality for the ExpiringMultiParty contract.
* contract is abstract as each derived contract that inherits `FeePayer` must implement `pfc()`.
*/
abstract contract FeePayer is AdministrateeInterface, Testable, Lockable {
using SafeMath for uint256;
using FixedPoint for FixedPoint.Unsigned;
using SafeERC20 for IERC20;
/****************************************
* FEE PAYER DATA STRUCTURES *
****************************************/
// The collateral currency used to back the positions in this contract.
IERC20 public collateralCurrency;
// Finder contract used to look up addresses for UMA system contracts.
FinderInterface public finder;
// Tracks the last block time when the fees were paid.
uint256 private lastPaymentTime;
// Tracks the cumulative fees that have been paid by the contract for use by derived contracts.
// The multiplier starts at 1, and is updated by computing cumulativeFeeMultiplier * (1 - effectiveFee).
// Put another way, the cumulativeFeeMultiplier is (1 - effectiveFee1) * (1 - effectiveFee2) ...
// For example:
// The cumulativeFeeMultiplier should start at 1.
// If a 1% fee is charged, the multiplier should update to .99.
// If another 1% fee is charged, the multiplier should be 0.99^2 (0.9801).
FixedPoint.Unsigned public cumulativeFeeMultiplier;
/****************************************
* EVENTS *
****************************************/
event RegularFeesPaid(uint256 indexed regularFee, uint256 indexed lateFee);
event FinalFeesPaid(uint256 indexed amount);
/****************************************
* MODIFIERS *
****************************************/
// modifier that calls payRegularFees().
modifier fees virtual {
// Note: the regular fee is applied on every fee-accruing transaction, where the total change is simply the
// regular fee applied linearly since the last update. This implies that the compounding rate depends on the
// frequency of update transactions that have this modifier, and it never reaches the ideal of continuous
// compounding. This approximate-compounding pattern is common in the Ethereum ecosystem because of the
// complexity of compounding data on-chain.
payRegularFees();
_;
}
/**
* @notice Constructs the FeePayer contract. Called by child contracts.
* @param _collateralAddress ERC20 token that is used as the underlying collateral for the synthetic.
* @param _finderAddress UMA protocol Finder used to discover other protocol contracts.
* @param _timerAddress Contract that stores the current time in a testing environment.
* Must be set to 0x0 for production environments that use live time.
*/
constructor(
address _collateralAddress,
address _finderAddress,
address _timerAddress
) Testable(_timerAddress) {
collateralCurrency = IERC20(_collateralAddress);
finder = FinderInterface(_finderAddress);
lastPaymentTime = getCurrentTime();
cumulativeFeeMultiplier = FixedPoint.fromUnscaledUint(1);
}
/****************************************
* FEE PAYMENT FUNCTIONS *
****************************************/
/**
* @notice Pays UMA DVM regular fees (as a % of the collateral pool) to the Store contract.
* @dev These must be paid periodically for the life of the contract. If the contract has not paid its regular fee
* in a week or more then a late penalty is applied which is sent to the caller. If the amount of
* fees owed are greater than the pfc, then this will pay as much as possible from the available collateral.
* An event is only fired if the fees charged are greater than 0.
* @return totalPaid Amount of collateral that the contract paid (sum of the amount paid to the Store and caller).
* This returns 0 and exit early if there is no pfc, fees were already paid during the current block, or the fee rate is 0.
*/
function payRegularFees() public nonReentrant() returns (FixedPoint.Unsigned memory) {
uint256 time = getCurrentTime();
FixedPoint.Unsigned memory collateralPool = _pfc();
// Fetch the regular fees, late penalty and the max possible to pay given the current collateral within the contract.
(
FixedPoint.Unsigned memory regularFee,
FixedPoint.Unsigned memory latePenalty,
FixedPoint.Unsigned memory totalPaid
) = getOutstandingRegularFees(time);
lastPaymentTime = time;
// If there are no fees to pay then exit early.
if (totalPaid.isEqual(0)) {
return totalPaid;
}
emit RegularFeesPaid(regularFee.rawValue, latePenalty.rawValue);
_adjustCumulativeFeeMultiplier(totalPaid, collateralPool);
if (regularFee.isGreaterThan(0)) {
StoreInterface store = _getStore();
collateralCurrency.safeIncreaseAllowance(address(store), regularFee.rawValue);
store.payOracleFeesErc20(address(collateralCurrency), regularFee);
}
if (latePenalty.isGreaterThan(0)) {
collateralCurrency.safeTransfer(msg.sender, latePenalty.rawValue);
}
return totalPaid;
}
/**
* @notice Fetch any regular fees that the contract has pending but has not yet paid. If the fees to be paid are more
* than the total collateral within the contract then the totalPaid returned is full contract collateral amount.
* @dev This returns 0 and exit early if there is no pfc, fees were already paid during the current block, or the fee rate is 0.
* @return regularFee outstanding unpaid regular fee.
* @return latePenalty outstanding unpaid late fee for being late in previous fee payments.
* @return totalPaid Amount of collateral that the contract paid (sum of the amount paid to the Store and caller).
*/
function getOutstandingRegularFees(uint256 time)
public
view
returns (
FixedPoint.Unsigned memory regularFee,
FixedPoint.Unsigned memory latePenalty,
FixedPoint.Unsigned memory totalPaid
)
{
StoreInterface store = _getStore();
FixedPoint.Unsigned memory collateralPool = _pfc();
// Exit early if there is no collateral or if fees were already paid during this block.
if (collateralPool.isEqual(0) || lastPaymentTime == time) {
return (regularFee, latePenalty, totalPaid);
}
(regularFee, latePenalty) = store.computeRegularFee(lastPaymentTime, time, collateralPool);
totalPaid = regularFee.add(latePenalty);
if (totalPaid.isEqual(0)) {
return (regularFee, latePenalty, totalPaid);
}
// If the effective fees paid as a % of the pfc is > 100%, then we need to reduce it and make the contract pay
// as much of the fee that it can (up to 100% of its pfc). We'll reduce the late penalty first and then the
// regular fee, which has the effect of paying the store first, followed by the caller if there is any fee remaining.
if (totalPaid.isGreaterThan(collateralPool)) {
FixedPoint.Unsigned memory deficit = totalPaid.sub(collateralPool);
FixedPoint.Unsigned memory latePenaltyReduction = FixedPoint.min(latePenalty, deficit);
latePenalty = latePenalty.sub(latePenaltyReduction);
deficit = deficit.sub(latePenaltyReduction);
regularFee = regularFee.sub(FixedPoint.min(regularFee, deficit));
totalPaid = collateralPool;
}
}
/**
* @notice Gets the current profit from corruption for this contract in terms of the collateral currency.
* @dev This is equivalent to the collateral pool available from which to pay fees. Therefore, derived contracts are
* expected to implement this so that pay-fee methods can correctly compute the owed fees as a % of PfC.
* @return pfc value for equal to the current profit from corruption denominated in collateral currency.
*/
function pfc() external view override nonReentrantView() returns (FixedPoint.Unsigned memory) {
return _pfc();
}
/**
* @notice Removes excess collateral balance not counted in the PfC by distributing it out pro-rata to all sponsors.
* @dev Multiplying the `cumulativeFeeMultiplier` by the ratio of non-PfC-collateral :: PfC-collateral effectively
* pays all sponsors a pro-rata portion of the excess collateral.
* @dev This will revert if PfC is 0 and this contract's collateral balance > 0.
*/
function gulp() external nonReentrant() {
_gulp();
}
/****************************************
* INTERNAL FUNCTIONS *
****************************************/
// Pays UMA Oracle final fees of `amount` in `collateralCurrency` to the Store contract. Final fee is a flat fee
// charged for each price request. If payer is the contract, adjusts internal bookkeeping variables. If payer is not
// the contract, pulls in `amount` of collateral currency.
function _payFinalFees(address payer, FixedPoint.Unsigned memory amount) internal {
if (amount.isEqual(0)) {
return;
}
if (payer != address(this)) {
// If the payer is not the contract pull the collateral from the payer.
collateralCurrency.safeTransferFrom(payer, address(this), amount.rawValue);
} else {
// If the payer is the contract, adjust the cumulativeFeeMultiplier to compensate.
FixedPoint.Unsigned memory collateralPool = _pfc();
// The final fee must be < available collateral or the fee will be larger than 100%.
// Note: revert reason removed to save bytecode.
require(collateralPool.isGreaterThan(amount));
_adjustCumulativeFeeMultiplier(amount, collateralPool);
}
emit FinalFeesPaid(amount.rawValue);
StoreInterface store = _getStore();
collateralCurrency.safeIncreaseAllowance(address(store), amount.rawValue);
store.payOracleFeesErc20(address(collateralCurrency), amount);
}
function _gulp() internal {
FixedPoint.Unsigned memory currentPfc = _pfc();
FixedPoint.Unsigned memory currentBalance = FixedPoint.Unsigned(collateralCurrency.balanceOf(address(this)));
if (currentPfc.isLessThan(currentBalance)) {
cumulativeFeeMultiplier = cumulativeFeeMultiplier.mul(currentBalance.div(currentPfc));
}
}
function _pfc() internal view virtual returns (FixedPoint.Unsigned memory);
function _getStore() internal view returns (StoreInterface) {
return StoreInterface(finder.getImplementationAddress(OracleInterfaces.Store));
}
function _computeFinalFees() internal view returns (FixedPoint.Unsigned memory finalFees) {
StoreInterface store = _getStore();
return store.computeFinalFee(address(collateralCurrency));
}
// Returns the user's collateral minus any fees that have been subtracted since it was originally
// deposited into the contract. Note: if the contract has paid fees since it was deployed, the raw
// value should be larger than the returned value.
function _getFeeAdjustedCollateral(FixedPoint.Unsigned memory rawCollateral)
internal
view
returns (FixedPoint.Unsigned memory collateral)
{
return rawCollateral.mul(cumulativeFeeMultiplier);
}
// Returns the user's collateral minus any pending fees that have yet to be subtracted.
function _getPendingRegularFeeAdjustedCollateral(FixedPoint.Unsigned memory rawCollateral)
internal
view
returns (FixedPoint.Unsigned memory)
{
(, , FixedPoint.Unsigned memory currentTotalOutstandingRegularFees) =
getOutstandingRegularFees(getCurrentTime());
if (currentTotalOutstandingRegularFees.isEqual(FixedPoint.fromUnscaledUint(0))) return rawCollateral;
// Calculate the total outstanding regular fee as a fraction of the total contract PFC.
FixedPoint.Unsigned memory effectiveOutstandingFee = currentTotalOutstandingRegularFees.divCeil(_pfc());
// Scale as rawCollateral* (1 - effectiveOutstandingFee) to apply the pro-rata amount to the regular fee.
return rawCollateral.mul(FixedPoint.fromUnscaledUint(1).sub(effectiveOutstandingFee));
}
// Converts a user-readable collateral value into a raw value that accounts for already-assessed fees. If any fees
// have been taken from this contract in the past, then the raw value will be larger than the user-readable value.
function _convertToRawCollateral(FixedPoint.Unsigned memory collateral)
internal
view
returns (FixedPoint.Unsigned memory rawCollateral)
{
return collateral.div(cumulativeFeeMultiplier);
}
// Decrease rawCollateral by a fee-adjusted collateralToRemove amount. Fee adjustment scales up collateralToRemove
// by dividing it by cumulativeFeeMultiplier. There is potential for this quotient to be floored, therefore
// rawCollateral is decreased by less than expected. Because this method is usually called in conjunction with an
// actual removal of collateral from this contract, return the fee-adjusted amount that the rawCollateral is
// decreased by so that the caller can minimize error between collateral removed and rawCollateral debited.
function _removeCollateral(FixedPoint.Unsigned storage rawCollateral, FixedPoint.Unsigned memory collateralToRemove)
internal
returns (FixedPoint.Unsigned memory removedCollateral)
{
FixedPoint.Unsigned memory initialBalance = _getFeeAdjustedCollateral(rawCollateral);
FixedPoint.Unsigned memory adjustedCollateral = _convertToRawCollateral(collateralToRemove);
rawCollateral.rawValue = rawCollateral.sub(adjustedCollateral).rawValue;
removedCollateral = initialBalance.sub(_getFeeAdjustedCollateral(rawCollateral));
}
// Increase rawCollateral by a fee-adjusted collateralToAdd amount. Fee adjustment scales up collateralToAdd
// by dividing it by cumulativeFeeMultiplier. There is potential for this quotient to be floored, therefore
// rawCollateral is increased by less than expected. Because this method is usually called in conjunction with an
// actual addition of collateral to this contract, return the fee-adjusted amount that the rawCollateral is
// increased by so that the caller can minimize error between collateral added and rawCollateral credited.
// NOTE: This return value exists only for the sake of symmetry with _removeCollateral. We don't actually use it
// because we are OK if more collateral is stored in the contract than is represented by rawTotalPositionCollateral.
function _addCollateral(FixedPoint.Unsigned storage rawCollateral, FixedPoint.Unsigned memory collateralToAdd)
internal
returns (FixedPoint.Unsigned memory addedCollateral)
{
FixedPoint.Unsigned memory initialBalance = _getFeeAdjustedCollateral(rawCollateral);
FixedPoint.Unsigned memory adjustedCollateral = _convertToRawCollateral(collateralToAdd);
rawCollateral.rawValue = rawCollateral.add(adjustedCollateral).rawValue;
addedCollateral = _getFeeAdjustedCollateral(rawCollateral).sub(initialBalance);
}
// Scale the cumulativeFeeMultiplier by the ratio of fees paid to the current available collateral.
function _adjustCumulativeFeeMultiplier(FixedPoint.Unsigned memory amount, FixedPoint.Unsigned memory currentPfc)
internal
{
FixedPoint.Unsigned memory effectiveFee = amount.divCeil(currentPfc);
cumulativeFeeMultiplier = cumulativeFeeMultiplier.mul(FixedPoint.fromUnscaledUint(1).sub(effectiveFee));
}
}