Generic Position Swapper#13
Conversation
3c4004b to
989dea3
Compare
| /// @notice Emitted when an approved pair is updated. | ||
| event ApprovedPairUpdated(address marketFrom, address marketTo, address helper, bool oldStatus, bool newStatus); |
There was a problem hiding this comment.
This event is no longer needed.
There was a problem hiding this comment.
Not used anywhere in the contract.
There was a problem hiding this comment.
Same for BorrowFailed, NoVTokenBalance, NoUnderlyingReceived and AccrueInterestFailed.
| /// @custom:error NoEnoughBalance | ||
| error NoEnoughBalance(); |
There was a problem hiding this comment.
| /// @custom:error NoEnoughBalance | |
| error NoEnoughBalance(); | |
| /// @custom:error NotEnoughBalance | |
| error NotEnoughBalance(); |
| if (address(_comptroller) == address(0)) revert ZeroAddress(); | ||
| if (address(_swapHelper) == address(0)) revert ZeroAddress(); | ||
| if (address(_wrappedNative) == address(0)) revert ZeroAddress(); | ||
| if (address(_nativeVToken) == address(0)) revert ZeroAddress(); |
There was a problem hiding this comment.
We can use ensureNonzeroAddress from "@venusprotocol/solidity-utilities/contracts/validators.sol".
| /** | ||
| * @notice Swap user's entire native collateral (e.g., vBNB) into wrapped collateral (e.g., vWBNB). | ||
| * @param user Address of the user. | ||
| */ |
There was a problem hiding this comment.
Please include events and errors in the natspec too.
| * @param user Address of the user. | ||
| */ |
There was a problem hiding this comment.
Please include events and errors in the natspec too.
| function _validateAndEnterMarket(address user, IVToken marketFrom, IVToken marketTo) internal { | ||
| if (COMPTROLLER.checkMembership(user, marketFrom) && !COMPTROLLER.checkMembership(user, marketTo)) { |
There was a problem hiding this comment.
We should create Natspec comments for all the internal functions to ensure consistency.
| * @notice Flash loan callback entrypoint called by Comptroller. | ||
| * @param vTokens Array with the borrowed vToken market (single element) | ||
| * @param amounts Array with the borrowed underlying amount (single element) | ||
| * @param premiums Array with the flash loan fee amount (single element) |
There was a problem hiding this comment.
| * @param premiums Array with the flash loan fee amount (single element) | |
| * @param premiums Array with the flash loan fee amount (single element) | |
| * @param /*initiator The address that initiated the flash loan (unused) |
| ) internal returns (uint256 borrowedAssetAmountToRepay) { | ||
| uint256 err; | ||
| // For vBNB to vWBNB we take flashLoan of WBNB as vBNB does not support flashLoan | ||
| if (borrowMarket != WRAPPED_NATIVE_MARKET) revert InvalidFlashLoanBorrowedAsset(); |
There was a problem hiding this comment.
IMO, this validation is unnecessary since we are hardcoding the value in the internal function.
| // supply new collateral | ||
| IERC20Upgradeable(address(WRAPPED_NATIVE)).forceApprove( | ||
| address(transientMarketTo), | ||
| borrowedAssetAmount - borrowedAssetFees |
There was a problem hiding this comment.
Should we store it in a variable mintAmount = borrowedAssetAmount - borrowedAssetFee?
| if (nativeBalanceAfter < borrowedAssetAmount) { | ||
| revert InsufficientFundsToRepayFlashloan(); | ||
| } |
There was a problem hiding this comment.
| if (nativeBalanceAfter < borrowedAssetAmount) { | |
| revert InsufficientFundsToRepayFlashloan(); | |
| } | |
| if (nativeBalanceAfter < borrowedAssetAmountToRepay) { | |
| revert InsufficientFundsToRepayFlashloan(); | |
| } |
| revert InsufficientFundsToRepayFlashloan(); | ||
| } | ||
| WRAPPED_NATIVE.deposit{ value: nativeBalanceAfter }(); | ||
| IERC20Upgradeable(address(WRAPPED_NATIVE)).approve(address(COMPTROLLER), borrowedAssetAmountToRepay); |
There was a problem hiding this comment.
| IERC20Upgradeable(address(WRAPPED_NATIVE)).approve(address(COMPTROLLER), borrowedAssetAmountToRepay); | |
| IERC20Upgradeable(address(WRAPPED_NATIVE)).approve(address(WRAPPED_NATIVE_MARKET), borrowedAssetAmountToRepay); |
The vWBNB will take the repayment.
| if (nativeBalanceAfter > borrowedAssetAmount) { | ||
| IERC20Upgradeable(address(WRAPPED_NATIVE)).safeTransfer(onBehalf, nativeBalanceAfter - borrowedAssetAmount); |
There was a problem hiding this comment.
We can create a transferDust function for this purpose, allowing for future reuse.
| address(transientMarketTo), | ||
| borrowedAssetAmount - borrowedAssetFees | ||
| ); | ||
| err = WRAPPED_NATIVE_MARKET.mintBehalf(onBehalf, borrowedAssetAmount - borrowedAssetFees); |
There was a problem hiding this comment.
Are we considering that the user collateral minted on vWBNB will be slightly less than the actual vBNB collateral?
| uint256 amountToRepay = _executeSwapDebtNativeToWrapped(onBehalf, vTokens[0], amounts[0], premiums[0]); | ||
| repayAmounts[0] = amountToRepay; | ||
| } else { | ||
| revert ExecuteOperationNotCalledCorrectly(); |
There was a problem hiding this comment.
| revert ExecuteOperationNotCalledCorrectly(); | |
| revert InvalidExecuteOperation(); |
?
| @@ -123,316 +87,764 @@ contract PositionSwapper is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable | |||
| */ | |||
| receive() external payable {} | |||
There was a problem hiding this comment.
should we add limitation here so that only WBNB could send fund ?
| * @custom:error EnterMarketFailed When Comptroller.enterMarketBehalf returns a non-zero error code | ||
| */ | ||
| function _validateAndEnterMarket(address user, IVToken marketFrom, IVToken marketTo) internal { | ||
| if (COMPTROLLER.checkMembership(user, marketFrom) && !COMPTROLLER.checkMembership(user, marketTo)) { |
There was a problem hiding this comment.
is it possible COMPTROLLER.checkMembership(user, marketFrom) -> false
There was a problem hiding this comment.
Yes, users can supply an asset without enabling it as collateral by choosing not to enter the market.
There was a problem hiding this comment.
and in this case, the position swapper will still be functioning 🤔
| // feeRate should be expressed as a scaled number, e.g. 9e14 for 0.09% (since 1e18 = 100%) | ||
| // FA = x / (1 - r) | ||
| uint256 denominator = MANTISSA_ONE - feeRate; | ||
| flashLoanAmount = (requiredAmount * MANTISSA_ONE) / denominator; |
There was a problem hiding this comment.
division here by default will round down, will it make the flashLoanAmount slightly smaller than we expect ?
There was a problem hiding this comment.
I think the rounding effect is minimal and safe. If flashLoanAmount is a bit lower due to truncation, the fee becomes proportionally smaller too (as this final amount is what we are taking as flashLoan), so the final received amount remains ≥ requiredAmount after subtracting the fee.
There was a problem hiding this comment.
True, but in our case the fee won’t be calculated on 1e18. It’s calculated on the value returned by calculateFlashLoanAmount.
required = 1e18 (1 ether)
feeRate = 9e14 (0.09%)
flashLoanAmount = (1e36) / (1e18 - 9e14)
= 1000900810729656691 // truncated
This truncated value is then used for the fee:
fee = flashLoanAmount * feeRate / 1e18
1000900810729656691 * 9e14 / 1e18 // truncated
So truncation happens in both steps.
Because of that, flashLoanAmount - fee still ends up ≥ required. (1000000000000000000) here
I don’t see any case where this fails, but I can switch to a ceil version for stricter validation and to avoid unnecessary reverts.
| if (toUnderlying.balanceOf(address(this)) < borrowedAssetAmount) revert InvalidFlashLoanAmountReceived(); | ||
|
|
||
| // Perform swap using SwapHelper | ||
| _performSwap( |
There was a problem hiding this comment.
- we should have return value i.e. the actual swap output amount here
- when repayBorrow, we should pass in the actual swap output amount because it could be greater than we expect, this also adheres with the naming here i.e. minDebtAmountToSwap
There was a problem hiding this comment.
transientDebtRepaymentAmount may hold the user’s full current debt in the case of a full repayment. We don’t want to repay more than that amount, and even if the swap returns more than expected, any excess will be sent back to the user using _refundDustToUser.
[VPD-306]: Position Swapper V2 Quantstamp Audit
[VPD-306]: Position Swapper V2 Cantina Audit


This PR contains generic position swapper contract with swap logic.
To run the tests use npm pack to pack the package (VenusProtocol/venus-protocol#648) and then import it in package.json file. Ex: "@venusprotocol/venus-protocol": "file:../venus-protocol/venusprotocol-venus-protocol-9.8.0-dev.19.tgz",