Skip to content

Commit 9d21d1b

Browse files
authored
feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583)
1 parent b141cf8 commit 9d21d1b

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

1101+
/**
1102+
* @notice Returns the deposit ID for an unsafe deposit. This function is used to compute the deposit ID
1103+
* in unsafeDepositV3 and is provided as a convenience.
1104+
* @dev msgSenderand depositor are both used as inputs to allow passthrough depositors to create unique
1105+
* deposit hash spaces for unique depositors.
1106+
* @param msgSender The caller of the transaction used as input to produce the deposit ID.
1107+
* @param depositor The depositor address used as input to produce the deposit ID.
1108+
* @param depositNonce The nonce used as input to produce the deposit ID.
1109+
* @return The deposit ID for the unsafe deposit.
1110+
*/
1111+
function getUnsafeDepositId(
1112+
address msgSender,
1113+
address depositor,
1114+
uint256 depositNonce
1115+
) public pure returns (uint256) {
1116+
return uint256(keccak256(abi.encodePacked(msgSender, depositor, depositNonce)));
1117+
}
1118+
10681119
/**************************************
10691120
* INTERNAL FUNCTIONS *
10701121
**************************************/
10711122

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

11961318
function _verifyUpdateV3DepositMessage(
11971319
address depositor,
1198-
uint32 depositId,
1320+
uint256 depositId,
11991321
uint256 originChainId,
12001322
uint256 updatedOutputAmount,
12011323
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)