Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FeeCurrency Wrapper #10905

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions packages/protocol/contracts-0.8/stability/FeeCurrencyWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.7 <0.8.20;

import "@openzeppelin/contracts8/access/Ownable.sol";
import "@openzeppelin/contracts8/token/ERC20/IERC20.sol";
import "forge-std/console.sol";

import "../../contracts/common/CalledByVm.sol";
import "../../contracts/common/Initializable.sol";
import "../../contracts/common/interfaces/ICeloVersionedContract.sol";
import "../../contracts/common/FixidityLib.sol";
import "../../contracts/stability/interfaces/ISortedOracles.sol";
import "./IFeeCurrency.sol";
import "./IDecimals.sol";

contract FeeCurrencyWrapper is Initializable, CalledByVm {
IFeeCurrency public wrappedToken;

uint96 public digitDifference;

uint256 public debited;

string public name;
string public symbol;

/**
* @notice Sets initialized == true on implementation contracts
* @param test Set to true to skip implementation initialization
*/
constructor(bool test) public Initializable(test) {}

/**
* @notice Returns the storage, major, minor, and patch version of the contract.
* @return Storage version of the contract.
* @return Major version of the contract.
* @return Minor version of the contract.
* @return Patch version of the contract.
*/
function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) {
return (1, 1, 0, 0);
}

/**
* @notice Used in place of the constructor to allow the contract to be upgradable via proxy.
* @param _wrappedToken The address of the wrapped token.
* @param _name The name of the wrapped token.
* @param _symbol The symbol of the wrapped token.
* @param _expectedDecimals The expected number of decimals of the wrapped token.
*/
function initialize(address _wrappedToken, string memory _name, string memory _symbol, uint8 _expectedDecimals)
external
initializer
{
wrappedToken = IFeeCurrency(_wrappedToken);
name = _name;
symbol = _symbol;
uint8 decimals = IDecimals(_wrappedToken).decimals();
digitDifference = uint96(10**(_expectedDecimals - decimals));
}

/**
* @notice Gets the balance of the specified address with correct digits.
* @param account The address to query the balance of.
* @return The balance of the specified address.
*/
function balanceOf(address account) public view returns (uint256) {
return upscale(wrappedToken.balanceOf(account));
}

/**
* Downscales value to the wrapped token's native digits and debits it.
* @param from from address
* @param value Debited value in the wrapped digits.
*/
function debitGasFees(address from, uint256 value) external onlyVm {
uint256 toDebit = downscale(value);
require(toDebit > 0, "Must debit at least one token.");
debited = toDebit;
wrappedToken.debitGasFees(from, toDebit);
}

/**
* Downscales value to the wrapped token's native digits and credits it.
* @param recipients The recipients
* @param amounts The amounts (in wrapped token digits)
*/
function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) public onlyVm {
if (debited == 0) {
return;
}
require(recipients.length == amounts.length, "Recipients and amounts must be the same length.");

uint256[] memory scaledAmounts = new uint256[](amounts.length);

uint256 totalToBeCredited = 0;

for (uint256 i = 0; i < amounts.length; i++) {
scaledAmounts[i] = downscale(amounts[i]);
totalToBeCredited += scaledAmounts[i];
}

require(totalToBeCredited <= debited, "Cannot credit more than debited.");

uint256 roundingError = debited - totalToBeCredited;
if (roundingError > 0) {
scaledAmounts[0] += roundingError;
}

wrappedToken.creditGasFees(recipients, scaledAmounts);
debited = 0;
}

/**
* Downscales value to the wrapped token's native digits and credits it.
* @param refundRecipient The recipient of the refund.
* @param tipRecipient The recipient of the tip.
* @param _gatewayFeeRecipient The recipient of the gateway fee. Unused.
* @param baseFeeRecipient The recipient of the base fee.
* @param refundAmount The amount to refund (in wrapped token digits).
* @param tipAmount The amount to tip (in wrapped token digits).
* @param _gatewayFeeAmount The amount of the gateway fee (in wrapped token digits). Unused.
* @param baseFeeAmount The amount of the base fee (in wrapped token digits).
*/
function creditGasFees(
address refundRecipient,
address tipRecipient,
address _gatewayFeeRecipient,
address baseFeeRecipient,
uint256 refundAmount,
uint256 tipAmount,
uint256 _gatewayFeeAmount,
uint256 baseFeeAmount
) public onlyVm {
if (debited == 0) {
return;
}

uint256 refundScaled = downscale(refundAmount);
uint256 tipTxFeeScaled = downscale(tipAmount);
uint256 baseTxFeeScaled = downscale(baseFeeAmount);

require(
refundScaled + tipTxFeeScaled + baseTxFeeScaled <= debited,
"Cannot credit more than debited."
);

uint256 roundingError = debited - (refundScaled + tipTxFeeScaled + baseTxFeeScaled);

if (roundingError > 0) {
baseTxFeeScaled += roundingError;
}
wrappedToken.creditGasFees(
refundRecipient,
tipRecipient,
address(0),
baseFeeRecipient,
refundScaled,
tipTxFeeScaled,
0,
baseTxFeeScaled
);

debited = 0;
}

function upscale(uint256 value) internal view returns (uint256) {
return value * digitDifference;
}

function downscale(uint256 value) internal view returns (uint256) {
return value / digitDifference;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pragma solidity ^0.8.13;

import "@openzeppelin/contracts8/token/ERC20/IERC20.sol";

interface IDecimals is IERC20 {
function decimals() external view returns (uint8);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
pragma solidity ^0.8.13;

import "@openzeppelin/contracts8/token/ERC20/IERC20.sol";

interface IFeeCurrency is IERC20 {
/*
This interface should be implemented for tokens which are supposed to
act as fee currencies on the Celo blockchain, meaning that they can be
used to pay gas fees for CIP-64 transactions (and some older tx types).
See https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0064.md

Before executing a tx with non-empty feeCurrency field, the fee
currency's `debitGasFees` function is called to reserve the maximum
amount that tx can spend on gas. After the tx has been executed, the
`creditGasFees` function is called to refund the unused gas and credit
the spent fees to the correct recipients. Events which are raised inside
these functions will show up for every transaction using the token as a
fee currency.

Requirements:
- The functions will be called by the blockchain client with `msg.sender
== address(0)`. If this condition is not met, the functions must
revert to prevent malicious users from crediting their accounts directly.
- `creditGasFees` must credit all specified amounts. If it impossible to
credit one of the recipients for some reason, add the amount to the
value credited to the first valid recipient. This is important to keep
the debited and credited amounts consistent.

Notes on compatibility:
- There are two versions of `creditGasFees`: one for the current
(2024-01-16) blockchain implementation and a more future-proof version
that avoids deprecated fields and allows new recipients that might
become necessary on later blockchain implementations. Both versions
should be implemented to increase compatibility.
- Future Celo blockchain implementations might provide a way for plain
ERC-20 tokens to be used as gas currencies without implementing this
interface. If this sounds preferable to you, please contact cLabs
before implementing this interface for your token.
*/

// Called before transaction execution to reserve the maximum amount of gas
// that can be used by the transaction.
// - The implementation must reduce `from`'s balance by `value`.
// - Must revert if `msg.sender` is not the zero address.
function debitGasFees(address from, uint256 value) external;

// New function signature, will be used when all fee currencies have migrated.
// Credited amounts are gas refund, base fee and tip. Additional components
// might be added, like an L1 gas fee when Celo becomes and L2.
// - The implementation must increase each `recipient`'s balance by respective `value`.
// - Must revert if `msg.sender` is not the zero address.
// - Must revert if `recipients` and `amounts` have different lengths.
function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) external;

// Old function signature for backwards compatibility
// - Must revert if `msg.sender` is not the zero address.
// - `refund` must be credited to `from`
// - `tipTxFee` must be credited to `feeRecipient`
// - `baseTxFee` must be credited to `communityFund`
// - `gatewayFeeRecipient` and `gatewayFee` only exist for backwards
// compatibility reasons and will always be zero.
function creditGasFees(
address from,
address feeRecipient,
address gatewayFeeRecipient,
address communityFund,
uint256 refund,
uint256 tipTxFee,
uint256 gatewayFee,
uint256 baseTxFee
) external;
}
Loading
Loading