The updatedAt
field in aggregator2
is being ignored.
If aggregator2
stops updating, stale prices will continue to be used as if they were fresh.
/// @notice Return the latest price combined from two Chainlink like oracle and the timestamp from the first aggregator
function latestRoundData()
public
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
(uint256 value, uint256 timestamp) = getAggregatorData(aggregator, aggregatorScale, aggregatorHeartbeat);
(uint256 value2, ) = getAggregatorData(aggregator2, aggregator2Scale, aggregator2Heartbeat);
uint256 price;
if(isMul) price = wmul(value, value2); else price = wdiv(value, value2);
return (0, int256(price), 0, timestamp, 0);
}
Consider changing to:
/// @notice Return the latest price combined from two Chainlink like oracle and the timestamp from the first aggregator
function latestRoundData()
public
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
(uint256 value1, uint256 updatedAt1) = getAggregatorData(aggregator, aggregatorScale, aggregatorHeartbeat);
(uint256 value2, uint256 updatedAt2) = getAggregatorData(aggregator2, aggregator2Scale, aggregator2Heartbeat);
uint256 price = isMul ? wmul(value1, value2) : wdiv(value1, value2);
uint256 updatedAt = updatedAt1 < updatedAt2 ? updatedAt1 : updatedAt2;
return (0, int256(price), 0, updatedAt, 0);
}
PositionAction.onFlashLoan()
leaves unused collateral
(excess collateral
remains) when CDPVault.tokenScale()
is greater than wad
Since tokens with decimals greater than 18 are quite rare, and the remaining amount in such cases would likely be negligible dust, we consider the impact of this issue to be minimal.
For example, when CDPVault.tokenScale() is and
collateral
is 1234567890123456789012345
onFlashLoan()
ICDPVault(leverParams.vault).modifyCollateralAndDebt()
/// @notice Callback function for the flash loan taken out in increaseLever
/// @param data The encoded bytes that were passed into the flash loan
function onFlashLoan(
address /*initiator*/,
address /*token*/,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
@@ 425,446 @@
if (msg.sender != address(flashlender)) revert PositionAction__onFlashLoan__invalidSender();
(LeverParams memory leverParams, address upFrontToken, uint256 upFrontAmount) = abi.decode(
data,
(LeverParams, address, uint256)
);
// perform a pre swap from arbitrary token to collateral token if necessary
if (leverParams.auxSwap.assetIn != address(0)) {
bytes memory auxSwapData = _delegateCall(
address(swapAction),
abi.encodeWithSelector(swapAction.swap.selector, leverParams.auxSwap)
);
upFrontAmount = abi.decode(auxSwapData, (uint256));
}
// handle the flash loan swap
bytes memory swapData = _delegateCall(
address(swapAction),
abi.encodeWithSelector(swapAction.swap.selector, leverParams.primarySwap)
);
uint256 swapAmountOut = abi.decode(swapData, (uint256));
// deposit collateral and handle any CDP specific actions
uint256 collateral = _onIncreaseLever(leverParams, upFrontToken, upFrontAmount, swapAmountOut);
// derive the amount of normal debt from the swap amount out
uint256 addDebt = amount + fee;
uint256 scaledCollateral = wdiv(collateral, ICDPVault(leverParams.vault).tokenScale());
uint256 scaledDebt = wdiv(addDebt, ICDPVault(leverParams.vault).poolUnderlyingScale());
// add collateral and debt
ICDPVault(leverParams.vault).modifyCollateralAndDebt(
leverParams.position,
address(this),
address(this),
toInt256(scaledCollateral),
toInt256(scaledDebt)
);
underlyingToken.forceApprove(address(flashlender), addDebt);
return CALLBACK_SUCCESS;
}
@@ 406,419 @@
/// @notice Modifies a Position's collateral and debt balances
/// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM,
/// - that the Position is still safe after the modification,
/// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio,
/// - that the msg.sender has the permission of the collateralizer to put up new collateral,
/// - that the msg.sender has the permission of the creditor to settle debt with their credit,
/// - that that the vault debt floor is exceeded
/// - that the vault minimum collateralization ratio is met
/// @param owner Address of the owner of the position
/// @param collateralizer Address of who puts up or receives the collateral delta
/// @param creditor Address of who provides or receives the credit delta for the debt delta
/// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad]
/// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or
/// to settle (-) on this position [wad]
function modifyCollateralAndDebt(
address owner,
address collateralizer,
address creditor,
int256 deltaCollateral,
int256 deltaDebt
) public {
@@ 427,505 @@
if (
// position is either more safe than before or msg.sender has the permission from the owner
((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) ||
// msg.sender has the permission of the collateralizer to collateralize the position using their cash
(deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) ||
// msg.sender has the permission of the creditor to use their credit to repay the debt
(deltaDebt < 0 && !hasPermission(creditor, msg.sender))
) revert CDPVault__modifyCollateralAndDebt_noPermission();
// if the vault is paused allow only debt decreases
if (deltaDebt > 0 || deltaCollateral != 0) {
_requireNotPaused();
}
Position memory position = positions[owner];
DebtData memory debtData = _calcDebt(position);
uint256 newDebt;
uint256 newCumulativeIndex;
uint256 profit;
int256 quotaRevenueChange;
if (deltaDebt > 0) {
uint256 debtToIncrease = uint256(deltaDebt);
// Internal debt calculation remains in 18-decimal precision
(newDebt, newCumulativeIndex) = CreditLogic.calcIncrease(
debtToIncrease,
position.debt,
debtData.cumulativeIndexNow,
position.cumulativeIndexLastUpdate
);
position.cumulativeQuotaInterest = debtData.cumulativeQuotaInterest;
position.cumulativeQuotaIndexLU = debtData.cumulativeQuotaIndexNow;
quotaRevenueChange = _calcQuotaRevenueChange(deltaDebt);
uint256 scaledDebtIncrease = wmul(debtToIncrease, poolUnderlyingScale);
pool.lendCreditAccount(scaledDebtIncrease, creditor);
} else if (deltaDebt < 0) {
uint256 debtToDecrease = abs(deltaDebt);
uint256 maxRepayment = calcTotalDebt(debtData);
if (debtToDecrease >= maxRepayment) {
debtToDecrease = maxRepayment;
deltaDebt = -toInt256(debtToDecrease);
}
uint256 scaledDebtDecrease = wmul(debtToDecrease, poolUnderlyingScale);
poolUnderlying.safeTransferFrom(creditor, address(pool), scaledDebtDecrease);
uint128 newCumulativeQuotaInterest;
if (debtToDecrease == maxRepayment) {
newDebt = 0;
newCumulativeIndex = debtData.cumulativeIndexNow;
profit = debtData.accruedInterest;
newCumulativeQuotaInterest = 0;
} else {
(newDebt, newCumulativeIndex, profit, newCumulativeQuotaInterest) = calcDecrease(
debtToDecrease,
position.debt,
debtData.cumulativeIndexNow,
position.cumulativeIndexLastUpdate,
debtData.cumulativeQuotaInterest
);
}
quotaRevenueChange = _calcQuotaRevenueChange(-int(debtData.debt - newDebt));
uint256 scaledRemainingDebt = wmul(debtData.debt - newDebt, poolUnderlyingScale);
uint256 scaledProfit = wmul(profit, poolUnderlyingScale);
pool.repayCreditAccount(scaledRemainingDebt, scaledProfit, 0);
position.cumulativeQuotaInterest = newCumulativeQuotaInterest;
position.cumulativeQuotaIndexLU = debtData.cumulativeQuotaIndexNow;
} else {
newDebt = position.debt;
newCumulativeIndex = debtData.cumulativeIndexLastUpdate;
}
if (deltaCollateral > 0) {
uint256 amount = wmul(deltaCollateral.toUint256(), tokenScale);
token.safeTransferFrom(collateralizer, address(this), amount);
} else if (deltaCollateral < 0) {
uint256 amount = wmul(abs(deltaCollateral), tokenScale);
token.safeTransfer(collateralizer, amount);
}
@@ 515,530 @@
position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt);
VaultConfig memory config = vaultConfig;
uint256 spotPrice_ = spotPrice();
uint256 collateralValue = wmul(position.collateral, spotPrice_);
if (
(deltaDebt > 0 || deltaCollateral < 0) &&
!_isCollateralized(calcTotalDebt(_calcDebt(position)), collateralValue, config.liquidationRatio)
) revert CDPVault__modifyCollateralAndDebt_notSafe();
if (quotaRevenueChange != 0) {
int256 scaledQuotaRevenueChange = wmul(poolUnderlyingScale, quotaRevenueChange);
IPoolV3(pool).updateQuotaRevenue(scaledQuotaRevenueChange);
}
emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt);
}
/// @notice Callback function for the flash loan taken out in increaseLever
/// @param data The encoded bytes that were passed into the flash loan
function onFlashLoan(
address /*initiator*/,
address /*token*/,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
@@ 425,446 @@
if (msg.sender != address(flashlender)) revert PositionAction__onFlashLoan__invalidSender();
(LeverParams memory leverParams, address upFrontToken, uint256 upFrontAmount) = abi.decode(
data,
(LeverParams, address, uint256)
);
// perform a pre swap from arbitrary token to collateral token if necessary
if (leverParams.auxSwap.assetIn != address(0)) {
bytes memory auxSwapData = _delegateCall(
address(swapAction),
abi.encodeWithSelector(swapAction.swap.selector, leverParams.auxSwap)
);
upFrontAmount = abi.decode(auxSwapData, (uint256));
}
// handle the flash loan swap
bytes memory swapData = _delegateCall(
address(swapAction),
abi.encodeWithSelector(swapAction.swap.selector, leverParams.primarySwap)
);
uint256 swapAmountOut = abi.decode(swapData, (uint256));
// deposit collateral and handle any CDP specific actions
uint256 collateral = _onIncreaseLever(leverParams, upFrontToken, upFrontAmount, swapAmountOut);
// derive the amount of normal debt from the swap amount out
uint256 addDebt = amount + fee;
uint256 scaledCollateral = wdiv(collateral, ICDPVault(leverParams.vault).tokenScale());
uint256 scaledDebt = wdiv(addDebt, ICDPVault(leverParams.vault).poolUnderlyingScale());
// add collateral and debt
ICDPVault(leverParams.vault).modifyCollateralAndDebt(
leverParams.position,
address(this),
address(this),
toInt256(scaledCollateral),
toInt256(scaledDebt)
);
underlyingToken.forceApprove(address(flashlender), addDebt);
return CALLBACK_SUCCESS;
}
@@ 406,419 @@
/// @notice Modifies a Position's collateral and debt balances
/// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM,
/// - that the Position is still safe after the modification,
/// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio,
/// - that the msg.sender has the permission of the collateralizer to put up new collateral,
/// - that the msg.sender has the permission of the creditor to settle debt with their credit,
/// - that that the vault debt floor is exceeded
/// - that the vault minimum collateralization ratio is met
/// @param owner Address of the owner of the position
/// @param collateralizer Address of who puts up or receives the collateral delta
/// @param creditor Address of who provides or receives the credit delta for the debt delta
/// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad]
/// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or
/// to settle (-) on this position [wad]
function modifyCollateralAndDebt(
address owner,
address collateralizer,
address creditor,
int256 deltaCollateral,
int256 deltaDebt
) public {
@@ 427,505 @@
if (
// position is either more safe than before or msg.sender has the permission from the owner
((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) ||
// msg.sender has the permission of the collateralizer to collateralize the position using their cash
(deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) ||
// msg.sender has the permission of the creditor to use their credit to repay the debt
(deltaDebt < 0 && !hasPermission(creditor, msg.sender))
) revert CDPVault__modifyCollateralAndDebt_noPermission();
// if the vault is paused allow only debt decreases
if (deltaDebt > 0 || deltaCollateral != 0) {
_requireNotPaused();
}
Position memory position = positions[owner];
DebtData memory debtData = _calcDebt(position);
uint256 newDebt;
uint256 newCumulativeIndex;
uint256 profit;
int256 quotaRevenueChange;
if (deltaDebt > 0) {
uint256 debtToIncrease = uint256(deltaDebt);
// Internal debt calculation remains in 18-decimal precision
(newDebt, newCumulativeIndex) = CreditLogic.calcIncrease(
debtToIncrease,
position.debt,
debtData.cumulativeIndexNow,
position.cumulativeIndexLastUpdate
);
position.cumulativeQuotaInterest = debtData.cumulativeQuotaInterest;
position.cumulativeQuotaIndexLU = debtData.cumulativeQuotaIndexNow;
quotaRevenueChange = _calcQuotaRevenueChange(deltaDebt);
uint256 scaledDebtIncrease = wmul(debtToIncrease, poolUnderlyingScale);
pool.lendCreditAccount(scaledDebtIncrease, creditor);
} else if (deltaDebt < 0) {
uint256 debtToDecrease = abs(deltaDebt);
uint256 maxRepayment = calcTotalDebt(debtData);
if (debtToDecrease >= maxRepayment) {
debtToDecrease = maxRepayment;
deltaDebt = -toInt256(debtToDecrease);
}
uint256 scaledDebtDecrease = wmul(debtToDecrease, poolUnderlyingScale);
poolUnderlying.safeTransferFrom(creditor, address(pool), scaledDebtDecrease);
uint128 newCumulativeQuotaInterest;
if (debtToDecrease == maxRepayment) {
newDebt = 0;
newCumulativeIndex = debtData.cumulativeIndexNow;
profit = debtData.accruedInterest;
newCumulativeQuotaInterest = 0;
} else {
(newDebt, newCumulativeIndex, profit, newCumulativeQuotaInterest) = calcDecrease(
debtToDecrease,
position.debt,
debtData.cumulativeIndexNow,
position.cumulativeIndexLastUpdate,
debtData.cumulativeQuotaInterest
);
}
quotaRevenueChange = _calcQuotaRevenueChange(-int(debtData.debt - newDebt));
uint256 scaledRemainingDebt = wmul(debtData.debt - newDebt, poolUnderlyingScale);
uint256 scaledProfit = wmul(profit, poolUnderlyingScale);
pool.repayCreditAccount(scaledRemainingDebt, scaledProfit, 0);
position.cumulativeQuotaInterest = newCumulativeQuotaInterest;
position.cumulativeQuotaIndexLU = debtData.cumulativeQuotaIndexNow;
} else {
newDebt = position.debt;
newCumulativeIndex = debtData.cumulativeIndexLastUpdate;
}
if (deltaCollateral > 0) {
uint256 amount = wmul(deltaCollateral.toUint256(), tokenScale);
token.safeTransferFrom(collateralizer, address(this), amount);
} else if (deltaCollateral < 0) {
uint256 amount = wmul(abs(deltaCollateral), tokenScale);
token.safeTransfer(collateralizer, amount);
}
@@ 515,530 @@
position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt);
VaultConfig memory config = vaultConfig;
uint256 spotPrice_ = spotPrice();
uint256 collateralValue = wmul(position.collateral, spotPrice_);
if (
(deltaDebt > 0 || deltaCollateral < 0) &&
!_isCollateralized(calcTotalDebt(_calcDebt(position)), collateralValue, config.liquidationRatio)
) revert CDPVault__modifyCollateralAndDebt_notSafe();
if (quotaRevenueChange != 0) {
int256 scaledQuotaRevenueChange = wmul(poolUnderlyingScale, quotaRevenueChange);
IPoolV3(pool).updateQuotaRevenue(scaledQuotaRevenueChange);
}
emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt);
}
The @return
NatSpec documentation in PositionAction._onIncreaseLever()
was changed from [wad]
to [CDPVault.tokenScale()]
, but the @return
NatSpec documentation in PositionAction4626._onIncreaseLever()
still uses the old version.
/// @notice Hook to increase lever by depositing collateral into the CDPVault, handles any CDP specific actions
/// @param leverParams LeverParams struct
/// @param upFrontToken the token passed up front
/// @param upFrontAmount the amount of `upFrontToken` (or amount received from the aux swap)[CDPVault.tokenScale()]
/// @param swapAmountOut the amount of tokens received from the underlying token flash loan swap [CDPVault.tokenScale()]
/// @return Amount of collateral added to CDPVault [CDPVault.tokenScale()]
function _onIncreaseLever(
LeverParams memory leverParams,
address upFrontToken,
uint256 upFrontAmount,
uint256 swapAmountOut
) internal virtual returns (uint256);
/// @notice Hook to decrease lever by depositing collateral into the Yearn Vault and the Yearn Vault
/// @param leverParams LeverParams struct
/// @param upFrontToken the token passed up front
/// @param upFrontAmount the amount of tokens passed up front [IYVault.decimals()]
/// @param swapAmountOut the amount of tokens received from the stablecoin flash loan swap [IYVault.decimals()]
/// @return Amount of collateral added to CDPVault position [wad]
function _onIncreaseLever(
LeverParams memory leverParams,
address upFrontToken,
uint256 upFrontAmount,
uint256 swapAmountOut
) internal override returns (uint256) {
@@ 97,140 @@
uint256 upFrontCollateral;
uint256 addCollateralAmount = swapAmountOut;
if (leverParams.collateralToken == upFrontToken && leverParams.auxSwap.assetIn == address(0)) {
// if there was no aux swap then treat this amount as the ERC4626 token
upFrontCollateral = upFrontAmount;
} else {
// otherwise treat as the ERC4626 underlying
addCollateralAmount += upFrontAmount;
}
address underlyingToken = IERC4626(leverParams.collateralToken).asset();
// join into the pool if needed
if (leverParams.auxAction.args.length != 0) {
address joinToken = swapAction.getSwapToken(leverParams.primarySwap);
address joinUpfrontToken = upFrontToken;
if (leverParams.auxSwap.assetIn != address(0)) {
joinUpfrontToken = swapAction.getSwapToken(leverParams.auxSwap);
}
// update the join parameters with the new amounts
PoolActionParams memory poolActionParams = poolAction.updateLeverJoin(
leverParams.auxAction,
joinToken,
joinUpfrontToken,
swapAmountOut,
upFrontAmount
);
_delegateCall(address(poolAction), abi.encodeWithSelector(poolAction.join.selector, poolActionParams));
// retrieve the total amount of collateral after the join
addCollateralAmount = IERC20(underlyingToken).balanceOf(address(this));
}
// deposit into the ERC4626 vault
IERC20(underlyingToken).forceApprove(leverParams.collateralToken, addCollateralAmount);
addCollateralAmount =
IERC4626(leverParams.collateralToken).deposit(addCollateralAmount, address(this)) +
upFrontCollateral;
// deposit into the CDP vault
IERC20(leverParams.collateralToken).forceApprove(leverParams.vault, addCollateralAmount);
return addCollateralAmount;
}
/// @notice Hook to decrease lever by depositing collateral into the Yearn Vault and the Yearn Vault
/// @param leverParams LeverParams struct
/// @param upFrontToken the token passed up front
/// @param upFrontAmount the amount of tokens passed up front [IYVault.decimals()]
/// @param swapAmountOut the amount of tokens received from the stablecoin flash loan swap [IYVault.decimals()]
/// @return Amount of collateral added to CDPVault position [wad]
function _onIncreaseLever(
LeverParams memory leverParams,
address upFrontToken,
uint256 upFrontAmount,
uint256 swapAmountOut
) internal override returns (uint256) {
@@ 97,140 @@
uint256 upFrontCollateral;
uint256 addCollateralAmount = swapAmountOut;
if (leverParams.collateralToken == upFrontToken && leverParams.auxSwap.assetIn == address(0)) {
// if there was no aux swap then treat this amount as the ERC4626 token
upFrontCollateral = upFrontAmount;
} else {
// otherwise treat as the ERC4626 underlying
addCollateralAmount += upFrontAmount;
}
address underlyingToken = IERC4626(leverParams.collateralToken).asset();
// join into the pool if needed
if (leverParams.auxAction.args.length != 0) {
address joinToken = swapAction.getSwapToken(leverParams.primarySwap);
address joinUpfrontToken = upFrontToken;
if (leverParams.auxSwap.assetIn != address(0)) {
joinUpfrontToken = swapAction.getSwapToken(leverParams.auxSwap);
}
// update the join parameters with the new amounts
PoolActionParams memory poolActionParams = poolAction.updateLeverJoin(
leverParams.auxAction,
joinToken,
joinUpfrontToken,
swapAmountOut,
upFrontAmount
);
_delegateCall(address(poolAction), abi.encodeWithSelector(poolAction.join.selector, poolActionParams));
// retrieve the total amount of collateral after the join
addCollateralAmount = IERC20(underlyingToken).balanceOf(address(this));
}
// deposit into the ERC4626 vault
IERC20(underlyingToken).forceApprove(leverParams.collateralToken, addCollateralAmount);
addCollateralAmount =
IERC4626(leverParams.collateralToken).deposit(addCollateralAmount, address(this)) +
upFrontCollateral;
// deposit into the CDP vault
IERC20(leverParams.collateralToken).forceApprove(leverParams.vault, addCollateralAmount);
return addCollateralAmount;
}
_onWithdraw(..., address dst,...)
when dst
is address(0)
leads to inconsistent handling across different implementationsPositionAction4626._onWithdraw()
, when dst == address(0)
, it's treated as withdrawing the underlying asset of ICDPVault(vault).token()PositionActionPendle._onWithdraw()
and PositionActionPenpie._onWithdraw()
, when dst == address(0)
, it's treated as withdrawing ICDPVault(vault).token() itself@@ 19,20 @@
/// @notice Struct containing parameters used for adding or removing a position's collateral
/// and optionally swapping an arbitrary token to the collateral token
struct CollateralParams {
// token passed in or received by the caller
address targetToken;
@@ 24,31 @@
// amount of collateral to add in CDPVault.tokenScale() or to remove in WAD
uint256 amount;
// address that will transfer the collateral or receive the collateral
address collateralizer;
// optional swap from `targetToken` to collateral, or collateral to `targetToken`
SwapParams auxSwap;
// minimum amount out for the aux swap
uint256 minAmountOut;
}
@@ 232,234 @@
/// @notice Removes collateral from a CDP Vault
/// @param position The CDP Vault position
/// @param vault The CDP Vault
/// @param collateralParams The collateral parameters
function withdraw(
address position,
address vault,
CollateralParams calldata collateralParams
) external onlyRegisteredVault(vault) onlyDelegatecall {
_withdraw(vault, position, collateralParams);
}
/// @notice Withdraws collateral from CDPVault (optionally swaps collateral to an arbitrary token)
/// @param vault The CDP Vault
/// @param collateralParams The collateral parameters
/// @return The amount of collateral withdrawn [token.decimals()]
function _withdraw(
address vault,
address position,
CollateralParams calldata collateralParams
) internal returns (uint256) {
uint256 withdrawnCollateral = _onWithdraw(vault, position, collateralParams.targetToken, collateralParams.amount, collateralParams.minAmountOut);
@@ 629,644 @@
// perform swap from collateral to arbitrary token
if (collateralParams.auxSwap.assetIn != address(0)) {
SwapParams memory auxSwap = collateralParams.auxSwap;
if (auxSwap.swapType == SwapType.EXACT_IN) {
auxSwap.amount = withdrawnCollateral;
}
_delegateCall(
address(swapAction),
abi.encodeWithSelector(swapAction.swap.selector, auxSwap)
);
} else {
// otherwise just send the collateral to `collateralizer`
IERC20(collateralParams.targetToken).safeTransfer(collateralParams.collateralizer, withdrawnCollateral);
}
return withdrawnCollateral;
}
/// @notice Hook to withdraw collateral from CDPVault, handles any CDP specific actions
/// @param vault The CDP Vault
/// @param position The CDP Vault position
/// @param dst Token the caller expects to receive
/// @param amount The amount of collateral to deposit [wad]
/// @param minAmountOut The minimum amount out for the aux swap
/// @return Amount of collateral (or dst) withdrawn [CDPVault.tokenScale()]
function _onWithdraw(
address vault,
address position,
address dst,
uint256 amount,
uint256 minAmountOut
) internal virtual returns (uint256);
/// @notice Withdraw collateral from the vault
/// @param vault Address of the vault
/// @param position Address of the position
/// @param dst Token the caller expects to receive
/// @param amount Amount of collateral to withdraw [wad]
/// @param /*minAmountOut*/ The minimum amount out for the aux swap
/// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
function _onWithdraw(
address vault,
address position,
address dst,
uint256 amount,
uint256 /*minAmountOut*/
) internal override returns (uint256) {
uint256 scaledCollateralWithdrawn = ICDPVault(vault).withdraw(position, amount);
uint256 collateralWithdrawn = wmul(scaledCollateralWithdrawn, ICDPVault(vault).tokenScale());
// if collateral is not the dst token, we need to withdraw the underlying from the ERC4626 vault
address collateral = address(ICDPVault(vault).token());
if (dst == collateral) {
return collateralWithdrawn;
} else {
return IERC4626(collateral).redeem(collateralWithdrawn, address(this), address(this));
}
}
/// @notice Withdraw collateral from the vault
/// @param vault Address of the vault
/// @param amount Amount of collateral to withdraw [wad]
/// @param minAmountOut The minimum amount out for the aux swap
/// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
function _onWithdraw(
address vault,
address position,
address dst,
uint256 amount,
uint256 minAmountOut
) internal override returns (uint256) {
uint256 scaledCollateralWithdrawn = ICDPVault(vault).withdraw(address(position), amount);
uint256 collateralWithdrawn = wmul(scaledCollateralWithdrawn, ICDPVault(vault).tokenScale());
address collateralToken = address(ICDPVault(vault).token());
if (dst != collateralToken && dst != address(0)) {
PoolActionParams memory poolActionParams = PoolActionParams({
protocol: Protocol.PENDLE,
minOut: minAmountOut,
recipient: address(this),
args: abi.encode(
collateralToken,
collateralWithdrawn,
dst
)
});
bytes memory exitData = _delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, poolActionParams)
);
collateralWithdrawn = abi.decode(exitData, (uint256));
}
return collateralWithdrawn;
}
/// @notice Withdraw collateral from the vault
/// @param vault Address of the vault
/// @param position Address of the position
/// @param dst Token the caller expects to receive
/// @param amount Amount of collateral to withdraw [wad]
/// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
function _onWithdraw(
address vault,
address position,
address dst,
uint256 amount,
uint256 /*minAmountOut*/
) internal override returns (uint256) {
uint256 scaledCollateralWithdrawn = ICDPVault(vault).withdraw(address(position), amount);
uint256 collateralWithdrawn = wmul(scaledCollateralWithdrawn, ICDPVault(vault).tokenScale());
address collateralToken = address(ICDPVault(vault).token());
if (dst != collateralToken && dst != address(0)) {
penpieHelper.withdrawMarket(dst, collateralWithdrawn);
}
return collateralWithdrawn;
}
@@ 19,20 @@
/// @notice Struct containing parameters used for adding or removing a position's collateral
/// and optionally swapping an arbitrary token to the collateral token
struct CollateralParams {
// token passed in or received by the caller
address targetToken;
@@ 24,31 @@
// amount of collateral to add in CDPVault.tokenScale() or to remove in WAD
uint256 amount;
// address that will transfer the collateral or receive the collateral
address collateralizer;
// optional swap from `targetToken` to collateral, or collateral to `targetToken`
SwapParams auxSwap;
// minimum amount out for the aux swap
uint256 minAmountOut;
}
@@ 232,234 @@
/// @notice Removes collateral from a CDP Vault
/// @param position The CDP Vault position
/// @param vault The CDP Vault
/// @param collateralParams The collateral parameters
function withdraw(
address position,
address vault,
CollateralParams calldata collateralParams
) external onlyRegisteredVault(vault) onlyDelegatecall {
_withdraw(vault, position, collateralParams);
}
/// @notice Withdraws collateral from CDPVault (optionally swaps collateral to an arbitrary token)
/// @param vault The CDP Vault
/// @param collateralParams The collateral parameters
/// @return The amount of collateral withdrawn [token.decimals()]
function _withdraw(
address vault,
address position,
CollateralParams calldata collateralParams
) internal returns (uint256) {
uint256 withdrawnCollateral = _onWithdraw(vault, position, collateralParams.targetToken, collateralParams.amount, collateralParams.minAmountOut);
@@ 629,644 @@
// perform swap from collateral to arbitrary token
if (collateralParams.auxSwap.assetIn != address(0)) {
SwapParams memory auxSwap = collateralParams.auxSwap;
if (auxSwap.swapType == SwapType.EXACT_IN) {
auxSwap.amount = withdrawnCollateral;
}
_delegateCall(
address(swapAction),
abi.encodeWithSelector(swapAction.swap.selector, auxSwap)
);
} else {
// otherwise just send the collateral to `collateralizer`
IERC20(collateralParams.targetToken).safeTransfer(collateralParams.collateralizer, withdrawnCollateral);
}
return withdrawnCollateral;
}
/// @notice Hook to withdraw collateral from CDPVault, handles any CDP specific actions
/// @param vault The CDP Vault
/// @param position The CDP Vault position
/// @param dst Token the caller expects to receive
/// @param amount The amount of collateral to deposit [wad]
/// @param minAmountOut The minimum amount out for the aux swap
/// @return Amount of collateral (or dst) withdrawn [CDPVault.tokenScale()]
function _onWithdraw(
address vault,
address position,
address dst,
uint256 amount,
uint256 minAmountOut
) internal virtual returns (uint256);
collateral
must implement standard IERC4626.redeem()
IERC4626(collateral).redeem()
for ERC4626 that requires a redemption request before the actual redemption.IERC4626(collateral).redeem()
for ERC4626 where the returned amount does not match the actual asset amount received by the receiver. /// @notice Withdraw collateral from the vault
/// @param vault Address of the vault
/// @param position Address of the position
/// @param dst Token the caller expects to receive
/// @param amount Amount of collateral to withdraw [wad]
/// @param /*minAmountOut*/ The minimum amount out for the aux swap
/// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
function _onWithdraw(
address vault,
address position,
address dst,
uint256 amount,
uint256 /*minAmountOut*/
) internal override returns (uint256) {
uint256 scaledCollateralWithdrawn = ICDPVault(vault).withdraw(position, amount);
uint256 collateralWithdrawn = wmul(scaledCollateralWithdrawn, ICDPVault(vault).tokenScale());
// if collateral is not the dst token, we need to withdraw the underlying from the ERC4626 vault
address collateral = address(ICDPVault(vault).token());
if (dst == collateral) {
return collateralWithdrawn;
} else {
return IERC4626(collateral).redeem(collateralWithdrawn, address(this), address(this));
}
}
/// @notice Hook to decrease lever by withdrawing collateral from the CDPVault and the ERC4626 Vault
/// @param leverParams LeverParams struct
/// @param subCollateral Amount of collateral to withdraw in CDPVault decimals [wad]
/// @return tokenOut Amount of underlying token withdrawn from the ERC4626 vault [10 ** IERC4626(collateralToken).asset().decimals()]
function _onDecreaseLever(
LeverParams memory leverParams,
uint256 subCollateral
) internal override returns (uint256 tokenOut) {
// withdraw collateral from vault
uint256 scaledWithdrawnCollateral = ICDPVault(leverParams.vault).withdraw(leverParams.position, subCollateral);
uint256 withdrawnCollateral = wmul(scaledWithdrawnCollateral, ICDPVault(leverParams.vault).tokenScale());
// withdraw collateral from the ERC4626 vault and return underlying assets
tokenOut = IERC4626(leverParams.collateralToken).redeem(withdrawnCollateral, address(this), address(this));
if (leverParams.auxAction.args.length != 0) {
_delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
);
tokenOut = IERC20(IERC4626(leverParams.collateralToken).asset()).balanceOf(address(this));
}
}
/// @notice Withdraw collateral from the vault
/// @param vault Address of the vault
/// @param position Address of the position
/// @param dst Token the caller expects to receive
/// @param amount Amount of collateral to withdraw [wad]
/// @param /*minAmountOut*/ The minimum amount out for the aux swap
/// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
function _onWithdraw(
address vault,
address position,
address dst,
uint256 amount,
uint256 /*minAmountOut*/
) internal override returns (uint256) {
uint256 scaledCollateralWithdrawn = ICDPVault(vault).withdraw(position, amount);
uint256 collateralWithdrawn = wmul(scaledCollateralWithdrawn, ICDPVault(vault).tokenScale());
// if collateral is not the dst token, we need to withdraw the underlying from the ERC4626 vault
address collateral = address(ICDPVault(vault).token());
if (dst == collateral) {
return collateralWithdrawn;
} else {
return IERC4626(collateral).redeem(collateralWithdrawn, address(this), address(this));
}
}
/// @notice Hook to decrease lever by withdrawing collateral from the CDPVault and the ERC4626 Vault
/// @param leverParams LeverParams struct
/// @param subCollateral Amount of collateral to withdraw in CDPVault decimals [wad]
/// @return tokenOut Amount of underlying token withdrawn from the ERC4626 vault [10 ** IERC4626(collateralToken).asset().decimals()]
function _onDecreaseLever(
LeverParams memory leverParams,
uint256 subCollateral
) internal override returns (uint256 tokenOut) {
// withdraw collateral from vault
uint256 scaledWithdrawnCollateral = ICDPVault(leverParams.vault).withdraw(leverParams.position, subCollateral);
uint256 withdrawnCollateral = wmul(scaledWithdrawnCollateral, ICDPVault(leverParams.vault).tokenScale());
// withdraw collateral from the ERC4626 vault and return underlying assets
tokenOut = IERC4626(leverParams.collateralToken).redeem(withdrawnCollateral, address(this), address(this));
if (leverParams.auxAction.args.length != 0) {
_delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
);
tokenOut = IERC20(IERC4626(leverParams.collateralToken).asset()).balanceOf(address(this));
}
}
PositionActionPenpie._onDecreaseLever()
L132 has redundant non-empty check for leverParams.auxAction.args
, since it was already validated to be non-empty at L130 /// @notice Hook to decrease lever by withdrawing collateral from the CDPVault
/// @param leverParams LeverParams struct
/// @param subCollateral Amount of collateral to subtract in CDPVault decimals [wad]
/// @return tokenOut Amount of underlying token of Pendle SY [underlying scale]
function _onDecreaseLever(
LeverParams memory leverParams,
uint256 subCollateral
) internal override returns (uint256 tokenOut) {
(address pendleToken, , ) = abi.decode(leverParams.auxAction.args, (address, uint256, address));
_onWithdraw(leverParams.vault, leverParams.position, pendleToken, subCollateral, 0);
if (leverParams.auxAction.args.length != 0) {
bytes memory exitData = _delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
);
tokenOut = abi.decode(exitData, (uint256));
}
}
poolAction.exit()
, PositionAction4626._onDecreaseLever()
does not return the amount of underlying tokens received from exiting the position like other _onDecreaseLever()
implementations do.Instead, it returns the remaining IERC4626(leverParams.collateralToken).asset()
(which could be LP) amount (likely to be 0).
/// @notice Hook to decrease lever by withdrawing collateral from the CDPVault and the ERC4626 Vault
/// @param leverParams LeverParams struct
/// @param subCollateral Amount of collateral to withdraw in CDPVault decimals [wad]
/// @return tokenOut Amount of underlying token withdrawn from the ERC4626 vault [10 ** IERC4626(collateralToken).asset().decimals()]
function _onDecreaseLever(
LeverParams memory leverParams,
uint256 subCollateral
) internal override returns (uint256 tokenOut) {
// withdraw collateral from vault
uint256 scaledWithdrawnCollateral = ICDPVault(leverParams.vault).withdraw(leverParams.position, subCollateral);
uint256 withdrawnCollateral = wmul(scaledWithdrawnCollateral, ICDPVault(leverParams.vault).tokenScale());
// withdraw collateral from the ERC4626 vault and return underlying assets
tokenOut = IERC4626(leverParams.collateralToken).redeem(withdrawnCollateral, address(this), address(this));
if (leverParams.auxAction.args.length != 0) {
_delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
);
tokenOut = IERC20(IERC4626(leverParams.collateralToken).asset()).balanceOf(address(this));
}
}
/// @notice Exit a protocol specific pool
/// @param poolActionParams The parameters for the exit
function exit(PoolActionParams memory poolActionParams) public returns (uint256 retAmount) {
if (poolActionParams.protocol == Protocol.BALANCER) {
retAmount = _balancerExit(poolActionParams);
} else if (poolActionParams.protocol == Protocol.PENDLE) {
retAmount = _pendleExit(poolActionParams);
} else if (poolActionParams.protocol == Protocol.TRANCHESS) {
retAmount = _tranchessExit(poolActionParams);
} else if (poolActionParams.protocol == Protocol.SPECTRA) {
retAmount = _spectraExit(poolActionParams);
} else revert PoolAction__exit_unsupportedProtocol();
}
@@ 344,430 @@
function _balancerExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(
bytes32 poolId,
address bpt,
uint256 bptAmount,
uint256 outIndex,
address[] memory assets,
uint256[] memory minAmountsOut
) = abi.decode(poolActionParams.args, (bytes32, address, uint256, uint256, address[], uint256[]));
if (bptAmount != 0) IERC20(bpt).forceApprove(address(balancerVault), bptAmount);
uint256 tmpOutIndex = outIndex;
for (uint256 i = 0; i <= tmpOutIndex; i++) if (assets[i] == bpt) tmpOutIndex++;
uint256 balanceBefore = IERC20(assets[tmpOutIndex]).balanceOf(poolActionParams.recipient);
balancerVault.exitPool(
poolId,
address(this),
payable(poolActionParams.recipient),
ExitPoolRequest({
assets: assets,
minAmountsOut: minAmountsOut,
userData: abi.encode(ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, bptAmount, outIndex),
toInternalBalance: false
})
);
return IERC20(assets[tmpOutIndex]).balanceOf(poolActionParams.recipient) - balanceBefore;
}
function _pendleExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(address market, uint256 netLpIn, address tokenOut) = abi.decode(
poolActionParams.args,
(address, uint256, address)
);
(IStandardizedYield SY, IPPrincipalToken PT, IPYieldToken YT) = IPMarket(market).readTokens();
if (poolActionParams.recipient != address(this)) {
IPMarket(market).transferFrom(poolActionParams.recipient, market, netLpIn);
} else {
IPMarket(market).transfer(market, netLpIn);
}
uint256 netSyToRedeem;
if (PT.isExpired()) {
(uint256 netSyRemoved, ) = IPMarket(market).burn(address(SY), address(YT), netLpIn);
uint256 netSyFromPt = YT.redeemPY(address(SY));
netSyToRedeem = netSyRemoved + netSyFromPt;
} else {
(uint256 netSyRemoved, uint256 netPtRemoved) = IPMarket(market).burn(address(SY), market, netLpIn);
bytes memory empty;
(uint256 netSySwappedOut, ) = IPMarket(market).swapExactPtForSy(address(SY), netPtRemoved, empty);
netSyToRedeem = netSyRemoved + netSySwappedOut;
}
return SY.redeem(poolActionParams.recipient, netSyToRedeem, tokenOut, poolActionParams.minOut, true);
}
function _tranchessExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(uint256 version, address lpToken, uint256 lpIn) = abi.decode(
poolActionParams.args,
(uint256, address, uint256)
);
IStableSwap stableSwap = IStableSwap(ILiquidityGauge(lpToken).stableSwap());
retAmount = stableSwap.removeQuoteLiquidity(version, lpIn, poolActionParams.minOut);
if (poolActionParams.recipient != address(this)) {
IERC20(stableSwap.quoteAddress()).safeTransfer(poolActionParams.recipient, retAmount);
}
}
function _spectraExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(bytes memory commands, bytes[] memory inputs, address tokenOut, uint256 deadline) = abi.decode(
poolActionParams.args,
(bytes, bytes[], address, uint256)
);
(address tokenIn, uint256 amountIn) = abi.decode(inputs[0], (address, uint256));
uint256 balBefore = IERC20(tokenOut).balanceOf(address(this));
IERC20(tokenIn).forceApprove(address(spectraRouter), amountIn);
spectraRouter.execute(commands, inputs, deadline);
retAmount = IERC20(tokenOut).balanceOf(address(this)) - balBefore;
}
As a reference for comparison, in PositionActionPenpie._onDecreaseLever()
and similar functions, the exitData
returned by poolAction.exit()
is used:
/// @notice Hook to decrease lever by withdrawing collateral from the CDPVault
/// @param leverParams LeverParams struct
/// @param subCollateral Amount of collateral to subtract in CDPVault decimals [wad]
/// @return tokenOut Amount of underlying token of Pendle SY [underlying scale]
function _onDecreaseLever(
LeverParams memory leverParams,
uint256 subCollateral
) internal override returns (uint256 tokenOut) {
(address pendleToken, , ) = abi.decode(leverParams.auxAction.args, (address, uint256, address));
_onWithdraw(leverParams.vault, leverParams.position, pendleToken, subCollateral, 0);
if (leverParams.auxAction.args.length != 0) {
bytes memory exitData = _delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
);
tokenOut = abi.decode(exitData, (uint256));
}
}
Change to:
bytes memory exitData = _delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
);
tokenOut = abi.decode(exitData, (uint256));
The corresponding comment at L146 should be updated accordingly.
/// @notice Hook to decrease lever by withdrawing collateral from the CDPVault and the ERC4626 Vault
/// @param leverParams LeverParams struct
/// @param subCollateral Amount of collateral to withdraw in CDPVault decimals [wad]
/// @return tokenOut Amount of underlying token withdrawn from the ERC4626 vault [10 ** IERC4626(collateralToken).asset().decimals()]
function _onDecreaseLever(
LeverParams memory leverParams,
uint256 subCollateral
) internal override returns (uint256 tokenOut) {
// withdraw collateral from vault
uint256 scaledWithdrawnCollateral = ICDPVault(leverParams.vault).withdraw(leverParams.position, subCollateral);
uint256 withdrawnCollateral = wmul(scaledWithdrawnCollateral, ICDPVault(leverParams.vault).tokenScale());
// withdraw collateral from the ERC4626 vault and return underlying assets
tokenOut = IERC4626(leverParams.collateralToken).redeem(withdrawnCollateral, address(this), address(this));
if (leverParams.auxAction.args.length != 0) {
_delegateCall(
address(poolAction),
abi.encodeWithSelector(poolAction.exit.selector, leverParams.auxAction)
);
tokenOut = IERC20(IERC4626(leverParams.collateralToken).asset()).balanceOf(address(this));
}
}
/// @notice Exit a protocol specific pool
/// @param poolActionParams The parameters for the exit
function exit(PoolActionParams memory poolActionParams) public returns (uint256 retAmount) {
if (poolActionParams.protocol == Protocol.BALANCER) {
retAmount = _balancerExit(poolActionParams);
} else if (poolActionParams.protocol == Protocol.PENDLE) {
retAmount = _pendleExit(poolActionParams);
} else if (poolActionParams.protocol == Protocol.TRANCHESS) {
retAmount = _tranchessExit(poolActionParams);
} else if (poolActionParams.protocol == Protocol.SPECTRA) {
retAmount = _spectraExit(poolActionParams);
} else revert PoolAction__exit_unsupportedProtocol();
}
@@ 344,430 @@
function _balancerExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(
bytes32 poolId,
address bpt,
uint256 bptAmount,
uint256 outIndex,
address[] memory assets,
uint256[] memory minAmountsOut
) = abi.decode(poolActionParams.args, (bytes32, address, uint256, uint256, address[], uint256[]));
if (bptAmount != 0) IERC20(bpt).forceApprove(address(balancerVault), bptAmount);
uint256 tmpOutIndex = outIndex;
for (uint256 i = 0; i <= tmpOutIndex; i++) if (assets[i] == bpt) tmpOutIndex++;
uint256 balanceBefore = IERC20(assets[tmpOutIndex]).balanceOf(poolActionParams.recipient);
balancerVault.exitPool(
poolId,
address(this),
payable(poolActionParams.recipient),
ExitPoolRequest({
assets: assets,
minAmountsOut: minAmountsOut,
userData: abi.encode(ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, bptAmount, outIndex),
toInternalBalance: false
})
);
return IERC20(assets[tmpOutIndex]).balanceOf(poolActionParams.recipient) - balanceBefore;
}
function _pendleExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(address market, uint256 netLpIn, address tokenOut) = abi.decode(
poolActionParams.args,
(address, uint256, address)
);
(IStandardizedYield SY, IPPrincipalToken PT, IPYieldToken YT) = IPMarket(market).readTokens();
if (poolActionParams.recipient != address(this)) {
IPMarket(market).transferFrom(poolActionParams.recipient, market, netLpIn);
} else {
IPMarket(market).transfer(market, netLpIn);
}
uint256 netSyToRedeem;
if (PT.isExpired()) {
(uint256 netSyRemoved, ) = IPMarket(market).burn(address(SY), address(YT), netLpIn);
uint256 netSyFromPt = YT.redeemPY(address(SY));
netSyToRedeem = netSyRemoved + netSyFromPt;
} else {
(uint256 netSyRemoved, uint256 netPtRemoved) = IPMarket(market).burn(address(SY), market, netLpIn);
bytes memory empty;
(uint256 netSySwappedOut, ) = IPMarket(market).swapExactPtForSy(address(SY), netPtRemoved, empty);
netSyToRedeem = netSyRemoved + netSySwappedOut;
}
return SY.redeem(poolActionParams.recipient, netSyToRedeem, tokenOut, poolActionParams.minOut, true);
}
function _tranchessExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(uint256 version, address lpToken, uint256 lpIn) = abi.decode(
poolActionParams.args,
(uint256, address, uint256)
);
IStableSwap stableSwap = IStableSwap(ILiquidityGauge(lpToken).stableSwap());
retAmount = stableSwap.removeQuoteLiquidity(version, lpIn, poolActionParams.minOut);
if (poolActionParams.recipient != address(this)) {
IERC20(stableSwap.quoteAddress()).safeTransfer(poolActionParams.recipient, retAmount);
}
}
function _spectraExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
(bytes memory commands, bytes[] memory inputs, address tokenOut, uint256 deadline) = abi.decode(
poolActionParams.args,
(bytes, bytes[], address, uint256)
);
(address tokenIn, uint256 amountIn) = abi.decode(inputs[0], (address, uint256));
uint256 balBefore = IERC20(tokenOut).balanceOf(address(this));
IERC20(tokenIn).forceApprove(address(spectraRouter), amountIn);
spectraRouter.execute(commands, inputs, deadline);
retAmount = IERC20(tokenOut).balanceOf(address(this)) - balBefore;
}