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
This compiles all Solidity contracts and generates TypeChain typings.
Run Tests
Run Local 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
| Contract | Address |
|---|
| LINK Token | 0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5 |
| Aave V3 Pool | 0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951 |
| Aave Data Provider | 0x3e9708d80f7B3e43118013075F7e95CE3AB31F31 |
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:
- Compiler Optimization: 200 runs for balanced optimization
- Via IR: Enabled for better optimization
- Immutable Variables: Used for addresses that never change
- Packed Storage: Careful ordering of state variables
- 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:
Deployment Checklist
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
- Private Keys: Never commit or share private keys
- Constructor Arguments: Double-check all deployment parameters
- Access Control: Verify owner addresses are correct
- Fee Limits: Set reasonable fee percentages
- Strategy Validation: Test strategies thoroughly before deploying
- Upgrade Path: Consider proxy patterns for upgradeable contracts
- 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
Next Steps