Skill: chainyoda/aaveclaw (version 1.0.0)

--- name: aaveclaw description: Aave V3 lending protocol on Base Sepolia testnet. Deposit WETH collateral, borrow USDC, repay loans, withdraw collateral, check health factor, and mint test tokens via faucet. Use when users want to interact with Aave lending, check their lending position health, or get testnet tokens. --- # aaveclaw - Aave V3 Lending on Base Sepolia Interact with Aave V3 lending protocol on Base Sepolia testnet. Manages the full lending lifecycle using the wallet from `~/.x402-config.json`. ## Setup Run `setup.sh` on first use to install dependencies (ethers v6): ``` bash scripts/setup.sh ``` ## Commands ### Check Health Factor Check the current lending position. Safe to run anytime, read-only. ``` bash scripts/health.sh [address] ``` If no address is provided, uses the configured wallet address. ### Mint Test Tokens (Faucet) Get testnet WETH or USDC from the Aave faucet. Run this first if the wallet has no tokens. ``` bash scripts/faucet.sh weth 1 # Mint 1 WETH bash scripts/faucet.sh usdc 1000 # Mint 1000 USDC ``` ### Deposit Collateral Deposit WETH as collateral into Aave. Auto-wraps native ETH to WETH if needed. ``` bash scripts/deposit.sh 0.5 # Deposit 0.5 WETH ``` ### Borrow USDC Borrow USDC against deposited collateral. Uses variable interest rate. ``` bash scripts/borrow.sh 100 # Borrow 100 USDC ``` ### Repay Debt Repay borrowed USDC. Use "max" to repay entire debt. ``` bash scripts/repay.sh 50 # Repay 50 USDC bash scripts/repay.sh max # Repay all debt ``` ### Withdraw Collateral Withdraw WETH collateral. Use "max" to withdraw everything (only if no debt). ``` bash scripts/withdraw.sh 0.5 # Withdraw 0.5 WETH bash scripts/withdraw.sh max # Withdraw all ``` ## Usage Guidelines - **Always run `health.sh` first** to see the current position before making changes. - **Ask the user for amounts** before executing deposit, borrow, repay, or withdraw. - **Always show the health factor** after any state-changing operation (the scripts do this automatically). - **Warn when health factor drops below 1.5** - the position is at risk of liquidation. - **Guide new users to the faucet** to get test tokens before depositing. - **Typical flow**: faucet (get tokens) -> deposit (add collateral) -> borrow (take loan) -> repay (pay back) -> withdraw (retrieve collateral). ## Network Details - **Network**: Base Sepolia (chain ID 84532) - **Explorer**: https://sepolia.basescan.org - **RPC**: https://sepolia.base.org - **Tokens**: WETH (18 decimals), USDC (6 decimals) ## Error Handling - If private key is missing: direct user to create `~/.x402-config.json` with `{"private_key": "0x..."}` - If insufficient balance: the scripts report exact balances and what is needed - If health factor would drop too low after borrow: Aave reverts the transaction automatically - If faucet fails: the faucet contract may have minting limits or may not be available

path: package-lock.json
content: {
  "name": "aaveclaw",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "aaveclaw",
      "version": "1.0.0",
      "dependencies": {
        "ethers": "^6.13.0"
      }
    },
    "node_modules/@adraffy/ens-normalize": {
      "version": "1.10.1",
      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@noble/curves": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@noble/hashes": "1.3.2"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@noble/hashes": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@types/node": {
      "version": "22.7.5",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.19.2"
      }
    },
    "node_modules/aes-js": {
      "version": "4.0.0-beta.5",
      "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ethers": {
      "version": "6.16.0",
      "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/ethers-io/"
        },
        {
          "type": "individual",
          "url": "https://www.buymeacoffee.com/ricmoo"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "@adraffy/ens-normalize": "1.10.1",
        "@noble/curves": "1.2.0",
        "@noble/hashes": "1.3.2",
        "@types/node": "22.7.5",
        "aes-js": "4.0.0-beta.5",
        "tslib": "2.7.0",
        "ws": "8.17.1"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/tslib": {
      "version": "2.7.0",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "0BSD"
    },
    "node_modules/undici-types": {
      "version": "6.19.8",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ws": {
      "version": "8.17.1",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: package.json
content: {
  "name": "aaveclaw",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "ethers": "^6.13.0"
  }
}


path: scripts/borrow.sh
content: #!/bin/bash
set -e

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$SKILL_DIR/node_modules" ]; then
  bash "$SKILL_DIR/scripts/setup.sh"
fi

node "$SKILL_DIR/scripts/entries/borrow.js" "$@"


path: scripts/deposit.sh
content: #!/bin/bash
set -e

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$SKILL_DIR/node_modules" ]; then
  bash "$SKILL_DIR/scripts/setup.sh"
fi

node "$SKILL_DIR/scripts/entries/deposit.js" "$@"


path: scripts/entries/borrow.js
content: import { borrow } from "../lib/aave.js";

const amount = process.argv[2];
if (!amount) {
  console.error("Usage: node borrow.js <amount_usdc>");
  console.error("Example: node borrow.js 100");
  process.exit(1);
}

try {
  await borrow(amount);
} catch (err) {
  console.error(`Error: ${err.message}`);
  process.exit(1);
}


path: scripts/entries/deposit.js
content: import { deposit } from "../lib/aave.js";

const amount = process.argv[2];
if (!amount) {
  console.error("Usage: node deposit.js <amount_eth>");
  console.error("Example: node deposit.js 0.5");
  process.exit(1);
}

try {
  await deposit(amount);
} catch (err) {
  console.error(`Error: ${err.message}`);
  process.exit(1);
}


path: scripts/entries/faucet.js
content: import { mintTestTokens } from "../lib/aave.js";

const [REDACTED_SECRET];
const amount = process.argv[3];

if (!token || !amount) {
  console.error("Usage: node faucet.js <weth|usdc> <amount>");
  console.error("Example: node faucet.js weth 1");
  console.error("Example: node faucet.js usdc 1000");
  process.exit(1);
}

try {
  await mintTestTokens(token, amount);
} catch (err) {
  console.error(`Error: ${err.message}`);
  process.exit(1);
}


path: scripts/entries/health.js
content: import { getHealthFactor } from "../lib/aave.js";

const address = process.argv[2] || undefined;

try {
  await getHealthFactor(address);
} catch (err) {
  console.error(`Error: ${err.message}`);
  process.exit(1);
}


path: scripts/entries/repay.js
content: import { repay } from "../lib/aave.js";

const amount = process.argv[2];
if (!amount) {
  console.error("Usage: node repay.js <amount_usdc|max>");
  console.error("Example: node repay.js 50");
  console.error("Example: node repay.js max");
  process.exit(1);
}

try {
  await repay(amount);
} catch (err) {
  console.error(`Error: ${err.message}`);
  process.exit(1);
}


path: scripts/entries/withdraw.js
content: import { withdraw } from "../lib/aave.js";

const amount = process.argv[2];
if (!amount) {
  console.error("Usage: node withdraw.js <amount_eth|max>");
  console.error("Example: node withdraw.js 0.5");
  console.error("Example: node withdraw.js max");
  process.exit(1);
}

try {
  await withdraw(amount);
} catch (err) {
  console.error(`Error: ${err.message}`);
  process.exit(1);
}


path: scripts/faucet.sh
content: #!/bin/bash
set -e

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$SKILL_DIR/node_modules" ]; then
  bash "$SKILL_DIR/scripts/setup.sh"
fi

node "$SKILL_DIR/scripts/entries/faucet.js" "$@"


path: scripts/health.sh
content: #!/bin/bash
set -e

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$SKILL_DIR/node_modules" ]; then
  bash "$SKILL_DIR/scripts/setup.sh"
fi

node "$SKILL_DIR/scripts/entries/health.js" "$@"


path: scripts/lib/aave.js
content: import { ethers } from "ethers";
import { ADDRESSES, getSigner, getProvider } from "./config.js";
import { txLink, printAccountSummary } from "./utils.js";

// Human-readable ABI fragments for Aave V3 Pool
const POOL_ABI = [
  "function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)",
  "function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf)",
  "function repay(address asset, uint256 amount, uint256 interestRateMode, address onBehalfOf) returns (uint256)",
  "function withdraw(address asset, uint256 amount, address to) returns (uint256)",
  "function getUserAccountData(address user) view returns (uint256 totalCollateralBase, uint256 totalDebtBase, uint256 availableBorrowsBase, uint256 currentLiquidationThreshold, uint256 ltv, uint256 healthFactor)",
];

const ERC20_ABI = [
  "function balanceOf(address owner) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "function decimals() view returns (uint8)",
];

const WETH_ABI = [...ERC20_ABI, "function deposit() payable"];

const FAUCET_ABI = ["function mint(address token, address to, uint256 amount)"];

// Variable rate mode (stable rate is deprecated in Aave V3)
const VARIABLE_RATE = 2;

async function ensureAllowance(tokenContract, spender, amount, signer) {
  const current = await tokenContract.allowance(signer.address, spender);
  if (current < amount) {
    console.log("Approving token spend...");
    const tx = await tokenContract.approve(spender, ethers.MaxUint256);
    await tx.wait();
    console.log(`Approval tx: ${txLink(tx.hash)}`);
  }
}

export async function getAccountData(address) {
  const provider = getProvider();
  const pool = new ethers.Contract(ADDRESSES.pool, POOL_ABI, provider);
  const result = await pool.getUserAccountData(address);
  return {
    totalCollateralBase: result[0],
    totalDebtBase: result[1],
    availableBorrowsBase: result[2],
    currentLiquidationThreshold: result[3],
    ltv: result[4],
    healthFactor: result[5],
  };
}

export async function deposit(amountEth) {
  const signer = getSigner();
  const amount = ethers.parseEther(amountEth);

  const weth = new ethers.Contract(ADDRESSES.weth, WETH_ABI, signer);
  const pool = new ethers.Contract(ADDRESSES.pool, POOL_ABI, signer);

  // Check WETH balance, auto-wrap ETH if needed
  const wethBalance = await weth.balanceOf(signer.address);
  if (wethBalance < amount) {
    const shortfall = amount - wethBalance;
    const ethBalance = await signer.provider.getBalance(signer.address);
    if (ethBalance < shortfall) {
      throw new Error(
        `Insufficient balance. Need ${ethers.formatEther(shortfall)} more WETH (or native ETH to wrap). ` +
          `WETH balance: ${ethers.formatEther(wethBalance)}, ETH balance: ${ethers.formatEther(ethBalance)}`,
      );
    }
    console.log(`Wrapping ${ethers.formatEther(shortfall)} ETH to WETH...`);
    const wrapTx = await weth.deposit({ value: shortfall });
    await wrapTx.wait();
    console.log(`Wrap tx: ${txLink(wrapTx.hash)}`);
  }

  // Approve Pool to spend WETH
  await ensureAllowance(weth, ADDRESSES.pool, amount, signer);

  // Supply WETH to Aave
  console.log(`Depositing ${amountEth} WETH as collateral...`);
  const tx = await pool.supply(ADDRESSES.weth, amount, signer.address, 0);
  const receipt = await tx.wait();
  console.log(`Deposit tx: ${txLink(tx.hash)}`);

  const data = await getAccountData(signer.address);
  printAccountSummary(data);
  return { tx: tx.hash, data };
}

export async function borrow(amountUsdc) {
  const signer = getSigner();
  const amount = ethers.parseUnits(amountUsdc, 6); // USDC has 6 decimals

  const pool = new ethers.Contract(ADDRESSES.pool, POOL_ABI, signer);

  // Check available borrows
  const accountData = await getAccountData(signer.address);
  if (accountData.availableBorrowsBase === 0n) {
    throw new Error("No borrowing power. Deposit collateral first.");
  }

  console.log(`Borrowing ${amountUsdc} USDC...`);
  const tx = await pool.borrow(ADDRESSES.usdc, amount, VARIABLE_RATE, 0, signer.address);
  const receipt = await tx.wait();
  console.log(`Borrow tx: ${txLink(tx.hash)}`);

  const data = await getAccountData(signer.address);
  printAccountSummary(data);
  return { tx: tx.hash, data };
}

export async function repay(amountOrMax) {
  const signer = getSigner();
  const isMax = amountOrMax.toLowerCase() === "max";
  const amount = isMax ? ethers.MaxUint256 : ethers.parseUnits(amountOrMax, 6);

  const usdc = new ethers.Contract(ADDRESSES.usdc, ERC20_ABI, signer);
  const pool = new ethers.Contract(ADDRESSES.pool, POOL_ABI, signer);

  // Check USDC balance (unless max, which Aave handles)
  if (!isMax) {
    const balance = await usdc.balanceOf(signer.address);
    if (balance < amount) {
      throw new Error(
        `Insufficient USDC balance. Have ${ethers.formatUnits(balance, 6)}, need ${amountOrMax}`,
      );
    }
  }

  // Approve Pool to spend USDC
  await ensureAllowance(usdc, ADDRESSES.pool, amount, signer);

  const label = isMax ? "all" : amountOrMax;
  console.log(`Repaying ${label} USDC debt...`);
  const tx = await pool.repay(ADDRESSES.usdc, amount, VARIABLE_RATE, signer.address);
  const receipt = await tx.wait();
  console.log(`Repay tx: ${txLink(tx.hash)}`);

  const data = await getAccountData(signer.address);
  printAccountSummary(data);
  return { tx: tx.hash, data };
}

export async function withdraw(amountOrMax) {
  const signer = getSigner();
  const isMax = amountOrMax.toLowerCase() === "max";
  const amount = isMax ? ethers.MaxUint256 : ethers.parseEther(amountOrMax);

  const pool = new ethers.Contract(ADDRESSES.pool, POOL_ABI, signer);

  const label = isMax ? "all" : `${amountOrMax} WETH`;
  console.log(`Withdrawing ${label} from Aave...`);
  const tx = await pool.withdraw(ADDRESSES.weth, amount, signer.address);
  const receipt = await tx.wait();
  console.log(`Withdraw tx: ${txLink(tx.hash)}`);

  const data = await getAccountData(signer.address);
  printAccountSummary(data);
  return { tx: tx.hash, data };
}

export async function getHealthFactor(address) {
  const resolvedAddress = address || getSigner().address;
  const data = await getAccountData(resolvedAddress);
  console.log(`\nAccount: ${resolvedAddress}`);
  printAccountSummary(data);
  return data;
}

export async function mintTestTokens(token, amount) {
  const signer = getSigner();

  const tokenLower = token.toLowerCase();
  let tokenAddr, decimals;
  if (tokenLower === "weth") {
    tokenAddr = ADDRESSES.weth;
    decimals = 18;
  } else if (tokenLower === "usdc") {
    tokenAddr = ADDRESSES.usdc;
    decimals = 6;
  } else {
    throw new Error(`Unknown token "${token}". Supported: weth, usdc`);
  }

  const parsedAmount = ethers.parseUnits(amount, decimals);
  const faucet = new ethers.Contract(ADDRESSES.faucet, FAUCET_ABI, signer);

  console.log(`Minting ${amount} ${token.toUpperCase()} via faucet...`);
  const tx = await faucet.mint(tokenAddr, signer.address, parsedAmount);
  const receipt = await tx.wait();
  console.log(`Faucet tx: ${txLink(tx.hash)}`);

  // Show new balance
  const tokenContract = new ethers.Contract(tokenAddr, ERC20_ABI, signer.provider);
  const balance = await tokenContract.balanceOf(signer.address);
  console.log(`New ${token.toUpperCase()} balance: ${ethers.formatUnits(balance, decimals)}`);

  return { tx: tx.hash };
}


path: scripts/lib/config.js
content: import { ethers } from "ethers";
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

export const CHAIN_ID = 84532;
export const RPC_URL = "https://sepolia.base.org";
export const EXPLORER_URL = "https://sepolia.basescan.org";

export const ADDRESSES = {
  pool: [REDACTED_SECRET],
  weth: "0x4200000000000000000000000000000000000006",
  usdc: [REDACTED_SECRET],
  faucet: [REDACTED_SECRET],
};

const CONFIG_LOCATIONS = [
  join(process.cwd(), "x402-config.json"),
  join(homedir(), ".x402-config.json"),
];

export function loadPrivateKey() {
  // Environment variable takes precedence
  if (process.env.X402_PRIVATE_KEY) {
    return process.env.X402_PRIVATE_KEY;
  }

  for (const configPath of CONFIG_LOCATIONS) {
    try {
      const raw = readFileSync(configPath, "utf-8");
      const config = JSON.parse(raw);
      if (config.private_key) {
        return config.private_key;
      }
    } catch {
      // File not found or invalid JSON, try next
    }
  }

  throw new Error(
    'Private key not found. Set X402_PRIVATE_KEY env var or create ~/.x402-config.json with {"private_key": "0x..."}',
  );
}

export function getProvider() {
  return new ethers.JsonRpcProvider(RPC_URL, CHAIN_ID);
}

export function getSigner() {
  const provider = getProvider();
  const privateKey = loadPrivateKey();
  return new ethers.Wallet(privateKey, provider);
}


path: scripts/lib/utils.js
content: import { EXPLORER_URL } from "./config.js";

// Health factor is scaled by 1e18 in Aave
const HF_DECIMALS = 18n;

export function formatHealthFactor(hfRaw) {
  if (hfRaw === 0n) return "N/A (no debt)";

  // MaxUint256 means no debt
  if (hfRaw >= 2n ** 128n) return "N/A (no debt)";

  const hf = Number(hfRaw) / 1e18;
  const hfStr = hf.toFixed(4);

  let label;
  if (hf >= 3) label = "SAFE";
  else if (hf >= 2) label = "GOOD";
  else if (hf >= 1.5) label = "MODERATE";
  else if (hf >= 1.1) label = "WARNING";
  else label = "DANGER";

  return `${hfStr} [${label}]`;
}

// Aave returns USD values scaled by 1e8
export function formatUsdValue(valueRaw) {
  const value = Number(valueRaw) / 1e8;
  return `$${value.toFixed(2)}`;
}

export function txLink(hash) {
  return `${EXPLORER_URL}/tx/${hash}`;
}

export function printAccountSummary(data) {
  console.log("\n--- Aave V3 Account Summary (Base Sepolia) ---");
  console.log(`Total Collateral:    ${formatUsdValue(data.totalCollateralBase)}`);
  console.log(`Total Debt:          ${formatUsdValue(data.totalDebtBase)}`);
  console.log(`Available to Borrow: ${formatUsdValue(data.availableBorrowsBase)}`);
  console.log(`Current LTV:         ${Number(data.ltv) / 100}%`);
  console.log(`Liquidation Threshold: ${Number(data.currentLiquidationThreshold) / 100}%`);
  console.log(`Health Factor:       ${formatHealthFactor(data.healthFactor)}`);
  console.log("-----------------------------------------------\n");
}


path: scripts/repay.sh
content: #!/bin/bash
set -e

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$SKILL_DIR/node_modules" ]; then
  bash "$SKILL_DIR/scripts/setup.sh"
fi

node "$SKILL_DIR/scripts/entries/repay.js" "$@"


path: scripts/setup.sh
content: #!/bin/bash
set -e

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$SKILL_DIR/node_modules" ]; then
  echo "Installing aaveclaw dependencies..."
  cd "$SKILL_DIR"
  npm install --omit=dev
  echo "Dependencies installed."
else
  echo "Dependencies already installed."
fi


path: scripts/withdraw.sh
content: #!/bin/bash
set -e

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$SKILL_DIR/node_modules" ]; then
  bash "$SKILL_DIR/scripts/setup.sh"
fi

node "$SKILL_DIR/scripts/entries/withdraw.js" "$@"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: clean (LOW)