Skip to content

Commit fac5dd1

Browse files
nicholaspaimrice32pxrl
authored
improve(SpokePool): _depositV3 interprets exclusivityParameter as an offset, or a timestamp (#670)
* Revert "feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583)" This reverts commit 9d21d1b. * Reapply "feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583)" This reverts commit d363bf0. * add deposit nonces to 7683 Signed-off-by: Matt Rice <[email protected]> * fix Signed-off-by: Matt Rice <[email protected]> * WIP Signed-off-by: Matt Rice <[email protected]> * feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583) * fix(SpokePool): Apply exclusivity consistently The new relative exclusivity check has not been propagated to fillV3RelayWithUpdatedDeposit(). Identified via test case failures in the relayer. Signed-off-by: Paul <[email protected]> * Also check on slow fill requests * Update contracts/SpokePool.sol * lint * Update * Add pure * Fix * Add tests * improve(SpokePool): _depositV3 interprets `exclusivityParameter` as 0, an offset, or a timestamp There should be a way for the deposit transaction to remove chain re-org risk affecting the block.timestamp by allowing the caller to set a fixed `exclusivityDeadline` value. This supports the existing behavior where the `exclusivityDeadline` is always emitted as its passed in. The new behavior is that if the `exclusivityParameter`, which replaces the `exclusivityDeadlineOffset` parameter, is 0 or greater than 1 year in seconds, then the `exclusivityDeadline` is equal to this parameter. Otherwise, its interpreted by `_depositV3()` as an offset. The offset would be useful in cases where the origin chain will not re-org, for example. * Update SpokePool.sol * Update SpokePool.Relay.ts * Update SpokePool.SlowRelay.ts * Update contracts/SpokePool.sol Co-authored-by: Paul <[email protected]> * Update SpokePool.sol * Update contracts/SpokePool.sol * rebase * Update SpokePool.sol --------- Signed-off-by: Matt Rice <[email protected]> Signed-off-by: Paul <[email protected]> Co-authored-by: Matt Rice <[email protected]> Co-authored-by: Paul <[email protected]>
1 parent 5fe9e35 commit fac5dd1

File tree

6 files changed

+301
-92
lines changed

6 files changed

+301
-92
lines changed

contracts/SpokePool.sol

Lines changed: 125 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ abstract contract SpokePool is
144144
// token for the input token. By using this magic value, off-chain validators do not have to keep
145145
// this event in their lookback window when querying for expired deposts.
146146
uint32 public constant INFINITE_FILL_DEADLINE = type(uint32).max;
147+
148+
// One year in seconds. If `exclusivityParameter` is set to a value less than this, then the emitted
149+
// exclusivityDeadline in a deposit event will be set to the current time plus this value.
150+
uint32 public constant MAX_EXCLUSIVITY_PERIOD_SECONDS = 31_536_000;
147151
/****************************************
148152
* EVENTS *
149153
****************************************/
@@ -429,9 +433,8 @@ abstract contract SpokePool is
429433
/**
430434
* @notice Previously, this function allowed the caller to specify the exclusivityDeadline, otherwise known as the
431435
* as exact timestamp on the destination chain before which only the exclusiveRelayer could fill the deposit. Now,
432-
* the caller is expected to pass in an exclusivityPeriod which is the number of seconds to be added to the
433-
* block.timestamp to produce the exclusivityDeadline. This allows the caller to ignore any latency associated
434-
* with this transaction being mined and propagating this transaction to the miner.
436+
* the caller is expected to pass in a number that will be interpreted either as an offset or a fixed
437+
* timestamp depending on its value.
435438
* @notice Request to bridge input token cross chain to a destination chain and receive a specified amount
436439
* of output tokens. The fee paid to relayers and the system should be captured in the spread between output
437440
* amount and input amount when adjusted to be denominated in the input token. A relayer on the destination
@@ -470,9 +473,17 @@ abstract contract SpokePool is
470473
* @param fillDeadline The deadline for the relayer to fill the deposit. After this destination chain timestamp,
471474
* the fill will revert on the destination chain. Must be set between [currentTime, currentTime + fillDeadlineBuffer]
472475
* where currentTime is block.timestamp on this chain or this transaction will revert.
473-
* @param exclusivityPeriod Added to the current time to set the exclusive relayer deadline,
474-
* which is the deadline for the exclusiveRelayer to fill the deposit. After this destination chain timestamp,
475-
* anyone can fill the deposit.
476+
* @param exclusivityParameter This value is used to set the exclusivity deadline timestamp in the emitted deposit
477+
* event. Before this destinationchain timestamp, only the exclusiveRelayer (if set to a non-zero address),
478+
* can fill this deposit. There are three ways to use this parameter:
479+
* 1. NO EXCLUSIVITY: If this value is set to 0, then a timestamp of 0 will be emitted,
480+
* meaning that there is no exclusivity period.
481+
* 2. OFFSET: If this value is less than MAX_EXCLUSIVITY_PERIOD_SECONDS, then add this value to
482+
* the block.timestamp to derive the exclusive relayer deadline. Note that using the parameter in this way
483+
* will expose the filler of the deposit to the risk that the block.timestamp of this event gets changed
484+
* due to a chain-reorg, which would also change the exclusivity timestamp.
485+
* 3. TIMESTAMP: Otherwise, set this value as the exclusivity deadline timestamp.
486+
* which is the deadline for the exclusiveRelayer to fill the deposit.
476487
* @param message The message to send to the recipient on the destination chain if the recipient is a contract.
477488
* If the message is not empty, the recipient contract must implement handleV3AcrossMessage() or the fill will revert.
478489
*/
@@ -487,62 +498,23 @@ abstract contract SpokePool is
487498
address exclusiveRelayer,
488499
uint32 quoteTimestamp,
489500
uint32 fillDeadline,
490-
uint32 exclusivityPeriod,
501+
uint32 exclusivityParameter,
491502
bytes calldata message
492503
) public payable override nonReentrant unpausedDeposits {
493-
// Check that deposit route is enabled for the input token. There are no checks required for the output token
494-
// which is pulled from the relayer at fill time and passed through this contract atomically to the recipient.
495-
if (!enabledDepositRoutes[inputToken][destinationChainId]) revert DisabledRoute();
496-
497-
// Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage.
498-
// It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the
499-
// SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp
500-
// within the configured buffer. The owner should pause deposits/fills if this is undesirable.
501-
// This will underflow if quoteTimestamp is more than depositQuoteTimeBuffer;
502-
// this is safe but will throw an unintuitive error.
503-
504-
// slither-disable-next-line timestamp
505-
uint256 currentTime = getCurrentTime();
506-
if (currentTime - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp();
507-
508-
// fillDeadline is relative to the destination chain.
509-
// Don’t allow fillDeadline to be more than several bundles into the future.
510-
// This limits the maximum required lookback for dataworker and relayer instances.
511-
// Also, don't allow fillDeadline to be in the past. This poses a potential UX issue if the destination
512-
// chain time keeping and this chain's time keeping are out of sync but is not really a practical hurdle
513-
// unless they are significantly out of sync or the depositor is setting very short fill deadlines. This latter
514-
// situation won't be a problem for honest users.
515-
if (fillDeadline < currentTime || fillDeadline > currentTime + fillDeadlineBuffer) revert InvalidFillDeadline();
516-
517-
// If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the
518-
// transaction then the user is sending the native token. In this case, the native token should be
519-
// wrapped.
520-
if (inputToken == address(wrappedNativeToken) && msg.value > 0) {
521-
if (msg.value != inputAmount) revert MsgValueDoesNotMatchInputAmount();
522-
wrappedNativeToken.deposit{ value: msg.value }();
523-
// Else, it is a normal ERC20. In this case pull the token from the caller as per normal.
524-
// Note: this includes the case where the L2 caller has WETH (already wrapped ETH) and wants to bridge them.
525-
// In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action.
526-
} else {
527-
// msg.value should be 0 if input token isn't the wrapped native token.
528-
if (msg.value != 0) revert MsgValueDoesNotMatchInputAmount();
529-
IERC20Upgradeable(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount);
530-
}
531-
532-
emit V3FundsDeposited(
504+
_depositV3(
505+
depositor,
506+
recipient,
533507
inputToken,
534508
outputToken,
535509
inputAmount,
536510
outputAmount,
537511
destinationChainId,
512+
exclusiveRelayer,
538513
// Increment count of deposits so that deposit ID for this spoke pool is unique.
539514
numberOfDeposits++,
540515
quoteTimestamp,
541516
fillDeadline,
542-
uint32(currentTime) + exclusivityPeriod,
543-
depositor,
544-
recipient,
545-
exclusiveRelayer,
517+
exclusivityParameter,
546518
message
547519
);
548520
}
@@ -763,7 +735,9 @@ abstract contract SpokePool is
763735
* - fillDeadline The deadline for the caller to fill the deposit. After this timestamp,
764736
* the fill will revert on the destination chain.
765737
* - exclusivityDeadline: The deadline for the exclusive relayer to fill the deposit. After this
766-
* timestamp, anyone can fill this deposit.
738+
* timestamp, anyone can fill this deposit. Note that if this value was set in depositV3 by adding an offset
739+
* to the deposit's block.timestamp, there is re-org risk for the caller of this method because the event's
740+
* block.timestamp can change. Read the comments in `depositV3` about the `exclusivityParameter` for more details.
767741
* - message The message to send to the recipient if the recipient is a contract that implements a
768742
* handleV3AcrossMessage() public function
769743
* @param repaymentChainId Chain of SpokePool where relayer wants to be refunded after the challenge window has
@@ -778,7 +752,7 @@ abstract contract SpokePool is
778752
// Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right
779753
// to fill the relay.
780754
if (
781-
_fillIsExclusive(relayData.exclusiveRelayer, relayData.exclusivityDeadline, uint32(getCurrentTime())) &&
755+
_fillIsExclusive(relayData.exclusivityDeadline, uint32(getCurrentTime())) &&
782756
relayData.exclusiveRelayer != msg.sender
783757
) {
784758
revert NotExclusiveRelayer();
@@ -824,7 +798,7 @@ abstract contract SpokePool is
824798
// Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right
825799
// to fill the relay.
826800
if (
827-
_fillIsExclusive(relayData.exclusiveRelayer, relayData.exclusivityDeadline, uint32(getCurrentTime())) &&
801+
_fillIsExclusive(relayData.exclusivityDeadline, uint32(getCurrentTime())) &&
828802
relayData.exclusiveRelayer != msg.sender
829803
) {
830804
revert NotExclusiveRelayer();
@@ -873,7 +847,7 @@ abstract contract SpokePool is
873847
// fast fill within this deadline. Moreover, the depositor should expect to get *fast* filled within
874848
// this deadline, not slow filled. As a simplifying assumption, we will not allow slow fills to be requested
875849
// during this exclusivity period.
876-
if (_fillIsExclusive(relayData.exclusiveRelayer, relayData.exclusivityDeadline, currentTime)) {
850+
if (_fillIsExclusive(relayData.exclusivityDeadline, currentTime)) {
877851
revert NoSlowFillsInExclusivityWindow();
878852
}
879853
if (relayData.fillDeadline < currentTime) revert ExpiredFillDeadline();
@@ -1048,6 +1022,99 @@ abstract contract SpokePool is
10481022
* INTERNAL FUNCTIONS *
10491023
**************************************/
10501024

1025+
function _depositV3(
1026+
address depositor,
1027+
address recipient,
1028+
address inputToken,
1029+
address outputToken,
1030+
uint256 inputAmount,
1031+
uint256 outputAmount,
1032+
uint256 destinationChainId,
1033+
address exclusiveRelayer,
1034+
uint32 depositId,
1035+
uint32 quoteTimestamp,
1036+
uint32 fillDeadline,
1037+
uint32 exclusivityParameter,
1038+
bytes calldata message
1039+
) internal {
1040+
// Check that deposit route is enabled for the input token. There are no checks required for the output token
1041+
// which is pulled from the relayer at fill time and passed through this contract atomically to the recipient.
1042+
if (!enabledDepositRoutes[inputToken][destinationChainId]) revert DisabledRoute();
1043+
1044+
// Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage.
1045+
// It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the
1046+
// SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp
1047+
// within the configured buffer. The owner should pause deposits/fills if this is undesirable.
1048+
// This will underflow if quoteTimestamp is more than depositQuoteTimeBuffer;
1049+
// this is safe but will throw an unintuitive error.
1050+
1051+
// slither-disable-next-line timestamp
1052+
uint256 currentTime = getCurrentTime();
1053+
if (currentTime - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp();
1054+
1055+
// fillDeadline is relative to the destination chain.
1056+
// Don’t allow fillDeadline to be more than several bundles into the future.
1057+
// This limits the maximum required lookback for dataworker and relayer instances.
1058+
// Also, don't allow fillDeadline to be in the past. This poses a potential UX issue if the destination
1059+
// chain time keeping and this chain's time keeping are out of sync but is not really a practical hurdle
1060+
// unless they are significantly out of sync or the depositor is setting very short fill deadlines. This latter
1061+
// situation won't be a problem for honest users.
1062+
if (fillDeadline < currentTime || fillDeadline > currentTime + fillDeadlineBuffer) revert InvalidFillDeadline();
1063+
1064+
// There are three cases for setting the exclusivity deadline using the exclusivity parameter:
1065+
// 1. If this parameter is 0, then there is no exclusivity period and emit 0 for the deadline. This
1066+
// means that fillers of this deposit do not have to worry about the block.timestamp of this event changing
1067+
// due to re-orgs when filling this deposit.
1068+
// 2. If the exclusivity parameter is less than or equal to MAX_EXCLUSIVITY_PERIOD_SECONDS, then the exclusivity
1069+
// deadline is set to the block.timestamp of this event plus the exclusivity parameter. This means that the
1070+
// filler of this deposit assumes re-org risk when filling this deposit because the block.timestamp of this
1071+
// event affects the exclusivity deadline.
1072+
// 3. Otherwise, interpret this parameter as a timestamp and emit it as the exclusivity deadline. This means
1073+
// that the filler of this deposit will not assume re-org risk related to the block.timestamp of this
1074+
// event changing.
1075+
uint32 exclusivityDeadline = exclusivityParameter;
1076+
if (exclusivityDeadline > 0) {
1077+
if (exclusivityDeadline <= MAX_EXCLUSIVITY_PERIOD_SECONDS) {
1078+
exclusivityDeadline += uint32(currentTime);
1079+
}
1080+
1081+
// As a safety measure, prevent caller from inadvertently locking funds during exclusivity period
1082+
// by forcing them to specify an exclusive relayer.
1083+
if (exclusiveRelayer == address(0)) revert InvalidExclusiveRelayer();
1084+
}
1085+
1086+
// If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the
1087+
// transaction then the user is sending the native token. In this case, the native token should be
1088+
// wrapped.
1089+
if (inputToken == address(wrappedNativeToken) && msg.value > 0) {
1090+
if (msg.value != inputAmount) revert MsgValueDoesNotMatchInputAmount();
1091+
wrappedNativeToken.deposit{ value: msg.value }();
1092+
// Else, it is a normal ERC20. In this case pull the token from the caller as per normal.
1093+
// Note: this includes the case where the L2 caller has WETH (already wrapped ETH) and wants to bridge them.
1094+
// In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action.
1095+
} else {
1096+
// msg.value should be 0 if input token isn't the wrapped native token.
1097+
if (msg.value != 0) revert MsgValueDoesNotMatchInputAmount();
1098+
IERC20Upgradeable(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount);
1099+
}
1100+
1101+
emit V3FundsDeposited(
1102+
inputToken,
1103+
outputToken,
1104+
inputAmount,
1105+
outputAmount,
1106+
destinationChainId,
1107+
depositId,
1108+
quoteTimestamp,
1109+
fillDeadline,
1110+
exclusivityDeadline,
1111+
depositor,
1112+
recipient,
1113+
exclusiveRelayer,
1114+
message
1115+
);
1116+
}
1117+
10511118
function _deposit(
10521119
address depositor,
10531120
address recipient,
@@ -1359,13 +1426,9 @@ abstract contract SpokePool is
13591426
}
13601427
}
13611428

1362-
// Determine whether the combination of exlcusiveRelayer and exclusivityDeadline implies active exclusivity.
1363-
function _fillIsExclusive(
1364-
address exclusiveRelayer,
1365-
uint32 exclusivityDeadline,
1366-
uint32 currentTime
1367-
) internal pure returns (bool) {
1368-
return exclusivityDeadline >= currentTime && exclusiveRelayer != address(0);
1429+
// Determine whether the exclusivityDeadline implies active exclusivity.
1430+
function _fillIsExclusive(uint32 exclusivityDeadline, uint32 currentTime) internal pure returns (bool) {
1431+
return exclusivityDeadline >= currentTime;
13691432
}
13701433

13711434
// Implementing contract needs to override this to ensure that only the appropriate cross chain admin can execute

contracts/interfaces/V3SpokePoolInterface.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ interface V3SpokePoolInterface {
222222
error DisabledRoute();
223223
error InvalidQuoteTimestamp();
224224
error InvalidFillDeadline();
225-
error InvalidExclusivityDeadline();
225+
error InvalidExclusiveRelayer();
226226
error MsgValueDoesNotMatchInputAmount();
227227
error NotExclusiveRelayer();
228228
error NoSlowFillsInExclusivityWindow();

0 commit comments

Comments
 (0)