diff --git a/evm/src/assets/TbrUser.sol b/evm/src/assets/TbrUser.sol index ad4652fb..b0f3f8f5 100644 --- a/evm/src/assets/TbrUser.sol +++ b/evm/src/assets/TbrUser.sol @@ -515,6 +515,9 @@ abstract contract TbrUser is TbrBase { (uint relayFee, uint wormholeFee) = _quoteRelay(chainId, gasDropoff, baseFee); uint totalFee = (relayFee + wormholeFee) / _TOTAL_FEE_DIVISOR; + // We need to round up to ensure that the quoted relay fee is able to cover the remainder. + if ((relayFee + wormholeFee) % _TOTAL_FEE_DIVISOR > 0) + ++totalFee; if (totalFee > type(uint64).max) revert FeeTooLarge(totalFee, commandIndex); diff --git a/evm/test/User.t.sol b/evm/test/User.t.sol index 463bd639..b5247a40 100644 --- a/evm/test/User.t.sol +++ b/evm/test/User.t.sol @@ -1122,6 +1122,46 @@ contract UserTest is TbrTestBase { assertEq(fee, expectedFee); } + function testRelayFee_RemainderBelowMwei(uint32 gasDropoff) public { + gasDropoff = uint32(bound(gasDropoff, 1, MAX_GAS_DROPOFF_AMOUNT)); + uint256 feeQuote = 1e6; + uint64 expectedFee = uint64(feeQuote) / 1e6 + 1; + + vm.mockCall( + address(oracle), + abi.encodeWithSelector(IPriceOracle.get1959.selector), + abi.encode(abi.encodePacked(uint256(feeQuote))) + ); + + // Less than 1kWei + uint fakeWormholeFee = 100; + vm.mockCall( + address(wormholeCore), + abi.encodeWithSelector( + wormholeCore.messageFee.selector + ), + abi.encode(uint256(fakeWormholeFee)) + ); + + bytes memory response = invokeTbr( + abi.encodePacked( + tbr.get1959.selector, + DISPATCHER_PROTOCOL_VERSION0, + RELAY_FEE_ID, + SOLANA_CHAIN_ID, + gasDropoff + ) + ); + + uint offset; + bool isPaused; + uint64 fee; + (isPaused, offset) = response.asBoolUnchecked(offset); + (fee, offset) = response.asUint64Unchecked(offset); + assertEq(isPaused, false); + assertEq(fee, expectedFee); + } + function testRelayFee_GasDropoffExceedsMaximum() public { uint32 gasDropoff = MAX_GAS_DROPOFF_AMOUNT + 1; uint commandIndex = 0;