feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
This commit is contained in:
114
test/flash/QuotePushFlashWorkflowBorrowerMainnetFork.t.sol
Normal file
114
test/flash/QuotePushFlashWorkflowBorrowerMainnetFork.t.sol
Normal file
@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
|
||||
import {SimpleERC3156FlashVault} from "../../contracts/flash/SimpleERC3156FlashVault.sol";
|
||||
import {
|
||||
QuotePushFlashWorkflowBorrower,
|
||||
IExternalUnwinder
|
||||
} from "../../contracts/flash/QuotePushFlashWorkflowBorrower.sol";
|
||||
|
||||
contract ForkMockExternalUnwinder is IExternalUnwinder {
|
||||
IERC20 public immutable base;
|
||||
IERC20 public immutable quote;
|
||||
uint256 public immutable numerator;
|
||||
uint256 public immutable denominator;
|
||||
|
||||
constructor(IERC20 base_, IERC20 quote_, uint256 numerator_, uint256 denominator_) {
|
||||
base = base_;
|
||||
quote = quote_;
|
||||
numerator = numerator_;
|
||||
denominator = denominator_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata)
|
||||
external
|
||||
override
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
require(tokenIn == address(base), "base only");
|
||||
require(tokenOut == address(quote), "quote only");
|
||||
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
|
||||
amountOut = amountIn * numerator / denominator;
|
||||
require(amountOut >= minAmountOut, "min unwind");
|
||||
IERC20(address(quote)).transfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
|
||||
contract QuotePushFlashWorkflowBorrowerMainnetForkTest is Test {
|
||||
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
||||
address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
|
||||
bool public forkAvailable;
|
||||
SimpleERC3156FlashVault internal vault;
|
||||
QuotePushFlashWorkflowBorrower internal borrower;
|
||||
ForkMockExternalUnwinder internal unwinder;
|
||||
|
||||
modifier skipIfNoFork() {
|
||||
if (!forkAvailable) {
|
||||
return;
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envOr("ETHEREUM_MAINNET_RPC", string(""));
|
||||
if (bytes(rpcUrl).length == 0) {
|
||||
forkAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try vm.createSelectFork(rpcUrl) {
|
||||
forkAvailable = true;
|
||||
} catch {
|
||||
forkAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
vault = new SimpleERC3156FlashVault(address(this), 5);
|
||||
vault.setTokenSupported(USDC, true);
|
||||
|
||||
borrower = new QuotePushFlashWorkflowBorrower(address(vault));
|
||||
unwinder = new ForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 112, 100);
|
||||
|
||||
// Seed the local lender and unwind venue with enough quote on the fork.
|
||||
deal(USDC, address(vault), 100_000_000); // 100 USDC
|
||||
deal(USDC, address(unwinder), 100_000_000); // 100 USDC
|
||||
}
|
||||
|
||||
function testFork_quotePush_usesLiveMainnetPmmLegAndRepays() public skipIfNoFork {
|
||||
uint256 amount = 2_964_298; // live safe tranche from 120/120 under 500 bps cap
|
||||
uint256 fee = vault.flashFee(USDC, amount);
|
||||
|
||||
QuotePushFlashWorkflowBorrower.QuotePushParams memory p = QuotePushFlashWorkflowBorrower.QuotePushParams({
|
||||
integration: DODO_PMM_INTEGRATION_MAINNET,
|
||||
pool: POOL_CWUSDC_USDC,
|
||||
baseToken: CWUSDC,
|
||||
externalUnwinder: address(unwinder),
|
||||
minOutPmm: 2_800_000,
|
||||
minOutUnwind: amount + fee,
|
||||
unwindData: bytes("")
|
||||
});
|
||||
|
||||
uint256 vaultBefore = IERC20(USDC).balanceOf(address(vault));
|
||||
uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
|
||||
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), USDC, amount, abi.encode(p));
|
||||
|
||||
uint256 vaultAfter = IERC20(USDC).balanceOf(address(vault));
|
||||
uint256 borrowerSurplus = IERC20(USDC).balanceOf(address(borrower));
|
||||
uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
|
||||
assertEq(vaultAfter, vaultBefore + fee, "vault repaid plus fee");
|
||||
assertGt(borrowerSurplus, 0, "borrower retains quote surplus");
|
||||
assertEq(IERC20(CWUSDC).balanceOf(address(borrower)), 0, "all base unwound");
|
||||
assertLt(poolBaseAfter, poolBaseBefore, "pool base decreased via quote push");
|
||||
assertGt(poolQuoteAfter, poolQuoteBefore, "pool quote increased via quote push");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user