Skip to main content

Overview

This guide covers deploying MetaVault AI smart contracts using Hardhat, including local development, testnet deployment, and mainnet deployment.

Prerequisites

Install Dependencies

cd packages/contracts
npm install

Key Dependencies

{
  "dependencies": {
    "@openzeppelin/contracts": "^5.4.0",
    "dotenv": "^17.2.3"
  },
  "devDependencies": {
    "hardhat": "^2.26.5",
    "@nomicfoundation/hardhat-toolbox": "^6.1.0",
    "@nomicfoundation/hardhat-ethers": "^3.1.2",
    "@nomicfoundation/hardhat-verify": "^2.1.3",
    "ethers": "^6.15.0",
    "typescript": "^5.9.3"
  }
}
Source: package.json

Environment Setup

Create a .env file in the contracts directory:
# Infura or Alchemy API key
INFURA_KEY=your_infura_project_id

# Private key for deployment (NEVER commit this!)
PRIVATE_KEY=your_private_key_without_0x

# Etherscan API key for contract verification
ETHERSCAN_API_KEY=your_etherscan_api_key
Never commit your .env file to version control. Add it to .gitignore.

Hardhat Configuration

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
dotenv.config();

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.28",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      viaIR: true
    }
  },
  networks: {
    hardhat: {
      forking: {
        url: `https://sepolia.infura.io/v3/${process.env.INFURA_KEY}`,
      }
    },
    sepolia: {
      url: `https://sepolia.infura.io/v3/${process.env.INFURA_KEY}`,
      accounts: [process.env.PRIVATE_KEY!]
    },
    localhost: {
      url: "http://127.0.0.1:8545"
    }
  }
};

export default config;
Source: hardhat.config.ts:1-34

Hardhat Commands

Compile Contracts

npx hardhat compile
This compiles all Solidity contracts and generates TypeChain typings.

Run Tests

npx hardhat test

Run Local Node

npx hardhat node
Starts a local Ethereum node with mainnet forking.

Deploy to Network

npx hardhat run scripts/deploy_vault_dashboard.ts --network sepolia

Deployment Script

The main deployment script deploys the Vault and integrates with the web app:
import { ethers, artifacts } from "hardhat";
import * as fs from "fs";
import * as path from "path";

async function main() {
    const [deployer] = await ethers.getSigners();
    console.log("Deploying contracts with the account:", deployer.address);

    // 1. Use Real LINK Token (Sepolia)
    const linkAddress = "0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5";
    console.log("Using existing LINK Token at:", linkAddress);

    // 2. Deploy Vault
    const feeRecipient = deployer.address;
    const performanceFeeBps = 1000; // 10%
    const withdrawFeeBps = 100; // 1%

    const Vault = await ethers.getContractFactory("Vault");
    const vault = await Vault.deploy(
        linkAddress,
        feeRecipient,
        performanceFeeBps,
        withdrawFeeBps
    );
    await vault.waitForDeployment();
    const vaultAddress = await vault.getAddress();
    console.log("Vault deployed to:", vaultAddress);

    // 3. Write to Web App Constants
    const webAppDir = path.join(__dirname, "../../web/defi-portfolio-app");
    const libDir = path.join(webAppDir, "lib");

    if (!fs.existsSync(libDir)) {
        fs.mkdirSync(libDir, { recursive: true });
    }

    const vaultArtifact = artifacts.readArtifactSync("Vault");

    const constantsContent = `
export const VAULT_ADDRESS = "${vaultAddress}";
export const LINK_ADDRESS = "${linkAddress}";

export const VAULT_ABI = ${JSON.stringify(vaultArtifact.abi, null, 2)} as const;
export const LINK_ABI = ${JSON.stringify(linkAbi, null, 2)} as const;
`;

    fs.writeFileSync(path.join(libDir, "constants.ts"), constantsContent);
    console.log("Written constants to web app");
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
Source: scripts/deploy_vault_dashboard.ts:1-79

Deployment Steps

1. Local Development

Deploy to local Hardhat network with mainnet forking:
# Terminal 1: Start local node
npx hardhat node

# Terminal 2: Deploy contracts
npx hardhat run scripts/deploy_vault_dashboard.ts --network localhost

2. Testnet Deployment (Sepolia)

# Deploy to Sepolia
npx hardhat run scripts/deploy_vault_dashboard.ts --network sepolia
Sepolia LINK Token: 0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5

3. Full System Deployment

Deploy Vault, Router, and Strategies:
// 1. Deploy Vault
const Vault = await ethers.getContractFactory("Vault");
const vault = await Vault.deploy(
    assetAddress,      // LINK or USDC
    feeRecipient,      // Fee recipient address
    1000,              // 10% performance fee
    100                // 1% withdrawal fee
);
await vault.waitForDeployment();

// 2. Deploy Strategy Router
const StrategyRouter = await ethers.getContractFactory("StrategyRouter");
const router = await StrategyRouter.deploy(
    await vault.getAddress(),
    deployer.address   // Owner
);
await router.waitForDeployment();

// 3. Deploy Aave Strategy
const StrategyAaveV3 = await ethers.getContractFactory("StrategyAaveV3");
const aaveStrategy = await StrategyAaveV3.deploy(
    assetAddress,              // LINK
    await vault.getAddress(),
    await router.getAddress(),
    aavePoolAddress,           // Aave V3 Pool
    dataProviderAddress        // Aave Data Provider
);
await aaveStrategy.waitForDeployment();

// 4. Deploy Leverage Strategy
const StrategyAaveLeverage = await ethers.getContractFactory("StrategyAaveLeverage");
const leverageStrategy = await StrategyAaveLeverage.deploy(
    assetAddress,              // LINK
    await vault.getAddress(),
    await router.getAddress(),
    aavePoolAddress,
    dataProviderAddress,
    swapRouterAddress,         // Uniswap V2 Router
    wethAddress,               // WETH
    oracleAddress              // Price Oracle
);
await leverageStrategy.waitForDeployment();

// 5. Configure Router with Strategies
await router.setStrategies(
    [await aaveStrategy.getAddress(), await leverageStrategy.getAddress()],
    [7000, 3000]  // 70% Aave, 30% Leverage
);

// 6. Connect Vault to Router
await vault.setRouter(await router.getAddress());

Contract Addresses

Sepolia Testnet

ContractAddress
LINK Token0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5
Aave V3 Pool0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951
Aave Data Provider0x3e9708d80f7B3e43118013075F7e95CE3AB31F31

Verify Contracts on Etherscan

npx hardhat verify --network sepolia <CONTRACT_ADDRESS> <CONSTRUCTOR_ARG1> <CONSTRUCTOR_ARG2> ...
Example:
npx hardhat verify --network sepolia 0x1234... \
  "0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5" \
  "0xYourFeeRecipient" \
  1000 \
  100

Post-Deployment Configuration

1. Fund Strategies

// Approve router to move funds
await token.approve(vaultAddress, ethers.parseEther("1000"));

// Deposit into vault
await vault.deposit(ethers.parseEther("1000"));

// Move funds to strategies
await router.moveFundsToStrategy(
    aaveStrategyAddress,
    ethers.parseEther("700")
);

2. Test Operations

// Check strategy balances
const aaveBalance = await aaveStrategy.strategyBalance();
const leverageBalance = await leverageStrategy.strategyBalance();

// Harvest profits
await router.harvestAll();

// Rebalance portfolio
await router.rebalance();

3. Monitor Leverage

// Check leverage state
const { deposited_, borrowed_, netExposure } = 
    await leverageStrategy.getLeverageState();

// Check LTV
const ltv = await leverageStrategy.getLTV();

// Check if at risk
const atRisk = await leverageStrategy.isAtRisk(ethers.parseEther("0.75"));

// Deleverage if needed
if (atRisk) {
    await router.triggerDeleverage(leverageStrategyAddress, 5);
}

Gas Optimization

The contracts use several gas optimization techniques:
  1. Compiler Optimization: 200 runs for balanced optimization
  2. Via IR: Enabled for better optimization
  3. Immutable Variables: Used for addresses that never change
  4. Packed Storage: Careful ordering of state variables
  5. Short-Circuit Logic: Early returns to save gas
solidity: {
  version: "0.8.28",
  settings: {
    optimizer: {
      enabled: true,
      runs: 200,
    },
    viaIR: true
  }
}
Source: hardhat.config.ts:7-15

Testing on Forked Network

Test against real protocols using Hardhat forking:
networks: {
  hardhat: {
    forking: {
      url: `https://sepolia.infura.io/v3/${process.env.INFURA_KEY}`,
    }
  }
}
Source: hardhat.config.ts:18-23 Run tests:
npx hardhat test

Deployment Checklist

  • Compile contracts: npx hardhat compile
  • Run tests: npx hardhat test
  • Configure .env with API keys and private key
  • Deploy Vault contract
  • Deploy StrategyRouter contract
  • Deploy Strategy contracts
  • Configure router with strategies and allocations
  • Connect vault to router
  • Verify contracts on Etherscan
  • Test deposit/withdraw flow
  • Test strategy operations (invest, harvest, rebalance)
  • Monitor gas costs and optimize if needed
  • Document deployed addresses
  • Update frontend with contract addresses and ABIs

Common Issues

Issue: Transaction Reverts

Solution: Check that:
  • Wallet has enough ETH for gas
  • Token approvals are set
  • Constructor arguments are correct
  • Network is correctly configured

Issue: Contract Not Verified

Solution: Run verify command with exact constructor arguments:
npx hardhat verify --network sepolia <ADDRESS> "<ARG1>" "<ARG2>"

Issue: Insufficient Liquidity

Solution: For leverage strategies on testnets:
  • Use smaller amounts
  • Reduce borrowFactor
  • Decrease maxDepth
  • Check pool liquidity before borrowing

Security Considerations

  1. Private Keys: Never commit or share private keys
  2. Constructor Arguments: Double-check all deployment parameters
  3. Access Control: Verify owner addresses are correct
  4. Fee Limits: Set reasonable fee percentages
  5. Strategy Validation: Test strategies thoroughly before deploying
  6. Upgrade Path: Consider proxy patterns for upgradeable contracts
  7. Timelock: Add timelock for sensitive operations on mainnet

Mainnet Deployment

Mainnet deployment requires extra caution. Consider:
  • Professional audit
  • Bug bounty program
  • Gradual rollout with caps
  • Multi-sig for admin functions
  • Emergency pause mechanism

Mainnet Checklist

  • Complete professional security audit
  • Deploy to testnet and run for at least 1 week
  • Set up monitoring and alerting
  • Prepare emergency response plan
  • Use multi-sig wallet for ownership
  • Set initial deposit caps
  • Announce deployment and risk disclosures

Next Steps

Build docs developers (and LLMs) love