Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions src/core/Netting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/libraries/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
77 changes: 77 additions & 0 deletions test/unit/Netting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================
Expand Down
Loading