diff --git a/contracts/BalanceTrackerFixedPrice.sol b/contracts/BalanceTrackerFixedPrice.sol new file mode 100644 index 0000000..2602a1b --- /dev/null +++ b/contracts/BalanceTrackerFixedPrice.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IMech} from "./interfaces/IMech.sol"; + +interface IMechMarketplace { + function fee() external returns(uint256); +} + +interface IToken { + /// @dev Transfers the token amount. + /// @param to Address to transfer to. + /// @param amount The amount to transfer. + /// @return True if the function execution is successful. + function transfer(address to, uint256 amount) external returns (bool); + + /// @dev Transfers the token amount that was previously approved up until the maximum allowance. + /// @param from Account address to transfer from. + /// @param to Account address to transfer to. + /// @param amount Amount to transfer to. + /// @return True if the function execution is successful. + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +interface IWrappedToken { + function deposit() external payable; +} + +/// @dev Only `manager` has a privilege, but the `sender` was provided. +/// @param sender Sender address. +/// @param manager Required sender address as a manager. +error ManagerOnly(address sender, address manager); + +/// @dev Provided zero address. +error ZeroAddress(); + +/// @dev Provided zero value. +error ZeroValue(); + +/// @dev Not enough balance to cover costs. +/// @param current Current balance. +/// @param required Required balance. +error InsufficientBalance(uint256 current, uint256 required); + +/// @dev Caught reentrancy violation. +error ReentrancyGuard(); + +/// @dev Account is unauthorized. +/// @param account Account address. +error UnauthorizedAccount(address account); + +/// @dev No incoming msg.value is allowed. +/// @param amount Value amount. +error NoDepositAllowed(uint256 amount); + +/// @dev Payload length is incorrect. +/// @param provided Provided payload length. +/// @param expected Expected payload length. +error InvalidPayloadLength(uint256 provided, uint256 expected); + +/// @dev Failure of a transfer. +/// @param token Address of a token. +/// @param from Address `from`. +/// @param to Address `to`. +/// @param amount Amount value. +error TransferFailed(address token, address from, address to, uint256 amount); + +contract BalanceTrackerFixedPrice { + event MechPaymentCalculated(address indexed mech, uint256 indexed requestId, uint256 deliveryRate, uint256 rateDiff); + event Deposit(address indexed account, address indexed token, uint256 amount); + event Withdraw(address indexed account, address indexed token, uint256 amount); + event Drained(address indexed token, uint256 collectedFees); + + // Mech marketplace address + address public immutable mechMarketplace; + // Wrapped native token address + address public immutable wrappedNativeToken; + // Buy back burner address + address public immutable buyBackBurner; + // Reentrancy lock + uint256 internal _locked = 1; + + // Map of requester => map of (token => current debit balance) + mapping(address => mapping(address => uint256)) public mapRequesterBalances; + // Map of mech => map of (token => current debit balance) + mapping(address => mapping(address => uint256)) public mapMechBalances; + // Map of token => collected fees + mapping(address => uint256) public mapCollectedFees; + // Map of requestId => token + mapping(uint256 => address) public mapRequestIdTokens; + + /// @dev BalanceTrackerFixedPrice constructor. + /// @param _mechMarketplace Mech marketplace address. + /// @param _wrappedNativeToken Wrapped native token address. + /// @param _buyBackBurner Buy back burner address. + constructor(address _mechMarketplace, address _wrappedNativeToken, address _buyBackBurner) { + // Check for zero address + if (_mechMarketplace == address(0) || _wrappedNativeToken == address(0) || _buyBackBurner == address(0)) { + revert ZeroAddress(); + } + + mechMarketplace = _mechMarketplace; + wrappedNativeToken = _wrappedNativeToken; + buyBackBurner = _buyBackBurner; + } + + function _wrap(uint256 amount) internal virtual { + IWrappedToken(wrappedNativeToken).deposit{value: amount}(); + } + + // Check and record delivery rate + function checkAndRecordDeliveryRate( + address mech, + address requester, + uint256 requestId, + bytes memory paymentData + ) external payable { + // Check for marketplace access + if (msg.sender != mechMarketplace) { + revert ManagerOnly(msg.sender, mechMarketplace); + } + + // Get mech max delivery rate + uint256 maxDeliveryRate = IMech(mech).maxDeliveryRate(); + + // Get payment token + address token; + if (paymentData.length == 32) { + // Extract token address + token = abi.decode(paymentData, (address)); + if (token != address(0) && msg.value > 0) { + revert NoDepositAllowed(msg.value); + } + } else if (paymentData.length > 0) { + revert InvalidPayloadLength(paymentData.length, 32); + } + + // Get account balance + uint256 balance = mapRequesterBalances[requester][token]; + + // Check the request delivery rate for a fixed price + if (balance < maxDeliveryRate) { + revert InsufficientBalance(balance, maxDeliveryRate); + } + + // Record request token + mapRequestIdTokens[requestId] = token; + + // Adjust account balance + balance -= maxDeliveryRate; + mapRequesterBalances[requester][token] = balance; + } + + /// @dev Finalizes mech delivery rate based on requested and actual ones. + /// @param mech Delivery mech address. + /// @param requester Requester address. + /// @param requestId Request Id. + /// @param maxDeliveryRate Requested max delivery rate. + function finalizeDeliveryRate(address mech, address requester, uint256 requestId, uint256 maxDeliveryRate) external { + // Check for marketplace access + if (msg.sender != mechMarketplace) { + revert ManagerOnly(msg.sender, mechMarketplace); + } + + // Get actual delivery rate + uint256 actualDeliveryRate = IMech(mech).getFinalizedDeliveryRate(requestId); + + // Check for zero value + if (actualDeliveryRate == 0) { + revert ZeroValue(); + } + + // Get token associated with request Id + address token = mapRequestIdTokens[requestId]; + + uint256 rateDiff; + if (maxDeliveryRate > actualDeliveryRate) { + // Return back requester overpayment debit + rateDiff = maxDeliveryRate - actualDeliveryRate; + mapRequesterBalances[requester][token] += rateDiff; + } else { + actualDeliveryRate = maxDeliveryRate; + } + + // Record payment into mech balance + mapMechBalances[mech][token] += actualDeliveryRate; + + emit MechPaymentCalculated(mech, requestId, actualDeliveryRate, rateDiff); + } + + // TODO buyBackBurner does not account for other tokens but WETH, OLAS + /// @dev Drains collected fees by sending them to a Buy back burner contract. + function drain(address token) external { + // Reentrancy guard + if (_locked > 1) { + revert ReentrancyGuard(); + } + _locked = 2; + + uint256 localCollectedFees = mapCollectedFees[token]; + + // TODO Limits + // Check for zero value + if (localCollectedFees == 0) { + revert ZeroValue(); + } + + mapCollectedFees[token] = 0; + + // Check token address + if (token == address (0)) { + // Wrap native tokens + _wrap(localCollectedFees); + // Transfer to Buy back burner + IToken(wrappedNativeToken).transfer(buyBackBurner, localCollectedFees); + } else { + IToken(token).transfer(buyBackBurner, localCollectedFees); + } + + emit Drained(token, localCollectedFees); + + _locked = 1; + } + + function _withdraw(address token, uint256 balance) internal { + bool success; + // Transfer mech balance + if (token == address(0)) { + // solhint-disable-next-line avoid-low-level-calls + (success, ) = msg.sender.call{value: balance}(""); + } else { + IToken(token).transfer(msg.sender, balance); + } + + // Check transfer + if (!success) { + revert TransferFailed(token, address(this), msg.sender, balance); + } + } + + /// @dev Processes mech payment by withdrawing funds. + function processPayment(address token) external returns (uint256 mechPayment, uint256 marketplaceFee) { + // Reentrancy guard + if (_locked > 1) { + revert ReentrancyGuard(); + } + _locked = 2; + + // Get mech balance + uint256 balance = mapMechBalances[msg.sender][token]; + // TODO limits? + if (balance == 0) { + revert ZeroValue(); + } + + // Calculate mech payment and marketplace fee + uint256 fee = IMechMarketplace(mechMarketplace).fee(); + marketplaceFee = (balance * fee) / 10_000; + mechPayment = balance - marketplaceFee; + + // Check for zero value, although this must never happen + if (mechPayment == 0) { + revert ZeroValue(); + } + + // Adjust marketplace fee + mapCollectedFees[token] += marketplaceFee; + + // Clear balances + mapMechBalances[msg.sender][token] = 0; + + // Process withdraw + _withdraw(token, balance); + + emit Withdraw(msg.sender, token, balance); + + _locked = 1; + } + + /// @dev Withdraws funds for a specific requester account. + function withdraw(address token) external { + // Reentrancy guard + if (_locked > 1) { + revert ReentrancyGuard(); + } + _locked = 2; + + // Get account balance + uint256 balance = mapRequesterBalances[msg.sender][token]; + // TODO limits? + if (balance == 0) { + revert ZeroValue(); + } + + // Clear balances + mapRequesterBalances[msg.sender][token] = 0; + + // Process withdraw + _withdraw(token, balance); + + emit Withdraw(msg.sender, token, balance); + + _locked = 1; + } + + // Deposits token funds for requester. + function deposit(address token, uint256 amount) external { + // TODO Accept deposits from mechs as well? + + if (token == address(0)) { + revert ZeroAddress(); + } + + // TODO: safe transfer? + IToken(token).transferFrom(msg.sender, address(0), amount); + + // Update account balances + mapRequesterBalances[msg.sender][address(0)] += amount; + + emit Deposit(msg.sender, token, amount); + } + + // Deposits native funds for requester. + receive() external payable { + // TODO Accept deposits from mechs as well? + + // Update account balances + mapRequesterBalances[msg.sender][address(0)] += msg.value; + + emit Deposit(msg.sender, address(0), msg.value); + } +} \ No newline at end of file diff --git a/contracts/EscrowBase.sol b/contracts/EscrowBase.sol deleted file mode 100644 index 735177c..0000000 --- a/contracts/EscrowBase.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -/// @dev Provided zero marketplace address. -error ZeroMarketplaceAddress(); - -/// @dev Only `manager` has a privilege, but the `sender` was provided. -/// @param sender Sender address. -/// @param manager Required sender address as a manager. -error ManagerOnly(address sender, address manager); - -abstract contract EscrowBase { - event Withdraw(address indexed mech, uint256 amount); - event Drained(uint256 collectedFees); - - // Mech marketplace address - address public immutable mechMarketplace; - - // Collected fees - uint256 public collectedFees; - // Reentrancy lock - uint256 internal _locked = 1; - - // Map of mech => its current balance - mapping(address => uint256) public mapMechBalances; - - constructor(address _mechMarketplace) { - // Check for zero address - if (_mechMarketplace == address(0)) { - revert ZeroMarketplaceAddress(); - } - - mechMarketplace = _mechMarketplace; - } - - // Check and escrow delivery rate - function checkAndEscrowDeliveryRate(address mech) external virtual payable; - - /// @dev Drains collected fees by sending them to a Buy back burner contract. - function drain() external virtual; - - function adjustBalances(address mech, uint256 mechPayment, uint256 marketplaceFee) external virtual { - if (msg.sender != mechMarketplace) { - revert ManagerOnly(msg.sender, mechMarketplace); - } - - // Record payment into mech balance - mapMechBalances[mech] += mechPayment; - - // Record collected fee - collectedFees += marketplaceFee; - } - - /// @dev Withdraws funds for a specific mech. - function withdraw() external virtual; -} \ No newline at end of file diff --git a/contracts/EscrowFixedPrice.sol b/contracts/EscrowFixedPrice.sol deleted file mode 100644 index b97301c..0000000 --- a/contracts/EscrowFixedPrice.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {EscrowBase} from "./EscrowBase.sol"; -import {IMech} from "./interfaces/IMech.sol"; - -interface IToken { - /// @dev Transfers the token amount. - /// @param to Address to transfer to. - /// @param amount The amount to transfer. - /// @return True if the function execution is successful. - function transfer(address to, uint256 amount) external returns (bool); -} - -interface IWrappedToken { - function deposit() external payable; -} - -/// @dev Provided zero address. -error ZeroAddress(); - -/// @dev Provided zero value. -error ZeroValue(); - -/// @dev Not enough balance to cover costs. -/// @param current Current balance. -/// @param required Required balance. -error InsufficientBalance(uint256 current, uint256 required); - -/// @dev Caught reentrancy violation. -error ReentrancyGuard(); - -/// @dev Account is unauthorized. -/// @param account Account address. -error UnauthorizedAccount(address account); - -/// @dev Failure of a transfer. -/// @param token Address of a token. -/// @param from Address `from`. -/// @param to Address `to`. -/// @param amount Amount value. -error TransferFailed(address token, address from, address to, uint256 amount); - -contract EscrowFixedPrice is EscrowBase { - // Wrapped native token address - address public immutable wrappedNativeToken; - // Buy back burner address - address public immutable buyBackBurner; - - /// @param _wrappedNativeToken Wrapped native token address. - /// @param _buyBackBurner Buy back burner address. - constructor(address _mechMarketplace, address _wrappedNativeToken, address _buyBackBurner) - EscrowBase(_mechMarketplace) - { - // Check for zero address - if (_wrappedNativeToken == address(0) || _buyBackBurner == address(0)) { - revert ZeroAddress(); - } - - wrappedNativeToken = _wrappedNativeToken; - buyBackBurner = _buyBackBurner; - } - - function _wrap(uint256 amount) internal virtual { - IWrappedToken(wrappedNativeToken).deposit{value: amount}(); - } - - // Check and escrow delivery rate - function checkAndEscrowDeliveryRate(address mech) external virtual override payable { - uint256 maxDeliveryRate = IMech(mech).maxDeliveryRate(); - - // Check the request delivery rate for a fixed price - if (msg.value < maxDeliveryRate) { - revert InsufficientBalance(msg.value, maxDeliveryRate); - } - } - - /// @dev Withdraws funds for a specific mech. - function withdraw() external virtual override { - // Reentrancy guard - if (_locked > 1) { - revert ReentrancyGuard(); - } - _locked = 2; - - // Get mech balance - uint256 balance = mapMechBalances[msg.sender]; - if (balance == 0) { - revert UnauthorizedAccount(msg.sender); - } - - // Transfer mech balance - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = msg.sender.call{value: balance}(""); - if (!success) { - revert TransferFailed(address(0), address(this), msg.sender, balance); - } - - emit Withdraw(msg.sender, balance); - - _locked = 1; - } - - /// @dev Drains collected fees by sending them to a Buy back burner contract. - function drain() external virtual override { - // Reentrancy guard - if (_locked > 1) { - revert ReentrancyGuard(); - } - _locked = 2; - - uint256 localCollectedFees = collectedFees; - - // Check for zero value - if (localCollectedFees == 0) { - revert ZeroValue(); - } - - collectedFees = 0; - - // Wrap native tokens - _wrap(localCollectedFees); - - // Transfer to Buy back burner - IToken(wrappedNativeToken).transfer(buyBackBurner, localCollectedFees); - - emit Drained(localCollectedFees); - - _locked = 1; - } -} \ No newline at end of file diff --git a/contracts/MechFixedPrice.sol b/contracts/MechFixedPrice.sol index fefe0c0..aba7809 100644 --- a/contracts/MechFixedPrice.sol +++ b/contracts/MechFixedPrice.sol @@ -11,7 +11,7 @@ contract MechFixedPrice is OlasMech { /// @param _serviceId Service Id. /// @param _maxDeliveryRate The maximum delivery rate. constructor(address _mechMarketplace, address _serviceRegistry, uint256 _serviceId, uint256 _maxDeliveryRate) - OlasMech(_mechMarketplace, _serviceRegistry, _serviceId, _maxDeliveryRate, MechType.FixedPrice) + OlasMech(_mechMarketplace, _serviceRegistry, _serviceId, _maxDeliveryRate, PaymentType.FixedPrice) {} /// @dev Performs actions before the delivery of a request. diff --git a/contracts/MechMarketplace.sol b/contracts/MechMarketplace.sol index 9851f68..3691eb9 100644 --- a/contracts/MechMarketplace.sol +++ b/contracts/MechMarketplace.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.28; import {IErrorsMarketplace} from "./interfaces/IErrorsMarketplace.sol"; -import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IBalanceTracker} from "./interfaces/IBalanceTracker.sol"; import {IKarma} from "./interfaces/IKarma.sol"; import {IMech} from "./interfaces/IMech.sol"; import {IServiceRegistry} from "./interfaces/IServiceRegistry.sol"; @@ -43,10 +43,10 @@ contract MechMarketplace is IErrorsMarketplace { event ImplementationUpdated(address indexed implementation); event MarketplaceParamsUpdated(uint256 fee, uint256 minResponseTimeout, uint256 maxResponseTimeout); event SetMechFactoryStatuses(address[] mechFactories, bool[] statuses); - event SetMechTypeEscrows(IMech.MechType[] mechTypes, address[] escrows); + event SetPaymentTypeBalanceTrackers(uint8[] paymentTypes, address[] balanceTrackers); event MarketplaceRequest(address indexed requester, address indexed requestedMech, uint256 requestId, bytes data); event MarketplaceDeliver(address indexed priorityMech, address indexed actualMech, address indexed requester, - uint256 requestId, bytes data, uint256 mechPayment, uint256 marketplaceFee); + uint256 requestId, bytes data); event DeliveryPaymentProcessed(uint256 indexed requestId, address indexed deliveryMech, uint256 deliveryRate, uint256 fee); enum RequestStatus { @@ -104,8 +104,8 @@ contract MechMarketplace is IErrorsMarketplace { mapping(address => bool) public mapMechFactories; // Map of mech => its creating factory mapping(address => address) public mapAgentMechFactories; - // Map of mech type => escrow address - mapping(IMech.MechType => address) public mapMechTypeEscrows; + // Map of mech type => balanceTracker address + mapping(uint8 => address) public mapPaymentTypeBalanceTrackers; // Mapping of account nonces mapping(address => uint256) public mapNonces; // Set of mechs created by this marketplace @@ -150,47 +150,6 @@ contract MechMarketplace is IErrorsMarketplace { ); } - /// @dev Calculates payment and fee based on delivery rates and mech type. - /// @param mech Delivery mech address. - /// @param requestId Request Id. - /// @param deliveryRate Delivery rate. - /// @param mechPayment Mech payment. - /// @param marketplaceFee Marketplace fee. - function _calculatePayment( - address mech, - uint256 requestId, - uint256 deliveryRate - ) internal virtual returns (uint256 mechPayment, uint256 marketplaceFee) { - // Get actual delivery rate - uint256 actualDeliveryRate = IMech(mech).getFinalizedDeliveryRate(requestId); - - // Check for zero value - if (actualDeliveryRate == 0) { - revert ZeroValue(); - } - - // TODO What to do if there is rateDIff leftovers? Keep as a fee or record into sender's map? - uint256 rateDiff; - if (actualDeliveryRate > deliveryRate) { - rateDiff = actualDeliveryRate - deliveryRate; - } - - // TODO what if fee is zero just because the delivery rate is in the order of 1..10_000? - // Calculate mech payment and marketplace fee - marketplaceFee = (actualDeliveryRate * fee) / 10_000; - mechPayment = actualDeliveryRate - marketplaceFee; - - // Check for zero value, although this must never happen - if (mechPayment == 0) { - revert ZeroValue(); - } - - // Record mech payment and marketplace fee into corresponding escrow balances - IMech.MechType mechType = IMech(mech).mechType(); - address escrow = mapMechTypeEscrows[mechType]; - IEscrow(escrow).adjustBalances(mech, mechPayment, marketplaceFee); - } - /// @dev Changes marketplace params. /// @param newFee New marketplace fee. /// @param newMinResponseTimeout New min response time in sec. @@ -347,30 +306,30 @@ contract MechMarketplace is IErrorsMarketplace { emit SetMechFactoryStatuses(mechFactories, statuses); } - /// @dev Sets mech type escrows. - /// @param mechTypes Mech types. - /// @param escrows Corresponding escrow addresses. - function setMechTypeEscrows(IMech.MechType[] memory mechTypes, address[] memory escrows) external { + /// @dev Sets mech payment type balanceTrackers. + /// @param paymentTypes Mech types. + /// @param balanceTrackers Corresponding balanceTracker addresses. + function setPaymentTypeBalanceTrackers(uint8[] memory paymentTypes, address[] memory balanceTrackers) external { // Check for the ownership if (msg.sender != owner) { revert OwnerOnly(msg.sender, owner); } - if (mechTypes.length != escrows.length) { - revert WrongArrayLength(mechTypes.length, escrows.length); + if (paymentTypes.length != balanceTrackers.length) { + revert WrongArrayLength(paymentTypes.length, balanceTrackers.length); } - // Traverse all the mech types and escrows - for (uint256 i = 0; i < mechTypes.length; ++i) { + // Traverse all the mech types and balanceTrackers + for (uint256 i = 0; i < paymentTypes.length; ++i) { // Check for zero address - if (escrows[i] == address(0)) { + if (balanceTrackers[i] == address(0)) { revert ZeroAddress(); } - mapMechTypeEscrows[mechTypes[i]] = escrows[i]; + mapPaymentTypeBalanceTrackers[paymentTypes[i]] = balanceTrackers[i]; } - emit SetMechTypeEscrows(mechTypes, escrows); + emit SetPaymentTypeBalanceTrackers(paymentTypes, balanceTrackers); } // TODO: leave optional fields or remove? @@ -383,6 +342,7 @@ contract MechMarketplace is IErrorsMarketplace { /// @param requesterStakingInstance Staking instance of a service whose multisig posts a request (optional). /// @param requesterServiceId Corresponding service Id in the staking contract (optional). /// @param responseTimeout Relative response time in sec. + /// @param paymentData Payment-related data, if applicable. /// @return requestId Request Id. function request( bytes memory data, @@ -391,7 +351,8 @@ contract MechMarketplace is IErrorsMarketplace { uint256 priorityMechServiceId, address requesterStakingInstance, uint256 requesterServiceId, - uint256 responseTimeout + uint256 responseTimeout, + bytes memory paymentData ) external payable returns (uint256 requestId) { // Reentrancy guard if (_locked > 1) { @@ -409,6 +370,7 @@ contract MechMarketplace is IErrorsMarketplace { revert UnauthorizedAccount(priorityMechStakingInstance); } + // TODO Shall we allow other mechs to post requests to mechs? Or completely prohibit mechs to post requests? // Check that msg.sender is not a mech if (msg.sender == priorityMech) { revert UnauthorizedAccount(msg.sender); @@ -434,15 +396,18 @@ contract MechMarketplace is IErrorsMarketplace { // Check requester checkRequester(msg.sender, requesterStakingInstance, requesterServiceId); - // Check and escrow delivery rate - IMech.MechType mechType = IMech(priorityMech).mechType(); - address escrow = mapMechTypeEscrows[mechType]; - IEscrow(escrow).checkAndEscrowDeliveryRate{value: msg.value}(priorityMech); - // Get the request Id requestId = getRequestId(msg.sender, data, mapNonces[msg.sender]); - // Update sender's nonce + // Get balance tracker address + uint8 mechPaymentType = IMech(priorityMech).getPaymentType(); + address balanceTracker = mapPaymentTypeBalanceTrackers[mechPaymentType]; + + // Check and record mech delivery rate + IBalanceTracker(balanceTracker).checkAndRecordDeliveryRate{value: msg.value}(priorityMech, msg.sender, + requestId, paymentData); + + // Update requester nonce mapNonces[msg.sender]++; // Get mech delivery info struct @@ -545,10 +510,15 @@ contract MechMarketplace is IErrorsMarketplace { // Increase mech karma that delivers the request IKarma(karma).changeMechKarma(msg.sender, 1); + // Get balance tracker address + uint8 mechPaymentType = IMech(priorityMech).getPaymentType(); + address balanceTracker = mapPaymentTypeBalanceTrackers[mechPaymentType]; + // Process payment - (uint256 mechPayment, uint256 marketplaceFee) = _calculatePayment(msg.sender, requestId, mechDelivery.deliveryRate); + IBalanceTracker(balanceTracker).finalizeDeliveryRate(msg.sender, mechDelivery.requester, requestId, + mechDelivery.deliveryRate); - emit MarketplaceDeliver(priorityMech, msg.sender, requester, requestId, requestData, mechPayment, marketplaceFee); + emit MarketplaceDeliver(priorityMech, msg.sender, requester, requestId, requestData); _locked = 1; } diff --git a/contracts/OlasMech.sol b/contracts/OlasMech.sol index 9f8ca92..25ec5d3 100644 --- a/contracts/OlasMech.sol +++ b/contracts/OlasMech.sol @@ -14,7 +14,7 @@ abstract contract OlasMech is Mech, IErrorsMech, ImmutableStorage { event Request(address indexed sender, uint256 requestId, bytes data); event RevokeRequest(address indexed sender, uint256 requestId); - enum MechType { + enum PaymentType { FixedPrice, Subscription } @@ -36,8 +36,8 @@ abstract contract OlasMech is Mech, IErrorsMech, ImmutableStorage { uint256 public immutable chainId; // Mech marketplace address address public immutable mechMarketplace; - // Mech type - MechType public immutable mechType; + // Mech payment type + PaymentType public immutable paymentType; // Maximum required delivery rate uint256 public maxDeliveryRate; @@ -71,7 +71,7 @@ abstract contract OlasMech is Mech, IErrorsMech, ImmutableStorage { address _serviceRegistry, uint256 _serviceId, uint256 _maxDeliveryRate, - MechType _mechType + PaymentType _paymentType ) { // Check for zero address if (_serviceRegistry == address(0)) { @@ -100,7 +100,7 @@ abstract contract OlasMech is Mech, IErrorsMech, ImmutableStorage { mechMarketplace = _mechMarketplace; maxDeliveryRate = _maxDeliveryRate; - mechType = _mechType; + paymentType = _paymentType; // Record chain Id @@ -443,6 +443,11 @@ abstract contract OlasMech is Mech, IErrorsMech, ImmutableStorage { } } + + function getPaymentType() external view returns (uint8) { + return uint8(paymentType); + } + /// @dev Gets finalized delivery rate for a request Id. /// @param requestId Request Id. /// @return Finalized delivery rate. diff --git a/contracts/integrations/nevermined/BalanceTrackerSubscription.sol b/contracts/integrations/nevermined/BalanceTrackerSubscription.sol new file mode 100644 index 0000000..61e4c09 --- /dev/null +++ b/contracts/integrations/nevermined/BalanceTrackerSubscription.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ERC1155TokenReceiver} from "../../../lib/autonolas-registries/lib/solmate/src/tokens/ERC1155.sol"; +import {IMech} from "../../interfaces/IMech.sol"; + +interface IERC1155 { + /// @dev Gets the amount of tokens owned by a specified account. + /// @param account Account address. + /// @param tokenId Token Id. + /// @return Amount of tokens owned. + function balanceOf(address account, uint256 tokenId) external view returns (uint256); + + /// @dev Burns a specified amount of account's tokens. + /// @param account Account address. + /// @param tokenId Token Id. + /// @param amount Amount of tokens. + function burn(address account, uint256 tokenId, uint256 amount) external; +} + +/// @dev Only `manager` has a privilege, but the `sender` was provided. +/// @param sender Sender address. +/// @param manager Required sender address as a manager. +error ManagerOnly(address sender, address manager); + +/// @dev Provided zero address. +error ZeroAddress(); + +/// @dev Provided zero value. +error ZeroValue(); + +/// @dev Not enough balance to cover costs. +/// @param current Current balance. +/// @param required Required balance. +error InsufficientBalance(uint256 current, uint256 required); + +/// @dev Value overflow. +/// @param provided Overflow value. +/// @param max Maximum possible value. +error Overflow(uint256 provided, uint256 max); + +/// @dev Caught reentrancy violation. +error ReentrancyGuard(); + +/// @dev Account is unauthorized. +/// @param account Account address. +error UnauthorizedAccount(address account); + +/// @dev No incoming msg.value is allowed. +/// @param amount Value amount. +error NoDepositAllowed(uint256 amount); + +contract BalanceTrackerSubscription is ERC1155TokenReceiver { + event MechPaymentCalculated(address indexed mech, uint256 indexed requestId, uint256 deliveryRate, uint256 rateDiff); + event CreditsAccounted(address indexed account, uint256 amount); + + // Mech marketplace address + address public immutable mechMarketplace; + // Subscription NFT + address public immutable subscriptionNFT; + // Subscription token Id + uint256 public immutable subscriptionTokenId; + + // Reentrancy lock + uint256 internal _locked = 1; + + // Map of requester => current credit balance + mapping(address => uint256) public mapRequesterBalances; + // Map of mech => current debit balance + mapping(address => uint256) public mapMechBalances; + + /// @dev BalanceTrackerSubscription constructor. + /// @param _mechMarketplace Mech marketplace address. + /// @param _subscriptionNFT Subscription NFT address. + /// @param _subscriptionTokenId Subscription token Id. + constructor(address _mechMarketplace, address _subscriptionNFT, uint256 _subscriptionTokenId) { + if (_subscriptionNFT == address(0)) { + revert ZeroAddress(); + } + + if (_subscriptionTokenId == 0) { + revert ZeroValue(); + } + + mechMarketplace = _mechMarketplace; + subscriptionNFT = _subscriptionNFT; + subscriptionTokenId = _subscriptionTokenId; + } + + // Check and record delivery rate + function checkAndRecordDeliveryRate( + address mech, + address requester, + uint256, + bytes memory + ) external payable { + // Check for marketplace access + if (msg.sender != mechMarketplace) { + revert ManagerOnly(msg.sender, mechMarketplace); + } + + // Get mech max delivery rate + uint256 maxDeliveryRate = IMech(mech).maxDeliveryRate(); + + // Check that there is no incoming deposit + if (msg.value > 0) { + revert NoDepositAllowed(msg.value); + } + + // Get requester credit balance + uint256 balance = mapRequesterBalances[requester]; + // Get requester actual subscription balance + uint256 subscriptionBalance = IERC1155(subscriptionNFT).balanceOf(requester, subscriptionTokenId); + + // Adjust requester balance with maxDeliveryRate credits + balance += maxDeliveryRate; + + // Check the request delivery rate for a fixed price + if (subscriptionBalance < balance) { + revert InsufficientBalance(subscriptionBalance, balance); + } + + // Adjust requester balance + mapRequesterBalances[requester] = balance; + } + + /// @dev Finalizes mech delivery rate based on requested and actual ones. + /// @param mech Delivery mech address. + /// @param requester Requester address. + /// @param requestId Request Id. + /// @param maxDeliveryRate Requested max delivery rate. + function finalizeDeliveryRate(address mech, address requester, uint256 requestId, uint256 maxDeliveryRate) external { + // Reentrancy guard + if (_locked > 1) { + revert ReentrancyGuard(); + } + _locked = 2; + + // Check for marketplace access + if (msg.sender != mechMarketplace) { + revert ManagerOnly(msg.sender, mechMarketplace); + } + + // Get actual delivery rate + uint256 actualDeliveryRate = IMech(mech).getFinalizedDeliveryRate(requestId); + + // Check for zero value + if (actualDeliveryRate == 0) { + revert ZeroValue(); + } + + uint256 rateDiff; + if (maxDeliveryRate > actualDeliveryRate) { + // Return back requester overpayment credit + rateDiff = maxDeliveryRate - actualDeliveryRate; + + // Get requester balance + uint256 balance = mapRequesterBalances[requester]; + + // This must never happen as max delivery rate is always bigger or equal to the actual delivery rate + if (rateDiff > balance) { + revert Overflow(rateDiff, balance); + } + + // Adjust requester balance + balance -= rateDiff; + mapRequesterBalances[requester] = balance; + } else { + actualDeliveryRate = maxDeliveryRate; + } + + // Record payment into mech balance + mapMechBalances[mech] += actualDeliveryRate; + + emit MechPaymentCalculated(mech, requestId, actualDeliveryRate, rateDiff); + + _locked = 1; + } + + /// @dev Processes payment. + function processPayment(address requester) external { + // Reentrancy guard + if (_locked > 1) { + revert ReentrancyGuard(); + } + _locked = 2; + + // Get requester credit balance + uint256 balance = mapRequesterBalances[requester]; + // Get requester actual subscription balance + uint256 subscriptionBalance = IERC1155(subscriptionNFT).balanceOf(requester, subscriptionTokenId); + + // This must never happen + if (subscriptionBalance < balance) { + revert InsufficientBalance(subscriptionBalance, balance); + } + + // Get credits to burn + uint256 creditsToBurn = subscriptionBalance - balance; + + // TODO limits and correct balance value + if (creditsToBurn == 0) { + revert InsufficientBalance(0, 0); + } + + // Clear balances + mapRequesterBalances[requester] = 0; + + // Burn credits of the request Id sender upon delivery + IERC1155(subscriptionNFT).burn(requester, subscriptionTokenId, creditsToBurn); + + emit CreditsAccounted(requester, creditsToBurn); + + _locked = 1; + } +} \ No newline at end of file diff --git a/contracts/integrations/nevermined/EscrowSubscription.sol b/contracts/integrations/nevermined/EscrowSubscription.sol deleted file mode 100644 index 66e2dfd..0000000 --- a/contracts/integrations/nevermined/EscrowSubscription.sol +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {ERC1155TokenReceiver} from "../../../lib/autonolas-registries/lib/solmate/src/tokens/ERC1155.sol"; -import {EscrowBase} from "../../EscrowBase.sol"; -import {IMech} from "../../interfaces/IMech.sol"; - -interface IERC1155 { - /// @dev Gets the amount of tokens owned by a specified account. - /// @param account Account address. - /// @param tokenId Token Id. - /// @return Amount of tokens owned. - function balanceOf(address account, uint256 tokenId) external view returns (uint256); - - /// @dev Burns a specified amount of account's tokens. - /// @param account Account address. - /// @param tokenId Token Id. - /// @param amount Amount of tokens. - function burn(address account, uint256 tokenId, uint256 amount) external; - - function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; -} - -/// @dev Only `owner` has a privilege, but the `sender` was provided. -/// @param sender Sender address. -/// @param owner Required sender address as an owner. -error OwnerOnly(address sender, address owner); - -/// @dev Provided zero address. -error ZeroAddress(); - -/// @dev Provided zero value. -error ZeroValue(); - -/// @dev Caught reentrancy violation. -error ReentrancyGuard(); - -/// @dev Account is unauthorized. -/// @param account Account address. -error UnauthorizedAccount(address account); - -/// @dev No incoming msg.value is allowed. -/// @param amount Value amount. -error NoDepositAllowed(uint256 amount); - -contract EscrowSubscription is EscrowBase, ERC1155TokenReceiver { - event SubscriptionUpdated(address indexed subscriptionNFT, uint256 subscriptionTokenId); - - // Owner address - address public owner; - // Subscription NFT - address public subscriptionNFT; - // Subscription token Id - uint256 public subscriptionTokenId; - - constructor(address _mechMarketplace, address _subscriptionNFT, uint256 _subscriptionTokenId) - EscrowBase(_mechMarketplace) - { - if (_subscriptionNFT == address(0)) { - revert ZeroAddress(); - } - - if (_subscriptionTokenId == 0) { - revert ZeroValue(); - } - - owner = msg.sender; - subscriptionNFT = _subscriptionNFT; - subscriptionTokenId = _subscriptionTokenId; - } - - // Check and escrow delivery rate - function checkAndEscrowDeliveryRate(address mech) external virtual override payable { - uint256 maxDeliveryRate = IMech(mech).maxDeliveryRate(); - - // Check that there is no incoming deposit - if (msg.value > 0) { - revert NoDepositAllowed(msg.value); - } - - // TODO Probably just check the amount of credits on a subscription for a msg.sender, as it's going to be managed by Nevermined - // Get max subscription rate for escrow from sender - IERC1155(subscriptionNFT).safeTransferFrom(msg.sender, address(this), subscriptionTokenId, maxDeliveryRate, ""); - } - - // TODO TBD - /// @dev Drains collected fees by sending them to a Buy back burner contract. - function drain() external virtual override {} - - /// @dev Withdraws funds for a specific mech. - function withdraw() external virtual override { - // Reentrancy guard - if (_locked > 1) { - revert ReentrancyGuard(); - } - _locked = 2; - - // Get mech balance - uint256 balance = mapMechBalances[msg.sender]; - if (balance == 0) { - revert UnauthorizedAccount(msg.sender); - } - - // Transfer mech balance - IERC1155(subscriptionNFT).safeTransferFrom(address(this), msg.sender, subscriptionTokenId, balance, ""); - - emit Withdraw(msg.sender, balance); - - _locked = 1; - } - - /// @dev Sets a new subscription. - /// @param newSubscriptionNFT New address of the NFT subscription. - /// @param newSubscriptionTokenId New subscription Id. - function setSubscription( - address newSubscriptionNFT, - uint256 newSubscriptionTokenId - ) external { - if (msg.sender != owner) { - revert OwnerOnly(msg.sender, owner); - } - - // Check for the subscription address - if (newSubscriptionNFT == address(0)) { - revert ZeroAddress(); - } - - // Check for the subscription token Id - if (newSubscriptionTokenId == 0) { - revert ZeroValue(); - } - - subscriptionNFT = newSubscriptionNFT; - subscriptionTokenId = newSubscriptionTokenId; - - emit SubscriptionUpdated(subscriptionNFT, subscriptionTokenId); - } -} \ No newline at end of file diff --git a/contracts/integrations/nevermined/MechNeverminedSubscription.sol b/contracts/integrations/nevermined/MechNeverminedSubscription.sol index 85a18fb..793d8b7 100644 --- a/contracts/integrations/nevermined/MechNeverminedSubscription.sol +++ b/contracts/integrations/nevermined/MechNeverminedSubscription.sol @@ -26,7 +26,7 @@ error ZeroValue(); /// @title AgentMechSubscription - Smart contract for extending AgentMech with subscription /// @dev A Mech that is operated by the holder of an ERC721 non-fungible token via a subscription. contract MechNeverminedSubscription is OlasMech { - event DeliveryRateFinalized(uint256 indexed requestId, uint256 deliveryRate); + event RequestRateFinalized(uint256 indexed requestId, uint256 deliveryRate); // Mapping for requestId => finalized delivery rates mapping(uint256 => uint256) public mapRequestIdFinalizedRates; @@ -42,50 +42,25 @@ contract MechNeverminedSubscription is OlasMech { uint256 _serviceId, uint256 _maxDeliveryRate ) - OlasMech(_mechMarketplace, _serviceRegistry, _serviceId, _maxDeliveryRate, MechType.Subscription) + OlasMech(_mechMarketplace, _serviceRegistry, _serviceId, _maxDeliveryRate, PaymentType.Subscription) {} /// @dev Performs actions before the delivery of a request. - /// @param account Request sender address. /// @param requestId Request Id. /// @param data Self-descriptive opaque data-blob. /// @return requestData Data for the request processing. function _preDeliver( - address account, + address, uint256 requestId, bytes memory data ) internal override returns (bytes memory requestData) { - // Reentrancy guard - if (_locked > 1) { - revert ReentrancyGuard(); - } - _locked = 2; - // Extract the request deliver rate as credits to burn - uint256 creditsToBurn; - (creditsToBurn, requestData) = abi.decode(data, (uint256, bytes)); - - mapRequestIdFinalizedRates[requestId] = creditsToBurn; - - // TODO: mocks, get subscriptionNFT and subscriptionTokenId from marketplace - address subscriptionNFT = address(1); - uint256 subscriptionTokenId = 1; - // Check for the number of credits available in the subscription - uint256 creditsBalance = IERC1155(subscriptionNFT).balanceOf(account, subscriptionTokenId); - - // Adjust the amount of credits to burn if the deliver price is bigger than the amount of credits available - if (creditsToBurn > creditsBalance) { - creditsToBurn = creditsBalance; - } - - // Burn credits of the request Id sender upon delivery - if (creditsToBurn > 0) { - IERC1155(subscriptionNFT).burn(account, subscriptionTokenId, creditsToBurn); - } + uint256 deliveryRate; + (deliveryRate, requestData) = abi.decode(data, (uint256, bytes)); - emit DeliveryRateFinalized(requestId, creditsToBurn); + mapRequestIdFinalizedRates[requestId] = deliveryRate; - _locked = 1; + emit RequestRateFinalized(requestId, deliveryRate); } /// @dev Gets finalized delivery rate for a request Id. diff --git a/contracts/interfaces/IBalanceTracker.sol b/contracts/interfaces/IBalanceTracker.sol new file mode 100644 index 0000000..70d8422 --- /dev/null +++ b/contracts/interfaces/IBalanceTracker.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +/// @dev Escrow interface +interface IBalanceTracker { + // Check and record delivery rate + function checkAndRecordDeliveryRate(address mech, address requester, uint256 requestId, bytes memory paymentData) + external payable; + + /// @dev Finalizes mech delivery rate based on requested and actual ones. + /// @param mech Delivery mech address. + /// @param requester Requester address. + /// @param requestId Request Id. + /// @param deliveryRate Requested delivery rate. + function finalizeDeliveryRate(address mech, address requester, uint256 requestId, uint256 deliveryRate) external; +} \ No newline at end of file diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol deleted file mode 100644 index 7e3d0a5..0000000 --- a/contracts/interfaces/IEscrow.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -/// @dev Escrow interface -interface IEscrow { - // Check and escrow delivery rate - function checkAndEscrowDeliveryRate(address mech) external payable; - - function adjustBalances(address mech, uint256 mechPayment, uint256 marketplaceFee) external; -} \ No newline at end of file diff --git a/contracts/interfaces/IMech.sol b/contracts/interfaces/IMech.sol index d103dce..461f0d5 100644 --- a/contracts/interfaces/IMech.sol +++ b/contracts/interfaces/IMech.sol @@ -3,11 +3,6 @@ pragma solidity ^0.8.28; /// @dev Mech interface interface IMech { - enum MechType { - FixedPrice, - Subscription - } - /// @dev Checks if the signer is the mech operator. function isOperator(address signer) external view returns (bool); @@ -24,7 +19,7 @@ interface IMech { function maxDeliveryRate() external returns (uint256); - function mechType() external returns (MechType); + function getPaymentType() external returns (uint8); /// @dev Gets finalized delivery rate for a request Id. /// @param requestId Request Id.