Closed code423n4 closed 2 years ago
Hm, I understand the logic here and appreciate it being laid out with such thought and detail.
I am not sure that I agree, however. One of the canonical examples (currently being used on our initial version of nxtp by lifi) is to use the calldata
to execute a swap to allow any to any crosschain transfers by calling an AMM on the destination chain. For this use case, you would not be able to simply transfer
to the destination contract as the AMM contracts generally need a transferFrom
call (same thing applies to many DeFi protocols which require approvals).
Either way, you are making an assumption that the contract can handle funds sent directly to it, or it needs an approval. From our experience, an approval is easier to manage. In the case of an approval, you can always write a thin wrapper contract to accept the funds, then transfer them directly to another contract before executing some calldata. In contrast, if you send funds directly to the contract and it has to approve a different one, you must put all of that logic into a fallback
function.
I actually agree with the sponsor here, either way the contract needs to be able to handle the funds and approving the contract seems to provide more flexibility about how those funds might be utilised. Marking as invalid
because the sponsor's design decision makes more sense.
Lines of code
https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L819 https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/helpers/Executor.sol#L143
Vulnerability details
Background
BridgeFacet.xcall
function can be used to perform a cross-chain transfer. During the transfer, user can also specify acallData
that is execute on the receiving chainProof-of-Concept
To execute a transfer on the destination domain, relayers will call the
BridgeFacet.execute
, which will in turn trigger theBridgeFacet._handleExecuteTransaction
function.Within the
BridgeFacet._handleExecuteTransaction
function, if the transfer is initiated with acallData
, it will proceed to call thes.executor.execute
function.https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L819
The
s.executor.execute
code references to the followingExecutor.execute
. This function is responsible for the arbitrary call data on a given address and transfer the tokens to the recipient.However, it was observed that in Line 143, instead of transfering the tokens directly to the destination address (
_args.to
), it choses to increase the allowance of destination address and expects the destination contract to perform aERC20.transferFrom
operation to retrieve their tokens from bridge.https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/helpers/Executor.sol#L143
Impact
User tokens will be stuck in Connext bridge.
Recommended Mitigation Steps
Based on the comments, it was understood that the rational for such an approach is that they are unclear if the destination contract could handle the funds transferred directly to them.
Although it is a good intention to prevent the tokens from being stuck in the destination/user contract, this is an unlikely scenario. This approach will cause more harm than benefits. It is the responsibility of the users to know that their destination contract can process the received tokens. If the tokens get stuck in the destination contract, the responsibility lies with the users, not the protocol.
The current approach is also not aligned with the default ERC20 or Ether transfer pattern. By default, all tokens or values transfer will be sent directly to the destination address, and this pattern should be followed as users will expect Connext's transfer to behave the same.
Protocol should not make assumption on the destination contract, and it should simply deliver the tokens to the destination. The assumption might be right on some contracts, but wrong in the rest of the contracts, therefore no assumption should be made at all.
Within the blockchain, smart contracts are always considered as immutable, therefore, majority of the contracts are built not to be upgradable. When a user transfers tokens to a destination contract, they will expect the tokens to be sent directly to the destination contract instead of having to manually retrieve them from Connext bridge. As a result, majority of the existing contracts will not have features to retrieve their tokens from Connext bridge and are immutable, thus their tokens will end up stuck in Connext bridge. If this incident happens, the responsibilty would gravitate towards Connext since it has made an assumption on behalf of the users beforehand.
Additionally, this also means that with the current approach, any existing contract that is not immutable will not be able to use Connext bridge for token transfer (with callData) because they have no way to include new logic to retrieve token from Connext bridge, and they also do not know what will be the Connext bridge address at this point of time since it has not been deployed to the production yet (e.g. Ethereum mainnet)
In summary, update the
execute
function to simply transfer the tokens to the destination contract/address