Skip to content

Generic Position Swapper#13

Open
Debugger022 wants to merge 42 commits into
developfrom
feat/generic-position-swapper
Open

Generic Position Swapper#13
Debugger022 wants to merge 42 commits into
developfrom
feat/generic-position-swapper

Conversation

@Debugger022

@Debugger022 Debugger022 commented Oct 8, 2025

Copy link
Copy Markdown
Contributor

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",

@Debugger022
Debugger022 marked this pull request as draft October 8, 2025 11:50
@Debugger022
Debugger022 force-pushed the feat/generic-position-swapper branch from 3c4004b to 989dea3 Compare October 14, 2025 11:48
Comment on lines 57 to 58
/// @notice Emitted when an approved pair is updated.
event ApprovedPairUpdated(address marketFrom, address marketTo, address helper, bool oldStatus, bool newStatus);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This event is no longer needed.

Comment on lines 71 to 72

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used anywhere in the contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for BorrowFailed, NoVTokenBalance, NoUnderlyingReceived and AccrueInterestFailed.

Comment on lines +143 to +144
/// @custom:error NoEnoughBalance
error NoEnoughBalance();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// @custom:error NoEnoughBalance
error NoEnoughBalance();
/// @custom:error NotEnoughBalance
error NotEnoughBalance();

Comment on lines +170 to +173
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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use ensureNonzeroAddress from "@venusprotocol/solidity-utilities/contracts/validators.sol".

Comment on lines +223 to +226
/**
* @notice Swap user's entire native collateral (e.g., vBNB) into wrapped collateral (e.g., vWBNB).
* @param user Address of the user.
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please include events and errors in the natspec too.

Comment on lines +238 to +239
* @param user Address of the user.
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please include events and errors in the natspec too.

Comment on lines +847 to +848
function _validateAndEnterMarket(address user, IVToken marketFrom, IVToken marketTo) internal {
if (COMPTROLLER.checkMembership(user, marketFrom) && !COMPTROLLER.checkMembership(user, marketTo)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we store it in a variable mintAmount = borrowedAssetAmount - borrowedAssetFee?

Comment on lines 645 to 647
if (nativeBalanceAfter < borrowedAssetAmount) {
revert InsufficientFundsToRepayFlashloan();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
IERC20Upgradeable(address(WRAPPED_NATIVE)).approve(address(COMPTROLLER), borrowedAssetAmountToRepay);
IERC20Upgradeable(address(WRAPPED_NATIVE)).approve(address(WRAPPED_NATIVE_MARKET), borrowedAssetAmountToRepay);

The vWBNB will take the repayment.

Comment on lines +652 to +653
if (nativeBalanceAfter > borrowedAssetAmount) {
IERC20Upgradeable(address(WRAPPED_NATIVE)).safeTransfer(onBehalf, nativeBalanceAfter - borrowedAssetAmount);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
revert ExecuteOperationNotCalledCorrectly();
revert InvalidExecuteOperation();

?

@GitGuru7
GitGuru7 marked this pull request as ready for review November 12, 2025 07:12
@@ -123,316 +87,764 @@ contract PositionSwapper is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable
*/
receive() external payable {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add limitation here so that only WBNB could send fund ?

@GitGuru7 GitGuru7 Nov 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @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)) {

@fred-venus fred-venus Nov 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible COMPTROLLER.checkMembership(user, marketFrom) -> false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, users can supply an asset without enabling it as collateral by choosing not to enter the market.

@fred-venus fred-venus Nov 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and in this case, the position swapper will still be functioning 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

division here by default will round down, will it make the flashLoanAmount slightly smaller than we expect ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@fred-venus fred-venus Nov 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think fee will be truncated, this is how fee is calculated
CleanShot 2025-11-20 at 20 19 53@2x

let's say feeRate = 9e14 i.e. 0.09%
borrowedAmt = 1 ether = 1e18
then fee = 1e18 * 9e14 / 1e18 = 9e14 ------- no truncation

But back here

XXX / (1e18 - 9e14) -------- truncation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be a issue here

CleanShot 2025-11-20 at 21 27 38@2x

@GitGuru7 GitGuru7 Nov 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (toUnderlying.balanceOf(address(this)) < borrowedAssetAmount) revert InvalidFlashLoanAmountReceived();

// Perform swap using SwapHelper
_performSwap(

@fred-venus fred-venus Nov 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. we should have return value i.e. the actual swap output amount here
  2. 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Base automatically changed from feat/VEN-1193 to develop December 2, 2025 12:53
@github-actions

github-actions Bot commented Feb 5, 2026

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
contracts 100% 100%
contracts.LeverageManager 94% 80%
contracts.PositionSwapper 93% 56%
contracts.SwapHelper 100% 100%
Summary 94% (456 / 484) 71% (187 / 264)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants