Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
642375a
feat: add generic swapper contract
kkirka Aug 28, 2025
e2312fa
feat: add backend signature verification to the swapper
kkirka Oct 3, 2025
2981707
feat: add SwapRouter contract
Debugger022 Oct 17, 2025
fbc3044
test: add unit test for SwapRouter
Debugger022 Oct 22, 2025
82082ba
feat: add deployment script
Debugger022 Oct 24, 2025
3417be5
feat: add SwapRouter fork test
Debugger022 Oct 28, 2025
f487c79
fix: fix comments
Debugger022 Nov 19, 2025
f2459c8
fix: major fixes
Debugger022 Nov 20, 2025
e21d01b
fix: fix unit and fork test
Debugger022 Nov 20, 2025
85f2930
Merge branch 'feat/swapper' into feat/swapRouter
Debugger022 Nov 20, 2025
e233965
Merge branch 'develop' into feat/swapRouter-develop-merge
Exef Dec 15, 2025
215a735
test: update unit test for swapRouter
Debugger022 Dec 18, 2025
059cd0a
Merge pull request #31 from VenusProtocol/feat/swapRouter-develop-merge
Debugger022 Dec 19, 2025
a5bf806
fix: tests
Debugger022 Dec 19, 2025
e6add02
test: update swapRouter fork tests
Debugger022 Dec 19, 2025
9f06257
feat: add swapRouter bscmainnet deployment
Debugger022 Dec 23, 2025
de928dd
feat: updating deployment files
Debugger022 Dec 23, 2025
4487e12
feat: add swapNativeAndRepayFull function
Debugger022 Dec 24, 2025
594b4ff
feat: L-01
Debugger022 Jan 12, 2026
aeb20e1
feat: L-05
Debugger022 Jan 12, 2026
f042ecd
refactor: consolidate slippage check in repayFull
Debugger022 Jan 13, 2026
0f8abbe
feat: I-01
Debugger022 Jan 15, 2026
50a4993
feat: add SwapRouter audits
Debugger022 Jan 21, 2026
f949020
Merge pull request #45 from VenusProtocol/fix/VPD-458
Debugger022 Jan 21, 2026
6b3774d
Merge pull request #43 from VenusProtocol/fix/vpd-440
Debugger022 Jan 21, 2026
d1d28c0
feat: add deployments
Debugger022 Jan 21, 2026
41bb9e0
feat: updating deployment files
Debugger022 Jan 21, 2026
ec44ad8
Merge branch 'develop' into feat/swapRouter
Debugger022 Feb 23, 2026
1db1685
feat: add barrel Interfaces.sol to re-export split interface files
Debugger022 Feb 23, 2026
8de1ccf
feat: updating deployment files
Debugger022 Feb 23, 2026
65165ef
chore: clean up .gitignore duplicates and organize sections
Debugger022 Feb 23, 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
2 changes: 2 additions & 0 deletions contracts/Interfaces.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ interface IVToken is IERC20Upgradeable {
}

interface IVBNB is IVToken {
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.

Expand Down
245 changes: 245 additions & 0 deletions contracts/SwapHelper/SwapHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.28;

import { SafeERC20Upgradeable, IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
* @title SwapHelper
* @author Venus Protocol
* @notice Helper contract for executing multiple token operations atomically
* @dev This contract provides utilities for managing approvals,
* and executing arbitrary calls in a single transaction. It supports
* signature verification using EIP-712 for backend-authorized operations.
* All functions except multicall are designed to be called internally via multicall.
* @custom:security-contact security@venus.io
*/
contract SwapHelper is EIP712, Ownable, ReentrancyGuard {
using SafeERC20Upgradeable for IERC20Upgradeable;
using AddressUpgradeable for address;

/// @notice EIP-712 typehash for Multicall struct used in signature verification
/// @dev keccak256("Multicall(bytes[] calls,uint256 deadline,bytes32 salt)")
bytes32 internal constant MULTICALL_TYPEHASH = keccak256("Multicall(bytes[] calls,uint256 deadline,bytes32 salt)");

/// @notice Address authorized to sign multicall operations
/// @dev Can be updated by contract owner via setBackendSigner
address public backendSigner;

/// @notice Mapping to track used salts for replay protection
/// @dev Maps salt => bool to prevent reuse of same salt
mapping(bytes32 => bool) public usedSalts;

/// @notice Error thrown when transaction deadline has passed
/// @dev Emitted when block.timestamp > deadline in multicall
error DeadlineReached();

/// @notice Error thrown when signature verification fails
/// @dev Emitted when recovered signer doesn't match backendSigner
error Unauthorized();

/// @notice Error thrown when zero address is provided as parameter
/// @dev Used in constructor and setBackendSigner validation
error ZeroAddress();

/// @notice Error thrown when salt has already been used
/// @dev Prevents replay attacks by ensuring each salt is used only once
error SaltAlreadyUsed();

/// @notice Error thrown when caller is not authorized
/// @dev Only owner or contract itself can call protected functions
error CallerNotAuthorized();

/// @notice Error thrown when no calls are provided to multicall
/// @dev Emitted when calls array is empty in multicall
error NoCallsProvided();

/// @notice Error thrown when signature is missing but required
/// @dev Emitted when signature length is zero but verification is expected
error MissingSignature();

/// @notice Event emitted when backend signer is updated
/// @param oldSigner Previous backend signer address
/// @param newSigner New backend signer address
event BackendSignerUpdated(address indexed oldSigner, address indexed newSigner);

/// @notice Event emitted when multicall is successfully executed
/// @param caller Address that initiated the multicall
/// @param callsCount Number of calls executed in the batch
/// @param deadline Deadline timestamp used for the operation
/// @param salt Salt used for replay protection
event MulticallExecuted(address indexed caller, uint256 callsCount, uint256 deadline, bytes32 salt);

/// @notice Event emitted when tokens are swept from the contract
/// @param token Address of the token swept
/// @param to Recipient address
/// @param amount Amount of tokens swept
event Swept(address indexed token, address indexed to, uint256 amount);

/// @notice Event emitted when maximum approval is granted
/// @param token Address of the token approved
/// @param spender Address granted the approval
event ApprovedMax(address indexed token, address indexed spender);

/// @notice Event emitted when generic call is executed
/// @param target Address of the contract called
/// @param data Encoded function call data
event GenericCallExecuted(address indexed target, bytes data);

/// @notice Constructor
/// @param backendSigner_ Address authorized to sign multicall operations
/// @dev Initializes EIP-712 domain with name "VenusSwap" and version "1"
/// @dev Transfers ownership to msg.sender
/// @dev Reverts with ZeroAddress if parameter is address(0)
/// @custom:error ZeroAddress if backendSigner_ is address(0)
constructor(address backendSigner_) EIP712("VenusSwap", "1") {
if (backendSigner_ == address(0)) {
revert ZeroAddress();
}

backendSigner = backendSigner_;
}

/// @notice Modifier to restrict access to owner or contract itself
/// @dev Reverts with CallerNotAuthorized if caller is neither owner nor this contract
modifier onlyOwnerOrSelf() {
if (msg.sender != owner() && msg.sender != address(this)) {
revert CallerNotAuthorized();
}
_;
}

/// @notice Multicall function to execute multiple calls in a single transaction
/// @param calls Array of encoded function calls to execute on this contract
/// @param deadline Unix timestamp after which the transaction will revert
/// @param salt Unique value to ensure this exact multicall can only be executed once
/// @param signature Optional EIP-712 signature from backend signer
/// @dev All calls are executed atomically - if any call fails, entire transaction reverts
/// @dev Calls must be to functions on this contract (address(this))
/// @dev Signature verification is only performed if signature.length != 0
/// @dev Protected by nonReentrant modifier to prevent reentrancy attacks
/// @custom:event MulticallExecuted emitted upon successful execution
/// @custom:security Only the contract itself can call sweep, approveMax, and genericCall
/// @custom:error NoCallsProvided if calls array is empty
/// @custom:error DeadlineReached if block.timestamp > deadline
/// @custom:error SaltAlreadyUsed if salt has been used before
/// @custom:error Unauthorized if signature verification fails
function multicall(
bytes[] calldata calls,
uint256 deadline,
bytes32 salt,
bytes calldata signature
) external nonReentrant {
if (calls.length == 0) {
revert NoCallsProvided();
}

if (block.timestamp > deadline) {
revert DeadlineReached();
}

if (signature.length == 0) {
revert MissingSignature();
}
if (usedSalts[salt]) {
revert SaltAlreadyUsed();
}
usedSalts[salt] = true;

bytes32 digest = _hashMulticall(calls, deadline, salt);
address signer = ECDSA.recover(digest, signature);
if (signer != backendSigner) {
revert Unauthorized();
}

for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory returnData) = address(this).call(calls[i]);
if (!success) {
assembly {
revert(add(returnData, 0x20), mload(returnData))
}
}
}

emit MulticallExecuted(msg.sender, calls.length, deadline, salt);
}

/// @notice Generic call function to execute a call to an arbitrary address
/// @param target Address of the contract to call
/// @param data Encoded function call data
/// @dev This function can interact with any external contract
/// @dev Should only be called via multicall for safety
/// @custom:security Use with extreme caution - can call any contract with any data
/// @custom:security Ensure proper validation of target and data in off-chain systems
/// @custom:error CallerNotAuthorized if caller is not owner or contract itself
function genericCall(address target, bytes calldata data) external onlyOwnerOrSelf {
target.functionCall(data);
emit GenericCallExecuted(target, data);
}

/// @notice Sweeps entire balance of an ERC-20 token to a specified address
/// @param token ERC-20 token contract to sweep
/// @param to Recipient address for the swept tokens
/// @dev Transfers the entire balance of token held by this contract
/// @dev Uses SafeERC20 for safe transfer operations
/// @dev Should only be called via multicall
/// @custom:error CallerNotAuthorized if caller is not owner or contract itself
function sweep(IERC20Upgradeable token, address to) external onlyOwnerOrSelf {
uint256 amount = token.balanceOf(address(this));
if (amount > 0) {
token.safeTransfer(to, amount);
}
emit Swept(address(token), to, amount);
}

/// @notice Approves maximum amount of an ERC-20 token to a specified spender
/// @param token ERC-20 token contract to approve
/// @param spender Address to grant approval to
/// @dev Sets approval to type(uint256).max for unlimited spending
/// @dev Uses forceApprove to handle tokens that require 0 approval first
/// @dev Should only be called via multicall
/// @custom:security Grants unlimited approval - ensure spender is trusted
/// @custom:error CallerNotAuthorized if caller is not owner or contract itself
function approveMax(IERC20Upgradeable token, address spender) external onlyOwnerOrSelf {
token.forceApprove(spender, type(uint256).max);
emit ApprovedMax(address(token), spender);
}

/// @notice Updates the backend signer address
/// @param newSigner New backend signer address
/// @dev Only callable by contract owner
/// @dev Reverts with ZeroAddress if newSigner is address(0)
/// @dev Emits BackendSignerUpdated event
/// @custom:error ZeroAddress if newSigner is address(0)
/// @custom:error Ownable: caller is not the owner (from OpenZeppelin Ownable)
function setBackendSigner(address newSigner) external onlyOwner {
if (newSigner == address(0)) {
revert ZeroAddress();
}

emit BackendSignerUpdated(backendSigner, newSigner);
backendSigner = newSigner;
}

/// @notice Produces an EIP-712 digest of the multicall data
/// @param calls Array of encoded function calls
/// @param deadline Unix timestamp deadline
/// @param salt Unique value to ensure replay protection
/// @return EIP-712 typed data hash for signature verification
/// @dev Hashes each call individually, then encodes with MULTICALL_TYPEHASH, deadline, and salt
/// @dev Uses EIP-712 _hashTypedDataV4 for domain-separated hashing
function _hashMulticall(bytes[] calldata calls, uint256 deadline, bytes32 salt) internal view returns (bytes32) {
bytes32[] memory callHashes = new bytes32[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
callHashes[i] = keccak256(calls[i]);
}
return
_hashTypedDataV4(
keccak256(abi.encode(MULTICALL_TYPEHASH, keccak256(abi.encodePacked(callHashes)), deadline, salt))
);
}
}
89 changes: 89 additions & 0 deletions contracts/SwapRouter/ISwapRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.25;

/**
* @title ISwapRouter
* @author Venus Protocol
* @notice Interface for the SwapRouter contract implementing swap features
*/
interface ISwapRouter {
/**
* @notice Swaps tokens and supplies the result to a Venus market
* @param vToken The vToken market to supply to
* @param tokenIn The input token to swap from
* @param amountIn The amount of input tokens to swap
* @param minAmountOut The minimum amount of output tokens expected
* @param swapData Array of bytes containing swap instructions
* @custom:event Emits SwapAndSupply event
*/
function swapAndSupply(
address vToken,
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.

) external payable;

/**
* @notice Swaps native tokens (BNB) and supplies to a Venus market
* @param vToken The vToken market to supply to
* @param minAmountOut The minimum amount of output tokens expected
* @param swapData Array of bytes containing swap instructions
* @custom:event Emits SwapAndSupply event
*/
function swapNativeAndSupply(address vToken, uint256 minAmountOut, bytes[] calldata swapData) external payable;

/**
* @notice Swaps tokens and repays debt to a Venus market
* @param vToken The vToken market to repay debt to
* @param tokenIn The input token to swap from
* @param amountIn The amount of input tokens to swap
* @param minAmountOut The minimum amount of output tokens expected
* @param swapData Array of bytes containing swap instructions
* @custom:event Emits SwapAndRepay event
*/
function swapAndRepay(
address vToken,
address tokenIn,
uint256 amountIn,
uint256 minAmountOut,
bytes[] calldata swapData
) external payable;

/**
* @notice Swaps native tokens and repays debt to a Venus market
* @param vToken The vToken market to repay debt to
* @param minAmountOut The minimum amount of output tokens expected
* @param swapData Array of bytes containing swap instructions
* @custom:event Emits SwapAndRepay event
*/
function swapNativeAndRepay(address vToken, uint256 minAmountOut, bytes[] calldata swapData) external payable;

/**
* @notice Swaps tokens and repays the full debt for a user
* @param vToken The vToken market to repay full debt to
* @param tokenIn The input token to swap from
* @param maxAmountIn The maximum amount of input tokens to use
* @param swapData Array of bytes containing swap instructions
* @custom:event Emits SwapAndRepay event
*/
function swapAndRepayFull(
address vToken,
address tokenIn,
uint256 maxAmountIn,
bytes[] calldata swapData
) external payable;

/**
* @notice Sweeps leftover ERC-20 tokens from the contract
* @param token The token to sweep
* @custom:event Emits SweepToken event
*/
function sweepToken(address token) external;

/**
* @notice Sweeps leftover native tokens from the contract
* @custom:event Emits SweepNative event
*/
function sweepNative() external;
}
Loading