Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions academy/lending-protocol/contracts/GlobalLedger.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/access/Ownable.sol";

/// @title GlobalLedger
/// @dev Tracks deposits, loans, and repayments across LendingPool contracts
contract GlobalLedger is Ownable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we already have the GlobalLedger inside CollateralManager.sol why did you want to write an amended contract? I would advise you to use the GlobalLedger inside CollateralManager.sol as it has a lot of functionality that would support backward compatiblity. So only thing you would have to change in GlobalLedger inside CollateralManager.sol would be add factory based role access to it, as your code for factory based role access looks good.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @gitshreevatsa, thanks for the update. I have made some changes on the code incuding removing Globalledger.sol to avoid duplicate and others.

struct Deposit {
uint256 amount;
bool exists;
}

struct Loan {
uint256 amount;
bool exists;
}

mapping(address => mapping(TokenId => Deposit)) public deposits;
mapping(address => mapping(TokenId => Loan)) public loans;
mapping(address => bool) public authorizedLendingPools;

event DepositRecorded(address indexed user, TokenId token, uint256 amount);
event LoanRecorded(address indexed user, TokenId token, uint256 amount);
event LoanRepaid(address indexed user, TokenId token, uint256 amount);
event LendingPoolRegistered(address indexed lendingPool);

/// @notice Modifier to restrict access to only authorized lending pools
modifier onlyLendingPool() {
require(
authorizedLendingPools[msg.sender],
"Not an authorized lending pool"
);
_;
}

/// @notice Register a new lending pool (only callable by factory or internal async call)
function registerLendingPool(address lendingPool) external {
require(
msg.sender == owner() || msg.sender == address(this),
"Unauthorized"
);
authorizedLendingPools[lendingPool] = true;
emit LendingPoolRegistered(lendingPool);
}

/// @notice Record a deposit from an authorized LendingPool
function recordDeposit(
address user,
TokenId token,
uint256 amount
) external payable onlyLendingPool {
require(amount > 0, "Invalid deposit amount");

if (!deposits[user][token].exists) {
deposits[user][token] = Deposit(amount, true);
} else {
deposits[user][token].amount += amount;
}

emit DepositRecorded(user, token, amount);
}

/// @notice Record a loan taken from an authorized LendingPool
function recordLoan(
address user,
TokenId token,
uint256 amount
) external payable onlyLendingPool {
require(amount > 0, "Invalid loan amount");

if (!loans[user][token].exists) {
loans[user][token] = Loan(amount, true);
} else {
loans[user][token].amount += amount;
}

emit LoanRecorded(user, token, amount);
}

/// @notice Record loan repayment and reduce outstanding balance
function repayLoan(
address user,
TokenId token,
uint256 amount
) external payable onlyLendingPool {
require(amount > 0, "Invalid repayment amount");
require(loans[user][token].exists, "No active loan");

if (loans[user][token].amount <= amount) {
delete loans[user][token];
} else {
loans[user][token].amount -= amount;
}

emit LoanRepaid(user, token, amount);
}
}
74 changes: 74 additions & 0 deletions academy/lending-protocol/contracts/LendingPoolFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";
import "./LendingPool.sol"; // Import LendingPool contract

/// @title LendingPoolFactory
/// @dev Handles deployment of LendingPool contracts across different shards
contract LendingPoolFactory {
address public globalLedger;
address public interestManager;
address public oracle;
TokenId public usdt;
TokenId public eth;
uint8 public shardCounter; // Tracks which shard to deploy to (0-3)

event LendingPoolDeployed(address pool, uint8 shardId, address owner);

constructor(
address _globalLedger,
address _interestManager,
address _oracle,
TokenId _usdt,
TokenId _eth
) {
globalLedger = _globalLedger;
interestManager = _interestManager;
oracle = _oracle;
usdt = _usdt;
eth = _eth;
shardCounter = 0;
}

/// @notice Deploys a new LendingPool contract to a shard
function deployLendingPool() external {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the deployment of the contract, there should be a function which calls the globalLedger to register the lending pool

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well noted Sir, I will make changes

bytes memory constructorArgs = abi.encode(
globalLedger,
interestManager,
oracle,
usdt,
eth,
msg.sender
);

// Compute full contract creation bytecode
bytes memory bytecode = bytes.concat(
type(LendingPool).creationCode,
constructorArgs
);

// Call asyncDeploy with correct parameters
address poolAddress = Nil.asyncDeploy(
shardCounter, // Shard ID
msg.sender, // Refund to the sender
address(0), // Bounce to (set to 0 for now)
0, // Fee credit (set to 0 unless required)
0, // Forward kind (set to 0 unless forwarding behavior is needed)
0, // Value (set to 0 unless ETH needs to be sent)
bytecode, // Contract creation code + constructor args
0 // Salt (set to 0; can be changed for deterministic addresses)
);

require(poolAddress != address(0), "Deployment failed");

emit LendingPoolDeployed(poolAddress, shardCounter, msg.sender);
shardCounter = (shardCounter + 1) % 4; // Cycle through shards

// Async call to register LendingPool in GlobalLedger
(bool success, ) = globalLedger.call(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call has to be made via asyncCall because when the global ledger is on different shard compared to the shard where Factory is deployed, this call would fail to work. Just include Nil.asyncCall and no need to resolve it!

abi.encodeWithSignature("registerLendingPool(address)", poolAddress)
);
require(success, "Async call to GlobalLedger failed");
}
}
26 changes: 13 additions & 13 deletions academy/lending-protocol/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,34 @@ import "@nomicfoundation/hardhat-ignition-ethers";
import "@nomicfoundation/hardhat-ethers";
import "@nomicfoundation/hardhat-ignition-ethers";
import "@typechain/hardhat";
import "@nomicfoundation/hardhat-toolbox";

import * as dotenv from "dotenv";
import type { HardhatUserConfig } from "hardhat/config";

import "@nomiclabs/hardhat-ethers"; // Import Ethers plugin


import "./task/run-lending-protocol";

dotenv.config();
dotenv.config(); // Load .env variables


const config: HardhatUserConfig = {
ignition: {
requiredConfirmations: 1,
},
defaultNetwork: "nil",
solidity: {
version: "0.8.28", // or your desired version
version: "0.8.28", // Match your Solidity version
settings: {
viaIR: true, // needed to compile router
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
nil: {
url: process.env.NIL_RPC_ENDPOINT,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
};

export default config;





4 changes: 3 additions & 1 deletion academy/lending-protocol/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion academy/lending-protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "This is an example repository to showcase how a lending protocol can be built on top of =nil;",
"devDependencies": {
"@nomicfoundation/hardhat-toolbox-viem": "^3.0.0",
"hardhat": "^2.22.18"
"hardhat": "^2.22.19"
},
"scripts": {
"compile": "npx hardhat compile",
Expand Down
42 changes: 42 additions & 0 deletions academy/lending-protocol/task/deployLendingPool.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { task } = require("hardhat/config");

task("deploy-lending-pool", "Deploys a new LendingPool contract and registers it in GlobalLedger")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the run-lending-protocol task, in that you can see that the contracts are in fact deployed rather than taken from the params.

.addParam("factory", "The address of the LendingPoolFactory contract")
.addParam("globalLedger", "The address of the GlobalLedger contract")
.addParam("interestManager", "The address of the InterestManager contract")
.addParam("oracle", "The address of the Oracle contract")
.addParam("usdt", "The TokenId for USDT")
.addParam("eth", "The TokenId for ETH")
.setAction(async (taskArgs) => {
const { ethers } = require("hardhat");

const factoryAddress = taskArgs.factory;
const globalLedgerAddress = taskArgs.globalLedger;
const interestManagerAddress = taskArgs.interestManager;
const oracleAddress = taskArgs.oracle;
const usdtTokenId = taskArgs.usdt;
const ethTokenId = taskArgs.eth;

console.log("Deploying LendingPool using the provided factory...");

// Attach to LendingPoolFactory contract
const LendingPoolFactory = await ethers.getContractFactory("LendingPoolFactory");
const factory = LendingPoolFactory.attach(factoryAddress);

// Deploy LendingPool via factory
const tx = await factory.deployLendingPool();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usage of ethers wouldn't work completely with =nil; as we have some differences while calling contracts. Please use niljs. You can learn more about niljs here.

await tx.wait(); // Wait for the transaction to be mined
console.log("LendingPool deployed successfully!");

// Interact with GlobalLedger to check if the LendingPool is registered
const GlobalLedger = await ethers.getContractFactory("GlobalLedger");
const globalLedger = GlobalLedger.attach(globalLedgerAddress);

// Check if LendingPool is registered in GlobalLedger
const isRegistered = await globalLedger.authorizedLendingPools(factoryAddress);
console.log("Is LendingPool registered in GlobalLedger?", isRegistered);

// You can also test other functionality like registering a deposit, loan, etc.
});

module.exports = {};
Loading