Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
33f00ab
feat: add generic swapper contract
Debugger022 Oct 8, 2025
7792b57
feat: add SwapHelper contract
Debugger022 Oct 8, 2025
988136d
refactor: update imports
Debugger022 Oct 8, 2025
d81840d
feat: update interface
Debugger022 Oct 8, 2025
989dea3
refactor: update PositionSwapper to intergrate Swaphelper
Debugger022 Oct 14, 2025
486d1cf
fix: minor fixes
Debugger022 Oct 14, 2025
5ae419f
refactor: update unit tests of position swapper
Debugger022 Oct 16, 2025
234b16c
feat: add position swapper unit tests for non native markets
Debugger022 Oct 16, 2025
300dfbb
fix: clean up approved pair logic after moving to a unified SwapHelper
GitGuru7 Oct 24, 2025
28c916a
feat: update PositionSwapper to use flashLoan
GitGuru7 Oct 28, 2025
f65aad5
feat: add IPositionSwapper and refactor PositionSwapper
GitGuru7 Oct 31, 2025
1572520
refactor: refactor and cleanup unused code
GitGuru7 Nov 3, 2025
d35a899
refactor: removed WBNB swap helper
GitGuru7 Nov 3, 2025
8d82fc2
refactor: use interface instead of contract directly
GitGuru7 Nov 3, 2025
55032b6
test: add unit tests for main swap functionality
GitGuru7 Nov 3, 2025
115dc94
test: add more unit tests
GitGuru7 Nov 3, 2025
73c6eb6
Merge branch 'feat/swapper' into feat/generic-position-swapper
GitGuru7 Nov 7, 2025
ffa6b81
feat: add deployment script
GitGuru7 Nov 7, 2025
0aa1785
fix: tests
GitGuru7 Nov 7, 2025
ac4402a
fix: update unit tests for swapHelper changes
GitGuru7 Nov 10, 2025
5add8bb
refactor: minor cleanups
GitGuru7 Nov 11, 2025
6c26ccd
test: add position-swapper fork test
GitGuru7 Nov 11, 2025
9dd9335
test: add more fork tests for position-swapper
GitGuru7 Nov 12, 2025
3259c8c
refactor: drop unnecessary payable and add mantissa-one constant
GitGuru7 Nov 18, 2025
972dd09
fix: add sender validation in receive
GitGuru7 Nov 20, 2025
893033e
refactor: combine full and partial swap functions
GitGuru7 Nov 20, 2025
bd3458d
fix: use ceil in calculateFlashLoanAmount
GitGuru7 Nov 21, 2025
d6362b4
Merge branch 'develop' into feat/generic-position-swapper-develop-merge
Exef Dec 15, 2025
10eab6c
Merge branch 'develop' into feat/generic-position-swapper
GitGuru7 Jan 13, 2026
08628a2
fix: formatting
GitGuru7 Jan 13, 2026
df1afeb
fix: tests
GitGuru7 Jan 13, 2026
15badf5
fix: tests ci check
GitGuru7 Jan 13, 2026
88f8d71
fix: tests ci check
GitGuru7 Jan 13, 2026
9a3e27c
fix: [VEN-1] Missing Initiator Validation in Flashloan Callback
GitGuru7 Jan 21, 2026
b3b5517
fix: [S1] No Validation that marketFrom and marketTo Are Different
GitGuru7 Jan 21, 2026
063c9f9
fix: [S2] Return Data From SWAP_HELPER.call() Is Ignored
GitGuru7 Jan 21, 2026
5f3a8d8
fix: [S3] Native Swap Functions Are Not Generic
GitGuru7 Jan 21, 2026
340b49f
fix: [1] Code Clarity
GitGuru7 Jan 27, 2026
e90fd7a
docs: add quantstamp audit report for position-swapper-v2
GitGuru7 Jan 29, 2026
5beef24
Merge pull request #46 from VenusProtocol/feat/vpd-306-quantstamp-audit
GitGuru7 Jan 29, 2026
3ed9fb3
docs: add cantina audit report for position-swapper-v2
GitGuru7 Feb 5, 2026
d99a896
Merge pull request #48 from VenusProtocol/feat/vpd-306-cantina-audit
GitGuru7 Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file added audits/172_Position_Swapper_2_Cantina_260204.pdf
Binary file not shown.
24 changes: 14 additions & 10 deletions contracts/Interfaces.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol";

interface IVToken is IERC20Upgradeable {

Check warning on line 7 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @notice tag in contract 'IVToken'

Check warning on line 7 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @author tag in contract 'IVToken'

Check warning on line 7 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @title tag in contract 'IVToken'
function accrueInterest() external returns (uint256);

Check warning on line 8 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Mismatch in @return count for function 'accrueInterest'. Expected: 1, Found: 0

Check warning on line 8 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @return tag in function 'accrueInterest'

Check warning on line 8 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @notice tag in function 'accrueInterest'

function redeem(uint256 redeemTokens) external returns (uint256);

Check warning on line 10 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @return tag in function 'redeem'

Check warning on line 10 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Mismatch in @PARAM names for function 'redeem'. Expected: [redeemTokens], Found: []

Check warning on line 10 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @PARAM tag in function 'redeem'

Check warning on line 10 in contracts/Interfaces.sol

View workflow job for this annotation

GitHub Actions / Lint

Missing @notice tag in function 'redeem'

function redeemUnderlying(uint256 redeemAmount) external returns (uint256);

Expand All @@ -23,16 +23,22 @@

function repayBorrowBehalf(address borrower, uint repayAmount) external returns (uint256);

function redeemUnderlyingBehalf(address redeemer, uint redeemAmount) external returns (uint);

function comptroller() external view returns (IComptroller);

function borrowBalanceStored(address account) external view returns (uint256);

function underlying() external view returns (address);

function redeemBehalf(address redeemer, uint256 redeemTokens) external returns (uint256);

function redeemUnderlyingBehalf(address redeemer, uint256 redeemAmount) external returns (uint256);

function flashLoanFeeMantissa() external view returns (uint256);
}

interface IVBNB is IVToken {
function mint() external payable;

function repayBorrowBehalf(address borrower) external payable;

function liquidateBorrow(address borrower, IVToken vTokenCollateral) external payable;
Expand All @@ -57,8 +63,6 @@

function enterMarketBehalf(address onBehalf, address vToken) external returns (uint256);

function enterMarket(address user, address vToken) external returns (uint256);

function liquidationIncentiveMantissa() external view returns (uint256);

function vaiController() external view returns (address);
Expand Down Expand Up @@ -94,6 +98,12 @@
) external;
}

interface IWBNB is IERC20Upgradeable {
function deposit() external payable;

function withdraw(uint256 amount) external;
}

interface IFlashLoanReceiver {
/**
* @notice Executes an operation after receiving the flash-borrowed assets.
Expand All @@ -119,12 +129,6 @@
) external returns (bool success, uint256[] memory repayAmounts);
}

interface IWBNB is IERC20Upgradeable {
function deposit() external payable;

function withdraw(uint256 amount) external;
}

interface IProtocolShareReserve {
enum IncomeType {
SPREAD,
Expand Down
181 changes: 181 additions & 0 deletions contracts/PositionSwapper/IPositionSwapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.28;

import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { IVToken } from "../Interfaces.sol";

interface IPositionSwapper {
enum OperationType {
NONE,
SWAP_COLLATERAL,
SWAP_DEBT,
SWAP_COLLATERAL_NATIVE_TO_WRAPPED,
SWAP_DEBT_NATIVE_TO_WRAPPED
}

/// @notice Emitted after a successful collateral swap
/// @param user The account whose collateral was migrated
/// @param marketFrom The vToken market from which collateral was removed
/// @param marketTo The vToken market to which collateral was supplied
/// @param collateralRemoved Amount of underlying removed from `marketFrom`
/// @param collateralSupplied Amount of underlying supplied to `marketTo`
event CollateralSwapped(
address indexed user,
address indexed marketFrom,
address indexed marketTo,
uint256 collateralRemoved,
uint256 collateralSupplied
);

/// @notice Emitted when a user swaps their debt from one market to another
/// @param user The account whose debt was migrated
/// @param marketFrom The vToken market where debt was repaid (removed)
/// @param marketTo The vToken market where new debt was opened
/// @param debtRemoved Amount of underlying debt repaid on `marketFrom`
/// @param debtOpened Amount of underlying debt opened on `marketTo`
event DebtSwapped(
address indexed user,
address indexed marketFrom,
address indexed marketTo,
uint256 debtRemoved,
uint256 debtOpened
);

/// @notice Emitted when the owner sweeps leftover ERC-20 tokens.
/// @param token The ERC-20 token that was swept
/// @param receiver The address that received the swept tokens
/// @param amount The amount of tokens transferred to `receiver`
event SweepToken(address indexed token, address indexed receiver, uint256 amount);

/// @notice Emitted when the owner sweeps leftover native tokens (e.g., BNB).
/// @param receiver The address that received the native tokens
/// @param amount The amount of native tokens transferred to `receiver`
event SweepNative(address indexed receiver, uint256 amount);

/// @custom:error UnauthorizedCaller Caller is neither the user nor an approved delegate.
error UnauthorizedCaller(address account);

/// @custom:error RedeemFailed Redeem on a vToken market returned a non-zero error code
error RedeemFailed(uint256 err);

/// @custom:error MintFailed Mint on a vToken market returned a non-zero error code
error MintFailed(uint256 err);

/// @custom:error RepayFailed Repay on a vToken market returned a non-zero error code
error RepayFailed(uint256 err);

/// @custom:error InsufficientCollateralBalance User has no collateral balance to swap
error InsufficientCollateralBalance();

/// @custom:error InsufficientBorrowBalance User has no borrow balance to swap
error InsufficientBorrowBalance();

/// @custom:error ZeroAmount Provided amount parameter is zero
error ZeroAmount();

/// @custom:error SwapCausesLiquidation Operation would put the account at risk (undercollateralized)
error SwapCausesLiquidation(uint256 err);

/// @custom:error MarketNotListed Provided vToken market is not listed in Comptroller
error MarketNotListed(address market);

/// @custom:error ZeroAddress One of the required addresses is zero
error ZeroAddress();

/// @custom:error TransferFailed Native or ERC-20 transfer failed
error TransferFailed();

/// @custom:error EnterMarketFailed Comptroller.enterMarket returned a non-zero error code
error EnterMarketFailed(uint256 err);

/// @custom:error TokenSwapCallFailed Swap helper call reverted or returned false
error TokenSwapCallFailed();

/// @custom:error InvalidFlashLoanAmountReceived Unexpected or insufficient tokens received for flash loan workflow
error InvalidFlashLoanAmountReceived();

/// @custom:error UnauthorizedExecutor Caller is not the expected Comptroller
error UnauthorizedExecutor();

/// @custom:error FlashLoanAssetOrAmountMismatch Invalid flash loan arrays length or >1 elements
error FlashLoanAssetOrAmountMismatch();

/// @custom:error InvalidExecuteOperation Unknown operation type in flash loan callback
error InvalidExecuteOperation();

/// @custom:error InsufficientFundsToRepayFlashloan Not enough proceeds to repay flash loan plus fees
error InsufficientFundsToRepayFlashloan();

/// @custom:error InsufficientAmountOutAfterSwap Swap output lower than required minimum
error InsufficientAmountOutAfterSwap();

/// @custom:error Thrown when unauthorized sender tries to send native tokens
error UnauthorizedNativeSender(address sender);

/// @custom:error InitiatorMismatch Invalid initiator address in flash loan callback
error InitiatorMismatch();

/// @custom:error SameMarket The source and destination markets cannot be the same
error SameMarket();

// External functions
/// @notice Allows the owner to sweep leftover ERC-20 tokens from the contract.
/// @param token The token to sweep.
/// @custom:event Emits SweepToken event.
function sweepToken(IERC20Upgradeable token) external;

/// @notice Allows the owner to sweep leftover native tokens (e.g., BNB) from the contract.
/// @custom:event Emits SweepNative event.
/// @custom:error TransferFailed Native transfer failed
function sweepNative() external;

/// @notice Swap user's entire native collateral (e.g., vBNB) into wrapped collateral (e.g., vWBNB).
/// @param user Address of the user.
/// @custom:error InsufficientCollateralBalance User has no native collateral
/// @custom:event Emits CollateralSwapped on success
function swapCollateralNativeToWrapped(address user) external;

/// @notice Swap user's entire native debt (e.g., vBNB borrow) into wrapped debt (e.g., vWBNB borrow).
/// @param user Address of the user.
/// @custom:error InsufficientBorrowBalance User has no native debt
/// @custom:event Emits DebtSwapped on success
function swapDebtNativeToWrapped(address user) external;

/// @notice Swaps collateral from one market to another.
/// @param user The address whose collateral is being swapped.
/// @param marketFrom The vToken market to seize from.
/// @param marketTo The vToken market to mint into.
/// @param amountToSwap The amount of underlying to swap from `marketFrom`. Use `type(uint256).max` to swap full balance.
/// @param minAmountToSupply Minimum amount of target underlying to supply after swap.
/// @param swapData Bytes containing swap instructions for the SwapHelper.
/// @custom:error ZeroAmount The `amountToSwap` is zero (and not `type(uint256).max`).
/// @custom:error InsufficientCollateralBalance The user has no or insufficient underlying balance in the `marketFrom`.
/// @custom:event Emits CollateralSwapped event.
function swapCollateral(
address user,
IVToken marketFrom,
IVToken marketTo,
uint256 amountToSwap,
uint256 minAmountToSupply,
bytes calldata swapData
) external;

/// @notice Swaps debt from one market to another.
/// @param user The address whose debt is being swapped.
/// @param marketFrom The vToken market from which debt is swapped.
/// @param marketTo The vToken market into which the new debt is borrowed.
/// @param repayAmount The amount of debt of `marketFrom` to repay. Use `type(uint256).max` to repay full debt.
/// @param maxDebtAmountToOpen Maximum amount to open as new debt on `marketTo` (before fee rounding).
/// @param swapData Bytes containing swap instructions for the SwapHelper.
/// @custom:error ZeroAmount The `repayAmount` is zero (and not `type(uint256).max`).
/// @custom:error InsufficientBorrowBalance The user has no or insufficient borrow balance in the `marketFrom`.
/// @custom:event Emits DebtSwapped event.
function swapDebt(
address user,
IVToken marketFrom,
IVToken marketTo,
uint256 repayAmount,
uint256 maxDebtAmountToOpen,
bytes calldata swapData
) external;
}
6 changes: 0 additions & 6 deletions contracts/PositionSwapper/ISwapHelper.sol

This file was deleted.

Loading