Skip to content

Commit c40c197

Browse files
committed
Reapply "feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583)"
This reverts commit d363bf0.
1 parent d363bf0 commit c40c197

File tree

4 files changed

+234
-52
lines changed

4 files changed

+234
-52
lines changed

contracts/SpokePool.sol

Lines changed: 166 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ abstract contract SpokePool is
125125

126126
bytes32 public constant UPDATE_V3_DEPOSIT_DETAILS_HASH =
127127
keccak256(
128-
"UpdateDepositDetails(uint32 depositId,uint256 originChainId,uint256 updatedOutputAmount,address updatedRecipient,bytes updatedMessage)"
128+
"UpdateDepositDetails(uint256 depositId,uint256 originChainId,uint256 updatedOutputAmount,address updatedRecipient,bytes updatedMessage)"
129129
);
130130

131131
// Default chain Id used to signify that no repayment is requested, for example when executing a slow fill.
@@ -549,59 +549,92 @@ abstract contract SpokePool is
549549
uint32 exclusivityPeriod,
550550
bytes calldata message
551551
) public payable override nonReentrant unpausedDeposits {
552-
// Check that deposit route is enabled for the input token. There are no checks required for the output token
553-
// which is pulled from the relayer at fill time and passed through this contract atomically to the recipient.
554-
if (!enabledDepositRoutes[inputToken][destinationChainId]) revert DisabledRoute();
555-
556-
// Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage.
557-
// It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the
558-
// SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp
559-
// within the configured buffer. The owner should pause deposits/fills if this is undesirable.
560-
// This will underflow if quoteTimestamp is more than depositQuoteTimeBuffer;
561-
// this is safe but will throw an unintuitive error.
562-
563-
// slither-disable-next-line timestamp
564-
uint256 currentTime = getCurrentTime();
565-
if (currentTime - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp();
566-
567-
// fillDeadline is relative to the destination chain.
568-
// Don’t allow fillDeadline to be more than several bundles into the future.
569-
// This limits the maximum required lookback for dataworker and relayer instances.
570-
// Also, don't allow fillDeadline to be in the past. This poses a potential UX issue if the destination
571-
// chain time keeping and this chain's time keeping are out of sync but is not really a practical hurdle
572-
// unless they are significantly out of sync or the depositor is setting very short fill deadlines. This latter
573-
// situation won't be a problem for honest users.
574-
if (fillDeadline < currentTime || fillDeadline > currentTime + fillDeadlineBuffer) revert InvalidFillDeadline();
575-
576-
// If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the
577-
// transaction then the user is sending the native token. In this case, the native token should be
578-
// wrapped.
579-
if (inputToken == address(wrappedNativeToken) && msg.value > 0) {
580-
if (msg.value != inputAmount) revert MsgValueDoesNotMatchInputAmount();
581-
wrappedNativeToken.deposit{ value: msg.value }();
582-
// Else, it is a normal ERC20. In this case pull the token from the caller as per normal.
583-
// Note: this includes the case where the L2 caller has WETH (already wrapped ETH) and wants to bridge them.
584-
// In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action.
585-
} else {
586-
// msg.value should be 0 if input token isn't the wrapped native token.
587-
if (msg.value != 0) revert MsgValueDoesNotMatchInputAmount();
588-
IERC20Upgradeable(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount);
589-
}
590-
591-
emit V3FundsDeposited(
552+
_depositV3(
553+
depositor,
554+
recipient,
592555
inputToken,
593556
outputToken,
594557
inputAmount,
595558
outputAmount,
596559
destinationChainId,
560+
exclusiveRelayer,
597561
// Increment count of deposits so that deposit ID for this spoke pool is unique.
562+
// @dev Implicitly casts from uint32 to uint256 by padding the left-most bytes with zeros. Guarantees
563+
// that the 24 most significant bytes are 0.
598564
numberOfDeposits++,
599565
quoteTimestamp,
600566
fillDeadline,
601-
uint32(currentTime) + exclusivityPeriod,
567+
uint32(getCurrentTime()) + exclusivityPeriod,
568+
message
569+
);
570+
}
571+
572+
/**
573+
* @notice See depositV3 for details. This function is identical to depositV3 except that it does not use the
574+
* global deposit ID counter as a deposit nonce, instead allowing the caller to pass in a deposit nonce. This
575+
* function is designed to be used by anyone who wants to pre-compute their resultant relay data hash, which
576+
* could be useful for filling a deposit faster and avoiding any risk of a relay hash unexpectedly changing
577+
* due to another deposit front-running this one and incrementing the global deposit ID counter.
578+
* @dev This is labeled "unsafe" because there is no guarantee that the depositId emitted in the resultant
579+
* V3FundsDeposited event is unique which means that the
580+
* corresponding fill might collide with an existing relay hash on the destination chain SpokePool,
581+
* which would make this deposit unfillable. In this case, the depositor would subsequently receive a refund
582+
* of `inputAmount` of `inputToken` on the origin chain after the fill deadline.
583+
* @dev On the destination chain, the hash of the deposit data will be used to uniquely identify this deposit, so
584+
* modifying any params in it will result in a different hash and a different deposit. The hash will comprise
585+
* all parameters to this function along with this chain's chainId(). Relayers are only refunded for filling
586+
* deposits with deposit hashes that map exactly to the one emitted by this contract.
587+
* @param depositNonce The nonce that uniquely identifies this deposit. This function will combine this parameter
588+
* with the msg.sender address to create a unique uint256 depositNonce and ensure that the msg.sender cannot
589+
* use this function to front-run another depositor's unsafe deposit. This function guarantees that the resultant
590+
* deposit nonce will not collide with a safe uint256 deposit nonce whose 24 most significant bytes are always 0.
591+
* @param depositor See identically named parameter in depositV3() comments.
592+
* @param recipient See identically named parameter in depositV3() comments.
593+
* @param inputToken See identically named parameter in depositV3() comments.
594+
* @param outputToken See identically named parameter in depositV3() comments.
595+
* @param inputAmount See identically named parameter in depositV3() comments.
596+
* @param outputAmount See identically named parameter in depositV3() comments.
597+
* @param destinationChainId See identically named parameter in depositV3() comments.
598+
* @param exclusiveRelayer See identically named parameter in depositV3() comments.
599+
* @param quoteTimestamp See identically named parameter in depositV3() comments.
600+
* @param fillDeadline See identically named parameter in depositV3() comments.
601+
* @param exclusivityPeriod See identically named parameter in depositV3() comments.
602+
* @param message See identically named parameter in depositV3() comments.
603+
*/
604+
function unsafeDepositV3(
605+
address depositor,
606+
address recipient,
607+
address inputToken,
608+
address outputToken,
609+
uint256 inputAmount,
610+
uint256 outputAmount,
611+
uint256 destinationChainId,
612+
address exclusiveRelayer,
613+
uint256 depositNonce,
614+
uint32 quoteTimestamp,
615+
uint32 fillDeadline,
616+
uint32 exclusivityPeriod,
617+
bytes calldata message
618+
) public payable nonReentrant unpausedDeposits {
619+
// @dev Create the uint256 deposit ID by concatenating the msg.sender and depositor address with the inputted
620+
// depositNonce parameter. The resultant 32 byte string will be hashed and then casted to an "unsafe"
621+
// uint256 deposit ID. The probability that the resultant ID collides with a "safe" deposit ID is
622+
// equal to the chance that the first 28 bytes of the hash are 0, which is too small for us to consider.
623+
624+
uint256 depositId = getUnsafeDepositId(msg.sender, depositor, depositNonce);
625+
_depositV3(
602626
depositor,
603627
recipient,
628+
inputToken,
629+
outputToken,
630+
inputAmount,
631+
outputAmount,
632+
destinationChainId,
604633
exclusiveRelayer,
634+
depositId,
635+
quoteTimestamp,
636+
fillDeadline,
637+
uint32(getCurrentTime()) + exclusivityPeriod,
605638
message
606639
);
607640
}
@@ -755,7 +788,7 @@ abstract contract SpokePool is
755788
*/
756789
function speedUpV3Deposit(
757790
address depositor,
758-
uint32 depositId,
791+
uint256 depositId,
759792
uint256 updatedOutputAmount,
760793
address updatedRecipient,
761794
bytes calldata updatedMessage,
@@ -1084,10 +1117,99 @@ abstract contract SpokePool is
10841117
return block.timestamp; // solhint-disable-line not-rely-on-time
10851118
}
10861119

1120+
/**
1121+
* @notice Returns the deposit ID for an unsafe deposit. This function is used to compute the deposit ID
1122+
* in unsafeDepositV3 and is provided as a convenience.
1123+
* @dev msgSenderand depositor are both used as inputs to allow passthrough depositors to create unique
1124+
* deposit hash spaces for unique depositors.
1125+
* @param msgSender The caller of the transaction used as input to produce the deposit ID.
1126+
* @param depositor The depositor address used as input to produce the deposit ID.
1127+
* @param depositNonce The nonce used as input to produce the deposit ID.
1128+
* @return The deposit ID for the unsafe deposit.
1129+
*/
1130+
function getUnsafeDepositId(
1131+
address msgSender,
1132+
address depositor,
1133+
uint256 depositNonce
1134+
) public pure returns (uint256) {
1135+
return uint256(keccak256(abi.encodePacked(msgSender, depositor, depositNonce)));
1136+
}
1137+
10871138
/**************************************
10881139
* INTERNAL FUNCTIONS *
10891140
**************************************/
10901141

1142+
function _depositV3(
1143+
address depositor,
1144+
address recipient,
1145+
address inputToken,
1146+
address outputToken,
1147+
uint256 inputAmount,
1148+
uint256 outputAmount,
1149+
uint256 destinationChainId,
1150+
address exclusiveRelayer,
1151+
uint256 depositId,
1152+
uint32 quoteTimestamp,
1153+
uint32 fillDeadline,
1154+
uint32 exclusivityDeadline,
1155+
bytes calldata message
1156+
) internal {
1157+
// Check that deposit route is enabled for the input token. There are no checks required for the output token
1158+
// which is pulled from the relayer at fill time and passed through this contract atomically to the recipient.
1159+
if (!enabledDepositRoutes[inputToken][destinationChainId]) revert DisabledRoute();
1160+
1161+
// Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage.
1162+
// It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the
1163+
// SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp
1164+
// within the configured buffer. The owner should pause deposits/fills if this is undesirable.
1165+
// This will underflow if quoteTimestamp is more than depositQuoteTimeBuffer;
1166+
// this is safe but will throw an unintuitive error.
1167+
1168+
// slither-disable-next-line timestamp
1169+
uint256 currentTime = getCurrentTime();
1170+
if (currentTime - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp();
1171+
1172+
// fillDeadline is relative to the destination chain.
1173+
// Don’t allow fillDeadline to be more than several bundles into the future.
1174+
// This limits the maximum required lookback for dataworker and relayer instances.
1175+
// Also, don't allow fillDeadline to be in the past. This poses a potential UX issue if the destination
1176+
// chain time keeping and this chain's time keeping are out of sync but is not really a practical hurdle
1177+
// unless they are significantly out of sync or the depositor is setting very short fill deadlines. This latter
1178+
// situation won't be a problem for honest users.
1179+
if (fillDeadline < currentTime || fillDeadline > currentTime + fillDeadlineBuffer) revert InvalidFillDeadline();
1180+
1181+
// If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the
1182+
// transaction then the user is sending the native token. In this case, the native token should be
1183+
// wrapped.
1184+
if (inputToken == address(wrappedNativeToken) && msg.value > 0) {
1185+
if (msg.value != inputAmount) revert MsgValueDoesNotMatchInputAmount();
1186+
wrappedNativeToken.deposit{ value: msg.value }();
1187+
// Else, it is a normal ERC20. In this case pull the token from the caller as per normal.
1188+
// Note: this includes the case where the L2 caller has WETH (already wrapped ETH) and wants to bridge them.
1189+
// In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action.
1190+
} else {
1191+
// msg.value should be 0 if input token isn't the wrapped native token.
1192+
if (msg.value != 0) revert MsgValueDoesNotMatchInputAmount();
1193+
IERC20Upgradeable(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount);
1194+
}
1195+
1196+
emit V3FundsDeposited(
1197+
inputToken,
1198+
outputToken,
1199+
inputAmount,
1200+
outputAmount,
1201+
destinationChainId,
1202+
depositId,
1203+
quoteTimestamp,
1204+
fillDeadline,
1205+
exclusivityDeadline,
1206+
depositor,
1207+
recipient,
1208+
exclusiveRelayer,
1209+
message
1210+
);
1211+
}
1212+
10911213
function _deposit(
10921214
address depositor,
10931215
address recipient,
@@ -1214,7 +1336,7 @@ abstract contract SpokePool is
12141336

12151337
function _verifyUpdateV3DepositMessage(
12161338
address depositor,
1217-
uint32 depositId,
1339+
uint256 depositId,
12181340
uint256 originChainId,
12191341
uint256 updatedOutputAmount,
12201342
address updatedRecipient,

contracts/interfaces/V3SpokePoolInterface.sol

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ interface V3SpokePoolInterface {
5353
// Origin chain id.
5454
uint256 originChainId;
5555
// The id uniquely identifying this deposit on the origin chain.
56-
uint32 depositId;
56+
uint256 depositId;
5757
// The timestamp on the destination chain after which this deposit can no longer be filled.
5858
uint32 fillDeadline;
5959
// The timestamp on the destination chain after which any relayer can fill the deposit.
@@ -102,7 +102,7 @@ interface V3SpokePoolInterface {
102102
uint256 inputAmount,
103103
uint256 outputAmount,
104104
uint256 indexed destinationChainId,
105-
uint32 indexed depositId,
105+
uint256 indexed depositId,
106106
uint32 quoteTimestamp,
107107
uint32 fillDeadline,
108108
uint32 exclusivityDeadline,
@@ -114,7 +114,7 @@ interface V3SpokePoolInterface {
114114

115115
event RequestedSpeedUpV3Deposit(
116116
uint256 updatedOutputAmount,
117-
uint32 indexed depositId,
117+
uint256 indexed depositId,
118118
address indexed depositor,
119119
address updatedRecipient,
120120
bytes updatedMessage,
@@ -128,7 +128,7 @@ interface V3SpokePoolInterface {
128128
uint256 outputAmount,
129129
uint256 repaymentChainId,
130130
uint256 indexed originChainId,
131-
uint32 indexed depositId,
131+
uint256 indexed depositId,
132132
uint32 fillDeadline,
133133
uint32 exclusivityDeadline,
134134
address exclusiveRelayer,
@@ -145,7 +145,7 @@ interface V3SpokePoolInterface {
145145
uint256 inputAmount,
146146
uint256 outputAmount,
147147
uint256 indexed originChainId,
148-
uint32 indexed depositId,
148+
uint256 indexed depositId,
149149
uint32 fillDeadline,
150150
uint32 exclusivityDeadline,
151151
address exclusiveRelayer,
@@ -189,7 +189,7 @@ interface V3SpokePoolInterface {
189189

190190
function speedUpV3Deposit(
191191
address depositor,
192-
uint32 depositId,
192+
uint256 depositId,
193193
uint256 updatedOutputAmount,
194194
address updatedRecipient,
195195
bytes calldata updatedMessage,

test/evm/hardhat/SpokePool.Deposit.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
amountReceived,
2626
MAX_UINT32,
2727
originChainId,
28-
zeroAddress,
2928
} from "./constants";
3029

3130
const { AddressZero: ZERO_ADDRESS } = ethers.constants;
@@ -366,6 +365,28 @@ describe("SpokePool Depositor Logic", async function () {
366365
_relayData.message,
367366
];
368367
}
368+
function getUnsafeDepositArgsFromRelayData(
369+
_relayData: V3RelayData,
370+
_depositId: string,
371+
_destinationChainId = destinationChainId,
372+
_quoteTimestamp = quoteTimestamp
373+
) {
374+
return [
375+
_relayData.depositor,
376+
_relayData.recipient,
377+
_relayData.inputToken,
378+
_relayData.outputToken,
379+
_relayData.inputAmount,
380+
_relayData.outputAmount,
381+
_destinationChainId,
382+
_relayData.exclusiveRelayer,
383+
_depositId,
384+
_quoteTimestamp,
385+
_relayData.fillDeadline,
386+
_relayData.exclusivityDeadline,
387+
_relayData.message,
388+
];
389+
}
369390
beforeEach(async function () {
370391
relayData = {
371392
depositor: depositor.address,
@@ -576,6 +597,45 @@ describe("SpokePool Depositor Logic", async function () {
576597
"ReentrancyGuard: reentrant call"
577598
);
578599
});
600+
it("unsafe deposit ID", async function () {
601+
const currentTime = (await spokePool.getCurrentTime()).toNumber();
602+
// new deposit ID should be the uint256 equivalent of the keccak256 hash of packed {msg.sender, depositor, forcedDepositId}.
603+
const forcedDepositId = "99";
604+
const expectedDepositId = BigNumber.from(
605+
ethers.utils.solidityKeccak256(
606+
["address", "address", "uint256"],
607+
[depositor.address, recipient.address, forcedDepositId]
608+
)
609+
);
610+
expect(await spokePool.getUnsafeDepositId(depositor.address, recipient.address, forcedDepositId)).to.equal(
611+
expectedDepositId
612+
);
613+
// Note: we deliberately set the depositor != msg.sender to test that the hashing algorithm correctly includes
614+
// both addresses in the hash.
615+
await expect(
616+
spokePool
617+
.connect(depositor)
618+
.unsafeDepositV3(
619+
...getUnsafeDepositArgsFromRelayData({ ...relayData, depositor: recipient.address }, forcedDepositId)
620+
)
621+
)
622+
.to.emit(spokePool, "V3FundsDeposited")
623+
.withArgs(
624+
relayData.inputToken,
625+
relayData.outputToken,
626+
relayData.inputAmount,
627+
relayData.outputAmount,
628+
destinationChainId,
629+
expectedDepositId,
630+
quoteTimestamp,
631+
relayData.fillDeadline,
632+
currentTime,
633+
recipient.address,
634+
relayData.recipient,
635+
relayData.exclusiveRelayer,
636+
relayData.message
637+
);
638+
});
579639
});
580640
describe("speed up V3 deposit", function () {
581641
const updatedOutputAmount = amountToDeposit.add(1);

0 commit comments

Comments
 (0)