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