Skip to content

feat: sponsored swap and deposits #790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
01f7e37
feat: add permit2 entrypoints to the periphery
bmzig Nov 29, 2024
c6b08de
Merge branch 'spokepool-periphery-multiple-exchanges' into bz/permit2…
nicholaspai Dec 1, 2024
3542114
Update test/evm/foundry/local/SpokePoolPeriphery.t.sol
nicholaspai Dec 1, 2024
6131d16
Update SpokePoolPeriphery.t.sol
nicholaspai Dec 1, 2024
4262f6e
Merge branch 'spokepool-periphery-multiple-exchanges' into bz/permit2…
nicholaspai Dec 2, 2024
0446725
move permit2 to proxy
nicholaspai Dec 2, 2024
4f3b2d6
fix permit2
bmzig Dec 2, 2024
ff13c12
wip: swap arguments refactor
bmzig Dec 3, 2024
7b6bd44
implement isValidSignature
bmzig Dec 3, 2024
aa31f5b
1271
bmzig Dec 3, 2024
93b90cd
simplify isValidSignature
bmzig Dec 3, 2024
9eda8c3
rebase
nicholaspai Dec 4, 2024
a9f019d
Merge branch 'spokepool-periphery-multiple-exchanges' into bz/permit2…
nicholaspai Dec 4, 2024
761c5f3
rebase /programs on master
nicholaspai Dec 4, 2024
19a38d4
clean up comments
nicholaspai Dec 4, 2024
458f67c
rebase programs
nicholaspai Dec 4, 2024
25366ef
feat: sponsored swap and deposits
bmzig Dec 4, 2024
e5473c5
fix: consolidate structs so that permit2 witnesses cover inputs
bmzig Dec 4, 2024
e116240
begin permit2 unit tests
bmzig Dec 5, 2024
1c42606
wip
nicholaspai Dec 5, 2024
0402a6b
rebase
nicholaspai Dec 5, 2024
9acb4a2
Update SpokePoolPeriphery.t.sol
nicholaspai Dec 5, 2024
1927810
move type definitions to interface
bmzig Dec 5, 2024
7e4611d
fix permit2 test
bmzig Dec 5, 2024
e3cfb18
transfer type tests
bmzig Dec 5, 2024
f773316
rename EIP1271Signature to Permi2Approval
bmzig Dec 6, 2024
40eac15
Merge branch 'bz/permit2Periphery' into bz/gaslessPeriphery
bmzig Dec 6, 2024
4d83179
add mockERC20 which implements permit/receiveWithAuthorization
bmzig Dec 6, 2024
3bc3bc9
update merge and fix EIP712 types
bmzig Dec 17, 2024
735a1b6
add tests for permit, permit2, and receiveWithAuth swaps/deposits
bmzig Dec 17, 2024
eab96ae
add tests for invalid witnesses
bmzig Dec 17, 2024
dfa6f89
factor out signature checking
bmzig Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 72 additions & 13 deletions contracts/SpokePoolV3Periphery.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IER
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { MultiCaller } from "@uma/core/contracts/common/implementation/MultiCaller.sol";
import { Lockable } from "./Lockable.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import { V3SpokePoolInterface } from "./interfaces/V3SpokePoolInterface.sol";
import { IERC20Auth } from "./external/interfaces/IERC20Auth.sol";
import { WETH9Interface } from "./external/interfaces/WETH9Interface.sol";
Expand Down Expand Up @@ -98,7 +100,7 @@ contract SpokePoolPeripheryProxy is SpokePoolV3PeripheryProxyInterface, Lockable
* contract may be deployed deterministically at the same address across different networks.
* @custom:security-contact bugs@across.to
*/
contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiCaller {
contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiCaller, EIP712 {
using SafeERC20 for IERC20;
using Address for address;

Expand Down Expand Up @@ -157,6 +159,7 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
error InvalidProxy();
error InvalidSwapToken();
error NotProxy();
error InvalidSignature();

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

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

// Verify that the signatureOwner signed the input swapAndDepositData.
_validateSignature(
signatureOwner,
PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData),
swapAndDepositDataSignature
);
_swapAndBridge(swapAndDepositData);
}

Expand Down Expand Up @@ -342,25 +357,29 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
* @notice Swaps an EIP-3009 token on this chain via specified router before submitting Across deposit atomically.
* Caller can specify their slippage tolerance for the swap and Across deposit params.
* @dev If swapToken does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert.
* @param signatureOwner The owner of the EIP3009 signature and swapAndDepositData signature. Assumed to be the depositor for the Across spoke pool.
* @param swapAndDepositData Specifies the params we need to perform a swap on a generic exchange.
* @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid.
* @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid.
* @param nonce Unique nonce used in the `receiveWithAuthorization` signature.
* @param receiveWithAuthSignature EIP3009 signature encoded adepositors (bytes32 r, bytes32 s, uint8 v).
* @param receiveWithAuthSignature EIP3009 signature encoded as (bytes32 r, bytes32 s, uint8 v).
* @param swapAndDepositDataSignature The signature against the input swapAndDepositData encoded as (bytes32 r, bytes32 s, uint8 v).
*/
function swapAndBridgeWithAuthorization(
address signatureOwner,
SwapAndDepositData calldata swapAndDepositData,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes calldata receiveWithAuthSignature
bytes calldata receiveWithAuthSignature,
bytes calldata swapAndDepositDataSignature
) external override nonReentrant {
(bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(receiveWithAuthSignature);
// While any contract can vacuously implement `transferWithAuthorization` (or just have a fallback),
// if tokens were not sent to this contract, by this call to swapData.swapToken, this function will revert
// when attempting to swap tokens it does not own.
IERC20Auth(address(swapAndDepositData.swapToken)).receiveWithAuthorization(
msg.sender,
signatureOwner,
address(this),
swapAndDepositData.swapTokenAmount,
validAfter,
Expand All @@ -371,20 +390,30 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
s
);

// Verify that the signatureOwner signed the input swapAndDepositData.
_validateSignature(
signatureOwner,
PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData),
swapAndDepositDataSignature
);
_swapAndBridge(swapAndDepositData);
}

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

// Verify that the signatureOwner signed the input depositData.
_validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature);
_depositV3(
depositData.baseDepositData.depositor,
depositData.baseDepositData.recipient,
Expand Down Expand Up @@ -461,26 +492,30 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
/**
* @notice Deposits an EIP-3009 compliant Across input token into the Spoke Pool contract.
* @dev If `acrossInputToken` does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert.
* @param signatureOwner The owner of the EIP3009 signature and depositData signature. Assumed to be the depositor for the Across spoke pool.
* @param depositData Specifies the Across deposit params to send.
* @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid.
* @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid.
* @param nonce Unique nonce used in the `receiveWithAuthorization` signature.
* @param receiveWithAuthSignature EIP3009 signature encoded as (bytes32 r, bytes32 s, uint8 v).
* @param depositDataSignature The signature against the input depositData encoded as (bytes32 r, bytes32 s, uint8 v).
*/
function depositWithAuthorization(
address signatureOwner,
DepositData calldata depositData,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes calldata receiveWithAuthSignature
bytes calldata receiveWithAuthSignature,
bytes calldata depositDataSignature
) external override nonReentrant {
// Load variables used multiple times onto the stack.
uint256 _inputAmount = depositData.inputAmount;

// Redeem the receiveWithAuthSignature.
(bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(receiveWithAuthSignature);
IERC20Auth(depositData.baseDepositData.inputToken).receiveWithAuthorization(
msg.sender,
signatureOwner,
address(this),
_inputAmount,
validAfter,
Expand All @@ -491,6 +526,8 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
s
);

// Verify that the signatureOwner signed the input depositData.
_validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature);
_depositV3(
depositData.baseDepositData.depositor,
depositData.baseDepositData.recipient,
Expand Down Expand Up @@ -519,6 +556,28 @@ contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiC
: EIP1271_INVALID_SIGNATURE;
}

/**
* @notice Returns the contract's EIP712 domain separator, used to sign hashed depositData/swapAndDepositData types.
*/
function domainSeparator() external view returns (bytes32) {
return _domainSeparatorV4();
}

/**
* @notice Validates that the typed data hash corresponds to the input signature owner and corresponding signature.
* @param signatureOwner The alledged signer of the input hash.
* @param typedDataHash The EIP712 data hash to check the signature against.
* @param signature The signature to validate.
*/
function _validateSignature(
address signatureOwner,
bytes32 typedDataHash,
bytes calldata signature
) private {
if (!SignatureChecker.isValidSignatureNow(signatureOwner, _hashTypedDataV4(typedDataHash), signature))
revert InvalidSignature();
}

/**
* @notice Approves the spoke pool and calls `depositV3` function with the specified input parameters.
* @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
Expand Down
16 changes: 12 additions & 4 deletions contracts/interfaces/SpokePoolV3PeripheryInterface.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,11 @@ interface SpokePoolV3PeripheryInterface {
function swapAndBridge(SwapAndDepositData calldata swapAndDepositData) external payable;

function swapAndBridgeWithPermit(
address signatureOwner,
SwapAndDepositData calldata swapAndDepositData,
uint256 deadline,
bytes calldata permitSignature
bytes calldata permitSignature,
bytes calldata swapAndDepositDataSignature
) external;

function swapAndBridgeWithPermit2(
Expand All @@ -118,17 +120,21 @@ interface SpokePoolV3PeripheryInterface {
) external;

function swapAndBridgeWithAuthorization(
address signatureOwner,
SwapAndDepositData calldata swapAndDepositData,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes calldata receiveWithAuthSignature
bytes calldata receiveWithAuthSignature,
bytes calldata swapAndDepositDataSignature
) external;

function depositWithPermit(
address signatureOwner,
DepositData calldata depositData,
uint256 deadline,
bytes calldata permitSignature
bytes calldata permitSignature,
bytes calldata depositDataSignature
) external;

function depositWithPermit2(
Expand All @@ -139,10 +145,12 @@ interface SpokePoolV3PeripheryInterface {
) external;

function depositWithAuthorization(
address signatureOwner,
DepositData calldata depositData,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes calldata receiveWithAuthSignature
bytes calldata receiveWithAuthSignature,
bytes calldata depositDataSignature
) external;
}
5 changes: 3 additions & 2 deletions contracts/libraries/PeripherySigningLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ library PeripherySigningLib {
);

// EIP712 Type hashes.
bytes32 internal constant EIP712_BASE_DEPOSIT_DATA_TYPEHASH = keccak256(EIP712_BASE_DEPOSIT_DATA_TYPE);
bytes32 internal constant EIP712_DEPOSIT_DATA_TYPEHASH =
keccak256(abi.encode(EIP712_DEPOSIT_DATA_TYPE, EIP712_BASE_DEPOSIT_DATA_TYPE));
bytes32 internal constant EIP712_SWAP_AND_DEPOSIT_DATA_TYPEHASH =
Expand Down Expand Up @@ -76,7 +77,7 @@ library PeripherySigningLib {
return
keccak256(
abi.encode(
EIP712_BASE_DEPOSIT_DATA_TYPE,
EIP712_BASE_DEPOSIT_DATA_TYPEHASH,
baseDepositData.outputToken,
baseDepositData.outputAmount,
baseDepositData.depositor,
Expand All @@ -103,7 +104,7 @@ library PeripherySigningLib {
return
keccak256(
abi.encode(
EIP712_DEPOSIT_DATA_TYPE,
EIP712_DEPOSIT_DATA_TYPEHASH,
hashBaseDepositData(depositData.baseDepositData),
depositData.inputAmount
)
Expand Down
51 changes: 51 additions & 0 deletions contracts/test/MockERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import { IERC20Auth } from "../external/interfaces/IERC20Auth.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

/**
* @title MockERC20
* @notice Implements mocked ERC20 contract with various features.
*/
contract MockERC20 is IERC20Auth, ERC20Permit {
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH =
keccak256(
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
// Expose the typehash in ERC20Permit.
bytes32 public constant PERMIT_TYPEHASH_EXTERNAL =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

constructor() ERC20Permit("MockERC20") ERC20("MockERC20", "ERC20") {}

// This does no nonce checking.
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(validAfter <= block.timestamp && validBefore >= block.timestamp, "Invalid time bounds");
require(msg.sender == to, "Receiver not caller");
bytes memory signature = bytes.concat(r, s, bytes1(v));

bytes32 structHash = keccak256(
abi.encode(RECEIVE_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce)
);
bytes32 sigHash = _hashTypedDataV4(structHash);
require(SignatureChecker.isValidSignatureNow(from, sigHash, signature), "Invalid signature");
_transfer(from, to, value);
}

function hashTypedData(bytes32 typedData) external returns (bytes32) {
return _hashTypedDataV4(typedData);
}
}
Loading
Loading