A decentralized lending protocol built on Ethereum that enables users to deposit collateral and borrow PUSD stablecoin. The protocol features a stability fund, liquidation engine, and treasury for robust risk management.
This repository is a cleaned-up, rebranded copy of a production DeFi lending
protocol I worked on as part of a two-engineer team. The contracts were
deployed and verified on Ethereum mainnet under a different name. The source
carries an MIT SPDX-License-Identifier in every file and is republished here
under those terms; see LICENSE for the copyright notice.
Deployment addresses, internal tooling, CI configuration and operational
artifacts have been removed. What remains is the contract source, the test
suite and the deployment scripts. Protocol-owned addresses in
script/NetworkAddresses.sol are left as address(0) and must be populated
before running any script against a live network; the third-party
infrastructure addresses (Chainlink, Uniswap V3, wstETH) are the canonical
public ones.
This lending protocol consists of several interconnected smart contracts:
- PUSD: Protocol stablecoin with restricted mint/burn functionality
- wPUSD: Wrapped PUSD token for the stability fund with yield generation
- ProtocolConfig: Central configuration contract for protocol parameters
- LendingVault: Main contract for deposits, borrowing, repayments, and withdrawals
- StabilityFund: Manages PUSD deposits, mints wPUSD, and provides liquidation liquidity
- LiquidationEngine: Handles liquidation of undercollateralized positions
- Treasury: Protocol treasury managing fees and backstop mechanisms
- Deposit Collateral: Deposit wstETH or other approved collateral
- Borrow PUSD: Mint PUSD stablecoin against collateral
- Repay Debt: Repay borrowed PUSD to reduce debt
- Withdraw Collateral: Withdraw excess collateral
- Earn Yield: Deposit PUSD in stability fund to earn yield via wPUSD
- Automated Liquidations: Liquidate undercollateralized positions
- Stability Fund: Provides liquidity and stability to the protocol
- Treasury Backstop: Emergency liquidity provision
- Upgradeable Contracts: UUPS proxy pattern for contract upgrades
- Pausable Operations: Emergency pause functionality
- Foundry
- Git
-
Clone the repository
git clone <repository-url> cd lending-protocol
-
Install dependencies
forge install
-
Set up environment variables
cp .env.example .env # Edit .env with your configuration -
Build contracts
forge build
-
Run tests
forge test
Create a .env file with the following variables:
# Required for all networks
DEPLOYER_PRIVATE_KEY=your_private_key_here
ETHERSCAN_API_KEY=your_etherscan_api_key_here
# RPC URLs (configure the ones you need)
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your_api_key
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your_api_keyDEPLOYER_PRIVATE_KEY: Private key for contract deployment and upgradesETHERSCAN_API_KEY: API key for contract verification on EtherscanSEPOLIA_RPC_URL: RPC endpoint for Sepolia testnetMAINNET_RPC_URL: RPC endpoint for Ethereum mainnet
The project includes comprehensive test coverage for all major contracts and functionality.
forge testforge test --gas-reportforge test --match-contract LendingVaultTest
forge test --match-test testDepositforge coverage# Generate LCOV file for entire repo
forge coverage --report lcov --out lcov.info
Tip: If you hit "stack too deep" during coverage, try:
forge coverage --ir-minimum --report lcov --out lcov.info-
Start local node
anvil
-
Deploy contracts
./scripts/deploy/deploy-contracts.sh local
-
Configure deployment flags Edit
script/deploy/Deploy.s.soland set the deployment flags totruefor contracts you want to deploy:function setDeployFlags() internal { deployProtocolConfig = true; deployPUSD = true; deployWPUSD = true; deployStabilityFund = true; deployTreasury = true; deployLiquidationEngine = true; deployLendingVault = true; deployCollateralToken = true; // Only for testnet }
-
Deploy to Sepolia
./scripts/deploy/deploy-contracts.sh sepolia
-
Configure deployment flags Edit
script/deploy/Deploy.s.soland set the deployment flags totruefor contracts you want to deploy:function setDeployFlags() internal { deployProtocolConfig = true; deployPUSD = true; deployWPUSD = true; deployStabilityFund = true; deployTreasury = true; deployLiquidationEngine = true; deployLendingVault = true; deployCollateralToken = false; // wstETH exists on mainnet }
-
Deploy to Mainnet
./scripts/deploy/deploy-contracts.sh mainnet
The project includes automated deployment scripts:
./scripts/deploy/deploy-contracts.sh [network]: Deploy contracts with selective deployment./scripts/deploy/upgrade-contracts.sh [network]: Upgrade contracts with selective upgrades./scripts/verification/verify-contracts.sh [network]: Verify deployed contracts on Etherscan./scripts/deploy/configure-contracts.sh [network]: Configure contract relationships and parameters
# Deploy on Sepolia
./scripts/deploy/deploy-contracts.sh sepolia
# Deploy on Mainnet
./scripts/deploy/deploy-contracts.sh mainnet
# Deploy on local network
./scripts/deploy/deploy-contracts.sh local
# Dry run (show what would be done)
./scripts/deploy/deploy-contracts.sh --dry-run sepolia
# Force deployment without confirmation
./scripts/deploy/deploy-contracts.sh --force sepoliaBefore running any script, ensure you have:
- Correct RPC URL configured in your
.envfile for the target network - Deployed contracts on the target network (addresses are auto-resolved)
- Proper deployment flags set in the deployment script for your needs
After deployment, contracts need to be configured with their relationships and parameters.
Run the configuration script to set up all contract relationships:
# Configure all contracts on Sepolia
./scripts/deploy/configure-contracts.sh sepolia
# Configure all contracts on Mainnet
./scripts/deploy/configure-contracts.sh mainnet
# Force configuration without confirmation
./scripts/deploy/configure-contracts.sh --force sepoliaYou can also configure individual contracts:
# Configure specific contracts
forge script script/config/ConfigurePUSD.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast
forge script script/config/ConfigureLendingVault.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast
forge script script/config/ConfigureProtocolConfig.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcastIndividual configuration scripts for each contract:
script/config/ConfigurePUSD.s.sol: Configure PUSD contract relationshipsscript/config/ConfigureWPUSD.s.sol: Configure wPUSD contract relationshipsscript/config/ConfigureLendingVault.s.sol: Configure LendingVault relationships and parametersscript/config/ConfigureStabilityFund.s.sol: Configure StabilityFund relationships and parametersscript/config/ConfigureTreasury.s.sol: Configure Treasury relationships and parametersscript/config/ConfigureLiquidationEngine.s.sol: Configure LiquidationEngine relationships and parametersscript/config/ConfigureProtocolConfig.s.sol: Configure ProtocolConfig parameters
Contract addresses are automatically resolved based on the current network using NetworkAddresses.sol. No manual configuration required!
The system automatically detects the network and provides the correct addresses:
import {NetworkAddresses} from "../NetworkAddresses.sol";
contract ConfigurePUSD is Script {
function run() external {
address pusd = NetworkAddresses.pusd();
address lendingVault = NetworkAddresses.lendingVault();
address stabilityFund = NetworkAddresses.stabilityFund();
// Addresses are automatically resolved based on block.chainid
}
}Supported networks: Sepolia (11155111), Mainnet (1)
The protocol uses UUPS proxy pattern for upgradeable contracts.
-
Configure upgrade flags Edit
script/deploy/Upgrade.s.soland set the upgrade flags totruefor contracts you want to upgrade:function setUpgradeFlags() internal { upgradeProtocolConfig = false; upgradePUSD = true; upgradeWPUSD = false; upgradeStabilityFund = false; upgradeTreasury = false; upgradeLiquidationEngine = false; upgradeLendingVault = false; }
-
Automatic address resolution Contract addresses are automatically resolved using
NetworkAddresses.solbased on the current network:import {NetworkAddresses} from "../NetworkAddresses.sol"; contract Upgrade is Script { function run() external { address pusdProxy = NetworkAddresses.pusd(); address lendingVaultProxy = NetworkAddresses.lendingVault(); // Addresses automatically resolved by chainId } }
-
Run upgrade
./scripts/deploy/upgrade-contracts.sh sepolia
# Upgrade on Sepolia
./scripts/deploy/upgrade-contracts.sh sepolia
# Upgrade on Mainnet
./scripts/deploy/upgrade-contracts.sh mainnet
# Dry run upgrade
./scripts/deploy/upgrade-contracts.sh --dry-run sepolia
# Force upgrade without confirmation
./scripts/deploy/upgrade-contracts.sh --force sepolia- Role-based permissions: Centralized role system managed through ProtocolConfig
- Multi-sig recommended: Use multi-signature wallet as owner for production
ADMIN_ROLE:keccak256("ADMIN_ROLE")- Controls all administrative functions across contractsUPGRADER_ROLE:keccak256("UPGRADER_ROLE")- Controls contract upgrades via UUPS proxy patternDEFAULT_ADMIN_ROLE:0x00- Supreme admin role (initially granted to deployer)
Key Functions:
grantRole(bytes32 role, address account)- Grant role to addressrevokeRole(bytes32 role, address account)- Revoke role from address
- Pausable contracts: Emergency pause functionality
- Reentrancy protection: Guards against reentrancy attacks
- Input validation: Comprehensive parameter validation
- Upgradeability: UUPS proxy pattern for contract upgrades
- Collateralization ratios: Configurable minimum collateral requirements
- Liquidation thresholds: Automatic liquidation of risky positions
- Debt ceiling: Maximum protocol debt limits
- Interest rate models: Dynamic interest rate calculations
-
Deposit Collateral
// Approve collateral token wstETH.approve(lendingVault, amount); // Deposit collateral lendingVault.deposit(amount, minCollateralRatio);
-
Borrow PUSD
lendingVault.borrow(pusdAmount); -
Repay Debt
// Approve PUSD pusd.approve(lendingVault, repayAmount); // Repay debt lendingVault.repay(repayAmount);
-
Withdraw Collateral
lendingVault.withdraw(collateralAmount);
-
Deposit PUSD for Yield
// Approve PUSD pusd.approve(stabilityFund, pusdAmount); // Deposit to stability fund stabilityFund.depositPUSD(pusdAmount);
-
Redeem wPUSD
stabilityFund.redeemWPUSD(wpusdAmount);
- LTV Ratio: 80%
- Liquidation Threshold: 80%
- Minimum Collateral Ratio: 120%
- Liquidation Penalty: 10%
- Interest Rate: ~0.5% APY
- Debt Ceiling: 1,000,000 PUSD
All parameters can be updated by the protocol owner through the ProtocolConfig contract.
- Solidity ^0.8.20
- OpenZeppelin contracts for security
- NatSpec documentation
- Comprehensive event logging
lending-protocol/
βββ contracts/ # Smart contracts
βββ deployments/ # Auto-generated deployment artifacts
β βββ abi/ # Contract ABIs
β βββ addresses/ # Deployed addresses by network
β βββ index.json # Deployment summary
βββ lib/ # Dependencies
βββ script/ # Foundry deployment and configuration scripts
β βββ NetworkAddresses.sol # Network addresses (shared across all scripts)
β βββ config/ # Configuration scripts for deployed contracts
β βββ deploy/ # Deployment scripts
β βββ testing-verification/ # Testing, debugging and verification scripts
β βββ liquidation/ # Liquidation testing scripts
βββ scripts/ # Bash automation scripts
β βββ deploy/ # Deployment automation
β βββ gas/ # Gas management
β βββ size/ # Size management
β βββ verification/ # Verification automation
βββ test/ # Test files
βββ tools/ # Node.js tooling
β βββ export-deployments.js # Deployment artifact exporter
βββ foundry.toml # Foundry configuration
βββ remappings.txt # Import remappings
βββ README.md # Documentation
This project uses a unified address management system that automatically syncs deployment addresses across different formats:
π― Single Source of Truth: All addresses come from Foundry broadcast files π€ Auto-Sync: Deployment artifacts and Solidity code stay in sync automatically π Multi-Format: Supports both JSON (frontend) and Solidity (scripts) consumption
- Deploy contracts using Foundry deploy scripts
- GitHub Action detects changes in
broadcast/directory - Auto-export runs (
tools/export-deployments.js) - Updates multiple targets:
deployments/addresses/*.jsonβ For frontend integrationscript/config/NetworkAddresses.solβ For Foundry scripts
- Auto-commits changes back to repository
Frontend (JavaScript/TypeScript):
import sepoliaDeployments from './deployments/addresses/sepolia.json';
const pusdAddress = sepoliaDeployments.contracts.PUSD.address;Foundry Scripts (Solidity):
import {NetworkAddresses} from "./script/NetworkAddresses.sol";
contract MyScript is Script {
function run() external {
address pusd = NetworkAddresses.pusd(); // Auto-resolves by chain ID
}
}Manual Export:
node tools/export-deployments.js- Fork the repository
- Create a feature branch
- Write comprehensive tests
- Ensure all tests pass
- Submit a pull request
- UUPS Proxy Pattern: Chosen for upgradeability while maintaining security
- Interest Rate Model: Simple compound interest with configurable rates
- Liquidation Mechanism: Dutch auction style with external AMM integration
- Yield Generation: Time-based yield calculation for stability fund
- Audit: These contracts were audited prior to their original mainnet deployment. This repository is published as a reference and portfolio artifact, not as deployment-ready code; any redeployment should be independently reviewed and audited.
- Testing: Thoroughly test on testnets before production use
- Risk: DeFi protocols carry inherent smart contract and financial risks
- Upgrades: Contract upgrades should be done through governance mechanisms