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 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 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
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
117 changes: 81 additions & 36 deletions contracts/SpokePoolV3Periphery.sol
Copy link
Contributor

@bmzig bmzig Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would greatly increase the diff, but what do you think about replacing all the duplicate natspec comments on the functions here which are defined in SpokePoolV3PeripheryInterface with a single // @inheritdoc SpokePoolV3PeripheryInterface? This way we don't need to maintain two identical pieces of documentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let's do that

Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,71 @@ import { V3SpokePoolInterface } from "./interfaces/V3SpokePoolInterface.sol";
import { IERC20Auth } from "./external/interfaces/IERC20Auth.sol";
import { WETH9Interface } from "./external/interfaces/WETH9Interface.sol";

/**
* @title SpokePoolProxy
* @notice User should only interact with SpokePool via the SpokePoolV3Periphery contract through this
* contract. This is purposefully a simple passthrough contract so that the user only approves this contract
* to pull its assets while the SpokePoolV3Periphery contract can be used to call any calldata on any exchange
* that the user wants to. By separating the contract that gets approved from the contract that executes arbitrary
* calldata, the SpokePoolPeriphery does not need to validate the calldata that gets executed.
* @dev If this proxy didn't exist and users instead approved and interacted directly with the SpokePoolV3Periphery
* then users would run the unneccessary risk that another user could instruct the Periphery contract to steal
* any approved tokens that the user had left outstanding.
*/
contract SpokePoolProxy is Lockable {
using SafeERC20 for IERC20;

error LeftoverInputTokens();

// The SpokePoolPeriphery should be deterministically deployed at the same address across all networks,
// so this contract should also be able to be deterministically deployed at the same address across all networks
// since the periphery address is the only constructor argument.
address public immutable SPOKE_POOL_PERIPHERY;

constructor(address _spokePoolPeriphery) {
SPOKE_POOL_PERIPHERY = _spokePoolPeriphery;
}

/**
* @notice Caller must insure that exactly `inputAmount` of `inputToken` is used in the subsequent call to
* the SpokePoolPeriphery contract. All of the periphery functions have an easily identifiable input
* amount and input token so this should be easy to verify. Any leftover tokens would be locked in this contract
* and could be used by ANYONE in a subsequent call to the SpokePoolPeriphery contract. So, this function
* attempts to protect the user from locking tokens by reverting if not exactly `inputAmount` of `inputToken`
* is used in the periphery contract call.
*/
function callSpokePoolPeriphery(
address inputToken,
uint256 inputAmount,
bytes memory peripheryFunctionCalldata
) external payable nonReentrant {
uint256 balanceBefore = IERC20(inputToken).balanceOf(address(this));
IERC20(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount);
IERC20(inputToken).forceApprove(SPOKE_POOL_PERIPHERY, inputAmount);
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory result) = SPOKE_POOL_PERIPHERY.call{ value: msg.value }(peripheryFunctionCalldata);
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
require(success, string(result));
uint256 balanceAfter = IERC20(inputToken).balanceOf(address(this));
if (balanceAfter != balanceBefore) revert LeftoverInputTokens();
}
}

/**
* @title SpokePoolV3Periphery
* @notice Contract for performing more complex interactions with an AcrossV3 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.
* @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 [email protected]
*/
contract SpokePoolV3Periphery is Lockable, MultiCaller {
using SafeERC20 for IERC20;
using Address for address;

// This contract performs a low level call with arbirary data to an external contract. This is a large attack
// surface and we should whitelist which function selectors are allowed to be called on the exchange.
mapping(bytes4 => bool) public allowedSelectors;

// Across SpokePool we'll submit deposits to with acrossInputToken as the input token.
V3SpokePoolInterface public spokePool;

// Exchange address or router where the swapping will happen.
address public exchange;

// Wrapped native token contract address.
WETH9Interface internal wrappedNativeToken;
WETH9Interface public wrappedNativeToken;

// Boolean indicating whether the contract is initialized.
bool private initialized;
Expand All @@ -52,7 +95,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
// chain and if this is a contract then they will receive an ERC20.
address recipient;
// The destination chain identifier.
uint256 destinationChainid;
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
Expand All @@ -69,6 +112,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {

event SwapBeforeBridge(
address exchange,
bytes exchangeCalldata,
address indexed swapToken,
address indexed acrossInputToken,
uint256 swapTokenAmount,
Expand All @@ -82,45 +126,36 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
****************************************/
error MinimumExpectedInputAmount();
error LeftoverSrcTokens();
error InvalidFunctionSelector();
error ContractInitialized();
error InvalidMsgValue();
error InvalidSpokePool();
error InvalidSwapToken();

/**
* @notice Construct a new SwapAndBridgeBase contract.
* @param _allowedSelectors Function selectors that are allowed to be called on the exchange.
* @dev Is empty and all of the state variables are initialized in the initialize function
* to allow for deployment at a deterministic address via create2, which requires that the bytecode
* across different networks is the same. Constructor parameters affect the bytecode so we can only
* add parameters here that are consistent across networks.
*/
constructor(bytes4[] memory _allowedSelectors) {
for (uint256 i = 0; i < _allowedSelectors.length; i++) {
allowedSelectors[_allowedSelectors[i]] = true;
}
}
constructor() {}

/**
* @notice Initializes the SwapAndBridgeBase contract.
* @dev Only the owner can call this function.
* @param _spokePool Address of the SpokePool contract that we'll submit deposits to.
* @param _wrappedNativeToken Address of the wrapped native token for the network this contract is deployed to.
* @param _exchange Address of the exchange where tokens will be swapped.
* @dev These values are initialized in a function and not in the constructor so that the creation code of this contract
* is the same across networks with different addresses for the wrapped native token, the exchange this contract uses to
* swap and bridge, and this network's corresponding spoke pool contract. This is to allow this contract to be deterministically
* deployed with CREATE2.
* @dev This function can be front-run by anybody, so it is critical to check that the `spokePool`, `wrappedNativeToken`, and `exchange`
* values used in the single call to this function were passed in correctly before enabling the usage of this contract.
* is the same across networks with different addresses for the wrapped native token and this network's
* corresponding spoke pool contract. This is to allow this contract to be deterministically deployed with CREATE2.
*/
function initialize(
V3SpokePoolInterface _spokePool,
WETH9Interface _wrappedNativeToken,
address _exchange
) external {
function initialize(V3SpokePoolInterface _spokePool, WETH9Interface _wrappedNativeToken) external nonReentrant {
if (initialized) revert ContractInitialized();
initialized = true;

if (!address(_spokePool).isContract()) revert InvalidSpokePool();
spokePool = _spokePool;
wrappedNativeToken = _wrappedNativeToken;
exchange = _exchange;
}

/**
Expand Down Expand Up @@ -184,6 +219,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
* the assumption is that this function will handle only ERC20 tokens.
* @param swapToken Address of the token that will be swapped for acrossInputToken.
* @param acrossInputToken Address of the token that will be bridged via Across as the inputToken.
* @param exchange Address of the exchange contract to call.
* @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for
* enough of acrossInputToken, otherwise this function will revert.
* @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken.
Expand All @@ -194,6 +230,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
function swapAndBridge(
bmzig marked this conversation as resolved.
Show resolved Hide resolved
IERC20 swapToken,
IERC20 acrossInputToken,
address exchange,
bytes calldata routerCalldata,
uint256 swapTokenAmount,
uint256 minExpectedInputTokenAmount,
Expand All @@ -209,6 +246,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
swapToken.safeTransferFrom(msg.sender, address(this), swapTokenAmount);
}
_swapAndBridge(
exchange,
routerCalldata,
swapTokenAmount,
minExpectedInputTokenAmount,
Expand All @@ -224,6 +262,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
* @dev If swapToken does not implement `permit` to the specifications of EIP-2612, this function will fail.
* @param swapToken Address of the token that will be swapped for acrossInputToken.
* @param acrossInputToken Address of the token that will be bridged via Across as the inputToken.
* @param exchange Address of the exchange contract to call.
* @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for
* enough of acrossInputToken, otherwise this function will revert.
* @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken.
Expand All @@ -238,6 +277,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
function swapAndBridgeWithPermit(
IERC20Permit swapToken,
IERC20 acrossInputToken,
address exchange,
bytes calldata routerCalldata,
uint256 swapTokenAmount,
uint256 minExpectedInputTokenAmount,
Expand All @@ -255,6 +295,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {

_swapToken.safeTransferFrom(msg.sender, address(this), swapTokenAmount);
_swapAndBridge(
exchange,
routerCalldata,
swapTokenAmount,
minExpectedInputTokenAmount,
Expand All @@ -270,6 +311,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
* @dev If swapToken does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert.
* @param swapToken Address of the token that will be swapped for acrossInputToken.
* @param acrossInputToken Address of the token that will be bridged via Across as the inputToken.
* @param exchange Address of the exchange contract to call.
* @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for
* enough of acrossInputToken, otherwise this function will revert.
* @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken.
Expand All @@ -286,6 +328,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
function swapAndBridgeWithAuthorization(
IERC20Auth swapToken,
IERC20 acrossInputToken,
address exchange,
bytes calldata routerCalldata,
uint256 swapTokenAmount,
uint256 minExpectedInputTokenAmount,
Expand Down Expand Up @@ -314,6 +357,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
IERC20 _swapToken = IERC20(address(swapToken)); // Cast IERC20Auth to IERC20.

_swapAndBridge(
exchange,
routerCalldata,
swapTokenAmount,
minExpectedInputTokenAmount,
Expand Down Expand Up @@ -411,7 +455,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
depositData.outputToken, // output token
_acrossInputAmount, // input amount.
depositData.outputAmount, // output amount
depositData.destinationChainid,
depositData.destinationChainId,
depositData.exclusiveRelayer,
depositData.quoteTimestamp,
depositData.fillDeadline,
Expand All @@ -422,18 +466,14 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {

// This contract supports two variants of swap and bridge, one that allows one token and another that allows the caller to pass them in.
function _swapAndBridge(
address exchange,
bytes calldata routerCalldata,
uint256 swapTokenAmount,
uint256 minExpectedInputTokenAmount,
DepositData calldata depositData,
IERC20 _swapToken,
IERC20 _acrossInputToken
) private {
// Note: this check should never be impactful, but is here out of an abundance of caution.
// For example, if the exchange address in the contract is also an ERC20 token that is approved by some
// user on this contract, a malicious actor could call transferFrom to steal the user's tokens.
if (!allowedSelectors[bytes4(routerCalldata)]) revert InvalidFunctionSelector();

// Swap and run safety checks.
uint256 srcBalanceBefore = _swapToken.balanceOf(address(this));
uint256 dstBalanceBefore = _acrossInputToken.balanceOf(address(this));
Expand All @@ -444,6 +484,8 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
require(success, string(result));

_checkSwapOutputAndDeposit(
exchange,
routerCalldata,
swapTokenAmount,
srcBalanceBefore,
dstBalanceBefore,
Expand All @@ -462,6 +504,8 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {
* @param minExpectedInputTokenAmount Minimum amount of received acrossInputToken that we'll bridge
**/
function _checkSwapOutputAndDeposit(
address exchange,
bytes memory routerCalldata,
uint256 swapTokenAmount,
uint256 swapTokenBalanceBefore,
uint256 inputTokenBalanceBefore,
Expand All @@ -480,6 +524,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller {

emit SwapBeforeBridge(
exchange,
routerCalldata,
address(_swapToken),
address(_acrossInputToken),
swapTokenAmount,
Expand Down
Loading
Loading