// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {DBISEngineXXautUsdcBorrowVault} from "../../contracts/flash/DBISEngineXXautUsdcBorrowVault.sol"; import {MockMintableToken} from "../dbis/MockMintableToken.sol"; contract DBISEngineXXautUsdcBorrowVaultTest is Test { MockMintableToken internal xaut; MockMintableToken internal usdc; MockMintableToken internal cwusdc; DBISEngineXXautUsdcBorrowVault internal vault; address internal constant BORROWER = address(0xB0B); address internal constant LENDER = address(0x1EAD); address internal constant LIQUIDATOR = address(0xA11CE); bytes32 internal constant PRICE_SOURCE_HASH = bytes32(uint256(0x5052494345)); bytes32 internal constant SWAP_TX = bytes32(uint256(0x51574150)); bytes32 internal constant ISO_HASH = bytes32(uint256(0x150)); bytes32 internal constant AUDIT_HASH = bytes32(uint256(0xA0017)); bytes32 internal constant PEG_HASH = bytes32(uint256(0x9E6)); uint256 internal constant XAUT_PRICE6 = 3_226_640_000; uint256 internal constant LTV_BPS = 7_500; uint256 internal constant LIQUIDATION_THRESHOLD_BPS = 8_000; uint256 internal constant MIN_HEALTH_FACTOR_BPS = 11_000; uint256 internal constant LIQUIDATION_BONUS_BPS = 500; uint256 internal constant LENDER_USDC = 5_000_000_000; event CwusdcSourcedRepay( address indexed account, address indexed payer, uint256 amount, bytes32 indexed publicSwapTxHash, bytes32 iso20022DocumentHash, bytes32 auditEnvelopeHash, bytes32 pegProofHash ); function setUp() public { xaut = new MockMintableToken("Tether Gold", "XAUt", 6, address(this)); usdc = new MockMintableToken("USD Coin", "USDC", 6, address(this)); cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this)); vault = new DBISEngineXXautUsdcBorrowVault( address(xaut), address(usdc), address(cwusdc), address(this), XAUT_PRICE6, LTV_BPS, LIQUIDATION_THRESHOLD_BPS, MIN_HEALTH_FACTOR_BPS, LIQUIDATION_BONUS_BPS, 0, PRICE_SOURCE_HASH ); usdc.mint(LENDER, LENDER_USDC); vm.startPrank(LENDER); usdc.approve(address(vault), type(uint256).max); vault.fundLender(LENDER_USDC); vm.stopPrank(); xaut.mint(BORROWER, 1_000_000); usdc.mint(BORROWER, 1_000_000_000); vm.startPrank(BORROWER); xaut.approve(address(vault), type(uint256).max); usdc.approve(address(vault), type(uint256).max); vm.stopPrank(); usdc.mint(LIQUIDATOR, 1_000_000_000); vm.prank(LIQUIDATOR); usdc.approve(address(vault), type(uint256).max); } function testBorrowRepayAndWithdrawCollateral() public { vm.startPrank(BORROWER); vault.supplyCollateral(1_000_000); assertEq(vault.collateralValueUsd6(BORROWER), XAUT_PRICE6, "1 XAUt value"); vault.borrowUsdc(2_000_000_000, BORROWER); assertEq(usdc.balanceOf(BORROWER), 3_000_000_000, "borrowed USDC"); assertEq(vault.lenderUsdcAvailable(), 3_000_000_000, "lender bucket lent out"); assertEq(vault.healthFactorBps(BORROWER), 12_906, "health factor"); vault.repayUsdc(2_000_000_000); vault.withdrawCollateral(1_000_000, BORROWER); vm.stopPrank(); (uint256 collateral, uint256 debt) = vault.positions(BORROWER); assertEq(collateral, 0, "collateral closed"); assertEq(debt, 0, "debt closed"); assertEq(xaut.balanceOf(BORROWER), 1_000_000, "xaut returned"); assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender restored"); } function testBorrowRejectsDebtAboveEffectiveCollateralLimit() public { vm.startPrank(BORROWER); vault.supplyCollateral(1_000_000); vm.expectRevert(bytes("exceeds collateral")); vault.borrowUsdc(2_400_000_000, BORROWER); vm.stopPrank(); } function testBorrowRejectsGlobalBorrowCap() public { vault.setRiskParams(LTV_BPS, LIQUIDATION_THRESHOLD_BPS, MIN_HEALTH_FACTOR_BPS, LIQUIDATION_BONUS_BPS, 1_000_000_000); vm.startPrank(BORROWER); vault.supplyCollateral(1_000_000); vm.expectRevert(bytes("max borrow exceeded")); vault.borrowUsdc(1_000_000_001, BORROWER); vm.stopPrank(); } function testRepayFromCwusdcProofStillSettlesInUsdcAndAnchorsHashes() public { vm.startPrank(BORROWER); vault.supplyCollateral(1_000_000); vault.borrowUsdc(1_000_000_000, BORROWER); vm.expectEmit(true, true, true, true, address(vault)); emit CwusdcSourcedRepay(BORROWER, BORROWER, 250_000_000, SWAP_TX, ISO_HASH, AUDIT_HASH, PEG_HASH); vault.repayUsdcFromCwusdcProof(250_000_000, SWAP_TX, ISO_HASH, AUDIT_HASH, PEG_HASH); vm.stopPrank(); (, uint256 debt) = vault.positions(BORROWER); assertEq(debt, 750_000_000, "debt reduced"); assertEq(vault.totalCwusdcProofRepayUsdc(), 250_000_000, "proof-sourced repay counter"); } function testLiquidationAfterPriceDrop() public { vm.startPrank(BORROWER); vault.supplyCollateral(1_000_000); vault.borrowUsdc(2_000_000_000, BORROWER); vm.stopPrank(); vault.setXautUsdPrice6(2_000_000_000, bytes32(uint256(0x44524f50))); assertEq(vault.healthFactorBps(BORROWER), 8_000, "unhealthy after price drop"); vm.prank(LIQUIDATOR); uint256 seized = vault.liquidate(BORROWER, 100_000_000); assertEq(seized, 52_500, "5 percent bonus on 0.05 XAUt"); assertEq(xaut.balanceOf(LIQUIDATOR), 52_500, "liquidator receives XAUt"); (, uint256 debt) = vault.positions(BORROWER); assertEq(debt, 1_900_000_000, "debt after partial liquidation"); } function testOwnerCanWithdrawOnlyUnborrowedLenderUsdc() public { vm.prank(BORROWER); vault.supplyCollateral(1_000_000); vm.prank(BORROWER); vault.borrowUsdc(500_000_000, BORROWER); vault.withdrawLenderUsdc(address(this), 4_500_000_000); assertEq(vault.lenderUsdcAvailable(), 0, "available bucket withdrawn"); vm.expectRevert(bytes("insufficient lender usdc")); vault.withdrawLenderUsdc(address(this), 1); } function testPauseBlocksMutableUserFlows() public { vault.pause(); vm.expectRevert(bytes("paused")); vm.prank(BORROWER); vault.supplyCollateral(1); vault.unpause(); vm.prank(BORROWER); vault.supplyCollateral(1); } }