diff --git a/academy/lending-protocol/contracts/CollateralManager.sol b/academy/lending-protocol/contracts/CollateralManager.sol index f7bb4e39d..b285143d8 100644 --- a/academy/lending-protocol/contracts/CollateralManager.sol +++ b/academy/lending-protocol/contracts/CollateralManager.sol @@ -3,61 +3,334 @@ pragma solidity ^0.8.28; import "@nilfoundation/smart-contracts/contracts/Nil.sol"; import "@nilfoundation/smart-contracts/contracts/NilTokenBase.sol"; +import "./LendingPool.sol"; -/// @title GlobalLedger -/// @dev The GlobalLedger contract is responsible for tracking user deposits and loans in the lending protocol. -/// It stores the deposit balances for users and keeps track of the loans each user has taken. -contract GlobalLedger { - /// @dev Mapping of user addresses to their token deposits (token -> amount). - mapping(address => mapping(TokenId => uint256)) public deposits; +/// @title GlobalLedger (Centralized Liquidity and Logic Hub) +/// @notice This contract manages all collateral, loans, and liquidity for the lending protocol. +/// It also handles the deployment and registration of LendingPool contracts across different shards. +/// @dev Acts as the central state and execution core. LendingPool contracts interact with this contract +/// via asynchronous calls to handle user deposits, borrows, and repayments. +contract GlobalLedger is NilTokenBase { + /// @notice The address of the deployer, allowed to call administrative functions. + address public deployer; + /// @notice The address of the InterestManager contract used to fetch interest rates. + address public interestManager; + /// @notice The address of the Oracle contract used to fetch token prices (indirectly via LendingPool). + address public oracle; + /// @notice The TokenId representing the USDT token. + TokenId public usdt; + /// @notice The TokenId representing the ETH token. + TokenId public eth; - /// @dev Mapping of user addresses to their loans (loan amount and loan token). + // --- Errors --- // + /// @dev Reverts if a function requiring deployer privileges is called by another address. + error OnlyDeployer(); + /// @dev Reverts if trying to register or use an invalid (zero) pool address. + error InvalidPoolAddress(); + /// @dev Reverts if trying to register a pool on a shard that already has one, or if the pool address is already registered. + error PoolAlreadyRegistered(); + /// @dev Reverts if a function requiring a registered LendingPool caller is called by another address. + error UnauthorizedCaller(); + /// @dev Reverts during borrow if the user's collateral balance is less than required. + error InsufficientCollateral(); + /// @dev Reverts during borrow if the GlobalLedger lacks sufficient liquidity of the requested token. + error InsufficientLiquidity(); + /// @dev Reverts during repayment if the amount sent is less than the required repayment (principal + interest). + error RepaymentInsufficient(); + /// @dev Reverts during repayment if the user has no active loan or is repaying the wrong token. + error NoActiveLoan(); + /// @dev Reverts if an expected cross-shard call (e.g., to Oracle, InterestManager) fails during processing. + error CrossShardCallFailed(string message); + /// @dev Reverts during deployment if an invalid shard ID (0 or >= 0xFFFF) is provided. + error ShardIdInvalid(); + + // --- Events --- // + /// @notice Emitted when a LendingPool address is successfully registered for a specific shard. + /// @param poolAddress The address of the registered LendingPool contract. + /// @param shardId The shard ID the pool is registered for. + event PoolRegistered(address indexed poolAddress, uint256 shardId); + /// @notice Emitted when a new LendingPool contract is successfully deployed via `deployLendingPools`. + /// @param poolAddress The address of the newly deployed LendingPool contract. + /// @param shardId The shard ID the pool was deployed onto. + event LendingPoolDeployed(address indexed poolAddress, uint256 shardId); + /// @notice Emitted when a deposit forwarded from a LendingPool is successfully processed. + /// @param user The original depositor's address. + /// @param token The TokenId of the deposited asset. + /// @param amount The amount deposited. + event DepositHandled(address indexed user, TokenId token, uint256 amount); + /// @notice Emitted when a borrow request is successfully processed and funds are sent. + /// @param borrower The borrower's address. + /// @param token The TokenId of the borrowed asset. + /// @param amount The amount borrowed. + event BorrowProcessed( + address indexed borrower, + TokenId token, + uint256 amount + ); + /// @notice Emitted when a loan repayment is successfully processed (loan record cleared). + /// @param borrower The borrower's address. + /// @param token The TokenId of the repaid asset (loan token). + /// @param amount The principal amount of the loan that was cleared. + event RepaymentProcessed( + address indexed borrower, + TokenId token, + uint256 amount + ); + /// @notice Emitted when collateral is successfully returned to the borrower after repayment. + /// @param borrower The borrower's address. + /// @param token The TokenId of the collateral asset returned. + /// @param amount The amount of collateral returned. + event CollateralReturned( + address indexed borrower, + TokenId token, + uint256 amount + ); + + // --- State Variables --- // + /// @notice Mapping from shard ID to the registered LendingPool address for that shard. + mapping(uint256 => address) public lendingPoolsByShard; + /// @notice Mapping from an address to a boolean indicating if it is a registered LendingPool. + /// @dev Provides a quick lookup for the `onlyRegisteredLendingPool` modifier. + mapping(address => bool) public isLendingPool; + + /// @notice Mapping storing user collateral balances. + /// @dev `collateralBalances[userAddress][tokenId] = amount` + mapping(address => mapping(TokenId => uint256)) public collateralBalances; + + /// @notice Mapping storing active loan details for each user. + /// @dev `loans[userAddress] = Loan({amount, token})` mapping(address => Loan) public loans; - /// @dev Struct to store loan details: amount and the token type. + /// @dev Struct to store loan details. struct Loan { - uint256 amount; - TokenId token; + uint256 amount; // Principal amount borrowed + TokenId token; // Token borrowed + } + + // --- Modifiers --- // + /// @dev Ensures that the caller is the deployer address. + modifier onlyDeployer() { + if (msg.sender != deployer) revert OnlyDeployer(); + _; + } + + /// @dev Ensures that the caller is a registered LendingPool contract. + modifier onlyRegisteredLendingPool() { + if (!isLendingPool[msg.sender]) revert UnauthorizedCaller(); + _; + } + + /// @notice Initializes the GlobalLedger contract. + /// @param _interestManager Address of the InterestManager contract. + /// @param _oracle Address of the Oracle contract. + /// @param _usdt TokenId for USDT. + /// @param _eth TokenId for ETH. + constructor( + address _interestManager, + address _oracle, + TokenId _usdt, + TokenId _eth + ) { + deployer = msg.sender; + interestManager = _interestManager; + oracle = _oracle; + usdt = _usdt; + eth = _eth; } - /// @notice Records a user's deposit into the ledger. - /// @dev Increases the deposit balance for the user for the specified token. - /// @param user The address of the user making the deposit. - /// @param token The token type being deposited (e.g., USDT, ETH). - /// @param amount The amount of the token being deposited. - function recordDeposit(address user, TokenId token, uint256 amount) public { - deposits[user][token] += amount; + /// @notice Manually registers a LendingPool contract for a specific shard. + /// @dev Can only be called by the deployer. Primarily used if pools are deployed separately. + /// @param poolAddress The address of the LendingPool to register. + function registerLendingPool(address poolAddress) public onlyDeployer { + if (poolAddress == address(0)) revert InvalidPoolAddress(); + + uint256 shardId = Nil.getShardId(poolAddress); + // Check if shard already has a pool or if address is already registered + if ( + lendingPoolsByShard[shardId] != address(0) || + isLendingPool[poolAddress] + ) { + revert PoolAlreadyRegistered(); + } + + lendingPoolsByShard[shardId] = poolAddress; + isLendingPool[poolAddress] = true; + emit PoolRegistered(poolAddress, shardId); } - /// @notice Fetches a user's deposit balance for a specific token. - /// @dev Returns the amount of the token deposited by the user. - /// @param user The address of the user whose deposit balance is being fetched. - /// @param token The token type for which the balance is being fetched. - /// @return uint256 The deposit amount for the given user and token. - function getDeposit( + /// @notice Deploys LendingPool contracts to the specified shards using `Nil.asyncDeploy`. + /// @dev Automatically registers the deployed pools. Skips shards where a pool is already registered. + /// Can only be called by the deployer. + /// @param shardIds An array of shard IDs to deploy LendingPool contracts onto. + function deployLendingPools(uint[] calldata shardIds) public onlyDeployer { + for (uint i = 0; i < shardIds.length; i++) { + uint shardId = shardIds[i]; + + // Check if a pool already exists for this shard + if (lendingPoolsByShard[shardId] != address(0)) { + continue; // Skip deployment for this shard + } + + if (shardId == 0 || shardId >= 0xFFFF) revert ShardIdInvalid(); + + // Generate a deterministic salt for the deployment + bytes32 salt = keccak256( + abi.encodePacked(deployer, shardId, address(this)) + ); + + // ABI-encode constructor arguments for LendingPool + bytes memory constructorArgs = abi.encode( + address(this), // _centralLedger + interestManager, + oracle, + usdt, + eth + ); + + // Combine creation code and arguments + bytes memory deploymentCode = bytes.concat( + type(LendingPool).creationCode, + constructorArgs + ); + + // Deploy asynchronously + address deployedPoolAddress = Nil.asyncDeploy( + shardId, + deployer, // refundTo (deployer initiated this) + deployer, // bounceTo (send error back to deployer if deployment fails) + 0, // feeCredit (default) + Nil.FORWARD_REMAINING, // forwardKind (default) + 0, // value + deploymentCode, + uint256(salt) + ); + + // Register the deployed address + lendingPoolsByShard[shardId] = deployedPoolAddress; + isLendingPool[deployedPoolAddress] = true; + emit PoolRegistered(deployedPoolAddress, shardId); // Emit for consistency + emit LendingPoolDeployed(deployedPoolAddress, shardId); + } + } + + /// @notice Processes a deposit forwarded asynchronously from a LendingPool contract. + /// @dev Expects the deposited tokens to be included in the transaction (`Nil.txnTokens`). + /// Increases the collateral balance for the original depositor. + /// Can only be called by a registered LendingPool. + /// @param depositor The address of the original user making the deposit. + function handleDeposit( + address depositor + ) public payable onlyRegisteredLendingPool { + Nil.Token[] memory tokens = Nil.txnTokens(); + TokenId token = tokens[0].id; + uint256 amount = tokens[0].amount; + + collateralBalances[depositor][token] += amount; + emit DepositHandled(depositor, token, amount); + } + + /// @notice Processes a borrow request forwarded asynchronously from a LendingPool contract. + /// @dev Checks liquidity, verifies collateral, records the loan, and sends the borrowed tokens. + /// Can only be called by a registered LendingPool. + /// @param borrower The address of the user borrowing. + /// @param amount The amount to borrow. + /// @param borrowToken The token to borrow. + /// @param requiredCollateral The minimum collateral value required (pre-calculated by LendingPool). + /// @param collateralToken The token used as collateral. + function handleBorrowRequest( + address borrower, + uint256 amount, + TokenId borrowToken, + uint256 requiredCollateral, + TokenId collateralToken + ) public onlyRegisteredLendingPool { + // Check internal liquidity (this contract's balance) + if (Nil.tokenBalance(address(this), borrowToken) < amount) { + revert InsufficientLiquidity(); + } + + // Check user's collateral balance stored here + if ( + collateralBalances[borrower][collateralToken] < requiredCollateral + ) { + revert InsufficientCollateral(); + } + + // Record the loan + loans[borrower] = Loan(amount, borrowToken); + + // Send the borrowed tokens directly to the borrower from this contract's funds + sendTokenInternal(borrower, borrowToken, amount); + + emit BorrowProcessed(borrower, borrowToken, amount); + } + + /// @notice Processes a repayment forwarded asynchronously from a LendingPool contract. + /// @dev Expects the repayment tokens to be included in the transaction (`Nil.txnTokens`). + /// Verifies the repayment amount against the required amount (principal + interest), + /// clears the loan record, and returns the collateral to the borrower. + /// Can only be called by a registered LendingPool. + /// @param borrower The address of the user repaying. + /// @param collateralToken The token used as collateral for the loan being repaid. + /// @param requiredRepaymentAmount The total amount (principal + interest) required (pre-calculated by LendingPool). + function processRepayment( + address borrower, + TokenId collateralToken, + uint256 requiredRepaymentAmount + ) public payable onlyRegisteredLendingPool { + Nil.Token[] memory tokens = Nil.txnTokens(); + TokenId repaidToken = tokens[0].id; + uint256 sentAmount = tokens[0].amount; + + Loan memory loan = loans[borrower]; + + // Check if there is an active loan and the correct token is being repaid + if (loan.amount == 0 || loan.token != repaidToken) { + revert NoActiveLoan(); + } + + // Ensure sufficient funds were sent for principal + interest + if (sentAmount < requiredRepaymentAmount) { + revert RepaymentInsufficient(); + } + + // Clear the loan record + delete loans[borrower]; + emit RepaymentProcessed(borrower, repaidToken, loan.amount); + + // Handle collateral release + uint256 collateralAmount = collateralBalances[borrower][ + collateralToken + ]; + if (collateralAmount > 0) { + delete collateralBalances[borrower][collateralToken]; + sendTokenInternal(borrower, collateralToken, collateralAmount); + emit CollateralReturned( + borrower, + collateralToken, + collateralAmount + ); + } + } + + /// @notice Fetches a user's collateral balance for a specific token. + /// @param user The address of the user. + /// @param token The token type. + /// @return uint256 The collateral amount. + function getCollateralBalance( address user, TokenId token ) public view returns (uint256) { - return deposits[user][token]; // Return the deposit amount for the given user and token - } - - /// @notice Records a user's loan in the ledger. - /// @dev Stores the amount of the loan and the token type used for the loan. - /// @param user The address of the user taking the loan. - /// @param token The token type used for the loan (e.g., USDT, ETH). - /// @param amount The amount of the loan being taken. - function recordLoan(address user, TokenId token, uint256 amount) public { - loans[user] = Loan(amount, token); + return collateralBalances[user][token]; } - /// @notice Retrieves a user's loan details. - /// @dev Returns the loan amount and the token used for the loan. - /// @param user The address of the user whose loan details are being fetched. - /// @return uint256 The loan amount. - /// @return TokenId The token type used for the loan. + /// @notice Retrieves a user's active loan details. + /// @param user The address of the user. + /// @return amount_ The loan principal amount (0 if no active loan). + /// @return token_ The token type used for the loan (address(0) if no active loan). function getLoanDetails( address user - ) public view returns (uint256, TokenId) { + ) public view returns (uint256 amount_, TokenId token_) { return (loans[user].amount, loans[user].token); } } diff --git a/academy/lending-protocol/contracts/LendingPool.sol b/academy/lending-protocol/contracts/LendingPool.sol index 6858a757b..4e6348710 100644 --- a/academy/lending-protocol/contracts/LendingPool.sol +++ b/academy/lending-protocol/contracts/LendingPool.sol @@ -4,117 +4,153 @@ pragma solidity ^0.8.28; import "@nilfoundation/smart-contracts/contracts/Nil.sol"; import "@nilfoundation/smart-contracts/contracts/NilTokenBase.sol"; -/// @title LendingPool -/// @dev The LendingPool contract facilitates lending and borrowing of tokens and handles collateral management. -/// It interacts with other contracts such as GlobalLedger, InterestManager, and Oracle for tracking deposits, calculating interest, and fetching token prices. -contract LendingPool is NilBase, NilTokenBase { - address public globalLedger; +/// @title LendingPool (Shard Entry Point) +/// @author Your Name/Organization +/// @notice Acts as a user-facing entry point on a specific shard for the lending protocol. +/// @dev Handles user interactions (deposit, borrow, repay) and orchestrates asynchronous calls +/// to the CentralLedger, Oracle, and InterestManager contracts to fulfill requests. +/// This contract itself does not hold significant state or liquidity. +contract LendingPool is NilTokenBase { + // --- State Variables --- // + /// @notice The address of the central GlobalLedger contract. + address public centralLedger; + /// @notice The address of the InterestManager contract. address public interestManager; + /// @notice The address of the Oracle contract. address public oracle; + /// @notice The TokenId for the USDT token. TokenId public usdt; + /// @notice The TokenId for the ETH token. TokenId public eth; - /// @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. + // --- Errors --- // + /// @dev Reverts if an unsupported or invalid token (not USDT or ETH) is used. + error InvalidToken(); + /// @dev Reverts if a user provides insufficient funds (e.g., for repayment) or collateral. + error InsufficientFunds(string message); + /// @dev Reverts if a required cross-shard call (e.g., to Oracle, InterestManager, CentralLedger) fails. + error CrossShardCallFailed(string message); + + // --- Events --- // + /// @notice Emitted when a user initiates a deposit via this pool. + /// @param user The address of the user initiating the deposit. + /// @param token The TokenId of the asset being deposited. + /// @param amount The amount being deposited. + event DepositInitiated(address indexed user, TokenId token, uint256 amount); + /// @notice Emitted when a user initiates a borrow request. + /// @param borrower The address of the user initiating the borrow. + /// @param amount The amount requested to borrow. + /// @param borrowToken The TokenId of the asset requested. + /// @param collateralToken The TokenId of the asset used as collateral. + event LoanRequested( + address indexed borrower, + uint256 amount, + TokenId borrowToken, + TokenId collateralToken + ); + /// @notice Emitted when a user initiates a loan repayment. + /// @param borrower The address of the user initiating the repayment. + /// @param token The TokenId of the asset being repaid. + /// @param amount The amount sent for repayment. + event RepaymentInitiated( + address indexed borrower, + TokenId token, + uint256 amount + ); + + /// @notice Initializes the LendingPool shard contract. + /// @param _centralLedger The address of the central GlobalLedger contract. /// @param _interestManager The address of the InterestManager contract. /// @param _oracle The address of the Oracle contract. /// @param _usdt The TokenId for USDT. /// @param _eth The TokenId for ETH. constructor( - address _globalLedger, + address _centralLedger, address _interestManager, address _oracle, TokenId _usdt, TokenId _eth ) { - globalLedger = _globalLedger; + centralLedger = _centralLedger; interestManager = _interestManager; oracle = _oracle; usdt = _usdt; eth = _eth; } - /// @notice Deposit function to deposit tokens into the lending pool. - /// @dev The deposited tokens are recorded in the GlobalLedger via an asynchronous call. + /// @notice Allows a user to deposit collateral (USDT or ETH) into the protocol. + /// @dev Forwards the deposit details and tokens to the `CentralLedger` via an asynchronous call. + /// Requires the user to send exactly one type of token (USDT or ETH) with the transaction. function deposit() public payable { - /// Retrieve the tokens being sent in the transaction Nil.Token[] memory tokens = Nil.txnTokens(); + require(tokens.length == 1, "Only one token type per deposit"); + address user = msg.sender; - /// @notice Encoding the call to the GlobalLedger to record the deposit - /// @dev The deposit details (user address, token type, and amount) are encoded for GlobalLedger. - /// @param callData The encoded call data for recording the deposit in GlobalLedger. - bytes memory callData = abi.encodeWithSignature( - "recordDeposit(address,address,uint256)", - msg.sender, - tokens[0].id, // The token being deposited (usdt or eth) - tokens[0].amount // The amount of the token being deposited + bytes memory callData = abi.encodeWithSelector( + bytes4(keccak256("handleDeposit(address)")), + user ); - /// @notice Making an asynchronous call to the GlobalLedger to record the deposit - /// @dev This ensures that the user's deposit is recorded in GlobalLedger asynchronously. - Nil.asyncCall(globalLedger, address(this), 0, callData); + // Use Nil.asyncCallWithTokens to forward deposit to CentralLedger + Nil.asyncCallWithTokens( + centralLedger, + user, // refundTo (original user) + user, // bounceTo (original user) + 0, // feeCredit (default) + Nil.FORWARD_REMAINING, // forwardKind (default) + 0, // value + tokens, // The deposit tokens + callData + ); + + emit DepositInitiated(user, tokens[0].id, tokens[0].amount); } - /// @notice Borrow function allows a user to borrow tokens (either USDT or ETH). - /// @dev Ensures sufficient liquidity, checks collateral, and processes the loan after fetching the price from the Oracle. + /// @notice Allows a user to initiate a request to borrow USDT or ETH. + /// @dev Requires the user to have sufficient collateral deposited in the CentralLedger. + /// The process involves multiple asynchronous steps: + /// 1. This function requests the price of the `borrowToken` from the `Oracle`. + /// 2. The `processOracleResponse` callback calculates required collateral and requests the user's collateral balance from `CentralLedger`. + /// 3. The `finalizeBorrow` callback verifies collateral and requests `CentralLedger` to execute the borrow. /// @param amount The amount of the token to borrow. - /// @param borrowToken The token the user wants to borrow (either USDT or ETH). + /// @param borrowToken The TokenId of the token to borrow (must be USDT or ETH). function borrow(uint256 amount, TokenId borrowToken) public payable { - /// @notice Ensure the token being borrowed is either USDT or ETH - /// @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" - ); + if (borrowToken != usdt && borrowToken != eth) revert InvalidToken(); - /// @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; - /// @notice Prepare a call to the Oracle to get the price of the borrow token - /// @dev The price of the borrow token is fetched from the Oracle to calculate collateral. - /// @param callData The encoded data to fetch the price from the Oracle. - bytes memory callData = abi.encodeWithSignature( - "getPrice(address)", + // Step 1: Get Price from Oracle + bytes memory oracleCallData = abi.encodeWithSignature( + "getPrice(address)", // Oracle expects address type for TokenId borrowToken ); - /// @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. bytes memory context = abi.encodeWithSelector( - this.processLoan.selector, - msg.sender, + this.processOracleResponse.selector, // Callback function + msg.sender, // borrower amount, borrowToken, collateralToken ); - /// @notice Send a request to the Oracle to get the price of the borrow token. - /// @dev This request is processed with a fee for the transaction, allowing the system to fetch the token price. - Nil.sendRequest(oracle, 0, 9_000_000, context, callData); + Nil.sendRequest(oracle, 0, 11_000_000, context, oracleCallData); + + emit LoanRequested(msg.sender, amount, borrowToken, collateralToken); } - /// @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. - /// @param returnData The price data returned from the Oracle. - /// @param context The context data containing borrower details, loan amount, and collateral token. - function processLoan( + /// @notice Callback function triggered after the Oracle returns the borrow token price. + /// @dev Calculates the required collateral value based on the price and LTV (hardcoded 120% here). + /// Sends a request to the `CentralLedger` to get the user's current collateral balance. + /// @param success Boolean indicating if the Oracle call was successful. + /// @param returnData Encoded price data (uint256) from the Oracle. + /// @param context Encoded context data passed from the initial `borrow` call. + function processOracleResponse( bool success, bytes memory returnData, bytes memory context ) public payable { - /// @notice Ensure the Oracle call was successful - /// @dev Verifies that the price data was successfully retrieved from the Oracle. - require(success, "Oracle call failed"); + if (!success) revert CrossShardCallFailed("Oracle price call failed"); - /// @notice Decode the context to extract borrower details, loan amount, and collateral token - /// @dev Decodes the context passed from the borrow function to retrieve necessary data. ( address borrower, uint256 amount, @@ -122,165 +158,161 @@ contract LendingPool is NilBase, NilTokenBase { TokenId collateralToken ) = abi.decode(context, (address, uint256, TokenId, TokenId)); - /// @notice Decode the price data returned from the Oracle - /// @dev The returned price data is used to calculate the loan value in USD. uint256 borrowTokenPrice = abi.decode(returnData, (uint256)); - /// @notice Calculate the loan value in USD - /// @dev Multiplies the amount by the borrow token price to get the loan value in USD. + + // Calculate required collateral value (e.g., 120% LTV) + // Ensure prices are scaled appropriately for calculation uint256 loanValueInUSD = amount * borrowTokenPrice; - /// @notice Calculate the required collateral (120% of the loan value) - /// @dev The collateral is calculated as 120% of the loan value to mitigate risk. - uint256 requiredCollateral = (loanValueInUSD * 120) / 100; + uint256 requiredCollateralValue = (loanValueInUSD * 120) / 100; - /// @notice Prepare a call to GlobalLedger to check the user's collateral balance - /// @dev Fetches the collateral balance from the GlobalLedger contract to ensure sufficient collateral. + // Step 2: Get Collateral Balance from CentralLedger bytes memory ledgerCallData = abi.encodeWithSignature( - "getDeposit(address,address)", + "getCollateralBalance(address,address)", // CentralLedger expects address type for TokenId borrower, collateralToken ); - /// @notice Encoding the context to finalize the loan once the collateral is validated - /// @dev Once the collateral balance is validated, the loan is finalized and processed. bytes memory ledgerContext = abi.encodeWithSelector( - this.finalizeLoan.selector, + this.finalizeBorrow.selector, // Next callback function borrower, amount, borrowToken, - requiredCollateral + requiredCollateralValue, // Pass the calculated required collateral value + collateralToken ); - /// @notice Send request to GlobalLedger to get the user's collateral - /// @dev The fee for this request is retained for processing the collateral validation response. Nil.sendRequest( - globalLedger, + centralLedger, 0, - 6_000_000, + 8_000_000, ledgerContext, ledgerCallData ); } - /// @notice Finalize the loan by ensuring sufficient collateral and recording the loan in GlobalLedger. - /// @dev Verifies that the user has enough collateral, processes the loan, and sends the borrowed tokens to the borrower. - /// @param success Indicates if the collateral check was successful. - /// @param returnData The collateral balance returned from the GlobalLedger. - /// @param context The context containing loan details. - function finalizeLoan( + /// @notice Callback function triggered after the CentralLedger returns the user's collateral balance. + /// @dev Verifies if the user's collateral value meets the required amount. + /// If sufficient, sends an asynchronous call to the `CentralLedger` to execute the borrow + /// (transfer funds to borrower, record loan state). + /// @param success Boolean indicating if the CentralLedger call was successful. + /// @param returnData Encoded collateral balance (uint256) from the CentralLedger. + /// @param context Encoded context data passed from the `processOracleResponse` call. + function finalizeBorrow( bool success, bytes memory returnData, bytes memory context ) public payable { - /// @notice Ensure the collateral check was successful - /// @dev Verifies the collateral validation result from GlobalLedger. - require(success, "Ledger call failed"); + if (!success) revert CrossShardCallFailed("Collateral check failed"); - /// @notice Decode the context to extract loan details - /// @dev Decodes the context passed from the processLoan function to retrieve loan data. ( address borrower, uint256 amount, TokenId borrowToken, - uint256 requiredCollateral - ) = abi.decode(context, (address, uint256, TokenId, uint256)); - - /// @notice Decode the user's collateral balance from GlobalLedger - /// @dev Retrieves the user's collateral balance from the GlobalLedger to compare it with the required collateral. - uint256 userCollateral = abi.decode(returnData, (uint256)); - - /// @notice Check if the user has enough collateral to cover the loan - /// @dev Ensures the borrower has sufficient collateral before proceeding with the loan. - require( - userCollateral >= requiredCollateral, - "Insufficient collateral" - ); - - /// @notice Record the loan in GlobalLedger - /// @dev The loan details are recorded in the GlobalLedger contract. - bytes memory recordLoanCallData = abi.encodeWithSignature( - "recordLoan(address,address,uint256)", + uint256 requiredCollateralValue, + TokenId collateralToken + ) = abi.decode(context, (address, uint256, TokenId, uint256, TokenId)); + + uint256 userCollateralValue = abi.decode(returnData, (uint256)); // Assuming Ledger returns collateral value + + // Check collateral sufficiency + if (userCollateralValue < requiredCollateralValue) { + revert InsufficientFunds("Insufficient collateral value"); + } + + // Step 3: Execute Borrow on CentralLedger + bytes memory centralLedgerCallData = abi.encodeWithSelector( + bytes4( + keccak256( + "handleBorrowRequest(address,uint256,address,uint256,address)" + ) + ), borrower, + amount, borrowToken, - amount + requiredCollateralValue, // Pass required value for CentralLedger check + collateralToken ); - Nil.asyncCall(globalLedger, address(this), 0, recordLoanCallData); - /// @notice Send the borrowed tokens to the borrower - /// @dev Transfers the loan amount to the borrower's address after finalizing the loan. - sendTokenInternal(borrower, borrowToken, amount); + // Use Nil.asyncCall with explicit refundTo/bounceTo + Nil.asyncCall( + centralLedger, + borrower, // refundTo (original user) + borrower, // bounceTo (original user) + 0, // feeCredit (default) + Nil.FORWARD_REMAINING, // forwardKind (default) + 0, // value + centralLedgerCallData + ); } - /// @notice Repay loan function called by the borrower to repay their loan. - /// @dev Initiates the repayment process by retrieving the loan details from GlobalLedger. + /// @notice Allows a user to initiate repayment of their active loan. + /// @dev Requires the user to send the repayment token (must match the borrowed token) with the transaction. + /// The process involves multiple asynchronous steps: + /// 1. This function requests loan details from the `CentralLedger`. + /// 2. `handleLoanDetailsForRepayment` callback verifies loan and requests interest rate from `InterestManager`. + /// 3. `processRepaymentCalculation` callback calculates total repayment and calls `CentralLedger` to process it (clear loan, return collateral). function repayLoan() public payable { - /// @notice Retrieve the tokens being sent in the transaction - /// @dev Retrieves the tokens involved in the repayment. Nil.Token[] memory tokens = Nil.txnTokens(); + require(tokens.length == 1, "Only one token type per repayment"); + TokenId repaidToken = tokens[0].id; + uint256 sentAmount = tokens[0].amount; + address borrower = msg.sender; - /// @notice Prepare to query the loan details from GlobalLedger - /// @dev Fetches the loan details of the borrower to proceed with repayment. - bytes memory callData = abi.encodeWithSignature( + // Step 1: Get Loan Details from CentralLedger + bytes memory getLoanCallData = abi.encodeWithSignature( "getLoanDetails(address)", - msg.sender + borrower ); - /// @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.encodeWithSelector( - this.handleRepayment.selector, - msg.sender, - tokens[0].amount + this.handleLoanDetailsForRepayment.selector, // Callback function + borrower, + sentAmount, + repaidToken // Pass token sent by user for validation ); - /// @notice Send request to GlobalLedger to fetch loan details - /// @dev Retrieves the borrower’s loan details before proceeding with the repayment. - Nil.sendRequest(globalLedger, 0, 11_000_000, context, callData); + Nil.sendRequest(centralLedger, 0, 8_000_000, context, getLoanCallData); + + emit RepaymentInitiated(borrower, repaidToken, sentAmount); } - /// @notice Handle the loan repayment, calculate the interest, and update GlobalLedger. - /// @dev Calculates the total repayment (principal + interest) and updates the loan status in GlobalLedger. - /// @param success Indicates if the loan details retrieval was successful. - /// @param returnData The loan details returned from the GlobalLedger. - /// @param context The context containing borrower and repayment details. - function handleRepayment( + /// @notice Callback function triggered after fetching loan details for repayment. + /// @dev Verifies that an active loan exists and the user is repaying the correct token. + /// Sends a request to the `InterestManager` to get the current interest rate. + /// @param success Boolean indicating if the CentralLedger call was successful. + /// @param returnData Encoded loan details (uint256 amount, TokenId token) from CentralLedger. + /// @param context Encoded context data passed from the initial `repayLoan` call. + function handleLoanDetailsForRepayment( bool success, bytes memory returnData, bytes memory context ) public payable { - /// @notice Ensure the GlobalLedger call was successful - /// @dev Verifies that the loan details were successfully retrieved from the GlobalLedger. - require(success, "Ledger call failed"); - - /// @notice Decode context and loan details - /// @dev Decodes the context and the return data to retrieve the borrower's loan details. - (address borrower, uint256 sentAmount) = abi.decode( - context, - (address, uint256) - ); - (uint256 amount, TokenId token) = abi.decode( + if (!success) revert CrossShardCallFailed("Get loan details failed"); + + (address borrower, uint256 sentAmount, TokenId repaidToken) = abi + .decode(context, (address, uint256, TokenId)); + (uint256 loanAmount, TokenId loanToken) = abi.decode( returnData, (uint256, TokenId) ); - /// @notice Ensure the borrower has an active loan - /// @dev Ensures the borrower has an outstanding loan before proceeding with repayment. - require(amount > 0, "No active loan"); + // Validate loan existence and correct repayment token + if (loanAmount == 0) revert InsufficientFunds("No active loan found"); + if (loanToken != repaidToken) revert InvalidToken(); - /// @notice Request the interest rate from the InterestManager - /// @dev Fetches the current interest rate for the loan from the InterestManager contract. + // Step 2: Get Interest Rate from InterestManager bytes memory interestCallData = abi.encodeWithSignature( "getInterestRate()" ); + bytes memory interestContext = abi.encodeWithSelector( - this.processRepayment.selector, + this.processRepaymentCalculation.selector, // Next callback function borrower, - amount, - token, - sentAmount + loanAmount, // Actual loan amount + loanToken, // Actual loan token + sentAmount // Amount user sent ); - /// @notice Send request to InterestManager to fetch interest rate - /// @dev This request fetches the interest rate that will be used to calculate the total repayment. Nil.sendRequest( interestManager, 0, @@ -290,157 +322,63 @@ contract LendingPool is NilBase, NilTokenBase { ); } - /// @notice Process the repayment, calculate the total repayment including interest. - /// @dev Finalizes the loan repayment, ensuring the borrower has sent sufficient funds. - /// @param success Indicates if the interest rate call was successful. - /// @param returnData The interest rate returned from the InterestManager. - /// @param context The context containing repayment details. - function processRepayment( + /// @notice Callback function triggered after fetching the interest rate for repayment. + /// @dev Calculates the total required repayment amount (principal + interest). + /// Verifies if the user sent sufficient funds. + /// Sends an asynchronous call to the `CentralLedger` to process the repayment, forwarding the repayment tokens. + /// @param success Boolean indicating if the InterestManager call was successful. + /// @param returnData Encoded interest rate (uint256) from the InterestManager. + /// @param context Encoded context data passed from the `handleLoanDetailsForRepayment` call. + function processRepaymentCalculation( bool success, bytes memory returnData, bytes memory context ) public payable { - /// @notice Ensure the interest rate call was successful - /// @dev Verifies that the interest rate retrieval was successful. - require(success, "Interest rate call failed"); + if (!success) revert CrossShardCallFailed("Interest rate call failed"); - /// @notice Decode the repayment details and the interest rate - /// @dev Decodes the repayment context and retrieves the interest rate for loan repayment. ( address borrower, - uint256 amount, - TokenId token, + uint256 loanAmount, + TokenId loanToken, uint256 sentAmount ) = abi.decode(context, (address, uint256, TokenId, uint256)); - /// @notice Decode the interest rate from the response - /// @dev Decodes the interest rate received from the InterestManager contract. - uint256 interestRate = abi.decode(returnData, (uint256)); - /// @notice Calculate the total repayment amount (principal + interest) - /// @dev Adds the interest to the principal to calculate the total repayment due. - uint256 totalRepayment = amount + ((amount * interestRate) / 100); - - /// @notice Ensure the borrower has sent sufficient funds for the repayment - /// @dev Verifies that the borrower has provided enough funds to repay the loan in full. - require(sentAmount >= totalRepayment, "Insufficient funds"); - - /// @notice Clear the loan and release collateral - /// @dev Marks the loan as repaid and releases any associated collateral back to the borrower. - bytes memory clearLoanCallData = abi.encodeWithSignature( - "recordLoan(address,address,uint256)", - borrower, - token, - 0 // Mark the loan as repaid - ); - bytes memory releaseCollateralContext = abi.encodeWithSelector( - this.releaseCollateral.selector, - borrower, - token - ); + uint256 interestRate = abi.decode(returnData, (uint256)); // Assume rate is % points (e.g., 5 for 5%) - /// @notice Send request to GlobalLedger to update the loan status - /// @dev Updates the loan status to indicate repayment completion in the GlobalLedger. - Nil.sendRequest( - globalLedger, - 0, - 6_000_000, - releaseCollateralContext, - clearLoanCallData - ); - } + // Calculate total repayment required + uint256 interestAmount = (loanAmount * interestRate) / 100; // Consider precision + uint256 totalRepayment = loanAmount + interestAmount; - /// @notice Release the collateral after the loan is repaid. - /// @dev Sends the collateral back to the borrower after confirming the loan is fully repaid. - /// @param success Indicates if the loan clearing was successful. - /// @param returnData The collateral data returned from the GlobalLedger. - /// @param context The context containing borrower and collateral token. - function releaseCollateral( - bool success, - bytes memory returnData, - bytes memory context - ) public payable { - /// @notice Ensure the loan clearing was successful - /// @dev Verifies the result of clearing the loan in the GlobalLedger. - require(success, "Loan clearing failed"); - - /// @notice Silence unused variable warning - /// @dev A placeholder for unused variables to avoid compiler warnings. - returnData; - - /// @notice Decode context for borrower and collateral token - /// @dev Decodes the context passed from the loan clearing function to retrieve the borrower's details. - (address borrower, TokenId borrowToken) = abi.decode( - context, - (address, TokenId) - ); - - /// @notice Determine the collateral token (opposite of borrow token) - /// @dev Identifies the token being used as collateral based on the borrow token. - TokenId collateralToken = (borrowToken == usdt) ? eth : usdt; + // Verify user sent enough + if (sentAmount < totalRepayment) { + revert InsufficientFunds("Insufficient amount sent for repayment"); + } - /// @notice Request collateral amount from GlobalLedger - /// @dev Retrieves the amount of collateral associated with the borrower from the GlobalLedger. - bytes memory getCollateralCallData = abi.encodeWithSignature( - "getDeposit(address,address)", - borrower, - collateralToken - ); + // Determine collateral token associated with the loan + TokenId collateralToken = (loanToken == usdt) ? eth : usdt; - /// @notice Context to send collateral to the borrower - /// @dev After confirming the collateral balance, it is returned to the borrower. - bytes memory sendCollateralContext = abi.encodeWithSelector( - this.sendCollateral.selector, + // Step 3: Call CentralLedger to Process Repayment + bytes memory processRepaymentCallData = abi.encodeWithSelector( + bytes4(keccak256("processRepayment(address,address,uint256)")), borrower, - collateralToken + collateralToken, + totalRepayment // Tell CentralLedger the amount *required* for accounting ); - /// @notice Send request to GlobalLedger to retrieve the collateral - /// @dev This request ensures that the correct collateral is available for release. - Nil.sendRequest( - globalLedger, - 0, - 3_50_000, - sendCollateralContext, - getCollateralCallData + // Prepare the tokens *actually sent* by the user to be forwarded + Nil.Token[] memory tokensToForward = new Nil.Token[](1); + tokensToForward[0] = Nil.Token(loanToken, sentAmount); + + // Use Nil.asyncCallWithTokens to forward repayment to CentralLedger + Nil.asyncCallWithTokens( + centralLedger, + borrower, // refundTo (original user) + borrower, // bounceTo (original user) + 0, // feeCredit (default) + Nil.FORWARD_REMAINING, // forwardKind (default) + 0, // value + tokensToForward, // The repayment tokens sent by the user + processRepaymentCallData ); } - - /// @notice Send the collateral back to the borrower. - /// @dev Ensures there is enough collateral to release and then sends the funds back to the borrower. - /// @param success Indicates if the collateral retrieval was successful. - /// @param returnData The amount of collateral available. - /// @param context The context containing borrower and collateral token. - function sendCollateral( - bool success, - bytes memory returnData, - bytes memory context - ) public payable { - /// @notice Ensure the collateral retrieval was successful - /// @dev Verifies that the request to retrieve the collateral was successful. - require(success, "Failed to retrieve collateral"); - - /// @notice Decode the collateral details - /// @dev Decodes the context passed from the releaseCollateral function to retrieve collateral details. - (address borrower, TokenId collateralToken) = abi.decode( - context, - (address, TokenId) - ); - uint256 collateralAmount = abi.decode(returnData, (uint256)); - - /// @notice Ensure there's collateral to release - /// @dev Verifies that there is enough collateral to be released. - require(collateralAmount > 0, "No collateral to release"); - - /// @notice Ensure sufficient balance in the LendingPool to send collateral - /// @dev Verifies that the LendingPool has enough collateral to send to the borrower. - require( - Nil.tokenBalance(address(this), collateralToken) >= - collateralAmount, - "Insufficient funds" - ); - - /// @notice Send the collateral tokens to the borrower - /// @dev Executes the transfer of collateral tokens back to the borrower. - sendTokenInternal(borrower, collateralToken, collateralAmount); - } } diff --git a/academy/lending-protocol/task/run-lending-protocol.ts b/academy/lending-protocol/task/run-lending-protocol.ts index a232b9d40..bbea7da61 100644 --- a/academy/lending-protocol/task/run-lending-protocol.ts +++ b/academy/lending-protocol/task/run-lending-protocol.ts @@ -6,6 +6,7 @@ import { generateSmartAccount, getContract, waitTillCompleted, + Transaction, } from "@nilfoundation/niljs"; import { type Abi, decodeFunctionResult, encodeFunctionData } from "viem"; @@ -14,6 +15,17 @@ import * as dotenv from "dotenv"; import { task } from "hardhat/config"; dotenv.config(); +// Define type for token info +interface TokenInfo { + id: `0x${string}`; + amount: bigint; +} + +// Helper function to convert token record to array +function tokensRecordToArray(record: Record): TokenInfo[] { + return Object.entries(record).map(([id, amount]) => ({ id: id as `0x${string}`, amount })); +} + task( "run-lending-protocol", "End to end test for the interaction page", @@ -23,12 +35,15 @@ task( const InterestManager = require("../artifacts/contracts/InterestManager.sol/InterestManager.json"); const LendingPool = require("../artifacts/contracts/LendingPool.sol/LendingPool.json"); const Oracle = require("../artifacts/contracts/Oracle.sol/Oracle.json"); + // Initialize the PublicClient to interact with the blockchain const client = new PublicClient({ transport: new HttpTransport({ endpoint: process.env.NIL_RPC_ENDPOINT as string, }), }); + const listOfShards = await client.getShardIdList(); + console.log("List of shards:", listOfShards); // Initialize the FaucetClient to top up accounts with test tokens const faucet = new FaucetClient({ @@ -42,7 +57,7 @@ task( // Deploying a new smart account for the deployer console.log("Deploying Wallet"); const deployerWallet = await generateSmartAccount({ - shardId: 1, + shardId: listOfShards[0], rpcEndpoint: process.env.NIL_RPC_ENDPOINT as string, faucetEndpoint: process.env.NIL_RPC_ENDPOINT as string, }); @@ -63,10 +78,10 @@ task( `Deployer smart account ${deployerWallet.address} has been topped up with 3000 USDT at tx hash ${topUpSmartAccount}`, ); - // Deploy InterestManager contract on shard 2 + // Deploy InterestManager contract on second shard const { address: deployInterestManager, tx: deployInterestManagerTx } = await deployerWallet.deployContract({ - shardId: 2, + shardId: listOfShards[1], args: [], bytecode: InterestManager.bytecode as `0x${string}`, abi: InterestManager.abi as Abi, @@ -75,28 +90,13 @@ task( await deployInterestManagerTx.wait(); console.log( - `Interest Manager deployed at ${deployInterestManager} with hash ${deployInterestManagerTx.hash} on shard 2`, - ); - - // Deploy GlobalLedger contract on shard 3 - const { address: deployGlobalLedger, tx: deployGlobalLedgerTx } = - await deployerWallet.deployContract({ - shardId: 3, - args: [], - bytecode: GlobalLedger.bytecode as `0x${string}`, - abi: GlobalLedger.abi as Abi, - salt: BigInt(Math.floor(Math.random() * 10000)), - }); - - await deployGlobalLedgerTx.wait(); - console.log( - `Global Ledger deployed at ${deployGlobalLedger} with hash ${deployGlobalLedgerTx.hash} on shard 3`, + `Interest Manager deployed at ${deployInterestManager} with hash ${deployInterestManagerTx.hash} on shard ${listOfShards[1]}`, ); - // Deploy Oracle contract on shard 4 + // Deploy Oracle contract on fourth shard const { address: deployOracle, tx: deployOracleTx } = await deployerWallet.deployContract({ - shardId: 4, + shardId: listOfShards[3], args: [], bytecode: Oracle.bytecode as `0x${string}`, abi: Oracle.abi as Abi, @@ -105,33 +105,73 @@ task( await deployOracleTx.wait(); console.log( - `Oracle deployed at ${deployOracle} with hash ${deployOracleTx.hash} on shard 4`, + `Oracle deployed at ${deployOracle} with hash ${deployOracleTx.hash} on shard ${listOfShards[3]}`, ); - // Deploy LendingPool contract on shard 1, linking all other contracts - const { address: deployLendingPool, tx: deployLendingPoolTx } = + // Deploy GlobalLedger (CentralLedger) contract on third shard + const { address: deployGlobalLedger, tx: deployGlobalLedgerTx } = await deployerWallet.deployContract({ - shardId: 1, + shardId: listOfShards[2], args: [ - deployGlobalLedger, deployInterestManager, deployOracle, - process.env.USDT, - process.env.ETH, + process.env.USDT as `0x${string}`, + process.env.ETH as `0x${string}`, ], - bytecode: LendingPool.bytecode as `0x${string}`, - abi: LendingPool.abi as Abi, + bytecode: GlobalLedger.bytecode as `0x${string}`, + abi: GlobalLedger.abi as Abi, salt: BigInt(Math.floor(Math.random() * 10000)), }); - await deployLendingPoolTx.wait(); + await deployGlobalLedgerTx.wait(); console.log( - `Lending Pool deployed at ${deployLendingPool} with hash ${deployLendingPoolTx.hash} on shard 1`, + `Global Ledger (Central) deployed at ${deployGlobalLedger} with hash ${deployGlobalLedgerTx.hash} on shard ${listOfShards[2]}`, ); + // Deploy LendingPool contracts via GlobalLedger + console.log("\\nDeploying LendingPool contracts via GlobalLedger..."); + const deployPoolsTx = await deployerWallet.sendTransaction({ + to: deployGlobalLedger, + functionName: "deployLendingPools", + args: [listOfShards], + abi: GlobalLedger.abi as Abi, + // No value or tokens needed for this call + }); + await deployPoolsTx.wait(); + console.log(`LendingPool deployment initiated via GlobalLedger at tx hash ${deployPoolsTx.hash}`); + + // Wait for async deployments to likely complete + console.log("Waiting a few seconds for LendingPools to deploy asynchronously..."); + await new Promise(resolve => setTimeout(resolve, 8000)); // Adjust wait time if needed + + // Fetch deployed LendingPool addresses from GlobalLedger state + console.log("Fetching deployed LendingPool addresses..."); + const lendingPools: { address: `0x${string}`; shardId: number }[] = []; + const globalLedgerReader = getContract({ client, abi: GlobalLedger.abi, address: deployGlobalLedger }); + + for (const shardId of listOfShards) { + try { + const poolAddress = await globalLedgerReader.read.lendingPoolsByShard([shardId]) as `0x${string}`; + if (poolAddress && poolAddress !== '0x0000000000000000000000000000000000000000') { + console.log(` Found LendingPool on shard ${shardId} at address: ${poolAddress}`); + lendingPools.push({ address: poolAddress, shardId }); + } else { + console.log(` No LendingPool found registered for shard ${shardId}.`); + } + } catch (error) { + console.error(`Error fetching pool address for shard ${shardId}:`, error); + } + } + + if (lendingPools.length !== listOfShards.length) { + console.warn(`WARNING: Expected ${listOfShards.length} pools, but only found ${lendingPools.length} registered in GlobalLedger.`); + // Depending on requirements, you might want to throw an error here + } + console.log("Lending Pool address fetching complete.\n"); + // Generate two smart accounts (account1 and account2) const account1 = await generateSmartAccount({ - shardId: 1, + shardId: listOfShards[0], rpcEndpoint: process.env.NIL_RPC_ENDPOINT as string, faucetEndpoint: process.env.NIL_RPC_ENDPOINT as string, }); @@ -139,7 +179,7 @@ task( console.log(`Account 1 generated at ${account1.address}`); const account2 = await generateSmartAccount({ - shardId: 3, + shardId: listOfShards[2], rpcEndpoint: process.env.NIL_RPC_ENDPOINT as string, faucetEndpoint: process.env.NIL_RPC_ENDPOINT as string, }); @@ -218,24 +258,24 @@ task( }); // Set the price for USDT - const setOraclePriceUSDT = await deployerWallet.sendTransaction({ + const setOraclePriceUSDTTx = await deployerWallet.sendTransaction({ to: deployOracle, data: setUSDTPrice, }); - await setOraclePriceUSDT.wait(); + await setOraclePriceUSDTTx.wait(); console.log( - `Oracle price set for USDT at tx hash ${setOraclePriceUSDT.hash}`, + `Oracle price set for USDT at tx hash ${setOraclePriceUSDTTx.hash}`, ); // Set the price for ETH - const setOraclePriceETH = await deployerWallet.sendTransaction({ + const setOraclePriceETHTx = await deployerWallet.sendTransaction({ to: deployOracle, data: setETHPrice, }); - await setOraclePriceETH.wait(); - console.log(`Oracle price set for ETH at tx hash ${setOraclePriceETH.hash}`); + await setOraclePriceETHTx.wait(); + console.log(`Oracle price set for ETH at tx hash ${setOraclePriceETHTx.hash}`); // Retrieve the prices of USDT and ETH from the Oracle contract const usdtPriceRequest = await client.call( @@ -279,88 +319,135 @@ 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 + // Get GlobalLedger contract instance for reading state later + const globalLedgerContract = getContract({ + client, + abi: GlobalLedger.abi, + address: deployGlobalLedger, + }); + + // Perform a deposit of USDT by account1 into the LendingPool on shard 0 + const depositAmountUSDT = 12n; const depositUSDT = { id: process.env.USDT as `0x${string}`, - amount: 12n, + amount: depositAmountUSDT, }; - const depositUSDTResponse = await account1.sendTransaction({ - to: deployLendingPool, + console.log(`Account 1 depositing ${depositAmountUSDT} USDT via Pool on Shard ${lendingPools[0].shardId}...`); + const depositUSDTResponseTx = await account1.sendTransaction({ + to: lendingPools[0].address, functionName: "deposit", abi: LendingPool.abi as Abi, tokens: [depositUSDT], feeCredit: convertEthToWei(0.001), }); - await waitTillCompleted(client, depositUSDTResponse); - console.log(`Account 1 deposited 12 USDT at tx hash ${depositUSDTResponse}`); + await depositUSDTResponseTx.wait(); + console.log( + `Account 1 deposit initiated at tx hash ${depositUSDTResponseTx.hash}` + ); - // Perform a deposit of ETH by account2 into the LendingPool + // Perform a deposit of ETH by account2 into the LendingPool on shard 2 + const depositAmountETH = 5n; const depositETH = { id: process.env.ETH as `0x${string}`, - amount: 5n, + amount: depositAmountETH, }; - const depositETHResponse = await account2.sendTransaction({ - to: deployLendingPool, + console.log(`Account 2 depositing ${depositAmountETH} ETH via Pool on Shard ${lendingPools[2].shardId}...`); + const depositETHResponseTx = await account2.sendTransaction({ + to: lendingPools[2].address, functionName: "deposit", abi: LendingPool.abi as Abi, tokens: [depositETH], feeCredit: convertEthToWei(0.001), }); - await depositETHResponse.wait(); + await depositETHResponseTx.wait(); console.log( - `Account 2 deposited 1 ETH at tx hash ${depositETHResponse.hash}`, + `Account 2 deposit initiated at tx hash ${depositETHResponseTx.hash}` ); - // Retrieve the deposit balances of account1 and account2 from GlobalLedger - const globalLedgerContract = getContract({ - client, - abi: GlobalLedger.abi, - address: deployGlobalLedger, - }); + // --- Add a delay or wait mechanism if necessary for async calls to process --- + console.log("Waiting a few seconds for deposits to process asynchronously..."); + await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second wait - const account1Balance = await globalLedgerContract.read.getDeposit([ + // Retrieve the collateral balances from GlobalLedger + console.log("Checking collateral balances in GlobalLedger..."); + const account1CollateralUSDT = await globalLedgerContract.read.getCollateralBalance([ account1.address, - process.env.USDT, - ]); - const account2Balance = await globalLedgerContract.read.getDeposit([ + process.env.USDT as `0x${string}`, + ]) as bigint; + const account2CollateralETH = await globalLedgerContract.read.getCollateralBalance([ account2.address, - process.env.ETH, - ]); - - 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 - const borrowETH = encodeFunctionData({ + process.env.ETH as `0x${string}`, + ]) as bigint; + + console.log(`Account 1 collateral balance (USDT) in GlobalLedger: ${account1CollateralUSDT}`); + console.log(`Account 2 collateral balance (ETH) in GlobalLedger: ${account2CollateralETH}`); + // Check if balances match deposited amounts + if (account1CollateralUSDT !== depositAmountUSDT) { + console.error(`ERROR: Account 1 USDT collateral (${account1CollateralUSDT}) does not match deposit (${depositAmountUSDT})!`); + } + if (account2CollateralETH !== depositAmountETH) { + console.error(`ERROR: Account 2 ETH collateral (${account2CollateralETH}) does not match deposit (${depositAmountETH})!`); + } + + // Perform a borrow operation by account1 for 5 ETH using USDT as collateral + const borrowAmountETH = 5n; + const borrowETHData = encodeFunctionData({ abi: LendingPool.abi as Abi, functionName: "borrow", - args: [5, process.env.ETH], + args: [borrowAmountETH, process.env.ETH], }); - const account1BalanceBeforeBorrow = await client.getTokens( - account1.address, - "latest", - ); - console.log("Account 1 balance before borrow:", account1BalanceBeforeBorrow); - - const borrowETHResponse = await account1.sendTransaction({ - to: deployLendingPool, - data: borrowETH, + console.log(`Account 1 borrowing ${borrowAmountETH} ETH via Pool on Shard ${lendingPools[0].shardId}...`); + const account1TokensRecordBeforeBorrow = await client.getTokens(account1.address, "latest"); + const account1TokensBeforeBorrow = tokensRecordToArray(account1TokensRecordBeforeBorrow); + const globalLedgerTokensRecordBeforeBorrow = await client.getTokens(deployGlobalLedger, "latest"); + const globalLedgerTokensBeforeBorrow = tokensRecordToArray(globalLedgerTokensRecordBeforeBorrow); + console.log("Account 1 Tokens BEFORE Borrow:", account1TokensBeforeBorrow); + console.log("GlobalLedger Tokens BEFORE Borrow:", globalLedgerTokensBeforeBorrow); + + const borrowETHResponseTx = await account1.sendTransaction({ + to: lendingPools[0].address, + data: borrowETHData, feeCredit: convertEthToWei(0.001), }); - await borrowETHResponse.wait(); - console.log(`Account 1 borrowed 5 ETH at tx hash ${borrowETHResponse.hash}`); - - const account1BalanceAfterBorrow = await client.getTokens( - account1.address, - "latest", + await borrowETHResponseTx.wait(); + console.log( + `Account 1 borrow initiated at tx hash ${borrowETHResponseTx.hash}` ); - console.log("Account 1 balance after borrow:", account1BalanceAfterBorrow); + + // --- Add delay for borrow processing --- + console.log("Waiting a few seconds for borrow to process asynchronously..."); + await new Promise(resolve => setTimeout(resolve, 8000)); // 8 second wait + + // Check balances and state after borrow + console.log("Checking state after borrow..."); + const account1TokensRecordAfterBorrow = await client.getTokens(account1.address, "latest"); + const account1TokensAfterBorrow = tokensRecordToArray(account1TokensRecordAfterBorrow); + const globalLedgerTokensRecordAfterBorrow = await client.getTokens(deployGlobalLedger, "latest"); + const globalLedgerTokensAfterBorrow = tokensRecordToArray(globalLedgerTokensRecordAfterBorrow); + const account1LoanDetails = await globalLedgerContract.read.getLoanDetails([account1.address]) as readonly [bigint, `0x${string}`]; + + console.log("Account 1 Tokens AFTER Borrow:", account1TokensAfterBorrow); + console.log("GlobalLedger Tokens AFTER Borrow:", globalLedgerTokensAfterBorrow); + console.log("Account 1 Loan Details in GlobalLedger:", account1LoanDetails); + + // Validate changes (simple checks, more robust checks might be needed) + const ethTokenInfoBefore = account1TokensBeforeBorrow.find((t: TokenInfo) => t.id === process.env.ETH as `0x${string}`); + const ethAmountBefore = ethTokenInfoBefore ? ethTokenInfoBefore.amount : 0n; + const ethTokenInfoAfter = account1TokensAfterBorrow.find((t: TokenInfo) => t.id === process.env.ETH as `0x${string}`); + const ethAmountAfter = ethTokenInfoAfter ? ethTokenInfoAfter.amount : 0n; + + if (ethAmountAfter !== ethAmountBefore + borrowAmountETH) { + console.error(`ERROR: Account 1 ETH balance did not increase correctly! Expected ${ethAmountBefore + borrowAmountETH}, got ${ethAmountAfter}`); + } + if (account1LoanDetails[0] !== borrowAmountETH || account1LoanDetails[1] !== process.env.ETH) { + console.error(`ERROR: Loan details incorrect in GlobalLedger! Expected ${borrowAmountETH} ETH, got ${account1LoanDetails[0]} ${account1LoanDetails[1]}`); + } // Top up account1 with NIL for loan repayment to avoid insufficient balance const topUpSmartAccount1WithNIL = await faucet.topUpAndWaitUntilCompletion( @@ -381,10 +468,11 @@ task( console.log("Account 1 balance after top up:", account1BalanceAfterTopUp); // Perform a loan repayment by account1 + const repayAmountETH = 6n; const repayETH = [ { id: process.env.ETH as `0x${string}`, - amount: BigInt(6), + amount: repayAmountETH, }, ]; @@ -394,19 +482,66 @@ task( args: [], }); - const repayETHResponse = await account1.sendTransaction({ - to: deployLendingPool, + console.log(`Account 1 repaying ${repayAmountETH} ETH via Pool on Shard ${lendingPools[0].shardId}...`); + const account1TokensRecordBeforeRepay = await client.getTokens(account1.address, "latest"); + const account1TokensBeforeRepay = tokensRecordToArray(account1TokensRecordBeforeRepay); + const account1CollateralBeforeRepay = await globalLedgerContract.read.getCollateralBalance([account1.address, process.env.USDT as `0x${string}`]) as bigint; + console.log("Account 1 Tokens BEFORE Repay:", account1TokensBeforeRepay); + console.log("Account 1 USDT Collateral BEFORE Repay:", account1CollateralBeforeRepay); + + const repayETHResponseTx = await account1.sendTransaction({ + to: lendingPools[0].address, data: repayETHData, tokens: repayETH, feeCredit: convertEthToWei(0.001), }); - await repayETHResponse.wait(); - console.log(`Account 1 repaid 1 ETH at tx hash ${repayETHResponse.hash}`); - - const account1BalanceAfterRepay = await client.getTokens( - account1.address, - "latest", + await repayETHResponseTx.wait(); + console.log( + `Account 1 repay initiated at tx hash ${repayETHResponseTx.hash}` ); - console.log("Account 1 balance after repay:", account1BalanceAfterRepay); + + // --- Add delay for repay processing --- + console.log("Waiting a few seconds for repayment and collateral release to process asynchronously..."); + await new Promise(resolve => setTimeout(resolve, 10000)); // 10 second wait + + // Check balances and state after repay + console.log("Checking state after repayment..."); + const account1TokensRecordAfterRepay = await client.getTokens(account1.address, "latest"); + const account1TokensAfterRepay = tokensRecordToArray(account1TokensRecordAfterRepay); + const account1LoanDetailsAfterRepay = await globalLedgerContract.read.getLoanDetails([account1.address]) as readonly [bigint, `0x${string}`]; + const account1CollateralAfterRepay = await globalLedgerContract.read.getCollateralBalance([account1.address, process.env.USDT as `0x${string}`]) as bigint; + + console.log("Account 1 Tokens AFTER Repay:", account1TokensAfterRepay); + console.log("Account 1 Loan Details AFTER Repay:", account1LoanDetailsAfterRepay); + console.log("Account 1 USDT Collateral AFTER Repay:", account1CollateralAfterRepay); + + // Validate changes + const ethTokenInfoBeforeRepay = account1TokensBeforeRepay.find((t: TokenInfo) => t.id === process.env.ETH as `0x${string}`); + const ethAmountBeforeRepay = ethTokenInfoBeforeRepay ? ethTokenInfoBeforeRepay.amount : 0n; + const ethTokenInfoAfterRepay = account1TokensAfterRepay.find((t: TokenInfo) => t.id === process.env.ETH as `0x${string}`); + const ethAmountAfterRepay = ethTokenInfoAfterRepay ? ethTokenInfoAfterRepay.amount : 0n; + + const usdtTokenInfoBeforeRepay = account1TokensBeforeRepay.find((t: TokenInfo) => t.id === process.env.USDT as `0x${string}`); + const usdtAmountBeforeRepay = usdtTokenInfoBeforeRepay ? usdtTokenInfoBeforeRepay.amount : 0n; + const usdtTokenInfoAfterRepay = account1TokensAfterRepay.find((t: TokenInfo) => t.id === process.env.USDT as `0x${string}`); + const usdtAmountAfterRepay = usdtTokenInfoAfterRepay ? usdtTokenInfoAfterRepay.amount : 0n; + + // Check ETH balance decreased by sent amount + if (ethAmountAfterRepay !== ethAmountBeforeRepay - repayAmountETH) { + console.error(`ERROR: Account 1 ETH balance did not decrease correctly after repay! Expected ${ethAmountBeforeRepay - repayAmountETH}, got ${ethAmountAfterRepay}`); + } + // Check loan is cleared + if (account1LoanDetailsAfterRepay[0] !== 0n) { + console.error(`ERROR: Loan details not cleared in GlobalLedger! Amount: ${account1LoanDetailsAfterRepay[0]}`); + } + // Check collateral is released (balance should be 0 in GL, and returned to user's wallet) + if (account1CollateralAfterRepay !== 0n) { + console.error(`ERROR: Collateral balance not cleared in GlobalLedger! Amount: ${account1CollateralAfterRepay}`); + } + if (usdtAmountAfterRepay !== usdtAmountBeforeRepay + account1CollateralBeforeRepay) { + console.error(`ERROR: Account 1 USDT balance did not increase correctly after collateral release! Expected ${usdtAmountBeforeRepay + account1CollateralBeforeRepay}, got ${usdtAmountAfterRepay}`); + } + + console.log("--- End-to-End Test Complete ---"); });