diff --git a/src/core/Netting.sol b/src/core/Netting.sol index da698e9..9af5605 100644 --- a/src/core/Netting.sol +++ b/src/core/Netting.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.24; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "../libraries/Types.sol"; import "../libraries/Constants.sol"; @@ -174,6 +176,95 @@ contract Netting is Ownable { return _calculateNetPositions(currentWindowId); } + /** + * @notice Forecast the next settlement using block numbers only. + */ + function getSettlementWindowForecast() external view returns (Types.SettlementWindowForecast memory forecast) { + forecast.windowId = currentWindowId; + forecast.settlementBlock = windowStartBlock + Constants.SETTLEMENT_BLOCK_OFFSET; + forecast.blocksRemaining = + block.number >= forecast.settlementBlock ? 0 : forecast.settlementBlock - block.number; + } + + /** + * @notice Return the selected bank's payable and receivable amounts by asset. + */ + function getBankNetPositions(address bank) external view returns (Types.BankNetPosition[] memory) { + return _getBankNetPositions(bank); + } + + /** + * @notice Return the assets and amounts the selected bank must provide for settlement. + */ + function getBankSettlementAssetRequirements(address bank) + external + view + returns (Types.SettlementAssetRequirement[] memory requirements) + { + Types.BankNetPosition[] memory bankPositions = _getBankNetPositions(bank); + uint256 count; + + for (uint256 i = 0; i < bankPositions.length; i++) { + if (bankPositions[i].payableAmount > 0) { + count++; + } + } + + requirements = new Types.SettlementAssetRequirement[](count); + uint256 index; + + for (uint256 i = 0; i < bankPositions.length; i++) { + if (bankPositions[i].payableAmount == 0) { + continue; + } + + requirements[index] = Types.SettlementAssetRequirement({ + asset: bankPositions[i].asset, requiredAmount: bankPositions[i].payableAmount + }); + index++; + } + } + + /** + * @notice Estimate how much the selected bank must borrow for each required asset. + */ + function getBankLiquidityShortfalls(address bank) + external + view + returns (Types.LiquidityShortfall[] memory shortfalls) + { + Types.BankNetPosition[] memory bankPositions = _getBankNetPositions(bank); + uint256 count; + + for (uint256 i = 0; i < bankPositions.length; i++) { + if (bankPositions[i].payableAmount > 0) { + count++; + } + } + + shortfalls = new Types.LiquidityShortfall[](count); + uint256 index; + + for (uint256 i = 0; i < bankPositions.length; i++) { + uint256 requiredAmount = bankPositions[i].payableAmount; + + if (requiredAmount == 0) { + continue; + } + + uint256 availableBalance = IERC20(bankPositions[i].asset).balanceOf(bank); + uint256 borrowAmount = availableBalance >= requiredAmount ? 0 : requiredAmount - availableBalance; + + shortfalls[index] = Types.LiquidityShortfall({ + asset: bankPositions[i].asset, + requiredAmount: requiredAmount, + availableBalance: availableBalance, + borrowAmount: borrowAmount + }); + index++; + } + } + /** * @notice Return public analytics metrics for the current window. */ @@ -230,6 +321,39 @@ contract Netting is Ownable { return trades[windowId][tradeId]; } + function _getBankNetPositions(address bank) internal view returns (Types.BankNetPosition[] memory result) { + if (bank == address(0)) { + revert Errors.ZeroAddress(); + } + + Types.NetPosition[] memory positions = _calculateNetPositions(currentWindowId); + uint256 count; + + for (uint256 i = 0; i < positions.length; i++) { + if (positions[i].participant == bank && positions[i].amount != 0) { + count++; + } + } + + result = new Types.BankNetPosition[](count); + uint256 index; + + for (uint256 i = 0; i < positions.length; i++) { + if (positions[i].participant != bank || positions[i].amount == 0) { + continue; + } + + int256 amount = positions[i].amount; + + result[index] = Types.BankNetPosition({ + asset: positions[i].asset, + payableAmount: amount < 0 ? SafeCast.toUint256(-amount) : 0, + receivableAmount: amount > 0 ? SafeCast.toUint256(amount) : 0 + }); + index++; + } + } + /** * @dev Calculate net positions for current window. * diff --git a/src/libraries/Types.sol b/src/libraries/Types.sol index 7ba950b..2991b50 100644 --- a/src/libraries/Types.sol +++ b/src/libraries/Types.sol @@ -15,4 +15,28 @@ library Types { address asset; int256 amount; } + + struct SettlementWindowForecast { + uint256 windowId; + uint256 settlementBlock; + uint256 blocksRemaining; + } + + struct BankNetPosition { + address asset; + uint256 payableAmount; + uint256 receivableAmount; + } + + struct SettlementAssetRequirement { + address asset; + uint256 requiredAmount; + } + + struct LiquidityShortfall { + address asset; + uint256 requiredAmount; + uint256 availableBalance; + uint256 borrowAmount; + } } diff --git a/test/unit/Netting.t.sol b/test/unit/Netting.t.sol index 9c69900..b4d7b87 100644 --- a/test/unit/Netting.t.sol +++ b/test/unit/Netting.t.sol @@ -279,6 +279,83 @@ contract NettingTest is Test { assertEq(positions[3].amount, -int256(5)); } + function testSettlementWindowForecast() public { + uint256 startBlock = netting.windowStartBlock(); + + Types.SettlementWindowForecast memory forecast = netting.getSettlementWindowForecast(); + + assertEq(forecast.windowId, 0); + assertEq(forecast.settlementBlock, startBlock + 13); + assertEq(forecast.blocksRemaining, 13); + + vm.roll(startBlock + 10); + + forecast = netting.getSettlementWindowForecast(); + + assertEq(forecast.blocksRemaining, 3); + + vm.roll(startBlock + 13); + + forecast = netting.getSettlementWindowForecast(); + + assertEq(forecast.blocksRemaining, 0); + } + + function testBankNetPositionsSeparatePayablesAndReceivables() public { + netting.submitTrade(alice, bob, 500 * USDC, 5); + + Types.BankNetPosition[] memory positions = netting.getBankNetPositions(alice); + + assertEq(positions.length, 2); + + assertEq(positions[0].asset, address(usdc)); + assertEq(positions[0].payableAmount, 500 * USDC); + assertEq(positions[0].receivableAmount, 0); + + assertEq(positions[1].asset, address(bond)); + assertEq(positions[1].payableAmount, 0); + assertEq(positions[1].receivableAmount, 5); + } + + function testBankSettlementAssetRequirementsOnlyIncludePayables() public { + netting.submitTrade(alice, bob, 500 * USDC, 5); + + Types.SettlementAssetRequirement[] memory aliceRequirements = netting.getBankSettlementAssetRequirements(alice); + Types.SettlementAssetRequirement[] memory bobRequirements = netting.getBankSettlementAssetRequirements(bob); + + assertEq(aliceRequirements.length, 1); + assertEq(aliceRequirements[0].asset, address(usdc)); + assertEq(aliceRequirements[0].requiredAmount, 500 * USDC); + + assertEq(bobRequirements.length, 1); + assertEq(bobRequirements[0].asset, address(bond)); + assertEq(bobRequirements[0].requiredAmount, 5); + } + + function testBankLiquidityShortfallsReturnExpectedBorrowAmount() public { + usdc.mint(charlie, 200 * USDC); + netting.submitTrade(charlie, bob, 500 * USDC, 5); + + Types.LiquidityShortfall[] memory shortfalls = netting.getBankLiquidityShortfalls(charlie); + + assertEq(shortfalls.length, 1); + assertEq(shortfalls[0].asset, address(usdc)); + assertEq(shortfalls[0].requiredAmount, 500 * USDC); + assertEq(shortfalls[0].availableBalance, 200 * USDC); + assertEq(shortfalls[0].borrowAmount, 300 * USDC); + } + + function testDashboardInterfacesRejectZeroBankAddress() public { + vm.expectRevert(Errors.ZeroAddress.selector); + netting.getBankNetPositions(address(0)); + + vm.expectRevert(Errors.ZeroAddress.selector); + netting.getBankSettlementAssetRequirements(address(0)); + + vm.expectRevert(Errors.ZeroAddress.selector); + netting.getBankLiquidityShortfalls(address(0)); + } + // ============================================================ // 8. Execute successful settlement // ============================================================