Skip to content

Commit c9017ff

Browse files
bmzignicholaspai
andauthored
feat: sponsored swap and deposits (#790)
* feat: add permit2 entrypoints to the periphery Signed-off-by: Bennett <bennett@umaproject.org> * Update test/evm/foundry/local/SpokePoolPeriphery.t.sol * Update SpokePoolPeriphery.t.sol * move permit2 to proxy * fix permit2 Signed-off-by: bennett <bennett@umaproject.org> * wip: swap arguments refactor Signed-off-by: bennett <bennett@umaproject.org> * implement isValidSignature Signed-off-by: bennett <bennett@umaproject.org> * 1271 Signed-off-by: bennett <bennett@umaproject.org> * simplify isValidSignature Signed-off-by: bennett <bennett@umaproject.org> * rebase /programs on master Signed-off-by: nicholaspai <npai.nyc@gmail.com> * clean up comments * rebase programs * feat: sponsored swap and deposits Signed-off-by: bennett <bennett@umaproject.org> * fix: consolidate structs so that permit2 witnesses cover inputs Signed-off-by: bennett <bennett@umaproject.org> * begin permit2 unit tests Signed-off-by: bennett <bennett@umaproject.org> * rebase * Update SpokePoolPeriphery.t.sol * move type definitions to interface Signed-off-by: bennett <bennett@umaproject.org> * fix permit2 test Signed-off-by: bennett <bennett@umaproject.org> * transfer type tests Signed-off-by: bennett <bennett@umaproject.org> * rename EIP1271Signature to Permi2Approval Signed-off-by: bennett <bennett@umaproject.org> * add mockERC20 which implements permit/receiveWithAuthorization Signed-off-by: bennett <bennett@umaproject.org> * add tests for permit, permit2, and receiveWithAuth swaps/deposits Signed-off-by: bennett <bennett@umaproject.org> * add tests for invalid witnesses Signed-off-by: bennett <bennett@umaproject.org> * factor out signature checking Signed-off-by: bennett <bennett@umaproject.org> --------- Signed-off-by: Bennett <bennett@umaproject.org> Signed-off-by: bennett <bennett@umaproject.org> Signed-off-by: nicholaspai <npai.nyc@gmail.com> Co-authored-by: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Co-authored-by: nicholaspai <npai.nyc@gmail.com>
1 parent 372d9cb commit c9017ff

File tree

5 files changed

+713
-36
lines changed

5 files changed

+713
-36
lines changed

contracts/SpokePoolV3Periphery.sol

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IER
77
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
88
import { MultiCaller } from "@uma/core/contracts/common/implementation/MultiCaller.sol";
99
import { Lockable } from "./Lockable.sol";
10+
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
11+
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
1012
import { V3SpokePoolInterface } from "./interfaces/V3SpokePoolInterface.sol";
1113
import { IERC20Auth } from "./external/interfaces/IERC20Auth.sol";
1214
import { WETH9Interface } from "./external/interfaces/WETH9Interface.sol";
@@ -98,7 +100,7 @@ contract SpokePoolPeripheryProxy is SpokePoolV3PeripheryProxyInterface, Lockable
98100
* contract may be deployed deterministically at the same address across different networks.
99101
* @custom:security-contact bugs@across.to
100102
*/
101-
contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiCaller {
103+
contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiCaller, EIP712 {
102104
using SafeERC20 for IERC20;
103105
using Address for address;
104106

@@ -157,6 +159,7 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
157159
error InvalidProxy();
158160
error InvalidSwapToken();
159161
error NotProxy();
162+
error InvalidSignature();
160163

161164
/**
162165
* @notice Construct a new Proxy contract.
@@ -165,7 +168,7 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
165168
* across different networks is the same. Constructor parameters affect the bytecode so we can only
166169
* add parameters here that are consistent across networks.
167170
*/
168-
constructor() {}
171+
constructor() EIP712("ACROSS-V3-PERIPHERY", "1.0.0") {}
169172

170173
/**
171174
* @notice Initializes the SwapAndBridgeBase contract.
@@ -282,14 +285,18 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
282285
* @notice Swaps an EIP-2612 token on this chain via specified router before submitting Across deposit atomically.
283286
* Caller can specify their slippage tolerance for the swap and Across deposit params.
284287
* @dev If the swapToken in swapData does not implement `permit` to the specifications of EIP-2612, this function will fail.
288+
* @param signatureOwner The owner of the permit signature and swapAndDepositData signature. Assumed to be the depositor for the Across spoke pool.
285289
* @param swapAndDepositData Specifies the params we need to perform a swap on a generic exchange.
286290
* @param deadline Deadline before which the permit signature is valid.
287291
* @param permitSignature Permit signature encoded as (bytes32 r, bytes32 s, uint8 v).
292+
* @param swapAndDepositDataSignature The signature against the input swapAndDepositData encoded as (bytes32 r, bytes32 s, uint8 v).
288293
*/
289294
function swapAndBridgeWithPermit(
295+
address signatureOwner,
290296
SwapAndDepositData calldata swapAndDepositData,
291297
uint256 deadline,
292-
bytes calldata permitSignature
298+
bytes calldata permitSignature,
299+
bytes calldata swapAndDepositDataSignature
293300
) external override nonReentrant {
294301
(bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(permitSignature);
295302
// Load variables used in this function onto the stack.
@@ -299,9 +306,17 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
299306
// For permit transactions, we wrap the call in a try/catch block so that the transaction will continue even if the call to
300307
// permit fails. For example, this may be useful if the permit signature, which can be redeemed by anyone, is executed by somebody
301308
// other than this contract.
302-
try IERC20Permit(_swapToken).permit(msg.sender, address(this), _swapTokenAmount, deadline, v, r, s) {} catch {}
303-
IERC20(_swapToken).safeTransferFrom(msg.sender, address(this), _swapTokenAmount);
309+
try
310+
IERC20Permit(_swapToken).permit(signatureOwner, address(this), _swapTokenAmount, deadline, v, r, s)
311+
{} catch {}
312+
IERC20(_swapToken).safeTransferFrom(signatureOwner, address(this), _swapTokenAmount);
304313

314+
// Verify that the signatureOwner signed the input swapAndDepositData.
315+
_validateSignature(
316+
signatureOwner,
317+
PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData),
318+
swapAndDepositDataSignature
319+
);
305320
_swapAndBridge(swapAndDepositData);
306321
}
307322

@@ -342,25 +357,29 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
342357
* @notice Swaps an EIP-3009 token on this chain via specified router before submitting Across deposit atomically.
343358
* Caller can specify their slippage tolerance for the swap and Across deposit params.
344359
* @dev If swapToken does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert.
360+
* @param signatureOwner The owner of the EIP3009 signature and swapAndDepositData signature. Assumed to be the depositor for the Across spoke pool.
345361
* @param swapAndDepositData Specifies the params we need to perform a swap on a generic exchange.
346362
* @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid.
347363
* @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid.
348364
* @param nonce Unique nonce used in the `receiveWithAuthorization` signature.
349-
* @param receiveWithAuthSignature EIP3009 signature encoded adepositors (bytes32 r, bytes32 s, uint8 v).
365+
* @param receiveWithAuthSignature EIP3009 signature encoded as (bytes32 r, bytes32 s, uint8 v).
366+
* @param swapAndDepositDataSignature The signature against the input swapAndDepositData encoded as (bytes32 r, bytes32 s, uint8 v).
350367
*/
351368
function swapAndBridgeWithAuthorization(
369+
address signatureOwner,
352370
SwapAndDepositData calldata swapAndDepositData,
353371
uint256 validAfter,
354372
uint256 validBefore,
355373
bytes32 nonce,
356-
bytes calldata receiveWithAuthSignature
374+
bytes calldata receiveWithAuthSignature,
375+
bytes calldata swapAndDepositDataSignature
357376
) external override nonReentrant {
358377
(bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(receiveWithAuthSignature);
359378
// While any contract can vacuously implement `transferWithAuthorization` (or just have a fallback),
360379
// if tokens were not sent to this contract, by this call to swapData.swapToken, this function will revert
361380
// when attempting to swap tokens it does not own.
362381
IERC20Auth(address(swapAndDepositData.swapToken)).receiveWithAuthorization(
363-
msg.sender,
382+
signatureOwner,
364383
address(this),
365384
swapAndDepositData.swapTokenAmount,
366385
validAfter,
@@ -371,20 +390,30 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
371390
s
372391
);
373392

393+
// Verify that the signatureOwner signed the input swapAndDepositData.
394+
_validateSignature(
395+
signatureOwner,
396+
PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData),
397+
swapAndDepositDataSignature
398+
);
374399
_swapAndBridge(swapAndDepositData);
375400
}
376401

377402
/**
378403
* @notice Deposits an EIP-2612 token Across input token into the Spoke Pool contract.
379404
* @dev If `acrossInputToken` does not implement `permit` to the specifications of EIP-2612, this function will fail.
405+
* @param signatureOwner The owner of the permit signature and depositData signature. Assumed to be the depositor for the Across spoke pool.
380406
* @param depositData Specifies the Across deposit params to send.
381407
* @param deadline Deadline before which the permit signature is valid.
382408
* @param permitSignature Permit signature encoded as (bytes32 r, bytes32 s, uint8 v).
409+
* @param depositDataSignature The signature against the input depositData encoded as (bytes32 r, bytes32 s, uint8 v).
383410
*/
384411
function depositWithPermit(
412+
address signatureOwner,
385413
DepositData calldata depositData,
386414
uint256 deadline,
387-
bytes calldata permitSignature
415+
bytes calldata permitSignature,
416+
bytes calldata depositDataSignature
388417
) external override nonReentrant {
389418
(bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(permitSignature);
390419
// Load variables used in this function onto the stack.
@@ -394,9 +423,11 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
394423
// For permit transactions, we wrap the call in a try/catch block so that the transaction will continue even if the call to
395424
// permit fails. For example, this may be useful if the permit signature, which can be redeemed by anyone, is executed by somebody
396425
// other than this contract.
397-
try IERC20Permit(_inputToken).permit(msg.sender, address(this), _inputAmount, deadline, v, r, s) {} catch {}
398-
IERC20(_inputToken).safeTransferFrom(msg.sender, address(this), _inputAmount);
426+
try IERC20Permit(_inputToken).permit(signatureOwner, address(this), _inputAmount, deadline, v, r, s) {} catch {}
427+
IERC20(_inputToken).safeTransferFrom(signatureOwner, address(this), _inputAmount);
399428

429+
// Verify that the signatureOwner signed the input depositData.
430+
_validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature);
400431
_depositV3(
401432
depositData.baseDepositData.depositor,
402433
depositData.baseDepositData.recipient,
@@ -461,26 +492,30 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
461492
/**
462493
* @notice Deposits an EIP-3009 compliant Across input token into the Spoke Pool contract.
463494
* @dev If `acrossInputToken` does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert.
495+
* @param signatureOwner The owner of the EIP3009 signature and depositData signature. Assumed to be the depositor for the Across spoke pool.
464496
* @param depositData Specifies the Across deposit params to send.
465497
* @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid.
466498
* @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid.
467499
* @param nonce Unique nonce used in the `receiveWithAuthorization` signature.
468500
* @param receiveWithAuthSignature EIP3009 signature encoded as (bytes32 r, bytes32 s, uint8 v).
501+
* @param depositDataSignature The signature against the input depositData encoded as (bytes32 r, bytes32 s, uint8 v).
469502
*/
470503
function depositWithAuthorization(
504+
address signatureOwner,
471505
DepositData calldata depositData,
472506
uint256 validAfter,
473507
uint256 validBefore,
474508
bytes32 nonce,
475-
bytes calldata receiveWithAuthSignature
509+
bytes calldata receiveWithAuthSignature,
510+
bytes calldata depositDataSignature
476511
) external override nonReentrant {
477512
// Load variables used multiple times onto the stack.
478513
uint256 _inputAmount = depositData.inputAmount;
479514

480515
// Redeem the receiveWithAuthSignature.
481516
(bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(receiveWithAuthSignature);
482517
IERC20Auth(depositData.baseDepositData.inputToken).receiveWithAuthorization(
483-
msg.sender,
518+
signatureOwner,
484519
address(this),
485520
_inputAmount,
486521
validAfter,
@@ -491,6 +526,8 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
491526
s
492527
);
493528

529+
// Verify that the signatureOwner signed the input depositData.
530+
_validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature);
494531
_depositV3(
495532
depositData.baseDepositData.depositor,
496533
depositData.baseDepositData.recipient,
@@ -519,6 +556,28 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
519556
: EIP1271_INVALID_SIGNATURE;
520557
}
521558

559+
/**
560+
* @notice Returns the contract's EIP712 domain separator, used to sign hashed depositData/swapAndDepositData types.
561+
*/
562+
function domainSeparator() external view returns (bytes32) {
563+
return _domainSeparatorV4();
564+
}
565+
566+
/**
567+
* @notice Validates that the typed data hash corresponds to the input signature owner and corresponding signature.
568+
* @param signatureOwner The alledged signer of the input hash.
569+
* @param typedDataHash The EIP712 data hash to check the signature against.
570+
* @param signature The signature to validate.
571+
*/
572+
function _validateSignature(
573+
address signatureOwner,
574+
bytes32 typedDataHash,
575+
bytes calldata signature
576+
) private {
577+
if (!SignatureChecker.isValidSignatureNow(signatureOwner, _hashTypedDataV4(typedDataHash), signature))
578+
revert InvalidSignature();
579+
}
580+
522581
/**
523582
* @notice Approves the spoke pool and calls `depositV3` function with the specified input parameters.
524583
* @param depositor The address on the origin chain which should be treated as the depositor by Across, and will therefore receive refunds if this deposit

contracts/interfaces/SpokePoolV3PeripheryInterface.sol

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,11 @@ interface SpokePoolV3PeripheryInterface {
105105
function swapAndBridge(SwapAndDepositData calldata swapAndDepositData) external payable;
106106

107107
function swapAndBridgeWithPermit(
108+
address signatureOwner,
108109
SwapAndDepositData calldata swapAndDepositData,
109110
uint256 deadline,
110-
bytes calldata permitSignature
111+
bytes calldata permitSignature,
112+
bytes calldata swapAndDepositDataSignature
111113
) external;
112114

113115
function swapAndBridgeWithPermit2(
@@ -118,17 +120,21 @@ interface SpokePoolV3PeripheryInterface {
118120
) external;
119121

120122
function swapAndBridgeWithAuthorization(
123+
address signatureOwner,
121124
SwapAndDepositData calldata swapAndDepositData,
122125
uint256 validAfter,
123126
uint256 validBefore,
124127
bytes32 nonce,
125-
bytes calldata receiveWithAuthSignature
128+
bytes calldata receiveWithAuthSignature,
129+
bytes calldata swapAndDepositDataSignature
126130
) external;
127131

128132
function depositWithPermit(
133+
address signatureOwner,
129134
DepositData calldata depositData,
130135
uint256 deadline,
131-
bytes calldata permitSignature
136+
bytes calldata permitSignature,
137+
bytes calldata depositDataSignature
132138
) external;
133139

134140
function depositWithPermit2(
@@ -139,10 +145,12 @@ interface SpokePoolV3PeripheryInterface {
139145
) external;
140146

141147
function depositWithAuthorization(
148+
address signatureOwner,
142149
DepositData calldata depositData,
143150
uint256 validAfter,
144151
uint256 validBefore,
145152
bytes32 nonce,
146-
bytes calldata receiveWithAuthSignature
153+
bytes calldata receiveWithAuthSignature,
154+
bytes calldata depositDataSignature
147155
) external;
148156
}

contracts/libraries/PeripherySigningLib.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ library PeripherySigningLib {
3535
);
3636

3737
// EIP712 Type hashes.
38+
bytes32 internal constant EIP712_BASE_DEPOSIT_DATA_TYPEHASH = keccak256(EIP712_BASE_DEPOSIT_DATA_TYPE);
3839
bytes32 internal constant EIP712_DEPOSIT_DATA_TYPEHASH =
3940
keccak256(abi.encode(EIP712_DEPOSIT_DATA_TYPE, EIP712_BASE_DEPOSIT_DATA_TYPE));
4041
bytes32 internal constant EIP712_SWAP_AND_DEPOSIT_DATA_TYPEHASH =
@@ -76,7 +77,7 @@ library PeripherySigningLib {
7677
return
7778
keccak256(
7879
abi.encode(
79-
EIP712_BASE_DEPOSIT_DATA_TYPE,
80+
EIP712_BASE_DEPOSIT_DATA_TYPEHASH,
8081
baseDepositData.outputToken,
8182
baseDepositData.outputAmount,
8283
baseDepositData.depositor,
@@ -103,7 +104,7 @@ library PeripherySigningLib {
103104
return
104105
keccak256(
105106
abi.encode(
106-
EIP712_DEPOSIT_DATA_TYPE,
107+
EIP712_DEPOSIT_DATA_TYPEHASH,
107108
hashBaseDepositData(depositData.baseDepositData),
108109
depositData.inputAmount
109110
)

contracts/test/MockERC20.sol

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//SPDX-License-Identifier: Unlicense
2+
pragma solidity ^0.8.0;
3+
4+
import { IERC20Auth } from "../external/interfaces/IERC20Auth.sol";
5+
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
6+
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7+
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
8+
9+
/**
10+
* @title MockERC20
11+
* @notice Implements mocked ERC20 contract with various features.
12+
*/
13+
contract MockERC20 is IERC20Auth, ERC20Permit {
14+
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH =
15+
keccak256(
16+
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
17+
);
18+
// Expose the typehash in ERC20Permit.
19+
bytes32 public constant PERMIT_TYPEHASH_EXTERNAL =
20+
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
21+
22+
constructor() ERC20Permit("MockERC20") ERC20("MockERC20", "ERC20") {}
23+
24+
// This does no nonce checking.
25+
function receiveWithAuthorization(
26+
address from,
27+
address to,
28+
uint256 value,
29+
uint256 validAfter,
30+
uint256 validBefore,
31+
bytes32 nonce,
32+
uint8 v,
33+
bytes32 r,
34+
bytes32 s
35+
) external {
36+
require(validAfter <= block.timestamp && validBefore >= block.timestamp, "Invalid time bounds");
37+
require(msg.sender == to, "Receiver not caller");
38+
bytes memory signature = bytes.concat(r, s, bytes1(v));
39+
40+
bytes32 structHash = keccak256(
41+
abi.encode(RECEIVE_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce)
42+
);
43+
bytes32 sigHash = _hashTypedDataV4(structHash);
44+
require(SignatureChecker.isValidSignatureNow(from, sigHash, signature), "Invalid signature");
45+
_transfer(from, to, value);
46+
}
47+
48+
function hashTypedData(bytes32 typedData) external returns (bytes32) {
49+
return _hashTypedDataV4(typedData);
50+
}
51+
}

0 commit comments

Comments
 (0)