A non-custodial lending protocol focused on Real World Assets (RWAs). Users can supply yield-bearing tokenized assets as collateral and borrow stablecoins against them directly on supported EVM testnets.
The protocol consists of an upgradeable core lending pool implemented in Solidity and a custom Next.js frontend that interacts with it through a thin compatibility layer. It is designed for demonstration and testing of RWA collateral mechanics including collateralization ratios, health factor monitoring, liquidations, and simple interest accrual.
- Supply and withdraw of supported assets into the pool
- Borrow and repay against supplied collateral
- Per-asset risk parameters: Loan-to-Value (LTV), liquidation threshold, and liquidation bonus
- On-chain health factor calculation used to gate withdrawals and enable liquidations
- Simple linear interest accrual on borrowed positions
- User-controlled toggling of assets as collateral
- Protocol pause capability controlled by the owner
- Owner-only reserve initialization and price updates
- Upgradeable deployment using the Transparent Upgradeable Proxy pattern
- Support for multiple isolated markets (different chains and token sets)
All core lending logic resides in a single upgradeable contract called AetherPool.
Deployment Model
- The implementation contract (
AetherPool) is deployed behind aTransparentUpgradeableProxy. - A
ProxyAdmincontract manages upgrades. - Deployment orchestration uses Hardhat Ignition modules.
Storage and Configuration
reserveConfigsmapping stores per-asset parameters:ltv,liquidationThreshold,liquidationBonus(uint16, in basis points)supplyRateBps,borrowRateBpspriceUsd(uint128, scaled by 8 decimals)- Flags for
borrowingEnabledandcollateralEnabled
reserveListtracks initialized assets in order.- User positions are tracked directly:
userSupplies[user][asset]userDebt[user][asset]totalSupplied[asset]totalBorrowed[asset]
- No separate aToken or variable debt token contracts are used. Position balances are derived from the pool's internal accounting and the actual ERC-20 balance held by the pool.
Constants
BPS_DENOM = 10000BASE_CURRENCY_DECIMALS = 8(all base currency values such as collateral and debt are expressed in this scale)HEALTH_FACTOR_DECIMALS = 1e18CLOSE_FACTOR_BPS = 5000(50% partial liquidation)FULL_LIQUIDATION_HF_THRESHOLD = 0.95e18
Key Functions
initReserve(...): Owner-only. Registers a new asset with its risk parameters and initial price. Reads token decimals from the ERC-20.setAssetPrice(asset, priceUsd): Owner-only. Updates the on-chain price for a reserve (scaled by 8 decimals).supply(asset, amount, onBehalfOf, referralCode): Transfers tokens into the pool and creditsuserSupplies.withdraw(asset, amount, to): Checks health factor after reduction of collateral. Transfers tokens out.borrow(...): Accrues interest, checks LTV after the borrow, transfers tokens to the borrower. Only the borrower themselves is supported (no credit delegation).repay(...): Accrues interest then reduces debt.setUserUseReserveAsCollateral(...): Enables or disables an asset for a user as collateral. Disabling performs an HF check when the user has debt.liquidationCall(...): Allows a liquidator to repay debt on behalf of an undercollateralized user and seize collateral plus a bonus. Implements close factor logic.getUserAccountData(user): Returns base-currency totals and health factor. Used by both contracts and the UI.getAllReservesData()/getUserReservesData(user): View functions returning aggregated data consumed by the frontend.getAssetPrice(asset): Returns the raw on-chain price.
Interest Accrual Interest is simple (non-compounding) and calculated on demand:
interest = principal * borrowRateBps * (block.timestamp - lastUpdate) / (BPS_DENOM * SECONDS_PER_YEAR)Accrual is triggered inside _accrue during borrow and repay operations and updates both the user's debt and the global totalBorrowed.
Health Factor and Risk Math
All values in getUserAccountData are computed in the 8-decimal base currency:
- Collateral value = userSupply * priceUsd / 10^decimals
- Weighted LTV and liquidation threshold are value-weighted averages.
availableBorrowsBase = max(0, collateralBase * ltv / 10000 - debtBase)- Health factor (when debt > 0):
HF = (totalCollateralBase * currentLiquidationThreshold * 1e18) / (totalDebtBase * 10000)
Liquidation Math When a position is liquidatable (HF < 1e18):
- A bonus is applied:
collateralToSeize = (debtValue * (10000 + bonus)) / (price * 10000)(adjusted for decimals). - Partial liquidations are capped at the close factor (50%) unless the position is deeply underwater.
Upgradeability
The contract inherits Initializable, OwnableUpgradeable, and PausableUpgradeable. A storage gap (__gap) is reserved for future variable additions.
The web application is a Next.js 16 project located under the web/ directory.
Wallet and Chain Handling
- wagmi + viem + ConnectKit handle connections.
- Configuration is deliberately restricted to testnet chains only (
testnetChainsinui-config/wagmiConfig.ts). - A global fetch interceptor in
providers.tsxblocks attempts to reach known mainnet RPC endpoints (prevents accidental CORS errors and unwanted mainnet calls). - An optional server-side RPC proxy exists at
app/api/rpc/route.tsand can be used viaServerJsonRpcProviderwhen private keys or rate limits require it.
Protocol Compatibility Layer Because the UI components originate from an Aave-style interface, a compatibility shim exists:
protocol/rwaContracts.ts: Low-level wrappers aroundAetherPoolusing ethers. ProvidesfetchRwaReserves,fetchRwaUserAccountData, raw data transformation (toReserveDataHumanized), and ABIs.protocol/aave-compat.ts: Implements many of the interfaces expected by the UI (Pool, UiPoolDataProvider, formatReservesAndIncentives, markets(), etc.). It translates raw on-chain data into the shapes used for risk displays, modals, and tables. Supply/borrow/repay actions are encoded directly against the pool.protocol/currentDeployment.ts: Holds the active market configuration (chain ID, pool address, token metadata). The UI switches behavior based on the selected market.protocol/mathUtils.ts: Provides the normalization, ray math stubs, and user summary formatting used across the app.
Markets System
Markets are defined in ui-config/marketsConfig.tsx:
proto_arbitrum_sepolia: Arbitrum RWA market (mock Ondo-style tokens).proto_robinhood_rwa: Robinhood testnet market.
Each market points at its own pool address and token list. Deployment JSONs (arbitrumDeployment.json, robinhoodDeployment.json) contain the live addresses and are imported at build time.
Data Flow
- React Query + Zustand manage loading state.
- On market load or polling,
useMarketsDatacalls the compatibilitymarkets()function. - This triggers
fetchRwaReserves()→pool.getAllReservesData(). - Raw data is transformed and fed into tables, the dashboard, and reserve overview pages.
- User actions (supply, etc.) construct raw transaction data via the compat layer and are submitted through wagmi/viem.
- Local transactions are recorded in
store/transactionsSlice.tsand merged into the history view (hooks/useTransactionHistory.tsx).
Transaction Modals and Flows
The transaction components (Supply, Borrow, Repay, Withdraw, Collateral Change) are located under components/transactions/. They use:
useReserveActionState- Approval checks via
useApprovedAmount - Gas estimation and simulation where possible
- Health factor impact previews
Styling and UI Primitives
- MUI + Emotion + custom primitives (
FormattedNumber,ReserveOverviewBox, etc.). - Lingui is present for internationalization (largely unused beyond stubs).
- Many original Aave info tooltips and warnings are retained for risk communication.
- Vercel configuration (
vercel.jsonat repo root) setsrootDirectorytoweb, framework to Next.js, and a custom build command. - The web app can also be run locally with
yarn devinsideweb/. - There is a minimal indexer under
indexer/(The Graph subgraph skeleton) but the application does not depend on it; all data is fetched directly via RPC.
Chain ID: 421614
Tokens (see contracts/config/tokens-arbitrum.js and arbitrumDeployment.json for exact addresses):
- USDC (6 decimals) — stablecoin, borrowable, 0 LTV
- OUSG (18 decimals) — RWA collateral, LTV 75%, liquidation threshold 80%
- USDY (18 decimals) — RWA collateral, LTV 70%, liquidation threshold 75%
Prices are set on-chain at 8 decimal precision (e.g., USDY = 1.05 × 10^8).
Chain ID: 46630
Tokens include USDG (stable) and various stock RWAs (TSLA, AMZN, etc.). See robinhoodDeployment.json.
Token and risk parameters are defined in two places that must stay in sync:
contracts/config/tokens-*.js— used by init scripts (priceUsd here is the human value; scripts scale to 8 decimals).web/protocol/currentDeployment.ts— used for UI metadata and mock fallbacks (human prices).
After any new deployment or reserve initialization, run the appropriate scripts/init-reserves-*.js (or the sync scripts) and update the JSON deployment files consumed by the frontend.
- Node.js + Yarn
- A wallet with testnet funds (see Faucets)
- Private key with gas for deployments (Arbitrum Sepolia uses
DEPLOYER_PRIVATE_KEYenv var)
cd contracts
yarn install
# Compile
npx hardhat compile
# Deploy to Arbitrum Sepolia (tokens + pool)
npx hardhat ignition deploy ignition/modules/ArbitrumDeploymentModule.ts --network arbitrumSepolia
# Initialize reserves and prices (replace with actual proxy address)
POOL_ADDRESS=0x... npx hardhat run scripts/init-reserves-arbitrum.js --network arbitrumSepoliaMint scripts (scripts/mint-tokens.js) and similar exist for distributing test tokens to a target address.
cd web
yarn install
yarn devThe app connects to Arbitrum Sepolia by default. Use the market switcher (when available) or update the active deployment in currentDeployment / marketsConfig to target Robinhood.
Production build for Vercel is performed from the repository root with the root vercel.json directing it at the web subdirectory.
To operate reliably inside a browser environment without triggering CORS errors:
- Only testnet chains are registered with wagmi.
getTransportand explicit transport maps point exclusively at public testnet RPCs.- A defensive fetch wrapper intercepts and neuters requests that would target Ethereum mainnet endpoints.
- The server proxy (
/api/rpc) can forward selected methods when needed and currently falls back to public endpoints for known testnets when Alchemy keys are absent.
Located in contracts/scripts/:
init-reserves-arbitrum.js— registers assets and sets prices.mint-tokens.js— mints mock ERC-20s (for networks that support it).- Various sync scripts that copy addresses into the UI config JSON files.
- Simple interest instead of utilization-based or compounded rates.
- All positions are directly visible in the pool contract (no privacy or isolation beyond per-market configuration).
- Prices are explicitly set by the owner rather than pulled from an external oracle (appropriate for a controlled demo environment).
- No flash loans, no stable-rate borrowing mode, no rewards distribution in the core contract.
- The frontend reuses a large number of Aave-derived components and therefore carries some unused code paths (E-Mode, GHO, etc.) that are stubbed or disabled.
- Health factor and risk calculations match the formulas implemented inside
AetherPool.getUserAccountData.
aether/
├── contracts/
│ ├── contracts/AetherPool.sol
│ ├── ignition/modules/ # Hardhat Ignition deployment modules
│ ├── scripts/ # init-reserves, mint, sync helpers
│ ├── config/ # token metadata and prices
│ └── hardhat.config.js
├── web/
│ ├── app/ # Next.js App Router (pages, api routes)
│ ├── protocol/ # rwaContracts, aave-compat, math, deployment state
│ ├── ui-config/ # marketsConfig, networks, deployment JSONs, wagmi config
│ ├── components/ # UI (transactions, markets, reserve overview, etc.)
│ └── hooks/ # data fetching, transaction handling
├── indexer/ # (optional) subgraph skeleton
└── vercel.json # points deployment at web/
- Robinhood Testnet: https://faucet.testnet.chain.robinhood.com/
- Arbitrum Sepolia: Use any public Arbitrum Sepolia faucet (QuickNode, Chainlink, etc.)
Addresses are maintained in the JSON files under web/ui-config/ after each deployment:
- Arbitrum Sepolia pool and tokens:
arbitrumDeployment.json - Robinhood testnet pool and tokens:
robinhoodDeployment.json
The live frontend is typically served from a Vercel project pointing at the web directory.
MIT