diff --git a/contracts/messaging/GasFeePricing.md b/contracts/messaging/GasFeePricing.md new file mode 100644 index 000000000..d1c23a36c --- /dev/null +++ b/contracts/messaging/GasFeePricing.md @@ -0,0 +1,63 @@ +# Gas Fee Pricing for Message Bridge + +## Setup + +On every chain `MessageBusUpgradeable` and `GasFeePricingUpgradeable` are deployed. `GasFeePricing` contracts are using `MessageBus` to communicate with each other, so they are set up as the "trusted remote" for one another. + +For every chain we're considering following information: + +- `gasTokenPrice`: price of chain's gas token (ETH, AVAX, BNB, ...) +- `gasUnitPrice`: price of a single gas unit (usually referred as chain's "gwei gas price") + +> Both values are supposed to reflect the latest "average" price. + +`GasFeePricing` contract is storing this information both for the local chain, as well as for all known remote chains. Whenever information for a **local chain** is updated on any of the `GasFeePricing` contracts, it sends messages to all the remote `GasFeePricing` contracts, so they can update it as well. + +This way, the information about chain's gas token/unit prices is synchronized across all chains. + +## Message Passing + +Any contract can interact with `MessageBus` to send a message to a remote chain, specifying both a gas airdrop and a gas limit on the remote chain. +Every remote chain has a different setting for the maximum amount of gas available for the airdrop. That way, the airdrop is fully flexible, and can not be taken advantage of by the bad actors. This value (for every remote chain) is also synchronized across all the chains. + +## Fee calculation + +### Basic calculation + +The fee is split into two parts. Both parts are quoted in the local gas token. + +- Fee for providing gas airdrop on a remote chain. Assuming dropping `gasDrop` worth of the remote gas token. + +```go +feeGasDrop = gasDrop * remoteGasTokenPrice / localGasTokenPrice +``` + +- Fee for executing message on a remote chain. Assuming providing `gasLimit` gas units on the remote chain. + +```go +feeGasUsage = max( + minRemoteFeeUsd / localGasTokenPrice, + gasLimit * remoteGasUnitPrice * remoteGasTokenPrice / localGasTokenPrice +) +``` + +> `minRemoteFeeUsd` is the minimum fee (in $), taken for the gas usage on a remote chain. It is specific to the remote chain, and does not take a local chain into account. `minRemoteFeeUsd` (for every remote chain) is also synchronized across all the chains. + +Note that both fees will express the _exact expected costs_ of delivering the message. + +### Monetizing the messaging. + +Introduce markup. Markup is a value of `0%`, or higher. Markup means how much more is `MessageBus` charging, compared to [expected costs](#basic-calculation). + +Markups are separate for the gas airdrop, and the gas usage. This means that the final fee formula is + +```go +fee = (100% + markupGasDrop) * feeGasDrop + (100% + markupGasUsage) * feeGasUsage +``` + +- Markups are specific to a "local chain - remote chain" pair. +- `markupGasUsage` should be set higher for the remote chains, known to have gas spikes (Ethereum, Fantom). +- `markupGasDrop` can be set to 0, when both local and remote chain are using the same gas token. + > Gas airdrop amount is limited, so it's not possible to bridge gas by sending an empty message with a huge airdrop. +- `markupGasDrop <= markupGasUsage` makes sense, as the price ratio for the gas usage is more volatile, since it's taking into account the gas unit price as well. +- `markupGasDrop` and `markupGasUsage` should be set higher for such a "local chain - remote chain" pair, where gas local token price is less correlated with the remote gas token price. diff --git a/contracts/messaging/GasFeePricingUpgradeable.sol b/contracts/messaging/GasFeePricingUpgradeable.sol new file mode 100644 index 000000000..508b4ec40 --- /dev/null +++ b/contracts/messaging/GasFeePricingUpgradeable.sol @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "./framework/SynMessagingReceiverUpgradeable.sol"; +import "./interfaces/IGasFeePricing.sol"; +import "./libraries/Options.sol"; +import "./libraries/GasFeePricingUpdates.sol"; + +contract GasFeePricingUpgradeable is SynMessagingReceiverUpgradeable { + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ STRUCTS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /** + * @notice Whenever the messaging fee is calculated, it takes into account things as: + * gas token prices on local and remote chain, gas limit for executing message on remote chain + * and gas unit price on remote chain. In other words, message sender is paying remote chain + * gas fees (to cover gas usage and gasdrop), but in local chain gas token. + * The price values are static, though are supposed to be updated in the event of high + * volatility. It is implied that gas token/unit prices reflect respective latest + * average prices. + * + * Because of this, the markups are used, both for "gas drop fee", and "gas usage fee". + * Markup is a value of 0% or higher. This is the coefficient applied to + * "projected gas fee", that is calculated using static gas token/unit prices. + * Markup of 0% means that exactly "projected gas fee" will be charged, markup of 50% + * will result in fee that is 50% higher than "projected", etc. + * + * There are separate markups for gasDrop and gasUsage. gasDropFee is calculated only using + * local and remote gas token prices, while gasUsageFee also takes into account remote chain gas + * unit price, which is an extra source of volatility. + * + * Generally, markupGasUsage >= markupGasDrop >= 0%. While markups can be set to 0%, + * this is not recommended. + */ + + /** + * @dev Chain's Config is supposed to be PARTLY synchronized cross-chain, i.e. + * GasFeePricing contracts on different chain will have the SAME values for + * the same remote chain: + * - gasDropMax: maximum gas airdrop available on chain + * uint112 => max value ~= 5 * 10**33 + * - gasUnitsRcvMsg: Amount of gas units needed for GasFeePricing contract + * to receive "update chain Config/Info" message + * uint80 => max value ~= 10**24 + * - minGasUsageFeeUsd: minimum amount of "gas usage" part of total messaging fee, + * when sending message to a given chain. + * Quoted in USD, multiplied by USD_DENOMINATOR + * uint32 => max value ~= 4 * 10**9 + * These are universal values, and they should be the same on all GasFeePricing + * contracts. + * ═══════════════════════════════════════════════════════════════════════════════════════ + * Some of the values, however, are set unique for every "local-remote" chain combination: + * - markupGasDrop: Markup for gas airdrop + * uint16 => max value = 65535 + * - markupGasUsage: Markup for gas usage + * uint16 => max value = 65535 + * These values depend on correlation between local and remote chains. For instance, + * if both chains have the same gas token (like ETH), markup for the gas drop + * can be set to 0, as gasDrop is limited, and the slight price difference between ETH + * on local and remote chain can not be taken advantage of. + * + * On the contrary, if local and remote gas tokens have proven to be not that correlated + * in terms of their price, higher markup is needed to compensate potential price spikes. + * + * ChainConfig is optimized to fit into one word of storage. + * ChainConfig is not supposed to be updated regularly (the values are more or less persistent). + */ + + struct ChainConfig { + /// @dev Values below are synchronized cross-chain + uint112 gasDropMax; + uint80 gasUnitsRcvMsg; + uint32 minGasUsageFeeUsd; + /// @dev Values below are local-chain specific + uint16 markupGasDrop; + uint16 markupGasUsage; + } + + /** + * @dev Chain's Info is supposed to be FULLY synchronized cross-chain, i.e. + * GasFeePricing contracts on different chain will have the SAME values for + * the same remote chain: + * - gasTokenPrice: Price of chain's gas token in USD, scaled to wei + * uint128 => max value ~= 3 * 10**38 + * - gasUnitPrice: Price of chain's 1 gas unit in wei + * uint128 => max value ~= 3 * 10**38 + * + * These are universal values, and they should be the same on all GasFeePricing + * contracts. + * + * ChainInfo is optimized to fit into one word of storage. + * ChainInfo is supposed to be updated regularly, as the chain's gas token or unit + * price changes drastically. + */ + struct ChainInfo { + /// @dev Values below are synchronized cross-chain + uint128 gasTokenPrice; + uint128 gasUnitPrice; + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ EVENTS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /// @dev see "Structs" docs + event ChainInfoUpdated(uint256 indexed chainId, uint256 gasTokenPrice, uint256 gasUnitPrice); + /// @dev see "Structs" docs + event MarkupsUpdated(uint256 indexed chainId, uint256 markupGasDrop, uint256 markupGasUsage); + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ REMOTE CHAINS STORAGE ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /// @dev remoteChainId => Info + mapping(uint256 => ChainInfo) public remoteInfo; + /// @dev remoteChainId => Config + mapping(uint256 => ChainConfig) public remoteConfig; + /// @dev list of all remote chain ids + uint256[] internal remoteChainIds; + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ SOURCE CHAIN STORAGE ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /// @dev See "Structs" docs + /// localConfig.markupGasDrop and localConfig.markupGasUsage values are not used + ChainConfig public localConfig; + ChainInfo public localInfo; + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ CONSTANTS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + uint256 public constant DEFAULT_GAS_LIMIT = 200000; + uint256 public constant MARKUP_DENOMINATOR = 100; + uint256 public constant USD_DENOMINATOR = 10000; + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ INITIALIZER ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function initialize(address _messageBus, uint256 _localGasTokenPrice) external initializer { + __Ownable_init_unchained(); + messageBus = _messageBus; + localInfo.gasTokenPrice = uint96(_localGasTokenPrice); + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ VIEWS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /// @notice Get the fee for sending a message to remote chain with given options + function estimateGasFee(uint256 _remoteChainId, bytes calldata _options) external view returns (uint256 fee) { + fee = _estimateGasFee(_remoteChainId, _options); + } + + /// @notice Get the fee for sending a message to a bunch of chains with given options + function estimateGasFees(uint256[] calldata _remoteChainIds, bytes[] calldata _options) + external + view + returns (uint256 fee) + { + require(_remoteChainIds.length == _options.length, "!arrays"); + for (uint256 i = 0; i < _remoteChainIds.length; ++i) { + fee = fee + _estimateGasFee(_remoteChainIds[i], _options[i]); + } + } + + /// @dev Extracts the gas information from options and calculates the messaging fee + function _estimateGasFee(uint256 _remoteChainId, bytes calldata _options) internal view returns (uint256 fee) { + uint256 gasAirdrop; + uint256 gasLimit; + if (_options.length != 0) { + (gasLimit, gasAirdrop, ) = Options.decode(_options); + } else { + gasLimit = DEFAULT_GAS_LIMIT; + } + fee = _estimateGasFee(_remoteChainId, gasAirdrop, gasLimit); + } + + /// @dev Returns a gas fee for sending a message to remote chain, given the amount of gas to airdrop, + /// and amount of gas units for message execution on remote chain. + function _estimateGasFee( + uint256 _chainId, + uint256 _gasAirdrop, + uint256 _gasLimit + ) internal view returns (uint256 fee) { + // Read config/info for destination (remote) chain + ChainConfig memory dstConfig = remoteConfig[_chainId]; + ChainInfo memory dstInfo = remoteInfo[_chainId]; + // Read info for source (local) chain + ChainInfo memory srcInfo = localInfo; + require(_gasAirdrop <= dstConfig.gasDropMax, "GasDrop higher than max"); + + // Calculate how much [gas airdrop] is worth in [local chain wei] + uint256 feeGasDrop = (_gasAirdrop * dstInfo.gasTokenPrice) / srcInfo.gasTokenPrice; + // Calculate how much [gas usage] is worth in [local chain wei] + uint256 feeGasUsage = (_gasLimit * dstInfo.gasUnitPrice * dstInfo.gasTokenPrice) / srcInfo.gasTokenPrice; + + // Sum up the fees multiplied by their respective markups + feeGasDrop = (feeGasDrop * (dstConfig.markupGasDrop + MARKUP_DENOMINATOR)) / MARKUP_DENOMINATOR; + feeGasUsage = (feeGasUsage * (dstConfig.markupGasUsage + MARKUP_DENOMINATOR)) / MARKUP_DENOMINATOR; + + // Calculate min fee (specific to destination chain) + // Multiply by 10**18 to convert to wei + // Multiply by 10**18 again, as gasTokenPrice is scaled by 10**18 + // Divide by USD_DENOMINATOR, as minGasUsageFeeUsd is scaled by USD_DENOMINATOR + uint256 minFee = (uint256(dstConfig.minGasUsageFeeUsd) * 10**36) / + (uint256(srcInfo.gasTokenPrice) * USD_DENOMINATOR); + if (feeGasUsage < minFee) feeGasUsage = minFee; + + fee = feeGasDrop + feeGasUsage; + } + + /// @notice Get total gas fee for calling updateChainInfo() + function estimateUpdateFees() external view returns (uint256 totalFee) { + (totalFee, ) = _estimateUpdateFees(); + } + + /// @dev Returns total gas fee for calling updateChainInfo(), as well as + /// fee for each remote chain. + function _estimateUpdateFees() internal view returns (uint256 totalFee, uint256[] memory fees) { + uint256[] memory _chainIds = remoteChainIds; + fees = new uint256[](_chainIds.length); + for (uint256 i = 0; i < _chainIds.length; ++i) { + uint256 chainId = _chainIds[i]; + uint256 gasLimit = remoteConfig[chainId].gasUnitsRcvMsg; + if (gasLimit == 0) gasLimit = DEFAULT_GAS_LIMIT; + + uint256 fee = _estimateGasFee(chainId, 0, gasLimit); + totalFee += fee; + fees[i] = fee; + } + } + + function _calculateMinGasUsageFee(uint256 _minFeeUsd, uint256 _gasTokenPrice) + internal + pure + returns (uint256 minFee) + { + minFee = (_minFeeUsd * 10**18) / _gasTokenPrice; + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ ONLY OWNER ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /// @dev Update config (gasLimit for sending messages to chain, max gas airdrop) for a bunch of chains. + function setRemoteConfig( + uint256[] memory _remoteChainId, + uint112[] memory _gasDropMax, + uint80[] memory _gasUnitsRcvMsg, + uint32[] memory _minGasUsageFeeUsd + ) external onlyOwner { + require( + _remoteChainId.length == _gasDropMax.length && + _remoteChainId.length == _gasUnitsRcvMsg.length && + _remoteChainId.length == _minGasUsageFeeUsd.length, + "!arrays" + ); + for (uint256 i = 0; i < _remoteChainId.length; ++i) { + _updateRemoteChainConfig(_remoteChainId[i], _gasDropMax[i], _gasUnitsRcvMsg[i], _minGasUsageFeeUsd[i]); + } + } + + /// @notice Update information about gas unit/token price for a bunch of chains. + /// Handy for initial setup. + function setRemoteInfo( + uint256[] memory _remoteChainId, + uint128[] memory _gasTokenPrice, + uint128[] memory _gasUnitPrice + ) external onlyOwner { + require( + _remoteChainId.length == _gasTokenPrice.length && _remoteChainId.length == _gasUnitPrice.length, + "!arrays" + ); + for (uint256 i = 0; i < _remoteChainId.length; ++i) { + _updateRemoteChainInfo(_remoteChainId[i], _gasTokenPrice[i], _gasUnitPrice[i]); + } + } + + /// @notice Sets markups (see "Structs" docs) for a bunch of chains. Markups are used for determining + /// how much fee to charge on top of "projected gas cost" of delivering the message. + function setRemoteMarkups( + uint256[] memory _remoteChainId, + uint16[] memory _markupGasDrop, + uint16[] memory _markupGasUsage + ) external onlyOwner { + require( + _remoteChainId.length == _markupGasDrop.length && _remoteChainId.length == _markupGasUsage.length, + "!arrays" + ); + for (uint256 i = 0; i < _remoteChainId.length; ++i) { + _updateMarkups(_remoteChainId[i], _markupGasDrop[i], _markupGasUsage[i]); + } + } + + /// @notice Update information about local chain config: + /// amount of gas needed to do _updateRemoteChainInfo() + /// and maximum airdrop available on this chain + function updateLocalConfig( + uint112 _gasDropMax, + uint80 _gasUnitsRcvMsg, + uint32 _minGasUsageFeeUsd + ) external payable onlyOwner { + require(_gasUnitsRcvMsg != 0, "Gas amount is not set"); + _sendUpdateMessages(GasFeePricingUpdates.encodeConfig(_gasDropMax, _gasUnitsRcvMsg, _minGasUsageFeeUsd)); + ChainConfig memory config = localConfig; + config.gasDropMax = _gasDropMax; + config.gasUnitsRcvMsg = _gasUnitsRcvMsg; + config.minGasUsageFeeUsd = _minGasUsageFeeUsd; + localConfig = config; + } + + /// @notice Update information about local chain gas token/unit price on all configured remote chains, + /// as well as on the local chain itself. + function updateLocalInfo(uint128 _gasTokenPrice, uint128 _gasUnitPrice) external payable onlyOwner { + /** + * @dev Some chains (i.e. Aurora) allow free transactions, + * so we're not checking gasUnitPrice for being zero. + * gasUnitPrice is never used as denominator, and there's + * a minimum fee for gas usage, so this can't be taken advantage of. + */ + require(_gasTokenPrice != 0, "Gas token price is not set"); + // send messages before updating the values, so that it's possible to use + // estimateUpdateFees() to calculate the needed fee for the update + _sendUpdateMessages(GasFeePricingUpdates.encodeInfo(_gasTokenPrice, _gasUnitPrice)); + _updateLocalChainInfo(_gasTokenPrice, _gasUnitPrice); + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ UPDATE STATE LOGIC ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /// @dev Updates information about local chain gas token/unit price. + function _updateLocalChainInfo(uint128 _gasTokenPrice, uint128 _gasUnitPrice) internal { + localInfo = ChainInfo({gasTokenPrice: _gasTokenPrice, gasUnitPrice: _gasUnitPrice}); + + // TODO: use context chainid here + emit ChainInfoUpdated(block.chainid, _gasTokenPrice, _gasUnitPrice); + } + + /// @dev Updates remote chain config: + /// Amount of gas needed to do _updateRemoteChainInfo() + /// Maximum airdrop available on this chain + function _updateRemoteChainConfig( + uint256 _remoteChainId, + uint112 _gasDropMax, + uint80 _gasUnitsRcvMsg, + uint32 _minGasUsageFeeUsd + ) internal { + require(_gasUnitsRcvMsg != 0, "Gas amount is not set"); + ChainConfig memory config = remoteConfig[_remoteChainId]; + config.gasDropMax = _gasDropMax; + config.gasUnitsRcvMsg = _gasUnitsRcvMsg; + config.minGasUsageFeeUsd = _minGasUsageFeeUsd; + remoteConfig[_remoteChainId] = config; + } + + /// @dev Updates information about remote chain gas token/unit price. + /// Remote chain ratios are updated as well. + function _updateRemoteChainInfo( + uint256 _remoteChainId, + uint128 _gasTokenPrice, + uint128 _gasUnitPrice + ) internal { + /** + * @dev Some chains (i.e. Aurora) allow free transactions, + * so we're not checking gasUnitPrice for being zero. + * gasUnitPrice is never used as denominator, and there's + * a minimum fee for gas usage, so this can't be taken advantage of. + */ + require(_gasTokenPrice != 0, "Remote gas token price is not set"); + uint256 _localGasTokenPrice = localInfo.gasTokenPrice; + require(_localGasTokenPrice != 0, "Local gas token price is not set"); + + if (remoteInfo[_remoteChainId].gasTokenPrice == 0) { + // store remote chainId only if it wasn't added already + remoteChainIds.push(_remoteChainId); + } + + remoteInfo[_remoteChainId] = ChainInfo({gasTokenPrice: _gasTokenPrice, gasUnitPrice: _gasUnitPrice}); + emit ChainInfoUpdated(_remoteChainId, _gasTokenPrice, _gasUnitPrice); + } + + /// @dev Updates the markups (see "Structs" docs). + /// Markup = 0% means exactly the "projected gas cost" will be charged. + function _updateMarkups( + uint256 _remoteChainId, + uint16 _markupGasDrop, + uint16 _markupGasUsage + ) internal { + ChainConfig memory config = remoteConfig[_remoteChainId]; + config.markupGasDrop = _markupGasDrop; + config.markupGasUsage = _markupGasUsage; + remoteConfig[_remoteChainId] = config; + emit MarkupsUpdated(_remoteChainId, _markupGasDrop, _markupGasUsage); + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ MESSAGING LOGIC ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + /// @dev Sends "something updated" messages to all registered remote chains + function _sendUpdateMessages(bytes memory _message) internal { + (uint256 totalFee, uint256[] memory fees) = _estimateUpdateFees(); + require(msg.value >= totalFee, "msg.value doesn't cover all the fees"); + + uint256[] memory chainIds = remoteChainIds; + bytes32[] memory receivers = new bytes32[](chainIds.length); + bytes[] memory options = new bytes[](chainIds.length); + + for (uint256 i = 0; i < chainIds.length; ++i) { + uint256 chainId = chainIds[i]; + uint256 gasLimit = remoteConfig[chainId].gasUnitsRcvMsg; + if (gasLimit == 0) gasLimit = DEFAULT_GAS_LIMIT; + + receivers[i] = trustedRemoteLookup[chainId]; + options[i] = Options.encode(gasLimit); + } + + _send(receivers, chainIds, _message, options, fees, payable(msg.sender)); + if (msg.value > totalFee) payable(msg.sender).transfer(msg.value - totalFee); + } + + /// @dev Handles the received message. + function _handleMessage( + bytes32, + uint256 _localChainId, + bytes memory _message, + address + ) internal override { + uint8 msgType = GasFeePricingUpdates.messageType(_message); + if (msgType == uint8(GasFeePricingUpdates.MsgType.UPDATE_CONFIG)) { + (uint112 gasDropMax, uint80 gasUnitsRcvMsg, uint32 minGasUsageFeeUsd) = GasFeePricingUpdates.decodeConfig( + _message + ); + _updateRemoteChainConfig(_localChainId, gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + } else if (msgType == uint8(GasFeePricingUpdates.MsgType.UPDATE_INFO)) { + (uint128 gasTokenPrice, uint128 gasUnitPrice) = GasFeePricingUpdates.decodeInfo(_message); + _updateRemoteChainInfo(_localChainId, gasTokenPrice, gasUnitPrice); + } else { + revert("Unknown message type"); + } + } +} diff --git a/contracts/messaging/MessageBusSenderUpgradeable.sol b/contracts/messaging/MessageBusSenderUpgradeable.sol index 9418e45bb..8cde074ef 100644 --- a/contracts/messaging/MessageBusSenderUpgradeable.sol +++ b/contracts/messaging/MessageBusSenderUpgradeable.sol @@ -45,7 +45,7 @@ contract MessageBusSenderUpgradeable is OwnableUpgradeable, PausableUpgradeable return keccak256(abi.encode(_srcAddress, _srcChainId, _dstAddress, _dstChainId, _srcNonce, _message)); } - function estimateFee(uint256 _dstChainId, bytes calldata _options) public returns (uint256) { + function estimateFee(uint256 _dstChainId, bytes calldata _options) public view returns (uint256) { uint256 fee = IGasFeePricing(gasFeePricing).estimateGasFee(_dstChainId, _options); require(fee != 0, "Fee not set"); return fee; diff --git a/contracts/messaging/framework/SynMessagingReceiverUpgradeable.sol b/contracts/messaging/framework/SynMessagingReceiverUpgradeable.sol index 0800c5cb9..90f59df34 100644 --- a/contracts/messaging/framework/SynMessagingReceiverUpgradeable.sol +++ b/contracts/messaging/framework/SynMessagingReceiverUpgradeable.sol @@ -12,7 +12,7 @@ abstract contract SynMessagingReceiverUpgradeable is ISynMessagingReceiver, Owna // Maps chain ID to the bytes32 trusted addresses allowed to be source senders mapping(uint256 => bytes32) internal trustedRemoteLookup; - event SetTrustedRemote(uint256 _srcChainId, bytes32 _srcAddress); + event SetTrustedRemote(uint256 indexed _srcChainId, bytes32 _srcAddress); /** * @notice Executes a message called by MessageBus (MessageBusReceiver) @@ -44,15 +44,17 @@ abstract contract SynMessagingReceiverUpgradeable is ISynMessagingReceiver, Owna address _executor ) internal virtual; + // Send a message using full msg.value as fees, refund extra to msg.sender function _send( bytes32 _receiver, uint256 _dstChainId, bytes memory _message, bytes memory _options ) internal virtual { - _send(_receiver, _dstChainId, _message, _options, payable(msg.sender)); + _send(_receiver, _dstChainId, _message, _options, msg.value, payable(msg.sender)); } + // Send a message using full msg.value as fees, refund extra to specified address function _send( bytes32 _receiver, uint256 _dstChainId, @@ -60,14 +62,58 @@ abstract contract SynMessagingReceiverUpgradeable is ISynMessagingReceiver, Owna bytes memory _options, address payable _refundAddress ) internal virtual { - require(trustedRemoteLookup[_dstChainId] != bytes32(0), "Receiver not trusted remote"); - IMessageBus(messageBus).sendMessage{value: msg.value}( - _receiver, - _dstChainId, - _message, - _options, - _refundAddress + _send(_receiver, _dstChainId, _message, _options, msg.value, _refundAddress); + } + + // Send a message to a bunch of chains, refund extra fees to specified address + function _send( + bytes32[] memory _receivers, + uint256[] memory _dstChainIds, + bytes memory _message, + bytes[] memory _options, + uint256[] memory _fees, + address payable _refundAddress + ) internal { + require( + _receivers.length == _fees.length && _dstChainIds.length == _fees.length && _options.length == _fees.length, + "!arrays" ); + for (uint256 i = 0; i < _fees.length; ++i) { + _send(_receivers[i], _dstChainIds[i], _message, _options[i], _fees[i], _refundAddress); + } + } + + // Send a bunch of messages to a bunch of chains, refund extra fees to specified address + function _send( + bytes32[] memory _receivers, + uint256[] memory _dstChainIds, + bytes[] memory _messages, + bytes[] memory _options, + uint256[] memory _fees, + address payable _refundAddress + ) internal { + require( + _receivers.length == _fees.length && + _dstChainIds.length == _fees.length && + _messages.length == _fees.length && + _options.length == _fees.length, + "!arrays" + ); + for (uint256 i = 0; i < _fees.length; ++i) { + _send(_receivers[i], _dstChainIds[i], _messages[i], _options[i], _fees[i], _refundAddress); + } + } + + function _send( + bytes32 _receiver, + uint256 _dstChainId, + bytes memory _message, + bytes memory _options, + uint256 _fee, + address payable _refundAddress + ) internal { + require(trustedRemoteLookup[_dstChainId] != bytes32(0), "Receiver not trusted remote"); + IMessageBus(messageBus).sendMessage{value: _fee}(_receiver, _dstChainId, _message, _options, _refundAddress); } //** Config Functions */ @@ -77,6 +123,17 @@ abstract contract SynMessagingReceiverUpgradeable is ISynMessagingReceiver, Owna // allow owner to set trusted addresses allowed to be source senders function setTrustedRemote(uint256 _srcChainId, bytes32 _srcAddress) external onlyOwner { + _setTrustedRemote(_srcChainId, _srcAddress); + } + + function setTrustedRemotes(uint256[] memory _srcChainIds, bytes32[] memory _srcAddresses) external onlyOwner { + require(_srcChainIds.length == _srcAddresses.length, "!arrays"); + for (uint256 i = 0; i < _srcChainIds.length; ++i) { + _setTrustedRemote(_srcChainIds[i], _srcAddresses[i]); + } + } + + function _setTrustedRemote(uint256 _srcChainId, bytes32 _srcAddress) internal { trustedRemoteLookup[_srcChainId] = _srcAddress; emit SetTrustedRemote(_srcChainId, _srcAddress); } diff --git a/contracts/messaging/interfaces/IGasFeePricing.sol b/contracts/messaging/interfaces/IGasFeePricing.sol index 27131a31b..9a071c1f1 100644 --- a/contracts/messaging/interfaces/IGasFeePricing.sol +++ b/contracts/messaging/interfaces/IGasFeePricing.sol @@ -24,5 +24,5 @@ interface IGasFeePricing { * @notice Returns srcGasToken fee to charge in wei for the cross-chain message based on the gas limit * @param _options Versioned struct used to instruct relayer on how to proceed with gas limits. Contains data on gas limit to submit tx with. */ - function estimateGasFee(uint256 _dstChainId, bytes calldata _options) external returns (uint256); + function estimateGasFee(uint256 _dstChainId, bytes calldata _options) external view returns (uint256); } diff --git a/contracts/messaging/libraries/GasFeePricingUpdates.sol b/contracts/messaging/libraries/GasFeePricingUpdates.sol new file mode 100644 index 000000000..5eb9a1359 --- /dev/null +++ b/contracts/messaging/libraries/GasFeePricingUpdates.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +library GasFeePricingUpdates { + enum MsgType { + UNKNOWN, + UPDATE_CONFIG, + UPDATE_INFO + } + + function encodeConfig( + uint112 _gasDropMax, + uint80 _gasUnitsRcvMsg, + uint32 _minGasUsageFeeUsd + ) internal pure returns (bytes memory) { + return abi.encodePacked(MsgType.UPDATE_CONFIG, _gasDropMax, _gasUnitsRcvMsg, _minGasUsageFeeUsd); + } + + function encodeInfo(uint128 _gasTokenPrice, uint128 _gasUnitPrice) internal pure returns (bytes memory) { + return abi.encodePacked(MsgType.UPDATE_INFO, _gasTokenPrice, _gasUnitPrice); + } + + function decodeConfig(bytes memory _message) + internal + pure + returns ( + uint112 gasDropMax, + uint80 gasUnitsRcvMsg, + uint32 minGasUsageFeeUsd + ) + { + // message: (uint8, uint112, uint80, uint32) + // length: (1, 14, 10, 4) + // offset: (1, 15, 25, 29) + require(_message.length == 29, "Wrong message length"); + uint8 msgType; + // solhint-disable-next-line + assembly { + msgType := mload(add(_message, 1)) + gasDropMax := mload(add(_message, 15)) + gasUnitsRcvMsg := mload(add(_message, 25)) + minGasUsageFeeUsd := mload(add(_message, 29)) + } + require(msgType == uint8(MsgType.UPDATE_CONFIG), "Wrong msgType"); + } + + function decodeInfo(bytes memory _message) internal pure returns (uint128 gasTokenPrice, uint128 gasUnitPrice) { + // message: (uint8, uint128, uint128) + // length: (1, 16, 16) + // offset: (1, 17, 33) + require(_message.length == 33, "Wrong message length"); + uint8 msgType; + // solhint-disable-next-line + assembly { + msgType := mload(add(_message, 1)) + gasTokenPrice := mload(add(_message, 17)) + gasUnitPrice := mload(add(_message, 33)) + } + require(msgType == uint8(MsgType.UPDATE_INFO), "Wrong msgType"); + } + + function messageType(bytes memory _message) internal pure returns (uint8 msgType) { + // solhint-disable-next-line + assembly { + msgType := mload(add(_message, 1)) + } + } +} diff --git a/contracts/messaging/libraries/Options.sol b/contracts/messaging/libraries/Options.sol new file mode 100644 index 000000000..913abfd2c --- /dev/null +++ b/contracts/messaging/libraries/Options.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +library Options { + enum TxType { + UNKNOWN, + DEFAULT, + GASDROP + } + + function encode(uint256 _gasLimit) internal pure returns (bytes memory) { + return abi.encodePacked(uint16(TxType.DEFAULT), _gasLimit); + } + + function encode( + uint256 _gasLimit, + uint256 _gasDropAmount, + bytes32 _dstReceiver + ) internal pure returns (bytes memory) { + return abi.encodePacked(uint16(TxType.GASDROP), _gasLimit, _gasDropAmount, _dstReceiver); + } + + function decode(bytes memory _options) + internal + pure + returns ( + uint256 gasLimit, + uint256 gasDropAmount, + bytes32 dstReceiver + ) + { + require(_options.length == 2 + 32 || _options.length == 2 + 32 * 3, "Wrong _options size"); + uint16 txType; + // solhint-disable-next-line + assembly { + txType := mload(add(_options, 2)) + gasLimit := mload(add(_options, 34)) + } + + if (txType == uint16(TxType.GASDROP)) { + // solhint-disable-next-line + assembly { + gasDropAmount := mload(add(_options, 66)) + dstReceiver := mload(add(_options, 98)) + } + require(gasDropAmount != 0, "gasDropAmount empty"); + require(dstReceiver != bytes32(0), "dstReceiver empty"); + } + } +} diff --git a/remappings.txt b/remappings.txt index b7ab34b6d..bf33463ad 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,4 @@ ds-test=lib/ds-test/src -forge-std=lib/forge-std/src \ No newline at end of file +forge-std=lib/forge-std/src + +src-messaging=contracts/messaging \ No newline at end of file diff --git a/test/dfk/HeroBridgeTest.sol b/test/dfk/HeroBridgeTest.sol index e6753bec4..42538765d 100644 --- a/test/dfk/HeroBridgeTest.sol +++ b/test/dfk/HeroBridgeTest.sol @@ -3,19 +3,19 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; import {Utilities} from "../utils/Utilities.sol"; -import "../../contracts/messaging/dfk/types/HeroTypes.sol"; - -import "../../contracts/messaging/dfk/bridge/HeroBridgeUpgradeable.sol"; -import "../../contracts/messaging/dfk/random/RandomGenerator.sol"; -import "../../contracts/messaging/dfk/auctions/AssistingAuctionUpgradeable.sol"; -import "../../contracts/messaging/dfk/StatScienceUpgradeable.sol"; -import "../../contracts/messaging/dfk/HeroCoreUpgradeable.sol"; - -import "../../contracts/messaging/MessageBusUpgradeable.sol"; -import "../../contracts/messaging/GasFeePricing.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; -import "../../contracts/messaging/apps/PingPong.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; +import "src-messaging/dfk/types/HeroTypes.sol"; + +import "src-messaging/dfk/bridge/HeroBridgeUpgradeable.sol"; +import "src-messaging/dfk/random/RandomGenerator.sol"; +import "src-messaging/dfk/auctions/AssistingAuctionUpgradeable.sol"; +import "src-messaging/dfk/StatScienceUpgradeable.sol"; +import "src-messaging/dfk/HeroCoreUpgradeable.sol"; + +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/GasFeePricing.sol"; +import "src-messaging/AuthVerifier.sol"; +import "src-messaging/apps/PingPong.sol"; +import "src-messaging/AuthVerifier.sol"; contract HeroBridgeUpgradeableTest is Test { Utilities internal utils; diff --git a/test/dfk/HeroEncoding.t.sol b/test/dfk/HeroEncoding.t.sol index bf2da81d2..087e0e5bb 100644 --- a/test/dfk/HeroEncoding.t.sol +++ b/test/dfk/HeroEncoding.t.sol @@ -1,7 +1,7 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; -import "../../contracts/messaging/dfk/types/HeroTypes.sol"; +import "src-messaging/dfk/types/HeroTypes.sol"; contract HeroEncodingTest is Test { function testHeroStruct() public { diff --git a/test/dfk/TearBridge.t.sol b/test/dfk/TearBridge.t.sol index a9e008d1f..1e0de8e57 100644 --- a/test/dfk/TearBridge.t.sol +++ b/test/dfk/TearBridge.t.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; import {Utilities} from "../utils/Utilities.sol"; -import "../../contracts/messaging/dfk/bridge/TearBridge.sol"; -import "../../contracts/messaging/dfk/inventory/GaiaTears.sol"; +import "src-messaging/dfk/bridge/TearBridge.sol"; +import "src-messaging/dfk/inventory/GaiaTears.sol"; -import "../../contracts/messaging/MessageBusUpgradeable.sol"; -import "../../contracts/messaging/GasFeePricing.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/GasFeePricing.sol"; +import "src-messaging/AuthVerifier.sol"; contract TearBridgeTest is Test { Utilities internal utils; diff --git a/test/messaging/AuthVerifier.t.sol b/test/messaging/AuthVerifier.t.sol index 8fb86af04..4e21ea4cc 100644 --- a/test/messaging/AuthVerifier.t.sol +++ b/test/messaging/AuthVerifier.t.sol @@ -1,7 +1,7 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; +import "src-messaging/AuthVerifier.sol"; contract AuthVerifierTest is Test { AuthVerifier public authVerifier; diff --git a/test/messaging/BatchMessageSender.t.sol b/test/messaging/BatchMessageSender.t.sol index e8ab1bc11..a73d7f75b 100644 --- a/test/messaging/BatchMessageSender.t.sol +++ b/test/messaging/BatchMessageSender.t.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; import {Utilities} from "../utils/Utilities.sol"; -import "../../contracts/messaging/MessageBusUpgradeable.sol"; -import "../../contracts/messaging/GasFeePricing.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; -import "../../contracts/messaging/apps/BatchMessageSender.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/GasFeePricing.sol"; +import "src-messaging/AuthVerifier.sol"; +import "src-messaging/apps/BatchMessageSender.sol"; +import "src-messaging/AuthVerifier.sol"; contract BatchMessageSenderTest is Test { Utilities internal utils; diff --git a/test/messaging/GasFeePricing.t.sol b/test/messaging/GasFeePricing.t.sol index 22216215b..fb7ac9279 100644 --- a/test/messaging/GasFeePricing.t.sol +++ b/test/messaging/GasFeePricing.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; -import "../../contracts/messaging/GasFeePricing.sol"; +import "src-messaging/GasFeePricing.sol"; contract GasFeePricingTest is Test { GasFeePricing public gasFeePricing; diff --git a/test/messaging/MessageBusReceiverUpgradeable.t.sol b/test/messaging/MessageBusReceiverUpgradeable.t.sol index 21b4a3ccb..f86e14d71 100644 --- a/test/messaging/MessageBusReceiverUpgradeable.t.sol +++ b/test/messaging/MessageBusReceiverUpgradeable.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; -import "../../contracts/messaging/MessageBusUpgradeable.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/AuthVerifier.sol"; import "@openzeppelin/contracts-4.5.0/proxy/transparent/TransparentUpgradeableProxy.sol"; diff --git a/test/messaging/MessageBusSenderUpgradeable.t.sol b/test/messaging/MessageBusSenderUpgradeable.t.sol index 8f4181e0c..2c99cb55d 100644 --- a/test/messaging/MessageBusSenderUpgradeable.t.sol +++ b/test/messaging/MessageBusSenderUpgradeable.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; -import "../../contracts/messaging/MessageBusUpgradeable.sol"; -import "../../contracts/messaging/GasFeePricing.sol"; +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/GasFeePricing.sol"; import "./GasFeePricing.t.sol"; diff --git a/test/messaging/MessageBusUpgradeable.t.sol b/test/messaging/MessageBusUpgradeable.t.sol index 5a87b4825..129bba6ce 100644 --- a/test/messaging/MessageBusUpgradeable.t.sol +++ b/test/messaging/MessageBusUpgradeable.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; -import "../../contracts/messaging/MessageBusUpgradeable.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/AuthVerifier.sol"; import "@openzeppelin/contracts-4.5.0/proxy/transparent/TransparentUpgradeableProxy.sol"; diff --git a/test/messaging/PingPong.t.sol b/test/messaging/PingPong.t.sol index 2957e4d18..51f9db42c 100644 --- a/test/messaging/PingPong.t.sol +++ b/test/messaging/PingPong.t.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.13; import "forge-std/Test.sol"; import {Utilities} from "../utils/Utilities.sol"; -import "../../contracts/messaging/MessageBusUpgradeable.sol"; -import "../../contracts/messaging/GasFeePricing.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; -import "../../contracts/messaging/apps/PingPong.sol"; -import "../../contracts/messaging/AuthVerifier.sol"; +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/GasFeePricing.sol"; +import "src-messaging/AuthVerifier.sol"; +import "src-messaging/apps/PingPong.sol"; +import "src-messaging/AuthVerifier.sol"; contract PingPongTest is Test { Utilities internal utils; diff --git a/test/messaging/gfp/GasFeePricingMessaging.t.sol b/test/messaging/gfp/GasFeePricingMessaging.t.sol new file mode 100644 index 000000000..0b598d3f3 --- /dev/null +++ b/test/messaging/gfp/GasFeePricingMessaging.t.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "./GasFeePricingSetup.t.sol"; +import "src-messaging/libraries/Options.sol"; + +contract GasFeePricingUpgradeableMessagingTest is GasFeePricingSetup { + event MessageSent( + address indexed sender, + uint256 srcChainID, + bytes32 receiver, + uint256 indexed dstChainId, + bytes message, + uint64 nonce, + bytes options, + uint256 fee, + bytes32 indexed messageId + ); + + // set this to true to do fee refund test + bool internal allowFeeRefund; + + receive() external payable override { + // making sure that all fee calculations are correct + // i.e. there are no fee refunds + if (!allowFeeRefund) revert("Received ether"); + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ ENCODING TESTS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function testEncodeConfig( + uint112 newValueA, + uint80 newValueB, + uint32 newValueC + ) public { + bytes memory message = GasFeePricingUpdates.encodeConfig(newValueA, newValueB, newValueC); + uint8 _msgType = GasFeePricingUpdates.messageType(message); + (uint112 _newValueA, uint80 _newValueB, uint32 _newValueC) = GasFeePricingUpdates.decodeConfig(message); + assertEq(_msgType, uint8(GasFeePricingUpdates.MsgType.UPDATE_CONFIG), "Failed to encode msgType"); + assertEq(_newValueA, newValueA, "Failed to encode newValueA"); + assertEq(_newValueB, newValueB, "Failed to encode newValueB"); + assertEq(_newValueC, newValueC, "Failed to encode newValueC"); + } + + function testEncodeInfo(uint128 newValueA, uint128 newValueB) public { + bytes memory message = GasFeePricingUpdates.encodeInfo(newValueA, newValueB); + uint8 _msgType = GasFeePricingUpdates.messageType(message); + (uint128 _newValueA, uint128 _newValueB) = GasFeePricingUpdates.decodeInfo(message); + assertEq(_msgType, uint8(GasFeePricingUpdates.MsgType.UPDATE_INFO), "Failed to encode msgType"); + assertEq(_newValueA, newValueA, "Failed to encode newValueA"); + assertEq(_newValueB, newValueB, "Failed to encode newValueB"); + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ MESSAGING TESTS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function testMinGasUsageFee() public { + uint256 chainId = remoteChainIds[0]; + uint128 gasUnitPrice = 10 * 10**9; + uint128 gasTokenPrice = uint128(localVars.gasTokenPrice * 1000); + // min fee = $2 + uint32 minGasUsageFeeUsd = 20000; + _setupSingleChain(chainId, 0, minGasUsageFeeUsd, gasTokenPrice, gasUnitPrice); + + // This gasLimit will result in gasUsage fee exactly $2 + uint256 gasLimit = 200000; + uint256 expectedFee = 2 * 10**18; + + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(gasLimit / 2)), + expectedFee, + "Wrong fee for 100,000 gas" + ); + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(gasLimit - 1)), + expectedFee, + "Wrong fee for 199,999 gas" + ); + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(gasLimit)), + expectedFee, + "Wrong fee for 200,000 gas" + ); + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(gasLimit + 1)), + (expectedFee * (gasLimit + 1)) / gasLimit, + "Wrong fee for 200,001 gas" + ); + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(gasLimit * 2)), + expectedFee * 2, + "Wrong fee for 400,000 gas" + ); + } + + function testMarkupGasDrop() public { + uint256 chainId = remoteChainIds[0]; + // set to 0, so that estimateFee would be only the airdrop cost + uint128 gasUnitPrice = 0; + uint128 gasTokenPrice = uint128(localVars.gasTokenPrice * 5); + uint32 minGasUsageFeeUsd = 0; + uint112 gasDropMax = 10**20; + + uint16 markupGasDrop = 69; + uint16 markupGasUsage = 100; + + _setupSingleChain(chainId, gasDropMax, minGasUsageFeeUsd, gasTokenPrice, gasUnitPrice); + _setupSingleChainMarkups(chainId, markupGasDrop, markupGasUsage); + + bytes32 receiver = keccak256("receiver"); + + // (2 * 10**18) remoteGas = (20 * 10**17) remoteGas = (100 * 10**17) localGas; + // +69% -> (169 * 10**17) localGas + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(0, 2 * 10**18, receiver)), + 169 * 10**17, + "Wrong markup for 2 * 10**18 gasDrop" + ); + + // 2 remoteGas = 10 localGas; +69% = 16 (rounded down) + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(0, 2, receiver)), + 16, + "Wrong markup for 2 gasDrop" + ); + } + + function testMarkupGasUsage() public { + uint256 chainId = remoteChainIds[0]; + uint128 gasUnitPrice = 2 * 10**9; + uint128 gasTokenPrice = uint128(localVars.gasTokenPrice * 5); + // set to 0, to check the markup being applied + uint32 minGasUsageFeeUsd = 0; + uint112 gasDropMax = 0; + + uint16 markupGasDrop = 42; + uint16 markupGasUsage = 69; + + _setupSingleChain(chainId, gasDropMax, minGasUsageFeeUsd, gasTokenPrice, gasUnitPrice); + _setupSingleChainMarkups(chainId, markupGasDrop, markupGasUsage); + + // (10**6 gasLimit) => (2 * 10**15 remoteGas cost) => (10**16 localGas) + // +69% -> 1.69 * 10**16 = 169 * 10**14 + assertEq( + gasFeePricing.estimateGasFee(chainId, Options.encode(10**6)), + 169 * 10**14, + "Wrong markup for 10**6 gasLimit" + ); + } + + function testSendUpdateConfig( + uint112 _gasDropMax, + uint80 _gasUnitsRcvMsg, + uint32 _minGasUsageFeeUsd + ) public { + vm.assume(_gasUnitsRcvMsg != 0); + _prepareSendingTests(); + uint256 totalFee = gasFeePricing.estimateUpdateFees(); + bytes memory message = GasFeePricingUpdates.encodeConfig(_gasDropMax, _gasUnitsRcvMsg, _minGasUsageFeeUsd); + + _expectMessagingEmits(message); + // receive() is disabled, so this will also check if the totalFee is exactly the needed fee + gasFeePricing.updateLocalConfig{value: totalFee}(_gasDropMax, _gasUnitsRcvMsg, _minGasUsageFeeUsd); + } + + function testSendUpdateInfo(uint128 _gasTokenPrice, uint128 _gasUnitPrice) public { + vm.assume(_gasTokenPrice != 0); + _prepareSendingTests(); + uint256 totalFee = gasFeePricing.estimateUpdateFees(); + bytes memory message = GasFeePricingUpdates.encodeInfo(_gasTokenPrice, _gasUnitPrice); + + _expectMessagingEmits(message); + // receive() is disabled, so this will also check if the totalFee is exactly the needed fee + gasFeePricing.updateLocalInfo{value: totalFee}(_gasTokenPrice, _gasUnitPrice); + } + + function testRcvUpdateConfig( + uint8 _chainIndex, + uint112 _gasDropMax, + uint80 _gasUnitsRcvMsg, + uint32 _minGasUsageFeeUsd + ) public { + vm.assume(_gasUnitsRcvMsg != 0); + uint256 chainId = remoteChainIds[_chainIndex % TEST_CHAINS]; + bytes32 messageId = utils.getNextKappa(); + bytes32 srcAddress = utils.addressToBytes32(remoteVars[chainId].gasFeePricing); + + bytes memory message = GasFeePricingUpdates.encodeConfig(_gasDropMax, _gasUnitsRcvMsg, _minGasUsageFeeUsd); + hoax(NODE); + messageBus.executeMessage(chainId, srcAddress, address(gasFeePricing), 100000, 0, message, messageId); + + remoteVars[chainId].gasDropMax = _gasDropMax; + remoteVars[chainId].gasUnitsRcvMsg = _gasUnitsRcvMsg; + remoteVars[chainId].minGasUsageFeeUsd = _minGasUsageFeeUsd; + _checkRemoteConfig(chainId); + } + + function testRcvUpdateInfo( + uint8 _chainIndex, + uint128 _gasTokenPrice, + uint128 _gasUnitPrice + ) public { + vm.assume(_gasTokenPrice != 0); + uint256 chainId = remoteChainIds[_chainIndex % TEST_CHAINS]; + bytes32 messageId = utils.getNextKappa(); + bytes32 srcAddress = utils.addressToBytes32(remoteVars[chainId].gasFeePricing); + + bytes memory message = GasFeePricingUpdates.encodeInfo(_gasTokenPrice, _gasUnitPrice); + hoax(NODE); + messageBus.executeMessage(chainId, srcAddress, address(gasFeePricing), 100000, 0, message, messageId); + + remoteVars[chainId].gasTokenPrice = _gasTokenPrice; + remoteVars[chainId].gasUnitPrice = _gasUnitPrice; + _checkRemoteInfo(chainId); + } + + function testGasDropMaxSucceeds() public { + uint112 gasDropMax = 10**18; + _testGasDrop(gasDropMax, gasDropMax); + } + + function testGasDropTooBigReverts() public { + uint112 gasDropMax = 10**18; + _testGasDrop(gasDropMax + 1, gasDropMax); + } + + function _testGasDrop(uint112 gasDropAmount, uint112 gasDropMax) internal { + uint256 chainId = remoteChainIds[0]; + uint256 gasLimit = 100000; + uint128 gasUnitPrice = 10**9; + _setupSingleChain(chainId, gasDropMax, 0, uint128(localVars.gasTokenPrice), gasUnitPrice); + uint256 fee = gasDropAmount + gasLimit * gasUnitPrice; + + bytes32 receiver = keccak256("Not a fake address"); + + bytes memory options = Options.encode(100000, gasDropAmount, receiver); + + if (gasDropAmount > gasDropMax) { + vm.expectRevert("GasDrop higher than max"); + } + + messageBus.sendMessage{value: fee}(receiver, chainId, bytes(""), options); + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ INTERNAL HELPERS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function _setupSingleChain( + uint256 _chainId, + uint112 _gasDropMax, + uint32 _minGasUsageFeeUsd, + uint128 _gasTokenPrice, + uint128 _gasUnitPrice + ) internal { + uint256[] memory chainIds = new uint256[](1); + uint112[] memory gasDropMax = new uint112[](1); + uint80[] memory gasUnitsRcvMsg = new uint80[](1); + uint32[] memory minGasUsageFeeUsd = new uint32[](1); + + chainIds[0] = _chainId; + gasDropMax[0] = _gasDropMax; + gasUnitsRcvMsg[0] = 100000; + minGasUsageFeeUsd[0] = _minGasUsageFeeUsd; + _setRemoteConfig(chainIds, gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + + uint128[] memory gasTokenPrice = new uint128[](1); + uint128[] memory gasUnitPrice = new uint128[](1); + gasTokenPrice[0] = _gasTokenPrice; + gasUnitPrice[0] = _gasUnitPrice; + _setRemoteInfo(chainIds, gasTokenPrice, gasUnitPrice); + } + + function _setupSingleChainMarkups( + uint256 _chainId, + uint16 _markupGasDrop, + uint16 _markupGasUsage + ) internal { + uint256[] memory chainIds = new uint256[](1); + uint16[] memory markupGasDrop = new uint16[](1); + uint16[] memory markupGasUsage = new uint16[](1); + chainIds[0] = _chainId; + markupGasDrop[0] = _markupGasDrop; + markupGasUsage[0] = _markupGasUsage; + _setRemoteMarkups(chainIds, markupGasDrop, markupGasUsage); + } + + function _expectMessagingEmits(bytes memory message) internal { + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + uint256 chainId = remoteChainIds[i]; + bytes memory options = Options.encode(remoteVars[chainId].gasUnitsRcvMsg); + uint256 fee = messageBus.estimateFee(chainId, options); + bytes32 receiver = utils.addressToBytes32(remoteVars[chainId].gasFeePricing); + uint64 nonce = uint64(i); + bytes32 messageId = messageBus.computeMessageId( + address(gasFeePricing), + block.chainid, + receiver, + chainId, + nonce, + message + ); + + vm.expectEmit(true, true, true, true); + emit MessageSent( + address(gasFeePricing), + block.chainid, + receiver, + chainId, + message, + nonce, + options, + fee, + messageId + ); + } + } + + function _prepareSendingTests() internal { + (uint128[] memory gasTokenPrices, uint128[] memory gasUnitPrices) = _generateTestInfoValues(); + _setRemoteInfo(remoteChainIds, gasTokenPrices, gasUnitPrices); + + uint112[] memory gasDropMax = new uint112[](TEST_CHAINS); + uint80[] memory gasUnitsRcvMsg = new uint80[](TEST_CHAINS); + uint32[] memory minGasUsageFeeUsd = new uint32[](TEST_CHAINS); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + gasDropMax[i] = 0; + gasUnitsRcvMsg[i] = uint80((i + 1) * 105000); + minGasUsageFeeUsd[i] = 0; + } + _setRemoteConfig(remoteChainIds, gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + } +} diff --git a/test/messaging/gfp/GasFeePricingSecurity.t.sol b/test/messaging/gfp/GasFeePricingSecurity.t.sol new file mode 100644 index 000000000..0708d66d5 --- /dev/null +++ b/test/messaging/gfp/GasFeePricingSecurity.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "./GasFeePricingSetup.t.sol"; + +contract GasFeePricingUpgradeableSecurityTest is GasFeePricingSetup { + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ SECURITY TESTS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function testInitializedCorrectly() public { + (uint128 _gasTokenPrice, ) = gasFeePricing.localInfo(); + assertEq(_gasTokenPrice, localVars.gasTokenPrice, "Failed to init: gasTokenPrice"); + assertEq(gasFeePricing.messageBus(), address(messageBus), "Failed to init: messageBus"); + } + + function testIsInitialized() public { + utils.checkAccess( + address(messageBus), + abi.encodeWithSelector(MessageBusUpgradeable.initialize.selector, address(0), address(0)), + "Initializable: contract is already initialized" + ); + + utils.checkAccess( + address(gasFeePricing), + abi.encodeWithSelector(GasFeePricingUpgradeable.initialize.selector, address(0), 0, 0, 0), + "Initializable: contract is already initialized" + ); + } + + function testCheckAccessControl() public { + address _gfp = address(gasFeePricing); + utils.checkAccess( + _gfp, + abi.encodeWithSelector( + GasFeePricingUpgradeable.setRemoteConfig.selector, + new uint256[](1), + new uint112[](1), + new uint80[](1), + new uint32[](1) + ), + "Ownable: caller is not the owner" + ); + utils.checkAccess( + _gfp, + abi.encodeWithSelector( + GasFeePricingUpgradeable.setRemoteInfo.selector, + new uint256[](1), + new uint128[](1), + new uint128[](1) + ), + "Ownable: caller is not the owner" + ); + utils.checkAccess( + _gfp, + abi.encodeWithSelector( + GasFeePricingUpgradeable.setRemoteMarkups.selector, + new uint256[](1), + new uint16[](1), + new uint16[](1) + ), + "Ownable: caller is not the owner" + ); + + utils.checkAccess( + _gfp, + abi.encodeWithSelector(GasFeePricingUpgradeable.updateLocalConfig.selector, 0, 0, 0), + "Ownable: caller is not the owner" + ); + + utils.checkAccess( + _gfp, + abi.encodeWithSelector(GasFeePricingUpgradeable.updateLocalInfo.selector, 0, 0), + "Ownable: caller is not the owner" + ); + } +} diff --git a/test/messaging/gfp/GasFeePricingSetters.t.sol b/test/messaging/gfp/GasFeePricingSetters.t.sol new file mode 100644 index 000000000..984a9530b --- /dev/null +++ b/test/messaging/gfp/GasFeePricingSetters.t.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "./GasFeePricingSetup.t.sol"; + +contract GasFeePricingUpgradeableSettersTest is GasFeePricingSetup { + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ GETTERS/SETTERS TESTS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function testSetRemoteConfig() public { + uint112[] memory gasDropMax = new uint112[](TEST_CHAINS); + uint80[] memory gasUnitsRcvMsg = new uint80[](TEST_CHAINS); + uint32[] memory minGasUsageFeeUsd = new uint32[](TEST_CHAINS); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + gasDropMax[i] = uint112((i + 1) * 10**18); + gasUnitsRcvMsg[i] = uint80((i + 1) * 420420); + minGasUsageFeeUsd[i] = uint32((i + 1) * 10000); + } + _setRemoteConfig(remoteChainIds, gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + _checkRemoteConfig(remoteChainIds[i]); + } + } + + function testSetRemoteConfigZeroDropSucceeds() public { + uint112[] memory gasDropMax = new uint112[](TEST_CHAINS); + uint80[] memory gasUnitsRcvMsg = new uint80[](TEST_CHAINS); + uint32[] memory minGasUsageFeeUsd = new uint32[](TEST_CHAINS); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + gasDropMax[i] = uint112(i * 10**17); + gasUnitsRcvMsg[i] = uint80((i + 1) * 133769); + minGasUsageFeeUsd[i] = uint32((i + 1) * 1000); + } + _setRemoteConfig(remoteChainIds, gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + _checkRemoteConfig(remoteChainIds[i]); + } + } + + function testSetRemoteConfigZeroFeeSucceeds() public { + uint112[] memory gasDropMax = new uint112[](TEST_CHAINS); + uint80[] memory gasUnitsRcvMsg = new uint80[](TEST_CHAINS); + uint32[] memory minGasUsageFeeUsd = new uint32[](TEST_CHAINS); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + gasDropMax[i] = uint112((i + 1) * 10**16); + gasUnitsRcvMsg[i] = uint80((i + 1) * 696969); + minGasUsageFeeUsd[i] = uint32(i * 5000); + } + _setRemoteConfig(remoteChainIds, gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + _checkRemoteConfig(remoteChainIds[i]); + } + } + + function testSetRemoteConfigZeroGasReverts() public { + uint112[] memory gasDropMax = new uint112[](TEST_CHAINS); + uint80[] memory gasUnitsRcvMsg = new uint80[](TEST_CHAINS); + uint32[] memory minGasUsageFeeUsd = new uint32[](TEST_CHAINS); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + gasDropMax[i] = uint112((i + 1) * 10**15); + gasUnitsRcvMsg[i] = uint80(i * 123456); + minGasUsageFeeUsd[i] = uint32((i + 1) * 5000); + } + utils.checkRevert( + address(this), + address(gasFeePricing), + abi.encodeWithSelector( + GasFeePricingUpgradeable.setRemoteConfig.selector, + remoteChainIds, + gasDropMax, + gasUnitsRcvMsg, + minGasUsageFeeUsd + ), + "Gas amount is not set" + ); + } + + function testSetRemoteInfo() public { + (uint128[] memory gasTokenPrices, uint128[] memory gasUnitPrices) = _generateTestInfoValues(); + _setRemoteInfo(remoteChainIds, gasTokenPrices, gasUnitPrices); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + uint256 chainId = remoteChainIds[i]; + _checkRemoteInfo(chainId); + } + } + + function testSetRemoteInfoZeroTokenPriceReverts() public { + (uint128[] memory gasTokenPrices, uint128[] memory gasUnitPrices) = _generateTestInfoValues(); + gasTokenPrices[2] = 0; + utils.checkRevert( + address(this), + address(gasFeePricing), + abi.encodeWithSelector( + GasFeePricingUpgradeable.setRemoteInfo.selector, + remoteChainIds, + gasTokenPrices, + gasUnitPrices + ), + "Remote gas token price is not set" + ); + } + + function testSetRemoteInfoZeroUnitPriceSucceeds() public { + (uint128[] memory gasTokenPrices, uint128[] memory gasUnitPrices) = _generateTestInfoValues(); + gasTokenPrices[2] = 100 * 10**18; + gasUnitPrices[3] = 0; + _setRemoteInfo(remoteChainIds, gasTokenPrices, gasUnitPrices); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + uint256 chainId = remoteChainIds[i]; + _checkRemoteInfo(chainId); + } + } + + function testSetRemoteMarkups() public { + uint16[] memory markupGasDrop = new uint16[](TEST_CHAINS); + uint16[] memory markupGasUsage = new uint16[](TEST_CHAINS); + for (uint16 i = 0; i < TEST_CHAINS; ++i) { + // this will set the first chain markups to [0, 0] + markupGasDrop[i] = i * 13; + markupGasUsage[i] = i * 42; + } + _setRemoteMarkups(remoteChainIds, markupGasDrop, markupGasUsage); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + _checkRemoteMarkups(remoteChainIds[i]); + } + } + + function testUpdateLocalConfig() public { + uint112 gasDropMax = 10 * 10**18; + uint80 gasUnitsRcvMsg = 10**6; + uint32 minGasUsageFeeUsd = 10**4; + + _updateLocalConfig(gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + _checkLocalConfig(); + } + + function testUpdateLocalConfigZeroDropSucceeds() public { + // should be able to set to zero + uint112 gasDropMax = 0; + uint80 gasUnitsRcvMsg = 2 * 10**6; + uint32 minGasUsageFeeUsd = 2 * 10**4; + _updateLocalConfig(gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + _checkLocalConfig(); + } + + function testUpdateLocalConfigZeroFeeSucceeds() public { + uint112 gasDropMax = 5 * 10**18; + uint80 gasUnitsRcvMsg = 2 * 10**6; + // should be able to set to zero + uint32 minGasUsageFeeUsd = 0; + _updateLocalConfig(gasDropMax, gasUnitsRcvMsg, minGasUsageFeeUsd); + _checkLocalConfig(); + } + + function testUpdateLocalConfigZeroGasReverts() public { + uint112 gasDropMax = 10**18; + // should NOT be able to set to zero + uint80 gasUnitsRcvMsg = 0; + uint32 minGasUsageFeeUsd = 3 * 10**4; + + utils.checkRevert( + address(this), + address(gasFeePricing), + abi.encodeWithSelector( + GasFeePricingUpgradeable.updateLocalConfig.selector, + gasDropMax, + gasUnitsRcvMsg, + minGasUsageFeeUsd + ), + "Gas amount is not set" + ); + } + + function testUpdateLocalInfo() public { + testSetRemoteInfo(); + + uint128 gasTokenPrice = 2 * 10**18; + uint128 gasUnitPrice = 10 * 10**9; + _updateLocalInfo(gasTokenPrice, gasUnitPrice); + _checkLocalInfo(); + } + + function testUpdateLocalInfoZeroTokenPriceReverts() public { + testSetRemoteInfo(); + + uint128 gasTokenPrice = 0; + uint128 gasUnitPrice = 10 * 10**9; + + utils.checkRevert( + address(this), + address(gasFeePricing), + abi.encodeWithSelector(GasFeePricingUpgradeable.updateLocalInfo.selector, gasTokenPrice, gasUnitPrice), + "Gas token price is not set" + ); + } + + function testUpdateLocalInfoZeroUnitPriceSucceeds() public { + testSetRemoteInfo(); + + uint128 gasTokenPrice = 4 * 10**17; + uint128 gasUnitPrice = 0; + _updateLocalInfo(gasTokenPrice, gasUnitPrice); + _checkLocalInfo(); + } +} diff --git a/test/messaging/gfp/GasFeePricingSetup.t.sol b/test/messaging/gfp/GasFeePricingSetup.t.sol new file mode 100644 index 000000000..43bd5bb73 --- /dev/null +++ b/test/messaging/gfp/GasFeePricingSetup.t.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "forge-std/Test.sol"; +import {Utilities} from "../../utils/Utilities.sol"; + +import "src-messaging/AuthVerifier.sol"; +import "src-messaging/GasFeePricingUpgradeable.sol"; +import "src-messaging/MessageBusUpgradeable.sol"; +import "src-messaging/libraries/GasFeePricingUpdates.sol"; + +abstract contract GasFeePricingSetup is Test { + struct ChainVars { + uint256 gasTokenPrice; + uint256 gasUnitPrice; + uint256 gasDropMax; + uint256 gasUnitsRcvMsg; + uint256 minGasUsageFeeUsd; + uint256 markupGasDrop; + uint256 markupGasUsage; + address gasFeePricing; + } + + Utilities internal utils; + + AuthVerifier internal authVerifier; + GasFeePricingUpgradeable internal gasFeePricing; + MessageBusUpgradeable internal messageBus; + + ChainVars internal localVars; + + mapping(uint256 => ChainVars) internal remoteVars; + + uint256[] internal remoteChainIds; + uint256 internal constant TEST_CHAINS = 5; + + address internal constant NODE = address(1337); + + // enable receiving overpaid fees + receive() external payable virtual { + this; + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ SETUP ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function setUp() public { + utils = new Utilities(); + authVerifier = new AuthVerifier(NODE); + + // local gas token is worth exactly 1 USD + localVars.gasTokenPrice = 10**18; + + MessageBusUpgradeable busImpl = new MessageBusUpgradeable(); + GasFeePricingUpgradeable pricingImpl = new GasFeePricingUpgradeable(); + + messageBus = MessageBusUpgradeable(utils.deployTransparentProxy(address(busImpl))); + gasFeePricing = GasFeePricingUpgradeable(utils.deployTransparentProxy(address(pricingImpl))); + + // I don't have extra 10M laying around, so let's initialize those proxies + messageBus.initialize(address(gasFeePricing), address(authVerifier)); + gasFeePricing.initialize(address(messageBus), localVars.gasTokenPrice); + + remoteChainIds = new uint256[](TEST_CHAINS); + for (uint256 i = 0; i < TEST_CHAINS; ++i) { + uint256 remoteChainId = i + 1; + remoteChainIds[i] = remoteChainId; + address remoteGasFeePricing = utils.getNextUserAddress(); + remoteVars[remoteChainId].gasFeePricing = remoteGasFeePricing; + gasFeePricing.setTrustedRemote(remoteChainId, utils.addressToBytes32(remoteGasFeePricing)); + } + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ VALUES GENERATORS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function _generateTestInfoValues() + internal + pure + returns (uint128[] memory gasTokenPrices, uint128[] memory gasUnitPrices) + { + gasTokenPrices = new uint128[](TEST_CHAINS); + gasUnitPrices = new uint128[](TEST_CHAINS); + // 100 gwei, gasToken = $2000 + gasTokenPrices[0] = 2000 * 10**18; + gasUnitPrices[0] = 100 * 10**9; + // 5 gwei, gasToken = $1000 + gasTokenPrices[1] = 1000 * 10**18; + gasUnitPrices[1] = 5 * 10**9; + // 2000 gwei, gasToken = $0.5 + gasTokenPrices[2] = (5 * 10**18) / 10; + gasUnitPrices[2] = 2000 * 10**9; + // 1 gwei, gasToken = $2000 + gasTokenPrices[3] = 2000 * 10**18; + gasUnitPrices[3] = 10**9; + // 0.04 gwei, gasToken = $0.01 + gasTokenPrices[4] = (1 * 10**18) / 100; + gasUnitPrices[4] = (4 * 10**9) / 100; + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ CHECKERS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function _checkRemoteConfig(uint256 _chainId) internal { + (uint112 gasDropMax, uint80 gasUnitsRcvMsg, uint32 minGasUsageFeeUsd, , ) = gasFeePricing.remoteConfig( + _chainId + ); + assertEq(gasDropMax, remoteVars[_chainId].gasDropMax, "remoteMaxGasDrop is incorrect"); + assertEq(gasUnitsRcvMsg, remoteVars[_chainId].gasUnitsRcvMsg, "remoteGasUnitsRcvMsg is incorrect"); + assertEq(minGasUsageFeeUsd, remoteVars[_chainId].minGasUsageFeeUsd, "remoteMinGasUsageFeeUsd is incorrect"); + } + + function _checkRemoteInfo(uint256 _chainId) internal { + (uint128 gasTokenPrice, uint128 gasUnitPrice) = gasFeePricing.remoteInfo(_chainId); + assertEq(gasTokenPrice, remoteVars[_chainId].gasTokenPrice, "remoteGasTokenPrice is incorrect"); + assertEq(gasUnitPrice, remoteVars[_chainId].gasUnitPrice, "remoteGasUnitPrice is incorrect"); + } + + function _checkRemoteMarkups(uint256 _chainId) internal { + (, , , uint16 markupGasDrop, uint16 markupGasUsage) = gasFeePricing.remoteConfig(_chainId); + assertEq(markupGasDrop, remoteVars[_chainId].markupGasDrop, "remoteMarkupGasDrop is incorrect"); + assertEq(markupGasUsage, remoteVars[_chainId].markupGasUsage, "remoteMarkupGasUsage is incorrect"); + } + + function _checkLocalConfig() internal { + (uint112 gasDropMax, uint80 gasUnitsRcvMsg, uint32 minGasUsageFeeUsd, , ) = gasFeePricing.localConfig(); + assertEq(gasDropMax, localVars.gasDropMax, "localMaxGasDrop is incorrect"); + assertEq(gasUnitsRcvMsg, localVars.gasUnitsRcvMsg, "localGasUnitsRcvMsg is incorrect"); + assertEq(minGasUsageFeeUsd, localVars.minGasUsageFeeUsd, "localMinGasUsageFeeUsd is incorrect"); + } + + function _checkLocalInfo() internal { + (uint128 gasTokenPrice, uint128 gasUnitPrice) = gasFeePricing.localInfo(); + assertEq(gasTokenPrice, localVars.gasTokenPrice, "localGasTokenPrice is incorrect"); + assertEq(gasUnitPrice, localVars.gasUnitPrice, "gasUnitPrice is incorrect"); + } + + /*╔══════════════════════════════════════════════════════════════════════╗*\ + ▏*║ SETTERS ║*▕ + \*╚══════════════════════════════════════════════════════════════════════╝*/ + + function _setRemoteConfig( + uint256[] memory _chainIds, + uint112[] memory _gasDropMax, + uint80[] memory _gasUnitsRcvMsg, + uint32[] memory _minGasUsageFeeUsd + ) internal { + for (uint256 i = 0; i < _chainIds.length; ++i) { + uint256 chainId = _chainIds[i]; + remoteVars[chainId].gasDropMax = _gasDropMax[i]; + remoteVars[chainId].gasUnitsRcvMsg = _gasUnitsRcvMsg[i]; + remoteVars[chainId].minGasUsageFeeUsd = _minGasUsageFeeUsd[i]; + } + gasFeePricing.setRemoteConfig(_chainIds, _gasDropMax, _gasUnitsRcvMsg, _minGasUsageFeeUsd); + } + + function _setRemoteInfo( + uint256[] memory _chainIds, + uint128[] memory _gasTokenPrice, + uint128[] memory _gasUnitPrice + ) internal { + for (uint256 i = 0; i < _chainIds.length; ++i) { + uint256 chainId = _chainIds[i]; + remoteVars[chainId].gasTokenPrice = _gasTokenPrice[i]; + remoteVars[chainId].gasUnitPrice = _gasUnitPrice[i]; + } + gasFeePricing.setRemoteInfo(_chainIds, _gasTokenPrice, _gasUnitPrice); + } + + function _setRemoteMarkups( + uint256[] memory _chainIds, + uint16[] memory _markupGasDrop, + uint16[] memory _markupGasUsage + ) internal { + for (uint256 i = 0; i < _chainIds.length; ++i) { + remoteVars[_chainIds[i]].markupGasDrop = _markupGasDrop[i]; + remoteVars[_chainIds[i]].markupGasUsage = _markupGasUsage[i]; + } + gasFeePricing.setRemoteMarkups(_chainIds, _markupGasDrop, _markupGasUsage); + } + + function _updateLocalConfig( + uint112 _gasDropMax, + uint80 _gasUnitsRcvMsg, + uint32 _minGasUsageFeeUsd + ) internal { + localVars.gasDropMax = _gasDropMax; + localVars.gasUnitsRcvMsg = _gasUnitsRcvMsg; + localVars.minGasUsageFeeUsd = _minGasUsageFeeUsd; + uint256 fee = gasFeePricing.estimateUpdateFees(); + gasFeePricing.updateLocalConfig{value: fee}(_gasDropMax, _gasUnitsRcvMsg, _minGasUsageFeeUsd); + } + + function _updateLocalInfo(uint128 _gasTokenPrice, uint128 _gasUnitPrice) internal { + localVars.gasTokenPrice = _gasTokenPrice; + localVars.gasUnitPrice = _gasUnitPrice; + uint256 fee = gasFeePricing.estimateUpdateFees(); + gasFeePricing.updateLocalInfo{value: fee}(_gasTokenPrice, _gasUnitPrice); + } +} diff --git a/test/utils/Utilities.sol b/test/utils/Utilities.sol index 1345fde84..62dce470b 100644 --- a/test/utils/Utilities.sol +++ b/test/utils/Utilities.sol @@ -1,28 +1,49 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: MIT pragma solidity >=0.8.0; import "forge-std/Test.sol"; +import "@openzeppelin/contracts-4.5.0/utils/Strings.sol"; +import "@openzeppelin/contracts-4.5.0/proxy/transparent/TransparentUpgradeableProxy.sol"; -//common utilities for forge tests +interface IAccessControl { + function getRoleMember(bytes32 role, uint256 index) external view returns (address); + + function grantRole(bytes32 role, address account) external; + + function revokeRole(bytes32 role, address account) external; +} + +interface IProxy { + function upgradeTo(address) external; +} + +// Common utilities for forge tests contract Utilities is Test { - bytes32 internal nextUser = keccak256(abi.encodePacked("user address")); + bytes32 internal nextUser = keccak256("user address"); + + bytes32 internal nextKappa = keccak256("kappa"); - function addressToBytes32(address _addr) public pure returns (bytes32) { - return bytes32(uint256(uint160(_addr))); + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + address internal immutable attacker; + + constructor() { + attacker = bytes32ToAddress(keccak256("user address")); } - function bytes32ToAddress(bytes32 bys) public pure returns (address) { - return address(uint160(uint256(bys))); + // -- CAST FUNCTIONS -- + + function addressToBytes32(address addr) public pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); } - function getNextUserAddress() external returns (address payable) { - //bytes32 to address conversion - address payable user = payable(address(uint160(uint256(nextUser)))); - nextUser = keccak256(abi.encodePacked(nextUser)); - return user; + function bytes32ToAddress(bytes32 value) public pure returns (address) { + return address(uint160(uint256(value))); } - //create users with 100 ether balance + // -- SETUP FUNCTIONS -- + + // create users with 100 ether balance function createUsers(uint256 userNum) external returns (address payable[] memory) { address payable[] memory users = new address payable[](userNum); for (uint256 i = 0; i < userNum; i++) { @@ -33,9 +54,126 @@ contract Utilities is Test { return users; } - //move block.number forward by a given number of blocks + function createEmptyUsers(uint256 userNum) external returns (address[] memory users) { + users = new address[](userNum); + for (uint256 i = 0; i < userNum; ++i) { + users[i] = this.getNextUserAddress(); + } + } + + // generate fresh address + function getNextUserAddress() external returns (address payable) { + //bytes32 to address conversion + address payable user = payable(address(uint160(uint256(nextUser)))); + nextUser = keccak256(abi.encodePacked(nextUser)); + return user; + } + + function getNextKappa() external returns (bytes32 kappa) { + kappa = nextKappa; + nextKappa = keccak256(abi.encodePacked(kappa)); + } + + function deployTransparentProxy(address impl) external returns (address proxy) { + // Setup proxy with needed logic and custom admin, + // we don't need to upgrade anything, so no need to setup ProxyAdmin + proxy = address(new TransparentUpgradeableProxy(impl, address(420), bytes(""))); + } + + // Upgrades Transparent Proxy implementation + function upgradeTo(address proxy, address impl) external { + address admin = bytes32ToAddress(vm.load(proxy, ADMIN_SLOT)); + vm.startPrank(admin); + IProxy(proxy).upgradeTo(impl); + vm.stopPrank(); + } + + // -- VIEW FUNCTIONS -- + + function getRoleMember(address accessControlled, bytes32 role) external view returns (address) { + return IAccessControl(accessControlled).getRoleMember(role, 0); + } + + // -- EVM FUNCTIONS -- + + /// @notice Get state modifying function return value without modifying the state + function peekReturnValue( + address caller, + address _contract, + bytes memory payload, + uint256 value + ) external { + vm.prank(caller); + (bool success, bytes memory data) = _contract.call{value: value}(payload); + assertTrue(success, "Call failed"); + revert(string(data)); + } + + // move block.number forward by a given number of blocks function mineBlocks(uint256 numBlocks) external { uint256 targetBlock = block.number + numBlocks; vm.roll(targetBlock); } + + function checkAccess( + address _contract, + bytes memory payload, + string memory revertMsg + ) external { + this.checkRevert(attacker, _contract, payload, "Attacker gained access", revertMsg); + } + + function checkAccessControl( + address _contract, + bytes memory payload, + bytes32 neededRole + ) external { + this.checkAccess(_contract, payload, _getAccessControlRevertMsg(neededRole, attacker)); + } + + function checkRevert( + address executor, + address _contract, + bytes memory payload, + string memory revertMsg + ) external { + this.checkRevert(executor, _contract, payload, revertMsg, revertMsg); + } + + function checkRevert( + address executor, + address _contract, + bytes memory payload, + string memory failReason, + string memory revertMsg + ) external { + hoax(executor); + (bool success, bytes memory returnData) = _contract.call(payload); + assertTrue(!success, failReason); + assertEq(this.getRevertMsg(returnData), revertMsg, "Unexpected revert message"); + } + + // -- INTERNAL STUFF -- + + function _getAccessControlRevertMsg(bytes32 role, address account) internal pure returns (string memory revertMsg) { + revertMsg = string( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(account), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + } + + function getRevertMsg(bytes memory _returnData) external pure returns (string memory) { + // If the _res length is less than 68, then the transaction failed silently (without a revert message) + if (_returnData.length < 68) return "Transaction reverted silently"; + + assembly { + // Slice the sighash. + _returnData := add(_returnData, 0x04) + } + return abi.decode(_returnData, (string)); // All that remains is the revert string + } }