TempleGold

TempleDAO
Foundry
25,000 USDC
View results
Submission Details
Severity: medium
Invalid

Loss of funds when transferring TGLD cross-chain via OFT

Summary

When transferring Temple gold cross-chain some amount is lost due to precision not being set.

Vulnerability Details

The Temple Gold token is non-transferable by design, but users are able to move it to different chains to their own wallets on the other chain. Layer Zero's OFT (Omnichain Fungible Token) is used to accomplish this. It is done by burning the tokens on the current chain and minting them on the target chain. The TempleGold::send function is used to do this

function send(
SendParam calldata _sendParam,
MessagingFee calldata _fee,
address _refundAddress
) external payable virtual override(IOFT, OFTCore) returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {
if (_sendParam.composeMsg.length > 0) { revert CannotCompose(); }
/// cast bytes32 to address
address _to = _sendParam.to.bytes32ToAddress();
/// @dev user can cross-chain transfer to self
if (msg.sender != _to) { revert ITempleGold.NonTransferrable(msg.sender, _to); }
// @dev Applies the token transfers regarding this send() operation.
// - amountSentLD is the amount in local decimals that was ACTUALLY sent/debited from the sender.
// - amountReceivedLD is the amount in local decimals that will be received/credited to the recipient on the remote OFT instance.
(uint256 amountSentLD, uint256 amountReceivedLD) = _debit(
msg.sender,
_sendParam.amountLD,
_sendParam.minAmountLD,
_sendParam.dstEid
);
// @dev Builds the options and OFT message to quote in the endpoint.
(bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD);
// @dev Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt.
msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress);
// @dev Formulate the OFT receipt.
oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD);
emit OFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, amountSentLD, amountReceivedLD);
}

The _debit() function burns the tokens on the current chain and _lzSend() send a message to the Lz router to mint tokens on the destination chain.
As can be seen in the above code snippet the _debit() function returns the amount to be send and the amount that would be received in amountSentLD and amountReceivedLD. The reason for this is that depending on how OFT is configured by the contract integrating it these two amount may differ due to cutting of some amount to send which is lost.

Looking in the _debit() function it calls _debitView

function _debit(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);

_debitView in it self calls _removeDust

function _debitView(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 /*_dstEid*/
) internal view virtual returns (uint256 amountSentLD, uint256 amountReceivedLD) {
// @dev Remove the dust so nothing is lost on the conversion between chains with different decimals for the token.
amountSentLD = _removeDust(_amountLD);

_removeDust cuts some decimals of the amount to send.

function _removeDust(uint256 _amountLD) internal view virtual returns (uint256 amountLD) {
return (_amountLD / decimalConversionRate) * decimalConversionRate;
}

This results in loss of funds, due to less tokens being minted on the destination chain. The decimalConversionRate by default 6 - a more detailed example can be seen in the LZ docs here: https://docs.layerzero.network/v2/developers/evm/oft/quickstart#example

This can be fixed by overriding OFT's sharedDecimals option and setting it to the number of decimals the token has which is 18: https://docs.layerzero.network/v2/developers/evm/oft/quickstart#optional-overriding-shareddecimals

This way the decimalConversionRate would become 1 and no precision would be lost during transfer.

decimalConversionRate = 10^(localDecimals − sharedDecimals) = 10^(18−18) = 10^0 = 1
and in _removeDust (_amountLD / decimalConversionRate) * decimalConversionRate would be amountLD or in other words amount send.

Impact

Loss of some amount of TGLD

Tools Used

Manual Review, LZ Docs

Recommendations

In TempleGold override the following function

+ function sharedDecimals() public view override returns (uint8) {
+ return 18;
+ }
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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