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:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user