Summary
Timeline: From 2025-03-28 To 2025-04-11
Total Issues: 37 (34 resolved, 2 partially resolved)
High Severity Issues: 1 (1 resolved)
Medium Severity Issues: 4 (4 resolved)
Low Severity Issues: 18 (16 resolved, 1 partially resolved)
Notes & Additional Information: 14 (13 resolved, 1 partially resolved)
Scope
OpenZeppelin audited the woof-software/migrator-v2 repository at commit f5385e7.
The fix review was conducted at commit 4f57d33. All fixes were submitted as one commit.
In scope were the following files:
contracts
├── MigratorV2.sol
├── adapters
│ ├── AaveV3Adapter.sol
│ ├── AaveV3UsdsAdapter.sol
│ ├── MorphoAdapter.sol
│ ├── MorphoUsdsAdapter.sol
│ ├── SparkAdapter.sol
│ └── SparkUsdsAdapter.sol
├── errors
│ └── CommonErrors.sol
├── modules
│ ├── ConvertModule.sol
│ └── SwapModule.sol
└── utils
├── DelegateReentrancyGuard.sol
└── UniswapV3PathFinder.sol
System Overview
MigratorV2
is a flexible and modular smart contract designed to facilitate the migration of user positions from multiple lending protocols (Aave, Spark, and Morpho) to Compound III (Comet). The contract leverages Uniswap V3 flash loans to streamline the migration process, minimizing gas costs, reducing manual interventions, and ensuring smooth transitions without disrupting user positions. With its adaptable architecture, MigratorV2
is designed to handle future protocol extensions without requiring redeployment.
Migration
The migration process involves several key steps to facilitate the transfer of assets between protocols.
- Initially, the contract may initiate a flash loan from Uniswap V3, using either the base token of the target market or DAI for USDS markets. The loaned funds are then used to repay the user’s existing debt in the source protocol, which could be Aave, Spark, or Morpho.
- Following debt repayment, the contract withdraws the user’s collateral from the source protocol. If necessary, the contract swaps assets via Uniswap V3 or converts DAI to USDS at a 1:1 ratio to enable smooth migration. Users have the option to select specific collaterals to migrate or migrate all available assets, either fully or partially.
- Finally, the migrated collateral is deposited into the selected Compound III market on behalf of the user, with the option to deposit in the market’s base token (such as USDC or USDS) or in one of the supported collateral tokens.
Flash Loan Repayment
The flash loan repayment logic involves different strategies depending on the type of collateral migrated. In the case of full collateral migration using the base token, part of the migrated funds will be used to repay the flash loan along with its associated fees. When collateral is migrated as non-base tokens, the contract attempts to repay the loan by withdrawing the necessary amount of the base token from the user’s Compound III balance. For mixed collateral migrations, where some funds are in base tokens and others are in collateral tokens, a debt is created if the base token balance is insufficient to cover the loan repayment. This debt can either be deducted from the base token or withdrawn from the collateral in Compound III.
Security Model and Trust Assumptions
During the audit, the following trust assumptions were made:
- For the adapter contracts, the provided swap paths are correctly formulated and appropriate for the type of swap requested.
- The deployer supplies accurate contract addresses for integrations with Uniswap V3 Pools, the lending protocols, and Comet Markets.
- The USDS-to-DAI and DAI-to-USDS conversion pathways will remain unpaused and fees cannot be enabled for these conversions.
Privileged Roles
The only privileged role in this deployment is the owner
of the MigratorV2
contract. This role has the ability to:
- Manage adapter contracts for integrating with various protocols,
- Configure the liquidity pools used to provide flash loans for specific Comet Markets, and
- Pause or unpause designated functions.
It is assumed that the Compound Timelock will be assigned as the owner
of the MigratorV2
contract.
Notes on UniswapV3PathFinder
The review identified multiple issues with the UniswapV3PathFinder
contract. In response to the issues raised, the Woof! team has indicated that the aforementioned contract has been deprecated and will not be used within the Compound ecosystem. The team has moved the contract to a non-production folder in commit 4f57d33.
It is noted that the UniswapV3PathFinder
contract should be re-audited before it is deployed and/or used in conjunction with any elements interacting with the Compound ecosystem (e.g., if it is to be used as a utility to provide information for a related UI).
High Severity
Multi-Swap Paths Cannot Be Constructed
The getBestMultiSwapPath
function of the UniswapV3PathFinder
contract finds the best multi-hop swap path for a given token pair. The MultiSwapParams
input allows the user to specify an array of intermediate tokens as part of the multi-hop swap. The issue arises when the function attempts to construct the best connector paths. The path is always constructed with either the tokenIn
or the tokenOut
set as the tokenIn
input variable for the _getBestSingleSwapPath
. The _connectorPaths
constructed in this way will always have either the params.tokenIn
or params.tokenOut
set as the inbound tokenIn
.
The issue is that the _connectorPaths
constructed above can now only be a single hop in length: from the tokenIn
to the intermediary token represented by connectors[i]
. These single-leg swaps are provided to the _getBestMultiSwapPath
function inside a for
loop which can only ever evaluate the single path it was provided and will, based on the estimatedAmount
returned, override the value of the path
named return variable. Consequently, the function is unable to evaluate and construct multi-swap paths and will simply return the path of the single path swap that returned the highest amount of tokens.
Consider correcting the path construction logic such that the outbound token for each hop of a multi-swap path becomes the inbound token for the next swap in the connector path.
Update: Resolved in commit 4f57d33. As per the Woof! team, the UniswapV3PathFinder
contract has been deprecated and will not be used within the Compound ecosystem.
Medium Severity
Pausable Functionality Not Integrated
The MigratorV2
contract inherits from OpenZeppelin’s Pausable
contract, enabling the owner to pause specific contract functionalities. However, the whenNotPaused
and whenPaused
modifiers are not applied to any of the contract functions. As a result, pausing the contract will have no impact, and all operations will continue, including the sensitive migrate
function. Additionally, the constructor
documentation states that “the contract is paused if any of the input arrays are empty”. However, neither is any check made against the length of the input arrays nor is an internal call made to _pause
. This, the contract will effectively be unpaused even if this condition is met.
Consider applying the whenNotPaused
or whenPaused
modifiers to all relevant functions in the contract that are intended to be pausable (e.g., migrate
). Also, consider pausing the contract as specified if the constructor is passed empty input arrays, or updating the documentation to reflect the current logic.
Update: Resolved in commit 4f57d33.
USDS
/DAI
Connector Tokens Incorrectly Handled
The UniswapV3PathFinder
contract is used to identify the optimal path for single-hop and multi-hop token swaps. In the case of a single-hop swap between USDS
and DAI
, the UniswapV3
pool is not utilized as for all other generic token swaps, and instead a 1-1 conversion is directly facilitated.
The getBestMultiSwapPath
function provides no similar special consideration for swaps between USDS
and DAI
. Instead, the _getBestSingleSwapPath
function is called internally as with all other token types. As a result, instead of utilizing a 1-1 conversion between the tokens, the UniswapV3
pool for these tokens will be used.
Consider applying the same 1-1 conversion rate for swaps between USDS
and DAI
within the multi-hop swap path.
Update: Resolved in commit 4f57d33. As per the Woof! team, the UniswapV3PathFinder
contract has been deprecated and will not be used within the Compound ecosystem.
Incorrect Flashloan Token Repaid
The AaveV3UsdsAdapter
, SparkUsdsAdapter
, and MorphoUsdsAdapter
contracts enable the migration of collateral and borrow positions from their respective lending protocols to the target Comet Market. These adapters extend the functionality of the standard adapters by supporting conversion between USDS and DAI across multiple entry points, including flashloan repayment. For any other tokens, the flashloan is repaid by withdrawing from the user’s Comet position (as in the standard adapter).
However, across the three USDS-enabled adapters, the wrong token is withdrawn from the Comet Market token when repaying the flashloan. Although the flashBaseToken
is repaid, the cometBaseToken
is withdrawn from the user’s position. As a result, the flashloan repayment may fail, and any migrations involving flashloans with tokens other than DAI or USDS will not be guaranteed to succeed.
Consider updating the adapters to ensure that the flashBaseToken
is withdrawn from the Comet Market rather than the cometBaseToken
.
Update: Resolved in commit 4f57d33. The fix has been applied across the SparkUsdsAdapter
, AaveV3UsdsAdapter
, and MorphoUsdsAdapter
contracts.
Migrations Using Flashloans Can Be Replayed
The MigratorV2
contract exposes a uniswapV3FlashCallBack
function which is intended to be called as part of the migrate
function flow when a user requires a flashloan as part of their migration. The uniswapV3FlashCallback
function is protected from unauthorized external calls by a _storedCallbackHash
check which ensures that the callback data supplied matches that which was crafted as part of the migrate
call. However, when a user requires a flashloan in order to execute the migration, _storedCallbackHash
is never reset. This means that an attacker can call uniswapV3FlashCallBack
with the same data
as the previous user and potentially replay the last user’s migration (if the user has granted enough allowance).
Consider resetting the _storedCallbackHash
at the end of the uniswapV3FlashCallBack
implementation to prevent the re-use of the same migration data by an attacker.
Update: Resolved in commit 4f57d33.