TempleGold

TempleDAO
Foundry
25,000 USDC
View results
Submission Details
Severity: low
Valid

Incorrect payload encoding in TempleTeleporter#quote function leads to fee underestimation for LayerZero

Summary

quote function uses incorrect message encoding, different from the actual teleport function. This results in a smaller message size (52 bytes vs 64 bytes), leading to underestimation of a required fee. Consequently, users relying on these quotes will likely experience transaction failures due to insufficient fees when attempting token transfers through Teleporter.

Vulnerability Details

The problem is in this part of the quote function:
See: TempleTeleporter.sol#L78-L94

function quote(
uint32 _dstEid,
address _to,
uint256 _amount,
bytes memory _options
) external view returns (MessagingFee memory fee) {
return _quote(_dstEid, abi.encodePacked(_to, _amount), _options, false); @< audit
}

It calculates a require fee to pay to LayerZero cross-chain messaging, in this case, cross-chain tokens transfer. However, it uses an a different message encoding from the actual message encoding in teleport function.
See: TempleTeleporter.sol#L34-L58

function teleport(
uint32 dstEid,
address to,
uint256 amount,
bytes calldata options
) external payable override returns(MessagingReceipt memory receipt) {
if (amount == 0) { revert CommonEventsAndErrors.ExpectedNonZero(); }
if (to == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); }
// Encodes the message before invoking _lzSend.
bytes memory _payload = abi.encodePacked(to.addressToBytes32(), amount); @< audit
// debit
temple.burnFrom(msg.sender, amount);
emit TempleTeleported(dstEid, msg.sender, to, amount);
receipt = _lzSend(dstEid, _payload, options, MessagingFee(msg.value, 0), payable(msg.sender));
}

quote uses abi.encodePacked(_to, _amount) which creates a 52-byte messages (20 bytes address + 32 bytes for uint)
teleport uses abi.encodePacked(to.addressToBytes32(), amount) which creates a 64-byte message

Because the quote function uses a shorter message than the actual transfer, it always calculates an underestimated fee.

Impact

  • Users who use quote to calculate fee for teleport function will always have their transaction revert from insufficient fee

Rationale for severity

When considering the typical user flow for cross-chain transfers, users are expected to first call the quote function to estimate the required fee, and then use this estimate when calling the teleport function.

However, because the quote function consistently return an underestimated fee, users following this path will always have their transaction revert due to insufficient fee. Thus, breaking the protocol's functionality.

Besides, it's worth nothing that in constast to this, if quote function were to overestimate the fee, the calculation is still incorrect but the execution is not interrupted and the overpaid fee will be refunded.

Hence, incorrect calculation of fee that always result in an underestimated fee in this case should have Medium severity.

Proof-of-Concept

The following test shows that:

  • fee calculation using an incorrect message encoding results in underestimated fee.

  • Calling teleport function with underestimated fee results in revert with insufficient fee.

Steps

  1. Apply below git diff to the repo.

  2. Run forge t --match-contract TempleTeleporterTest --match-test test_quote_incorrect_fee -vv

  3. Observe that the test fail due to LZ_InsufficientFee

diff --git a/protocol/test/forge/templegold/TempleTeleporter.t.sol b/protocol/test/forge/templegold/TempleTeleporter.t.sol
index fed5c1b..a32ce25 100644
--- a/protocol/test/forge/templegold/TempleTeleporter.t.sol
+++ b/protocol/test/forge/templegold/TempleTeleporter.t.sol
@@ -16,6 +16,7 @@ import { CommonEventsAndErrors } from "contracts/common/CommonEventsAndErrors.so
// DevTools imports -- needs to use the local version to avoid stack too deep
import { TestHelperOz5 } from "test/forge/lz-devtools/TestHelperOz5.sol";
+import {console} from "forge-std/Test.sol";
contract TempleTeleporterTest is TestHelperOz5 {
using OptionsBuilder for bytes;
@@ -92,4 +93,19 @@ contract TempleTeleporterTest is TestHelperOz5 {
assertEq(bTemple.balanceOf(userA), tokensToSend);
assertEq(aTemple.balanceOf(userA), initialBalance - 2 * tokensToSend);
}
+ function test_quote_incorrect_fee() public{
+ uint256 tokensToSend = 1 ether;
+ bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0);
+ MessagingFee memory feeCorrect = aTT.quote(bEid, abi.encode(userB, tokensToSend), options);
+ MessagingFee memory feeIncorrect = aTT.quote(bEid, userB, tokensToSend, options);
+ console.log("@> feeCorrect is calculated from correct quote function. feeIncorrect is calculated from vulnerable quote function.");
+ console.log("@> feeCorrect: %s", feeCorrect.nativeFee);
+ console.log("@> feeIncorrect: %s", feeIncorrect.nativeFee);
+ assertGt( feeCorrect.nativeFee, feeIncorrect.nativeFee );
+ vm.startPrank(userA);
+ aTemple.approve(address(aTT), type(uint).max);
+ aTT.teleport{ value: feeCorrect.nativeFee }(bEid, userA, tokensToSend, options);
+ console.log("@> Teleport success using feeCorrect");
+ aTT.teleport{ value: feeIncorrect.nativeFee }(bEid, userA, tokensToSend, options);
+ }
}

Recommended Mitigations

  • Correct payload (message) encoding to match with the one use in teleport function

Suggested Fix

function quote(
uint32 _dstEid,
address _to,
uint256 _amount,
bytes memory _options
) external view returns (MessagingFee memory fee) {
return _quote(_dstEid, abi.encodePacked(_to.addressToBytes32(), _amount), _options, false);
}

Reference

See: https://docs.layerzero.network/v2/developers/evm/gas-settings/gas-fees

Make sure that the arguments passed into the _quote function identically match the parameters used in the lzSend function. If parameters mismatch, you may run into errors as your msg.value will not match the actual gas quote.
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Incorrect payload bytes in `quote()` they use `abi.encodePacked(_to, _amount)` instead of `abi.encodePacked(_to.addressToBytes32(), _amount)`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.