// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AtomicBridgeCoordinator} from "./AtomicBridgeCoordinator.sol"; interface ILegacyBurnableERC20 is IERC20 { function burn(uint256 amount) external; } interface IMintableERC20 is IERC20 { function mint(address to, uint256 amount) external; } interface IAtomicObligationEscrowAddress { function obligationEscrow() external view returns (address); } /** * @title AtomicX402SourceAdapter * @notice Normalizes Chain 138 cUSDC/cUSDC_V2 input into cUSDC_V2 before creating * an atomic x402 obligation. Legacy cUSDC is burned by this adapter after * transfer-in; cUSDC_V2 is escrowed as the coordinator assetIn. */ contract AtomicX402SourceAdapter { using SafeERC20 for IERC20; uint64 public immutable sourceChain; uint64 public immutable destinationChainSelector; ILegacyBurnableERC20 public immutable legacyCUSDC; IMintableERC20 public immutable cUSDCV2; IERC20 public immutable cWUSDCV2; AtomicBridgeCoordinator public immutable coordinator; address public immutable escrow; event SourceIntentCreated( bytes32 indexed obligationId, address indexed payer, address indexed sourceToken, address recipient, uint256 amountIn, uint256 minAmountOut ); error UnsupportedSourceToken(address token); error ZeroAddress(); error ZeroAmount(); constructor( address legacyCUSDC_, address cUSDCV2_, address cWUSDCV2_, address coordinator_, uint64 sourceChain_, uint64 destinationChainSelector_ ) { if ( legacyCUSDC_ == address(0) || cUSDCV2_ == address(0) || cWUSDCV2_ == address(0) || coordinator_ == address(0) ) { revert ZeroAddress(); } legacyCUSDC = ILegacyBurnableERC20(legacyCUSDC_); cUSDCV2 = IMintableERC20(cUSDCV2_); cWUSDCV2 = IERC20(cWUSDCV2_); coordinator = AtomicBridgeCoordinator(coordinator_); sourceChain = sourceChain_; destinationChainSelector = destinationChainSelector_; escrow = IAtomicObligationEscrowAddress(coordinator_).obligationEscrow(); IERC20(cUSDCV2_).forceApprove(escrow, type(uint256).max); } function createIntent( address sourceToken, uint256 amountIn, uint256 minAmountOut, address recipient, uint256 deadline, bytes32 routeId ) external returns (bytes32 obligationId) { if (amountIn == 0 || minAmountOut == 0) revert ZeroAmount(); if (recipient == address(0)) revert ZeroAddress(); _normalizeToV2(sourceToken, amountIn); obligationId = coordinator.createIntentFor( AtomicBridgeCoordinator.CreateIntentParams({ sourceChain: sourceChain, destinationChain: destinationChainSelector, assetIn: address(cUSDCV2), assetOut: address(cWUSDCV2), amountIn: amountIn, minAmountOut: minAmountOut, recipient: recipient, deadline: deadline, routeId: routeId }), address(this), msg.sender ); emit SourceIntentCreated(obligationId, msg.sender, sourceToken, recipient, amountIn, minAmountOut); } function _normalizeToV2(address sourceToken, uint256 amountIn) internal { if (sourceToken == address(legacyCUSDC)) { IERC20(sourceToken).safeTransferFrom(msg.sender, address(this), amountIn); legacyCUSDC.burn(amountIn); cUSDCV2.mint(address(this), amountIn); return; } if (sourceToken == address(cUSDCV2)) { IERC20(sourceToken).safeTransferFrom(msg.sender, address(this), amountIn); return; } revert UnsupportedSourceToken(sourceToken); } }