TempleGold

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

Incosistent message generation in TempleTeleporter.quote() and TempleTeleporter.teleport() results in inaccurate required fee calculation by TempleTeleporter.quote()

Summary

TempleTeleporter.quote() will return a fee value lower than will be required for sending a message via TempleTeleporter.teleport(). This is because the way both functions construct the layerzero message/payload is different. TempleTeleporter.quote() uses abi.encodePacked(_to, _amount) while TempleTeleporter.teleport() uses abi.encodePacked(to.addressToBytes32(), amount).

TempleTeleporter.quote() is a function used for getting the fee amount needed for sending a layerZero message via TempleTeleporter.teleport(). TempleTeleporter.quote() does not calculate properly/underestimates the fee amount because of this difference mentioned above.

Vulnerability Details

The conversion of the recipient address to into bytes, as implemented in TempleTeleporter.teleport(), results in a higher messaging fee for a sucessfull teleport() call. This occurs because the message or payload generated in TempleTeleporter.teleport() is longer due to the address conversion to bytes before encoding. In contrast, TempleTeleporter.quote() does not convert the address to bytes before encoding, resulting in a shorter message and thus a lower message fee is calculated. Because of thie quote() will always return an underestimated value required for a sucessfull teleport() call.

https://github.com/Cyfrin/2024-07-templegold/blob/57a3e597e9199f9e9e0c26aab2123332eb19cc28/protocol/contracts/templegold/TempleTeleporter.sol#L43-L52

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);

https://github.com/Cyfrin/2024-07-templegold/blob/57a3e597e9199f9e9e0c26aab2123332eb19cc28/protocol/contracts/templegold/TempleTeleporter.sol#L87C1-L93C81

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);

The layerzero docs advise that the arguments passed into the quote() function identically match the parameters used in the lzSend() function to call _lzSend() because If parameters mismatch, you can run into errors as your msg.value will not match the actual gas quote. --> https://docs.layerzero.network/v2/developers/evm/oapp/overview#estimating-gas-fees

Proof Of Concept

The script below shows the differences fee estimation via LZendpoint.quote() which is eventually called by the OApp's internal _quote() function. It compares the fees between messages created with address as address before encoding and address converted to bytes before encoding.

  • paste code below in new file created in ./protocol/script/ folder

  • run code with forge script --chain mainnet script/Counter.s.sol:QuoteTestScript --rpc-url **YOUR_RPC_URL** --broadcast -vv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console} from 'forge-std/Script.sol';
import {MessagingParams, MessagingFee, MessagingReceipt, ILayerZeroEndpointV2} from '@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol';
import {OAppOptionsType3} from '@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OAppOptionsType3.sol';
import {OptionsBuilder} from '@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol';
contract QuoteTestScript is Script {
using OptionsBuilder for bytes;
function setUp() public {}
function run() public {
vm.startBroadcast();
teleport_quote();
vm.stopBroadcast();
}
function _addressToBytes32(address _addr) internal pure returns (bytes32) {
return bytes32(uint256(uint160(_addr)));
}
function teleport_quote() public {
//aim is to check if fee value returned when message is abi.encodePacked(_to, _amount) as seen in templeTeleporter.quote() is same for when message
// is abi.encodePacked(to.addressToBytes32(), amount) which is used in templeTeleporter.teleport()
//address of layerZero endpoint on ethereum mainnet
address endpoint = 0x1a44076050125825900e736c501f859c50fE728c;
uint amount = 100;
address to = makeAddr("to");
uint32 dstEid = 30102; //LZ destination endpoint ID
bytes memory options = OptionsBuilder
.newOptions()
.addExecutorLzReceiveOption(200000, 0); //build options
//build two different message params, one with address as bytes in message, the other with plain address in message
MessagingParams memory messageParamsWithAddressAsBytes = MessagingParams({
dstEid: dstEid, //endpoint id for avalanche
message: abi.encodePacked(_addressToBytes32(to), amount),
options: options,
payInLzToken: false,
receiver: _addressToBytes32(makeAddr("receiver"))
});
MessagingParams memory messageParamsWithAddressAsAddress = MessagingParams({
dstEid: dstEid, //endpoint id for avalanche
message: abi.encodePacked(to, amount),
options: options,
payInLzToken: false,
receiver: _addressToBytes32(makeAddr("receiver"))
});
address sender = makeAddr("sender");
//call endpoint with the two different message params,
//only diff between the two is the address as address in one and address as bytes in the other
MessagingFee memory fee1 = ILayerZeroEndpointV2(endpoint).quote(
messageParamsWithAddressAsBytes,
sender
);
MessagingFee memory fee2 = ILayerZeroEndpointV2(endpoint).quote(
messageParamsWithAddressAsAddress,
sender
);
require(fee1.nativeFee != fee2.nativeFee, "same value returned");
console.log("fee when address is converted to bytes: ", fee1.nativeFee);
console.log("fee when address is not converted to bytes: ", fee2.nativeFee);
console.log("fee charged when address is not converted to bytes is lesser");
console.log(
"the templeTeleporter.quote() fcn will return a smaller fee value than required by templeTeleporter.teleport() "
);
require(fee1.nativeFee > fee2.nativeFee);
}
}

Impact

TempleTeleporter.quote()does not return the correct fee amount require for a layerZero message via TempleTeleporter.teleport().

Tools Used

manual review , foundry

Recommendations

ensure consistency between both functions, use same methods for generating the layerzero payload/message in both functions.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months 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.