UnoRouterV2 is an upgradeable swap router for the UNO wallet that enables atomic token swaps with optional deposit and send operations. The contract extends the original UnoRouter with new atomic swap functions while maintaining full backward compatibility with existing functionality and events.
Key Features:
- Atomic token-to-token swaps via approved aggregators (0x, 1inch)
- Atomic swap and send to any recipient address
- Atomic swap and deposit into ERC4626 vaults (e.g., Morpho)
- Permit2 signature-based approvals for gasless transactions
- Upgradeable via UUPS proxy pattern
- Full backward compatibility with UnoRouter v1 events and functionality
- UnoRouterV2: Main upgradeable router contract with atomic swap functions
- BaseAggregator: Core swap logic with Permit2 integration (inherited)
- Permit2Helper: Helper contract for Permit2 signature transfers
- SwapParams: Struct containing swap parameters (sell/buy tokens, target, calldata, amounts, fee)
fillQuoteTokenToToken: Legacy swap function (unchanged from UnoRouter v1)fillQuoteEthToToken: ETH-to-token swaps (unchanged)fillQuoteTokenToEth: Token-to-ETH swaps (unchanged)fillQuoteTokenToTokenAndSend: NEW - Atomic swap and send to recipientfillQuoteTokenToTokenAndDeposit: NEW - Atomic swap and deposit to vaultupdateSwapTargets: Admin function to manage approved aggregatorswithdrawToken/withdrawEth: Admin functions for emergency withdrawals
The contract maintains all legacy events for analytics compatibility:
FillQuoteTokenToToken: Emitted for all token-to-token swapsFillQuoteEthToToken: Emitted for ETH-to-token swapsFillQuoteTokenToEth: Emitted for token-to-ETH swaps
New events for atomic operations:
FillQuoteAndSend: Emitted when tokens are sent to recipient after swapFillQuoteAndDeposit: Emitted when tokens are deposited to vault after swap
# Install dependencies
bun install
# Build contracts
forge build
# Run tests
forge testFoundry typically uses git submodules to manage dependencies, but this project uses Node.js packages because submodules don't scale.
To add a new dependency:
- Install the dependency using your preferred package manager, e.g.
bun install dependency-name- Use this syntax to install from GitHub:
bun install github:username/repo-name
- Use this syntax to install from GitHub:
- Add a remapping for the dependency in remappings.txt, e.g.
dependency-name=node_modules/dependency-name
Note that OpenZeppelin Contracts is pre-installed, so you can follow that as an example.
# Run all tests
forge test
# Run tests with gas report
forge test --gas-report
# Run specific test file
forge test --match-path test/UnoRouterV2.t.sol
# Run tests with verbose output
forge test -vvvFor production deployment (DeployUnoRouterV2.s.sol):
PRIVATE_KEY: Deployer private key (must matchEXPECTED_DEPLOYER)OWNER: Address that will own the contract (typically multisig)PERMIT2: Permit2 contract address on target chainSWAP_TARGETS: Comma-separated list of approved aggregator addresses (optional, defaults to empty)
# Deploy to local Anvil
forge script script/DeployUnoRouterV2.s.sol --broadcast --fork-url http://localhost:8545
# Deploy to testnet
forge script script/DeployUnoRouterV2.s.sol --broadcast --rpc-url $WORLDCHAIN_RPC_URL --private-key $PRIVATE_KEY
# Deploy to mainnet
forge script script/DeployUnoRouterV2.s.sol --broadcast --rpc-url $WORLDCHAIN_RPC_URL --private-key $PRIVATE_KEY --verify
# Upgrade contract
forge script script/Upgrade.s.sol:Upgrade --broadcast --rpc-url $WORLDCHAIN_RPC_URL --private-key $PRIVATE_KEY <PROXY_ADDRESS>// Legacy token-to-token swap (unchanged from UnoRouter v1)
function fillQuoteTokenToToken(
address sellTokenAddress,
address buyTokenAddress,
address payable target,
bytes calldata swapCallData,
uint256 sellAmount,
FeeToken feeToken,
uint256 feeAmount,
Permit2 calldata permit
) external payable;
// Atomic swap and send to recipient
function fillQuoteTokenToTokenAndSend(
SwapParams calldata params,
address recipient,
Permit2 calldata permit
) external payable;
// Atomic swap and deposit to vault
function fillQuoteTokenToTokenAndDeposit(
SwapParams calldata params,
address vault,
address receiver,
Permit2 calldata permit
) external payable returns (uint256 shares);// Update swap target approval
function updateSwapTargets(address target, bool add) external onlyOwner;
// Withdraw ERC20 tokens
function withdrawToken(address token, address to, uint256 amount) external onlyOwner;
// Withdraw ETH
function withdrawEth(address to, uint256 amount) external onlyOwner;event FillQuoteTokenToToken(
address indexed sellToken,
address indexed buyToken,
address indexed user,
address target,
uint256 amountSold,
uint256 amountBought,
FeeToken feeToken,
uint256 feeAmount
);
event FillQuoteAndSend(
address indexed buyToken,
uint256 buyTokenAmount,
address sendTo
);
event FillQuoteAndDeposit(
address indexed buyToken,
uint256 buyTokenAmount,
address depositTo,
address vault
);- Signature Verification: All token transfers use Permit2 signature-based approvals
- Nonce Management: Permit2 handles nonce tracking to prevent replay attacks
- Deadline Validation: Permits must have valid expiration and signature deadlines
- Target Authorization: Only approved swap targets can be called
- Allowance Validation: All allowances are cleared after swaps to prevent residual approvals
- Reentrancy Protection: All external functions are protected with ReentrancyGuard
- Input Validation: Zero address checks and amount validations throughout
- Upgrade Authorization: Only owner can upgrade implementation via UUPS pattern
- Token Rescue: Owner can rescue tokens in emergency situations
- Fee Validation: Fees cannot exceed output amounts
- Atomic Operations: All swap+send and swap+deposit operations are atomic
- Fee-on-Transfer Tokens: Not supported (excluded for safety)
- Rebasing Tokens: Not supported (excluded for safety)
- Vault Pre-checks: No pre-checks for vault paused/capacity states - vault's own validation handles this
This project is licensed under GPL-3.0.