TempleGold

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

an address can bypass of token transfer authorization via cross chain transfers.

Summary

send() and lzReceive() dont check if addresses they are sending/minting tokens to are authorized so it is possible use them to do cross chain transfers to/from unauthorized addresses.

Vulnerability Details

_update() is meant to stop transfer to or from unwhitelisted/unauthorized addreess but the check is skipped on _mint() and _burn(). TempleGod.send() and TempleGold._lzReceive() call internal OFT functions _debit() and _credit() to burn and mint tokens during a cross chain transfer. The _debit() and _credit() functions use _mint() and _burn() to do this. _mint() and _burn() call _update() as seen here.

https://github.com/LayerZero-Labs/LayerZero-v2/blob/7aebbd7c79b2dc818f7bb054aed2405ca076b9d6/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol#L69-L98

function _debit(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);
// @dev In NON-default OFT, amountSentLD could be 100, with a 10% fee, the amountReceivedLD amount is 90,
// therefore amountSentLD CAN differ from amountReceivedLD.
// @dev Default OFT burns on src.
_burn(msg.sender, amountSentLD);
}
/**
* @dev Credits tokens to the specified address.
* @param _to The address to credit the tokens to.
* @param _amountLD The amount of tokens to credit in local decimals.
* @dev _srcEid The source chain ID.
* @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals.
*/
function _credit(
address _to,
uint256 _amountLD,
uint32 /*_srcEid*/
) internal virtual override returns (uint256 amountReceivedLD) {
// @dev Default OFT mints on dst.
_mint(_to, _amountLD);
// @dev In the case of NON-default OFT, the _amountLD MIGHT not be == amountReceivedLD.
return _amountLD;

Because update() skips check for if address to be minted to is authorized, TempleGold._lzReceive() function will sucessfully mint to an unauthorized address. Also because TempleGod.send() does not check that the _sendParam.to address is authorized, a sucucessful cross chain message can be sent to deliver tokens to an unauthorized address on the other chain.

Scenario 1

  • address A has 100 temple Gold tokens

  • address A gets marked unauthorized by admin and cant make any transfers via transfer()/transferFrom() on the chain.

  • address A decides to beat this restriction by transferring out to the second chain via cross chain transfer via send(). address A makes a cross chain message to transfers to address B (which may be authorized/unauthorized). 100 tokens is burnt from address A on local chain.

  • lzReceive() is called on remote chain and because address B is not checked to be authorized/unauthorized, 100 tokens is minted to address B on the remote chain.

This shows that the 100 tokens frozen have been "unfrozen". Also shows that an address marked as unauthorized to receive tokens, can still receive tokens via cross chain transfers. The fact that function send() is open and callable by anyone makes it easy for any token holder to bypass the restriction.

Proof Of Concept

In the templeGold.send() function below we can see that the _sendParam.to value is never checked to validate if it is authorized or not before the cross chain message is sucessfully construsted and sent.

https://github.com/Cyfrin/2024-07-templegold/blob/57a3e597e9199f9e9e0c26aab2123332eb19cc28/protocol/contracts/templegold/TempleGold.sol#L281-L311

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); } //@audit send does not check if the "to" address is authorized.
// @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.
// @dev Formulate the OFT 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);
}

In the _lzReceive() function snippet below we can see there is no check for the address toAddress = _message.sendTo().bytes32ToAddress(); to confirm if it is an authorized address or not. This allows for the sucessfull mint of tokens to an unauthorized address and ensures the sucess of a token transfer, even though it is cross chain.

function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address /*_executor*/, // @dev unused in the default implementation.
bytes calldata /*_extraData*/ // @dev unused in the default implementation.
) internal virtual override {
// @dev The src sending chain doesnt know the address length on this chain (potentially non-evm)
// Thus everything is bytes32() encoded in flight.
address toAddress = _message.sendTo().bytes32ToAddress(); //@audit this address is not checked to be authorized or not
// @dev Credit the amountLD to the recipient and return the ACTUAL amount the recipient received in local decimals
uint256 amountReceivedLD = _credit(toAddress, _toLD(_message.amountSD()), _origin.srcEid);
/// @dev Disallow further execution on destination by ignoring composed message
if (_message.isComposed()) { revert CannotCompose(); }
emit OFTReceived(_guid, _origin.srcEid, toAddress, amountReceivedLD);
}

Tools Used

manual review

Recommendations

check the sendParam.to and message.to addresses in send() and _lzReceive() functions respectively. send() should revert if sendParam.to is marked unauthorized. and _lzReceive() should not mint/fail silently if message.to address is marked unauthorized.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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