diff --git a/academy/lending-protocol/contracts/LendingPool.sol b/academy/lending-protocol/contracts/LendingPool.sol index 54dff4982..d9d1d96f7 100644 --- a/academy/lending-protocol/contracts/LendingPool.sol +++ b/academy/lending-protocol/contracts/LendingPool.sol @@ -15,6 +15,13 @@ contract LendingPool is NilBase, NilTokenBase, NilAwaitable { TokenId public usdt; TokenId public eth; + // Array to store lending pool addresses across different shards + address[] public lendingPoolsByShard; + uint256 public currentShardId; + + // Constants for shards + uint256 private constant MAX_SHARDS = 4; + /// @notice Constructor to initialize the LendingPool contract with addresses for dependencies. /// @dev Sets the contract addresses for GlobalLedger, InterestManager, Oracle, USDT, and ETH tokens. /// @param _globalLedger The address of the GlobalLedger contract. @@ -34,6 +41,15 @@ contract LendingPool is NilBase, NilTokenBase, NilAwaitable { oracle = _oracle; usdt = _usdt; eth = _eth; + currentShardId = Nil.getShardId(address(this)); + } + + /// @notice Registers lending pools across different shards + /// @dev This function should be called after deploying all lending pools to establish cross-shard communication + /// @param _lendingPools Array of lending pool addresses across different shards + function registerLendingPools(address[] memory _lendingPools) public { + require(_lendingPools.length <= MAX_SHARDS, "Too many lending pools"); + lendingPoolsByShard = _lendingPools; } /// @notice Deposit function to deposit tokens into the lending pool. @@ -66,13 +82,22 @@ contract LendingPool is NilBase, NilTokenBase, NilAwaitable { /// @dev Prevents invalid token types from being borrowed. require(borrowToken == usdt || borrowToken == eth, "Invalid token"); - /// @notice Ensure that the LendingPool has enough liquidity of the requested borrow token - /// @dev Checks the LendingPool's balance to confirm it has enough tokens to fulfill the borrow request. - require( - Nil.tokenBalance(address(this), borrowToken) >= amount, - "Insufficient funds" - ); + /// @notice Check if the current shard's lending pool has enough liquidity + /// @dev If local shard has insufficient liquidity, it will trigger cross-shard borrowing + if (Nil.tokenBalance(address(this), borrowToken) >= amount) { + // Local shard has enough liquidity, process locally + processLocalBorrow(amount, borrowToken); + } else { + // Insufficient local liquidity, try cross-shard borrowing + initiateShardLiquidityCheck(amount, borrowToken); + } + } + /// @notice Process a local borrow when the current shard has sufficient liquidity + /// @dev This is the original borrow flow when liquidity is available locally + /// @param amount The amount of the token to borrow + /// @param borrowToken The token the user wants to borrow + function processLocalBorrow(uint256 amount, TokenId borrowToken) internal { /// @notice Determine which collateral token will be used (opposite of the borrow token) /// @dev Identifies the collateral token by comparing the borrow token. TokenId collateralToken = (borrowToken == usdt) ? eth : usdt; @@ -86,7 +111,7 @@ contract LendingPool is NilBase, NilTokenBase, NilAwaitable { ); /// @notice Encoding the context to process the loan after the price is fetched - /// @dev The context contains the borrower’s details, loan amount, borrow token, and collateral token. + /// @dev The context contains the borrower's details, loan amount, borrow token, and collateral token. bytes memory context = abi.encode( msg.sender, amount, @@ -99,6 +124,309 @@ contract LendingPool is NilBase, NilTokenBase, NilAwaitable { sendRequest(oracle, 0, 9_000_000, context, callData, processLoan); } + /// @notice Initiates a check for liquidity across all shards + /// @dev Queries other lending pools in different shards to find available liquidity + /// @param amount The amount of the token to borrow + /// @param borrowToken The token the user wants to borrow + function initiateShardLiquidityCheck( + uint256 amount, + TokenId borrowToken + ) internal { + // Create context for the callback + bytes memory context = abi.encode( + msg.sender, + amount, + borrowToken, + currentShardId, + 0 // Starting shard index to check + ); + + // Start checking other shards from index 0 + checkNextShardLiquidity(context); + } + + /// @notice Recursively checks liquidity in lending pools across different shards + /// @dev Queries each shard's lending pool until it finds one with sufficient liquidity + /// @param context Contains borrower, amount, token, current shard, and shard index to check + function checkNextShardLiquidity(bytes memory context) internal { + ( + address borrower, + uint256 amount, + TokenId borrowToken, + uint256 originShardId, + uint256 shardIndex + ) = abi.decode(context, (address, uint256, TokenId, uint256, uint256)); + + // If we've checked all shards, notify the user of insufficient liquidity + if (shardIndex >= lendingPoolsByShard.length) { + revert("Insufficient liquidity across all shards"); + } + + // Skip the current shard as we've already checked it + if (Nil.getShardId(lendingPoolsByShard[shardIndex]) == originShardId) { + // Update context with next shard index + bytes memory newContext = abi.encode( + borrower, + amount, + borrowToken, + originShardId, + shardIndex + 1 + ); + checkNextShardLiquidity(newContext); + return; + } + + // Prepare to query the lending pool on another shard for liquidity + bytes memory callData = abi.encodeWithSignature( + "checkLiquidity(address,uint256)", + borrowToken, + amount + ); + + // Create context for the callback with all needed information + bytes memory requestContext = abi.encode( + borrower, + amount, + borrowToken, + originShardId, + shardIndex + ); + + // Send cross-shard request to check liquidity + sendRequest( + lendingPoolsByShard[shardIndex], + 0, + 6_000_000, + requestContext, + callData, + handleLiquidityCheckResponse + ); + } + + /// @notice Handles the response from a cross-shard liquidity check + /// @dev If liquidity is found, initiates borrowing from that shard, otherwise checks the next shard + /// @param success Whether the liquidity check was successful + /// @param returnData Result from the liquidity check + /// @param context Contains borrower, amount, token, current shard, and shard index checked + function handleLiquidityCheckResponse( + bool success, + bytes memory returnData, + bytes memory context + ) public payable { + require(success, "Shard liquidity check failed"); + + ( + address borrower, + uint256 amount, + TokenId borrowToken, + uint256 originShardId, + uint256 shardIndex + ) = abi.decode(context, (address, uint256, TokenId, uint256, uint256)); + + // Decode the response to determine if the shard has sufficient liquidity + bool hasLiquidity = abi.decode(returnData, (bool)); + + if (hasLiquidity) { + // Found liquidity in this shard, initiate cross-shard borrowing + initiateCrossShardBorrow(borrower, amount, borrowToken, shardIndex); + } else { + // No liquidity in this shard, check the next shard + bytes memory newContext = abi.encode( + borrower, + amount, + borrowToken, + originShardId, + shardIndex + 1 + ); + checkNextShardLiquidity(newContext); + } + } + + /// @notice Initiates borrowing from a lending pool in another shard + /// @dev Sends a request to the lending pool in the source shard to transfer tokens + /// @param borrower Address of the user borrowing tokens + /// @param amount Amount of tokens to borrow + /// @param borrowToken Token to borrow + /// @param sourceShardIndex Index of the source shard in the lendingPoolsByShard array + function initiateCrossShardBorrow( + address borrower, + uint256 amount, + TokenId borrowToken, + uint256 sourceShardIndex + ) internal { + // Determine which collateral token will be used + TokenId collateralToken = (borrowToken == usdt) ? eth : usdt; + + // Query the oracle for the price to calculate collateral + bytes memory callData = abi.encodeWithSignature( + "getPrice(address)", + borrowToken + ); + + // Create context for the oracle callback + bytes memory context = abi.encode( + borrower, + amount, + borrowToken, + collateralToken, + sourceShardIndex + ); + + // Send request to oracle to get token price + sendRequest( + oracle, + 0, + 9_000_000, + context, + callData, + processCrossShardLoan + ); + } + + /// @notice Process a cross-shard loan after getting the price from the oracle + /// @dev Similar to processLoan but for cross-shard borrowing + /// @param success Whether the oracle call was successful + /// @param returnData Price data from the oracle + /// @param context Contains borrower details and source shard information + function processCrossShardLoan( + bool success, + bytes memory returnData, + bytes memory context + ) public payable { + require(success, "Oracle call failed"); + + ( + address borrower, + uint256 amount, + TokenId borrowToken, + TokenId collateralToken, + uint256 sourceShardIndex + ) = abi.decode(context, (address, uint256, TokenId, TokenId, uint256)); + + // Decode the price data + uint256 borrowTokenPrice = abi.decode(returnData, (uint256)); + uint256 loanValueInUSD = amount * borrowTokenPrice; + uint256 requiredCollateral = (loanValueInUSD * 120) / 100; + + // Check user's collateral with GlobalLedger + bytes memory ledgerCallData = abi.encodeWithSignature( + "getDeposit(address,address)", + borrower, + collateralToken + ); + + bytes memory ledgerContext = abi.encode( + borrower, + amount, + borrowToken, + requiredCollateral, + sourceShardIndex + ); + + sendRequest( + globalLedger, + 0, + 6_000_000, + ledgerContext, + ledgerCallData, + finalizeCrossShardLoan + ); + } + + /// @notice Finalizes a cross-shard loan after validating collateral + /// @dev Sends a request to the source shard's lending pool to transfer tokens + /// @param success Whether the collateral check was successful + /// @param returnData User's collateral balance from GlobalLedger + /// @param context Contains loan details and source shard information + function finalizeCrossShardLoan( + bool success, + bytes memory returnData, + bytes memory context + ) public payable { + require(success, "Ledger call failed"); + + ( + address borrower, + uint256 amount, + TokenId borrowToken, + uint256 requiredCollateral, + uint256 sourceShardIndex + ) = abi.decode(context, (address, uint256, TokenId, uint256, uint256)); + + uint256 userCollateral = abi.decode(returnData, (uint256)); + + require( + userCollateral >= requiredCollateral, + "Insufficient collateral" + ); + + // Record the loan in GlobalLedger + bytes memory recordLoanCallData = abi.encodeWithSignature( + "recordLoan(address,address,uint256)", + borrower, + borrowToken, + amount + ); + Nil.asyncCall(globalLedger, address(this), 0, recordLoanCallData); + + // Request the source shard's lending pool to transfer tokens to the borrower + bytes memory transferCallData = abi.encodeWithSignature( + "transferToBorrower(address,address,uint256)", + borrower, + borrowToken, + amount + ); + + Nil.asyncCall( + lendingPoolsByShard[sourceShardIndex], + address(this), + 0, + transferCallData + ); + } + + /// @notice Transfers tokens to a borrower as part of cross-shard borrowing + /// @dev Called by lending pools from other shards to initiate token transfer + /// @param borrower The address of the borrower to receive tokens + /// @param token The token to transfer + /// @param amount The amount to transfer + function transferToBorrower( + address borrower, + TokenId token, + uint256 amount + ) public { + // Ensure the caller is another registered lending pool + bool isRegisteredPool = false; + for (uint i = 0; i < lendingPoolsByShard.length; i++) { + if (lendingPoolsByShard[i] == msg.sender) { + isRegisteredPool = true; + break; + } + } + require(isRegisteredPool, "Not a registered lending pool"); + + // Check if this pool has enough liquidity + require( + Nil.tokenBalance(address(this), token) >= amount, + "Insufficient funds" + ); + + // Transfer tokens to the borrower + sendTokenInternal(borrower, token, amount); + } + + /// @notice Checks if the lending pool has sufficient liquidity for a token + /// @dev Called by other lending pools to check liquidity in this pool + /// @param token The token to check + /// @param amount The amount needed + /// @return bool Whether this pool has sufficient liquidity + function checkLiquidity( + TokenId token, + uint256 amount + ) public view returns (bool) { + return Nil.tokenBalance(address(this), token) >= amount; + } + /// @notice Callback function to process the loan after the price data is retrieved from Oracle. /// @dev Ensures that the borrower has enough collateral, calculates the loan value, and initiates loan processing. /// @param success Indicates if the Oracle call was successful. @@ -226,14 +554,18 @@ contract LendingPool is NilBase, NilTokenBase, NilAwaitable { /// @notice Encoding the context to handle repayment after loan details are fetched /// @dev Once the loan details are retrieved, the repayment amount is processed. - bytes memory context = abi.encode( - msg.sender, - tokens[0].amount - ); + bytes memory context = abi.encode(msg.sender, tokens[0].amount); /// @notice Send request to GlobalLedger to fetch loan details - /// @dev Retrieves the borrower’s loan details before proceeding with the repayment. - sendRequest(globalLedger, 0, 11_000_000, context, callData, handleRepayment); + /// @dev Retrieves the borrower's loan details before proceeding with the repayment. + sendRequest( + globalLedger, + 0, + 11_000_000, + context, + callData, + handleRepayment + ); } /// @notice Handle the loan repayment, calculate the interest, and update GlobalLedger. @@ -331,10 +663,7 @@ contract LendingPool is NilBase, NilTokenBase, NilAwaitable { token, 0 // Mark the loan as repaid ); - bytes memory releaseCollateralContext = abi.encode( - borrower, - token - ); + bytes memory releaseCollateralContext = abi.encode(borrower, token); /// @notice Send request to GlobalLedger to update the loan status /// @dev Updates the loan status to indicate repayment completion in the GlobalLedger. diff --git a/academy/lending-protocol/task/run-lending-protocol.ts b/academy/lending-protocol/task/run-lending-protocol.ts index a232b9d40..3d2b79ba3 100644 --- a/academy/lending-protocol/task/run-lending-protocol.ts +++ b/academy/lending-protocol/task/run-lending-protocol.ts @@ -108,8 +108,11 @@ task( `Oracle deployed at ${deployOracle} with hash ${deployOracleTx.hash} on shard 4`, ); - // Deploy LendingPool contract on shard 1, linking all other contracts - const { address: deployLendingPool, tx: deployLendingPoolTx } = + // Deploy LendingPool contracts on all four shards + const lendingPoolAddresses: string[] = []; + + // Deploy LendingPool contract on shard 1 + const { address: deployLendingPool1, tx: deployLendingPool1Tx } = await deployerWallet.deployContract({ shardId: 1, args: [ @@ -124,10 +127,96 @@ task( salt: BigInt(Math.floor(Math.random() * 10000)), }); - await deployLendingPoolTx.wait(); + await deployLendingPool1Tx.wait(); + console.log( + `Lending Pool deployed at ${deployLendingPool1} with hash ${deployLendingPool1Tx.hash} on shard 1`, + ); + lendingPoolAddresses.push(deployLendingPool1); + + // Deploy LendingPool contract on shard 2 + const { address: deployLendingPool2, tx: deployLendingPool2Tx } = + await deployerWallet.deployContract({ + shardId: 2, + args: [ + deployGlobalLedger, + deployInterestManager, + deployOracle, + process.env.USDT, + process.env.ETH, + ], + bytecode: LendingPool.bytecode as `0x${string}`, + abi: LendingPool.abi as Abi, + salt: BigInt(Math.floor(Math.random() * 10000)), + }); + + await deployLendingPool2Tx.wait(); + console.log( + `Lending Pool deployed at ${deployLendingPool2} with hash ${deployLendingPool2Tx.hash} on shard 2`, + ); + lendingPoolAddresses.push(deployLendingPool2); + + // Deploy LendingPool contract on shard 3 + const { address: deployLendingPool3, tx: deployLendingPool3Tx } = + await deployerWallet.deployContract({ + shardId: 3, + args: [ + deployGlobalLedger, + deployInterestManager, + deployOracle, + process.env.USDT, + process.env.ETH, + ], + bytecode: LendingPool.bytecode as `0x${string}`, + abi: LendingPool.abi as Abi, + salt: BigInt(Math.floor(Math.random() * 10000)), + }); + + await deployLendingPool3Tx.wait(); console.log( - `Lending Pool deployed at ${deployLendingPool} with hash ${deployLendingPoolTx.hash} on shard 1`, + `Lending Pool deployed at ${deployLendingPool3} with hash ${deployLendingPool3Tx.hash} on shard 3`, ); + lendingPoolAddresses.push(deployLendingPool3); + + // Deploy LendingPool contract on shard 4 + const { address: deployLendingPool4, tx: deployLendingPool4Tx } = + await deployerWallet.deployContract({ + shardId: 4, + args: [ + deployGlobalLedger, + deployInterestManager, + deployOracle, + process.env.USDT, + process.env.ETH, + ], + bytecode: LendingPool.bytecode as `0x${string}`, + abi: LendingPool.abi as Abi, + salt: BigInt(Math.floor(Math.random() * 10000)), + }); + + await deployLendingPool4Tx.wait(); + console.log( + `Lending Pool deployed at ${deployLendingPool4} with hash ${deployLendingPool4Tx.hash} on shard 4`, + ); + lendingPoolAddresses.push(deployLendingPool4); + + // Register all lending pools in each lending pool contract for cross-shard communication + for (let i = 0; i < lendingPoolAddresses.length; i++) { + const registerPools = encodeFunctionData({ + abi: LendingPool.abi as Abi, + functionName: "registerLendingPools", + args: [lendingPoolAddresses], + }); + + const registerPoolsTx = await deployerWallet.sendTransaction({ + to: lendingPoolAddresses[i], + data: registerPools, + }); + + await registerPoolsTx.wait(); + console.log( + `Registered all lending pools in lending pool ${lendingPoolAddresses[i]} with tx hash ${registerPoolsTx.hash}`, + ); + } // Generate two smart accounts (account1 and account2) const account1 = await generateSmartAccount({ @@ -279,14 +368,15 @@ task( console.log(`Price of USDT is ${usdtPrice}`); console.log(`Price of ETH is ${ethPrice}`); - // Perform a deposit of USDT by account1 into the LendingPool + // Perform deposits across different shards + // Account1 deposits USDT into the LendingPool on shard 1 const depositUSDT = { id: process.env.USDT as `0x${string}`, amount: 12n, }; const depositUSDTResponse = await account1.sendTransaction({ - to: deployLendingPool, + to: deployLendingPool1, functionName: "deposit", abi: LendingPool.abi as Abi, tokens: [depositUSDT], @@ -296,14 +386,14 @@ task( await waitTillCompleted(client, depositUSDTResponse); console.log(`Account 1 deposited 12 USDT at tx hash ${depositUSDTResponse}`); - // Perform a deposit of ETH by account2 into the LendingPool + // Account2 deposits ETH into the LendingPool on shard 3 const depositETH = { id: process.env.ETH as `0x${string}`, amount: 5n, }; const depositETHResponse = await account2.sendTransaction({ - to: deployLendingPool, + to: deployLendingPool3, functionName: "deposit", abi: LendingPool.abi as Abi, tokens: [depositETH], @@ -312,7 +402,7 @@ task( await depositETHResponse.wait(); console.log( - `Account 2 deposited 1 ETH at tx hash ${depositETHResponse.hash}`, + `Account 2 deposited 5 ETH at tx hash ${depositETHResponse.hash}`, ); // Retrieve the deposit balances of account1 and account2 from GlobalLedger @@ -334,7 +424,7 @@ task( console.log(`Account 1 balance in global ledger is ${account1Balance}`); console.log(`Account 2 balance in global ledger is ${account2Balance}`); - // Perform a borrow operation by account1 for 1 ETH + // Test cross-shard borrowing: Account1 tries to borrow ETH from shard 1, which will need to pull from shard 3 const borrowETH = encodeFunctionData({ abi: LendingPool.abi as Abi, functionName: "borrow", @@ -348,7 +438,7 @@ task( console.log("Account 1 balance before borrow:", account1BalanceBeforeBorrow); const borrowETHResponse = await account1.sendTransaction({ - to: deployLendingPool, + to: deployLendingPool1, data: borrowETH, feeCredit: convertEthToWei(0.001), }); @@ -395,14 +485,14 @@ task( }); const repayETHResponse = await account1.sendTransaction({ - to: deployLendingPool, + to: deployLendingPool1, data: repayETHData, tokens: repayETH, feeCredit: convertEthToWei(0.001), }); await repayETHResponse.wait(); - console.log(`Account 1 repaid 1 ETH at tx hash ${repayETHResponse.hash}`); + console.log(`Account 1 repaid 6 ETH at tx hash ${repayETHResponse.hash}`); const account1BalanceAfterRepay = await client.getTokens( account1.address,