@@ -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
0 commit comments