Skip to content

duckling69/aether

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Aether

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.

Features

  • 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)

Architecture Overview

Smart Contracts

All core lending logic resides in a single upgradeable contract called AetherPool.

Deployment Model

  • The implementation contract (AetherPool) is deployed behind a TransparentUpgradeableProxy.
  • A ProxyAdmin contract manages upgrades.
  • Deployment orchestration uses Hardhat Ignition modules.

Storage and Configuration

  • reserveConfigs mapping stores per-asset parameters:
    • ltv, liquidationThreshold, liquidationBonus (uint16, in basis points)
    • supplyRateBps, borrowRateBps
    • priceUsd (uint128, scaled by 8 decimals)
    • Flags for borrowingEnabled and collateralEnabled
  • reserveList tracks 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 = 10000
  • BASE_CURRENCY_DECIMALS = 8 (all base currency values such as collateral and debt are expressed in this scale)
  • HEALTH_FACTOR_DECIMALS = 1e18
  • CLOSE_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 credits userSupplies.
  • 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.

Frontend

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 (testnetChains in ui-config/wagmiConfig.ts).
  • A global fetch interceptor in providers.tsx blocks 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.ts and can be used via ServerJsonRpcProvider when 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 around AetherPool using ethers. Provides fetchRwaReserves, 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

  1. React Query + Zustand manage loading state.
  2. On market load or polling, useMarketsData calls the compatibility markets() function.
  3. This triggers fetchRwaReserves()pool.getAllReservesData().
  4. Raw data is transformed and fed into tables, the dashboard, and reserve overview pages.
  5. User actions (supply, etc.) construct raw transaction data via the compat layer and are submitted through wagmi/viem.
  6. Local transactions are recorded in store/transactionsSlice.ts and 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.

Deployment and Infrastructure

  • Vercel configuration (vercel.json at repo root) sets rootDirectory to web, framework to Next.js, and a custom build command.
  • The web app can also be run locally with yarn dev inside web/.
  • 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.

Supported Markets and Tokens

Arbitrum Sepolia RWA Market (default)

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).

Robinhood Testnet Market

Chain ID: 46630

Tokens include USDG (stable) and various stock RWAs (TSLA, AMZN, etc.). See robinhoodDeployment.json.

Configuration

Token and risk parameters are defined in two places that must stay in sync:

  1. contracts/config/tokens-*.js — used by init scripts (priceUsd here is the human value; scripts scale to 8 decimals).
  2. 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.

Running the Project

Prerequisites

  • Node.js + Yarn
  • A wallet with testnet funds (see Faucets)
  • Private key with gas for deployments (Arbitrum Sepolia uses DEPLOYER_PRIVATE_KEY env var)

Contracts

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 arbitrumSepolia

Mint scripts (scripts/mint-tokens.js) and similar exist for distributing test tokens to a target address.

Frontend

cd web
yarn install
yarn dev

The 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.

RPC and Network Handling

To operate reliably inside a browser environment without triggering CORS errors:

  • Only testnet chains are registered with wagmi.
  • getTransport and 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.

Scripts

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.

Design Choices and Limitations

  • 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.

Project Directory Structure

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/

Faucets

Current Deployment Addresses

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.

License

MIT

About

Aether — non-custodial RWA lending protocol on Robinhood testnet and Arbitrum Sepolia

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages