Platform: Code4rena
Start Date: 08/11/2022
Pot Size: $60,500 USDC
Total HM: 6
Participants: 72
Period: 5 days
Judge: Picodes
Total Solo HM: 2
Id: 178
League: ETH
Rank: 46/72
Findings: 1
Award: $36.34
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: RaymondFam
Also found by: 0x1f8b, 0x52, 0xSmartContract, 0xc0ffEE, 0xhacksmithh, 8olidity, Awesome, BClabs, Bnke0x0, Chom, Deivitto, Hashlock, IllIllI, Josiah, KingNFT, Nyx, R2, ReyAdmirado, Rolezn, SamGMK, Sathish9098, SinceJuly, V_B, Vadis, Waze, a12jmx, adriro, ajtra, aphak5010, bearonbike, bin, brgltd, carlitox477, carrotsmuggler, cccz, ch0bu, chaduke, datapunk, delfin454000, erictee, fatherOfBlocks, fs0c, horsefacts, jayphbee, ktg, ladboy233, pashov, perseverancesuccess, rbserver, ret2basic, tnevler, zaskoh
36.3434 USDC - $36.34
Low level token transfer helpers like LowLevelERC20Transfer
make low level calls without checking that the target address is a contract. If there is no contract code at the target address, the call will succeed without actually executing a token transfer. If downstream contracts do not perform this check, this could allow malicious users to "transfer" nonexistent tokens in exchange for real ones. (In practice, this is not the case, see "Impact" below).
function _executeERC20TransferFrom( address currency, address from, address to, uint256 amount ) internal { (bool status, bytes memory data) = currency.call( abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount) ); if (!status) revert ERC20TransferFromFail(); if (data.length > 0) { if (!abi.decode(data, (bool))) revert ERC20TransferFromFail(); } }
Other low level token transfer helpers similarly skip checking the code size of the call target:
LowLevelERC20Approve#_executeERC20Approve
LowLevelERC20Transfer#_executeERC20DirectTransfer
LowLevelERC721Transfer#_executeERC721TransferFrom
LowLevelERC1155Transfer#_executeERC1155SafeTransferFrom
LowLevelERC1155Transfer#_executeERC1155SafeBatchTransferFrom
Impact:
Although it's possible to "transfer" a nonexistent token into the LooksRareAggregator
, the existing LooksRareProxy
and SeaportProxy
will both revert on attempts to execute orders with nonexistent tokens. However, since the aggregator is designed to support additional generic proxies, it's important to ensure that any new proxies added to the system will correctly handle this case, and take extra caution wherever you're using these functions.
Recommendation: Check the codesize of the call target before making a low-level call:
function _executeERC20TransferFrom( address currency, address from, address to, uint256 amount ) internal { if (currency.code.length == 0) revert InvalidCurrency(); (bool status, bytes memory data) = currency.call( abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount) ); if (!status) revert ERC20TransferFromFail(); if (data.length > 0) { if (!abi.decode(data, (bool))) revert ERC20TransferFromFail(); } }
Test cases:
LowLevelERC20Transfer.t.sol
:
function testTransferFromERC20NoCode(uint256 amount) external asPrankedUser(_sender) { lowLevelERC20Transfer.transferFromERC20( address(0), _sender, _recipient, amount ); } function testTransferERC20NoCode(uint256 amount) external asPrankedUser(_sender) { lowLevelERC20Transfer.transferERC20(address(0), _recipient, amount); }
LowLevelERC721Transfer.t.sol
:
function testTransferFromERC721NoCode(uint256 tokenId) external asPrankedUser(_sender) { lowLevelERC721Transfer.transferERC721( address(0), _sender, _recipient, tokenId ); }
LowLevelERC1155Transfer.t.sol
:
function testSafeTransferFromERC1155NoCode(uint256 tokenId, uint256 amount) external asPrankedUser(_sender) { lowLevelERC1155Transfer.safeTransferFromERC1155( address(0), _sender, _recipient, tokenId, amount ); }
_tranferTokenToRecipient
overlaps ERC20 interfaceSince ERC20 and ER721 both include a transferFrom(address,address,uint256)
function, it's possible to pass an ERC20 token as the collection
address to TokenTransferrer#_transferTokenToRecipient
and transfer an amount of ERC20 tokens equal to the expected tokenId
:
TokenTransferrer#_transferTokenToRecipient
function _transferTokenToRecipient( CollectionType collectionType, address recipient, address collection, uint256 tokenId, uint256 amount ) internal { if (collectionType == CollectionType.ERC721) { IERC721(collection).transferFrom(address(this), recipient, tokenId); } else if (collectionType == CollectionType.ERC1155) { IERC1155(collection).safeTransferFrom( address(this), recipient, tokenId, amount, "" ); } }
Recommendation
Consider using ERC721 safeTransferFrom
instead, which does not overlap the ERC20 interface.
#0 - c4-judge
2022-11-21T17:06:11Z
Picodes marked the issue as grade-b
#1 - 0xhiroshi
2022-11-24T18:24:25Z
Low-level transfers do not check contract existence - valid _tranferTokenToRecipient overlaps ERC20 interface - invalid
#2 - c4-sponsor
2022-11-24T18:24:30Z
0xhiroshi requested judge review