Files
smom-dbis-138/contracts/bridge/CWMultiTokenBridgeL1.sol
defiQUG 76aa419320 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
2026-04-07 23:40:52 -07:00

359 lines
15 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../ccip/IRouterClient.sol";
import "./integration/ICWReserveVerifier.sol";
/**
* @title CWMultiTokenBridgeL1
* @notice Chain 138 side escrow bridge for non-prefunded c* -> cW* routing.
* @dev Locks canonical c* tokens on send and releases them on return messages.
*/
contract CWMultiTokenBridgeL1 {
using SafeERC20 for IERC20;
IRouterClient public sendRouter;
address public receiveRouter;
address public feeToken;
address public admin;
address public reserveVerifier;
struct DestinationConfig {
address receiverBridge;
bool enabled;
}
mapping(address => mapping(uint64 => DestinationConfig)) public destinations;
mapping(bytes32 => bool) public processed;
mapping(address => bool) public supportedCanonicalToken;
mapping(address => bool) public paused;
mapping(address => uint256) public lockedBalance;
mapping(address => uint256) public totalOutstanding;
mapping(address => mapping(uint64 => uint256)) public outstandingMinted;
mapping(address => mapping(uint64 => uint256)) public maxOutstanding;
event Locked(address indexed canonicalToken, address indexed user, uint256 amount);
event Released(address indexed canonicalToken, address indexed recipient, uint256 amount);
event MessageSent(
bytes32 indexed messageId,
address indexed canonicalToken,
uint64 indexed destinationChainSelector,
address recipient,
uint256 amount
);
event DestinationConfigured(address indexed canonicalToken, uint64 indexed chainSelector, address receiverBridge, bool enabled);
event SupportedCanonicalTokenConfigured(address indexed canonicalToken, bool enabled);
event TokenPauseUpdated(address indexed canonicalToken, bool paused);
event MaxOutstandingUpdated(address indexed canonicalToken, uint64 indexed chainSelector, uint256 maxOutstandingAmount);
event OutstandingUpdated(
address indexed canonicalToken,
uint64 indexed chainSelector,
uint256 chainOutstanding,
uint256 totalOutstandingAmount,
uint256 lockedBalanceAmount
);
event SendRouterUpdated(address indexed newRouter);
event ReceiveRouterUpdated(address indexed newRouter);
event FeeTokenUpdated(address indexed newFeeToken);
event AdminChanged(address indexed newAdmin);
event ReserveVerifierUpdated(address indexed newVerifier);
modifier onlyAdmin() {
require(msg.sender == admin, "CWMultiTokenBridgeL1: only admin");
_;
}
modifier onlyReceiveRouter() {
require(msg.sender == receiveRouter, "CWMultiTokenBridgeL1: only receive router");
_;
}
constructor(address _sendRouter, address _receiveRouter, address _feeToken) {
require(_sendRouter != address(0), "CWMultiTokenBridgeL1: zero send router");
require(_receiveRouter != address(0), "CWMultiTokenBridgeL1: zero receive router");
sendRouter = IRouterClient(_sendRouter);
receiveRouter = _receiveRouter;
feeToken = _feeToken;
admin = msg.sender;
}
function configureDestination(
address canonicalToken,
uint64 chainSelector,
address receiverBridge,
bool enabled
) external onlyAdmin {
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
require(receiverBridge != address(0), "CWMultiTokenBridgeL1: zero bridge");
destinations[canonicalToken][chainSelector] = DestinationConfig({
receiverBridge: receiverBridge,
enabled: enabled
});
emit DestinationConfigured(canonicalToken, chainSelector, receiverBridge, enabled);
}
function configureSupportedCanonicalToken(address canonicalToken, bool enabled) external onlyAdmin {
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
if (!enabled) {
require(lockedBalance[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still locked");
require(totalOutstanding[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still outstanding");
}
supportedCanonicalToken[canonicalToken] = enabled;
emit SupportedCanonicalTokenConfigured(canonicalToken, enabled);
}
function setTokenPaused(address canonicalToken, bool isPaused) external onlyAdmin {
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
paused[canonicalToken] = isPaused;
emit TokenPauseUpdated(canonicalToken, isPaused);
}
function setMaxOutstanding(address canonicalToken, uint64 chainSelector, uint256 amount) external onlyAdmin {
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
maxOutstanding[canonicalToken][chainSelector] = amount;
emit MaxOutstandingUpdated(canonicalToken, chainSelector, amount);
}
function setSendRouter(address newRouter) external onlyAdmin {
require(newRouter != address(0), "CWMultiTokenBridgeL1: zero router");
sendRouter = IRouterClient(newRouter);
emit SendRouterUpdated(newRouter);
}
function setReceiveRouter(address newRouter) external onlyAdmin {
require(newRouter != address(0), "CWMultiTokenBridgeL1: zero router");
receiveRouter = newRouter;
emit ReceiveRouterUpdated(newRouter);
}
function setFeeToken(address newFeeToken) external onlyAdmin {
feeToken = newFeeToken;
emit FeeTokenUpdated(newFeeToken);
}
function changeAdmin(address newAdmin) external onlyAdmin {
require(newAdmin != address(0), "CWMultiTokenBridgeL1: zero admin");
admin = newAdmin;
emit AdminChanged(newAdmin);
}
function setReserveVerifier(address newVerifier) external onlyAdmin {
reserveVerifier = newVerifier;
emit ReserveVerifierUpdated(newVerifier);
}
function calculateFee(
address canonicalToken,
uint64 destinationChainSelector,
address recipient,
uint256 amount
) external view returns (uint256) {
DestinationConfig memory dest = destinations[canonicalToken][destinationChainSelector];
require(dest.enabled, "CWMultiTokenBridgeL1: destination disabled");
return sendRouter.getFee(destinationChainSelector, _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount));
}
function availableToMint(
address canonicalToken,
uint64 destinationChainSelector
) public view returns (uint256) {
return _availableToMint(
canonicalToken,
destinationChainSelector,
lockedBalance[canonicalToken],
totalOutstanding[canonicalToken],
outstandingMinted[canonicalToken][destinationChainSelector]
);
}
function lockAndSend(
address canonicalToken,
uint64 destinationChainSelector,
address recipient,
uint256 amount
) external payable returns (bytes32 messageId) {
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient");
require(amount > 0, "CWMultiTokenBridgeL1: zero amount");
require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token");
require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused");
DestinationConfig memory dest = destinations[canonicalToken][destinationChainSelector];
require(dest.enabled, "CWMultiTokenBridgeL1: destination disabled");
IERC20(canonicalToken).safeTransferFrom(msg.sender, address(this), amount);
uint256 nextLockedBalance = lockedBalance[canonicalToken] + amount;
uint256 currentTotalOutstanding = totalOutstanding[canonicalToken];
uint256 currentChainOutstanding = outstandingMinted[canonicalToken][destinationChainSelector];
require(
amount <= _availableToMint(
canonicalToken,
destinationChainSelector,
nextLockedBalance,
currentTotalOutstanding,
currentChainOutstanding
),
"CWMultiTokenBridgeL1: exceeds escrow capacity"
);
lockedBalance[canonicalToken] = nextLockedBalance;
totalOutstanding[canonicalToken] = currentTotalOutstanding + amount;
outstandingMinted[canonicalToken][destinationChainSelector] = currentChainOutstanding + amount;
if (reserveVerifier != address(0) && !ICWReserveVerifier(reserveVerifier).verifyLock(canonicalToken, destinationChainSelector, amount)) {
revert("CWMultiTokenBridgeL1: reserve verification failed");
}
emit Locked(canonicalToken, msg.sender, amount);
emit OutstandingUpdated(
canonicalToken,
destinationChainSelector,
outstandingMinted[canonicalToken][destinationChainSelector],
totalOutstanding[canonicalToken],
lockedBalance[canonicalToken]
);
IRouterClient.EVM2AnyMessage memory message =
_buildMessage(dest.receiverBridge, canonicalToken, recipient, amount);
uint256 fee = sendRouter.getFee(destinationChainSelector, message);
_collectAndApproveFee(fee);
(messageId, ) = feeToken == address(0)
? sendRouter.ccipSend{value: fee}(destinationChainSelector, message)
: sendRouter.ccipSend(destinationChainSelector, message);
emit MessageSent(messageId, canonicalToken, destinationChainSelector, recipient, amount);
_refundNativeExcess(fee);
return messageId;
}
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyReceiveRouter {
require(!processed[message.messageId], "CWMultiTokenBridgeL1: replayed");
(address canonicalToken, address recipient, uint256 amount) =
abi.decode(message.data, (address, address, uint256));
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient");
require(amount > 0, "CWMultiTokenBridgeL1: zero amount");
require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token");
require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused");
DestinationConfig memory peer = destinations[canonicalToken][message.sourceChainSelector];
require(peer.enabled, "CWMultiTokenBridgeL1: peer disabled");
require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL1: peer mismatch");
require(outstandingMinted[canonicalToken][message.sourceChainSelector] >= amount, "CWMultiTokenBridgeL1: outstanding underflow");
require(totalOutstanding[canonicalToken] >= amount, "CWMultiTokenBridgeL1: total outstanding underflow");
require(lockedBalance[canonicalToken] >= amount, "CWMultiTokenBridgeL1: locked underflow");
processed[message.messageId] = true;
outstandingMinted[canonicalToken][message.sourceChainSelector] -= amount;
totalOutstanding[canonicalToken] -= amount;
lockedBalance[canonicalToken] -= amount;
IERC20(canonicalToken).safeTransfer(recipient, amount);
emit Released(canonicalToken, recipient, amount);
emit OutstandingUpdated(
canonicalToken,
message.sourceChainSelector,
outstandingMinted[canonicalToken][message.sourceChainSelector],
totalOutstanding[canonicalToken],
lockedBalance[canonicalToken]
);
}
function withdrawToken(address token, address to, uint256 amount) external onlyAdmin {
if (supportedCanonicalToken[token] || lockedBalance[token] > 0) {
uint256 currentBalance = IERC20(token).balanceOf(address(this));
require(currentBalance >= lockedBalance[token], "CWMultiTokenBridgeL1: escrow invariant broken");
require(amount <= currentBalance - lockedBalance[token], "CWMultiTokenBridgeL1: amount locked");
}
IERC20(token).safeTransfer(to, amount);
}
function withdrawNative(address payable to, uint256 amount) external onlyAdmin {
(bool ok, ) = to.call{value: amount}("");
require(ok, "CWMultiTokenBridgeL1: native transfer failed");
}
receive() external payable {}
function _buildMessage(
address receiverBridge,
address canonicalToken,
address recipient,
uint256 amount
) internal view returns (IRouterClient.EVM2AnyMessage memory message) {
message = IRouterClient.EVM2AnyMessage({
receiver: abi.encode(receiverBridge),
data: abi.encode(canonicalToken, recipient, amount),
tokenAmounts: new IRouterClient.TokenAmount[](0),
feeToken: feeToken,
extraArgs: ""
});
}
function _collectAndApproveFee(uint256 fee) internal {
if (fee == 0) {
return;
}
if (feeToken == address(0)) {
require(msg.value >= fee, "CWMultiTokenBridgeL1: insufficient native fee");
return;
}
IERC20 feeErc20 = IERC20(feeToken);
feeErc20.safeTransferFrom(msg.sender, address(this), fee);
feeErc20.forceApprove(address(sendRouter), fee);
}
function _refundNativeExcess(uint256 feeUsed) internal {
if (feeToken != address(0) || msg.value <= feeUsed) {
return;
}
uint256 refund = msg.value - feeUsed;
(bool ok, ) = payable(msg.sender).call{value: refund}("");
require(ok, "CWMultiTokenBridgeL1: refund failed");
}
function _decodeSender(bytes memory senderData) internal pure returns (address senderAddress) {
if (senderData.length == 0) {
return address(0);
}
senderAddress = abi.decode(senderData, (address));
}
function _availableToMint(
address canonicalToken,
uint64 destinationChainSelector,
uint256 lockedBalanceAmount,
uint256 totalOutstandingAmount,
uint256 chainOutstanding
) internal view returns (uint256) {
if (!supportedCanonicalToken[canonicalToken]) {
return 0;
}
uint256 globalAvailable = lockedBalanceAmount > totalOutstandingAmount
? lockedBalanceAmount - totalOutstandingAmount
: 0;
uint256 chainCap = maxOutstanding[canonicalToken][destinationChainSelector];
if (chainCap == 0) {
return globalAvailable;
}
if (chainOutstanding >= chainCap) {
return 0;
}
uint256 chainAvailable = chainCap - chainOutstanding;
return globalAvailable < chainAvailable ? globalAvailable : chainAvailable;
}
}