The Comet Migrator v2 is a set of contracts to transfer one or more positions from Compound II and other DeFi protocols to Compound III.
The CometMigrator contract is used to transfer any number of positions where a user is borrowing a token from Compound II, Aave, or Maker to a position where that user is now borrowing the base asset (e.g. USDC) in a Compound III deployment. We use a flash loan to facilitate the transition. Positions can be transferred in whole or in part.
Users can specify the following parameters, generally:
- Collateral to transfer: A user may choose how much collateral to transfer, e.g. all of my UNI and part of my COMP.
- Amount to repay: The user may choose how much to repay of USDC (e.g. all of it or 2000 USDC).
- Position source: The user may specify which DeFi protocol (e.g. Compound II, Aave, Maker) a position currently lives on.
- Max loan: The user may specify the max size of the flash loan to take when migrating positions. This can be used to prevent the user from paying too much slippage and fees during the Uniswap swaps.
comet: Comet
immutable: The Comet Ethereum mainnet USDC contract.uniswapLiquidityPool: IUniswapV3Pool
immutable: The Uniswap pool used by this contract to source liquidity (i.e. flash loans).swapRouter: ISwapRouter
immutable: The Uniswap router for facilitating token swaps.isUniswapLiquidityPoolToken0: boolean
immutable: True if borrow token is token 0 in the Uniswap liquidity pool, otherwise false if token 1.baseToken: IERC20
immutable: The base token of the Compound III market (e.g.USDC
).cETH: CToken
immutable: The address of thecETH
token.weth: WETH9
immutable: The address of theweth
token.aaveV2LendingPool: ILendingPool
immutable: The address of the Aave v2 LendingPool contract. This is the contract that allwithdraw
andrepay
transactions through.cdpManager: CDPManagerLike
immutable: The address of the Maker CDP Manager contract. This is used to manage CDP positions owned by the user.daiJoin: DaiJoin
immutable: The address of the DaiJoin contract used to deposit/withdraw DAI into the Maker system.dai: IERC20
immutable: The address of theDAI
token.sweepee: address
immutable: Sweep excess tokens to this address.inMigration: uint256
: A reentrancy guard.
Represents the configuration for executing a Uniswap swap.
struct Swap {
bytes path; // empty path if no swap is required (e.g. repaying USDC borrow)
uint256 amountInMaximum; // Note: Can be set as `type(uint256).max`
}
Represents a set of positions on Compound II to migrate:
struct CompoundV2Position {
CompoundV2Collateral[] collateral;
CompoundV2Borrow[] borrows;
Swap[] swaps;
}
Represents a given amount of collateral to migrate.
struct CompoundV2Collateral {
CToken cToken;
uint256 amount; // Note: amount of cToken
}
Represents a given amount of borrow to migrate.
struct CompoundV2Borrow {
CToken cToken;
uint256 amount; // Note: amount of underlying
}
Represents a set of positions on Aave V2 to migrate:
struct AaveV2Position {
AaveV2Collateral[] collateral;
AaveV2Borrow[] borrows;
Swap[] swaps;
}
Represents a given amount of collateral to migrate.
struct AaveV2Collateral {
AToken aToken;
uint256 amount;
}
Represents a given amount of borrow to migrate.
struct AaveV2Borrow {
ADebtToken aDebtToken; // Note: Aave has two separate debt tokens per asset: stable and variable rate
uint256 amount;
}
Represents a CDP position on Maker to migrate:
struct CDPPosition {
uint256 cdpId;
uint256 collateralAmount;
uint256 borrowAmount;
Swap swap;
GemJoin gemJoin; // the adapter contract for depositing/withdrawing collateral
}
Represents all data required to continue operation after a flash loan is initiated.
struct MigrationCallbackData {
address user,
uint256 flashAmount,
CompoundV2Position compoundV2Position,
AaveV2Position aaveV2Position,
CDPPosition[] cdpPositions
}
event Migrated(
address indexed user,
CompoundV2Position compoundV2Position,
AaveV2Position aaveV2Position,
CDPPosition[] cdpPositions,
uint256 flashAmount,
uint256 flashAmountWithFee)
This function describes the initialization process for this contract. We set the Compound III contract address and track valid collateral tokens.
comet_: Comet
: The Comet Ethereum mainnet USDC contract.baseToken_: IERC20
: The base token of the Compound III market (e.g.USDC
).cETH_: CToken
: The address of thecETH
token.weth_: IWETH9
: The address of theWETH9
token.aaveV2LendingPool: ILendingPool
: The address of the Aave v2 LendingPool contract. This is the contract that allwithdraw
andrepay
transactions go through.cdpManager: CDPManagerLike
: The address of the Maker CDP Manager contract. This is used to manage CDP positions owned by the user.daiJoin: DaiJoin
: The address of the DaiJoin contract used to deposit/withdraw DAI into the Maker system.uniswapLiquidityPool_: IUniswapV3Pool
: The Uniswap pool used by this contract to source liquidity (i.e. flash loans).swapRouter_: ISwapRouter
: The Uniswap router for facilitating token swaps.sweepee_: address
: Sweep excess tokens to this address.
function CometMigrator(Comet comet_, IERC20 baseToken_, CToken cETH_, WETH9 weth, ILendingPool aaveV2LendingPool_, CDPManagerLike cdpManager_, DaiJoin daiJoin_, UniswapV3Pool uniswapLiquidityPool_, ISwapRouter swapRouter_, address sweepee_) external
- WRITE IMMUTABLE
comet = comet_
- WRITE IMMUTABLE
baseToken = baseToken_
- WRITE IMMUTABLE
cETH = cETH_
- WRITE IMMUTABLE
weth = weth_
- WRITE IMMUTABLE
aaveV2LendingPool = aaveV2LendingPool_
- WRITE IMMUTABLE
cdpManager = cdpManager_
- WRITE IMMUTABLE
daiJoin = daiJoin_
- WRITE IMMUTABLE
dai = daiJoin_.gem()
- WRITE IMMUTABLE
uniswapLiquidityPool = uniswapLiquidityPool_
- WRITE IMMUTABLE
isUniswapLiquidityPoolToken0 = uniswapLiquidityPool.token0() == baseToken
- REQUIRE
isUniswapLiquidityPoolToken0 || uniswapLiquidityPool.token1() == baseToken
- WRITE IMMUTABLE
swapRouter = swapRouter_
- WRITE IMMUTABLE
sweepee = sweepee_
- CALL
baseToken.approve(address(swapRouter), type(uint256).max)
This is the core function of this contract, migrating a position from Compound II to Compound III. We use a flash loan from Uniswap to provide liquidity to move the position.
N.B. Collateral requirements may be different in Compound II and Compound III. This may lead to a migration failing or being less collateralized after the migration. There are fees associated with the flash loan, which may affect position or cause migration to fail.
Before calling this function, a user is required to:
- a) Call
comet.allow(migrator, true)
- b) For each
{cToken, amount}
inCompoundV2Position.collateral
, callcToken.approve(migrator, amount)
. - c) For each
{aToken, amount}
inAaveV2Position.collateral
, callaToken.approve(migrator, amount)
. - d) For each
cdpId
inCDPPosition
, callcdpManager.cdpAllow(cdpId, migrator, 1)
.
Notes for (b):
- allowance may be greater than
amount
, such as max uint256, but may not be less. - allowances are in native cToken, not underlying amounts.
compoundV2Position: CompoundV2Position
- Structure containing the user’s Compound V2 collateral and borrow positions to migrate to Compound III. See notes below.aaveV2Position: AaveV2Position
- Structure containing the user’s Aave V2 collateral and borrow positions to migrate to Compound III. See notes below.cdpPositions: CDPPosition[]
- List of structures that each represent a single CDP’s collateral and borrow position to migrate to Compound III. See notes below.flashAmount: uint256
- Amount of base asset to borrow from the Uniswap flash loan to facilitate the migration. See notes below.
Notes:
- Each
collateral
market must be supported in Compound III. collateral
amounts of max uint256 are set to the user's current balance.flashAmount
is provided by the user as a hint to the Migrator to know the maximum expected cost (in terms of the base asset) of the migration. IfflashAmount
is less than the total amount needed to migrate the user’s positions, the transaction will revert.
user: address
: Alias formsg.sender
data: bytes[]
: The ABI-encoding of theMigrationCallbackData
, to be passed to the Uniswap Liquidity Pool Callback.
function migrate(compoundV2Position: CompoundV2Position, aaveV2Position: AaveV2Position, cdpPositions: CDPPosition[], flashAmount: uint256) external
- REQUIRE
inMigration == 0
- STORE
inMigration += 1
- BIND
user = msg.sender
- REQUIRE
compoundV2Position.borrows.length == compoundV2Position.swaps.length
- REQUIRE
aaveV2Position.borrows.length == aaveV2Position.swaps.length
- BIND
data = abi.encode(MigrationCallbackData{user, flashAmount, compoundV2Position, aaveV2Position, makerPositions})
- CALL
uniswapLiquidityPool.flash(address(this), isUniswapLiquidityPoolToken0 ? flashAmount : 0, isUniswapLiquidityPoolToken0 ? 0 : flashAmount, data)
- STORE
inMigration -= 1
This function handles a callback from the Uniswap Liquidity Pool after it has sent this contract the requested tokens. We are responsible for repaying those tokens, with a fee, before we return from this function call.
This function may only be called during a migration command. We check that the call originates from the expected Uniswap pool, and we check that we are actively processing a migration. This combination of events should ensure that no external party can trigger this code, though it's not clear it would be dangerous even if such a party did.
uint256 fee0
: The fee for borrowing token0 from pool.uint256 fee1
: The fee for borrowing token1 from pool.calldata data
: The data encoded above, which is the ABI-encoding ofMigrationCallbackData
.
user: address
: Alias formsg.sender
flashAmount: uint256
: The amount of base asset borrowed as part of the Uniswap flash loan.flashAmountWithFee: uint256
: The amount to borrow from Compound III to pay back the flash loan, accounting for fees.compoundV2Position: CompoundV2Position
: Structure containing the user’s Compound II collateral and borrow positions to migrate to Compound III. Array of collateral to transfer into Compound III.aaveV2Position: AaveV2Position
: Structure containing the user’s Aave V2 collateral and borrow positions to migrate to Compound III.cdpPositions: CDPPosition[]
: List of structures that each represent a single CDP’s collateral and borrow position to migrate to Compound III.underlying: IERC20
: The underlying of a cToken, orweth
in the case ofcETH
.
function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data)
- REQUIRE
inMigration == 1
- REQUIRE
msg.sender == uniswapLiquidityPool
- BIND
MigrationCallbackData{user, flashAmount, compoundV2Position, aaveV2Position, cdpPositions} = abi.decode(data, (MigrationCallbackData))
- BIND
flashAmountWithFee = flashAmount + isUniswapLiquidityPoolToken0 ? fee0 : fee1
- EXEC
migrateCompoundV2Position(user, compoundV2Position)
- EXEC
migrateAaveV2Position(user, aaveV2Position)
- EXEC
migrateCdpPositions(user, cdpPositions)
- WHEN
baseToken.balanceOf(address(this)) < flashAmountWithFee
:- CALL
comet.withdrawFrom(user, address(this), baseToken, flashAmountWithFee - baseToken.balanceOf(address(this)))
- CALL
- CALL
baseToken.transfer(address(uniswapLiquidityPool), flashAmountWithFee)
- EMIT
Migrated(user, compoundV2Position, aaveV2Position, cdpPositions, flashAmount, flashAmountWithFee)
This internal helper function repays the user’s borrow positions on Compound V2 (executing swaps first if necessary) before migrating their collateral over to Compound III.
address user
: Alias for themsg.sender
of the originalmigrate
callcompoundV2Position CompoundV2Position
- Structure containing the user’s Compound V2 collateral and borrow positions to migrate to Compound III.
user: address
: Alias formsg.sender
repayAmount: uint256
: The amount to repay for each borrow position.underlying: IERC20
: The underlying of a cToken, orweth
in the case ofcETH
.cTokenAmount: uint256
: The amount ofcToken
to redeem from Compound V2.underlyingCollateralAmount: uint256
: The amount of the underlying collateral to supply to Compound III.
function migrateCompoundV2Position(address user, CompoundV2Position position) internal
- FOREACH
(cToken, borrowAmount): CompoundV2Borrow, swap: Swap
inposition
:- WHEN
borrowAmount == type(uint256).max)
:- BIND READ
repayAmount = cToken.borrowBalanceCurrent(user)
- BIND READ
- ELSE
- BIND
repayAmount = borrowAmount
- BIND
- WHEN
swap.path.length > 0
:- CALL
ISwapRouter.exactOutput(ExactOutputParams({path: swap.path, recipient: address(this), amountOut: repayAmount, amountInMaximum: swap.amountInMaximum})
- CALL
- WHEN
cToken == cETH
- CALL
weth.withdraw(repayAmount)
- CALL
cToken.repayBorrowBehalf{value: repayAmount}(user)
- CALL
- ELSE
- CALL
cToken.underlying().approve(address(cToken), repayAmount)
- CALL
cToken.repayBorrowBehalf(user, repayAmount)
- CALL
- CALL
- WHEN
- FOREACH
(cToken, amount): CompoundV2Collateral
inposition.collateral
:- BIND
cTokenAmount = amount == type(uint256).max ? cToken.balanceOf(user) : amount)
- CALL
cToken.transferFrom(user, address(this), cTokenAmount)
- CALL
cToken.redeem(cTokenAmount)
- BIND
underlyingCollateralAmount = collateral.cToken.exchangeRateStored() * cTokenAmount / 1e18
- WHEN
cToken == cETH
:- CALL
weth.deposit{value: underlyingCollateralAmount}()
- BIND
underlying = weth
- CALL
- ELSE
- BIND READ
underlying = cToken.underlying()
- BIND READ
- CALL
underlying.approve(address(comet), underlyingCollateralAmount)
- CALL
comet.supplyTo(user, underlying, underlyingCollateralAmount)
- BIND
This internal helper function repays the user’s borrow positions on Aave V2 (executing swaps first if necessary) before migrating their collateral over to Compound III.
address user
: Alias for themsg.sender
of the originalmigrate
callaaveV2Position AaveV2Position
- Structure containing the user’s Aave V2 collateral and borrow positions to migrate to Compound III.
user: address
: Alias formsg.sender
repayAmount: uint256
: The amount to repay for each borrow position.rateMode: uint256
: The rate mode for the current borrow. 1 for stable, 2 for variable.underlyingDebt: IERC20
: The underlying asset of an Aave debt token.underlyingCollateral: IERC20
: The underlying asset of an Aave aToken. No special handling needed for ETH because Aave v2 uses WETH.aTokenAmount: uint256
: The amount ofaToken
to withdraw from Aave V2 and supply to Compound III.
function migrateAaveV2Position(address user, AaveV2Position position) internal
- FOREACH
(aDebtToken, borrowAmount): AaveV2Borrow, swap: Swap
inposition
:- WHEN
borrowAmount == type(uint256).max)
:- BIND READ
repayAmount = aDebtToken.balanceOf(user)
- BIND READ
- ELSE
- BIND
repayAmount = borrowAmount
- BIND
- WHEN
swap.path.length > 0
:- CALL
ISwapRouter.exactOutput(ExactOutputParams({path: swap.path, recipient: address(this), amountOut: repayAmount, amountInMaximum: swap.amountInMaximum})
- CALL
- BIND READ
underlyingDebt = aDebtToken.UNDERLYING_ASSET_ADDRESS()
- BIND READ
rateMode = aDebtToken.DEBT_TOKEN_REVISION()
- CALL
underlyingDebt.approve(address(aaveV2LendingPool), repayAmount)
- CALL
aaveV2LendingPool.repay(underlyingDebt, repayAmount, rateMode, user)
- WHEN
- FOREACH
(aToken, amount): AaveV2Collateral
inposition.collateral
:- BIND
aTokenAmount = amount == type(uint256).max ? aToken.balanceOf(user) : amount)
- CALL
aToken.transferFrom(user, address(this), aTokenAmount)
- BIND READ
underlyingCollateral = aToken.UNDERLYING_ASSET_ADDRESS()
- CALL
aaveV2LendingPool.withdraw(underlyingCollateral, aTokenAmount, address(this))
- CALL
underlyingCollateral.approve(address(comet), aTokenAmount)
- CALL
comet.supplyTo(user, underlyingCollateral, aTokenAmount)
- BIND
This internal helper function repays the user’s borrow positions on Maker (executing swaps first if necessary) before migrating their collateral over to Compound III.
address user
: Alias for themsg.sender
of the originalmigrate
callcdpPositions CDPPosition[]
- List of structures that each represent a single CDP’s collateral and borrow position to migrate to Compound III.
user: address
: Alias formsg.sender
.withdrawAmount: uint256
: The amount of collateral to withdraw.withdrawAmount18: uint256
: The amount of collateral to withdraw, scaled up to 18 decimals.repayAmount: uint256
: The amount to repay for each borrow position.underlyingDebt: IERC20
- The underlying asset of an Aave debt token.underlyingCollateral: IERC20
- The underlying asset of an Aave aToken. No special handling needed for ETH because Aave v2 uses WETH.
function migrateCDPPositions(address user, CDPPosition[] positions) internal
- FOREACH
(cdpId, borrowAmount, collateralAmount, swap, gemJoin): CDPPosition
inpositions
:- WHEN
borrowAmount == type(uint256).max) || collateralAmount == type(uint256).max
:- BIND READ
(withdrawAmount18, repayAmount) = cdpManager.vat().urns(cdpManager.ilks(cdpId), cdpManager.urns(cdpId))
- BIND
withdrawAmount = withdrawAmount18 / (10 ** (18 - gemJoin.dec()))
- BIND READ
- WHEN
borrowAmount != type(uint256).max
- BIND
repayAmount = borrowAmount
- BIND
- WHEN
collateralAmount != type(uint256).max
- BIND
withdrawAmount = collateralAmount
- BIND
withdrawAmount18 = collateralAmount * (10 ** (18 - gemJoin.dec()))
- BIND
- WHEN
swap.path.length > 0
:- CALL
ISwapRouter.exactOutput(ExactOutputParams({path: swap.path, recipient: address(this), amountOut: repayAmount, amountInMaximum: swap.amountInMaximum})
- CALL
- CALL
dai.approve(daiJoin, repayAmount)
- CALL
daiJoin.join(cdpManager.urns(cdpId), repayAmount)
- CALL
cdpManager.frob(cdpId, 0, -repayAmount)
- CALL
cdpManager.frob(cdpId, -withdrawAmount18, 0)
- CALL
cdpManager.flux(cdpId, address(this), withdrawAmount18)
- CALL
gemJoin.exit(address(this), withdrawAmount)
- BIND READ
underlyingCollateral = gemJoin.gem()
- CALL
underlyingCollateral.approve(address(comet), type(uint256).max)
- CALL
comet.supplyTo(user, underlying, underlying.balanceOf(address(this)))
- WHEN
Sends any tokens in this contract to the sweepee address. This contract should never hold tokens, so this is just to fix any anomalistic situations where tokens end up locked in the contract.
token: IERC20
: The token to sweep, or zero to sweep Ether
function sweep(IERC20 token)
- REQUIRE
inMigration == 0
- WHEN
token == 0x0000000000000000000000000000000000000000
:- EXEC
sweepee.send(address(this).balance)
- EMIT
Sweep(msg.sender, sweepee, address(0), address(this).balance)
- EXEC
- ELSE
- CALL
token.transfer(sweepee, token.balanceOf(address(this)))
- EMIT
Sweep(msg.sender, sweepee, address(token), token.balanceOf(address(this)))
- CALL