The migration tool was not audited by a third-party. the migrator itself is essentially 1 function that follows a very narrow code path, and does not maintain any state. it is essentially bundling all the necessary actions to move a position, but doing it with special privileges so that you don't need to manually free up funds to repay your debt.
Below is the migration code.
function migrateVaults(
address startingYieldToken,
address targetYieldToken,
uint256 shares,
uint256 minReturnShares,
uint256 minReturnUnderlying
) external override returns (uint256) {
// Yield tokens cannot be the same to prevent slippage on current position
if (startingYieldToken == targetYieldToken) {
revert IllegalArgument("Yield tokens cannot be the same");
}
// If either yield token is invalid, revert
if (!alchemist.isSupportedYieldToken(startingYieldToken)) {
revert IllegalArgument("Yield token is not supported");
}
if (!alchemist.isSupportedYieldToken(targetYieldToken)) {
revert IllegalArgument("Yield token is not supported");
}
IAlchemistV2State.YieldTokenParams memory startingParams = alchemist.getYieldTokenParameters(startingYieldToken);
IAlchemistV2State.YieldTokenParams memory targetParams = alchemist.getYieldTokenParameters(targetYieldToken);
// If starting and target underlying tokens are not the same then revert
if (startingParams.underlyingToken != targetParams.underlyingToken) {
revert IllegalArgument("Cannot swap between different collaterals");
}
// Original debt
(int256 debt, ) = alchemist.accounts(msg.sender);
// Avoid calculations and repayments if user doesn't need this to migrate
uint256 debtTokenValue;
uint256 mintable;
if (debt > 0) {
// Convert shares to amount of debt tokens
debtTokenValue = _convertToDebt(shares, startingYieldToken, startingParams.underlyingToken);
mintable = debtTokenValue * FIXED_POINT_SCALAR / alchemist.minimumCollateralization();
// Mint tokens to this contract and burn them in the name of the user
alchemicToken.mint(address(this), mintable);
TokenUtils.safeApprove(address(alchemicToken), address(alchemist), mintable);
alchemist.burn(mintable, msg.sender);
}
// Withdraw what you can from the old position
uint256 underlyingWithdrawn = alchemist.withdrawUnderlyingFrom(msg.sender, startingYieldToken, shares, address(this), minReturnUnderlying);
// Deposit into new position
TokenUtils.safeApprove(targetParams.underlyingToken, address(alchemist), underlyingWithdrawn);
uint256 newPositionShares = alchemist.depositUnderlying(targetYieldToken, underlyingWithdrawn, msg.sender, minReturnShares);
if (debt > 0) {
// Mint al token which will be burned to fulfill flash loan requirements
alchemist.mintFrom(msg.sender, mintable, address(this));
alchemicToken.burn(mintable);
}
return newPositionShares;
}
As you can see, the codepath is very narrow, with only 1 major branch (outside of the initial checks before actually running the logic). We are not moving any raw ETH, and we're not sending any tokens with callbacks, so there are no re-entrancy attack vectors.