-
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
Changes from 3 commits
799d438
80b080e
abe1ca9
27a67ec
ec2cfa9
44ac914
2cd3bbd
0808820
da5cc18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,9 @@ pragma solidity ^0.8.0; | |
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import "@openzeppelin/contracts/utils/Address.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
|
||
import "@uma/core/contracts/common/implementation/Testable.sol"; | ||
import "@uma/core/contracts/common/implementation/Lockable.sol"; | ||
import "@uma/core/contracts/common/implementation/MultiCaller.sol"; | ||
|
@@ -204,12 +207,6 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller { | |
uint256 maxTokensToSend, | ||
uint256 repaymentChain | ||
) public { | ||
// We limit the relay fees to prevent the user spending all their funds on fees. | ||
require( | ||
relayerFeePct <= 0.5e18 && realizedLpFeePct <= 0.5e18 && (relayerFeePct + realizedLpFeePct) < 1e18, | ||
"invalid fees" | ||
); | ||
|
||
// Each relay attempt is mapped to the hash of data uniquely identifying it, which includes the deposit data | ||
// such as the origin chain ID and the deposit ID, and the data in a relay attempt such as who the recipient | ||
// is, which chain and currency the recipient wants to receive funds on, and the relay fees. | ||
|
@@ -225,49 +222,130 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller { | |
}); | ||
bytes32 relayHash = _getRelayHash(relayData); | ||
|
||
_fillRelay(relayHash, relayData, relayerFeePct, maxTokensToSend, repaymentChain); | ||
} | ||
|
||
function _fillRelay( | ||
bytes32 relayHash, | ||
RelayData memory relayData, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a gas difference between this being There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. according to this answer, Also aside, when testing I noticed that declaring a variable as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that |
||
uint64 updatableRelayerFeePct, | ||
uint256 maxTokensToSend, | ||
uint256 repaymentChain | ||
) 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 commentThe reason will be displayed to describe this comment to others. Learn more. I think one of these checks is redundant. if you validate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then just check that: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true |
||
relayData.realizedLpFeePct <= 0.5e18 && | ||
(updatableRelayerFeePct + relayData.realizedLpFeePct) < 1e18, | ||
"invalid fees" | ||
); | ||
|
||
// Check that the caller is filling a non zero amount of the relay and the relay has not already been completely | ||
// filled. Note that the `relays` mapping will point to the amount filled so far for a particular `relayHash`, | ||
// so this will start at 0 and increment with each fill. | ||
require(maxTokensToSend > 0 && relayFills[relayHash] < totalRelayAmount, "Cannot send 0, or relay filled"); | ||
require(maxTokensToSend > 0 && relayFills[relayHash] < relayData.relayAmount, "Cannot send 0, or relay filled"); | ||
|
||
// Stores the equivalent amount to be sent by the relayer before fees have been taken out. | ||
uint256 fillAmountPreFees = _computeAmountPreFees(maxTokensToSend, (realizedLpFeePct + relayerFeePct)); | ||
uint256 fillAmountPreFees = _computeAmountPreFees( | ||
maxTokensToSend, | ||
(relayData.realizedLpFeePct + updatableRelayerFeePct) | ||
); | ||
|
||
// Adding brackets "stack too deep" solidity error. | ||
{ | ||
// If user's specified max amount to send is greater than the amount of the relay remaining pre-fees, | ||
// we'll pull exactly enough tokens to complete the relay. | ||
uint256 amountToSend = maxTokensToSend; | ||
if (totalRelayAmount - relayFills[relayHash] < fillAmountPreFees) { | ||
fillAmountPreFees = totalRelayAmount - relayFills[relayHash]; | ||
amountToSend = _computeAmountPostFees(fillAmountPreFees, (realizedLpFeePct + relayerFeePct)); | ||
if (relayData.relayAmount - relayFills[relayHash] < fillAmountPreFees) { | ||
fillAmountPreFees = relayData.relayAmount - relayFills[relayHash]; | ||
amountToSend = _computeAmountPostFees( | ||
fillAmountPreFees, | ||
relayData.realizedLpFeePct + updatableRelayerFeePct | ||
); | ||
} | ||
relayFills[relayHash] += fillAmountPreFees; | ||
// If relay token is weth then unwrap and send eth. | ||
if (destinationToken == address(weth)) { | ||
IERC20(destinationToken).safeTransferFrom(msg.sender, address(this), amountToSend); | ||
_unwrapWETHTo(payable(recipient), amountToSend); | ||
if (relayData.destinationToken == address(weth)) { | ||
IERC20(relayData.destinationToken).safeTransferFrom(msg.sender, address(this), amountToSend); | ||
_unwrapWETHTo(payable(relayData.recipient), amountToSend); | ||
// Else, this is a normal ERC20 token. Send to recipient. | ||
} else IERC20(destinationToken).safeTransferFrom(msg.sender, recipient, amountToSend); | ||
} else IERC20(relayData.destinationToken).safeTransferFrom(msg.sender, relayData.recipient, amountToSend); | ||
} | ||
|
||
emit FilledRelay( | ||
relayHash, | ||
relayData.relayAmount, | ||
relayFills[relayHash], | ||
fillAmountPreFees, | ||
repaymentChain, | ||
relayData.originChainId, | ||
relayData.depositId, | ||
relayData.relayerFeePct, | ||
relayData.realizedLpFeePct, | ||
relayData.destinationToken, | ||
msg.sender, | ||
relayData.depositor, | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. dont emit There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. haha yep this is just unreadable to get around stack too deep |
||
); | ||
} | ||
|
||
// We overload `fillRelay` logic to allow the relayer to optionally pass in an updated `relayerFeePct` and a signature | ||
// proving that the depositor agreed to the updated fee. | ||
struct UpdatedRelayerFeeData { | ||
// Note: This struct is used to make sure that the following overloaded `fillRelay` does not create a stack | ||
// too deep compile-time error. | ||
uint64 newRelayerFeePct; | ||
bytes32 depositorMessageHash; | ||
bytes depositorSignature; | ||
} | ||
|
||
function fillRelayUpdatedFee( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
address depositor, | ||
address recipient, | ||
address destinationToken, | ||
uint64 realizedLpFeePct, | ||
uint64 relayerFeePct, | ||
uint64 depositId, | ||
uint256 originChainId, | ||
uint256 totalRelayAmount, | ||
uint256 maxTokensToSend, | ||
uint256 repaymentChain, | ||
UpdatedRelayerFeeData memory updatedRelayerFeeData // Note: Ideally we don't pass in any complex objects to | ||
) | ||
public | ||
// public methods but I couldn't figure out a way to pass this in without encounering a stack too deep error. | ||
nonReentrant | ||
{ | ||
// Grouping the signature validation logic into brackets to address stack too deep error. | ||
{ | ||
// Depositor should have signed a hash of the relayer fee % to update to and information uniquely identifying | ||
// the deposit to relay. This ensures that this signature cannot be re-used for other deposits. The version | ||
// string is included as a precaution in case this contract is upgraded. | ||
// Note: we use encode instead of encodePacked because it is more secure, more in the "warning" section | ||
// here: https://docs.soliditylang.org/en/v0.8.11/abi-spec.html#non-standard-packed-mode | ||
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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. ah true! |
||
|
||
// Check the hash corresponding to the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] | ||
// JSON-RPC method as part of EIP-191. We use OZ's signature checker library with adds support for | ||
// EIP-1271 which can verify messages signed by smart contract wallets like Argent and Gnosis safes. | ||
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(expectedDepositorMessageHash); | ||
require( | ||
SignatureChecker.isValidSignatureNow( | ||
depositor, | ||
ethSignedMessageHash, | ||
updatedRelayerFeeData.depositorSignature | ||
), | ||
"invalid signature" | ||
); | ||
} | ||
|
||
// Now follow the default `fillRelay` flow with the updated fee and the original relay hash. | ||
RelayData memory relayData = RelayData({ | ||
depositor: depositor, | ||
recipient: recipient, | ||
destinationToken: destinationToken, | ||
realizedLpFeePct: realizedLpFeePct, | ||
relayerFeePct: relayerFeePct, | ||
depositId: depositId, | ||
originChainId: originChainId, | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. +1! |
||
} | ||
|
||
// This internal method should be called by an external "initializeRelayerRefund" function that validates the | ||
// cross domain sender is the HubPool. This validation step differs for each L2, which is why the implementation | ||
// specifics are left to the implementor of this abstract contract. | ||
|
@@ -323,6 +401,32 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller { | |
} | ||
} | ||
|
||
struct FillRelayEventData { | ||
bytes32 relayHash; | ||
RelayData relayData; | ||
uint64 updatableRelayerFeePct; | ||
uint256 fillAmountPreFees; | ||
uint256 repaymentChain; | ||
} | ||
|
||
function _emitFillRelay(FillRelayEventData memory eventData) internal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. exactly |
||
emit FilledRelay( | ||
eventData.relayHash, | ||
eventData.relayData.relayAmount, | ||
relayFills[eventData.relayHash], | ||
eventData.fillAmountPreFees, | ||
eventData.repaymentChain, | ||
eventData.relayData.originChainId, | ||
eventData.relayData.depositId, | ||
eventData.updatableRelayerFeePct, | ||
eventData.relayData.realizedLpFeePct, | ||
eventData.relayData.destinationToken, | ||
msg.sender, | ||
eventData.relayData.depositor, | ||
eventData.relayData.recipient | ||
); | ||
} | ||
|
||
// Added to enable the this contract to receive ETH. Used when unwrapping Weth. | ||
receive() external payable {} | ||
} |
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