-
Notifications
You must be signed in to change notification settings - Fork 6
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
Conversation
Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com>
@@ -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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
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;
There was a problem hiding this comment.
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);
}
}
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wdyt @0xDiscotech
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@@ -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; |
There was a problem hiding this comment.
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 | ||
); | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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
a591a95
to
56f49a3
Compare
*/ | ||
function receiveMessage(address _user, uint256 _amount) external override onlyLinkedAdapter { | ||
function receiveMessage(address _user, address, uint256 _amount) external override onlyLinkedAdapter { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
function receiveMessage(address _user, address, uint256 _amount) external override onlyLinkedAdapter { | |
function receiveMessage(address _user, uint256 _amount) external override onlyLinkedAdapter { |
There was a problem hiding this comment.
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.
* @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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com>
/*/////////////////////////////////////////////////////////////// | ||
INTERNAL FUNCTIONS | ||
///////////////////////////////////////////////////////////////*/ | ||
function _sendMessage(address _from, address _to, uint256 _amount, uint32 _minGasLimit) internal { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing NATSPEC here
Based on Cantina comments