Skip to content

[M-02] Replay Attacks on SpokePoolPeriphery Possible #1015

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 3 commits into from
Jun 24, 2025
Merged

Conversation

mrice32
Copy link
Contributor

@mrice32 mrice32 commented Jun 4, 2025

Open question: can we avoid having a separate nonce by piggybacking on the permit nonce? I think we can, but this would mean we would have to require that the permit call succeed, which would open up frontrun griefing attacks. However, I'm not sure how big a deal those are in practice (private mempools and no profit opportunity).

The SpokePoolPeriphery contract enables users to deposit or swap and deposit tokens into a SpokePool. In order to do that, the assets are first transferred from the depositor's account, then optionally swapped to a different token and finally deposited to a SpokePool.

Assets can be transferred from the depositor's account in several different ways, including approval followed by the transferFrom call, approval through the ERC-2612 permit function followed by transferFrom, transfer through the Permit2 contract and transfer through the ERC-3009 receiveWithAuthorization function. The last three methods require additional user's signatures and may be executed by anyone on behalf of a given user.

However, the data to be signed for deposits or swaps and deposits with ERC-2612 permit and with ERC-3009 receiveWithAuthorization does not contain a nonce, and as such, the signatures used for these methods once can be replayed later.

The attack can be performed if a victim signed data for a function relying on the ERC-2612 permit function and wants to deposit tokens once again using the same method and token within the time window determined by the depositQuoteTimeBuffer parameter. In such a case, an attacker can first approve tokens on behalf of the victim and then call the swapAndBridgeWithPermit function or the depositWithPermit function providing a signature for deposit or swap and deposit from the past, which included less tokens than the approved amount. As a result, the tokens will be deposited and potentially swapped before, using the data from an old signature, forcing the victim to either perform an unintended swap or to bridge tokens to a different chain than intended. Furthermore, since the attack consumes some part of the permit approval, it will not be possible to deposit tokens on behalf of a depositor using the new signature until the full amount of tokens is approved by them once again.

A similar attack is also possible for the functions relying on the ERC-3009 receiveWithAuthorization function, but it requires the amount of token to be transferred to be identical to the one from the past.

Consider adding a nonce field into the SwapAndDepositData and DepositData structs and storing a nonce for each user in the SpokePoolPeriphery contract, which would be incremented when a signature is verified and accepted.

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
@mrice32 mrice32 requested a review from grasphoper June 4, 2025 05:14
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
@mrice32 mrice32 merged commit 405125a into may-14-audit Jun 24, 2025
9 checks passed
@mrice32 mrice32 deleted the m14_m02 branch June 24, 2025 02:01
mrice32 added a commit that referenced this pull request Aug 5, 2025
* feat(SpokePoolPeriphery): Support multiple exchanges  (#777)

* feat(SpokePoolPeriphery): Support multiple exchanges

Currently we can only initialize the periphery contract with a single exchange to swap with. This PR allows us to initialize it with multiple exchanges to swap with. Like before, these initial set of exchanges and function selectors cannot be changed post-initialization, which gives the user assurances.

* rename

* Update SpokeV3PoolPeriphery.sol

* Update SpokeV3PoolPeriphery.sol

* Update SpokeV3PoolPeriphery.sol

* Add unit tests

* Add whitelistExchanges only owner method

* rename

* Remove onlyOwner

* Remove whitelist of exchanges, add proxy to bypass approval abuse

Make user approve proxy contract so no one can use `exchange` + `routerCalldata` to steal their already approved funds via the `SpokePoolPeriphery`

* Add some protection to callSpokePoolPeriphery

* Only call swapAndBridge through proxy

* move periphery funcs into proxy

* Update SpokePoolV3Periphery.sol

* remove depositERC20

* Update SpokePoolV3Periphery.sol

* Add back safeTransferFron's to permit funcs

* Add unit tests that check if calling deposit and swapAndBridge with no value fails directly

* Add interfaces to make sure we don't add new functions as easily

* Add Create2Factory

* feat: add permit2 entrypoints to the periphery (#782)

* 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

* 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>

---------

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>

* 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>

* feat: Delete SwapAndBridge and add submission fees to gasless flow (#809)

* 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>

* feat: Delete SwapAndBridge and add submission fees to gasless flow

SwapAndBridge is to be replaced with SpokePoolV3Periphery

Gasless flows will require user to cover gas cost of whoever submits the transaction, but they can be set to 0 if the user wants to submit themselves.

* Internal refactor

* Update SpokePoolV3Periphery.sol

* Update PeripherySigningLib.sol

* Update SpokePoolV3Periphery.sol

* Update PeripherySigningLib.sol

---------

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: Bennett <bennett@umaproject.org>

* Update SpokePoolV3Periphery.sol

* Update SpokePoolPeriphery.t.sol

* Move all comments to interface and use inherit doc

* fix: eip712 types and hashes (#821)

* refactor comments

Signed-off-by: bennett <bennett@umaproject.org>

* Create IERC20Auth.sol

* fix tests

* Comments

---------

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: bmzig <57361391+bmzig@users.noreply.github.com>
Co-authored-by: Bennett <bennett@umaproject.org>
Co-authored-by: Dong-Ha Kim <dongha.kim210@gmail.com>

* feat: optionally adjust output amount by swap price improvement over min (#998)

* feat: add proportional adjustment to outputAmount

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* add option to disable output adjustment

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* format

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* fix: add Nick's comment that was omitted in previous merge (#1001)

* feat: add proportional adjustment to outputAmount

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* add option to disable output adjustment

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* format

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* fix: fix Nick's comment

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* feat: add balance injection to multicall handler calls (#1000)

* feat: add balance replacements for MulticallHandler

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* fix: restructure periphery contracts to avoid frontrunning attacks (#1002)

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* fix adjustment

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* make non evm compatible, use deposit method

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* comments

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* one more comment

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* remove stray comments

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* remove unnecessary variable

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* remove errors

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* nit and type string

Signed-off-by: bennett <bennett@umaproject.org>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* Remove factory, remove initialize, remove wrappedNativeToken and spokePool from storage

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* Remove repetitive event

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* event definition too

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* naming

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
Signed-off-by: bennett <bennett@umaproject.org>
Co-authored-by: bennett <bennett@umaproject.org>

* [H-01] Incorrect Nonce Passed to the Permit2.permit Function (#1013)

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* [M-02] Replay Attacks on SpokePoolPeriphery Possible (#1015)

* [M-02] Replay Attacks on SpokePoolPeriphery Possible

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* [N-10] Remove unused code to improve clarity (#1025)

- Remove unused InvalidSignatureLength error from SpokePoolPeriphery.sol
- Remove unused IERC20Auth import from SpokePoolPeripheryInterface.sol

These changes improve the overall clarity, intentionality, and readability of the codebase by removing currently unused code.

* [N-08] Typographical Errors (#1024)

Fix several typographical errors throughout the codebase:
- Fix 'calldData' to 'callData' on line 48 of MulticallHandler.sol
- Remove unnecessary 'at' from 'receive funds at on destination chain' on line 113 of SpokePoolPeripheryInterface.sol
- Fix 'depositData/swapAndDepositData' to 'DepositData/SwapAndDepositData' on line 500 of SpokePoolPeriphery.sol

These corrections improve the clarity and readability of the codebase.

* [N-05] Insufficient Documentation (#1023)

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* [L-05] Inflexible Fee Recipient Field Blocks Open Relaying (#1021)

* [L-05] Inflexible Fee Recipient Field Blocks Open Relaying

Currently, every DepositData and SwapAndDepositData payload must include a hard-coded fee recipient address, and upon successful deposit or swap-and-bridge the periphery pays submission fees to that exact address.

While this ensures the user knows in advance exactly who will receive their fee, it also prevents open relayer competition or fallback options when the chosen relayer is unavailable or underperforms.

This change keeps the explicit fee recipient field option in SwapAndDepositData but introduces a "zero‐address" convention:

- If the fee recipient is equal to the zero address, the periphery defaults to using msg.sender as the payee
- Otherwise, transfer fees to the signed recipient as today

This enables open relayer competition while maintaining backward compatibility.

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* Rename deposit function to depositNative for clarity (#1019)

The deposit function of the SpokePoolPeriphery contract allows users to deposit native value to the SpokePool.

Although it is possible to specify the inputToken parameter, it is not possible to deposit other tokens through this function. Because of that, it could be renamed to depositNative or a similar name in order to make this fact clear.

Consider renaming the deposit function in order to improve readability of the codebase.

* [M-05] Incorrect EIP-712 Encoding (#1017)

* M-05 Incorrect EIP-712 Encoding

Fix EIP-712 encoding issue by replacing TransferType enum with uint8
in SwapAndDepositData type string and cast enum to uint8 in hash encoding.
Add comments to TransferType enum values showing their integer mappings.

* Remove unnecessary uint8 cast in EIP-712 encoding

The cast to uint8 is redundant since the EIP-712 type string already
specifies uint8 transferType, making the encoding identical without
the explicit cast.

* [N-12] Misleading Documentation (#1026)

* [N-12] Misleading Documentation

Throughout the codebase, there are several instances of misleading documentation. The examples are listed below.

The swapAndBridgeWithPermit and depositWithPermit functions are documented to fail if the provided token does not support the EIP-2612 permit function. However, the implementation contradicts this statement as in both functions, the call to permit is wrapped in a try/catch block, and any failure is silently ignored.
The comment refers to the transferWithAuthorization function, while it should mention the receiveWithAuthorization function instead.
The documentation of the SpokePoolPeriphery contract and the SpokePoolPeripheryInterface interface contain an outdated comment claiming that certain variables are not marked immutable or set in the constructor to allow deterministic deployment. This is no longer true as the variables are now immutable and set via the constructor.
Consider fixing the instances mentioned above in order to enhance the clarity of the codebase.

* Clarify permit call documentation

Update comment to specify that the permit call result is ignored rather than the call itself when tokens don't implement EIP-2612 permit functionality.

* [M-03] DoS Attack on Swapping via Permit2 Possible (#1016)

* M-03: Prevent DoS attack by disallowing Permit2 as exchange address

* fix test

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* L-01 deposit Will Not Work for Non-EVM Target Chains (#1018)

The deposit function of the SpokePoolPeriphery contract allows users to deposit native value to the SpokePool.

However, its recipient and exclusiveRelayer arguments are both of type address and are then casted to bytes32. As a result, it is not possible to bridge wrapped native tokens to non-EVM blockchains.

Consider changing the type of the recipient and exclusiveRelayer arguments of the deposit function so that callers are allowed to specify non-EVM addresses for deposits.

* [L-03] Document integer overflow case in _swapAndBridge function (#1020)

Add documentation to all external swap-and-bridge functions explaining
the potential overflow case when depositData.outputAmount * returnAmount
exceeds 2^256-1 during proportional adjustment calculations.

* [N-03] Optimization Opportunities (#1022)

* [N-03] Optimization Opportunities

Throughout the codebase, there are several places where the code could be optimized in order to save gas. The examples are:

The checks validating that a given address refers to a contract in lines 204, 231 and 553 are not necessary as in case when the addresses do not refer to contracts, the subsequent calls at lines 207, 233 and 555 will revert as the Solidity compiler inserts similar code size checks before each high-level call.
The "0x" string passed to permit call could be replaced with "".
The check could be removed as the same check is already performed in SpokePools. This would additionally allow users to deposit non-native tokens through the SpokePoolPeriphery.deposit function.
The replacement argument of the makeCallWithBalance function could be stored in calldata instead of memory.
The use of the Lockable contract is inefficient. OpenZeppelin's ReentrancyGuard delivers significantly lower gas overhead by using a two‐word uint256 status in place of a bool, reducing SSTORE costs, and swapping long revert strings for a 4-byte custom error to shrink both bytecode and revert gas. For deployments on chains that support EIP-1153 (transient storage), adopting ReentrancyGuardTransient can further nearly eliminate reentrancy‐guard gas costs.
Consider applying the suggestions above in order to provide more gas efficient code.

* WIP

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* optimize: store replacement array in calldata instead of memory

Changes the replacement parameter in makeCallWithBalance function from
memory to calldata for gas optimization since the array is only read
from and never modified.

* fix test

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

* fix tests

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>

---------

Signed-off-by: Bennett <bennett@umaproject.org>
Signed-off-by: bennett <bennett@umaproject.org>
Signed-off-by: nicholaspai <npai.nyc@gmail.com>
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
Co-authored-by: nicholaspai <9457025+nicholaspai@users.noreply.github.com>
Co-authored-by: bmzig <57361391+bmzig@users.noreply.github.com>
Co-authored-by: Bennett <bennett@umaproject.org>
Co-authored-by: Dong-Ha Kim <dongha.kim210@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant