feat(token-aggregation): reports, PMM quotes, config; Engine X flash vaults

- Expand token-aggregation API (report routes), canonical tokens, pools
- Add flash vault contracts + tests (indexed, DODO cwUSDC, XAUT borrow)
- PMM pools JSON, deploy/export scripts, metamask verified list

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-10 12:56:30 -07:00
parent 27f8e3a500
commit 76143a8fe3
67 changed files with 6972 additions and 136 deletions

View File

@@ -0,0 +1,137 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {DBISEngineXIndexedLiquidityVault} from "../../contracts/flash/DBISEngineXIndexedLiquidityVault.sol";
import {MockMintableToken} from "../dbis/MockMintableToken.sol";
contract MockEngineXUniswapV3Pool {
address public immutable token0;
address public immutable token1;
uint24 public immutable fee;
uint160 public sqrtPriceX96;
int24 public tick;
uint128 public liquidity;
constructor(address token0_, address token1_, uint24 fee_) {
token0 = token0_;
token1 = token1_;
fee = fee_;
}
function setSlot0(uint160 sqrtPriceX96_, int24 tick_) external {
sqrtPriceX96 = sqrtPriceX96_;
tick = tick_;
}
function setLiquidity(uint128 liquidity_) external {
liquidity = liquidity_;
}
function slot0() external view returns (uint160, int24, uint16, uint16, uint16, uint8, bool) {
return (sqrtPriceX96, tick, 0, 0, 0, 0, true);
}
}
contract DBISEngineXIndexedLiquidityVaultTest is Test {
MockMintableToken internal cwusdc;
MockMintableToken internal usdc;
MockEngineXUniswapV3Pool internal pool;
DBISEngineXIndexedLiquidityVault internal vault;
address internal constant RECIPIENT = address(0xD00D);
uint160 internal constant ONE_TO_ONE_SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336;
bytes32 internal constant PROOF_ID = bytes32("indexed-proof");
bytes32 internal constant SWAP_TX = bytes32(uint256(0xA1));
bytes32 internal constant LIQUIDITY_TX = bytes32(uint256(0xB1));
bytes32 internal constant ISO_HASH = bytes32(uint256(0x1001));
bytes32 internal constant AUDIT_HASH = bytes32(uint256(0x1002));
bytes32 internal constant PEG_HASH = bytes32(uint256(0x1003));
function setUp() public {
cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this));
usdc = new MockMintableToken("USD Coin", "USDC", 6, address(this));
pool = new MockEngineXUniswapV3Pool(address(cwusdc), address(usdc), 100);
pool.setSlot0(ONE_TO_ONE_SQRT_PRICE_X96, 0);
pool.setLiquidity(1_000_000);
cwusdc.mint(address(pool), 100_000_000);
usdc.mint(address(pool), 100_000_000);
vault = new DBISEngineXIndexedLiquidityVault(
address(cwusdc), address(usdc), address(pool), address(this), 100, 1_000, 1_000_000
);
}
function testRecordIndexedProofAnchorsPublicPoolState() public {
DBISEngineXIndexedLiquidityVault.IndexedProof memory proof = _proof(PROOF_ID);
(uint160 sqrtPriceX96, int24 tick, uint128 liquidity) = vault.recordIndexedProof(proof);
assertEq(sqrtPriceX96, ONE_TO_ONE_SQRT_PRICE_X96, "sqrt price");
assertEq(tick, 0, "tick");
assertEq(liquidity, 1_000_000, "liquidity");
assertTrue(vault.usedProofIds(PROOF_ID), "proof consumed");
}
function testRejectsDuplicateProofId() public {
vault.recordIndexedProof(_proof(PROOF_ID));
vm.expectRevert(bytes("proof used"));
vault.recordIndexedProof(_proof(PROOF_ID));
}
function testRejectsTickDrift() public {
pool.setSlot0(ONE_TO_ONE_SQRT_PRICE_X96, 101);
vm.expectRevert(bytes("tick drift too high"));
vault.recordIndexedProof(_proof(PROOF_ID));
}
function testRejectsInsufficientLiquidity() public {
pool.setLiquidity(999);
vm.expectRevert(bytes("insufficient liquidity"));
vault.recordIndexedProof(_proof(PROOF_ID));
}
function testRejectsOversizedProofAmount() public {
DBISEngineXIndexedLiquidityVault.IndexedProof memory proof = _proof(PROOF_ID);
proof.exactOutputAmount = 1_000_001;
vm.expectRevert(bytes("proof amount too high"));
vault.recordIndexedProof(proof);
}
function testOperatorAllowlist() public {
vault.setOperatorAllowlistEnabled(true);
vm.expectRevert(bytes("operator not approved"));
vm.prank(address(0xBEEF));
vault.recordIndexedProof(_proof(PROOF_ID));
vault.setOperatorApproved(address(0xBEEF), true);
vm.prank(address(0xBEEF));
vault.recordIndexedProof(_proof(PROOF_ID));
assertTrue(vault.usedProofIds(PROOF_ID), "proof consumed");
}
function testPauseBlocksProofs() public {
vault.pause();
vm.expectRevert(bytes("paused"));
vault.recordIndexedProof(_proof(PROOF_ID));
}
function _proof(bytes32 proofId) internal pure returns (DBISEngineXIndexedLiquidityVault.IndexedProof memory) {
return DBISEngineXIndexedLiquidityVault.IndexedProof({
proofId: proofId,
publicSwapTxHash: SWAP_TX,
liquidityTxHash: LIQUIDITY_TX,
outputRecipient: RECIPIENT,
exactOutputAmount: 100_000,
iso20022DocumentHash: ISO_HASH,
auditEnvelopeHash: AUDIT_HASH,
pegProofHash: PEG_HASH
});
}
}

View File

@@ -0,0 +1,179 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Test} from "forge-std/Test.sol";
import {DBISEngineXSingleSidedDodoCwusdcVault} from
"../../contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol";
import {MockMintableToken} from "../dbis/MockMintableToken.sol";
contract MockDodoPool {
using SafeERC20 for IERC20;
address public immutable base;
address public immutable quote;
uint256 public baseReserve;
uint256 public quoteReserve;
constructor(address base_, address quote_) {
base = base_;
quote = quote_;
}
function _BASE_TOKEN_() external view returns (address) {
return base;
}
function _QUOTE_TOKEN_() external view returns (address) {
return quote;
}
function querySellBase(address, uint256 payBaseAmount) external pure returns (uint256 receiveQuoteAmount, uint256 mtFee) {
return (payBaseAmount, 0);
}
function querySellQuote(address, uint256 payQuoteAmount) external pure returns (uint256 receiveBaseAmount, uint256 mtFee) {
return (payQuoteAmount, 0);
}
function getVaultReserve() external view returns (uint256, uint256) {
return (baseReserve, quoteReserve);
}
function buyShares(address) external returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) {
uint256 baseBalance = IERC20(base).balanceOf(address(this));
uint256 quoteBalance = IERC20(quote).balanceOf(address(this));
baseShare = baseBalance - baseReserve;
quoteShare = quoteBalance - quoteReserve;
baseReserve = baseBalance;
quoteReserve = quoteBalance;
lpShare = baseShare < quoteShare ? baseShare : quoteShare;
}
}
contract MockDodoIntegration {
using SafeERC20 for IERC20;
function addLiquidity(address pool, uint256 baseAmount, uint256 quoteAmount)
external
returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare)
{
require(baseAmount > 0 && quoteAmount > 0, "zero amount");
address base = MockDodoPool(pool)._BASE_TOKEN_();
address quote = MockDodoPool(pool)._QUOTE_TOKEN_();
IERC20(base).safeTransferFrom(msg.sender, pool, baseAmount);
IERC20(quote).safeTransferFrom(msg.sender, pool, quoteAmount);
return MockDodoPool(pool).buyShares(msg.sender);
}
}
contract DBISEngineXSingleSidedDodoCwusdcVaultTest is Test {
MockMintableToken internal cwusdc;
MockMintableToken internal weth;
MockDodoIntegration internal integration;
MockDodoPool internal pool;
DBISEngineXSingleSidedDodoCwusdcVault internal vault;
address internal constant FUNDER = address(0xF00D);
address internal constant OWNER = address(0xA11CE);
function setUp() public {
cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this));
weth = new MockMintableToken("Wrapped Ether", "WETH", 18, address(this));
integration = new MockDodoIntegration();
pool = new MockDodoPool(address(cwusdc), address(weth));
vault = new DBISEngineXSingleSidedDodoCwusdcVault(address(cwusdc), address(weth), address(integration), OWNER);
cwusdc.mint(FUNDER, 100_000_000);
weth.mint(FUNDER, 1 ether);
vm.startPrank(FUNDER);
cwusdc.approve(address(vault), type(uint256).max);
weth.approve(address(vault), type(uint256).max);
vm.stopPrank();
}
function testAcceptsSingleSidedCwusdcAsInventoryButNotExecutable() public {
vm.prank(FUNDER);
vault.depositCwusdc(10_000_000);
(
uint256 cwusdcBalance,
uint256 quoteBalance,
uint256 cwusdcInventory,
uint256 quoteInventory,
bool solvent,
bool executable
) = vault.solvencyState();
assertEq(cwusdcBalance, 10_000_000, "cw balance");
assertEq(quoteBalance, 0, "quote balance");
assertEq(cwusdcInventory, 10_000_000, "cw inventory");
assertEq(quoteInventory, 0, "quote inventory");
assertTrue(solvent, "single-sided inventory is solvent");
assertFalse(executable, "single-sided inventory is not executable DODO liquidity");
}
function testPromoteRequiresTwoSidedInventory() public {
vm.prank(OWNER);
vault.setDodoPool(address(pool));
vm.prank(OWNER);
vault.setCanary(1_000, 1_000, 1_000, 1_000);
vm.prank(FUNDER);
vault.depositCwusdc(10_000_000);
vm.expectRevert(bytes("two-sided required"));
vm.prank(OWNER);
vault.promoteToDodo(1_000_000, 0, 0, 0, 0);
vm.expectRevert(bytes("insufficient quote inventory"));
vm.prank(OWNER);
vault.promoteToDodo(1_000_000, 1, 0, 0, 0);
}
function testPromotesTwoSidedInventoryAndPassesCanary() public {
vm.prank(OWNER);
vault.setDodoPool(address(pool));
vm.prank(OWNER);
vault.setCanary(1_000, 1_000, 1_000, 1_000);
vm.startPrank(FUNDER);
vault.depositCwusdc(10_000_000);
vault.depositQuote(1 ether);
vm.stopPrank();
vm.prank(OWNER);
(uint256 baseShare, uint256 quoteShare, uint256 lpShare) =
vault.promoteToDodo(2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000);
assertEq(baseShare, 2_000_000, "base share");
assertEq(quoteShare, 2_000_000, "quote share");
assertEq(lpShare, 2_000_000, "lp share");
assertEq(vault.accountedCwusdcInventory(), 8_000_000, "remaining cw inventory");
assertEq(vault.accountedQuoteInventory(), 1 ether - 2_000_000, "remaining quote inventory");
assertEq(cwusdc.balanceOf(address(pool)), 2_000_000, "pool cw balance");
assertEq(weth.balanceOf(address(pool)), 2_000_000, "pool quote balance");
assertTrue(vault.canaryPasses(), "canary passes");
}
function testCannotRescueAccountedInventory() public {
vm.prank(FUNDER);
vault.depositCwusdc(10_000_000);
vm.expectRevert(bytes("cwusdc insolvent"));
vm.prank(OWNER);
vault.rescueUnaccountedToken(address(cwusdc), OWNER, 1);
}
function testOwnerCanWithdrawAccountedInventory() public {
vm.prank(FUNDER);
vault.depositCwusdc(10_000_000);
vm.prank(OWNER);
vault.withdrawCwusdcInventory(OWNER, 4_000_000);
assertEq(vault.accountedCwusdcInventory(), 6_000_000, "inventory decremented");
assertEq(cwusdc.balanceOf(OWNER), 4_000_000, "owner received");
}
}

View File

@@ -2,9 +2,32 @@
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {DBISEngineXFlashProofBorrower} from "../../contracts/flash/DBISEngineXFlashProofBorrower.sol";
import {DBISEngineXVirtualBatchVault} from "../../contracts/flash/DBISEngineXVirtualBatchVault.sol";
import {MockMintableToken} from "../dbis/MockMintableToken.sol";
contract EngineXFlashBorrower is IERC3156FlashBorrower {
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
bool public repay = true;
function setRepay(bool repay_) external {
repay = repay_;
}
function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata)
external
override
returns (bytes32)
{
if (repay) {
IERC20(token).transfer(msg.sender, amount + fee);
}
return _RETURN_VALUE;
}
}
contract DBISEngineXVirtualBatchVaultTest is Test {
MockMintableToken internal cwusdc;
MockMintableToken internal usdc;
@@ -138,15 +161,7 @@ contract DBISEngineXVirtualBatchVaultTest is Test {
);
vm.prank(USER);
vault.runVirtualProofExactOutTo(
proofId,
LENDER_USDC,
3,
OUTPUT_RECIPIENT,
exactOutput,
ROUNDING_RECEIVER,
ISO_HASH,
AUDIT_HASH,
PEG_HASH
proofId, LENDER_USDC, 3, OUTPUT_RECIPIENT, exactOutput, ROUNDING_RECEIVER, ISO_HASH, AUDIT_HASH, PEG_HASH
);
assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift");
@@ -243,4 +258,154 @@ contract DBISEngineXVirtualBatchVaultTest is Test {
PEG_HASH
);
}
function testWithdrawPoolLiquidityUpdatesAccountingAndPreservesMaintainedPool() public {
uint256 withdrawAmount = 10_000_000;
uint256 ownerCwusdcBefore = cwusdc.balanceOf(address(this));
uint256 ownerUsdcBefore = usdc.balanceOf(address(this));
vault.withdrawPoolLiquidity(address(this), withdrawAmount, withdrawAmount);
assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE - withdrawAmount, "pool cWUSDC accounting");
assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE - withdrawAmount, "pool USDC accounting");
assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender accounting unchanged");
assertEq(cwusdc.balanceOf(address(this)), ownerCwusdcBefore + withdrawAmount, "owner cWUSDC received");
assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + withdrawAmount, "owner USDC received");
}
function testWithdrawPoolLiquidityRejectsBreakingMaintainedPool() public {
vm.expectRevert(bytes("would break maintained pool"));
vault.withdrawPoolLiquidity(address(this), 1, 0);
}
function testWithdrawLenderUsdcUpdatesAccounting() public {
uint256 withdrawAmount = 1_000_000;
uint256 ownerUsdcBefore = usdc.balanceOf(address(this));
vault.withdrawLenderUsdc(address(this), withdrawAmount);
assertEq(vault.lenderUsdcAvailable(), LENDER_USDC - withdrawAmount, "lender accounting");
assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool accounting unchanged");
assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + withdrawAmount, "owner USDC received");
}
function testGenericWithdrawCannotTouchAccountedBalances() public {
vm.expectRevert(bytes("accounting undercollateralized"));
vault.withdraw(address(usdc), address(this), 1);
}
function testGenericWithdrawCanRescueUnaccountedTokens() public {
uint256 dust = 123;
usdc.mint(address(vault), dust);
uint256 ownerUsdcBefore = usdc.balanceOf(address(this));
vault.withdraw(address(usdc), address(this), dust);
assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool accounting unchanged");
assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender accounting unchanged");
assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + dust, "owner receives unaccounted dust");
}
function testFlashLoanUsesLenderBucketAndCollectsFee() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
uint256 amount = 1_000_000;
uint256 fee = vault.flashFee(address(usdc), amount);
usdc.mint(address(borrower), fee);
vm.prank(USER);
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, "");
assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
assertEq(vault.totalFlashFeesCollectedUsdc(), fee, "fee accounting");
assertEq(usdc.balanceOf(address(vault)), LIVE_POOL_RESERVE + LENDER_USDC + fee, "USDC backing");
}
function testFlashLoanCanPullRepaymentByAllowance() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
uint256 amount = 1_000_000;
uint256 fee = vault.flashFee(address(usdc), amount);
usdc.mint(address(borrower), fee);
borrower.setRepay(false);
vm.prank(address(borrower));
usdc.approve(address(vault), type(uint256).max);
vm.prank(USER);
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, "");
assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
assertEq(vault.totalFlashFeesCollectedUsdc(), fee, "fee accounting");
}
function testFlashLoanRejectsBorrowingPoolUsdc() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
vm.expectRevert(bytes("insufficient lender usdc"));
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), LENDER_USDC + 1, "");
}
function testFlashLoanRejectsUnsupportedToken() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
vm.expectRevert(bytes("unsupported flash token"));
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(cwusdc), 1, "");
}
function testFlashLoanCanBeCapped() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
vault.setMaxFlashLoanAmount(999_999);
vm.expectRevert(bytes("flash amount too high"));
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1_000_000, "");
}
function testFlashLoanAllowlistRejectsUnapprovedBorrower() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
vault.setFlashBorrowerAllowlistEnabled(true);
vm.expectRevert(bytes("flash borrower not approved"));
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1, "");
}
function testFlashLoanAllowlistAllowsApprovedBorrower() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
uint256 amount = 1_000_000;
uint256 fee = vault.flashFee(address(usdc), amount);
usdc.mint(address(borrower), fee);
vault.setFlashBorrowerAllowlistEnabled(true);
vault.setFlashBorrowerApproved(address(borrower), true);
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, "");
assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
}
function testPauseBlocksProofsAndFlashLoans() public {
EngineXFlashBorrower borrower = new EngineXFlashBorrower();
vault.pause();
vm.expectRevert(bytes("paused"));
vm.prank(USER);
vault.runVirtualProof(bytes32("proof-paused"), LENDER_USDC, 1);
vm.expectRevert(bytes("paused"));
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1, "");
assertEq(vault.maxFlashLoan(address(usdc)), 0, "paused max flash");
}
function testEngineXFlashProofBorrowerRunsProofFlash() public {
DBISEngineXFlashProofBorrower borrower =
new DBISEngineXFlashProofBorrower(address(vault), address(usdc), address(this));
uint256 amount = 1_000_000;
uint256 fee = vault.flashFee(address(usdc), amount);
bytes32 proofId = bytes32("flash-proof");
usdc.mint(address(borrower), fee);
borrower.runFlashProof(amount, proofId, ISO_HASH, AUDIT_HASH, PEG_HASH);
assertTrue(borrower.usedProofIds(proofId), "proof consumed");
assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
}
}

View File

@@ -0,0 +1,175 @@
// 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);
}
}