// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.18; import "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // TODO: Waiting for tokenomics to be finalized // TODO: Replace ERC2771ContextUpgradeable with MutableERC2771ContextUpgradeable /** * @title CreditManager. * @author Arianee - Dynamic NFTs for real-world use cases and consumer engagement (www.arianee.org). * @notice The CreditManager contract is part of the Arianee protocol. Its facilitates the purchase, consumption, and management of credits associated with specific contracts. * @dev See: https://docs.arianee.org */ contract CreditManager is ERC2771ContextUpgradeable, OwnableUpgradeable, UUPSUpgradeable { /** * @notice Represents a credit purchase. * @param price The price of the credit. * @param quantity The quantity of credits purchased. */ struct CreditBuy { uint256 price; uint256 quantity; address contractAddress; } /** * @notice Address allowed to modify the exchange rate between ARIA and USD. */ address public exchangeModifier; /** * @notice Exchange rate between ARIA and USD. */ uint256 public ariaUsdExchangeRate; /** * @notice Address of the ERC20 token used to purchase credits. */ IERC20 public acceptedToken; /** * @notice Arianee infrastructure address. */ address public infraAddress; /** * @notice Arianee protocol address. */ address public protocolAddress; /** * @notice Array of credit batches */ CreditBuy[] public creditBatches; /** * @notice Mapping from spender & contract address to credit batches */ mapping(address => mapping(address => uint256[])) public creditHistory; /** * @notice Mapping from spender & contract address to credit batches index to use */ mapping(address => mapping(address => uint256)) public creditHistoryIndex; /** * @notice Mapping from spender & contract address to total credits owned for this contract */ mapping(address => mapping(address => uint256)) public totalCredits; /** * @notice Mapping from credit to credit price in usd */ mapping(address => uint256) public creditPriceUSD; /** * @notice Mapping of how the reward is dispatched */ mapping(uint256 => uint256) public dispatchPercent; /** * @notice This emits when the dispatch percent is changed. */ event DispatchRewardChanged( uint8 _percentInfra, uint8 _percentCreatorProvider, uint8 _percentConsumerProvider, uint8 _percentArianeeProject, uint8 _percentFirstOwner ); /** * @notice This emits when a credit is bought */ event CreditBought(address indexed buyer, address indexed _receiver, address indexed _contract, uint256 quantity); /** * @notice This emits when a credit is consumed */ event CreditConsumed(address indexed _spender, address indexed _contract); /** * @notice This emits when the aria echange rate is updated */ event NewAriaUsdExchangeRate(uint256 _ariaUsdExchangeRate); /** * @notice This emits when the address allowed to change the aria usd rate is changed */ event NewExchangeModifier(address _exchangeModifier); /** * @notice This emits when the address rewarded for the infrastructure is changed */ event NewInfraAddress(address _newInfraAddress); /** * @notice This emits when the address rewarded for the protocol is changed */ event NewProtocolAddress(address _newProtocolAddress); /** * @notice This emits when the usd price of a credit is change */ event NewCreditPriceUsd(address _contract, uint256 _creditPriceUsd); /** * @notice Initializer of the contract, used instead of constructor to let the proxy pattern works. * @param _exchangeModifier address allowed to change the aria usd rate. * @param _acceptedToken address of the ERC20 token used to purchase credits. */ function initialize(address _exchangeModifier, address _acceptedToken) public initializer { exchangeModifier = _exchangeModifier; acceptedToken = IERC20(address(_acceptedToken)); __Ownable_init(); __UUPSUpgradeable_init(); } /// @custom:oz-upgrades-unsafe-allow constructor constructor(address trustedForwarder_) ERC2771ContextUpgradeable(trustedForwarder_) { _disableInitializers(); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} /** * @notice Check if the `_msgSender()` is allowed to change the exchange rate. * @dev See {IRulesManager-isAllowedEventEmitter}. */ modifier onlyExchangeModifier() { require(_msgSender() == exchangeModifier, "You are not the exchange modifier"); _; } /** * @notice Buy credit for a specific contract * @param _contract address of the contract to buy credit for. * @param _quantity quantity of credit to buy. * @param _receiver address of the receiver of the credit. */ function buyCredit(address _contract, uint256 _quantity, address _receiver) public { require(_quantity > 0, "Credit Manager: The quantity must be more than 0" ); uint256 creditPrice = getCreditPriceUsd(_contract) * ariaUsdExchangeRate; uint256 totalPrice = creditPrice * _quantity; require( acceptedToken.transferFrom(_msgSender(), address(this), totalPrice), "Can't buy credit, or you don't have enough aria, or you haven't approve this contract" ); CreditBuy memory creditBuy = CreditBuy({price: creditPrice, quantity: _quantity, contractAddress: _contract}); creditBatches.push(creditBuy); creditHistory[_receiver][_contract].push(creditBatches.length-1); totalCredits[_receiver][_contract] = totalCredits[_receiver][_contract] + _quantity; emit CreditBought(_msgSender(), _receiver, _contract, _quantity); } /** * @notice Consume a credit and dispatch the all rewards. Should be called buy the contract that consume the credit. * @param _spender address of the credit owner * @param _creatorProvider address that will be rewarded for the creation interface * @param _consumerProvider address that will be rewarded for the consumer interface * @param _firstOwner address that will be rewarded has owner of the token */ function consumeCredit( address _spender, address _creatorProvider, address _consumerProvider, address _firstOwner ) public { uint256 batchIndex = _consumeCredit(_spender); uint256 price = creditBatches[batchIndex].price; _dispatchReward(price, _creatorProvider, _consumerProvider, _firstOwner); } /** * @notice Consume a credit and dispatch only creator rewards. Should be called buy the contract that consume the credit. * @param _spender address of the credit owner * @param _creatorProvider address that will be rewarded for the creation interface * @return batchIndex index of the credit batch. Will be needed to trigger rewardClaim */ function consumeCredit( address _spender, address _creatorProvider ) public returns (uint256){ uint256 batchIndex = _consumeCredit(_spender); uint256 price = creditBatches[batchIndex].price; _dispatchRewardCreation(price, _creatorProvider); return batchIndex; } /** * @notice Dispatch the consumer reward. Should be called buy the contract that consume the credit. * @param _batchIndex index of the credit batch * @param _consumerProvider address that will be rewarded for the consumer interface * @param _firstOwner address that will be rewarded has owner of the token */ function rewardClaim( uint256 _batchIndex, address _consumerProvider, address _firstOwner ) public { require(creditBatches[_batchIndex].contractAddress == _msgSender(), "Credit Manager: you are not the spender of this creditBatch"); uint256 price = creditBatches[_batchIndex].price; _dispatchRewardClaim(price, _consumerProvider, _firstOwner); } /** * @notice Consume a credit */ function _consumeCredit(address _spender) internal returns(uint256) { require(totalCredits[_spender][_msgSender()] > 0, "Credit Manager: No credit for this contract"); uint256 index = creditHistoryIndex[_spender][_msgSender()]; uint256 batchIndex = creditHistory[_spender][_msgSender()][index]; creditBatches[batchIndex].quantity = creditBatches[batchIndex].quantity - 1; if (creditBatches[batchIndex].quantity == 0) { creditHistoryIndex[_spender][_msgSender()] = creditHistoryIndex[_spender][_msgSender()] + 1; } totalCredits[_spender][_msgSender()] = totalCredits[_spender][_msgSender()] - 1; emit CreditConsumed(_spender, _msgSender()); return batchIndex; } /** * @notice Dispatch all the rewards */ function _dispatchReward( uint256 _reward, address _creatorProvider, address _consumerProvider, address _firstOwner ) internal { _dispatchRewardCreation(_reward, _creatorProvider); _dispatchRewardClaim(_reward, _consumerProvider, _firstOwner); } /** * @notice Dispatch creation rewards */ function _dispatchRewardCreation( uint256 _reward, address _creatorProvider ) internal{ acceptedToken.transfer(_creatorProvider, (_reward / 100) * dispatchPercent[1]); acceptedToken.transfer(infraAddress, (_reward / 100) * dispatchPercent[0]); acceptedToken.transfer(protocolAddress, (_reward / 100) * dispatchPercent[3]); } /** * @notice Dispatch consumer rewards */ function _dispatchRewardClaim( uint256 _reward, address _consumerProvider, address _firstOwner ) internal{ acceptedToken.transfer(_consumerProvider, (_reward / 100) * dispatchPercent[2]); acceptedToken.transfer(_firstOwner, (_reward / 100) * dispatchPercent[4]); } /** * @notice Change the dispatch percent * @param _percentInfra percent of the reward for the infrastructure * @param _percentCreatorProvider percent of the reward for the creator provider * @param _percentConsumerProvider percent of the reward for the consumer provider * @param _percentArianeeProject percent of the reward for the arianee project * @param _percentFirstOwner percent of the reward for the nft owner */ function setDispatchPercent( uint8 _percentInfra, uint8 _percentCreatorProvider, uint8 _percentConsumerProvider, uint8 _percentArianeeProject, uint8 _percentFirstOwner ) external onlyOwner { require( _percentInfra + _percentCreatorProvider + _percentConsumerProvider + _percentArianeeProject + _percentFirstOwner == 100 ); dispatchPercent[0] = _percentInfra; dispatchPercent[1] = _percentCreatorProvider; dispatchPercent[2] = _percentConsumerProvider; dispatchPercent[3] = _percentArianeeProject; dispatchPercent[4] = _percentFirstOwner; emit DispatchRewardChanged( _percentInfra, _percentCreatorProvider, _percentConsumerProvider, _percentArianeeProject, _percentFirstOwner ); } /** * @notice Change the exchange rate between ARIA and USD. * @param _ariaUsdExchangeRate exchange rate between ARIA and USD. (value of 0.01$ in aria) */ function setAriaUsdExchangeRate(uint256 _ariaUsdExchangeRate) public onlyExchangeModifier { ariaUsdExchangeRate = _ariaUsdExchangeRate; emit NewAriaUsdExchangeRate(ariaUsdExchangeRate); } /** * @notice Change the address allowed to change the exchange rate between ARIA and USD. * @param _exchangeModifier address allowed to change the exchange rate between ARIA and USD. */ function setExchangeModifier(address _exchangeModifier) public onlyOwner { exchangeModifier = _exchangeModifier; emit NewExchangeModifier(exchangeModifier); } /** * @notice Change the address that will be rewarded for the infrastructure. * @param _newInfraAddress address that will be rewarded for the infrastructure. */ function setInfraAddress(address _newInfraAddress) public onlyOwner { infraAddress = _newInfraAddress; emit NewInfraAddress(infraAddress); } /** * @notice Change the address that will be rewarded for the arianee project. * @param _newProtocolAddress address that will be rewarded for the arianee project. */ function setProtocolAddress(address _newProtocolAddress) public onlyOwner { protocolAddress = _newProtocolAddress; emit NewProtocolAddress(protocolAddress); } /** * @notice Change the price of a credit in USD for a specific contract. * @param _contract address of the contract to change the credit price for. * @param _creditPriceUsd price of a credit in USD. */ function setCreditPriceUsd(address _contract, uint256 _creditPriceUsd) public onlyOwner { require(_creditPriceUsd > 0, "Credit price should be higher than 0"); creditPriceUSD[_contract] = _creditPriceUsd; emit NewCreditPriceUsd(_contract, _creditPriceUsd); } /** * @notice Get the price of a credit in USD for a specific contract. * @param _contract address of the contract to get the credit price for. */ function getCreditPriceUsd(address _contract) public view returns (uint256) { if (creditPriceUSD[_contract] > 0) { return creditPriceUSD[_contract]; } return 10; } /** * @notice Get the total credits owned for a specific contract. * @param _creditOwner address of the credit owner. * @param _contract address of the contract to get the total credits for. */ function balanceOf(address _creditOwner, address _contract) public view returns (uint256) { return totalCredits[_creditOwner][_contract]; } /** * @dev See {ERC2771ContextUpgradeable-_msgSender}. */ function _msgSender() internal view override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); } /** * @dev See {ERC2771ContextUpgradeable-_msgData}. */ function _msgData() internal view override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (bytes calldata ret) { return ERC2771ContextUpgradeable._msgData(); } }