Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: relay old failed messages after migration #111

Merged
merged 31 commits into from
Jul 25, 2024

Conversation

hexshire
Copy link
Member

@hexshire hexshire commented Jul 16, 2024

@@ -207,6 +211,7 @@ contract L2OpUSDCBridgeAdapter is IL2OpUSDCBridgeAdapter, OpUSDCBridgeAdapter {
* @param _amount The amount of tokens to mint
*/
function receiveMessage(address _user, uint256 _amount) external override onlyLinkedAdapter {
if (isMigrated) revert IOpUSDCBridgeAdapter_Migrated();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was talking a bit more with spearbit about this, and was thinking we could do the following

As L1 should still hold sufficient funds, what if we trigger a receiveMessage on L1, so we send a new message back to L1 to withdraw on the home chain instead of reverting, what do you guys think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also since the same spearbit ticket has this can you do the following as well in this PR:

  • Remove the L2 Adapter as a minter in the receiveMigrateToNative
  • add the updateMasterMinter call as a blacklisted selector in callUsdcTransaction (double check the actual fn name)

ref:
https://cantina.xyz/code/4b344c50-c217-4624-9e08-4298ffb5df47/findings/21?severity=medium

https://cantina.xyz/code/4b344c50-c217-4624-9e08-4298ffb5df47/findings/4?severity=medium

Copy link
Contributor

@0xDiscotech 0xDiscotech Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add an integration test also? Testing this flow:

  • User replays a failed mint message after migration happened
  • No amount is minted but instead a message is sent to L1 back so the user can withdraw his funds

Copy link
Member Author

@hexshire hexshire Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@excaliborr

Was talking a bit more with spearbit about this, and was thinking we could do the following

As L1 should still hold sufficient funds, what if we trigger a receiveMessage on L1, so we send a new message back to L1 to withdraw on the home chain instead of reverting, what do you guys think?

To make this work we should avoid burning the balance of the l1adapter, right? The following piece of code should be changed. We should burn only the equivalent to the totalSupply on the L2

      uint256 _balanceOf = IUSDC(USDC).balanceOf(address(this));
      _burnAmount = _burnAmount > _balanceOf ? _balanceOf : _burnAmount;

Copy link
Member Author

@hexshire hexshire Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we also need to add the caller in the receiveMessage function and we also need a minGasLimit to return the funds to the right user. I think is too much for this PR.

  function receiveMessage(address _user, uint256 _amount, address _caller) external override onlyLinkedAdapter {
    if (isMigrated) {
      // Send the message to the linked adapter
      ICrossDomainMessenger(MESSENGER).sendMessage(
        LINKED_ADAPTER, abi.encodeWithSignature('receiveMessage(address,uint256)', _caller, _amount), _minGasLimit
      );
    } else {
      // Mint the tokens to the user
      try IUSDC(USDC).mint(_user, _amount) {
        emit MessageReceived(_user, _amount, MESSENGER);
      } catch {
        userBlacklistedFunds[_user] += _amount;
        emit MessageFailed(_user, _amount);
      }
    }
  }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just use _user instead of _caller no need for a new param

Copy link
Contributor

@excaliborr excaliborr Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo hardcoding a _minGasLimit for this edgecase should be fine as its a super edgecase, I would just it to like 150_000, if for some reason it does fail, they can replay it on L1, you could also try using something like gas() as well to just copy what its being called with now, but this can be inconsistent

Just be sure to document the reason for hardcoding it as well, which is to not add unneccesary overhead in the 99% of messages

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can´t use _user to return the funds because from L1 you are not sending the funds to the msg.sender you are picking a desired address, in order to return the funds we need to know who sent the original message and we also need to set the minGasLimit since the user will relay a receiveMessage but that one will create another receiveMessage that will go to L1 to return the funds.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree using _caller will be more consistent as the target address could be a contract which might not exist on L1, so lets add this extra param then.

@hexshire hexshire self-assigned this Jul 23, 2024
@@ -125,7 +127,7 @@ contract L2OpUSDCBridgeAdapter is IL2OpUSDCBridgeAdapter, OpUSDCBridgeAdapter {
*/
function receiveResumeMessaging() external onlyLinkedAdapter {
// NOTE: This is safe because this message can only be received when messaging is not deprecated on the L1 messenger
isMessagingDisabled = false;
messengerStatus = Status.Active;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking more about this lets add a sanity check that messengerStatus != Deprecated it doesnt hurt and helps be safe against potential sequencer ordering bugs

ICrossDomainMessenger(MESSENGER).sendMessage(
LINKED_ADAPTER, abi.encodeCall(IOpUSDCBridgeAdapter.receiveMessage, (_spender, _spender, _amount)), 0
);
}
Copy link
Contributor

@excaliborr excaliborr Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be an if..else block, so the try is only executed if the if is false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add a comment for why we send to spender instead of user

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also dont use 0 as its guaranteed to fail, put like 150_000 which should usually pass

@hexshire hexshire requested a review from excaliborr July 25, 2024 02:45
if (messengerStatus == Status.Deprecated) {
// Return the funds to the address where the tokens were sent from
ICrossDomainMessenger(MESSENGER).sendMessage(
LINKED_ADAPTER, abi.encodeCall(IOpUSDCBridgeAdapter.receiveMessage, (_spender, _spender, _amount)), 150_000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment like

// We send funds to the spender incase the target on L2 is a contract that doesn't exist on L1

*/
function receiveMessage(address _user, uint256 _amount) external override onlyLinkedAdapter {
function receiveMessage(address _user, address, uint256 _amount) external override onlyLinkedAdapter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still call this address _spender, and put in the natspec its unused on L1, but is set to match the universal selector

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or have one receiveMessage function per adapter?

Copy link
Member Author

@hexshire hexshire Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it matches the universal selector this way and we are not adding an unused var. Unless I add it and also add a linter comment to ignore it.

*/
function receiveMessage(address _user, uint256 _amount) external override onlyLinkedAdapter {
function receiveMessage(address _user, address, uint256 _amount) external override onlyLinkedAdapter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this unused address param for?

Suggested change
function receiveMessage(address _user, address, uint256 _amount) external override onlyLinkedAdapter {
function receiveMessage(address _user, uint256 _amount) external override onlyLinkedAdapter {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is mainly used on L2 and on sendMessage functions.

src/contracts/L2OpUSDCBridgeAdapter.sol Outdated Show resolved Hide resolved
* @param _amount The amount of tokens to mint
*/
function receiveMessage(address _user, uint256 _amount) external virtual;
function receiveMessage(address _user, address _spender, uint256 _amount) external virtual;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving this function to each adapter's itnerface, since one needs the _spender and the other one doesn't

*/
function receiveMessage(address _user, uint256 _amount) external override onlyLinkedAdapter {
function receiveMessage(address _user, address, uint256 _amount) external override onlyLinkedAdapter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or have one receiveMessage function per adapter?

/*///////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
///////////////////////////////////////////////////////////////*/
function _sendMessage(address _from, address _to, uint256 _amount, uint32 _minGasLimit) internal {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing NATSPEC here

@excaliborr excaliborr merged commit 39a260f into audit/spearbit Jul 25, 2024
4 checks passed
@excaliborr excaliborr deleted the fix/relay-failed-messages branch July 25, 2024 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants