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:
137
test/flash/DBISEngineXIndexedLiquidityVault.t.sol
Normal file
137
test/flash/DBISEngineXIndexedLiquidityVault.t.sol
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
179
test/flash/DBISEngineXSingleSidedDodoCwusdcVault.t.sol
Normal file
179
test/flash/DBISEngineXSingleSidedDodoCwusdcVault.t.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
175
test/flash/DBISEngineXXautUsdcBorrowVault.t.sol
Normal file
175
test/flash/DBISEngineXXautUsdcBorrowVault.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user