Final project for Peer2Peer Systems and Blockchains A.A. 2025/26.
This project implements a decentralized lending service on Ethereum. Contributors deposit ETH into a shared funding pool, applicants submit loan proposals, contributors vote with weight proportional to their disposable contribution, and every approved loan is managed by a dedicated LoanContract.
A trusted off-chain Bitcoin oracle scans Bitcoin data and publishes BTC-address liquidity to an on-chain BitcoinOracle, which the lending pool uses during proposal resolution.
Contributor deposits ETH
↓
Applicant requests/updates Bitcoin liquidity
↓
Applicant submits a loan proposal
↓
Contributors vote approve/reject
↓
Applicant requests proposal resolution
↓
LendingPool checks:
- available pool liquidity
- Bitcoin liquidity
- weighted vote result
↓
Approved proposal → dedicated LoanContract
↓
Borrower claims the principal
↓
Borrower repays through LendingPool
↓
Principal, interest, gains, and compensation are distributed
| Contract | Responsibility |
|---|---|
LendingPool.sol |
Deposits, withdrawals, proposals, voting, fund locking, repayments, success/failure, compensation, and unclaimed-loan refunds |
LoanContract.sol |
Dedicated state and ETH custody for one approved loan |
BitcoinOracle.sol |
On-chain storage of Bitcoin-address balances and oracle-update requests |
VulnerableLendingPool.sol |
Intentionally unsafe contract used only for the reentrancy demonstration |
ReentrancyAttacker.sol |
Malicious contract used to attack the vulnerable pool |
- The borrower repays through one public path only:
LendingPool.repayLoan(). - A failed loan may still receive late repayments, but it remains permanently failed.
- Principal repayment priority uses contributors' original locked amounts, not the changing remaining amounts.
- Compensation can be claimed multiple times as the compensation pool is refilled.
loanLockedAmountrepresents the remaining uncompensated loss.- An expired loan that was never claimed is cancelled, refunded to the pool, and contributors' funds are unlocked.
- The oracle minimum fee is calculated as:
updateBalance transaction gas used × 0.1 gwei
- The genesis-prefunded account is used only for plain ETH transfers to newly generated accounts.
- The production lending pool uses reentrancy protection.
DecentralizedLendingProject/
├── contracts/
│ ├── BitcoinOracle.sol
│ ├── LendingPool.sol
│ ├── LoanContract.sol
│ ├── VulnerableLendingPool.sol
│ └── ReentrancyAttacker.sol
│
├── scripts/
│ ├── setup_service.py
│ ├── example_operations.py
│ ├── auto_contributor.py
│ ├── gas_evaluation.py
│ ├── compensation_gas.py
│ ├── bitcoin_oracle.py
│ ├── bitcoin_oracle_full.py
│ └── deployed_addresses.json # Generated by setup_service.py
│
├── test/
│ ├── BitcoinOracle.test.js
│ ├── FailedLoanRepayment.test.js
│ ├── GasEvaluation.test.js
│ ├── LoanContract.test.js
│ ├── LoanProposal.test.js
│ ├── OracleMinimumFee.test.js
│ ├── OrderedRepayment.test.js
│ ├── OriginalRepaymentOrder.test.js
│ ├── ProposalResolution.test.js
│ ├── ReentrancyAttack.test.js
│ ├── ReentrancyVulnerable.test.js
│ └── UnclaimedLoanRefund.test.js
│
├── report/
│ └── Decentralized_Lending_Service_Report.pdf
│
├── hardhat.config.js
├── package.json
├── package-lock.json
├── project2526genesis.json
├── block100000.json
├── oracle_131000_output.txt
├── .env.example
├── .gitignore
└── README.md
Install:
- Node.js and npm
- Python 3.10 or newer
- Bitcoin Core and
bitcoin-clionly for the full Bitcoin scan - Geth only when using the supplied course private chain
Install Python packages:
python3 -m pip install web3 python-dotenvFrom the project folder:
cd DecentralizedLendingProject
npm installCompile the smart contracts:
npx hardhat compileEquivalent npm command:
npm run compileCompilation creates the ABI and bytecode files inside artifacts/. The Python deployment scripts require these files.
Create the real environment file:
cp .env.example .envThe account funded in project2526genesis.json must be available through
one of the following credential methods.
RPC_URL=http://127.0.0.1:8545
EXPECTED_CHAIN_ID=202526
EXPECTED_PREFUNDED_ADDRESS=0xd278d247A52C550508ea2b2C9321d816238fb523
PREFUNDED_PRIVATE_KEY=0x64_HEXADECIMAL_CHARACTERS
PREFUNDED_KEYSTORE_FILE=
PREFUNDED_KEYSTORE_PASSWORD=
PREFUNDED_KEYSTORE_PASSWORD_FILE=
DEPLOYER_FUND_ETH=100
CONTRIBUTOR_FUND_ETH=20
BITCOIN_CONF="/absolute/path/to/bitcoin.conf"The project package contains the matching encrypted course keystore and its password file. The default relative configuration is:
RPC_URL=http://127.0.0.1:8545
EXPECTED_CHAIN_ID=202526
EXPECTED_PREFUNDED_ADDRESS=0xd278d247A52C550508ea2b2C9321d816238fb523
PREFUNDED_PRIVATE_KEY=
PREFUNDED_KEYSTORE_FILE=private-chain-data/keystore/UTC--2026-05-05T14-09-10.723312492Z--d278d247a52c550508ea2b2c9321d816238fb523
PREFUNDED_KEYSTORE_PASSWORD=
PREFUNDED_KEYSTORE_PASSWORD_FILE=0xd278d247A52C550508ea2b2C9321d816238fb523psw.txt
DEPLOYER_FUND_ETH=100
CONTRIBUTOR_FUND_ETH=20
BITCOIN_CONF="/absolute/path/to/bitcoin.conf"To apply this configuration automatically to an existing .env, run:
python3 scripts/configure_course_credentials.pyThe script decrypts the keystore only to verify its address. It does not print or save the decrypted private key.
Alternatively, put the password directly in
PREFUNDED_KEYSTORE_PASSWORD and leave
PREFUNDED_KEYSTORE_PASSWORD_FILE empty. Do not configure both.
The supplied password text project2526 is used only to decrypt the included
Geth keystore. Do not put that password in PREFUNDED_PRIVATE_KEY.
setup_service.py now validates:
- the raw key format;
- the keystore file and password;
- the derived account address against the genesis address;
- the chain ID and prefunded account balance.
It prints a specific configuration error instead of treating a placeholder or password as a private key.
For a fast local demonstration, start npx hardhat node, then use Account #0 as the prefunded development account:
RPC_URL=http://127.0.0.1:8545
EXPECTED_CHAIN_ID=31337
EXPECTED_PREFUNDED_ADDRESS=hardhat_account_0_address
PREFUNDED_PRIVATE_KEY=hardhat_account_0_private_key
PREFUNDED_KEYSTORE_FILE=
PREFUNDED_KEYSTORE_PASSWORD=
PREFUNDED_KEYSTORE_PASSWORD_FILE=
DEPLOYER_FUND_ETH=100
CONTRIBUTOR_FUND_ETH=20
BITCOIN_CONF="/absolute/path/to/bitcoin.conf"Hardhat's printed keys are public development keys. Never send real ETH to them.
setup_service.py creates:
- one deployer/oracle-owner account;
- two contributor accounts.
The setup script automatically writes their keys into .env as:
PRIVATE_KEY=generated_deployer_private_key
PRIVATE_KEY_1=generated_contributor_1_private_key
PRIVATE_KEY_2=generated_contributor_2_private_keyIt also saves a backup in .env.generated.
No manual copying is required.
Never commit or submit .env or .env.generated because they contain private keys.
The test command creates its own temporary Hardhat blockchain. A separate npx hardhat node process is not required.
Run all tests:
npm testEquivalent command:
npx hardhat testExpected result for the current project:
41 passing
Run individual test files:
npx hardhat test test/BitcoinOracle.test.js
npx hardhat test test/FailedLoanRepayment.test.js
npx hardhat test test/GasEvaluation.test.js
npx hardhat test test/LoanContract.test.js
npx hardhat test test/LoanProposal.test.js
npx hardhat test test/OracleMinimumFee.test.js
npx hardhat test test/OrderedRepayment.test.js
npx hardhat test test/OriginalRepaymentOrder.test.js
npx hardhat test test/ProposalResolution.test.js
npx hardhat test test/ReentrancyAttack.test.js
npx hardhat test test/ReentrancyVulnerable.test.js
npx hardhat test test/UnclaimedLoanRefund.test.jsUse two terminals.
cd DecentralizedLendingProject
npx hardhat nodeKeep this terminal open.
cd DecentralizedLendingProject
npx hardhat compile
cp .env.example .envEdit .env and configure the Hardhat Account #0 address and private key as described in section 5.2.
python3 scripts/setup_service.pyThe script:
- verifies the RPC connection and chain ID;
- verifies the prefunded account;
- generates a deployer and two contributor accounts;
- uses the prefunded account only to fund those accounts;
- deploys a temporary oracle;
- measures real
updateBalance()gas usage; - calculates the production oracle minimum fee;
- deploys
BitcoinOracleandLendingPoolfrom the generated deployer; - writes generated account keys automatically into
.env; - saves deployment metadata in
scripts/deployed_addresses.json.
python3 scripts/example_operations.pyThe example demonstrates:
- contributor deposit;
- Bitcoin-oracle balance update;
- loan-proposal submission;
- contributor voting;
- voting-period block progression;
- proposal resolution;
- dedicated
LoanContractcreation; - borrower loan claim;
- borrower repayment through
LendingPool; - principal/interest distribution and final state output.
| Script | When to run | Purpose |
|---|---|---|
setup_service.py |
First, after starting a fresh blockchain | Creates/funds accounts, measures oracle gas, calculates the fee, deploys contracts, and saves configuration |
example_operations.py |
After setup | Runs the complete normal lending workflow |
auto_contributor.py |
After setup, in a separate terminal | Watches for proposals and automatically votes approve |
bitcoin_oracle.py |
After setup | Demonstrates request, owner update, and reading of one BTC balance |
bitcoin_oracle_full.py |
After Bitcoin Core is ready | Processes Bitcoin blocks, builds a UTXO set, calculates a balance, and optionally updates Ethereum |
gas_evaluation.py |
After setup | Measures gas usage for the main operations |
compensation_gas.py |
After setup | Measures failure/compensation-related gas usage |
deployed_addresses.json is data, not an executable script. It is regenerated by setup_service.py.
After deployment, run in a separate terminal:
python3 scripts/auto_contributor.pyThe script:
- loads the generated accounts from
.env; - ensures they have disposable funds;
- watches for new proposals;
- votes
approvewhen necessary.
Stop it with:
Ctrl + C
A final KeyboardInterrupt message after pressing Ctrl + C is normal.
Run the Python gas scripts after setup:
python3 scripts/gas_evaluation.py
python3 scripts/compensation_gas.pyRun the Hardhat gas test:
npx hardhat test test/GasEvaluation.test.jsThe oracle-fe test verifies:
npx hardhat test test/OracleMinimumFee.test.jsThe production contract is protected by OpenZeppelin ReentrancyGuard and the checks-effects-interactions pattern.
The vulnerable demonstration uses:
VulnerableLendingPool.sol;ReentrancyAttacker.sol.
Run:
npx hardhat test test/ReentrancyVulnerable.test.js
npx hardhat test test/ReentrancyAttack.test.jsThe first test proves that the intentionally vulnerable contract can be drained. The second proves that the production pool blocks the same attack.
After deployment:
python3 scripts/bitcoin_oracle.pyThe script:
- pays the minimum fee to request a balance update;
- uses the oracle-owner account to store a sample BTC balance;
- reads the stored balance from Ethereum.
This requires Bitcoin Core, bitcoin-cli, and access to the required historical blocks.
Configure an absolute BITCOIN_CONF path in .env, for example:
BITCOIN_CONF="/Users/username/Library/Application Support/Bitcoin/bitcoin.conf"Run the project scan:
python3 -u scripts/bitcoin_oracle_full.py \
--to-block 131000 \
--address 1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn \
--verbose-every 1000 \
--update-ethereum | tee oracle_131000_output.txtRun without updating Ethereum:
python3 -u scripts/bitcoin_oracle_full.py \
--to-block 131000 \
--address 1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn \
--verbose-every 1000Arguments:
| Argument | Meaning |
|---|---|
--to-block |
Last Bitcoin block height to process |
--address |
Bitcoin address whose UTXO balance is calculated |
--verbose-every |
Progress-print interval |
--update-ethereum |
Also writes the calculated balance to BitcoinOracle |
The official project includes project2526genesis.json, whose chain ID is 202526 and whose prefunded signer address is:
0xd278d247A52C550508ea2b2C9321d816238fb523
Initialize a Geth data directory:
mkdir -p ethdata
geth --datadir ethdata init project2526genesis.jsonThe matching course signer keystore is included in
private-chain-data/keystore/. Configure .env by running:
python3 scripts/configure_course_credentials.pyThen start the Clique node using the course-provided Geth version and commands.
The node must expose HTTP RPC at http://127.0.0.1:8545 and produce new blocks.
Modern Geth releases no longer seal Clique blocks, so use the version specified
by the course environment rather than automatically upgrading Geth.
Run:
npx hardhat compile
python3 scripts/setup_service.pyThe prefunded genesis account performs only ETH transfers. All contract deployment is performed by the newly generated deployer account.
example_operations.py uses Hardhat's evm_mine RPC method to advance blocks quickly. For the course Geth chain, allow Clique to produce blocks normally or replace the development mining helper with a wait-for-blocks loop.
Stop a running Hardhat node with:
Ctrl + C
A new npx hardhat node starts a fresh empty blockchain, so the contracts must be deployed again.
Terminal 1:
npx hardhat nodeTerminal 2:
npx hardhat compile
python3 scripts/setup_service.py
python3 scripts/example_operations.pyThe setup script automatically replaces the generated PRIVATE_KEY, PRIVATE_KEY_1, and PRIVATE_KEY_2 values in .env.
npx hardhat clean
npx hardhat compilerm -f scripts/deployed_addresses.json .env.generatedDo not commit or submit:
.env
.env.generated
.env.save
scripts/deployed_addresses.json
node_modules/
artifacts/
cache/
.git/
.DS_Store
__MACOSX/
Keep and submit:
.env.example
.gitignore
contracts/
scripts/
test/
report/
package.json
package-lock.json
hardhat.config.js
project2526genesis.json
README.md
Commands:
npx hardhat test
npx hardhat test test/FailedLoanRepayment.test.js
npx hardhat test test/OriginalRepaymentOrder.test.js
npx hardhat test test/UnclaimedLoanRefund.test.js
npx hardhat test test/ReentrancyVulnerable.test.js
npx hardhat test test/ReentrancyAttack.test.js# Install dependencies
npm install
python3 -m pip install web3 python-dotenv
# Compile
npx hardhat compile
# Test
npx hardhat test
# Start local development blockchain
npx hardhat node
# Deploy/configure service
python3 scripts/setup_service.py
# Run complete demo
python3 scripts/example_operations.py
# Run automated contributor
python3 scripts/auto_contributor.py
# Run simple oracle demo
python3 scripts/bitcoin_oracle.py
# Gas evaluation
python3 scripts/gas_evaluation.py
python3 scripts/compensation_gas.py