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(); }
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.
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 {
address endpoint = 0x1a44076050125825900e736c501f859c50fE728c;
uint amount = 100;
address to = makeAddr("to");
uint32 dstEid = 30102;
bytes memory options = OptionsBuilder
.newOptions()
.addExecutorLzReceiveOption(200000, 0);
MessagingParams memory messageParamsWithAddressAsBytes = MessagingParams({
dstEid: dstEid,
message: abi.encodePacked(_addressToBytes32(to), amount),
options: options,
payInLzToken: false,
receiver: _addressToBytes32(makeAddr("receiver"))
});
MessagingParams memory messageParamsWithAddressAsAddress = MessagingParams({
dstEid: dstEid,
message: abi.encodePacked(to, amount),
options: options,
payInLzToken: false,
receiver: _addressToBytes32(makeAddr("receiver"))
});
address sender = makeAddr("sender");
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.