Skip to content
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

feat(SpokePoolPeriphery): Support multiple exchanges #777

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
db4bf7b
feat(SpokePoolPeriphery): Support multiple exchanges
nicholaspai Nov 27, 2024
df04404
rename
nicholaspai Nov 27, 2024
1954009
Update SpokeV3PoolPeriphery.sol
nicholaspai Nov 27, 2024
fc1ff8f
Update SpokeV3PoolPeriphery.sol
nicholaspai Nov 27, 2024
c1b6d4f
Update SpokeV3PoolPeriphery.sol
nicholaspai Nov 27, 2024
3b3352e
Add unit tests
nicholaspai Nov 27, 2024
aca4b06
Add whitelistExchanges only owner method
nicholaspai Nov 27, 2024
7149287
rename
nicholaspai Nov 27, 2024
dda8499
Remove onlyOwner
nicholaspai Nov 27, 2024
2da9c63
Remove whitelist of exchanges, add proxy to bypass approval abuse
nicholaspai Nov 28, 2024
90e7cd0
Add some protection to callSpokePoolPeriphery
nicholaspai Nov 28, 2024
9511666
Only call swapAndBridge through proxy
nicholaspai Dec 1, 2024
e0bead2
move periphery funcs into proxy
nicholaspai Dec 2, 2024
5494ee5
Update SpokePoolV3Periphery.sol
nicholaspai Dec 3, 2024
b6db47b
remove depositERC20
nicholaspai Dec 3, 2024
66df238
Merge branch 'master' into spokepool-periphery-multiple-exchanges
nicholaspai Dec 4, 2024
d0a9d0f
Update SpokePoolV3Periphery.sol
nicholaspai Dec 4, 2024
6635803
Add back safeTransferFron's to permit funcs
nicholaspai Dec 4, 2024
6b995e5
Merge branch 'master' into spokepool-periphery-multiple-exchanges
nicholaspai Dec 4, 2024
0de384e
Add unit tests that check if calling deposit and swapAndBridge with n…
nicholaspai Dec 5, 2024
100e707
Add interfaces to make sure we don't add new functions as easily
nicholaspai Dec 5, 2024
6db7d87
Add Create2Factory
nicholaspai Dec 6, 2024
3a16809
Merge branch 'master' into spokepool-periphery-multiple-exchanges
nicholaspai Dec 6, 2024
022a8ec
feat: add permit2 entrypoints to the periphery (#782)
bmzig Dec 6, 2024
372d9cb
Merge branch 'master' into spokepool-periphery-multiple-exchanges
nicholaspai Dec 9, 2024
c9017ff
feat: sponsored swap and deposits (#790)
bmzig Dec 18, 2024
9bc7d91
feat: Delete SwapAndBridge and add submission fees to gasless flow (#…
nicholaspai Dec 19, 2024
f4250d0
Update SpokePoolV3Periphery.sol
nicholaspai Dec 19, 2024
9a4ef73
Update SpokePoolPeriphery.t.sol
nicholaspai Dec 19, 2024
26110a9
fix: eip712 types and hashes (#821)
dohaki Dec 24, 2024
c1f6181
refactor comments
bmzig Dec 25, 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
36 changes: 36 additions & 0 deletions contracts/Create2Factory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol";
import { Lockable } from "./Lockable.sol";

/**
* @title Create2Factory
* @notice Deploys a new contract via create2 at a deterministic address and then atomically initializes the contract
* @dev Contracts designed to be deployed at deterministic addresses should initialize via a non-constructor
* initializer to maintain bytecode across different chains.
* @custom:security-contact bugs@across.to
*/
contract Create2Factory is Lockable {
/// @notice Emitted when the initialization to a newly deployed contract fails
error InitializationFailed();

/**
* @notice Deploys a new contract via create2 at a deterministic address and then atomically initializes the contract
* @param amount The amount of ETH to send with the deployment. If this is not zero then the contract must have a payable constructor
* @param salt The salt to use for the create2 deployment. Must not have been used before for the bytecode
* @param bytecode The bytecode of the contract to deploy
* @param initializationCode The initialization code to call on the deployed contract
*/
function deploy(
uint256 amount,
bytes32 salt,
bytes calldata bytecode,
bytes calldata initializationCode
) external nonReentrant returns (address) {
address deployedAddress = Create2.deploy(amount, salt, bytecode);
(bool success, ) = deployedAddress.call(initializationCode);
if (!success) revert InitializationFailed();
return deployedAddress;
}
}
670 changes: 416 additions & 254 deletions contracts/SpokePoolV3Periphery.sol
bmzig marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/SwapAndBridge.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//SPDX-License-Identifier: Unlicense
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import "./interfaces/V3SpokePoolInterface.sol";
Expand Down
19 changes: 19 additions & 0 deletions contracts/external/interfaces/IPermit2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
pragma solidity ^0.8.0;

interface IPermit2 {
struct PermitDetails {
address token;
uint160 amount;
uint48 expiration;
uint48 nonce;
}

struct PermitSingle {
PermitDetails details;
address spender;
uint256 sigDeadline;
}

struct TokenPermissions {
address token;
uint256 amount;
Expand All @@ -18,6 +31,12 @@ interface IPermit2 {
uint256 requestedAmount;
}

function permit(
address owner,
PermitSingle memory permitSingle,
bytes calldata signature
) external;

function permitWitnessTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
Expand Down
148 changes: 148 additions & 0 deletions contracts/interfaces/SpokePoolV3PeripheryInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import { IERC20Auth } from "../external/interfaces/IERC20Auth.sol";
import { SpokePoolV3Periphery } from "../SpokePoolV3Periphery.sol";
import { PeripherySigningLib } from "../libraries/PeripherySigningLib.sol";
import { IPermit2 } from "../external/interfaces/IPermit2.sol";

interface SpokePoolV3PeripheryProxyInterface {
function swapAndBridge(SpokePoolV3PeripheryInterface.SwapAndDepositData calldata swapAndDepositData) external;
}

/**
* @title SpokePoolV3Periphery
* @notice Contract for performing more complex interactions with an Across spoke pool deployment.
* @dev Variables which may be immutable are not marked as immutable, nor defined in the constructor, so that this
* contract may be deployed deterministically at the same address across different networks.
* @custom:security-contact bugs@across.to
*/
interface SpokePoolV3PeripheryInterface {
// Enum describing the method of transferring tokens to an exchange.
enum TransferType {
// Approve the exchange so that it may transfer tokens from this contract.
Approval,
// Transfer tokens to the exchange before calling it in this contract.
Transfer,
// Approve the exchange by authorizing a transfer with Permit2.
Permit2Approval
}

// Params we'll need caller to pass in to specify an Across Deposit. The input token will be swapped into first
// before submitting a bridge deposit, which is why we don't include the input token amount as it is not known
// until after the swap.
struct BaseDepositData {
// Token deposited on origin chain.
address inputToken;
// Token received on destination chain.
address outputToken;
// Amount of output token to be received by recipient.
uint256 outputAmount;
// The account credited with deposit who can submit speedups to the Across deposit.
address depositor;
// The account that will receive the output token on the destination chain. If the output token is
// wrapped native token, then if this is an EOA then they will receive native token on the destination
// chain and if this is a contract then they will receive an ERC20.
address recipient;
// The destination chain identifier.
uint256 destinationChainId;
// The account that can exclusively fill the deposit before the exclusivity parameter.
address exclusiveRelayer;
// Timestamp of the deposit used by system to charge fees. Must be within short window of time into the past
// relative to this chain's current time or deposit will revert.
uint32 quoteTimestamp;
// The timestamp on the destination chain after which this deposit can no longer be filled.
uint32 fillDeadline;
// The timestamp or offset on the destination chain after which anyone can fill the deposit. A detailed description on
// how the parameter is interpreted by the V3 spoke pool can be found at https://github.com/across-protocol/contracts/blob/fa67f5e97eabade68c67127f2261c2d44d9b007e/contracts/SpokePool.sol#L476
uint32 exclusivityParameter;
// Data that is forwarded to the recipient if the recipient is a contract.
bytes message;
}

// Minimum amount of parameters needed to perform a swap on an exchange specified. We include information beyond just the router calldata
// and exchange address so that we may ensure that the swap was performed properly.
struct SwapAndDepositData {
// Deposit data to use when interacting with the Across spoke pool.
BaseDepositData depositData;
// Token to swap.
address swapToken;
// Address of the exchange to use in the swap.
address exchange;
// Method of transferring tokens to the exchange.
TransferType transferType;
// Amount of the token to swap on the exchange.
uint256 swapTokenAmount;
// Minimum output amount of the exchange, and, by extension, the minimum required amount to deposit into an Across spoke pool.
uint256 minExpectedInputTokenAmount;
// The calldata to use when calling the exchange.
bytes routerCalldata;
}

// Extended deposit data to be used specifically for signing off on periphery deposits.
struct DepositData {
// Deposit data describing the parameters for the V3 Across deposit.
BaseDepositData baseDepositData;
// The precise input amount to deposit into the spoke pool.
uint256 inputAmount;
}

function deposit(
address recipient,
address inputToken,
uint256 inputAmount,
uint256 outputAmount,
uint256 destinationChainId,
address exclusiveRelayer,
uint32 quoteTimestamp,
uint32 fillDeadline,
uint32 exclusivityParameter,
bytes memory message
) external payable;

function swapAndBridge(SwapAndDepositData calldata swapAndDepositData) external payable;

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

function swapAndBridgeWithPermit2(
address signatureOwner,
SwapAndDepositData calldata swapAndDepositData,
IPermit2.PermitTransferFrom calldata permit,
bytes calldata signature
) external;

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

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

function depositWithPermit2(
address signatureOwner,
DepositData calldata depositData,
IPermit2.PermitTransferFrom calldata permit,
bytes calldata signature
) external;

function depositWithAuthorization(
DepositData calldata depositData,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes calldata receiveWithAuthSignature
) external;
}
155 changes: 155 additions & 0 deletions contracts/libraries/PeripherySigningLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { SpokePoolV3PeripheryInterface } from "../interfaces/SpokePoolV3PeripheryInterface.sol";

library PeripherySigningLib {
// Typed structured data for the structs to sign against in the periphery.
bytes internal constant EIP712_BASE_DEPOSIT_DATA_TYPE =
abi.encodePacked(
"BaseDepositData(",
"address inputToken",
"address outputToken",
"uint256 outputAmount",
"address depositor",
"address recipient",
"uint256 destinationChainId",
"address exclusiveRelayer",
"uint32 quoteTimestamp",
"uint32 fillDeadline",
"uint32 exclusivityParameter",
"bytes message)"
);
bytes internal constant EIP712_DEPOSIT_DATA_TYPE =
abi.encodePacked("DepositData(BaseDepositData baseDepositData,uint256 inputAmount)");
bytes internal constant EIP712_SWAP_AND_DEPOSIT_DATA_TYPE =
abi.encodePacked(
"SwapAndDepositData(",
"BaseDepositData depositData",
"address swapToken",
"address exchange",
"TransferType transferType",
"uint256 swapTokenAmount",
"uint256 minExpectedInputTokenAmount",
"bytes routerCalldata)"
);

// EIP712 Type hashes.
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 =
keccak256(abi.encode(EIP712_SWAP_AND_DEPOSIT_DATA_TYPE, EIP712_BASE_DEPOSIT_DATA_TYPE));

// EIP712 Type strings.
string internal constant TOKEN_PERMISSIONS_TYPE = "TokenPermissions(address token,uint256 amount)";
string internal constant EIP712_SWAP_AND_DEPOSIT_TYPE_STRING =
string(
abi.encodePacked(
"SwapAndDepositData witness)",
EIP712_BASE_DEPOSIT_DATA_TYPE,
EIP712_SWAP_AND_DEPOSIT_DATA_TYPE,
TOKEN_PERMISSIONS_TYPE
)
);
string internal constant EIP712_DEPOSIT_TYPE_STRING =
string(
abi.encodePacked(
"DepositData witness)",
EIP712_BASE_DEPOSIT_DATA_TYPE,
EIP712_DEPOSIT_DATA_TYPE,
TOKEN_PERMISSIONS_TYPE
)
);

error InvalidSignature();

/**
* @notice Creates the EIP712 compliant hashed data corresponding to the BaseDepositData struct.
* @param baseDepositData Input struct whose values are hashed.
* @dev BaseDepositData is only used as a nested struct for both DepositData and SwapAndDepositData.
*/
function hashBaseDepositData(SpokePoolV3PeripheryInterface.BaseDepositData calldata baseDepositData)
internal
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
EIP712_BASE_DEPOSIT_DATA_TYPE,
baseDepositData.outputToken,
baseDepositData.outputAmount,
baseDepositData.depositor,
baseDepositData.recipient,
baseDepositData.destinationChainId,
baseDepositData.exclusiveRelayer,
baseDepositData.quoteTimestamp,
baseDepositData.fillDeadline,
baseDepositData.exclusivityParameter,
keccak256(baseDepositData.message)
)
);
}

/**
* @notice Creates the EIP712 compliant hashed data corresponding to the DepositData struct.
* @param depositData Input struct whose values are hashed.
*/
function hashDepositData(SpokePoolV3PeripheryInterface.DepositData calldata depositData)
internal
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
EIP712_DEPOSIT_DATA_TYPE,
hashBaseDepositData(depositData.baseDepositData),
depositData.inputAmount
)
);
}

/**
* @notice Creates the EIP712 compliant hashed data corresponding to the SwapAndDepositData struct.
* @param swapAndDepositData Input struct whose values are hashed.
*/
function hashSwapAndDepositData(SpokePoolV3PeripheryInterface.SwapAndDepositData calldata swapAndDepositData)
internal
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
EIP712_SWAP_AND_DEPOSIT_DATA_TYPEHASH,
hashBaseDepositData(swapAndDepositData.depositData),
swapAndDepositData.swapToken,
swapAndDepositData.exchange,
swapAndDepositData.transferType,
swapAndDepositData.swapTokenAmount,
swapAndDepositData.minExpectedInputTokenAmount,
keccak256(swapAndDepositData.routerCalldata)
)
);
}

/**
* @notice Reads an input bytes, and, assuming it is a signature for a 32-byte hash, returns the v, r, and s values.
* @param _signature The input signature to deserialize.
*/
function deserializeSignature(bytes calldata _signature)
internal
pure
returns (
bytes32 r,
bytes32 s,
uint8 v
)
{
if (_signature.length != 65) revert InvalidSignature();
v = uint8(_signature[64]);
r = bytes32(_signature[0:32]);
s = bytes32(_signature[32:64]);
}
}
Loading
Loading