Platform: Code4rena
Start Date: 05/09/2023
Pot Size: $50,000 USDC
Total HM: 2
Participants: 16
Period: 6 days
Judge: GalloDaSballo
Total Solo HM: 2
Id: 284
League: ETH
Rank: 10/16
Findings: 1
Award: $40.13
π Selected for report: 0
π Solo Findings: 0
40.1294 USDC - $40.13
https://github.com/code-423n4/2023-09-delegate/blob/main/src/DelegateToken.sol#L89 https://github.com/code-423n4/2023-09-delegate/blob/main/src/libraries/DelegateTokenTransferHelpers.sol#L79 https://github.com/code-423n4/2023-09-delegate/blob/main/src/DelegateToken.sol#L408
The initial value of erc1155PullAuthorization
is ERC1155_NOT_PULLED
. And it would be updated to ERC1155_PULLED
in DelegateTokenTransferHelpers.checkERC1155BeforePull
. And when DelegateToken
receives the erc 1155 token, DelegateToken.onERC1155Received
will be trigger then update erc1155PullAuthorization
to ERC1155_NOT_PULLED
. However, DelegateToken.onERC1155Received
doesnβt check if the msg.sender
is the erc1155 contract. Any contract can call the function to update erc1155PullAuthorization
. Therefore, the real token transfer would be blocked.
Usually, DelegateToken.onERC1155Received
would be called by ERC115 safeTransferFrom.
https://github.com/code-423n4/2023-09-delegate/blob/main/src/DelegateToken.sol#L89
/// @inheritdoc IERC1155Receiver function onERC1155Received(address operator, address, uint256, uint256, bytes calldata) external returns (bytes4) { TransferHelpers.revertInvalidERC1155PullCheck(erc1155PullAuthorization, operator); return IERC1155Receiver.onERC1155Received.selector; }
And it calls revertInvalidERC1155PullCheck
to update erc1155PullAuthorization
to ERC1155_NOT_PULLED
. Also, we can notice that onERC1155Received
reverts when erc1155Pulled
is not ERC1155_PULLED
.
https://github.com/code-423n4/2023-09-delegate/blob/main/src/libraries/DelegateTokenTransferHelpers.sol#L79
function checkERC1155Pulled(Structs.Uint256 storage erc1155Pulled, address operator) internal returns (bool) { if (erc1155Pulled.flag == ERC1155_PULLED && address(this) == operator) { erc1155Pulled.flag = ERC1155_NOT_PULLED; return true; } return false; } function revertInvalidERC1155PullCheck(Structs.Uint256 storage erc1155PullAuthorization, address operator) internal { if (!checkERC1155Pulled(erc1155PullAuthorization, operator)) revert Errors.ERC1155PullNotRequested(operator); }
Let's take a look at the flash loan logic for ERC1155. https://github.com/code-423n4/2023-09-delegate/blob/main/src/DelegateToken.sol#L408
function flashloan(Structs.FlashInfo calldata info) external payable nonReentrant { β¦ } else if (info.tokenType == IDelegateRegistry.DelegationType.ERC1155) { RegistryHelpers.revertERC1155FlashAmountUnavailable(delegateRegistry, info); TransferHelpers.checkERC1155BeforePull(erc1155PullAuthorization, info.amount); IERC1155(info.tokenContract).safeTransferFrom(address(this), info.receiver, info.tokenId, info.amount, ""); Helpers.revertOnCallingInvalidFlashloan(info); TransferHelpers.pullERC1155AfterCheck(erc1155PullAuthorization, info.amount, info.tokenContract, info.tokenId); } }
The normal case is:
TransferHelpers.checkERC1155BeforePull
updates erc1155PullAuthorization
to ERC1155_PULLED
TransferHelpers.pullERC1155AfterCheck
calls IERC1155(underlyingContract).safeTransferFrom
to transfer back the erc1155 tokenDelegateToken.onERC1155Received
would be trigger to update erc1155PullAuthorization
to ERC1155_NOT_PULLED
But the following case could happen and make the translation revert.
TransferHelpers.checkERC1155BeforePull
updates erc1155PullAuthorization
to ERC1155_PULLED
Helpers.revertOnCallingInvalidFlashloan(info)
is called. The flash loan caller may call some smart contracts.DelegateToken.onERC1155Received
to update erc1155PullAuthorization
to ERC1155_NOT_PULLED
calls
IERC1155(underlyingContract).safeTransferFrom` to transfer back the erc1155 tokenDelegateToken.onERC1155Received
would be trigger but it would revert since erc1155PullAuthorization
is not ERC1155_PULLED
Manual Review
The root cause is that any contract can call DelegateToken.onERC1155Received
. The only legit caller is the erc 1155 token contract.
Add a state variable to record the erc 1155 token contract.
Structs.Uint256 internal erc1155PullAuthorization = Structs.Uint256(TransferHelpers.ERC1155_NOT_PULLED); + address internal erc1155contract = address(0); /// @inheritdoc IERC1155Receiver function onERC1155Received(address operator, address, uint256, uint256, bytes calldata) external returns (bytes4) { + if (msg.sender != erc1155contract) revert Errors.InvalidERC1155ContractAddress(); TransferHelpers.revertInvalidERC1155PullCheck(erc1155PullAuthorization, operator); return IERC1155Receiver.onERC1155Received.selector; } function flashloan(Structs.FlashInfo calldata info) external payable nonReentrant { β¦ } else if (info.tokenType == IDelegateRegistry.DelegationType.ERC1155) { RegistryHelpers.revertERC1155FlashAmountUnavailable(delegateRegistry, info); TransferHelpers.checkERC1155BeforePull(erc1155PullAuthorization, info.amount); + erc1155contract = info.tokenContract; IERC1155(info.tokenContract).safeTransferFrom(address(this), info.receiver, info.tokenId, info.amount, ""); Helpers.revertOnCallingInvalidFlashloan(info); TransferHelpers.pullERC1155AfterCheck(erc1155PullAuthorization, info.amount, info.tokenContract, info.tokenId); + erc1155contract = address(0); } }
Token-Transfer
#0 - 0xfoobar
2023-09-20T22:45:45Z
Need to double-check this but issue is plausible
#1 - c4-sponsor
2023-09-20T22:45:56Z
0xfoobar (sponsor) confirmed
#2 - GalloDaSballo
2023-09-29T08:52:53Z
There's 2 sides to this finding:
Impact -> Impact doesn't seem to be fully explored, this similar idea of anyone being able to call the onReceived is fairly widespread but the finding simply uses it to cause a revert which is arguably just a grief
The lack of validation for the caller may be a component of a broader attack, since there's no verification that the intended ERC1155 is the one confirming the transfer
#3 - GalloDaSballo
2023-10-01T18:45:17Z
It is possible for an external contract to prevent the Flashloan to work
However, the control as to how that would happen is under the callers control
While there is a lack of check for toggling of erc1155Pulled.flag
this will cause the tx to revert
Due to the lowered impact, I'm downgrading to Qa
#4 - c4-judge
2023-10-01T18:45:21Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#5 - c4-judge
2023-10-02T11:42:42Z
GalloDaSballo marked the issue as grade-b
#6 - GalloDaSballo
2023-10-02T11:43:05Z
Awarding QA - B in spite of low score due to the finding being notable
#7 - DadeKuma
2023-10-05T13:41:05Z
I think that this issue is invalid.
onERC1155Received
will not revert only when the current state is ERC1155_PULLED
function onERC1155Received(address operator, address, uint256, uint256, bytes calldata) external returns (bytes4) { TransferHelpers.revertInvalidERC1155PullCheck(erc1155PullAuthorization, operator); return IERC1155Receiver.onERC1155Received.selector; } function revertInvalidERC1155PullCheck(Structs.Uint256 storage erc1155PullAuthorization, address operator) internal { if (!checkERC1155Pulled(erc1155PullAuthorization, operator)) revert Errors.ERC1155PullNotRequested(operator); } function checkERC1155Pulled(Structs.Uint256 storage erc1155Pulled, address operator) internal returns (bool) { if (erc1155Pulled.flag == ERC1155_PULLED && address(this) == operator) { erc1155Pulled.flag = ERC1155_NOT_PULLED; return true; } return false; }
State ERC1155_PULLED
is set here:
function checkERC1155BeforePull(Structs.Uint256 storage erc1155Pulled, uint256 pullAmount) internal { if (pullAmount == 0) revert Errors.WrongAmountForType(IDelegateRegistry.DelegationType.ERC1155, pullAmount); if (erc1155Pulled.flag == ERC1155_NOT_PULLED) { erc1155Pulled.flag = ERC1155_PULLED; } else { revert Errors.ERC1155Pulled(); } }
checkERC1155BeforePull
is called only during a flashloan.
function flashloan(Structs.FlashInfo calldata info) external payable nonReentrant { StorageHelpers.revertNotOperator(accountOperator, info.delegateHolder); if (info.tokenType == IDelegateRegistry.DelegationType.ERC721) { RegistryHelpers.revertERC721FlashUnavailable(delegateRegistry, info); IERC721(info.tokenContract).transferFrom(address(this), info.receiver, info.tokenId); Helpers.revertOnCallingInvalidFlashloan(info); TransferHelpers.checkERC721BeforePull(info.amount, info.tokenContract, info.tokenId); TransferHelpers.pullERC721AfterCheck(info.tokenContract, info.tokenId); } else if (info.tokenType == IDelegateRegistry.DelegationType.ERC20) { RegistryHelpers.revertERC20FlashAmountUnavailable(delegateRegistry, info); SafeERC20.safeTransfer(IERC20(info.tokenContract), info.receiver, info.amount); Helpers.revertOnCallingInvalidFlashloan(info); TransferHelpers.checkERC20BeforePull(info.amount, info.tokenContract, info.tokenId); TransferHelpers.pullERC20AfterCheck(info.tokenContract, info.amount); } else if (info.tokenType == IDelegateRegistry.DelegationType.ERC1155) { RegistryHelpers.revertERC1155FlashAmountUnavailable(delegateRegistry, info); > TransferHelpers.checkERC1155BeforePull(erc1155PullAuthorization, info.amount); IERC1155(info.tokenContract).safeTransferFrom(address(this), info.receiver, info.tokenId, info.amount, ""); Helpers.revertOnCallingInvalidFlashloan(info); TransferHelpers.pullERC1155AfterCheck(erc1155PullAuthorization, info.amount, info.tokenContract, info.tokenId); } }
This means that the info.receiver
must call a malicious contract after it receives the token, which then should send an ERC1155 to DelegateToken, just to make the transaction revert without gaining anything?
What if the malicious contract is the 1155 contract itself? Then the mitigation would not have any effect, as it wouldn't revert when DelegateToken receives the token. I don't think this is a valid issue, as it's not preventable + the impact is non-existent.
#8 - GalloDaSballo
2023-10-08T08:18:06Z
The finding highlights how the onERC1155Received
pattern, without verifying the caller can be griefed
The impact falls under self-rekt / gotcha as the contract that is interacted with must be malicious
Interestingly enough this could be used to prevent claims from flashloans if the token is ERC1155
I believe the finding to be notable enough to warrant it being added to the report