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:
20
forkproof/foundry.toml
Normal file
20
forkproof/foundry.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[profile.default]
|
||||
src = "src"
|
||||
test = "test"
|
||||
out = "out"
|
||||
libs = ["../lib"]
|
||||
solc = "0.8.20"
|
||||
optimizer = true
|
||||
optimizer_runs = 1
|
||||
via_ir = true
|
||||
evm_version = "cancun"
|
||||
allow_paths = [".."]
|
||||
fs_permissions = [
|
||||
{ access = "read", path = "../config" }
|
||||
]
|
||||
remappings = [
|
||||
"@openzeppelin/contracts/=../lib/openzeppelin-contracts/contracts/",
|
||||
"forge-std/=../lib/forge-std/src/",
|
||||
"atomic/=../contracts/bridge/atomic/",
|
||||
"repo-test/=../test/"
|
||||
]
|
||||
218
forkproof/src/AaveQuotePushFlashReceiver.sol
Normal file
218
forkproof/src/AaveQuotePushFlashReceiver.sol
Normal file
@@ -0,0 +1,218 @@
|
||||
// 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";
|
||||
|
||||
interface IAavePoolLike {
|
||||
function flashLoanSimple(
|
||||
address receiverAddress,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
bytes calldata params,
|
||||
uint16 referralCode
|
||||
) external;
|
||||
}
|
||||
|
||||
interface IAaveFlashLoanSimpleReceiver {
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
address initiator,
|
||||
bytes calldata params
|
||||
) external returns (bool);
|
||||
}
|
||||
|
||||
interface IAaveDODOQuotePushSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface IAaveExternalUnwinder {
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface IAaveAtomicBridgeCoordinator {
|
||||
struct CreateIntentParams {
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address assetIn;
|
||||
address assetOut;
|
||||
uint256 amountIn;
|
||||
uint256 minAmountOut;
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
bytes32 routeId;
|
||||
}
|
||||
|
||||
function createIntent(CreateIntentParams calldata p) external returns (bytes32 obligationId);
|
||||
|
||||
function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external;
|
||||
|
||||
function obligationEscrow() external view returns (address);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title AaveQuotePushFlashReceiver
|
||||
* @notice Aave V3 flashLoanSimple receiver for the quote-push workflow:
|
||||
* flash borrow quote -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus.
|
||||
*/
|
||||
contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable pool;
|
||||
|
||||
struct QuotePushParams {
|
||||
address integration;
|
||||
address pmmPool;
|
||||
address baseToken;
|
||||
address externalUnwinder;
|
||||
uint256 minOutPmm;
|
||||
uint256 minOutUnwind;
|
||||
bytes unwindData;
|
||||
AtomicBridgeParams atomicBridge;
|
||||
}
|
||||
|
||||
struct AtomicBridgeParams {
|
||||
address coordinator;
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address destinationAsset;
|
||||
uint256 bridgeAmount;
|
||||
uint256 minDestinationAmount;
|
||||
address destinationRecipient;
|
||||
uint256 destinationDeadline;
|
||||
bytes32 routeId;
|
||||
bytes32 settlementMode;
|
||||
bool submitCommitment;
|
||||
}
|
||||
|
||||
error UntrustedPool();
|
||||
error UntrustedInitiator();
|
||||
error BadParams();
|
||||
error InsufficientToRepay();
|
||||
error InvalidAtomicBridge();
|
||||
|
||||
event QuotePushExecuted(
|
||||
address indexed quoteToken,
|
||||
address indexed baseToken,
|
||||
uint256 borrowedAmount,
|
||||
uint256 premium,
|
||||
uint256 baseOut,
|
||||
uint256 unwindOut,
|
||||
uint256 surplus
|
||||
);
|
||||
event AtomicBridgeTriggered(
|
||||
bytes32 indexed obligationId,
|
||||
address indexed coordinator,
|
||||
address indexed destinationRecipient,
|
||||
uint256 bridgeAmount,
|
||||
uint256 minDestinationAmount
|
||||
);
|
||||
|
||||
constructor(address pool_) {
|
||||
pool = pool_;
|
||||
}
|
||||
|
||||
function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external {
|
||||
IAavePoolLike(pool).flashLoanSimple(address(this), asset, amount, abi.encode(address(this), params), 0);
|
||||
}
|
||||
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
address initiator,
|
||||
bytes calldata params
|
||||
) external returns (bool) {
|
||||
if (msg.sender != pool) revert UntrustedPool();
|
||||
(address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams));
|
||||
if (initiator != expectedInitiator) revert UntrustedInitiator();
|
||||
if (
|
||||
p.integration == address(0) || p.pmmPool == address(0) || p.baseToken == address(0)
|
||||
|| p.externalUnwinder == address(0)
|
||||
) revert BadParams();
|
||||
if (p.baseToken == asset) revert BadParams();
|
||||
|
||||
uint256 baseOut = _swapQuoteForBase(asset, amount, p.integration, p.pmmPool, p.minOutPmm);
|
||||
|
||||
uint256 baseBal = IERC20(p.baseToken).balanceOf(address(this));
|
||||
if (p.atomicBridge.coordinator != address(0)) {
|
||||
_triggerAtomicBridge(p.baseToken, baseBal, p.atomicBridge);
|
||||
}
|
||||
|
||||
uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData);
|
||||
uint256 surplus = _approveRepayment(asset, amount + premium);
|
||||
emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm)
|
||||
internal
|
||||
returns (uint256 baseOut)
|
||||
{
|
||||
IERC20(asset).forceApprove(integration, amount);
|
||||
baseOut = IAaveDODOQuotePushSwapExactIn(integration).swapExactIn(pmmPool, asset, amount, minOutPmm);
|
||||
}
|
||||
|
||||
function _unwindBaseIntoQuote(
|
||||
address baseToken,
|
||||
address quoteToken,
|
||||
address externalUnwinder,
|
||||
uint256 minOutUnwind,
|
||||
bytes memory unwindData
|
||||
) internal returns (uint256 unwindOut) {
|
||||
uint256 remainingBase = IERC20(baseToken).balanceOf(address(this));
|
||||
IERC20(baseToken).forceApprove(externalUnwinder, remainingBase);
|
||||
unwindOut =
|
||||
IAaveExternalUnwinder(externalUnwinder).unwind(baseToken, quoteToken, remainingBase, minOutUnwind, unwindData);
|
||||
}
|
||||
|
||||
function _approveRepayment(address quoteToken, uint256 need) internal returns (uint256 surplus) {
|
||||
IERC20 quote = IERC20(quoteToken);
|
||||
uint256 quoteBal = quote.balanceOf(address(this));
|
||||
if (quoteBal < need) revert InsufficientToRepay();
|
||||
surplus = quoteBal - need;
|
||||
quote.forceApprove(pool, need);
|
||||
}
|
||||
|
||||
function _triggerAtomicBridge(address baseToken, uint256 baseBal, AtomicBridgeParams memory atomicBridge) internal {
|
||||
if (
|
||||
atomicBridge.destinationAsset == address(0) || atomicBridge.destinationRecipient == address(0)
|
||||
|| atomicBridge.bridgeAmount == 0 || atomicBridge.bridgeAmount > baseBal
|
||||
|| atomicBridge.destinationDeadline <= block.timestamp
|
||||
) revert InvalidAtomicBridge();
|
||||
|
||||
address escrowAddress = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).obligationEscrow();
|
||||
IERC20(baseToken).forceApprove(escrowAddress, atomicBridge.bridgeAmount);
|
||||
bytes32 obligationId = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).createIntent(
|
||||
IAaveAtomicBridgeCoordinator.CreateIntentParams({
|
||||
sourceChain: atomicBridge.sourceChain,
|
||||
destinationChain: atomicBridge.destinationChain,
|
||||
assetIn: baseToken,
|
||||
assetOut: atomicBridge.destinationAsset,
|
||||
amountIn: atomicBridge.bridgeAmount,
|
||||
minAmountOut: atomicBridge.minDestinationAmount,
|
||||
recipient: atomicBridge.destinationRecipient,
|
||||
deadline: atomicBridge.destinationDeadline,
|
||||
routeId: atomicBridge.routeId
|
||||
})
|
||||
);
|
||||
if (atomicBridge.submitCommitment) {
|
||||
IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).submitCommitment(
|
||||
obligationId, atomicBridge.settlementMode
|
||||
);
|
||||
}
|
||||
emit AtomicBridgeTriggered(
|
||||
obligationId,
|
||||
atomicBridge.coordinator,
|
||||
atomicBridge.destinationRecipient,
|
||||
atomicBridge.bridgeAmount,
|
||||
atomicBridge.minDestinationAmount
|
||||
);
|
||||
}
|
||||
}
|
||||
44
forkproof/src/DODOIntegrationExternalUnwinder.sol
Normal file
44
forkproof/src/DODOIntegrationExternalUnwinder.sol
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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";
|
||||
|
||||
interface IDODOIntegrationSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title DODOIntegrationExternalUnwinder
|
||||
* @notice Unwinds base -> quote through a DODO PMM integration.
|
||||
* @dev `data` must be `abi.encode(address pool)` selecting the registered pool to use.
|
||||
*/
|
||||
contract DODOIntegrationExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable integration;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address integration_) {
|
||||
integration = integration_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
if (data.length != 32) revert BadParams();
|
||||
|
||||
address pool = abi.decode(data, (address));
|
||||
if (pool == address(0)) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(integration, amountIn);
|
||||
amountOut = IDODOIntegrationSwapExactIn(integration).swapExactIn(pool, tokenIn, amountIn, minAmountOut);
|
||||
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
65
forkproof/src/DODOToUniswapV3MultiHopExternalUnwinder.sol
Normal file
65
forkproof/src/DODOToUniswapV3MultiHopExternalUnwinder.sol
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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";
|
||||
|
||||
interface IDODOMultiHopSwapExactInLike {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface ISwapRouterLikeMultiHop {
|
||||
struct ExactInputParams {
|
||||
bytes path;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
}
|
||||
|
||||
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
contract DODOToUniswapV3MultiHopExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable integration;
|
||||
address public immutable router;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address integration_, address router_) {
|
||||
integration = integration_;
|
||||
router = router_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
|
||||
(address dodoPool, address intermediateToken, uint256 minIntermediateOut, bytes memory uniswapPath) =
|
||||
abi.decode(data, (address, address, uint256, bytes));
|
||||
if (dodoPool == address(0) || intermediateToken == address(0) || intermediateToken == tokenIn || intermediateToken == tokenOut) {
|
||||
revert BadParams();
|
||||
}
|
||||
if (uniswapPath.length < 43) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(integration, amountIn);
|
||||
uint256 intermediateOut =
|
||||
IDODOMultiHopSwapExactInLike(integration).swapExactIn(dodoPool, tokenIn, amountIn, minIntermediateOut);
|
||||
|
||||
IERC20(intermediateToken).forceApprove(router, intermediateOut);
|
||||
amountOut = ISwapRouterLikeMultiHop(router).exactInput(
|
||||
ISwapRouterLikeMultiHop.ExactInputParams({
|
||||
path: uniswapPath,
|
||||
recipient: msg.sender,
|
||||
amountIn: intermediateOut,
|
||||
amountOutMinimum: minAmountOut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
74
forkproof/src/UniswapV3ExternalUnwinder.sol
Normal file
74
forkproof/src/UniswapV3ExternalUnwinder.sol
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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";
|
||||
|
||||
interface ISwapRouterLike {
|
||||
struct ExactInputSingleParams {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint24 fee;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
uint160 sqrtPriceLimitX96;
|
||||
}
|
||||
|
||||
struct ExactInputParams {
|
||||
bytes path;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
}
|
||||
|
||||
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
|
||||
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
contract UniswapV3ExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable router;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address router_) {
|
||||
router = router_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(router, amountIn);
|
||||
|
||||
if (data.length == 32) {
|
||||
uint24 fee = abi.decode(data, (uint24));
|
||||
return ISwapRouterLike(router).exactInputSingle(
|
||||
ISwapRouterLike.ExactInputSingleParams({
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
fee: fee,
|
||||
recipient: msg.sender,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: minAmountOut,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
bytes memory path = abi.decode(data, (bytes));
|
||||
return ISwapRouterLike(router).exactInput(
|
||||
ISwapRouterLike.ExactInputParams({
|
||||
path: path,
|
||||
recipient: msg.sender,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: minAmountOut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
391
forkproof/test/AaveQuotePushFlashReceiverMainnetFork.t.sol
Normal file
391
forkproof/test/AaveQuotePushFlashReceiverMainnetFork.t.sol
Normal file
@@ -0,0 +1,391 @@
|
||||
// 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 {
|
||||
AaveQuotePushFlashReceiver,
|
||||
IAaveExternalUnwinder
|
||||
} from "../src/AaveQuotePushFlashReceiver.sol";
|
||||
import {AtomicBridgeCoordinator} from "atomic/AtomicBridgeCoordinator.sol";
|
||||
import {AtomicFeePolicy} from "atomic/AtomicFeePolicy.sol";
|
||||
import {AtomicFulfillerRegistry} from "atomic/AtomicFulfillerRegistry.sol";
|
||||
import {AtomicLiquidityVault} from "atomic/AtomicLiquidityVault.sol";
|
||||
import {AtomicObligationEscrow} from "atomic/AtomicObligationEscrow.sol";
|
||||
import {AtomicSettlementRouter} from "atomic/AtomicSettlementRouter.sol";
|
||||
import {AtomicSlashingManager} from "atomic/AtomicSlashingManager.sol";
|
||||
import {AtomicTypes} from "atomic/AtomicTypes.sol";
|
||||
import {IAtomicSettlementAdapter} from "atomic/interfaces/IAtomicSettlementAdapter.sol";
|
||||
import {MockMintableToken} from "repo-test/dbis/MockMintableToken.sol";
|
||||
|
||||
contract AaveForkMockExternalUnwinder is IAaveExternalUnwinder {
|
||||
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 MockAtomicSettlementAdapter is IAtomicSettlementAdapter {
|
||||
address public lastToken;
|
||||
uint256 public lastAmount;
|
||||
address public lastRecipient;
|
||||
bytes32 public lastObligationId;
|
||||
|
||||
function executeSettlement(
|
||||
bytes32 obligationId,
|
||||
address token,
|
||||
uint256 amount,
|
||||
address recipient,
|
||||
bytes calldata
|
||||
) external payable returns (bytes32 settlementId) {
|
||||
lastObligationId = obligationId;
|
||||
lastToken = token;
|
||||
lastAmount = amount;
|
||||
lastRecipient = recipient;
|
||||
settlementId = keccak256(abi.encode(obligationId, token, amount, recipient, block.timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
contract AaveQuotePushFlashReceiverMainnetForkTest is Test {
|
||||
address constant AAVE_POOL_MAINNET = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
|
||||
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
||||
address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
bytes32 constant MOCK_SETTLEMENT_MODE = keccak256("MOCK_SETTLEMENT_MODE");
|
||||
|
||||
AaveQuotePushFlashReceiver internal receiver;
|
||||
AaveForkMockExternalUnwinder internal unwinder;
|
||||
MockMintableToken internal cusdc138;
|
||||
MockMintableToken internal bondToken;
|
||||
AtomicLiquidityVault internal atomicVault;
|
||||
AtomicFulfillerRegistry internal atomicRegistry;
|
||||
AtomicFeePolicy internal atomicFeePolicy;
|
||||
AtomicObligationEscrow internal atomicEscrow;
|
||||
AtomicSettlementRouter internal atomicRouter;
|
||||
AtomicSlashingManager internal atomicSlashingManager;
|
||||
AtomicBridgeCoordinator internal atomicCoordinator;
|
||||
MockAtomicSettlementAdapter internal mockSettlementAdapter;
|
||||
bytes32 internal atomicCorridorId;
|
||||
address internal destinationRecipient = address(0x138138);
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
|
||||
receiver = new AaveQuotePushFlashReceiver(AAVE_POOL_MAINNET);
|
||||
unwinder = new AaveForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 130, 100);
|
||||
deal(USDC, address(unwinder), 100_000_000);
|
||||
|
||||
cusdc138 = new MockMintableToken("Chain 138 USDC", "cUSDC", 6, address(this));
|
||||
bondToken = new MockMintableToken("Atomic Bond", "aBOND", 6, address(this));
|
||||
atomicVault = new AtomicLiquidityVault(address(this));
|
||||
atomicRegistry = new AtomicFulfillerRegistry(address(bondToken), address(this));
|
||||
atomicFeePolicy = new AtomicFeePolicy(address(this));
|
||||
atomicEscrow = new AtomicObligationEscrow(address(this));
|
||||
atomicRouter = new AtomicSettlementRouter(address(this));
|
||||
atomicSlashingManager = new AtomicSlashingManager(address(atomicRegistry), address(this));
|
||||
mockSettlementAdapter = new MockAtomicSettlementAdapter();
|
||||
atomicCoordinator = new AtomicBridgeCoordinator(
|
||||
address(atomicVault),
|
||||
address(atomicRegistry),
|
||||
address(atomicEscrow),
|
||||
address(atomicRouter),
|
||||
address(atomicFeePolicy),
|
||||
address(atomicSlashingManager),
|
||||
address(this),
|
||||
address(this)
|
||||
);
|
||||
|
||||
atomicVault.grantRole(atomicVault.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicVault.grantRole(atomicVault.RECONCILER_ROLE(), address(atomicCoordinator));
|
||||
atomicRegistry.grantRole(atomicRegistry.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicRegistry.grantRole(atomicRegistry.SLASHER_ROLE(), address(atomicSlashingManager));
|
||||
atomicEscrow.grantRole(atomicEscrow.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicRouter.grantRole(atomicRouter.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicSlashingManager.grantRole(atomicSlashingManager.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicRouter.setAdapter(MOCK_SETTLEMENT_MODE, address(mockSettlementAdapter));
|
||||
|
||||
atomicCorridorId = atomicCoordinator.getCorridorId(1, 138, CWUSDC, address(cusdc138));
|
||||
atomicCoordinator.configureCorridor(
|
||||
AtomicTypes.CorridorConfig({
|
||||
enabled: true,
|
||||
degraded: false,
|
||||
sourceChain: 1,
|
||||
destinationChain: 138,
|
||||
assetIn: CWUSDC,
|
||||
assetOut: address(cusdc138),
|
||||
maxNotional: 10_000_000,
|
||||
maxReservedBps: 8_000,
|
||||
targetBuffer: 100_000,
|
||||
maxSettlementBacklog: 5_000_000,
|
||||
maxOracleDriftBps: 500,
|
||||
fulfilmentTimeout: 1 days,
|
||||
settlementTimeout: 2 days,
|
||||
defaultSettlementMode: MOCK_SETTLEMENT_MODE
|
||||
})
|
||||
);
|
||||
atomicFeePolicy.setCorridorPolicy(atomicCorridorId, 25, 10, 12_000, 500, 1 days, 2 days);
|
||||
atomicVault.setTargetBuffer(atomicCorridorId, address(cusdc138), 100_000);
|
||||
cusdc138.mint(address(this), 5_000_000);
|
||||
cusdc138.approve(address(atomicVault), type(uint256).max);
|
||||
atomicVault.fundCorridor(atomicCorridorId, address(cusdc138), 5_000_000);
|
||||
|
||||
bondToken.mint(address(receiver), 5_000_000);
|
||||
vm.startPrank(address(receiver));
|
||||
bondToken.approve(address(atomicRegistry), type(uint256).max);
|
||||
atomicRegistry.depositBond(3_000_000);
|
||||
vm.stopPrank();
|
||||
atomicRegistry.setFulfillerActive(address(receiver), true);
|
||||
atomicRegistry.setCorridorAuthorization(address(receiver), atomicCorridorId, true);
|
||||
}
|
||||
|
||||
function testFork_aaveQuotePush_usesRealAaveAndRealMainnetPmm() public {
|
||||
uint256 amount = 2_964_298;
|
||||
uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
|
||||
uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: DODO_PMM_INTEGRATION_MAINNET,
|
||||
pmmPool: POOL_CWUSDC_USDC,
|
||||
baseToken: CWUSDC,
|
||||
externalUnwinder: address(unwinder),
|
||||
minOutPmm: 2_800_000,
|
||||
minOutUnwind: amount + 1_483,
|
||||
unwindData: bytes(""),
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(0),
|
||||
sourceChain: 0,
|
||||
destinationChain: 0,
|
||||
destinationAsset: address(0),
|
||||
bridgeAmount: 0,
|
||||
minDestinationAmount: 0,
|
||||
destinationRecipient: address(0),
|
||||
destinationDeadline: 0,
|
||||
routeId: bytes32(0),
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: false
|
||||
})
|
||||
});
|
||||
|
||||
receiver.flashQuotePush(USDC, amount, p);
|
||||
|
||||
uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
|
||||
uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
|
||||
uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
|
||||
uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
|
||||
uint256 premium = _aavePremium(amount);
|
||||
(uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
|
||||
_predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, 0, premium);
|
||||
uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);
|
||||
|
||||
assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver retains surplus");
|
||||
assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "base fully unwound");
|
||||
assertLt(poolBaseAfter, poolBaseBefore, "pool base decreased");
|
||||
assertGt(poolQuoteAfter, poolQuoteBefore, "pool quote increased");
|
||||
_assertWithinOnePercent(actualBaseOut, predictedBaseOut, "baseOut");
|
||||
_assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "netQuoteIn");
|
||||
_assertWithinOnePercent(actualSurplus, predictedSurplus, "surplus");
|
||||
assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "unwindOut");
|
||||
}
|
||||
|
||||
function testFork_aaveQuotePush_atomicCorridorFulfillment_1_to_138_cwusdc_to_cusdc() public {
|
||||
uint256 amount = 2_964_298;
|
||||
uint256 bridgeAmount = 500_000;
|
||||
uint256 destinationRecipientBefore = cusdc138.balanceOf(destinationRecipient);
|
||||
uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: DODO_PMM_INTEGRATION_MAINNET,
|
||||
pmmPool: POOL_CWUSDC_USDC,
|
||||
baseToken: CWUSDC,
|
||||
externalUnwinder: address(unwinder),
|
||||
minOutPmm: 2_800_000,
|
||||
minOutUnwind: amount + 1_483,
|
||||
unwindData: bytes(""),
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(atomicCoordinator),
|
||||
sourceChain: 1,
|
||||
destinationChain: 138,
|
||||
destinationAsset: address(cusdc138),
|
||||
bridgeAmount: bridgeAmount,
|
||||
minDestinationAmount: bridgeAmount,
|
||||
destinationRecipient: destinationRecipient,
|
||||
destinationDeadline: block.timestamp + 1 hours,
|
||||
routeId: atomicCorridorId,
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: true
|
||||
})
|
||||
});
|
||||
|
||||
uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
|
||||
receiver.flashQuotePush(USDC, amount, p);
|
||||
|
||||
uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
|
||||
uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
|
||||
uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
|
||||
uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
|
||||
uint256 premium = _aavePremium(amount);
|
||||
(uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
|
||||
_predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, bridgeAmount, premium);
|
||||
uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);
|
||||
assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver still retains quote surplus");
|
||||
assertEq(cusdc138.balanceOf(destinationRecipient), destinationRecipientBefore + bridgeAmount, "destination funded");
|
||||
assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "remaining base fully unwound");
|
||||
_assertWithinOnePercent(actualBaseOut, predictedBaseOut, "atomic baseOut");
|
||||
_assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "atomic netQuoteIn");
|
||||
_assertWithinOnePercent(actualSurplus, predictedSurplus, "atomic surplus");
|
||||
assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "atomic unwindOut");
|
||||
|
||||
AtomicTypes.CorridorLiquidityState memory state =
|
||||
atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
|
||||
assertEq(state.settlementBacklog, bridgeAmount, "backlog increased by delivered amount");
|
||||
assertEq(state.totalLiquidity, 5_000_000 - bridgeAmount, "vault liquidity debited on immediate fulfillment");
|
||||
}
|
||||
|
||||
function testFork_aaveQuotePush_atomicCorridorSettlementConfirmation_1_to_138() public {
|
||||
uint256 amount = 2_964_298;
|
||||
uint256 bridgeAmount = 500_000;
|
||||
uint256 deadline = block.timestamp + 1 hours;
|
||||
uint256 receiverBondBefore = atomicRegistry.availableBond(address(receiver));
|
||||
uint256 treasuryBaseBefore = IERC20(CWUSDC).balanceOf(address(this));
|
||||
uint256 receiverBaseBefore = IERC20(CWUSDC).balanceOf(address(receiver));
|
||||
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: DODO_PMM_INTEGRATION_MAINNET,
|
||||
pmmPool: POOL_CWUSDC_USDC,
|
||||
baseToken: CWUSDC,
|
||||
externalUnwinder: address(unwinder),
|
||||
minOutPmm: 2_800_000,
|
||||
minOutUnwind: amount + 1_483,
|
||||
unwindData: bytes(""),
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(atomicCoordinator),
|
||||
sourceChain: 1,
|
||||
destinationChain: 138,
|
||||
destinationAsset: address(cusdc138),
|
||||
bridgeAmount: bridgeAmount,
|
||||
minDestinationAmount: bridgeAmount,
|
||||
destinationRecipient: destinationRecipient,
|
||||
destinationDeadline: deadline,
|
||||
routeId: atomicCorridorId,
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: true
|
||||
})
|
||||
});
|
||||
|
||||
receiver.flashQuotePush(USDC, amount, p);
|
||||
bytes32 obligationId = _deriveObligationId(bridgeAmount, deadline);
|
||||
|
||||
AtomicTypes.AtomicObligation memory fulfilled = atomicCoordinator.getObligation(obligationId);
|
||||
assertEq(uint8(fulfilled.status), uint8(AtomicTypes.ObligationStatus.Fulfilled), "obligation fulfilled");
|
||||
|
||||
atomicCoordinator.initiateSettlement(obligationId, abi.encodePacked(bytes32(uint256(138))));
|
||||
|
||||
AtomicTypes.AtomicObligation memory pending = atomicCoordinator.getObligation(obligationId);
|
||||
assertEq(uint8(pending.status), uint8(AtomicTypes.ObligationStatus.SettlementPending), "obligation pending");
|
||||
assertEq(mockSettlementAdapter.lastObligationId(), obligationId, "adapter saw obligation");
|
||||
assertEq(mockSettlementAdapter.lastToken(), CWUSDC, "adapter token");
|
||||
assertEq(mockSettlementAdapter.lastRecipient(), destinationRecipient, "adapter recipient");
|
||||
|
||||
uint256 expectedFulfillerFee = (bridgeAmount * 25) / 10_000;
|
||||
uint256 expectedProtocolFee = (bridgeAmount * 10) / 10_000;
|
||||
uint256 expectedSettlementAmount = bridgeAmount - expectedFulfillerFee - expectedProtocolFee;
|
||||
assertEq(mockSettlementAdapter.lastAmount(), expectedSettlementAmount, "adapter amount");
|
||||
assertEq(IERC20(CWUSDC).balanceOf(address(this)), treasuryBaseBefore + expectedProtocolFee, "protocol fee received");
|
||||
assertEq(
|
||||
IERC20(CWUSDC).balanceOf(address(receiver)),
|
||||
receiverBaseBefore + expectedFulfillerFee,
|
||||
"fulfiller fee received"
|
||||
);
|
||||
|
||||
cusdc138.mint(address(this), bridgeAmount);
|
||||
cusdc138.approve(address(atomicVault), bridgeAmount);
|
||||
atomicCoordinator.confirmSettlement(obligationId, bridgeAmount);
|
||||
|
||||
AtomicTypes.AtomicObligation memory settled = atomicCoordinator.getObligation(obligationId);
|
||||
assertEq(uint8(settled.status), uint8(AtomicTypes.ObligationStatus.Settled), "obligation settled");
|
||||
|
||||
AtomicTypes.CorridorLiquidityState memory state =
|
||||
atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
|
||||
assertEq(state.settlementBacklog, 0, "backlog cleared");
|
||||
assertEq(state.totalLiquidity, 5_000_000, "vault replenished");
|
||||
assertEq(atomicRegistry.availableBond(address(receiver)), receiverBondBefore, "bond released");
|
||||
}
|
||||
|
||||
function _deriveObligationId(uint256 bridgeAmount, uint256 deadline) internal view returns (bytes32) {
|
||||
bytes32 intentId = keccak256(
|
||||
abi.encode(
|
||||
block.chainid,
|
||||
address(receiver),
|
||||
uint256(1),
|
||||
atomicCorridorId,
|
||||
bridgeAmount,
|
||||
bridgeAmount,
|
||||
deadline,
|
||||
atomicCorridorId
|
||||
)
|
||||
);
|
||||
return keccak256(abi.encode(intentId, destinationRecipient));
|
||||
}
|
||||
|
||||
function _predictQuotePush(
|
||||
uint256 baseReserve,
|
||||
uint256 quoteReserve,
|
||||
uint256 grossQuoteIn,
|
||||
uint256 lpFeeBps,
|
||||
uint256 unwindNumerator,
|
||||
uint256 unwindDenominator,
|
||||
uint256 bridgeAmount,
|
||||
uint256 premium
|
||||
) internal pure returns (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) {
|
||||
uint256 netQuoteIn = (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
|
||||
predictedBaseOut = (netQuoteIn * baseReserve) / (quoteReserve + netQuoteIn);
|
||||
uint256 remainingBase = predictedBaseOut - bridgeAmount;
|
||||
predictedUnwindOut = (remainingBase * unwindNumerator) / unwindDenominator;
|
||||
predictedSurplus = predictedUnwindOut - grossQuoteIn - premium;
|
||||
}
|
||||
|
||||
function _netQuoteIn(uint256 grossQuoteIn, uint256 lpFeeBps) internal pure returns (uint256) {
|
||||
return (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
|
||||
}
|
||||
|
||||
function _assertWithinOnePercent(uint256 actual, uint256 expected, string memory label) internal pure {
|
||||
if (expected == 0) {
|
||||
require(actual == 0, label);
|
||||
return;
|
||||
}
|
||||
uint256 diff = actual > expected ? actual - expected : expected - actual;
|
||||
require(diff * 10_000 <= expected * 100, label);
|
||||
}
|
||||
|
||||
function _aavePremium(uint256 amount) internal pure returns (uint256) {
|
||||
return (amount * 5) / 10_000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 {DODOIntegrationExternalUnwinder} from "../src/DODOIntegrationExternalUnwinder.sol";
|
||||
|
||||
contract DODOIntegrationExternalUnwinderMainnetForkTest is Test {
|
||||
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
||||
address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
|
||||
DODOIntegrationExternalUnwinder internal unwinder;
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
unwinder = new DODOIntegrationExternalUnwinder(DODO_PMM_INTEGRATION_MAINNET);
|
||||
}
|
||||
|
||||
function testFork_cWUSDCToUSDC_unwindsThroughMainnetDodoIntegration() public {
|
||||
uint256 amountIn = 1_000_000;
|
||||
deal(CWUSDC, address(this), amountIn);
|
||||
IERC20(CWUSDC).approve(address(unwinder), amountIn);
|
||||
|
||||
uint256 before = IERC20(USDC).balanceOf(address(this));
|
||||
uint256 amountOut = unwinder.unwind(CWUSDC, USDC, amountIn, 1, abi.encode(POOL_CWUSDC_USDC));
|
||||
uint256 afterBal = IERC20(USDC).balanceOf(address(this));
|
||||
|
||||
assertGt(amountOut, 0, "amountOut > 0");
|
||||
assertEq(afterBal - before, amountOut, "USDC received");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 {DODOToUniswapV3MultiHopExternalUnwinder} from "../src/DODOToUniswapV3MultiHopExternalUnwinder.sol";
|
||||
|
||||
contract DODOToUniswapV3MultiHopExternalUnwinderMainnetForkTest is Test {
|
||||
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
||||
address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
|
||||
address constant POOL_CWUSDC_USDT = 0xCC0fd27A40775c9AfcD2BBd3f7c902b0192c247A;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
|
||||
DODOToUniswapV3MultiHopExternalUnwinder internal unwinder;
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
unwinder = new DODOToUniswapV3MultiHopExternalUnwinder(DODO_PMM_INTEGRATION_MAINNET, UNISWAP_V3_ROUTER);
|
||||
}
|
||||
|
||||
function testFork_cWUSDCToUSDC_multihopViaUSDT_works() public {
|
||||
uint256 amountIn = 1_000_000;
|
||||
deal(CWUSDC, address(this), amountIn);
|
||||
IERC20(CWUSDC).approve(address(unwinder), amountIn);
|
||||
|
||||
bytes memory path = abi.encodePacked(USDT, uint24(100), USDC);
|
||||
bytes memory data = abi.encode(POOL_CWUSDC_USDT, USDT, uint256(1), path);
|
||||
|
||||
uint256 before = IERC20(USDC).balanceOf(address(this));
|
||||
uint256 amountOut = unwinder.unwind(CWUSDC, USDC, amountIn, 1, data);
|
||||
uint256 afterBal = IERC20(USDC).balanceOf(address(this));
|
||||
|
||||
assertGt(amountOut, 0, "amountOut > 0");
|
||||
assertEq(afterBal - before, amountOut, "USDC received");
|
||||
}
|
||||
}
|
||||
47
forkproof/test/UniswapV3ExternalUnwinderMainnetFork.t.sol
Normal file
47
forkproof/test/UniswapV3ExternalUnwinderMainnetFork.t.sol
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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 {UniswapV3ExternalUnwinder} from "../src/UniswapV3ExternalUnwinder.sol";
|
||||
|
||||
interface IWETHFork {
|
||||
function deposit() external payable;
|
||||
function transfer(address to, uint256 value) external returns (bool);
|
||||
}
|
||||
|
||||
contract UniswapV3ExternalUnwinderMainnetForkTest is Test {
|
||||
address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
|
||||
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
|
||||
UniswapV3ExternalUnwinder internal unwinder;
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
unwinder = new UniswapV3ExternalUnwinder(UNISWAP_V3_ROUTER);
|
||||
}
|
||||
|
||||
function testFork_knownRoute_WETHToUSDC_singleHopWorks() public {
|
||||
vm.deal(address(this), 1 ether);
|
||||
IWETHFork(WETH).deposit{value: 1 ether}();
|
||||
IERC20(WETH).approve(address(unwinder), 1 ether);
|
||||
uint256 before = IERC20(USDC).balanceOf(address(this));
|
||||
|
||||
uint256 amountOut = unwinder.unwind(WETH, USDC, 1 ether, 1, abi.encode(uint24(3000)));
|
||||
|
||||
uint256 afterBal = IERC20(USDC).balanceOf(address(this));
|
||||
assertGt(amountOut, 0, "amountOut > 0");
|
||||
assertEq(afterBal - before, amountOut, "USDC received");
|
||||
}
|
||||
|
||||
function testFork_cWUSDCToUSDC_routeUnavailableOnUniswapV3() public {
|
||||
deal(CWUSDC, address(this), 1_000_000);
|
||||
IERC20(CWUSDC).approve(address(unwinder), 1_000_000);
|
||||
|
||||
vm.expectRevert();
|
||||
unwinder.unwind(CWUSDC, USDC, 1_000_000, 1, abi.encode(uint24(3000)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user