What is LZD
LZEndpointDollar (LZD) is the canonical USD-denominated fee token for LayerZero on Tempo. Because Tempo has no native gas token, EndpointV2Alt uses LZD as the ERC-20 token for all LayerZero messaging fees.
The endpoint could accept a single TIP-20 token directly, but that would force users to hold one specific stablecoin. LZD is an ERC-20 wrapper around multiple whitelisted 6-decimal stablecoins (pathUSD, USDC.e, or USDT0), so users can pay fees with whichever USD stablecoin they have. Users wrap a supported stablecoin into LZD, then approve the endpoint or OFT to spend it.
Endpoint mode detection
On any EVM chain, you can check the endpoint type by calling nativeToken(). On Tempo, it returns the LZD address:
address feeToken = ILayerZeroEndpointV2(endpoint).nativeToken();
if (feeToken == address(0)) {
// Standard Endpoint: pay fees with native token (msg.value)
} else {
// Endpoint Alt: pay fees with ERC20 at feeToken address
// On Tempo, feeToken = LZD address
}
Fee payment flow
Paying LayerZero fees on Tempo requires wrapping a whitelisted stablecoin into LZD before sending:
- Quote: call
quoteSend(sendParam, _payInLzToken) to receive both nativeFee and lzTokenFee. LayerZero lets you choose the fee token via _payInLzToken. At the time of writing, _payInLzToken is not enabled on Tempo, so set it to false and use nativeFee (LZD).
- Approve stablecoin: approve the LZD contract to spend your stablecoin.
- Wrap: call
LZD.wrap(feeToken, recipient, amount) to convert your stablecoin into LZD.
- Approve LZD: approve the OFT contract to spend your LZD.
- Send: call
send() with msg.value = 0 and MessagingFee(nativeFee, 0).
// 1. Quote the fee
MessagingFee memory fee = oft.quoteSend(sendParam, false);
// 2. Approve stablecoin to LZD
IERC20(usdce).approve(address(lzd), fee.nativeFee);
// 3. Wrap USDC.e into LZD
lzd.wrap(usdce, msg.sender, fee.nativeFee);
// 4. Approve LZD to the OFT
IERC20(address(lzd)).approve(address(oft), fee.nativeFee);
// 5. Send with msg.value = 0
oft.send{value: 0}(sendParam, fee, refundAddress);
At the time of writing, _payInLzToken is not enabled on Tempo, so set it to false.
API reference
wrap
function wrap(address _token, address _to, uint256 _amount) public nonReentrant onlyWhitelistedToken(_token)
Wraps a whitelisted stablecoin into LZD. Transfers the underlying token from msg.sender and mints LZD to _to.
Parameters
| Name | Type | Description |
|---|
| _token | address | whitelisted stablecoin to wrap |
| _to | address | address to receive minted LZD |
| _amount | uint256 | amount to wrap (6-decimal) |
unwrap
function unwrap(address _token, address _to, uint256 _amount) public nonReentrant onlyWhitelistedToken(_token)
Unwraps LZD back into a whitelisted stablecoin. Burns LZD from msg.sender and transfers the underlying token to _to.
Parameters
| Name | Type | Description |
|---|
| _token | address | whitelisted stablecoin to receive |
| _to | address | address to receive underlying tokens |
| _amount | uint256 | amount to unwrap (6-decimal) |
whitelistToken
function whitelistToken(address _token) public onlyOwner
Adds a token to the whitelist. This is an owner-only admin function, so new fee tokens can only be added by the LZD contract owner. The token must have exactly 6 decimals and must not already be whitelisted.
Parameters
| Name | Type | Description |
|---|
| _token | address | token address to add |
unwhitelistToken
function unwhitelistToken(address _token) public onlyOwner
Removes a token from the whitelist.
Parameters
| Name | Type | Description |
|---|
| _token | address | token address to remove |
isWhitelistedToken
function isWhitelistedToken(address _token) public view returns (bool isWhitelisted)
Returns whether a token is whitelisted.
Parameters
| Name | Type | Description |
|---|
| _token | address | token to check |
getWhitelistedTokens
function getWhitelistedTokens() public view returns (address[] memory tokens)
Returns all whitelisted token addresses.
getTokenBalance
function getTokenBalance(address _token) public view returns (uint256 balance)
Returns the contract’s balance of a specific underlying token.
Parameters
| Name | Type | Description |
|---|
| _token | address | token to check |
decimals
function decimals() public view returns (uint8)
Returns 6. LZD uses 6 decimals to match its underlying whitelisted stablecoins.
Events
TokenWrapped
event TokenWrapped(address indexed token, address indexed from, address indexed to, uint256 amount)
Emitted when a whitelisted token is wrapped into LZD.
TokenUnwrapped
event TokenUnwrapped(address indexed token, address indexed from, address indexed to, uint256 amount)
Emitted when LZD is unwrapped back into a whitelisted token.
TokenWhitelisted
event TokenWhitelisted(address indexed token, bool whitelisted)
Emitted when a token is added to or removed from the whitelist.
Errors
| Error | Cause |
|---|
NotWhitelisted(token) | token is not on the whitelist |
Whitelisted(token) | token is already whitelisted (duplicate add) |
InvalidToken(token) | token is zero address or the LZD contract |
InvalidTokenDecimals(token, decimals) | token does not have 6 decimals |
Quoting fees
quoteSend() returns a MessagingFee with both nativeFee and lzTokenFee. LayerZero lets applications choose the payment token via _payInLzToken. At the time of writing, _payInLzToken is not enabled on Tempo, so set it to false and read the fee from nativeFee (LZD):
// false = quote in native fee token (LZD on Tempo)
MessagingFee memory fee = oft.quoteSend(sendParam, false);
// fee.nativeFee = amount of LZD required
// fee.lzTokenFee = 0 on Tempo while _payInLzToken is disabled
msg.value behavior
Tempo’s EndpointV2Alt rejects any transaction that sends native value:
msg.value > 0 reverts with LZ_OnlyAltToken
- There is no native drop.
addExecutorNativeDropOption is not supported
Always set msg.value = 0 when calling send() on Tempo. Do not attach any native value to LayerZero transactions.
Failure modes
| Revert reason | Cause | Mitigation |
|---|
OFTAltCore__msg_value_not_zero | msg.value > 0 when calling send() on an OFTAlt | set msg.value = 0 |
LZ_OnlyAltToken | msg.value > 0 reaching the endpoint directly | set msg.value = 0 |
LZ_LzTokenUnavailable | quoteSend(_, true) called | use quoteSend(_, false) |
NotWhitelisted(token) | fee token not whitelisted by LZD | use pathUSD, USDC.e, or USDT0 |
InvalidTokenDecimals(token, d) | token does not have 6 decimals | use a 6-decimal stablecoin |
| ERC-20 transfer failure | insufficient LZD balance or missing approval | wrap stablecoin into LZD and approve the OFT |
Contract addresses
| Contract | Address |
|---|
| LZEndpointDollar | 0x0ceb237e109ee22374a567c6b09f373c73fa4cbb |