Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions scripts/DeployCCTPBridge.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.28;

import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol';
import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol';
import {AaveV3Optimism} from 'aave-address-book/AaveV3Optimism.sol';
import {AaveV3Polygon} from 'aave-address-book/AaveV3Polygon.sol';
import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol';
import {ArbitrumScript, BaseScript, EthereumScript, OptimismScript, PolygonScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol';
import {AaveCctpBridge} from 'src/bridges/cctp/AaveCctpBridge.sol';
import {CctpConstants} from 'src/bridges/cctp/CctpConstants.sol';

address constant TOKEN_LOGIC = 0x3765A685a401622C060E5D700D9ad89413363a91;
address constant GUARDIAN = 0x3765A685a401622C060E5D700D9ad89413363a91;

contract DeployCCTPBridgeEthereum is EthereumScript {
function run() external broadcast {
bytes32 salt = 'Aave CCTP Bridge';
new AaveCctpBridge{salt: salt}(
CctpConstants.ETHEREUM_TOKEN_MESSENGER,
CctpConstants.ETHEREUM_USDC,
TOKEN_LOGIC,
GUARDIAN,
address(AaveV3Ethereum.COLLECTOR)
);
}
}

contract DeployCCTPBridgeArbitrum is ArbitrumScript {
function run() external broadcast {
bytes32 salt = 'Aave CCTP Bridge';
new AaveCctpBridge{salt: salt}(
CctpConstants.ARBITRUM_TOKEN_MESSENGER,
CctpConstants.ARBITRUM_USDC,
TOKEN_LOGIC,
GUARDIAN,
address(AaveV3Arbitrum.COLLECTOR)
);
}
}

contract DeployCCTPBridgeOptimism is OptimismScript {
function run() external broadcast {
bytes32 salt = 'Aave CCTP Bridge';
new AaveCctpBridge{salt: salt}(
CctpConstants.OPTIMISM_TOKEN_MESSENGER,
CctpConstants.OPTIMISM_USDC,
TOKEN_LOGIC,
GUARDIAN,
address(AaveV3Optimism.COLLECTOR)
);
}
}

contract DeployCCTPBridgePolygon is PolygonScript {
function run() external broadcast {
bytes32 salt = 'Aave CCTP Bridge';
new AaveCctpBridge{salt: salt}(
CctpConstants.POLYGON_TOKEN_MESSENGER,
CctpConstants.POLYGON_USDC,
TOKEN_LOGIC,
GUARDIAN,
address(AaveV3Polygon.COLLECTOR)
);
}
}

contract DeployCCTPBridgeBase is BaseScript {
function run() external broadcast {
bytes32 salt = 'Aave CCTP Bridge';
new AaveCctpBridge{salt: salt}(
CctpConstants.BASE_TOKEN_MESSENGER,
CctpConstants.BASE_USDC,
TOKEN_LOGIC,
GUARDIAN,
address(AaveV3Base.COLLECTOR)
);
}
}
155 changes: 155 additions & 0 deletions src/bridges/cctp/AaveCctpBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {ICollector} from 'aave-address-book/AaveV3.sol';
import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol';
import {OwnableWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol';
import {RescuableBase} from 'solidity-utils/contracts/utils/RescuableBase.sol';

import {IAaveCctpBridge} from './interfaces/IAaveCctpBridge.sol';
import {IMessageTransmitterV2} from './interfaces/IMessageTransmitterV2.sol';
import {ITokenMessengerV2} from './interfaces/ITokenMessengerV2.sol';

/// @title AaveCctpBridge
/// @author stevyhacker (TokenLogic)
/// @notice Helper contract to bridge USDC using Circle's CCTP V2
contract AaveCctpBridge is OwnableWithGuardian, RescuableBase, IAaveCctpBridge {
using SafeERC20 for IERC20;

/// @notice Finality threshold constant for Fast Transfer is 1000 and means just tx confirmation is sufficient
/// @dev Required confirmations per chain https://developers.circle.com/cctp/required-block-confirmations#cctp-fast-message-attestation-times
uint32 public constant FAST = 1000;

/// @notice Finality threshold constant for Standard Transfer is 2000 and means hard finality is requested
/// @dev Required confirmations per chain https://developers.circle.com/cctp/required-block-confirmations#cctp-standard-message-attestation-times
uint32 public constant STANDARD = 2000;

/// @inheritdoc IAaveCctpBridge
address public immutable TOKEN_MESSENGER;

/// @inheritdoc IAaveCctpBridge
address public immutable USDC;

/// @inheritdoc IAaveCctpBridge
address public immutable COLLECTOR;

/// @inheritdoc IAaveCctpBridge
uint32 public immutable LOCAL_DOMAIN;

/// @inheritdoc IAaveCctpBridge
mapping(bytes32 receiver => bool allowed) public isAllowedReceiver;

/// @param tokenMessenger The TokenMessengerV2 address on this chain
/// @param usdc The USDC token address on this chain
/// @param owner The owner of the contract upon deployment
/// @param guardian The initial guardian of the contract upon deployment
/// @param collector The address of the source collector on this chain
constructor(
address tokenMessenger,
address usdc,
address owner,
address guardian,
address collector
) OwnableWithGuardian(owner, guardian) {
if (tokenMessenger == address(0)) revert InvalidZeroAddress();
if (usdc == address(0)) revert InvalidZeroAddress();
if (guardian == address(0)) revert InvalidZeroAddress();
if (collector == address(0)) revert InvalidZeroAddress();

TOKEN_MESSENGER = tokenMessenger;
USDC = usdc;
COLLECTOR = collector;

address localMessageTransmitter = ITokenMessengerV2(tokenMessenger).localMessageTransmitter();
LOCAL_DOMAIN = IMessageTransmitterV2(localMessageTransmitter).localDomain();
}

/// @dev Enables receiving native tokens so they can be rescued back to COLLECTOR if needed
receive() external payable {}

/// @inheritdoc IAaveCctpBridge
function bridge(
uint32 destinationDomain,
uint256 amount,
address receiver,
uint256 maxFee,
TransferSpeed speed
) external onlyOwnerOrGuardian {
_bridge(destinationDomain, amount, bytes32(uint256(uint160(receiver))), maxFee, speed);
}

/// @inheritdoc IAaveCctpBridge
function bridgeNonEvm(
uint32 destinationDomain,
uint256 amount,
bytes32 receiver,
uint256 maxFee,
TransferSpeed speed
) external onlyOwnerOrGuardian {
_bridge(destinationDomain, amount, receiver, maxFee, speed);
}

/// @inheritdoc IAaveCctpBridge
function setAllowedReceiver(address receiver, bool allowed) external onlyOwner {
if (receiver == address(0)) revert InvalidZeroAddress();
isAllowedReceiver[bytes32(uint256(uint160(receiver)))] = allowed;
}

/// @inheritdoc IAaveCctpBridge
function setAllowedReceiverNonEVM(bytes32 receiver, bool allowed) external onlyOwner {
if (receiver == bytes32(0)) revert InvalidZeroAddress();
isAllowedReceiver[receiver] = allowed;
}

/// @inheritdoc IAaveCctpBridge
function rescueToken(address token) external onlyOwnerOrGuardian {
_emergencyTokenTransfer(token, COLLECTOR, type(uint256).max);
}

/// @inheritdoc IAaveCctpBridge
function rescueEth() external onlyOwnerOrGuardian {
_emergencyEtherTransfer(COLLECTOR, address(this).balance);
}

/// @inheritdoc RescuableBase
function maxRescue(address token) public view override(RescuableBase) returns (uint256) {
return IERC20(token).balanceOf(address(this));
}

/// @notice Executes a USDC bridge transfer using CCTP V2
/// @dev Pulls USDC from the source collector, approves the token messenger, and burns for minting on the destination domain
/// @param destinationDomain The CCTP domain of the destination chain
/// @param amount The amount of USDC to bridge, denominated in USDC with 6 decimals, 1 USDC = 1_000_000
/// @param receiver The destination receiver identifier
/// @param maxFee Maximum fee willing to pay for a Fast Transfer, denominated in USDC with 6 decimals, 1 USDC = 1_000_000
/// @param speed Transfer speed (Fast or Standard)
function _bridge(
uint32 destinationDomain,
uint256 amount,
bytes32 receiver,
uint256 maxFee,
TransferSpeed speed
) internal {
if (amount == 0) revert InvalidZeroAmount();
if (destinationDomain == LOCAL_DOMAIN) revert InvalidDestinationDomain();
if (!isAllowedReceiver[receiver]) revert OnlyAllowedRecipients();

uint32 finalityThreshold = speed == TransferSpeed.Fast ? FAST : STANDARD;

ICollector(COLLECTOR).transfer(IERC20(USDC), address(this), amount);
IERC20(USDC).forceApprove(TOKEN_MESSENGER, amount);

ITokenMessengerV2(TOKEN_MESSENGER).depositForBurn(
amount,
destinationDomain,
receiver,
USDC,
bytes32(0),
maxFee,
finalityThreshold
);

emit Bridge(USDC, destinationDomain, receiver, amount, speed);
}
}
49 changes: 49 additions & 0 deletions src/bridges/cctp/CctpConstants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title CctpConstants
/// @notice Constants for CCTP V2 bridge testing
/// @dev Domain IDs from https://developers.circle.com/cctp/cctp-supported-blockchains
library CctpConstants {
// CCTP Domain IDs
uint32 public constant ETHEREUM_DOMAIN = 0;
uint32 public constant AVALANCHE_DOMAIN = 1;
uint32 public constant OPTIMISM_DOMAIN = 2;
uint32 public constant ARBITRUM_DOMAIN = 3;
uint32 public constant SOLANA_DOMAIN = 5;
uint32 public constant BASE_DOMAIN = 6;
uint32 public constant POLYGON_DOMAIN = 7;
uint32 public constant UNICHAIN_DOMAIN = 10;
uint32 public constant LINEA_DOMAIN = 11;
uint32 public constant SONIC_DOMAIN = 13;
uint32 public constant MONAD_DOMAIN = 15;
uint32 public constant INK_DOMAIN = 21;

// Finality Thresholds
uint32 public constant FAST_FINALITY_THRESHOLD = 1000;
uint32 public constant STANDARD_FINALITY_THRESHOLD = 2000;

// Mainnet TokenMessengerV2 addresses
// https://etherscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d
address public constant ETHEREUM_TOKEN_MESSENGER = 0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d;
// https://arbiscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d
address public constant ARBITRUM_TOKEN_MESSENGER = 0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d;
// https://basescan.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d
address public constant BASE_TOKEN_MESSENGER = 0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d;
// https://optimistic.etherscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d
address public constant OPTIMISM_TOKEN_MESSENGER = 0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d;
// https://polygonscan.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d
address public constant POLYGON_TOKEN_MESSENGER = 0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d;

// Mainnet USDC addresses
// https://etherscan.io/address/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
address public constant ETHEREUM_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
// https://arbiscan.io/address/0xaf88d065e77c8cC2239327C5EDb3A432268e5831
address public constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
// https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
address public constant BASE_USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
// https://optimistic.etherscan.io/address/0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85
address public constant OPTIMISM_USDC = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85;
// https://polygonscan.com/address/0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
address public constant POLYGON_USDC = 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359;
}
Loading