diff --git a/FunctionSignatures.md b/FunctionSignatures.md index 3eb60034..557f95f5 100644 --- a/FunctionSignatures.md +++ b/FunctionSignatures.md @@ -169,7 +169,7 @@ | `requestOwnershipHandover` | `0x25692962` | | `sbType` | `0x745de344` | | `transferOwnership` | `0xf2fde38b` | -| `transmitterFees` | `0xefb4cdea` | +| `transmitterCredits` | `0xefb4cdea` | | `unblockAndAssignFees` | `0x3c5366a2` | | `unblockFees` | `0xc1867a4b` | | `watcherPrecompileConfig` | `0x8618a912` | diff --git a/contracts/base/AppGatewayBase.sol b/contracts/base/AppGatewayBase.sol index 0216e44d..4b7e9950 100644 --- a/contracts/base/AppGatewayBase.sol +++ b/contracts/base/AppGatewayBase.sol @@ -7,39 +7,64 @@ import "../interfaces/IForwarder.sol"; import "../interfaces/IMiddleware.sol"; import "../interfaces/IPromise.sol"; -import {FeesPlugin} from "../protocol/utils/FeesPlugin.sol"; import {InvalidPromise, FeesNotSet, AsyncModifierNotUsed} from "../protocol/utils/common/Errors.sol"; import {FAST} from "../protocol/utils/common/Constants.sol"; /// @title AppGatewayBase /// @notice Abstract contract for the app gateway /// @dev This contract contains helpers for contract deployment, overrides, hooks and request processing -abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin { +abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { OverrideParams public overrideParams; bool public isAsyncModifierSet; address public auctionManager; bytes32 public sbType; bytes public onCompleteData; + uint256 public maxFees; mapping(address => bool) public isValidPromise; mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses; mapping(bytes32 => bytes) public creationCodeWithArgs; + address public consumeFrom; + /// @notice Modifier to treat functions async - modifier async() { - if (fees.feePoolChain == 0) revert FeesNotSet(); + modifier async(bytes memory feesApprovalData_) { + _preAsync(feesApprovalData_); + _; + _postAsync(); + } + // todo: can't overload modifier with same name, can rename later + /// @notice Modifier to treat functions async with consume from address + modifier asyncWithConsume(address consumeFrom_) { + _preAsync(new bytes(0)); + consumeFrom = consumeFrom_; + _; + _postAsync(); + } + + function _postAsync() internal { + isAsyncModifierSet = false; + + deliveryHelper__().batch(maxFees, auctionManager, consumeFrom, onCompleteData); + _markValidPromises(); + onCompleteData = bytes(""); + } + + function _preAsync(bytes memory feesApprovalData_) internal { isAsyncModifierSet = true; _clearOverrides(); deliveryHelper__().clearQueue(); addressResolver__.clearPromises(); - _; + _handleFeesApproval(feesApprovalData_); + } - isAsyncModifierSet = false; - deliveryHelper__().batch(fees, auctionManager, onCompleteData); - _markValidPromises(); - onCompleteData = bytes(""); + function _handleFeesApproval(bytes memory feesApprovalData_) internal { + if (feesApprovalData_.length > 0) { + (consumeFrom, , ) = IFeesManager(addressResolver__.feesManager()) + .whitelistAppGatewayWithSignature(feesApprovalData_); + } else consumeFrom = address(this); } /// @notice Modifier to ensure only valid promises can call the function @@ -190,19 +215,19 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin /// @notice Sets multiple overrides in one call /// @param isReadCall_ The read call flag - /// @param fees_ The fees configuration + /// @param fees_ The maxFees configuration /// @param gasLimit_ The gas limit /// @param isParallelCall_ The sequential call flag function _setOverrides( Read isReadCall_, Parallel isParallelCall_, uint256 gasLimit_, - Fees memory fees_ + uint256 fees_ ) internal { overrideParams.isReadCall = isReadCall_; overrideParams.isParallelCall = isParallelCall_; overrideParams.gasLimit = gasLimit_; - fees = fees_; + maxFees = fees_; } function _clearOverrides() internal { @@ -214,7 +239,7 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin overrideParams.writeFinality = WriteFinality.LOW; } - /// @notice Sets isReadCall, fees and gasLimit overrides + /// @notice Sets isReadCall, maxFees and gasLimit overrides /// @param isReadCall_ The read call flag /// @param isParallelCall_ The sequential call flag /// @param gasLimit_ The gas limit @@ -276,10 +301,10 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin overrideParams.value = value_; } - /// @notice Sets fees overrides - /// @param fees_ The fees configuration - function _setOverrides(Fees memory fees_) internal { - fees = fees_; + /// @notice Sets maxFees overrides + /// @param fees_ The maxFees configuration + function _setMaxFees(uint256 fees_) internal { + maxFees = fees_; } function getOverrideParams() @@ -308,7 +333,7 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin deliveryHelper__().cancelRequest(requestCount_); } - /// @notice increases the transaction fees + /// @notice increases the transaction maxFees /// @param requestCount_ The async ID function _increaseFees(uint40 requestCount_, uint256 newMaxFees_) internal { deliveryHelper__().increaseFees(requestCount_, newMaxFees_); @@ -332,7 +357,7 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin amount_, receiver_, auctionManager, - fees + maxFees ); } diff --git a/contracts/interfaces/IAppGateway.sol b/contracts/interfaces/IAppGateway.sol index 8997710b..c8e9fd73 100644 --- a/contracts/interfaces/IAppGateway.sol +++ b/contracts/interfaces/IAppGateway.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {Fees, Read, Parallel, QueuePayloadParams, OverrideParams, CallType, WriteFinality, PayloadParams} from "../protocol/utils/common/Structs.sol"; +import {Read, Parallel, QueuePayloadParams, OverrideParams, CallType, WriteFinality, PayloadParams} from "../protocol/utils/common/Structs.sol"; /// @title IAppGateway /// @notice Interface for the app gateway diff --git a/contracts/interfaces/IAuctionManager.sol b/contracts/interfaces/IAuctionManager.sol index 45344b7a..a67f0200 100644 --- a/contracts/interfaces/IAuctionManager.sol +++ b/contracts/interfaces/IAuctionManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {Bid, Fees, RequestMetadata, RequestParams} from "../protocol/utils/common/Structs.sol"; +import {Bid, RequestMetadata, RequestParams} from "../protocol/utils/common/Structs.sol"; interface IAuctionManager { /// @notice Bids for an auction diff --git a/contracts/interfaces/IFeesManager.sol b/contracts/interfaces/IFeesManager.sol index 82f5fd8f..02701602 100644 --- a/contracts/interfaces/IFeesManager.sol +++ b/contracts/interfaces/IFeesManager.sol @@ -1,31 +1,66 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {Fees, Bid, QueuePayloadParams} from "../protocol/utils/common/Structs.sol"; +import {Bid, QueuePayloadParams, PayloadSubmitParams, AppGatewayWhitelistParams} from "../protocol/utils/common/Structs.sol"; interface IFeesManager { - function blockFees( + function blockCredits( + address consumeFrom_, + uint256 transmitterCredits_, + uint40 requestCount_ + ) external; + + function unblockCredits(uint40 requestCount_) external; + + function isUserCreditsEnough( + address consumeFrom_, address appGateway_, - Fees memory fees_, - Bid memory winningBid_, + uint256 amount_ + ) external view returns (bool); + + function unblockAndAssignCredits(uint40 requestCount_, address transmitter_) external; + + function assignWatcherPrecompileCreditsFromRequestCount( + uint256 fees_, uint40 requestCount_ ) external; - function unblockFees(uint40 requestCount_) external; + function assignWatcherPrecompileCreditsFromAddress( + uint256 fees_, + address consumeFrom_ + ) external; + + function whitelistAppGatewayWithSignature( + bytes memory feeApprovalData_ + ) external returns (address consumeFrom, address appGateway, bool isApproved); - function isFeesEnough(address appGateway_, Fees memory fees_) external view returns (bool); + function whitelistAppGateways(AppGatewayWhitelistParams[] calldata params_) external; - function unblockAndAssignFees( - uint40 requestCount_, + function getWithdrawTransmitterCreditsPayloadParams( address transmitter_, - address appGateway_ - ) external; + uint32 chainSlug_, + address token_, + address receiver_, + uint256 amount_ + ) external returns (PayloadSubmitParams[] memory); - function withdrawFees( - address appGateway_, + function getMaxCreditsAvailableForWithdraw( + address transmitter_ + ) external view returns (uint256); + + function withdrawCredits( + address originAppGatewayOrUser_, uint32 chainSlug_, address token_, uint256 amount_, address receiver_ ) external; + + function depositCredits( + address depositTo_, + uint32 chainSlug_, + address token_, + uint256 signatureNonce_, + bytes memory signature_ + ) external payable; } diff --git a/contracts/interfaces/IFeesPlug.sol b/contracts/interfaces/IFeesPlug.sol index 1f4357b8..46797d62 100644 --- a/contracts/interfaces/IFeesPlug.sol +++ b/contracts/interfaces/IFeesPlug.sol @@ -2,18 +2,11 @@ pragma solidity ^0.8.21; interface IFeesPlug { - function balanceOf(address token_) external view returns (uint256); + function depositToFee(address token_, address receiver_, uint256 amount_) external; - function feesRedeemed(bytes32 feesId_) external view returns (bool); + function depositToFeeAndNative(address token_, address receiver_, uint256 amount_) external; - function deposit(address token_, address appGateway_, uint256 amount_) external payable; + function depositToNative(address token_, address receiver_, uint256 amount_) external; - function distributeFee( - address feeToken_, - uint256 fee_, - address transmitter_, - bytes32 feesId_ - ) external; - - function withdrawFees(address token_, uint256 amount_, address receiver_) external; + function withdrawFees(address token_, address receiver_, uint256 amount_) external; } diff --git a/contracts/interfaces/IMiddleware.sol b/contracts/interfaces/IMiddleware.sol index 54b50a53..6bbd5a3d 100644 --- a/contracts/interfaces/IMiddleware.sol +++ b/contracts/interfaces/IMiddleware.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {PayloadSubmitParams, QueuePayloadParams, Bid, Fees, WriteFinality, CallType, Parallel, IsPlug, RequestMetadata} from "../protocol/utils/common/Structs.sol"; +import {PayloadSubmitParams, QueuePayloadParams, Bid, WriteFinality, BatchParams, CallType, Parallel, IsPlug, RequestMetadata} from "../protocol/utils/common/Structs.sol"; /// @title IMiddleware /// @notice Interface for the Middleware contract @@ -28,8 +28,9 @@ interface IMiddleware { /// @param onCompleteData_ The data to be passed to the onComplete callback /// @return requestCount The request id function batch( - Fees memory fees_, + uint256 fees_, address auctionManager_, + address consumeFrom_, bytes memory onCompleteData_ ) external returns (uint40 requestCount); @@ -46,7 +47,7 @@ interface IMiddleware { uint256 amount_, address receiver_, address auctionManager_, - Fees memory fees_ + uint256 fees_ ) external returns (uint40); /// @notice Cancels a request @@ -64,7 +65,7 @@ interface IMiddleware { function startRequestProcessing(uint40 requestCount_, Bid memory winningBid_) external; /// @notice Returns the fees for a request - function getFees(uint40 requestCount_) external view returns (Fees memory); + function getFees(uint40 requestCount_) external view returns (uint256); /// @notice Finishes a request by assigning fees and calling the onComplete callback /// @param requestCount_ The request id diff --git a/contracts/interfaces/IWatcherPrecompile.sol b/contracts/interfaces/IWatcherPrecompile.sol index 0def0da2..1265f677 100644 --- a/contracts/interfaces/IWatcherPrecompile.sol +++ b/contracts/interfaces/IWatcherPrecompile.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {DigestParams, ResolvedPromises, PayloadParams, TriggerParams, PayloadSubmitParams, RequestParams} from "../protocol/utils/common/Structs.sol"; +import {DigestParams, ResolvedPromises, PayloadParams, TriggerParams, PayloadSubmitParams, Bid, RequestParams, RequestMetadata} from "../protocol/utils/common/Structs.sol"; import {IWatcherPrecompileLimits} from "./IWatcherPrecompileLimits.sol"; import {IWatcherPrecompileConfig} from "./IWatcherPrecompileConfig.sol"; diff --git a/contracts/interfaces/IWatcherPrecompileLimits.sol b/contracts/interfaces/IWatcherPrecompileLimits.sol index c56ed635..02899a72 100644 --- a/contracts/interfaces/IWatcherPrecompileLimits.sol +++ b/contracts/interfaces/IWatcherPrecompileLimits.sol @@ -48,6 +48,18 @@ interface IWatcherPrecompileLimits { /// @param consumeLimit_ The amount of limit to consume function consumeLimit(address appGateway_, bytes32 limitType_, uint256 consumeLimit_) external; + function getTotalFeesRequired( + uint256 queryCount_, + uint256 finalizeCount_, + uint256 scheduleCount_, + uint256 callbackCount_ + ) external view returns (uint256); + + function queryFees() external view returns (uint256); + function finalizeFees() external view returns (uint256); + function timeoutFees() external view returns (uint256); + function callBackFees() external view returns (uint256); + /// @notice Emitted when limit parameters are updated event LimitParamsUpdated(UpdateLimitParams[] updates); diff --git a/contracts/protocol/payload-delivery/AuctionManager.sol b/contracts/protocol/payload-delivery/AuctionManager.sol index 422a3863..40f1d38d 100644 --- a/contracts/protocol/payload-delivery/AuctionManager.sol +++ b/contracts/protocol/payload-delivery/AuctionManager.sol @@ -95,11 +95,11 @@ contract AuctionManager is /// @notice Places a bid for an auction /// @param requestCount_ The ID of the auction - /// @param fee The bid amount + /// @param bidFees The bid amount /// @param transmitterSignature The signature of the transmitter function bid( uint40 requestCount_, - uint256 fee, + uint256 bidFees, bytes memory transmitterSignature, bytes memory extraData ) external { @@ -107,28 +107,24 @@ contract AuctionManager is // check if the transmitter is valid address transmitter = _recoverSigner( - keccak256(abi.encode(address(this), evmxSlug, requestCount_, fee, extraData)), + keccak256(abi.encode(address(this), evmxSlug, requestCount_, bidFees, extraData)), transmitterSignature ); if (!_hasRole(TRANSMITTER_ROLE, transmitter)) revert InvalidTransmitter(); - // get the request metadata - RequestMetadata memory requestMetadata = IMiddleware(addressResolver__.deliveryHelper()) - .getRequestMetadata(requestCount_); + uint256 transmitterCredits = getTransmitterMaxFeesAvailable(requestCount_); - // check if the bid is for this auction manager - if (requestMetadata.auctionManager != address(this)) revert InvalidBid(); - // check if the bid exceeds the max fees quoted by app gateway - if (fee > requestMetadata.fees.amount) revert BidExceedsMaxFees(); + // check if the bid exceeds the max fees quoted by app gateway subtracting the watcher fees + if (bidFees > transmitterCredits) revert BidExceedsMaxFees(); // check if the bid is lower than the existing bid if ( winningBids[requestCount_].transmitter != address(0) && - fee >= winningBids[requestCount_].fee + bidFees >= winningBids[requestCount_].fee ) revert LowerBidAlreadyExists(); // create a new bid - Bid memory newBid = Bid({fee: fee, transmitter: transmitter, extraData: extraData}); + Bid memory newBid = Bid({fee: bidFees, transmitter: transmitter, extraData: extraData}); // update the winning bid winningBids[requestCount_] = newBid; @@ -144,17 +140,25 @@ contract AuctionManager is _endAuction(requestCount_); } - // block the fees - IFeesManager(addressResolver__.feesManager()).blockFees( - requestMetadata.appGateway, - requestMetadata.fees, - newBid, - requestCount_ - ); - emit BidPlaced(requestCount_, newBid); } + function getTransmitterMaxFeesAvailable(uint40 requestCount_) public view returns (uint256) { + RequestMetadata memory requestMetadata = _getRequestMetadata(requestCount_); + + // check if the bid is for this auction manager + if (requestMetadata.auctionManager != address(this)) revert InvalidBid(); + + // get the total fees required for the watcher precompile ops + uint256 watcherFees = watcherPrecompileLimits().getTotalFeesRequired( + requestMetadata.queryCount, + requestMetadata.finalizeCount, + 0, + 0 + ); + return requestMetadata.maxFees - watcherFees; + } + /// @notice Ends an auction /// @param requestCount_ The ID of the auction function endAuction(uint40 requestCount_) external onlyWatcherPrecompile { @@ -168,6 +172,13 @@ contract AuctionManager is if (winningBid.transmitter == address(0)) revert InvalidTransmitter(); auctionClosed[requestCount_] = true; + RequestMetadata memory requestMetadata = _getRequestMetadata(requestCount_); + // block the fees + IFeesManager(addressResolver__.feesManager()).blockCredits( + requestMetadata.consumeFrom, + winningBid.fee, + requestCount_ + ); // set the timeout for the bid expiration // useful in case a transmitter did bid but did not execute payloads @@ -199,7 +210,7 @@ contract AuctionManager is auctionClosed[requestCount_] = false; reAuctionCount[requestCount_]++; - IFeesManager(addressResolver__.feesManager()).unblockFees(requestCount_); + IFeesManager(addressResolver__.feesManager()).unblockCredits(requestCount_); emit AuctionRestarted(requestCount_); } @@ -219,4 +230,10 @@ contract AuctionManager is // recovered signer is checked for the valid roles later signer = ECDSA.recover(digest, signature_); } + + function _getRequestMetadata( + uint40 requestCount_ + ) internal view returns (RequestMetadata memory) { + return IMiddleware(addressResolver__.deliveryHelper()).getRequestMetadata(requestCount_); + } } diff --git a/contracts/protocol/payload-delivery/FeesManager.sol b/contracts/protocol/payload-delivery/FeesManager.sol index 8cb9ff30..b2e5b75e 100644 --- a/contracts/protocol/payload-delivery/FeesManager.sol +++ b/contracts/protocol/payload-delivery/FeesManager.sol @@ -4,13 +4,11 @@ pragma solidity ^0.8.21; import {Ownable} from "solady/auth/Ownable.sol"; import "solady/utils/Initializable.sol"; import "solady/utils/ECDSA.sol"; - import {IFeesPlug} from "../../interfaces/IFeesPlug.sol"; -import {IFeesManager} from "../../interfaces/IFeesManager.sol"; - +import "../../interfaces/IFeesManager.sol"; import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; import {NotAuctionManager, InvalidWatcherSignature, NonceUsed} from "../utils/common/Errors.sol"; -import {Bid, Fees, CallType, Parallel, WriteFinality, TokenBalance, QueuePayloadParams, IsPlug, PayloadSubmitParams, RequestParams, RequestMetadata} from "../utils/common/Structs.sol"; +import {Bid, CallType, Parallel, WriteFinality, QueuePayloadParams, IsPlug, PayloadSubmitParams, RequestMetadata, UserCredits} from "../utils/common/Structs.sol"; abstract contract FeesManagerStorage is IFeesManager { // slots [0-49] reserved for gap @@ -25,21 +23,27 @@ abstract contract FeesManagerStorage is IFeesManager { // slot 52 bytes32 public sbType; - // slot 53 - /// @notice Master mapping tracking all fee information - /// @dev appGateway => chainSlug => token => TokenBalance - mapping(address => mapping(uint32 => mapping(address => TokenBalance))) - public appGatewayFeeBalances; + // user credits + mapping(address => UserCredits) public userCredits; + + // user nonce + mapping(address => uint256) public userNonce; + + // token pool balances + // chainSlug => token address => amount + mapping(uint32 => mapping(address => uint256)) public tokenPoolBalances; + + // user approved app gateways + // userAddress => appGateway => isWhitelisted + mapping(address => mapping(address => bool)) public isAppGatewayWhitelisted; // slot 54 - /// @notice Mapping to track blocked fees for each async id - /// @dev requestCount => Fees - mapping(uint40 => Fees) public requestCountBlockedFees; + /// @notice Mapping to track request credits details for each request count + /// @dev requestCount => RequestFee + mapping(uint40 => uint256) public requestCountCredits; - // slot 55 - /// @notice Mapping to track fees to be distributed to transmitters - /// @dev transmitter => chainSlug => token => amount - mapping(address => mapping(uint32 => mapping(address => uint256))) public transmitterFees; + // @dev amount + uint256 public watcherPrecompileCredits; // slot 56 /// @notice Mapping to track nonce to whether it has been used @@ -57,32 +61,26 @@ abstract contract FeesManagerStorage is IFeesManager { contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResolverUtil { /// @notice Emitted when fees are blocked for a batch /// @param requestCount The batch identifier - /// @param chainSlug The chain identifier - /// @param token The token address + /// @param consumeFrom The consume from address /// @param amount The blocked amount - event FeesBlocked( - uint40 indexed requestCount, - uint32 indexed chainSlug, - address indexed token, - uint256 amount - ); + event CreditsBlocked(uint40 indexed requestCount, address indexed consumeFrom, uint256 amount); /// @notice Emitted when transmitter fees are updated /// @param requestCount The batch identifier /// @param transmitter The transmitter address /// @param amount The new amount deposited - event TransmitterFeesUpdated( + event TransmitterCreditsUpdated( uint40 indexed requestCount, address indexed transmitter, uint256 amount ); - + event WatcherPrecompileCreditsAssigned(uint256 amount, address consumeFrom); /// @notice Emitted when fees deposited are updated /// @param chainSlug The chain identifier /// @param appGateway The app gateway address /// @param token The token address /// @param amount The new amount deposited - event FeesDepositedUpdated( + event CreditsDeposited( uint32 indexed chainSlug, address indexed appGateway, address indexed token, @@ -93,7 +91,7 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol /// @param requestCount The batch identifier /// @param transmitter The transmitter address /// @param amount The unblocked amount - event FeesUnblockedAndAssigned( + event CreditsUnblockedAndAssigned( uint40 indexed requestCount, address indexed transmitter, uint256 amount @@ -102,16 +100,36 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol /// @notice Emitted when fees are unblocked /// @param requestCount The batch identifier /// @param appGateway The app gateway address - event FeesUnblocked(uint40 indexed requestCount, address indexed appGateway); + event CreditsUnblocked(uint40 indexed requestCount, address indexed appGateway); + + /// @notice Emitted when insufficient watcher precompile fees are available + event InsufficientWatcherPrecompileCreditsAvailable( + uint32 chainSlug, + address token, + address consumeFrom + ); + + /// @notice Emitted when credits are wrapped + event CreditsWrapped(address indexed consumeFrom, uint256 amount); + + /// @notice Emitted when credits are unwrapped + event CreditsUnwrapped(address indexed consumeFrom, uint256 amount); /// @notice Error thrown when insufficient fees are available - error InsufficientFeesAvailable(); + error InsufficientCreditsAvailable(); /// @notice Error thrown when no fees are available for a transmitter error NoFeesForTransmitter(); /// @notice Error thrown when no fees was blocked - error NoFeesBlocked(); + error NoCreditsBlocked(); /// @notice Error thrown when caller is invalid error InvalidCaller(); + /// @notice Error thrown when user signature is invalid + error InvalidUserSignature(); + /// @notice Error thrown when app gateway is not whitelisted + error AppGatewayNotWhitelisted(); + + error InvalidAmount(); + error InsufficientBalance(); constructor() { _disableInitializers(); // disable for implementation @@ -134,137 +152,215 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol } /// @notice Returns available (unblocked) fees for a gateway - /// @param chainSlug_ The chain identifier - /// @param appGateway_ The app gateway address - /// @param token_ The token address + /// @param consumeFrom_ The app gateway address /// @return The available fee amount - function getAvailableFees( - uint32 chainSlug_, - address appGateway_, - address token_ - ) public view returns (uint256) { - TokenBalance memory tokenBalance = appGatewayFeeBalances[appGateway_][chainSlug_][token_]; - if (tokenBalance.deposited == 0 || tokenBalance.deposited <= tokenBalance.blocked) return 0; - return tokenBalance.deposited - tokenBalance.blocked; + function getAvailableCredits(address consumeFrom_) public view returns (uint256) { + UserCredits memory userCredit = userCredits[consumeFrom_]; + if (userCredit.totalCredits == 0 || userCredit.totalCredits <= userCredit.blockedCredits) + return 0; + return userCredit.totalCredits - userCredit.blockedCredits; } /// @notice Adds the fees deposited for an app gateway on a chain - /// @param chainSlug_ The chain identifier - /// @param originAppGateway_ The app gateway address - /// @param token_ The token address - /// @param amount_ The amount deposited - function incrementFeesDeposited( + /// @param depositTo_ The app gateway address + // @dev only callable by watcher precompile + // @dev will need tokenAmount_ and creditAmount_ when introduce tokens except stables + function depositCredits( + address depositTo_, uint32 chainSlug_, - address originAppGateway_, address token_, - uint256 amount_, uint256 signatureNonce_, bytes memory signature_ - ) external { - _isWatcherSignatureValid( - abi.encode(chainSlug_, originAppGateway_, token_, amount_), - signatureNonce_, - signature_ + ) external payable { + if (isNonceUsed[signatureNonce_]) revert NonceUsed(); + isNonceUsed[signatureNonce_] = true; + + uint256 amount = msg.value; + + // check signature + bytes32 digest = keccak256( + abi.encode( + depositTo_, + chainSlug_, + token_, + amount, + address(this), + evmxSlug, + signatureNonce_ + ) ); - address appGateway = _getCoreAppGateway(originAppGateway_); + if (_recoverSigner(digest, signature_) != owner()) revert InvalidWatcherSignature(); - TokenBalance storage tokenBalance = appGatewayFeeBalances[appGateway][chainSlug_][token_]; - tokenBalance.deposited += amount_; - emit FeesDepositedUpdated(chainSlug_, appGateway, token_, amount_); + UserCredits storage userCredit = userCredits[depositTo_]; + userCredit.totalCredits += amount; + tokenPoolBalances[chainSlug_][token_] += amount; + emit CreditsDeposited(chainSlug_, depositTo_, token_, amount); } - function isFeesEnough( - address originAppGateway_, - Fees memory fees_ + function wrap() external payable { + UserCredits storage userCredit = userCredits[msg.sender]; + userCredit.totalCredits += msg.value; + emit CreditsWrapped(msg.sender, msg.value); + } + + function unwrap(uint256 amount_) external { + UserCredits storage userCredit = userCredits[msg.sender]; + if (userCredit.totalCredits < amount_) revert InsufficientCreditsAvailable(); + userCredit.totalCredits -= amount_; + + // todo: if contract balance not enough, take from our pool? + if (address(this).balance < amount_) revert InsufficientBalance(); + payable(msg.sender).transfer(amount_); + emit CreditsUnwrapped(msg.sender, amount_); + } + + function isUserCreditsEnough( + address consumeFrom_, + address appGateway_, + uint256 amount_ ) external view returns (bool) { - address appGateway = _getCoreAppGateway(originAppGateway_); - uint256 availableFees = getAvailableFees( - fees_.feePoolChain, - appGateway, - fees_.feePoolToken + // If consumeFrom is not appGateway, check if it is whitelisted + if (consumeFrom_ != appGateway_ && !isAppGatewayWhitelisted[consumeFrom_][appGateway_]) + revert AppGatewayNotWhitelisted(); + return getAvailableCredits(consumeFrom_) >= amount_; + } + + function _processFeeApprovalData( + bytes memory feeApprovalData_ + ) internal returns (address, address, bool) { + (address consumeFrom, address appGateway, bool isApproved, bytes memory signature_) = abi + .decode(feeApprovalData_, (address, address, bool, bytes)); + if (signature_.length == 0) { + // If no signature, consumeFrom is appGateway + return (appGateway, appGateway, isApproved); + } + bytes32 digest = keccak256( + abi.encode( + address(this), + evmxSlug, + consumeFrom, + appGateway, + userNonce[consumeFrom], + isApproved + ) ); - return availableFees >= fees_.amount; + if (_recoverSigner(digest, signature_) != consumeFrom) revert InvalidUserSignature(); + isAppGatewayWhitelisted[consumeFrom][appGateway] = isApproved; + userNonce[consumeFrom]++; + + return (consumeFrom, appGateway, isApproved); + } + + function whitelistAppGatewayWithSignature( + bytes memory feeApprovalData_ + ) external returns (address consumeFrom, address appGateway, bool isApproved) { + return _processFeeApprovalData(feeApprovalData_); + } + + /// @notice Whitelists multiple app gateways for the caller + /// @param params_ Array of app gateway addresses to whitelist + function whitelistAppGateways(AppGatewayWhitelistParams[] calldata params_) external { + for (uint256 i = 0; i < params_.length; i++) { + isAppGatewayWhitelisted[msg.sender][params_[i].appGateway] = params_[i].isApproved; + } + } + + modifier onlyAuctionManager(uint40 requestCount_) { + if (msg.sender != deliveryHelper__().getRequestMetadata(requestCount_).auctionManager) + revert NotAuctionManager(); + _; } /// @notice Blocks fees for a request count - /// @param originAppGateway_ The app gateway address - /// @param feesGivenByApp_ The fees data struct given by the app gateway + /// @param consumeFrom_ The fees payer address + /// @param transmitterCredits_ The total fees to block /// @param requestCount_ The batch identifier /// @dev Only callable by delivery helper - function blockFees( - address originAppGateway_, - Fees memory feesGivenByApp_, - Bid memory winningBid_, + function blockCredits( + address consumeFrom_, + uint256 transmitterCredits_, uint40 requestCount_ - ) external { - if (msg.sender != deliveryHelper__().getRequestMetadata(requestCount_).auctionManager) - revert NotAuctionManager(); - - address appGateway = _getCoreAppGateway(originAppGateway_); + ) external onlyAuctionManager(requestCount_) { // Block fees - uint256 availableFees = getAvailableFees( - feesGivenByApp_.feePoolChain, - appGateway, - feesGivenByApp_.feePoolToken - ); + if (getAvailableCredits(consumeFrom_) < transmitterCredits_) + revert InsufficientCreditsAvailable(); - if (requestCountBlockedFees[requestCount_].amount > 0) - availableFees += requestCountBlockedFees[requestCount_].amount; + UserCredits storage userCredit = userCredits[consumeFrom_]; + userCredit.blockedCredits += transmitterCredits_; - if (availableFees < winningBid_.fee) revert InsufficientFeesAvailable(); - TokenBalance storage tokenBalance = appGatewayFeeBalances[appGateway][ - feesGivenByApp_.feePoolChain - ][feesGivenByApp_.feePoolToken]; + requestCountCredits[requestCount_] = transmitterCredits_; - tokenBalance.blocked = - tokenBalance.blocked + - winningBid_.fee - - requestCountBlockedFees[requestCount_].amount; - - requestCountBlockedFees[requestCount_] = Fees({ - feePoolChain: feesGivenByApp_.feePoolChain, - feePoolToken: feesGivenByApp_.feePoolToken, - amount: winningBid_.fee - }); - - emit FeesBlocked( - requestCount_, - feesGivenByApp_.feePoolChain, - feesGivenByApp_.feePoolToken, - winningBid_.fee - ); + emit CreditsBlocked(requestCount_, consumeFrom_, transmitterCredits_); } /// @notice Unblocks fees after successful execution and assigns them to the transmitter /// @param requestCount_ The async ID of the executed batch /// @param transmitter_ The address of the transmitter who executed the batch - function unblockAndAssignFees( + function unblockAndAssignCredits( uint40 requestCount_, - address transmitter_, - address originAppGateway_ + address transmitter_ ) external override onlyDeliveryHelper { - Fees memory fees = requestCountBlockedFees[requestCount_]; - if (fees.amount == 0) return; - - address appGateway = _getCoreAppGateway(originAppGateway_); - TokenBalance storage tokenBalance = appGatewayFeeBalances[appGateway][fees.feePoolChain][ - fees.feePoolToken - ]; + uint256 blockedCredits = requestCountCredits[requestCount_]; + if (blockedCredits == 0) return; + RequestMetadata memory requestMetadata = deliveryHelper__().getRequestMetadata( + requestCount_ + ); + uint256 fees = requestMetadata.winningBid.fee; // Unblock fees from deposit - tokenBalance.blocked -= fees.amount; - tokenBalance.deposited -= fees.amount; + _useBlockedUserCredits(requestMetadata.consumeFrom, blockedCredits, fees); // Assign fees to transmitter - transmitterFees[transmitter_][fees.feePoolChain][fees.feePoolToken] += fees.amount; + userCredits[transmitter_].totalCredits += fees; // Clean up storage - delete requestCountBlockedFees[requestCount_]; - emit FeesUnblockedAndAssigned(requestCount_, transmitter_, fees.amount); + delete requestCountCredits[requestCount_]; + emit CreditsUnblockedAndAssigned(requestCount_, transmitter_, fees); + } + + function _useBlockedUserCredits( + address consumeFrom_, + uint256 toConsumeFromBlocked_, + uint256 toConsumeFromTotal_ + ) internal { + UserCredits storage userCredit = userCredits[consumeFrom_]; + userCredit.blockedCredits -= toConsumeFromBlocked_; + userCredit.totalCredits -= toConsumeFromTotal_; } - function unblockFees(uint40 requestCount_) external { + function _useAvailableUserCredits(address consumeFrom_, uint256 toConsume_) internal { + UserCredits storage userCredit = userCredits[consumeFrom_]; + if (userCredit.totalCredits < toConsume_) revert InsufficientCreditsAvailable(); + userCredit.totalCredits -= toConsume_; + } + + function assignWatcherPrecompileCreditsFromRequestCount( + uint256 amount_, + uint40 requestCount_ + ) external onlyWatcherPrecompile { + RequestMetadata memory requestMetadata = deliveryHelper__().getRequestMetadata( + requestCount_ + ); + _assignWatcherPrecompileCredits(amount_, requestMetadata.consumeFrom); + } + + function assignWatcherPrecompileCreditsFromAddress( + uint256 amount_, + address consumeFrom_ + ) external onlyWatcherPrecompile { + _assignWatcherPrecompileCredits(amount_, consumeFrom_); + } + + function _assignWatcherPrecompileCredits(uint256 amount_, address consumeFrom_) internal { + // deduct the fees from the user + _useAvailableUserCredits(consumeFrom_, amount_); + // add the fees to the watcher precompile + watcherPrecompileCredits += amount_; + emit WatcherPrecompileCreditsAssigned(amount_, consumeFrom_); + } + + function unblockCredits(uint40 requestCount_) external { RequestMetadata memory requestMetadata = deliveryHelper__().getRequestMetadata( requestCount_ ); @@ -274,82 +370,64 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol msg.sender != address(deliveryHelper__()) ) revert InvalidCaller(); - Fees memory fees = requestCountBlockedFees[requestCount_]; - if (fees.amount == 0) return; - - TokenBalance storage tokenBalance = appGatewayFeeBalances[requestMetadata.appGateway][ - fees.feePoolChain - ][fees.feePoolToken]; + uint256 blockedCredits = requestCountCredits[requestCount_]; + if (blockedCredits == 0) return; // Unblock fees from deposit - tokenBalance.blocked -= fees.amount; - tokenBalance.deposited += fees.amount; - - delete requestCountBlockedFees[requestCount_]; - emit FeesUnblocked(requestCount_, requestMetadata.appGateway); - } - - /// @notice Withdraws fees to a specified receiver - /// @param chainSlug_ The chain identifier - /// @param token_ The token address - /// @param receiver_ The address of the receiver - function withdrawTransmitterFees( - uint32 chainSlug_, - address token_, - address receiver_ - ) external returns (uint40 requestCount) { - address transmitter = msg.sender; - // Get total fees for the transmitter in given chain and token - uint256 totalFees = transmitterFees[transmitter][chainSlug_][token_]; - if (totalFees == 0) revert NoFeesForTransmitter(); - - // Clean up storage - transmitterFees[transmitter][chainSlug_][token_] = 0; + UserCredits storage userCredit = userCredits[requestMetadata.consumeFrom]; + userCredit.blockedCredits -= blockedCredits; - // Create fee distribution payload - bytes32 feesId = _encodeFeesId(feesCounter++); - bytes memory payload = abi.encodeCall( - IFeesPlug.distributeFee, - (token_, totalFees, receiver_, feesId) - ); - - // finalize for plug contract - return _submitAndStartProcessing(chainSlug_, payload, transmitter); + delete requestCountCredits[requestCount_]; + emit CreditsUnblocked(requestCount_, requestMetadata.consumeFrom); } /// @notice Withdraws funds to a specified receiver /// @dev This function is used to withdraw fees from the fees plug - /// @param originAppGateway_ The address of the app gateway + /// @param originAppGatewayOrUser_ The address of the app gateway /// @param chainSlug_ The chain identifier /// @param token_ The address of the token /// @param amount_ The amount of tokens to withdraw /// @param receiver_ The address of the receiver - function withdrawFees( - address originAppGateway_, + function withdrawCredits( + address originAppGatewayOrUser_, uint32 chainSlug_, address token_, uint256 amount_, address receiver_ ) public { - if (msg.sender != address(deliveryHelper__())) originAppGateway_ = msg.sender; - address appGateway = _getCoreAppGateway(originAppGateway_); + if (msg.sender != address(deliveryHelper__())) originAppGatewayOrUser_ = msg.sender; + address source = _getCoreAppGateway(originAppGatewayOrUser_); // Check if amount is available in fees plug - uint256 availableAmount = getAvailableFees(chainSlug_, appGateway, token_); - if (availableAmount < amount_) revert InsufficientFeesAvailable(); + uint256 availableAmount = getAvailableCredits(source); + if (availableAmount < amount_) revert InsufficientCreditsAvailable(); - TokenBalance storage tokenBalance = appGatewayFeeBalances[appGateway][chainSlug_][token_]; - tokenBalance.deposited -= amount_; + _useAvailableUserCredits(source, amount_); + tokenPoolBalances[chainSlug_][token_] -= amount_; // Add it to the queue and submit request - _queue(chainSlug_, abi.encodeCall(IFeesPlug.withdrawFees, (token_, amount_, receiver_))); + _queue(chainSlug_, abi.encodeCall(IFeesPlug.withdrawFees, (token_, receiver_, amount_))); } - function _submitAndStartProcessing( + /// @notice Withdraws fees to a specified receiver + /// @param chainSlug_ The chain identifier + /// @param token_ The token address + /// @param receiver_ The address of the receiver + function getWithdrawTransmitterCreditsPayloadParams( + address transmitter_, uint32 chainSlug_, - bytes memory payload_, - address transmitter_ - ) internal returns (uint40 requestCount) { + address token_, + address receiver_, + uint256 amount_ + ) external onlyDeliveryHelper returns (PayloadSubmitParams[] memory) { + uint256 maxCreditsAvailableForWithdraw = getMaxCreditsAvailableForWithdraw(transmitter_); + if (amount_ > maxCreditsAvailableForWithdraw) revert InsufficientCreditsAvailable(); + + // Clean up storage + _useAvailableUserCredits(transmitter_, amount_); + tokenPoolBalances[chainSlug_][token_] -= amount_; + + bytes memory payload = abi.encodeCall(IFeesPlug.withdrawFees, (token_, receiver_, amount_)); PayloadSubmitParams[] memory payloadSubmitParamsArray = new PayloadSubmitParams[](1); payloadSubmitParamsArray[0] = PayloadSubmitParams({ levelNumber: 0, @@ -364,12 +442,15 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol gasLimit: 10000000, value: 0, readAt: 0, - payload: payload_ + payload: payload }); - requestCount = watcherPrecompile__().submitRequest(payloadSubmitParamsArray); + return payloadSubmitParamsArray; + } - // same transmitter can execute requests without auction - watcherPrecompile__().startProcessingRequest(requestCount, transmitter_); + function getMaxCreditsAvailableForWithdraw(address transmitter_) public view returns (uint256) { + uint256 watcherFees = watcherPrecompileLimits().getTotalFeesRequired(0, 1, 0, 1); + uint256 transmitterCredits = userCredits[transmitter_].totalCredits; + return transmitterCredits > watcherFees ? transmitterCredits - watcherFees : 0; } function _getSwitchboard(uint32 chainSlug_) internal view returns (address) { @@ -400,7 +481,7 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol } /// @notice hook called by watcher precompile when request is finished - function finishRequest(uint40) external {} + function onRequestComplete(uint40 requestCount_, bytes memory) external {} function _queue(uint32 chainSlug_, bytes memory payload_) internal { QueuePayloadParams memory queuePayloadParams = _createQueuePayloadParams( @@ -410,27 +491,16 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol deliveryHelper__().queue(queuePayloadParams); } - function _encodeFeesId(uint256 feesCounter_) internal view returns (bytes32) { - // watcher address (160 bits) | counter (64 bits) - return bytes32((uint256(uint160(address(this))) << 64) | feesCounter_); - } - function _getFeesPlugAddress(uint32 chainSlug_) internal view returns (address) { return watcherPrecompileConfig().feesPlug(chainSlug_); } - function _isWatcherSignatureValid( - bytes memory digest_, - uint256 signatureNonce_, + function _recoverSigner( + bytes32 digest_, bytes memory signature_ - ) internal { - if (isNonceUsed[signatureNonce_]) revert NonceUsed(); - isNonceUsed[signatureNonce_] = true; - - bytes32 digest = keccak256(abi.encode(address(this), evmxSlug, signatureNonce_, digest_)); - digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); + ) internal view returns (address signer) { + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); // recovered signer is checked for the valid roles later - address signer = ECDSA.recover(digest, signature_); - if (signer != owner()) revert InvalidWatcherSignature(); + signer = ECDSA.recover(digest, signature_); } } diff --git a/contracts/protocol/payload-delivery/FeesPlug.sol b/contracts/protocol/payload-delivery/FeesPlug.sol index a685dfe1..98ae9a46 100644 --- a/contracts/protocol/payload-delivery/FeesPlug.sol +++ b/contracts/protocol/payload-delivery/FeesPlug.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.21; import "solady/utils/SafeTransferLib.sol"; +import "solady/tokens/ERC20.sol"; import "../../base/PlugBase.sol"; import "../utils/AccessControl.sol"; import {RESCUE_ROLE} from "../utils/common/AccessRoles.sol"; @@ -15,32 +16,29 @@ import {InvalidTokenAddress, FeesAlreadyPaid} from "../utils/common/Errors.sol"; /// @dev The amount deposited here is locked and updated in the EVMx for an app gateway /// @dev The fees are redeemed by the transmitters executing request or can be withdrawn by the owner contract FeesPlug is IFeesPlug, PlugBase, AccessControl { - /// @notice Mapping to store the balance of each token - mapping(address => uint256) public override balanceOf; - /// @notice Mapping to store if fees have been redeemed for a given fees ID - mapping(bytes32 => bool) public override feesRedeemed; /// @notice Mapping to store if a token is whitelisted mapping(address => bool) public whitelistedTokens; /// @notice Error thrown when balance is not enough to cover fees - error InsufficientTokenBalance(address token_); + error InsufficientTokenBalance(address token_, uint256 balance_, uint256 fee_); /// @notice Error thrown when deposit amount does not match msg.value error InvalidDepositAmount(); /// @notice Error thrown when token is not whitelisted error TokenNotWhitelisted(address token_); /// @notice Event emitted when fees are deposited - event FeesDeposited(address appGateway, address token, uint256 amount); + event FeesDeposited(address token, address receiver, uint256 feeAmount, uint256 nativeAmount); /// @notice Event emitted when fees are withdrawn - event FeesWithdrawn(address token, uint256 amount, address receiver); + event FeesWithdrawn(address token, address receiver, uint256 amount); /// @notice Event emitted when a token is whitelisted event TokenWhitelisted(address token); /// @notice Event emitted when a token is removed from whitelist event TokenRemovedFromWhitelist(address token); /// @notice Modifier to check if the balance of a token is enough to withdraw - modifier isFeesEnough(uint256 fee_, address feeToken_) { - if (balanceOf[feeToken_] < fee_) revert InsufficientTokenBalance(feeToken_); + modifier isUserCreditsEnough(address feeToken_, uint256 fee_) { + uint balance_ = ERC20(feeToken_).balanceOf(address(this)); + if (balance_ < fee_) revert InsufficientTokenBalance(feeToken_, balance_, fee_); _; } @@ -50,27 +48,6 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { constructor(address socket_, address owner_) { _setSocket(socket_); _initializeOwner(owner_); - - // ETH is whitelisted by default - whitelistedTokens[ETH_ADDRESS] = true; - } - - /// @notice Distributes fees to the transmitter - /// @param feeToken_ The token address - /// @param fee_ The amount of fees - /// @param transmitter_ The transmitter address - /// @param feesId_ The fees ID - function distributeFee( - address feeToken_, - uint256 fee_, - address transmitter_, - bytes32 feesId_ - ) external override onlySocket isFeesEnough(fee_, feeToken_) { - if (feesRedeemed[feesId_]) revert FeesAlreadyPaid(); - feesRedeemed[feesId_] = true; - balanceOf[feeToken_] -= fee_; - - _transferTokens(feeToken_, fee_, transmitter_); } /// @notice Withdraws fees @@ -79,64 +56,52 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { /// @param receiver_ The receiver address function withdrawFees( address token_, - uint256 amount_, - address receiver_ - ) external override onlySocket isFeesEnough(amount_, token_) { - balanceOf[token_] -= amount_; + address receiver_, + uint256 amount_ + ) external override onlySocket isUserCreditsEnough(token_, amount_) { + SafeTransferLib.safeTransfer(token_, receiver_, amount_); + emit FeesWithdrawn(token_, receiver_, amount_); + } - _transferTokens(token_, amount_, receiver_); - emit FeesWithdrawn(token_, amount_, receiver_); + function depositToFee(address token_, address receiver_, uint256 amount_) external override { + _deposit(token_, receiver_, amount_, 0); } - /// @notice Deposits funds - /// @param token_ The token address - /// @param amount_ The amount - /// @param appGateway_ The app gateway address - function deposit( + function depositToFeeAndNative( address token_, - address appGateway_, + address receiver_, uint256 amount_ - ) external payable override { - if (!whitelistedTokens[token_]) revert TokenNotWhitelisted(token_); - - if (token_ == ETH_ADDRESS) { - if (msg.value != amount_) revert InvalidDepositAmount(); - } else { - if (token_.code.length == 0) revert InvalidTokenAddress(); - } - - balanceOf[token_] += amount_; - - if (token_ != ETH_ADDRESS) { - SafeTransferLib.safeTransferFrom(token_, msg.sender, address(this), amount_); - } + ) external override { + uint256 nativeAmount_ = amount_ / 10; + uint256 feeAmount_ = amount_ - nativeAmount_; + _deposit(token_, receiver_, feeAmount_, nativeAmount_); + } - emit FeesDeposited(appGateway_, token_, amount_); + function depositToNative(address token_, address receiver_, uint256 amount_) external override { + _deposit(token_, receiver_, 0, amount_); } - /// @notice Transfers tokens + /// @notice Deposits funds /// @param token_ The token address - /// @param amount_ The amount + /// @param feeAmount_ The amount of fees + /// @param nativeAmount_ The amount of native tokens /// @param receiver_ The receiver address - function _transferTokens(address token_, uint256 amount_, address receiver_) internal { - if (token_ == ETH_ADDRESS) { - SafeTransferLib.forceSafeTransferETH(receiver_, amount_); - } else { - SafeTransferLib.safeTransfer(token_, receiver_, amount_); - } - } - - function connectSocket( - bytes32 appGatewayId_, - address socket_, - address switchboard_ - ) external onlyOwner { - _connectSocket(appGatewayId_, socket_, switchboard_); + function _deposit( + address token_, + address receiver_, + uint256 feeAmount_, + uint256 nativeAmount_ + ) internal { + uint256 totalAmount_ = feeAmount_ + nativeAmount_; + if (!whitelistedTokens[token_]) revert TokenNotWhitelisted(token_); + SafeTransferLib.safeTransferFrom(token_, msg.sender, address(this), totalAmount_); + emit FeesDeposited(receiver_, token_, feeAmount_, nativeAmount_); } /// @notice Adds a token to the whitelist /// @param token_ The token address to whitelist function whitelistToken(address token_) external onlyOwner { + if (token_.code.length == 0) revert InvalidTokenAddress(); whitelistedTokens[token_] = true; emit TokenWhitelisted(token_); } @@ -144,11 +109,17 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { /// @notice Removes a token from the whitelist /// @param token_ The token address to remove function removeTokenFromWhitelist(address token_) external onlyOwner { - if (token_ == ETH_ADDRESS) revert(); // Cannot remove ETH from whitelist whitelistedTokens[token_] = false; emit TokenRemovedFromWhitelist(token_); } + function connectSocket( + bytes32 appGatewayId_, + address socket_, + address switchboard_ + ) external onlyOwner { + _connectSocket(appGatewayId_, socket_, switchboard_); + } /** * @notice Rescues funds from the contract if they are locked by mistake. This contract does not * theoretically need this function but it is added for safety. @@ -163,6 +134,4 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { ) external onlyRole(RESCUE_ROLE) { RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); } - - receive() external payable {} } diff --git a/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol b/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol index 6df859ac..24a78e3b 100644 --- a/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol +++ b/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol @@ -38,7 +38,7 @@ contract DeliveryHelper is FeesHelpers { RequestMetadata storage requestMetadata_ = requests[requestCount_]; // if a transmitter was already assigned, it means the request was restarted bool isRestarted = requestMetadata_.winningBid.transmitter != address(0); - requestMetadata_.winningBid.transmitter = winningBid_.transmitter; + requestMetadata_.winningBid = winningBid_; if (!isRestarted) { watcherPrecompile__().startProcessingRequest(requestCount_, winningBid_.transmitter); @@ -52,17 +52,19 @@ contract DeliveryHelper is FeesHelpers { function finishRequest(uint40 requestCount_) external onlyWatcherPrecompile { RequestMetadata storage requestMetadata_ = requests[requestCount_]; + // todo: move it to watcher precompile if (requestMetadata_.winningBid.transmitter != address(0)) - IFeesManager(addressResolver__.feesManager()).unblockAndAssignFees( + IFeesManager(addressResolver__.feesManager()).unblockAndAssignCredits( requestCount_, - requestMetadata_.winningBid.transmitter, - requestMetadata_.appGateway + requestMetadata_.winningBid.transmitter ); - IAppGateway(requestMetadata_.appGateway).onRequestComplete( - requestCount_, - requestMetadata_.onCompleteData - ); + if (requestMetadata_.appGateway.code.length > 0) { + IAppGateway(requestMetadata_.appGateway).onRequestComplete( + requestCount_, + requestMetadata_.onCompleteData + ); + } } /// @notice Cancels a request and settles the fees @@ -92,14 +94,13 @@ contract DeliveryHelper is FeesHelpers { function _settleFees(uint40 requestCount_) internal { // If the request has a winning bid, ie. transmitter already assigned, unblock and assign fees if (requests[requestCount_].winningBid.transmitter != address(0)) { - IFeesManager(addressResolver__.feesManager()).unblockAndAssignFees( + IFeesManager(addressResolver__.feesManager()).unblockAndAssignCredits( requestCount_, - requests[requestCount_].winningBid.transmitter, - requests[requestCount_].appGateway + requests[requestCount_].winningBid.transmitter ); } else { // If the request has no winning bid, ie. transmitter not assigned, unblock fees - IFeesManager(addressResolver__.feesManager()).unblockFees(requestCount_); + IFeesManager(addressResolver__.feesManager()).unblockCredits(requestCount_); } } diff --git a/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol b/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol index 0a63674b..38fcecc9 100644 --- a/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol +++ b/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol @@ -33,14 +33,17 @@ abstract contract DeliveryUtils is error MaxMsgValueLimitExceeded(); event BidTimeoutUpdated(uint256 newBidTimeout); + + /// @notice Emitted when a payload is submitted event PayloadSubmitted( uint40 indexed requestCount, address indexed appGateway, PayloadSubmitParams[] payloadSubmitParams, - Fees fees, + uint256 fees, address auctionManager, bool onlyReadRequests ); + /// @notice Emitted when fees are increased event FeesIncreased( address indexed appGateway, diff --git a/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol b/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol index 1c59ac64..f1845865 100644 --- a/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol +++ b/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol @@ -9,19 +9,21 @@ abstract contract FeesHelpers is RequestQueue { // slots [258-308] reserved for gap uint256[50] _gap_batch_async; + error NewMaxFeesLowerThanCurrent(uint256 current, uint256 new_); + /// @notice Increases the fees for a request if no bid is placed /// @param requestCount_ The ID of the request /// @param newMaxFees_ The new maximum fees function increaseFees(uint40 requestCount_, uint256 newMaxFees_) external override { address appGateway = _getCoreAppGateway(msg.sender); - // todo: should we allow core app gateway too? if (appGateway != requests[requestCount_].appGateway) { revert OnlyAppGateway(); } - if (requests[requestCount_].winningBid.transmitter != address(0)) revert WinningBidExists(); - requests[requestCount_].fees.amount = newMaxFees_; + if (requests[requestCount_].maxFees >= newMaxFees_) + revert NewMaxFeesLowerThanCurrent(requests[requestCount_].maxFees, newMaxFees_); + requests[requestCount_].maxFees = newMaxFees_; emit FeesIncreased(appGateway, requestCount_, newMaxFees_); } @@ -37,23 +39,61 @@ abstract contract FeesHelpers is RequestQueue { uint256 amount_, address receiver_, address auctionManager_, - Fees memory fees_ + uint256 fees_ ) external returns (uint40) { - IFeesManager(addressResolver__.feesManager()).withdrawFees( + IFeesManager(addressResolver__.feesManager()).withdrawCredits( msg.sender, chainSlug_, token_, amount_, receiver_ ); + return _batch(msg.sender, auctionManager_, msg.sender, fees_, bytes("")); + } + + /// @notice Withdraws fees to a specified receiver + /// @param chainSlug_ The chain identifier + /// @param token_ The token address + /// @param receiver_ The address of the receiver + function withdrawTransmitterFees( + uint32 chainSlug_, + address token_, + address receiver_, + uint256 amount_ + ) external returns (uint40 requestCount) { + address transmitter = msg.sender; + + PayloadSubmitParams[] memory payloadSubmitParamsArray = IFeesManager( + addressResolver__.feesManager() + ).getWithdrawTransmitterCreditsPayloadParams( + transmitter, + chainSlug_, + token_, + receiver_, + amount_ + ); - return _batch(msg.sender, auctionManager_, fees_, bytes("")); + RequestMetadata memory requestMetadata = RequestMetadata({ + appGateway: addressResolver__.feesManager(), + auctionManager: address(0), + maxFees: 0, + winningBid: Bid({transmitter: transmitter, fee: 0, extraData: new bytes(0)}), + onCompleteData: bytes(""), + onlyReadRequests: false, + consumeFrom: transmitter, + queryCount: 0, + finalizeCount: 1 + }); // finalize for plug contract + requestCount = watcherPrecompile__().submitRequest(payloadSubmitParamsArray); + requests[requestCount] = requestMetadata; + // same transmitter can execute requests without auction + watcherPrecompile__().startProcessingRequest(requestCount, transmitter); } /// @notice Returns the fees for a request /// @param requestCount_ The ID of the request /// @return fees The fees data - function getFees(uint40 requestCount_) external view returns (Fees memory) { - return requests[requestCount_].fees; + function getFees(uint40 requestCount_) external view returns (uint256) { + return requests[requestCount_].maxFees; } } diff --git a/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol b/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol index 0729e35a..73a7b809 100644 --- a/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol +++ b/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol @@ -20,33 +20,17 @@ abstract contract RequestQueue is DeliveryUtils { } /// @notice Initiates a batch of payloads - /// @param fees_ The fees data + /// @param maxFees_ The fees data /// @param auctionManager_ The auction manager address /// @return requestCount The ID of the batch function batch( - Fees memory fees_, + uint256 maxFees_, address auctionManager_, + address consumeFrom_, bytes memory onCompleteData_ ) external returns (uint40 requestCount) { address appGateway = _getCoreAppGateway(msg.sender); - return _batch(appGateway, auctionManager_, fees_, onCompleteData_); - } - - function _checkBatch( - address appGateway_, - address auctionManager_, - Fees memory fees_ - ) internal view returns (address) { - if (queuePayloadParams.length > REQUEST_PAYLOAD_COUNT_LIMIT) - revert RequestPayloadCountLimitExceeded(); - - if (!IFeesManager(addressResolver__.feesManager()).isFeesEnough(appGateway_, fees_)) - revert InsufficientFees(); - - return - auctionManager_ == address(0) - ? IAddressResolver(addressResolver__).defaultAuctionManager() - : auctionManager_; + return _batch(appGateway, auctionManager_, consumeFrom_, maxFees_, onCompleteData_); } /// @notice Initiates a batch of payloads @@ -56,59 +40,127 @@ abstract contract RequestQueue is DeliveryUtils { function _batch( address appGateway_, address auctionManager_, - Fees memory fees_, + address consumeFrom_, + uint256 maxFees_, bytes memory onCompleteData_ ) internal returns (uint40 requestCount) { if (queuePayloadParams.length == 0) return 0; - auctionManager_ = _checkBatch(appGateway_, auctionManager_, fees_); - // create the payload submit params array in desired format + BatchParams memory params = BatchParams({ + appGateway: appGateway_, + auctionManager: _getAuctionManager(auctionManager_), + maxFees: maxFees_, + onCompleteData: onCompleteData_, + onlyReadRequests: false, + queryCount: 0, + finalizeCount: 0 + }); + + // Split the function into smaller parts ( PayloadSubmitParams[] memory payloadSubmitParamsArray, - bool onlyReadRequests + bool onlyReadRequests, + uint256 queryCount, + uint256 finalizeCount ) = _createPayloadSubmitParamsArray(); + params.onlyReadRequests = onlyReadRequests; + params.queryCount = queryCount; + params.finalizeCount = finalizeCount; + + _checkBatch(consumeFrom_, params.appGateway, params.maxFees); + + return _submitBatchRequest(payloadSubmitParamsArray, consumeFrom_, params); + } + + function _submitBatchRequest( + PayloadSubmitParams[] memory payloadSubmitParamsArray, + address consumeFrom_, + BatchParams memory params + ) internal returns (uint40 requestCount) { RequestMetadata memory requestMetadata = RequestMetadata({ - appGateway: appGateway_, - auctionManager: auctionManager_, - fees: fees_, + appGateway: params.appGateway, + auctionManager: params.auctionManager, + maxFees: params.maxFees, winningBid: Bid({fee: 0, transmitter: address(0), extraData: new bytes(0)}), - onCompleteData: onCompleteData_, - onlyReadRequests: onlyReadRequests + onCompleteData: params.onCompleteData, + onlyReadRequests: params.onlyReadRequests, + consumeFrom: consumeFrom_, + queryCount: params.queryCount, + finalizeCount: params.finalizeCount }); - // process and submit the queue of payloads to watcher precompile requestCount = watcherPrecompile__().submitRequest(payloadSubmitParamsArray); requests[requestCount] = requestMetadata; - // send query directly if request contains only reads - // transmitter should ignore the batch for auction, the transaction will also revert if someone bids - if (onlyReadRequests) + if (params.onlyReadRequests) { watcherPrecompile__().startProcessingRequest(requestCount, address(0)); + } + + uint256 watcherFees = watcherPrecompileLimits().getTotalFeesRequired( + params.queryCount, + params.finalizeCount, + 0, + 0 + ); + if (watcherFees > params.maxFees) revert InsufficientFees(); + uint256 maxTransmitterFees = params.maxFees - watcherFees; emit PayloadSubmitted( requestCount, - appGateway_, + params.appGateway, payloadSubmitParamsArray, - fees_, - auctionManager_, - onlyReadRequests + maxTransmitterFees, + params.auctionManager, + params.onlyReadRequests ); } + function _getAuctionManager(address auctionManager_) internal view returns (address) { + return + auctionManager_ == address(0) + ? IAddressResolver(addressResolver__).defaultAuctionManager() + : auctionManager_; + } + + function _checkBatch( + address consumeFrom_, + address appGateway_, + uint256 maxFees_ + ) internal view { + if (queuePayloadParams.length > REQUEST_PAYLOAD_COUNT_LIMIT) + revert RequestPayloadCountLimitExceeded(); + + if ( + !IFeesManager(addressResolver__.feesManager()).isUserCreditsEnough( + consumeFrom_, + appGateway_, + maxFees_ + ) + ) revert InsufficientFees(); + } + /// @notice Creates an array of payload details /// @return payloadDetailsArray An array of payload details function _createPayloadSubmitParamsArray() internal - returns (PayloadSubmitParams[] memory payloadDetailsArray, bool onlyReadRequests) + returns ( + PayloadSubmitParams[] memory payloadDetailsArray, + bool onlyReadRequests, + uint256 queryCount, + uint256 finalizeCount + ) { payloadDetailsArray = new PayloadSubmitParams[](queuePayloadParams.length); onlyReadRequests = queuePayloadParams[0].callType == CallType.READ; uint256 currentLevel = 0; for (uint256 i = 0; i < queuePayloadParams.length; i++) { - if (queuePayloadParams[i].callType != CallType.READ) { + if (queuePayloadParams[i].callType == CallType.READ) { + queryCount++; + } else { onlyReadRequests = false; + finalizeCount++; } // Update level for calls diff --git a/contracts/protocol/utils/AddressResolverUtil.sol b/contracts/protocol/utils/AddressResolverUtil.sol index ec6ba238..21f24611 100644 --- a/contracts/protocol/utils/AddressResolverUtil.sol +++ b/contracts/protocol/utils/AddressResolverUtil.sol @@ -6,6 +6,7 @@ import "../../interfaces/IMiddleware.sol"; import "../../interfaces/IWatcherPrecompile.sol"; import "../../interfaces/IWatcherPrecompileConfig.sol"; import "../../interfaces/IWatcherPrecompileLimits.sol"; +import "../../interfaces/IFeesManager.sol"; /// @title AddressResolverUtil /// @notice Utility contract for resolving system contract addresses diff --git a/contracts/protocol/utils/FeesPlugin.sol b/contracts/protocol/utils/FeesPlugin.sol deleted file mode 100644 index c78bc932..00000000 --- a/contracts/protocol/utils/FeesPlugin.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import {Fees} from "../utils/common/Structs.sol"; - -/// @title FeesPlugin -/// @notice Abstract contract for managing fee configurations -/// @dev Provides base functionality for fee management in the system -abstract contract FeesPlugin { - /// @notice Storage for the current fee configuration - /// @dev Contains fee parameters like chain slug, token address, and amount - Fees public fees; - - /// @notice Retrieves the current fee configuration - /// @return Current fee configuration struct - /// @dev Public view function accessible to any caller - /// @dev Used by external contracts to verify fee parameters - function getFees() public view returns (Fees memory) { - return fees; - } -} diff --git a/contracts/protocol/utils/common/Constants.sol b/contracts/protocol/utils/common/Constants.sol index bf6c8d4a..7b17f483 100644 --- a/contracts/protocol/utils/common/Constants.sol +++ b/contracts/protocol/utils/common/Constants.sol @@ -9,6 +9,7 @@ bytes32 constant DEPLOY = keccak256("DEPLOY"); bytes32 constant QUERY = keccak256("QUERY"); bytes32 constant FINALIZE = keccak256("FINALIZE"); bytes32 constant SCHEDULE = keccak256("SCHEDULE"); +bytes32 constant CALLBACK = keccak256("CALLBACK"); bytes32 constant FAST = keccak256("FAST"); uint256 constant REQUEST_PAYLOAD_COUNT_LIMIT = 10; uint256 constant PAYLOAD_SIZE_LIMIT = 24_500; diff --git a/contracts/protocol/utils/common/Structs.sol b/contracts/protocol/utils/common/Structs.sol index 16eb6736..301cca0c 100644 --- a/contracts/protocol/utils/common/Structs.sol +++ b/contracts/protocol/utils/common/Structs.sol @@ -50,6 +50,22 @@ enum ExecutionStatus { Reverted } +/// @notice Creates a struct to hold batch parameters +struct BatchParams { + address appGateway; + address auctionManager; + uint256 maxFees; + bytes onCompleteData; + bool onlyReadRequests; + uint256 queryCount; + uint256 finalizeCount; +} + +struct AppGatewayWhitelistParams { + address appGateway; + bool isApproved; +} + //// STRUCTS //// // plug: struct LimitParams { @@ -108,6 +124,12 @@ struct Bid { bytes extraData; } +struct OnChainFees { + uint32 chainSlug; + address token; + uint256 amount; +} + // App gateway base: struct OverrideParams { Read isReadCall; @@ -118,11 +140,9 @@ struct OverrideParams { uint256 readAt; } -// FM: -struct Fees { - uint32 feePoolChain; - address feePoolToken; - uint256 amount; +struct UserCredits { + uint256 totalCredits; + uint256 blockedCredits; } // digest: @@ -204,6 +224,9 @@ struct RequestParams { // updated while processing request uint256 currentBatchPayloadsLeft; uint256 payloadsRemaining; + uint256 queryCount; + uint256 finalizeCount; + uint256 scheduleCount; address middleware; // updated after auction address transmitter; @@ -213,10 +236,13 @@ struct RequestParams { struct RequestMetadata { address appGateway; address auctionManager; - Fees fees; + uint256 maxFees; Bid winningBid; bytes onCompleteData; bool onlyReadRequests; + address consumeFrom; + uint256 queryCount; + uint256 finalizeCount; } struct ExecuteParams { @@ -247,9 +273,3 @@ struct PayloadIdParams { address switchboard; uint32 chainSlug; } - -/// @notice Struct containing fee amounts and status -struct TokenBalance { - uint256 deposited; // Amount deposited - uint256 blocked; // Amount blocked -} diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol index 5db1ec49..22c85d4f 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol +++ b/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol @@ -206,14 +206,16 @@ contract WatcherPrecompileConfig is } function _isWatcherSignatureValid( - bytes memory digest_, + bytes memory inputData_, uint256 signatureNonce_, bytes memory signature_ ) internal { if (isNonceUsed[signatureNonce_]) revert NonceUsed(); isNonceUsed[signatureNonce_] = true; - bytes32 digest = keccak256(abi.encode(address(this), evmxSlug, signatureNonce_, digest_)); + bytes32 digest = keccak256( + abi.encode(address(this), evmxSlug, signatureNonce_, inputData_) + ); digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); // recovered signer is checked for the valid roles later diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol index 8303a440..01a084da 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol +++ b/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol @@ -6,7 +6,7 @@ import {Ownable} from "solady/auth/Ownable.sol"; import {Gauge} from "../utils/Gauge.sol"; import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; import "../../interfaces/IWatcherPrecompileLimits.sol"; -import {SCHEDULE, QUERY, FINALIZE} from "../utils/common/Constants.sol"; +import {SCHEDULE, QUERY, FINALIZE, CALLBACK} from "../utils/common/Constants.sol"; /// @title WatcherPrecompileLimits /// @notice Contract for managing watcher precompile limits @@ -43,9 +43,17 @@ contract WatcherPrecompileLimits is // Mapping to track active app gateways mapping(address => bool) internal _activeAppGateways; + // slot 157: fees + uint256 public queryFees; + uint256 public finalizeFees; + uint256 public timeoutFees; + uint256 public callBackFees; + /// @notice Emitted when the default limit and rate per second are set event DefaultLimitAndRatePerSecondSet(uint256 defaultLimit, uint256 defaultRatePerSecond); + error WatcherFeesNotSet(bytes32 limitType); + /// @notice Initial initialization (version 1) function initialize( address owner_, @@ -156,4 +164,35 @@ contract WatcherPrecompileLimits is emit DefaultLimitAndRatePerSecondSet(defaultLimit, defaultRatePerSecond); } + + function setQueryFees(uint256 queryFees_) external onlyOwner { + queryFees = queryFees_; + } + + function setFinalizeFees(uint256 finalizeFees_) external onlyOwner { + finalizeFees = finalizeFees_; + } + + function setTimeoutFees(uint256 timeoutFees_) external onlyOwner { + timeoutFees = timeoutFees_; + } + + function setCallBackFees(uint256 callBackFees_) external onlyOwner { + callBackFees = callBackFees_; + } + + function getTotalFeesRequired( + uint256 queryCount_, + uint256 finalizeCount_, + uint256 scheduleCount_, + uint256 callbackCount_ + ) external view returns (uint256) { + uint256 totalFees = 0; + totalFees += callbackCount_ * callBackFees; + totalFees += queryCount_ * queryFees; + totalFees += finalizeCount_ * finalizeFees; + totalFees += scheduleCount_ * timeoutFees; + + return totalFees; + } } diff --git a/contracts/protocol/watcherPrecompile/core/RequestHandler.sol b/contracts/protocol/watcherPrecompile/core/RequestHandler.sol index 0d3a2851..1cb77e73 100644 --- a/contracts/protocol/watcherPrecompile/core/RequestHandler.sol +++ b/contracts/protocol/watcherPrecompile/core/RequestHandler.sol @@ -14,14 +14,14 @@ abstract contract RequestHandler is WatcherPrecompileCore { uint256[50] _request_handler_gap; /// @notice Submits a batch of payload requests from middleware - /// @param payloadSubmitParams Array of payload submit parameters + /// @param payloadSubmitParams_ Array of payload submit parameters /// @return requestCount The unique identifier for the submitted request /// @dev This function processes a batch of payload requests and assigns them to batches /// @dev It also consumes limits for the app gateway based on the number of reads and writes function submitRequest( - PayloadSubmitParams[] calldata payloadSubmitParams + PayloadSubmitParams[] calldata payloadSubmitParams_ ) public returns (uint40 requestCount) { - address appGateway = _checkAppGateways(payloadSubmitParams); + address appGateway = _checkAppGateways(payloadSubmitParams_); requestCount = nextRequestCount++; uint40 batchCount = nextBatchCount; @@ -31,8 +31,8 @@ abstract contract RequestHandler is WatcherPrecompileCore { uint256 writeCount; PayloadSubmitParams memory lastP; - for (uint256 i = 0; i < payloadSubmitParams.length; i++) { - PayloadSubmitParams memory p = payloadSubmitParams[i]; + for (uint256 i = 0; i < payloadSubmitParams_.length; i++) { + PayloadSubmitParams memory p = payloadSubmitParams_[i]; // Count reads and writes for checking limits if (p.callType == CallType.READ) { @@ -92,8 +92,11 @@ abstract contract RequestHandler is WatcherPrecompileCore { watcherPrecompileLimits__.consumeLimit(appGateway, QUERY, readCount); watcherPrecompileLimits__.consumeLimit(appGateway, FINALIZE, writeCount); + requestParams[requestCount].queryCount = readCount; + requestParams[requestCount].finalizeCount = writeCount; + requestParams[requestCount].currentBatch = currentBatch; - requestParams[requestCount].payloadsRemaining = payloadSubmitParams.length; + requestParams[requestCount].payloadsRemaining = payloadSubmitParams_.length; requestParams[requestCount].middleware = msg.sender; emit RequestSubmitted( @@ -129,17 +132,17 @@ abstract contract RequestHandler is WatcherPrecompileCore { /// @notice Starts processing a request with a specified transmitter /// @param requestCount The request count to start processing - /// @param transmitter The address of the transmitter + /// @param transmitter_ The winning bid, contains fees, transmitter and extra data /// @dev This function initiates the processing of a request by a transmitter /// @dev It verifies that the caller is the middleware and that the request hasn't been started yet - function startProcessingRequest(uint40 requestCount, address transmitter) public { + function startProcessingRequest(uint40 requestCount, address transmitter_) public { RequestParams storage r = requestParams[requestCount]; if (r.middleware != msg.sender) revert InvalidCaller(); if (r.transmitter != address(0)) revert AlreadyStarted(); if (r.currentBatchPayloadsLeft > 0) revert AlreadyStarted(); uint40 batchCount = r.payloadParamsArray[0].payloadHeader.getBatchCount(); - r.transmitter = transmitter; + r.transmitter = transmitter_; r.currentBatch = batchCount; _processBatch(requestCount, batchCount); diff --git a/contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol b/contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol index 6e42c4f1..48cae8fe 100644 --- a/contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol +++ b/contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol @@ -175,7 +175,6 @@ contract WatcherPrecompile is RequestHandler { if (r.middleware != msg.sender) revert InvalidCaller(); r.isRequestCancelled = true; - emit RequestCancelledFromGateway(requestCount); } @@ -202,11 +201,14 @@ contract WatcherPrecompile is RequestHandler { PayloadParams memory payloadParams = payloads[resolvedPromises_[i].payloadId]; address asyncPromise = payloadParams.asyncPromise; + uint40 requestCount = payloadParams.payloadHeader.getRequestCount(); + // todo: non trusted call if (asyncPromise != address(0)) { + // todo: limit the gas used for promise resolution // Resolve each promise with its corresponding return data bool success = IPromise(asyncPromise).markResolved( - payloadParams.payloadHeader.getRequestCount(), + requestCount, resolvedPromises_[i].payloadId, resolvedPromises_[i].returnData ); @@ -218,9 +220,7 @@ contract WatcherPrecompile is RequestHandler { } isPromiseExecuted[resolvedPromises_[i].payloadId] = true; - RequestParams storage requestParams_ = requestParams[ - payloadParams.payloadHeader.getRequestCount() - ]; + RequestParams storage requestParams_ = requestParams[requestCount]; requestParams_.currentBatchPayloadsLeft--; requestParams_.payloadsRemaining--; @@ -228,17 +228,12 @@ contract WatcherPrecompile is RequestHandler { if ( requestParams_.currentBatchPayloadsLeft == 0 && requestParams_.payloadsRemaining > 0 ) { - _processBatch( - payloadParams.payloadHeader.getRequestCount(), - ++requestParams_.currentBatch - ); + _processBatch(requestCount, ++requestParams_.currentBatch); } // if all payloads of a request are executed, finish the request if (requestParams_.payloadsRemaining == 0) { - IMiddleware(requestParams_.middleware).finishRequest( - payloadParams.payloadHeader.getRequestCount() - ); + IMiddleware(requestParams_.middleware).finishRequest(requestCount); } emit PromiseResolved(resolvedPromises_[i].payloadId, asyncPromise); } @@ -316,6 +311,11 @@ contract WatcherPrecompile is RequestHandler { ) ) revert InvalidCallerTriggered(); + IFeesManager(addressResolver__.feesManager()).assignWatcherPrecompileCreditsFromAddress( + watcherPrecompileLimits__.callBackFees(), + appGateway + ); + appGatewayCaller = appGateway; appGatewayCalled[params_[i].triggerId] = true; diff --git a/contracts/protocol/watcherPrecompile/core/WatcherPrecompileCore.sol b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileCore.sol index 873f97f3..402532bc 100644 --- a/contracts/protocol/watcherPrecompile/core/WatcherPrecompileCore.sol +++ b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileCore.sol @@ -6,6 +6,7 @@ import {ECDSA} from "solady/utils/ECDSA.sol"; import {AccessControl} from "../../utils/AccessControl.sol"; import "solady/utils/Initializable.sol"; import {AddressResolverUtil} from "../../utils/AddressResolverUtil.sol"; +import {IFeesManager} from "../../../interfaces/IFeesManager.sol"; import "./WatcherPrecompileUtils.sol"; /// @title WatcherPrecompileCore @@ -35,6 +36,8 @@ abstract contract WatcherPrecompileCore is ) internal returns (bytes32 timeoutId) { if (delayInSeconds_ > maxTimeoutDelayInSeconds) revert TimeoutDelayTooLarge(); + _consumeCallbackFeesFromAddress(watcherPrecompileLimits__.timeoutFees(), msg.sender); + uint256 executeAt = block.timestamp + delayInSeconds_; timeoutId = _encodeTimeoutId(); @@ -76,6 +79,11 @@ abstract contract WatcherPrecompileCore is requestParams[params_.payloadHeader.getRequestCount()].middleware ); + _consumeCallbackFeesFromRequestCount( + watcherPrecompileLimits__.finalizeFees(), + params_.payloadHeader.getRequestCount() + ); + uint256 deadline = block.timestamp + expiryTime; payloads[params_.payloadId].deadline = deadline; payloads[params_.payloadId].finalizedTransmitter = transmitter_; @@ -109,6 +117,11 @@ abstract contract WatcherPrecompileCore is /// @param params_ The payload parameters for the query /// @dev This function sets up a query request and emits a QueryRequested event function _query(PayloadParams memory params_) internal { + _consumeCallbackFeesFromRequestCount( + watcherPrecompileLimits__.queryFees(), + params_.payloadHeader.getRequestCount() + ); + payloads[params_.payloadId].prevDigestsHash = _getPreviousDigestsHash( params_.payloadHeader.getBatchCount() ); @@ -217,19 +230,21 @@ abstract contract WatcherPrecompileCore is } /// @notice Verifies that a watcher signature is valid - /// @param digest_ The digest to verify + /// @param inputData_ The input data to verify /// @param signatureNonce_ The nonce of the signature /// @param signature_ The signature to verify /// @dev This function verifies that the signature was created by the watcher and that the nonce has not been used before function _isWatcherSignatureValid( - bytes memory digest_, + bytes memory inputData_, uint256 signatureNonce_, bytes memory signature_ ) internal { if (isNonceUsed[signatureNonce_]) revert NonceUsed(); isNonceUsed[signatureNonce_] = true; - bytes32 digest = keccak256(abi.encode(address(this), evmxSlug, signatureNonce_, digest_)); + bytes32 digest = keccak256( + abi.encode(address(this), evmxSlug, signatureNonce_, inputData_) + ); digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); // recovered signer is checked for the valid roles later @@ -237,6 +252,22 @@ abstract contract WatcherPrecompileCore is if (signer != owner()) revert InvalidWatcherSignature(); } + function _consumeCallbackFeesFromRequestCount(uint256 fees_, uint40 requestCount_) internal { + // for callbacks in all precompiles + uint256 feesToConsume = fees_ + watcherPrecompileLimits__.callBackFees(); + IFeesManager(addressResolver__.feesManager()) + .assignWatcherPrecompileCreditsFromRequestCount(feesToConsume, requestCount_); + } + + function _consumeCallbackFeesFromAddress(uint256 fees_, address consumeFrom_) internal { + // for callbacks in all precompiles + uint256 feesToConsume = fees_ + watcherPrecompileLimits__.callBackFees(); + IFeesManager(addressResolver__.feesManager()).assignWatcherPrecompileCreditsFromAddress( + feesToConsume, + consumeFrom_ + ); + } + /// @notice Gets the batch IDs for a request /// @param requestCount_ The request count to get the batch IDs for /// @return An array of batch IDs for the given request diff --git a/contracts/protocol/watcherPrecompile/core/WatcherPrecompileStorage.sol b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileStorage.sol index d13ad022..12dd3d53 100644 --- a/contracts/protocol/watcherPrecompile/core/WatcherPrecompileStorage.sol +++ b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileStorage.sol @@ -96,8 +96,12 @@ abstract contract WatcherPrecompileStorage is IWatcherPrecompile { // slot 64 IWatcherPrecompileConfig public watcherPrecompileConfig__; - // slots [65-114]: gap for future storage variables - uint256[50] _gap_after; + // slot 65 + /// @notice Mapping to store the request metadata for each request count + mapping(uint40 => RequestMetadata) public requestMetadata; + + // slots [67-114]: gap for future storage variables + uint256[48] _gap_after; // slots 115-165 (51) reserved for access control // slots 166-216 (51) reserved for addr resolver util diff --git a/script/counter/DeployEVMxCounterApp.s.sol b/script/counter/DeployEVMxCounterApp.s.sol index 9e0cbee8..85755639 100644 --- a/script/counter/DeployEVMxCounterApp.s.sol +++ b/script/counter/DeployEVMxCounterApp.s.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.21; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; -import {Fees} from "../../contracts/protocol/utils/common/Structs.sol"; import {ETH_ADDRESS} from "../../contracts/protocol/utils/common/Constants.sol"; // source .env && forge script script/counter/deployEVMxCounterApp.s.sol --broadcast --skip-simulation --legacy --gas-price 0 @@ -18,11 +17,7 @@ contract CounterDeploy is Script { vm.startBroadcast(deployerPrivateKey); // Setting fee payment on Arbitrum Sepolia - Fees memory fees = Fees({ - feePoolChain: 421614, - feePoolToken: ETH_ADDRESS, - amount: 0.00001 ether - }); + uint256 fees = 0.00001 ether; CounterAppGateway gateway = new CounterAppGateway(addressResolver, fees); diff --git a/script/counter/SetFees.s.sol b/script/counter/SetFees.s.sol index c97a2b0b..3f87f4ac 100644 --- a/script/counter/SetFees.s.sol +++ b/script/counter/SetFees.s.sol @@ -5,7 +5,6 @@ import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; import {ETH_ADDRESS} from "../../contracts/protocol/utils/common/Constants.sol"; import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; -import {Fees} from "../../contracts/protocol/utils/common/Structs.sol"; // source .env && forge script script/counter/DeployCounterOnchain.s.sol --broadcast --skip-simulation --legacy --gas-price 0 contract CounterSetFees is Script { @@ -22,11 +21,7 @@ contract CounterSetFees is Script { console.log("Setting fees..."); // Setting fee payment on Arbitrum Sepolia - Fees memory fees = Fees({ - feePoolChain: 421614, - feePoolToken: ETH_ADDRESS, - amount: 0.00001 ether - }); - appGateway.setFees(fees); + uint256 fees = 0.00001 ether; + // appGateway.setFees(fees); } } diff --git a/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol b/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol index 3a78e9cf..ecd8b2b6 100644 --- a/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol +++ b/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol @@ -17,11 +17,7 @@ contract WithdrawFees is Script { address appGatewayAddress = vm.envAddress("APP_GATEWAY"); CounterAppGateway appGateway = CounterAppGateway(appGatewayAddress); - uint256 availableFees = feesManager.getAvailableFees( - 421614, - appGatewayAddress, - ETH_ADDRESS - ); + uint256 availableFees = feesManager.getMaxCreditsAvailableForWithdraw(appGatewayAddress); console.log("Available fees:", availableFees); if (availableFees > 0) { diff --git a/script/helpers/AppGatewayFeeBalance.s.sol b/script/helpers/AppGatewayFeeBalance.s.sol index 395c542e..8e80861f 100644 --- a/script/helpers/AppGatewayFeeBalance.s.sol +++ b/script/helpers/AppGatewayFeeBalance.s.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.21; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; import {FeesManager} from "../../contracts/protocol/payload-delivery/FeesManager.sol"; -import {Fees} from "../../contracts/protocol/utils/common/Structs.sol"; import {ETH_ADDRESS} from "../../contracts/protocol/utils/common/Constants.sol"; contract CheckDepositedFees is Script { @@ -12,21 +11,14 @@ contract CheckDepositedFees is Script { vm.createSelectFork(vm.envString("EVMX_RPC")); FeesManager feesManager = FeesManager(payable(vm.envAddress("FEES_MANAGER"))); address appGateway = vm.envAddress("APP_GATEWAY"); - uint32 chain = 421614; - address token = ETH_ADDRESS; - (uint256 deposited, uint256 blocked) = feesManager.appGatewayFeeBalances( - appGateway, - chain, - token - ); + + (uint256 totalCredits, uint256 blockedCredits) = feesManager.userCredits(appGateway); console.log("App Gateway:", appGateway); console.log("Fees Manager:", address(feesManager)); - console.logUint(chain); - console.log("Token:", token); - console.log("Deposited fees:", deposited); - console.log("Blocked fees:", blocked); + console.log("totalCredits fees:", totalCredits); + console.log("blockedCredits fees:", blockedCredits); - uint256 availableFees = feesManager.getAvailableFees(chain, appGateway, token); + uint256 availableFees = feesManager.getAvailableCredits(appGateway); console.log("Available fees:", availableFees); } } diff --git a/script/helpers/PayFeesInArbitrumETH.s.sol b/script/helpers/PayFeesInArbitrumETH.s.sol index edb8b2fd..0940009a 100644 --- a/script/helpers/PayFeesInArbitrumETH.s.sol +++ b/script/helpers/PayFeesInArbitrumETH.s.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.21; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; import {FeesPlug} from "../../contracts/protocol/payload-delivery/FeesPlug.sol"; -import {Fees} from "../../contracts/protocol/utils/common/Structs.sol"; import {ETH_ADDRESS} from "../../contracts/protocol/utils/common/Constants.sol"; // source .env && forge script script/helpers/PayFeesInArbitrumETH.s.sol --broadcast --skip-simulation @@ -24,6 +23,6 @@ contract DepositFees is Script { console.log("App Gateway:", appGateway); console.log("Fees Plug:", address(feesPlug)); uint feesAmount = 0.001 ether; - feesPlug.deposit{value: feesAmount}(ETH_ADDRESS, appGateway, feesAmount); + feesPlug.depositToFeeAndNative(ETH_ADDRESS, appGateway, feesAmount); } } diff --git a/test/DeliveryHelper.t.sol b/test/DeliveryHelper.t.sol index a969ad5c..e8a689ad 100644 --- a/test/DeliveryHelper.t.sol +++ b/test/DeliveryHelper.t.sol @@ -27,7 +27,7 @@ contract DeliveryHelperTest is SetupTest { uint40 indexed requestCount, address indexed appGateway, PayloadSubmitParams[] payloadSubmitParams, - Fees fees, + uint256 maxFees, address auctionManager, bool onlyReadRequests ); @@ -111,6 +111,15 @@ contract DeliveryHelperTest is SetupTest { arbConfig = deploySocket(arbChainSlug); optConfig = deploySocket(optChainSlug); connectDeliveryHelper(); + + depositUSDCFees( + address(auctionManager), + OnChainFees({ + chainSlug: arbChainSlug, + token: address(arbConfig.feesTokenUSDC), + amount: 1 ether + }) + ); } function connectDeliveryHelper() internal { @@ -173,33 +182,61 @@ contract DeliveryHelperTest is SetupTest { //////////////////////////////////// Fees //////////////////////////////////// - function depositFees(address appGateway_, Fees memory fees_) internal { - SocketContracts memory socketConfig = getSocketConfig(fees_.feePoolChain); - socketConfig.feesPlug.deposit{value: fees_.amount}( - fees_.feePoolToken, - appGateway_, - fees_.amount + function depositUSDCFees(address appGateway_, OnChainFees memory fees_) internal { + SocketContracts memory socketConfig = getSocketConfig(fees_.chainSlug); + vm.startPrank(owner); + ERC20(fees_.token).approve(address(socketConfig.feesPlug), fees_.amount); + socketConfig.feesPlug.depositToFeeAndNative(fees_.token, appGateway_, fees_.amount); + vm.stopPrank(); + + bytes32 digest = keccak256( + abi.encode( + appGateway_, + fees_.chainSlug, + fees_.token, + fees_.amount, + address(feesManager), + evmxSlug, + signatureNonce + ) ); - bytes memory bytesInput = abi.encode( - fees_.feePoolChain, + feesManager.depositCredits{value: fees_.amount}( appGateway_, - fees_.feePoolToken, - fees_.amount + fees_.chainSlug, + fees_.token, + signatureNonce++, + _createSignature(digest, watcherPrivateKey) ); + } + function whitelistAppGateway( + address appGateway_, + address user_, + uint256 userPrivateKey_, + uint32 chainSlug_ + ) internal { + SocketContracts memory socketConfig = getSocketConfig(chainSlug_); + // Create fee approval data with signature bytes32 digest = keccak256( - abi.encode(address(feesManager), evmxSlug, signatureNonce, bytesInput) - ); - bytes memory sig = _createSignature(digest, watcherPrivateKey); - feesManager.incrementFeesDeposited( - fees_.feePoolChain, - appGateway_, - fees_.feePoolToken, - fees_.amount, - signatureNonce++, - sig + abi.encode( + address(feesManager), + evmxSlug, + user_, + appGateway_, + feesManager.userNonce(user_), + true + ) ); + + // Sign with consumeFrom's private key + bytes memory signature = _createSignature(digest, userPrivateKey_); + + // Encode approval data + bytes memory feeApprovalData = abi.encode(user_, appGateway_, true, signature); + + // Call whitelistAppGatewayWithSignature with approval data + feesManager.whitelistAppGatewayWithSignature(feeApprovalData); } ////////////////////////////////// Deployment helpers //////////////////////////////////// diff --git a/test/FeesTest.t.sol b/test/FeesTest.t.sol index ec2f9632..1f6a0ba0 100644 --- a/test/FeesTest.t.sol +++ b/test/FeesTest.t.sol @@ -9,7 +9,7 @@ contract FeesTest is DeliveryHelperTest { uint256 constant depositAmount = 1 ether; uint256 constant feesAmount = 0.01 ether; address receiver = address(uint160(c++)); - + address user = address(uint160(c++)); uint32 feesChainSlug = arbChainSlug; SocketContracts feesConfig; @@ -19,74 +19,102 @@ contract FeesTest is DeliveryHelperTest { setUpDeliveryHelper(); feesConfig = getSocketConfig(feesChainSlug); - counterGateway = new CounterAppGateway(address(addressResolver), createFees(feesAmount)); - depositFees(address(counterGateway), createFees(depositAmount)); + counterGateway = new CounterAppGateway(address(addressResolver), feesAmount); + depositUSDCFees( + address(counterGateway), + OnChainFees({ + chainSlug: feesChainSlug, + token: address(feesConfig.feesTokenUSDC), + amount: depositAmount + }) + ); bytes32[] memory contractIds = new bytes32[](1); contractIds[0] = counterGateway.counter(); _deploy(feesChainSlug, IAppGateway(counterGateway), contractIds); } - function testDistributeFee() public { - uint256 initialFeesPlugBalance = address(feesConfig.feesPlug).balance; - - assertEq( - initialFeesPlugBalance, - address(feesConfig.feesPlug).balance, - "FeesPlug Balance should be correct" + function testWithdrawTransmitterFees() public { + uint256 initialFeesPlugBalance = feesConfig.feesTokenUSDC.balanceOf( + address(feesConfig.feesPlug) ); assertEq( initialFeesPlugBalance, - feesConfig.feesPlug.balanceOf(ETH_ADDRESS), - "FeesPlug balance of counterGateway should be correct" + feesConfig.feesTokenUSDC.balanceOf(address(feesConfig.feesPlug)), + "FeesPlug Balance should be correct" ); - uint256 transmitterReceiverBalanceBefore = address(receiver).balance; - - hoax(transmitterEOA); - uint40 requestCount = feesManager.withdrawTransmitterFees( + uint256 transmitterReceiverBalanceBefore = feesConfig.feesTokenUSDC.balanceOf(receiver); + uint256 withdrawAmount = feesManager.getMaxCreditsAvailableForWithdraw(transmitterEOA); + vm.startPrank(transmitterEOA); + uint40 requestCount = deliveryHelper.withdrawTransmitterFees( feesChainSlug, - ETH_ADDRESS, - address(receiver) + address(feesConfig.feesTokenUSDC), + address(receiver), + withdrawAmount ); + vm.stopPrank(); uint40[] memory batches = watcherPrecompile.getBatches(requestCount); _finalizeBatch(batches[0], new bytes[](0), 0, false); - assertEq( - transmitterReceiverBalanceBefore + bidAmount, - address(receiver).balance, + transmitterReceiverBalanceBefore + withdrawAmount, + feesConfig.feesTokenUSDC.balanceOf(receiver), "Transmitter Balance should be correct" ); assertEq( - initialFeesPlugBalance - bidAmount, - address(feesConfig.feesPlug).balance, + initialFeesPlugBalance - withdrawAmount, + feesConfig.feesTokenUSDC.balanceOf(address(feesConfig.feesPlug)), "FeesPlug Balance should be correct" ); } - function testWithdrawFeeTokens() public { - assertEq( - depositAmount, - feesConfig.feesPlug.balanceOf(ETH_ADDRESS), - "Balance should be correct" - ); - - uint256 receiverBalanceBefore = receiver.balance; + function testWithdrawFeeTokensAppGateway() public { + uint256 receiverBalanceBefore = feesConfig.feesTokenUSDC.balanceOf(receiver); uint256 withdrawAmount = 0.5 ether; - counterGateway.withdrawFeeTokens(feesChainSlug, ETH_ADDRESS, withdrawAmount, receiver); + counterGateway.withdrawFeeTokens( + feesChainSlug, + address(feesConfig.feesTokenUSDC), + withdrawAmount, + receiver + ); executeRequest(new bytes[](0)); assertEq( - depositAmount - withdrawAmount, - address(feesConfig.feesPlug).balance, - "Fees Balance should be correct" + receiverBalanceBefore + withdrawAmount, + feesConfig.feesTokenUSDC.balanceOf(receiver), + "Receiver Balance should be correct" + ); + } + + function testWithdrawFeeTokensUser() public { + depositUSDCFees( + user, + OnChainFees({ + chainSlug: feesChainSlug, + token: address(feesConfig.feesTokenUSDC), + amount: depositAmount + }) ); + uint256 receiverBalanceBefore = feesConfig.feesTokenUSDC.balanceOf(user); + uint256 withdrawAmount = 0.5 ether; + + vm.prank(user); + deliveryHelper.withdrawTo( + feesChainSlug, + address(feesConfig.feesTokenUSDC), + withdrawAmount, + user, + address(auctionManager), + maxFees + ); + executeRequest(new bytes[](0)); + assertEq( receiverBalanceBefore + withdrawAmount, - receiver.balance, + feesConfig.feesTokenUSDC.balanceOf(user), "Receiver Balance should be correct" ); } diff --git a/test/Inbox.t.sol b/test/Inbox.t.sol index cf3bbb40..45caa25f 100644 --- a/test/Inbox.t.sol +++ b/test/Inbox.t.sol @@ -26,7 +26,7 @@ contract TriggerTest is DeliveryHelperTest { counter = new Counter(); // Deploy the gateway with fees - gateway = new CounterAppGateway(address(addressResolver), createFees(feesAmount)); + gateway = new CounterAppGateway(address(addressResolver), feesAmount); gateway.setIsValidPlug(arbChainSlug, address(counter)); // Connect the counter to the gateway and socket @@ -59,7 +59,14 @@ contract TriggerTest is DeliveryHelperTest { function testIncrementAfterTrigger() public { // Initial counter value should be 0 assertEq(gateway.counterVal(), 0, "Initial gateway counter should be 0"); - + depositUSDCFees( + address(gateway), + OnChainFees({ + chainSlug: arbChainSlug, + token: address(arbConfig.feesTokenUSDC), + amount: 1 ether + }) + ); // Simulate a message from another chain through the watcher uint256 incrementValue = 5; bytes32 triggerId = _encodeTriggerId(address(arbConfig.socket), arbChainSlug); diff --git a/test/SetupTest.t.sol b/test/SetupTest.t.sol index 6efb5045..fbd05066 100644 --- a/test/SetupTest.t.sol +++ b/test/SetupTest.t.sol @@ -19,9 +19,10 @@ import {ContractFactoryPlug} from "../contracts/protocol/payload-delivery/Contra import {FeesPlug} from "../contracts/protocol/payload-delivery/FeesPlug.sol"; import {SocketFeeManager} from "../contracts/protocol/socket/SocketFeeManager.sol"; import {ETH_ADDRESS} from "../contracts/protocol/utils/common/Constants.sol"; -import {ResolvedPromises} from "../contracts/protocol/utils/common/Structs.sol"; +import {ResolvedPromises, OnChainFees} from "../contracts/protocol/utils/common/Structs.sol"; import "solady/utils/ERC1967Factory.sol"; +import "./apps/app-gateways/USDC.sol"; contract SetupTest is Test { using PayloadHeaderDecoder for bytes32; @@ -58,6 +59,7 @@ contract SetupTest is Test { SocketBatcher socketBatcher; ContractFactoryPlug contractFactoryPlug; FeesPlug feesPlug; + ERC20 feesTokenUSDC; } AddressResolver public addressResolver; @@ -85,10 +87,12 @@ contract SetupTest is Test { SocketBatcher socketBatcher = new SocketBatcher(owner, socket); FastSwitchboard switchboard = new FastSwitchboard(chainSlug_, socket, owner); + ERC20 feesTokenUSDC = new USDC("USDC", "USDC", 6, owner, 1000000000000000000000000); FeesPlug feesPlug = new FeesPlug(address(socket), owner); ContractFactoryPlug contractFactoryPlug = new ContractFactoryPlug(address(socket), owner); - vm.startPrank(owner); + // feePlug whitelist token + feesPlug.whitelistToken(address(feesTokenUSDC)); // socket socket.grantRole(GOVERNANCE_ROLE, address(owner)); @@ -116,7 +120,8 @@ contract SetupTest is Test { switchboard: switchboard, socketBatcher: socketBatcher, contractFactoryPlug: contractFactoryPlug, - feesPlug: feesPlug + feesPlug: feesPlug, + feesTokenUSDC: feesTokenUSDC }); } @@ -194,6 +199,11 @@ contract SetupTest is Test { vm.startPrank(watcherEOA); addressResolver.setWatcherPrecompile(address(watcherPrecompile)); + watcherPrecompileLimits.setCallBackFees(1); + watcherPrecompileLimits.setFinalizeFees(1); + watcherPrecompileLimits.setQueryFees(1); + watcherPrecompileLimits.setTimeoutFees(1); + vm.stopPrank(); } @@ -294,10 +304,6 @@ contract SetupTest is Test { return chainSlug_ == arbChainSlug ? arbConfig : optConfig; } - function createFees(uint256 maxFees_) internal view returns (Fees memory) { - return Fees({feePoolChain: arbChainSlug, feePoolToken: ETH_ADDRESS, amount: maxFees_}); - } - function _generateWatcherProof( PayloadParams memory params_ ) internal view returns (bytes memory, bytes32) { diff --git a/test/apps/Counter.t.sol b/test/apps/Counter.t.sol index cb9b8290..8980a314 100644 --- a/test/apps/Counter.t.sol +++ b/test/apps/Counter.t.sol @@ -16,8 +16,15 @@ contract CounterTest is DeliveryHelperTest { function deploySetup() internal { setUpDeliveryHelper(); - counterGateway = new CounterAppGateway(address(addressResolver), createFees(feesAmount)); - depositFees(address(counterGateway), createFees(1 ether)); + counterGateway = new CounterAppGateway(address(addressResolver), feesAmount); + depositUSDCFees( + address(counterGateway), + OnChainFees({ + chainSlug: arbChainSlug, + token: address(arbConfig.feesTokenUSDC), + amount: 1 ether + }) + ); counterId = counterGateway.counter(); contractIds[0] = counterId; diff --git a/test/apps/ParallelCounter.t.sol b/test/apps/ParallelCounter.t.sol index fdaa8ac4..5e7e193f 100644 --- a/test/apps/ParallelCounter.t.sol +++ b/test/apps/ParallelCounter.t.sol @@ -16,11 +16,15 @@ contract ParallelCounterTest is DeliveryHelperTest { function deploySetup() internal { setUpDeliveryHelper(); - parallelCounterGateway = new CounterAppGateway( - address(addressResolver), - createFees(feesAmount) + parallelCounterGateway = new CounterAppGateway(address(addressResolver), feesAmount); + depositUSDCFees( + address(parallelCounterGateway), + OnChainFees({ + chainSlug: arbChainSlug, + token: address(arbConfig.feesTokenUSDC), + amount: 1 ether + }) ); - depositFees(address(parallelCounterGateway), createFees(1 ether)); counterId1 = parallelCounterGateway.counter1(); counterId2 = parallelCounterGateway.counter(); contractIds[0] = counterId1; diff --git a/test/apps/SuperToken.t.sol b/test/apps/SuperToken.t.sol index f33ca720..bd5c88ab 100644 --- a/test/apps/SuperToken.t.sol +++ b/test/apps/SuperToken.t.sol @@ -65,7 +65,7 @@ contract SuperTokenTest is DeliveryHelperTest { SuperTokenAppGateway superTokenApp = new SuperTokenAppGateway( address(addressResolver), owner, - createFees(maxFees), + maxFees, SuperTokenAppGateway.ConstructorParams({ name_: "SUPER TOKEN", symbol_: "SUPER", @@ -76,7 +76,14 @@ contract SuperTokenTest is DeliveryHelperTest { ); // Enable app gateways to do all operations in the Watcher: Read, Write and Schedule on EVMx // Watcher sets the limits for apps in this SOCKET protocol version - depositFees(address(superTokenApp), createFees(1 ether)); + depositUSDCFees( + address(superTokenApp), + OnChainFees({ + chainSlug: arbChainSlug, + token: address(arbConfig.feesTokenUSDC), + amount: 1 ether + }) + ); appContracts = AppContracts({ superTokenApp: superTokenApp, diff --git a/test/apps/app-gateways/USDC.sol b/test/apps/app-gateways/USDC.sol new file mode 100644 index 00000000..df08d360 --- /dev/null +++ b/test/apps/app-gateways/USDC.sol @@ -0,0 +1,41 @@ +pragma solidity ^0.8.21; + +import "solady/tokens/ERC20.sol"; + +contract USDC is ERC20 { + string private _name; + string private _symbol; + uint8 private _decimals; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address initialSupplyHolder_, + uint256 initialSupply_ + ) { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + _mint(initialSupplyHolder_, initialSupply_); + } + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external { + _burn(from, amount); + } +} diff --git a/test/apps/app-gateways/counter/CounterAppGateway.sol b/test/apps/app-gateways/counter/CounterAppGateway.sol index a27b2db1..f5bca934 100644 --- a/test/apps/app-gateways/counter/CounterAppGateway.sol +++ b/test/apps/app-gateways/counter/CounterAppGateway.sol @@ -17,15 +17,15 @@ contract CounterAppGateway is AppGatewayBase, Ownable { uint256 public optCounter; event TimeoutResolved(uint256 creationTimestamp, uint256 executionTimestamp); - constructor(address addressResolver_, Fees memory fees_) AppGatewayBase(addressResolver_) { + constructor(address addressResolver_, uint256 fees_) AppGatewayBase(addressResolver_) { creationCodeWithArgs[counter] = abi.encodePacked(type(Counter).creationCode); creationCodeWithArgs[counter1] = abi.encodePacked(type(Counter).creationCode); - _setOverrides(fees_); + _setMaxFees(fees_); _initializeOwner(msg.sender); } // deploy contracts - function deployContracts(uint32 chainSlug_) external async { + function deployContracts(uint32 chainSlug_) external async(bytes("")) { _deploy(counter, chainSlug_, IsPlug.YES); } @@ -33,14 +33,14 @@ contract CounterAppGateway is AppGatewayBase, Ownable { _deploy(counter, chainSlug_, IsPlug.YES); } - function deployParallelContracts(uint32 chainSlug_) external async { + function deployParallelContracts(uint32 chainSlug_) external async(bytes("")) { _setOverrides(Parallel.ON); _deploy(counter, chainSlug_, IsPlug.YES); _deploy(counter1, chainSlug_, IsPlug.YES); _setOverrides(Parallel.OFF); } - function deployMultiChainContracts(uint32[] memory chainSlugs_) external async { + function deployMultiChainContracts(uint32[] memory chainSlugs_) external async(bytes("")) { _setOverrides(Parallel.ON); for (uint32 i = 0; i < chainSlugs_.length; i++) { _deploy(counter, chainSlugs_[i], IsPlug.YES); @@ -53,7 +53,7 @@ contract CounterAppGateway is AppGatewayBase, Ownable { return; } - function incrementCounters(address[] memory instances_) public async { + function incrementCounters(address[] memory instances_) public async(bytes("")) { // the increase function is called on given list of instances // this for (uint256 i = 0; i < instances_.length; i++) { @@ -69,7 +69,7 @@ contract CounterAppGateway is AppGatewayBase, Ownable { } } - function readCounters(address[] memory instances_) public async { + function readCounters(address[] memory instances_) public async(bytes("")) { // the increase function is called on given list of instances _setOverrides(Read.ON, Parallel.ON); for (uint256 i = 0; i < instances_.length; i++) { @@ -80,7 +80,7 @@ contract CounterAppGateway is AppGatewayBase, Ownable { _setOverrides(Read.OFF, Parallel.OFF); } - function readCounterAtBlock(address instance_, uint256 blockNumber_) public async { + function readCounterAtBlock(address instance_, uint256 blockNumber_) public async(bytes("")) { uint32 chainSlug = IForwarder(instance_).getChainSlug(); _setOverrides(Read.ON, Parallel.ON, blockNumber_); ICounter(instance_).getCounter(); @@ -120,8 +120,8 @@ contract CounterAppGateway is AppGatewayBase, Ownable { } // UTILS - function setFees(Fees memory fees_) public { - fees = fees_; + function setMaxFees(uint256 fees_) public { + maxFees = fees_; } function withdrawFeeTokens( @@ -133,12 +133,12 @@ contract CounterAppGateway is AppGatewayBase, Ownable { return _withdrawFeeTokens(chainSlug_, token_, amount_, receiver_); } - function testOnChainRevert(uint32 chainSlug) public async { + function testOnChainRevert(uint32 chainSlug) public async(bytes("")) { address instance = forwarderAddresses[counter][chainSlug]; ICounter(instance).wrongFunction(); } - function testCallBackRevert(uint32 chainSlug) public async { + function testCallBackRevert(uint32 chainSlug) public async(bytes("")) { // the increase function is called on given list of instances _setOverrides(Read.ON, Parallel.ON); address instance = forwarderAddresses[counter][chainSlug]; diff --git a/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol b/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol index 3110d497..fbaf6f15 100644 --- a/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol +++ b/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol @@ -29,7 +29,7 @@ contract SuperTokenAppGateway is AppGatewayBase, Ownable { constructor( address addressResolver_, address owner_, - Fees memory fees_, + uint256 fees_, ConstructorParams memory params_ ) AppGatewayBase(addressResolver_) { creationCodeWithArgs[superToken] = abi.encodePacked( @@ -45,11 +45,11 @@ contract SuperTokenAppGateway is AppGatewayBase, Ownable { // sets the fees data like max fees, chain and token for all transfers // they can be updated for each transfer as well - _setOverrides(fees_); + _setMaxFees(fees_); _initializeOwner(owner_); } - function deployContracts(uint32 chainSlug_) external async { + function deployContracts(uint32 chainSlug_) external async(bytes("")) { bytes memory initData = abi.encodeWithSelector(SuperToken.setOwner.selector, owner()); _deploy(superToken, chainSlug_, IsPlug.YES, initData); } @@ -60,7 +60,7 @@ contract SuperTokenAppGateway is AppGatewayBase, Ownable { return; } - function transfer(bytes memory order_) external async { + function transfer(bytes memory order_) external async(bytes("")) { TransferOrder memory order = abi.decode(order_, (TransferOrder)); ISuperToken(order.srcToken).burn(order.user, order.srcAmount); ISuperToken(order.dstToken).mint(order.user, order.srcAmount); diff --git a/test/mock/MockFastSwitchboard.sol b/test/mock/MockFastSwitchboard.sol index e831d02c..3341fab8 100644 --- a/test/mock/MockFastSwitchboard.sol +++ b/test/mock/MockFastSwitchboard.sol @@ -21,11 +21,11 @@ contract MockFastSwitchboard is ISwitchboard { owner = owner_; } - function attest(bytes32 , bytes calldata ) external { + function attest(bytes32, bytes calldata) external { // TODO: implement } - function allowPayload(bytes32 , bytes32) external pure returns (bool) { + function allowPayload(bytes32, bytes32) external pure returns (bool) { // digest has enough attestations return true; }