Skip to content

Conversation

@Debugger022
Copy link
Contributor

This PR adds the swapRouter contract, which has the swap-and-supply and swap-and-repay features.

@Debugger022 Debugger022 self-assigned this Oct 18, 2025
@Debugger022 Debugger022 marked this pull request as ready for review November 4, 2025 13:19
Comment on lines +34 to 38
function mint() external payable;

function repayBorrowBehalf(address borrower) external payable;

function liquidateBorrow(address borrower, IVToken vTokenCollateral) external payable;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please include explicit return types in the function signatures.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

VBNB doesn't have return types in the following functions.

Comment on lines 24 to 30
IComptroller public immutable COMPTROLLER;

/// @notice The swap helper contract for executing token swaps
SwapHelper public immutable swapHelper;

/// @notice The wrapped native token contract (e.g., WBNB)
IWBNB public immutable wrappedNative;
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency, could we use capitalized naming for all immutable variables?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

event SweepNative(address indexed receiver, uint256 amount);

/// @custom:error Thrown when an invalid vToken address is provided
error InvalidVToken(address vToken);
Copy link
Contributor

Choose a reason for hiding this comment

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

InvalidVToken doesn’t seem to be used anywhere, we can remove it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IWBNB public immutable wrappedNative;

/// @notice The native vToken address (e.g., vBNB)
address public immutable NATIVE_VTOKEN;
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency with other immutable contract references, can we use the IVBNB interface here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 341 to 342
if (vToken == NATIVE_VTOKEN) {
return address(wrappedNative);
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand we’re returning the wrapped asset here instead of the native one since the native token doesn’t have an address and it simplifies the swap logic. This is later handled in _supply().
However, should we continue supporting this mapping for the native market? Since we plan to deprecate the native market and move fully to the wrapped version, should we all together skip the vBNB minting wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with this approach, and it's worth discussing further.

uint256 balanceBefore = IERC20Upgradeable(tokenOut).balanceOf(address(this));

// Execute swap using SwapHelper multicall
bytes memory swapCallData = abi.encodeWithSelector(swapHelper.multicall.selector, swapData);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update swapHelper, the multicall interface has changed, so the encoding here should be adjusted accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

* @param amount The amount to supply
* @return amountSupplied The actual amount supplied
*/
function _supply(address vToken, address token, uint256 amount) internal returns (uint256 amountSupplied) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we rename the token parameter to asset or underlyingToken? for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 444 to 446
uint256 vTokenBalance = IERC20Upgradeable(vToken).balanceOf(address(this));
if (vTokenBalance > 0) {
IERC20Upgradeable(vToken).safeTransfer(msg.sender, vTokenBalance);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we decide to support vBNB minting, I would prefer to move this transfer block probably inside the NATIVE_VTOKEN specific if block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you clarify why we should move it inside the if block?

Copy link
Contributor

@fred-venus fred-venus Nov 20, 2025

Choose a reason for hiding this comment

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

agree, why not move this into NATIVE_VTOKEN block ?

The reason for this is only NATIVE_VTOKEN doesn't support mintBehalf.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

* @return actualAmountIn The actual amount transferred
*/
function _handleTokenInput(address tokenIn, uint256 amountIn) internal returns (uint256 actualAmountIn) {
if (tokenIn == address(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of mapping the native token to address(0), I’d prefer using the NATIVE_TOKEN_ADDR constant (0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB) for consistency with how we handle native tokens in other contracts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 465 to 470
if (vToken == NATIVE_VTOKEN) {
// Handle native token repayment
IWBNB(token).withdraw(repayAmount);
IVBNB(vToken).repayBorrowBehalf{ value: repayAmount }(msg.sender);
amountRepaid = repayAmount;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer this design:

  1. Do not allow supplying collateral or repaying for the native market (vBNB).
    As we are deprecating the native market.

  2. It is still fine to accept native BNB as tokenIn.
    The contract can wrap the native BNB, use it as input for swaps, and then supply/repay any non-native market.
    Native BNB should only act as input to the flow not as the target market itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

i think it's fine if we support it, it doesn't bring any drawback or complicate the logic a lot

Comment on lines 436 to 437
IERC20Upgradeable(token).safeApprove(vToken, 0);
IERC20Upgradeable(token).safeApprove(vToken, amount);
Copy link
Contributor

Choose a reason for hiding this comment

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

safeApprove is deprecated in oz v4 and removed in oz v5. We could use forceApprove or approveOrRevert

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 480 to 485
if (amount > repayAmount) {
uint256 excess = amount - repayAmount;
if (vToken == NATIVE_VTOKEN) {
// Convert back to wrapped native and transfer
wrappedNative.deposit{ value: excess }();
IERC20Upgradeable(address(wrappedNative)).safeTransfer(msg.sender, excess);
Copy link
Contributor

Choose a reason for hiding this comment

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

In _repay, we only unwrap repayAmount, not the full amount.
So the excess stays as WBNB, not native BNB.
But the code tries to re-wrap it using:
wrappedNative.deposit{ value: excess }();

This might fails because there’s no native BNB corresponding to that excess.
We should just transfer the excess WBNB back to the user instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed f2459c8

uint256 amountIn,
uint256 minAmountOut,
bytes[] calldata swapData
) external payable nonReentrant {
Copy link
Contributor

Choose a reason for hiding this comment

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

Any specific reason for making swapAndSupply and swapAndRepay payable when native handling already uses separate functions (swapNativeAndSupply and swapNativeAndRepay)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed payable f2459c8

Comment on lines 459 to 460
uint256 debtAmount = IVToken(vToken).borrowBalanceCurrent(msg.sender);
if (debtAmount == 0) revert ZeroAmount();
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer adding this as an early-revert check in all external repay-related functions instead of here, similar to how it’s already done in swapAndRepayFull

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 289 to 293
// Return any excess tokens to user
if (amountOut > debtAmount) {
uint256 excess = amountOut - debtAmount;
IERC20Upgradeable(tokenOut).safeTransfer(msg.sender, excess);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Can we refactor the logic so excess tokens are transferred back to the user in one place instead of doing it twice, once here and once inside _repay?
I understand the calculations differ, but we can still consolidate the return excess logic to avoid duplication and make the flow cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

* @dev For native vToken (vBNB), returns wrapped native token address
*/
function _getUnderlyingToken(address vToken) internal view returns (address underlying) {
if (vToken == address(NATIVE_VTOKEN)) {
Copy link
Contributor

@fred-venus fred-venus Nov 20, 2025

Choose a reason for hiding this comment

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

so what's the value of IVToken(vWBNB).underlying() -> WETH ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will be WBNB. However, for VBNB, we are also returning WBNB.

amountSupplied = amount;
} else {
// Handle ERC20 token supply
IERC20Upgradeable(token).forceApprove(vToken, amount);
Copy link
Contributor

Choose a reason for hiding this comment

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

should we reset the approve to 0 after mintBehalf ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not needed.

address tokenIn,
uint256 amountIn,
uint256 minAmountOut,
bytes[] calldata swapData
Copy link
Contributor

Choose a reason for hiding this comment

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

should be " bytes calldata swapData " ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

/**
* @notice Accepts native tokens sent to this contract
*/
receive() external payable {}
Copy link
Contributor

Choose a reason for hiding this comment

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

lets add limit so that only wBNB can send fund, wdyt ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

uint256 amountIn,
uint256 minAmountOut,
bytes calldata swapCallData
) external payable nonReentrant {
Copy link
Contributor

Choose a reason for hiding this comment

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

can remove payable here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

uint256 actualAmountIn = _handleTokenInput(tokenIn, maxAmountIn);

// Perform swap - no minAmountOut since we need exact debt amount
uint256 amountOut = _performSwap(tokenIn, tokenOut, actualAmountIn, 0, swapCallData);
Copy link
Contributor

Choose a reason for hiding this comment

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

even tho we should still pass in debtAmount here, i mean we cant guarantee the underlying swap will return exact debtAmount

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The function validates post-swap that amountOut >= debtAmount, which is more practical than constraining the swap upfront - this allows natural slippage while ensuring sufficient funds for full debt repayment.

uint256 minAmountOut,
bytes calldata swapCallData
) external payable nonReentrant {
if (msg.value == 0) revert ZeroAmount();
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this function uses mintBehalf it might be a good idea to check if msg.sender delegated this contract.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mintBehalf function doesn't require delegation logic.

uint256 minAmountOut,
bytes calldata swapCallData
) external nonReentrant {
if (amountIn == 0) revert ZeroAmount();
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this function uses mintBehalf it might be a good idea to check if msg.sender delegated this contract.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

uint256 minAmountOut,
bytes calldata swapCallData
) external payable nonReentrant {
if (msg.value == 0) revert ZeroAmount();
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this function uses mintBehalf it might be a good idea to check if msg.sender delegated this contract.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

uint256 maxAmountIn,
bytes calldata swapCallData
) external payable nonReentrant {
if (maxAmountIn == 0) revert ZeroAmount();
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this function uses mintBehalf it might be a good idea to check if msg.sender delegated this contract.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

NATIVE_VTOKEN.mint{ value: amount }();
amountSupplied = amount;

// Transfer vBNB tokens to user (only needed for NATIVE_VTOKEN since it doesn't support mintBehalf)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do I understand it correctly: mintBehalf like behaviour on vBNB market is to mint for a contract and transfer vTokens to user ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, VBNB doesn't have any mintBehalf logic.

@fred-venus
Copy link
Contributor

overall lgtm, no other major issue found

Base automatically changed from feat/swapper to develop December 12, 2025 11:39
@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
contracts 100% 100%
contracts.LeverageManager 94% 80%
contracts.PositionSwapper 0% 0%
contracts.SwapHelper 90% 85%
contracts.SwapRouter 79% 55%
Summary 67% (361 / 542) 46% (163 / 352)

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.

6 participants