-
Notifications
You must be signed in to change notification settings - Fork 50
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
feat(spoke-pool): Allow relayer and depositor to agree to update relay fee #27
Conversation
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.
Looks awesome! A few minor questions/comments
contracts/SpokePool.sol
Outdated
require(relayFills[relayHash] < totalRelayAmount, "filled relay"); | ||
|
||
// Depositor should have signed a hash of the relayer fee % to update to. | ||
bytes32 expectedDepositorMessageHash = keccak256(abi.encode(newRelayerFeePct)); |
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 need more than this to avoid replay attacks. For instance, if I agree to a relayer fee of 23% on a small relay, then I send a large relay, someone could take my signed message from the small relay and replay it to get a large fee from me in the in the large relay.
A few additional fields I think we need: depositId, chainId. We might also want include a constant value in front, like "ACROSS-V2 FEE 1.0" or something that we could increment if we ever updated spoke pools and ended up resetting the depositId counter.
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.
ah good point these are easy to include
contracts/SpokePool.sol
Outdated
@@ -268,6 +272,61 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller { | |||
); | |||
} | |||
|
|||
function increaseRelayFee( |
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.
Should the relayer be able to use this method to actually fill the relay? If not, how does it get connected with a particular fill? Or is the idea that we would swap out the relayerFeePct for every FilledRelay that was submitted afterward?
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.
The relayer in this implementation would call this method separately and it would not add to the fill. We could add an additonal method increaseRelayFeeAndFillRelay
(bad name) that would call fillRelay
AND modify the fee WDYT?
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.
relayer in this implementation would call this method separately and it would not add to the fill
How would this work? Wouldn't the subsequent fill still be using the old relayer fee (since that's the only one that would match the hash)? So in that case, too much money would be sent to the recipient, no?
We don't have to do it this way, but the way I had conceptualized this was that we'd have a fill method that's identical to the existing one with two additional args: bytes memory signature, uint64 updatedRelayerFeePct
. The signature would be verified against the provided fee, and the updated fee fee would be emitted in the FilledRelay
event (we could also include a second event to signify that this fee was overridden or something).
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.
So the relay hash would stay the same, with the old relayer fee, but the event would just change? (or have an additional one emitted)
I agree wth you that fillRelay
could be overridden with a separate interface
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.
Yeah, in the solution I had been thinking of, the relay hash/data would always be the same, the signature would be provided essentially as an override to the originally-set relayer fee.
So the event would be different and the amount sent to the recipient would be different (i.e. the relayFeePct used throughout the method).
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.
interesting let me try this
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.
ye, what @mrice32 describes fits with my understanding of this method. you can actually just overload the fillRelay
method with an extra param which optionally lets you change the originally provided relayFeePct
.
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.
The only problem with overloading is that overloading doesn't play nicely with ethers like it does with web3. Do you have a strong opinion about overloading versus creating a new function name?
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.
…with updating fee
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.
Looks good! Minor comments
contracts/SpokePool.sol
Outdated
bytes depositorSignature; | ||
} | ||
|
||
function fillRelayUpdatedFee( |
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.
nit: since this method is public, should it be moved up next to the fillRelay?
contracts/SpokePool.sol
Outdated
bytes32 expectedDepositorMessageHash = keccak256( | ||
abi.encode("ACROSS-V2-FEE-1.0", updatedRelayerFeeData.newRelayerFeePct, depositId, originChainId) | ||
); | ||
require(expectedDepositorMessageHash == updatedRelayerFeeData.depositorMessageHash, "incorrect new fee"); |
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.
Why do we need them to pass in the message hash? Aren't we already generating it from other data they've provided?
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.
ah true!
contracts/SpokePool.sol
Outdated
relayAmount: totalRelayAmount | ||
}); | ||
bytes32 relayHash = _getRelayHash(relayData); | ||
_fillRelay(relayHash, relayData, updatedRelayerFeeData.newRelayerFeePct, maxTokensToSend, repaymentChain); |
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.
+1!
uint256 repaymentChain; | ||
} | ||
|
||
function _emitFillRelay(FillRelayEventData memory eventData) 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.
Is this extra method here just to avoid stack too deep errors?
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.
exactly
contracts/SpokePool.sol
Outdated
|
||
function _fillRelay( | ||
bytes32 relayHash, | ||
RelayData memory relayData, |
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.
is there a gas difference between this being memory
and calldata
?
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.
according to this answer, calldata
is cheaper.
Also aside, when testing I noticed that declaring a variable as memory
does not contribute to the "stack too deep error", while calldata
does?
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.
Note that calldata
is only avaialble for external
methods. source
contracts/SpokePool.sol
Outdated
_fillRelay(relayHash, relayData, relayerFeePct, maxTokensToSend, repaymentChain); | ||
} | ||
|
||
function _fillRelay( |
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.
do you have a section for internal methods? should probs be located there.
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.
done
contracts/SpokePool.sol
Outdated
) internal { | ||
// We limit the relay fees to prevent the user spending all their funds on fees. | ||
require( | ||
updatableRelayerFeePct <= 0.5e18 && |
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 one of these checks is redundant. if you validate relayData.realizedLpFeePct
and (updatableRelayerFeePct + relayData.realizedLpFeePct) < 1e18
then it's impossible for updatableRelayerFeePct
to be >0.5e18
.
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 if realizedLpFeePct = 0
but updatableRelayerFeePct = 0.6
?
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.
then just check that: updatableRelayerFeePct <= 0.5e18 && relayData.realizedLpFeePct <= 0.5e18
?
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.
true
contracts/SpokePool.sol
Outdated
relayData.recipient | ||
// Needed to resolve stack too deep compile-time error | ||
_emitFillRelay( | ||
FillRelayEventData(relayHash, relayData, updatableRelayerFeePct, fillAmountPreFees, repaymentChain) |
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.
dont emit RelayData
. we dont want to emit structs. ideally what you had before was better so etherscan can parse it.
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 take that back. your method you call does this 🙏
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.
haha yep this is just unreadable to get around stack too deep
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.
LGTM!
// above keccak156 hash), then this will revert. | ||
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(expectedDepositorMessageHash); | ||
require( | ||
SignatureChecker.isValidSignatureNow(depositor, ethSignedMessageHash, depositorSignature), |
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.
Minor thing we should be aware of: since this allows for ERC1271 "contract signatures", this can make a call out to the caller under the hood. Shouldn't be a problem, just wanted to mention it.
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.
Hmmm thats a good point. As long as we reentrancy guard this method it should be ok though? I suppose also the calling contrct could call this contract back into fillRelay
which I don't think could cause issues, but we could easily remedy by moving all this signature verification logic AFTER _fillRelay
. This would also be more consistent perhaps with Check-effects-interaction pattern. WDYT?
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.
Reading more into the method, I really don't think this is a problem, and it's arguable that it's even an interaction, because it makes a staticcall (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/63b466901fb015538913f811c5112a2775042177/contracts/utils/cryptography/SignatureChecker.sol#L35), meaning no state can be modified anywhere inside that call (and subcalls). It's more akin to making a balanceOf call, which I think is considered a check rather than an interaction.
I think this is safe.
Remaining comment here: #27 (comment) @chrismaree wdyt? |
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.
LGTM!
// above keccak156 hash), then this will revert. | ||
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(expectedDepositorMessageHash); | ||
require( | ||
SignatureChecker.isValidSignatureNow(depositor, ethSignedMessageHash, depositorSignature), |
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.
Reading more into the method, I really don't think this is a problem, and it's arguable that it's even an interaction, because it makes a staticcall (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/63b466901fb015538913f811c5112a2775042177/contracts/utils/cryptography/SignatureChecker.sol#L35), meaning no state can be modified anywhere inside that call (and subcalls). It's more akin to making a balanceOf call, which I think is considered a check rather than an interaction.
I think this is safe.
…gnatures (#27) Co-authored-by: nicholaspai <[email protected]>
No description provided.