RateHopper Contracts is a smart contract system that enables users to automatically switch their borrowing positions between different DeFi lending protocols to take advantage of the best borrowing rates. This helps users optimize their borrowing costs by seamlessly moving their debt between protocols when better rates are available.
-
Multi-Protocol Support: Currently supports borrowing from:
- Aave V3
- Compound
- Morpho
- Moonwell
- Fluid
-
Flash Loan Integration: Uses Uniswap V3 flash loans to facilitate debt position transfers without requiring users to have the full repayment amount upfront.
-
Collateral Management: Handles multiple collateral assets across different protocols during debt transfers.
-
Paraswap Integration: Uses Paraswap for efficient token swaps when debt assets differ between protocols.
-
Protocol Fee: Configurable protocol fee system with a designated fee beneficiary.
-
Safe Module Integration: Supports Gnosis Safe integration through dedicated Safe modules.
-
Leveraged Positions: Enables creation of leveraged positions across supported protocols.
-
Uniswap V3 LP Lifecycle:
RatehopperUniV3Positionsis a Gnosis Safe module that opens, harvests fees from, and closes WETH/USDC LP positions atomically. Charges a configurable performance fee on profit at close, plus a separate fee on accrued LP fees. Critical setters are timelock-gated. -
Unified Yield Module (SafeYieldManager):
SafeYieldManageris the single Safe module for all yield (LP) protocols — the yield-side counterpart ofSafeDebtManager. Users enable this ONE contract as a module; per-protocol mechanics live in stateless handlers (UniV3YieldHandler,AerodromeYieldHandler,UniV4YieldHandler) invoked via delegatecall. Protocols are identified by plainuint8ids (theYIELD_PROTOCOL_*constants inTypes.soldocument the canonical assignment), so adding a protocol is a handler deployment + timelockedsetYieldHandleron the already-deployed manager — no redeploy, and not a new module every user must enable. The standaloneRatehopperUniV3Positionsmodule above is legacy: it keeps serving positions opened through it (coexistence, no state migration), while new positions open throughSafeYieldManager— Uniswap V3, Aerodrome Slipstream (CL, with optional staking for AERO emissions), and Uniswap V4 (including native ETH pools), over any allow-listed pair.
See docs/SECURITY_MODEL.md for what the Safe-module
custody model implies: why the performance fee is cooperative, why repayment
residue is refunded, and why the pauser can delay exits but never trap a
position.
The system consists of several key components:
-
Governance & Access Control:
TimelockController: OpenZeppelin's timelock implementation with 2-day delay by default for critical operationsProtocolRegistry.sol: Central registry with hybrid access control:DEFAULT_ADMIN_ROLE: For routine operations (whitelist, token mappings) - immediate executionCRITICAL_ROLE: For critical operations (setParaswapV6, setOperator) - requires timelock
-
SafeDebtManager.sol: The main contract that orchestrates the debt switching process using flash loans.
-
Debt Handlers (
contracts/debt/handlers/): Individual handlers for each supported lending protocol, all implementingIDebtHandlerand extendingBaseDebtHandler.sol:AaveV3DebtHandler.sol: Handles interactions with Aave V3 protocolCompoundDebtHandler.sol: Handles interactions with Compound protocolMorphoDebtHandler.sol: Handles interactions with Morpho protocolMoonwellDebtHandler.sol: Handles interactions with Moonwell protocolFluidSafeDebtHandler.sol: Handles interactions with Fluid protocol through Safe
-
Safe Modules: Modules for Gnosis Safe integration:
SafeDebtManager.sol: Enables debt swaps through Gnosis Safe- Both operator-initiated and Safe owner-initiated transactions supported
-
LeveragedPosition.sol: Facilitates creation of leveraged positions across protocols.
-
RatehopperUniV3Positions.sol: Standalone Gnosis Safe module for Uniswap V3 WETH/USDC LP lifecycle. Three external entry points:
openLp()— splits the Safe's USDC, swaps half to WETH via the pinned SwapRouter02, mints a Uniswap V3 LP NFT on the Safe.closeLp()— partial or full unwind (exitBps): harvests accrued fees,decreaseLiquidity, collects principal, optionallyburns, swaps the WETH leg back to USDC.collectLp()— mid-position fee harvest with no decrease/burn.
Caller passes per-call
swapAmountOutMinanddeadline(audit fixes C-01 / H-02). The constructor rejects any non-WETH/USDC token pair (M-08). Performance fee is charged on net profit (currentValueUsd6 - basisUsd6); fee-collect is charged on accrued fees only. All fee setters are gated byCRITICAL_ROLEon a TimelockController (H-04);rescueTokenand similar emergency ops are gated byDEFAULT_ADMIN_ROLE. -
SafeYieldManager.sol + Yield Handlers (
contracts/yield/): Adapter-pattern successor to (6).SafeYieldManageris the single Safe module users enable; it owns basis bookkeeping (residualBasisUsd6Of, keyed by(uint8 protocolId, tokenId)), the performance fee, pause / per-protocol disable switches, and the timelocked setter surface. Protocol ids are plainuint8(not a Solidity enum) end-to-end, so a NEW protocol registers on the deployed manager viasetYieldHandler(id, handler)— followed by the pauser enabling open/close and the admin allow-listing pool params — with no redeploy; theYIELD_PROTOCOL_*constants inTypes.soljust document the canonical id assignment (append-only). Protocol mechanics live in stateless handlers executed via delegatecall:switchLp()atomically closes a full position and opens its replacement in another allowed pool/protocol, carrying only the cost basis attributable to value actually deployed into the new LP.BaseYieldHandler.sol— the sharedopenLp/closeLp/collectLpflow for V3-style CL protocols over ANY token pair; protocol diffs are isolated in virtual hooks (pool resolution, pair decoding,slot0read, swap calldata, mint calldata,positionsdecoding). USDC stays the sole funding/accounting currency: each non-USDC side of the pair is acquired/realized through its own USDCSwapLeg(a side that IS USDC needs no swap), so a WETH/USDC position swaps one leg and a WBTC/USDT-style position swaps both.V3StyleYieldHandler.sol— theBaseYieldHandlerhooks implemented once against the canonical Uniswap V3 interfaces (factorygetPoolbyuint24 feeTier, 7-fieldslot0, SwapRouter02), with the protocol id as a constructor argument so V3-shaped protocols and tests reuse the hook bodies.UniV3YieldHandler.sol(extendsV3StyleYieldHandler) /AerodromeYieldHandler.sol(extendsBaseYieldHandler) — concrete adapters holding protocol immutables.UniV4YieldHandler.sol— implementsIYieldHandlerdirectly (the V4 singleton/actions model doesn't fit the V3-shaped hooks): pool params are the full V4PoolKeytuple (keccak256(poolParam)IS the V4 PoolId), ERC20 sides route through two-step Permit2 approvals and UniversalRouter swaps, and native ETH pools (currency0 == address(0)) are supported.- Shared mutable state lives in an ERC-7201 namespace (
YieldStorage,ratehopper.storage.yield), so handler delegatecode can never collide with the manager's inherited storage. Handlers MUST NOT declare storage variables. - Pool selection params are ABI-encoded bytes carrying the PAIR plus the protocol key (
abi.encode(token0, token1, uint24 feeTier)for Uniswap V3,abi.encode(token0, token1, int24 tickSpacing)for Aerodrome), allow-listed bykeccak256(poolParam)— richer identifiers fit without interface changes (Uniswap V4 uses the fullPoolKeytuple). A protocol whose mechanics don't fitBaseYieldHandlerimplementsIYieldHandlerdirectly.
-
Morpho Libraries: Supporting libraries for the Morpho protocol:
MathLib.sol: Provides fixed-point arithmetic operations for the Morpho protocolSharesMathLib.sol: Handles share-to-asset conversion with virtual shares to protect against share price manipulations
Aave V3:
- Approve aToken when switching from Aave
- Approve debt delegation when switching to Aave
- Extra data:
"0x"
Compound V3:
- Call
allow()to authorize the SafeDebtManager contract - Extra data:
"0x"
Morpho:
- Call
setAuthorization(safeDebtManager, true) - Extra data: Encode
(MarketParams, borrowShares)- REQUIRED
Moonwell:
- No pre-approval required
- Extra data:
"0x"
Fluid:
- No pre-approval required
- Extra data: Encode
(vaultAddress, nftId, isFullRepay)- REQUIRED
- Flash Loans: Uses Uniswap V3 flash loans for atomic debt transfers
- Protocol Fee: Configurable fee (max 1%) taken from destination debt
- Slippage: Include
srcAmountwith slippage adjustment inParaswapParamsfor token swaps - Collateral: Automatically moved from source to destination protocol
- Amount: Use
MaxUint256for full debt repayment, or specify exact amount
executeDebtSwap: Main entry point for initiating a debt position transferuniswapV3FlashCallback: Handles the flash loan callback from Uniswap V3setProtocolFee: Sets the protocol fee percentage (basis points)setFeeBeneficiary: Sets the address that receives protocol feesgetHandler: Retrieves the handler address for a specific protocolemergencyWithdraw: Allows the owner to withdraw tokens in case of emergency
Each debt handler implements the following key functions:
getDebtAmount: Retrieves current debt amount for a userswitchIn: Handles debt switching within the same protocolswitchFrom: Handles debt repayment on the original protocolswitchTo: Handles borrowing on the new protocolrepay: Handles repayment of remaining balances
To execute a debt swap, you'll need to provide the following parameters:
function executeDebtSwap(
address _flashloanPool, // Uniswap V3 pool address for flash loan
DebtProtocol _fromProtocol, // Source protocol enum (AAVE_V3, COMPOUND, MORPHO, FLUID, MOONWELL)
DebtProtocol _toProtocol, // Destination protocol enum
address _fromDebtAsset, // Debt asset address on source protocol
address _toDebtAsset, // Debt asset address on destination protocol
uint256 _amount, // Amount to swap (use type(uint256).max for full debt)
CollateralAsset[] calldata _collateralAssets, // Array of collateral assets
address _onBehalfOf, // Safe address the swap is executed for
bytes[2] calldata _extraData, // [fromExtraData, toExtraData] for the two protocols
ParaswapParams calldata _paraswapParams // Paraswap parameters for token swaps
)struct CollateralAsset {
address asset; // Collateral asset address
uint256 amount; // Collateral amount
}struct ParaswapParams {
uint256 srcAmount; // Source amount with slippage adjustment (for token swaps)
bytes swapData; // Encoded swap data from Paraswap API
}Create a .env file with the following required variables (use .env.sample as a template):
# Core deploy
ADMIN_ADDRESS=0x... # Initial admin and timelock proposer/executor (used by all deploy modules)
SAFE_OPERATOR_ADDRESS=0x... # Operator address for Safe interactions
PAUSER_ADDRESS=0x... # Address that can pause contracts
DEPLOYER_PRIVATE_KEY=... # Private key for deployment
EXPLORER_KEY=... # Block explorer API key for verification
BASE_FORK_BLOCK_NUMBER=49470000 # Optional deterministic fork block for CI
# Yield deploy config (deploy:2_yield_manager)
# Resolution order: SYM_* module override → shared unprefixed name → legacy
# RHP_* fallback (addresses only) → default. Empty values (X=) count as
# unset and fall through.
TREASURY=0x... # Fee treasury for all yield modules. REQUIRED.
REGISTRY=0x... # Optional. Falls back to PROTOCOL_REGISTRY_ADDRESS in contractAddresses.ts
INITIAL_ADMIN=0x... # Optional. DEFAULT_ADMIN_ROLE holder. Falls back to ADMIN_ADDRESS
RHP_TIMELOCK=0x... # Optional. Reuse an existing TimelockController for the yield module only
# (module override: SYM_TIMELOCK). The registry deploy uses REGISTRY_TIMELOCK.
PERFORMANCE_FEE_BPS=1000 # Optional. Performance fee on profit at closeLp (bps). Default 1000 (10%)
FEE_COLLECT_BPS=250 # Optional. Fee on accrued LP fees (bps). Default 250 (2.5%)
MAX_FEE_BPS=2000 # Optional. Hard upper bound on BOTH fees (bps). Default 2000 (20%)
MIN_POSITION_LIQUIDITY=10000 # Optional. Floor on NPM mint liquidity. Default 10000
MIN_POOL_LIQUIDITY=0 # Optional. Floor on pool.liquidity() for spot-price reads. Default 0 (disabled)
# Optional — TimelockController sub-module (shared by all deploys)
TIMELOCK_ADMIN=0x... # Proposer + executor on the new timelock. Falls back to ADMIN_ADDRESS
TIMELOCK_DELAY=172800 # Min delay before queued ops execute (seconds). Default 172800 (2 days)
# Optional — per-module overrides: the same suffix with the module prefix wins
# over the shared name, e.g. SYM_TREASURY / RHP_MAX_FEE_BPS.
# SafeYieldManager-specific:
SYM_PAUSER=0x... # Pauser (pause / per-protocol disable). Falls back to PAUSER_ADDRESS, then ADMIN_ADDRESS
SYM_UNIV3_MIN_POSITION_LIQUIDITY=10000 # Per-protocol floors; fall back to MIN_POSITION_LIQUIDITY /
SYM_AERODROME_MIN_POSITION_LIQUIDITY=10000 # MIN_POOL_LIQUIDITY, then the defaults
SYM_UNIV3_MIN_POOL_LIQUIDITY=0 # Set independently after measuring the target Uni V3 pool
SYM_AERODROME_MIN_POOL_LIQUIDITY=0 # Set independently after measuring the target Slipstream pool
SYM_UNIV4_MIN_POSITION_LIQUIDITY=10000
SYM_UNIV4_MIN_POOL_LIQUIDITY=0 # Set independently after measuring the target V4 pool
# Optional Uniswap V4 address overrides — default to the canonical Base
# addresses in contractAddresses.ts; only set when targeting another network.
# SYM_UNIV4_POSITION_MANAGER=0x... # V4 PositionManager
# SYM_UNIVERSAL_ROUTER=0x... # UniversalRouter
# SYM_PERMIT2=0x... # Permit2 (Base uses 0x...B43aC78BA3)
# SYM_UNIV4_STATE_VIEW=0x... # StateView lens- Install dependencies:
yarn install- Compile contracts:
yarn compile- Run tests:
yarn testThe project uses:
- Solidity version 0.8.28
- Hardhat for development and testing
- Hardhat Ignition for deployments
- OpenZeppelin contracts for standard implementations
- Uniswap V3 for flash loans
- Paraswap for token swaps
Comprehensive tests are available in the /test directory, organized by area (test/debt/, test/yield/, test/registry/, test/legacy/, plus shared fixtures in test/helpers/), covering:
- Individual debt protocol handlers
- Cross-protocol debt switching flows
- Multiple collateral asset scenarios
- Safe module integration
- Leveraged position creation
- Yield stack:
SafeYieldManagerunit suites (mock-driven) and per-protocol fork suites (Uniswap V3, Aerodrome, Uniswap V4, cross-protocolswitchLp) - Legacy standalone yield module (
test/legacy/)
The *Fork.ts suites run against a Base mainnet fork and need BASE_RPC_URL (see Environment Variables).
Run tests with:
yarn testThe contracts use Hardhat Ignition for declarative deployments. Make sure you complete the sections Environment Variables and Setup and Development and make sure all tests pass before deploying.
Hardhat Ignition is a declarative deployment framework. Instead of writing imperative scripts that send transactions one by one, you define a module that describes what to deploy and the dependencies between steps. Ignition then:
- Resolves the dependency graph and executes steps in the correct order
- Tracks state in
ignition/deployments/chain-<chainId>/so deployments can be resumed if interrupted - Records constructor args in
journal.jsonlfor reproducibility and verification - Supports
--verifyto automatically submit contracts to Etherscan after deployment - Supports per-future wipes with
hardhat ignition wipe <deploymentId> <futureId>
The core contracts are defined in a single module at ignition/modules/1_DeployCore.ts. Every step is chained sequentially via after dependencies to avoid nonce race conditions.
ABI files for the five core integration contracts are exported to abis/ from Hardhat artifacts:
yarn abisThis compiles the contracts and writes:
abis/LeveragedPosition.jsonabis/RatehopperUniV3Positions.jsonabis/SafeDebtManager.jsonabis/SafeExecTransactionWrapper.jsonabis/SafeYieldManager.json
The deploy scripts below run the ABI exporter automatically after a successful deployment.
Deploy only ProtocolRegistry:
yarn deploy:0_registryThis deploys and configures ProtocolRegistry, syncs the registry address into contractAddresses.ts, and refreshes the ABI files in abis/.
Deploy the core debt-management contracts:
yarn deploy:1_coreThis deploys ignition/modules/1_DeployCore.ts to Base with verification enabled, reusing the configured registry from deploy:0_registry, syncs the registry address into contractAddresses.ts, and refreshes the ABI files in abis/.
This deploys all contracts sequentially in a single transaction chain:
- Configured ProtocolRegistry from
DeployRegistryOnly - Handlers: AaveV3 → Compound → Morpho → FluidSafe → Moonwell
- SafeDebtManager →
transferOwnershiptoADMIN_ADDRESS - LeveragedPosition →
transferOwnershiptoADMIN_ADDRESS - SafeExecTransactionWrapper
Deploys the adapter-pattern yield stack: UniV3YieldHandler + AerodromeYieldHandler + UniV4YieldHandler + SafeYieldManager.
yarn deploy:2_yield_managerThis deploys ignition/modules/2_DeployYieldManager.ts to Base with verification enabled and refreshes the ABI files in abis/.
The module by default deploys:
- TimelockController (shared
TimelockControllerModule; skipped whenSYM_TIMELOCK/RHP_TIMELOCKis set) - UniV3YieldHandler, AerodromeYieldHandler and UniV4YieldHandler (stateless delegatecall targets pinned to the canonical Base position-manager / factory / router / Permit2 / USDC addresses)
- SafeYieldManager with all three handlers registered, protocols enabled, and default pool-param allow-lists seeded (WETH/USDC on Uniswap V3 fee tiers
{100, 500, 3000}and Aerodrome tick spacings{100, 200}; the hookless native ETH/USDC 0.05% pool on Uniswap V4). Additional pools are allow-listed post-deploy viasetPoolParamAllowed. Hooked V4 pool keys are admitted only through the timelockedallowHookedPoolParamafter the hook has been reviewed (routinesetPoolParamAllowedrefuses them), and an exit swap leg may route through a hooked pool only if it is allow-listed. De-listing a pool param stops NEW opens through it only; existing positions keep every exit (closeLp,collectLp,withdrawLp) — exit swap legs are validated for pair shape, theminPoolLiquidityfloor and the reference-TWAP floor, not for allow-list membership.
Coexistence note: the standalone RatehopperUniV3Positions deployment keeps serving positions opened through it. SafeYieldManager rejects those tokenIds (UnknownPosition) and vice versa — there is no basis migration; legacy positions drain naturally via the legacy module.
After deployment, Ignition saves state to:
ignition/deployments/<deployment-id>/
├── deployed_addresses.json # All contract addresses
├── journal.jsonl # Full deployment log (includes constructor args)
└── artifacts/ # Contract ABIs and build info
Inspect deployed addresses:
cat ignition/deployments/chain-8453/deployed_addresses.jsonIgnition state is intentionally gitignored. Every deploy command also syncs a
stable, reviewable public manifest to deployments/base.json; commit that file
after a production deployment so downstream consumers do not depend on a local
Ignition directory. It can also be refreshed manually with:
yarn deployments:syncUse wipe:all when you want to clear all local Ignition state for Base and redeploy everything from scratch:
yarn wipe:allThis removes ignition/deployments/chain-8453. It does not delete on-chain contracts; it only resets this repo's local Ignition deployment journal and generated deployment artifacts for Base.
Shortcut scripts are available for common futures:
yarn wipe:leveraged-position
yarn wipe:safe-debt-manager
yarn wipe:safe-wrapper
yarn wipe:yield-managerIn short: use wipe:all for a clean redeploy of the whole Base deployment, and use wipe or a wipe:* shortcut only when you intentionally want to rerun one named future.
The --verify flag on yarn deploy:1_core may fail due to a known hardhat-verify v2.x bug with Etherscan's V2 API (the plugin's GET requests strip the chainid parameter). Use the standalone verification script instead:
yarn verify
# or equivalently:
npx hardhat run scripts/verifyAll.ts --network baseThis script:
- Reads deployed addresses from
ignition/deployments/chain-<chainId>/deployed_addresses.json - Reads constructor args from
journal.jsonl(no hardcoding needed) - Checks each contract via the Etherscan V2 API (skips already-verified)
- Submits unverified contracts via
hardhat verify - Polls the V2 API with retries (5 attempts, 10s apart) to confirm verification despite the plugin bug
Critical ProtocolRegistry setters (setParaswapV6, setOperator) carry CRITICAL_ROLE and revert unless msg.sender is the timelock, so they must be scheduled and executed through the TimelockController (2-day delay by default). Each script is a two-step flow: schedule, wait for the delay, then re-run with EXECUTE=true reusing the same OPERATION_ID printed during scheduling.
SafeDebtManager does not store the timelock itself — it only keeps an immutable registry reference, and the TimelockController address lives on the ProtocolRegistry (timelock() getter). So given a deployed SafeDebtManager, resolve the timelock by hopping through the registry.
On-chain (authoritative — this is the address the contracts actually enforce):
# 1. Read the registry from the deployed SafeDebtManager
REGISTRY=$(cast call <SAFE_DEBT_MANAGER_ADDRESS> "registry()(address)" --rpc-url <BASE_RPC_URL>)
# 2. Read the timelock from that registry
cast call $REGISTRY "timelock()(address)" --rpc-url <BASE_RPC_URL>SafeDebtManager.registry() and ProtocolRegistry.timelock() are both public getters, so any RPC reader (cast, ethers, a block explorer's "Read Contract" tab) works.
Off-chain (from this repo's Ignition deployment): the address is recorded under the TimelockControllerModule#TimelockController key — the timelock is a shared sub-module, so it keeps that stable ID regardless of which top-level module deployed it.
cat ignition/deployments/chain-8453/deployed_addresses.json
# → look for "TimelockControllerModule#TimelockController"Use the resulting address as TIMELOCK_ADDRESS in the commands below.
Updating the Paraswap address:
# Schedule operation (requires proposer role)
TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \
yarn hardhat run scripts/timelockUpdateParaswap.ts --network base
# Wait for the timelock delay, then execute
EXECUTE=true OPERATION_ID="..." TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \
yarn hardhat run scripts/timelockUpdateParaswap.ts --network baseUpdating the operator address:
# Schedule operation (requires proposer role)
TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \
yarn hardhat run scripts/timelockUpdateOperator.ts --network base
# Wait for the timelock delay, then execute (reuse the OPERATION_ID printed during scheduling)
EXECUTE=true OPERATION_ID="..." TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \
yarn hardhat run scripts/timelockUpdateOperator.ts --network baseOperational scripts for driving a deployed SafeYieldManager from a user's own Safe (the msg.sender == Safe path of onlyOperatorOrSafe) live in scripts/:
openLpBySafe.ts— opens an LP position (Uniswap V3 / Aerodrome / Uniswap V4 pool params supported)closeLpBySafe.ts— partial or full closecollectLpBySafe.ts— mid-position fee harvestswitchLpBySafe.ts— atomic move of a position to another allowed pool/protocollpSafeShared.ts— shared ABIs/helpers (no entry point)
Each script documents its configuration constants in its header comment; set SAFE_OWNER_PRIVATE_KEY (or DEPLOYER_PRIVATE_KEY) in .env and run with npx hardhat run scripts/<name>.ts --network base.
The contracts include several security features:
- Timelock Controller: 2-day delay by default for critical operations (Paraswap and operator updates)
- Hybrid Access Control:
DEFAULT_ADMIN_ROLE: For routine operations (immediate execution)CRITICAL_ROLE: For critical operations (requires timelock)
- Operator Authorization: Centralized operator management through ProtocolRegistry
- Both
SafeDebtManagerandLeveragedPositionread operator from registry - Supports both operator-initiated and Safe owner-initiated transactions
- Both
- Reentrancy Protection: All state-changing functions protected via OpenZeppelin's
ReentrancyGuard - Ownership Pattern: Uses OpenZeppelin's
Ownablefor administrative functions - Safe ERC20 Operations: Uses OpenZeppelin's
SafeERC20for secure token transfers - Flash Loan Validation: Validates Uniswap V3 pool callbacks via
CallbackValidation
- Authorization Check: Only authorized callers (operator or Safe itself) can execute operations
- Safe Multi-sig Support: Safe owners must use multi-sig process to manage positions
- No Individual Owner Calls: Individual Safe owners cannot call directly (prevents malicious contract exploits)
- Emergency Withdrawal: Owner can withdraw stuck tokens in emergency situations
- Protocol Fee Limits: Maximum fee capped at 1% (100 basis points)
- Pausable Contracts: Designated pauser can pause operations in emergency situations
- Input Validation: Comprehensive checks on all function parameters
- Whitelist System: Only whitelisted tokens can be used in the protocol
Business Source License 1.1 (BUSL-1.1)
Licensed under the Business Source License 1.1. After December 8, 2028 (4 years from initial release), the license converts to GPL-2.0-or-later.
See LICENSE for details.