Files
smom-dbis-138/contracts/bridge/CWMultiTokenBridgeL2.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

305 lines
12 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";
interface ICWMintBurnToken is IERC20 {
function mint(address to, uint256 amount) external;
function burnFrom(address from, uint256 amount) external;
}
/**
* @title CWMultiTokenBridgeL2
* @notice Destination/public-chain bridge for minting and burning cW* assets without prefunding.
* @dev Supports multiple canonical -> mirrored token pairs behind one bridge address.
*/
contract CWMultiTokenBridgeL2 {
using SafeERC20 for IERC20;
IRouterClient public sendRouter;
address public receiveRouter;
address public feeToken;
address public admin;
struct DestinationConfig {
address receiverBridge;
bool enabled;
}
mapping(uint64 => DestinationConfig) public destinations;
mapping(address => address) public canonicalToMirrored;
mapping(address => address) public mirroredToCanonical;
mapping(bytes32 => bool) public processed;
mapping(address => bool) public tokenPairFrozen;
mapping(uint64 => bool) public destinationFrozen;
mapping(address => bool) public paused;
mapping(address => uint256) public mintedTotal;
mapping(address => uint256) public burnedTotal;
event TokenPairConfigured(address indexed canonicalToken, address indexed mirroredToken);
event DestinationConfigured(uint64 indexed chainSelector, address receiverBridge, bool enabled);
event Minted(address indexed canonicalToken, address indexed mirroredToken, address indexed recipient, uint256 amount);
event Burned(address indexed canonicalToken, address indexed mirroredToken, address indexed user, uint256 amount);
event MessageSent(
bytes32 indexed messageId,
address indexed canonicalToken,
address indexed mirroredToken,
uint64 destinationChainSelector,
address recipient,
uint256 amount
);
event TokenPairFrozen(address indexed canonicalToken, address indexed mirroredToken);
event DestinationFrozen(uint64 indexed chainSelector, address receiverBridge);
event MirroredTokenPauseUpdated(address indexed mirroredToken, bool paused);
event SupplyAccountingUpdated(
address indexed canonicalToken,
address indexed mirroredToken,
uint256 mintedTotalAmount,
uint256 burnedTotalAmount,
uint256 liveSupply
);
event SendRouterUpdated(address indexed newRouter);
event ReceiveRouterUpdated(address indexed newRouter);
event FeeTokenUpdated(address indexed newFeeToken);
event AdminChanged(address indexed newAdmin);
modifier onlyAdmin() {
require(msg.sender == admin, "CWMultiTokenBridgeL2: only admin");
_;
}
modifier onlyReceiveRouter() {
require(msg.sender == receiveRouter, "CWMultiTokenBridgeL2: only receive router");
_;
}
constructor(address _sendRouter, address _receiveRouter, address _feeToken) {
require(_sendRouter != address(0), "CWMultiTokenBridgeL2: zero send router");
require(_receiveRouter != address(0), "CWMultiTokenBridgeL2: zero receive router");
sendRouter = IRouterClient(_sendRouter);
receiveRouter = _receiveRouter;
feeToken = _feeToken;
admin = msg.sender;
}
function configureTokenPair(address canonicalToken, address mirroredToken) external onlyAdmin {
require(canonicalToken != address(0), "CWMultiTokenBridgeL2: zero canonical");
require(mirroredToken != address(0), "CWMultiTokenBridgeL2: zero mirrored");
require(!tokenPairFrozen[canonicalToken], "CWMultiTokenBridgeL2: token pair frozen");
address previousCanonical = mirroredToCanonical[mirroredToken];
if (previousCanonical != address(0) && previousCanonical != canonicalToken) {
require(!tokenPairFrozen[previousCanonical], "CWMultiTokenBridgeL2: token pair frozen");
delete canonicalToMirrored[previousCanonical];
}
canonicalToMirrored[canonicalToken] = mirroredToken;
mirroredToCanonical[mirroredToken] = canonicalToken;
emit TokenPairConfigured(canonicalToken, mirroredToken);
}
function configureDestination(uint64 chainSelector, address receiverBridge, bool enabled) external onlyAdmin {
require(receiverBridge != address(0), "CWMultiTokenBridgeL2: zero bridge");
require(!destinationFrozen[chainSelector], "CWMultiTokenBridgeL2: destination frozen");
destinations[chainSelector] = DestinationConfig({
receiverBridge: receiverBridge,
enabled: enabled
});
emit DestinationConfigured(chainSelector, receiverBridge, enabled);
}
function freezeTokenPair(address canonicalToken) external onlyAdmin {
address mirroredToken = canonicalToMirrored[canonicalToken];
require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured");
tokenPairFrozen[canonicalToken] = true;
emit TokenPairFrozen(canonicalToken, mirroredToken);
}
function freezeDestination(uint64 chainSelector) external onlyAdmin {
address receiverBridge = destinations[chainSelector].receiverBridge;
require(receiverBridge != address(0), "CWMultiTokenBridgeL2: destination not configured");
destinationFrozen[chainSelector] = true;
emit DestinationFrozen(chainSelector, receiverBridge);
}
function setTokenPaused(address mirroredToken, bool isPaused) external onlyAdmin {
require(mirroredToCanonical[mirroredToken] != address(0), "CWMultiTokenBridgeL2: token not configured");
paused[mirroredToken] = isPaused;
emit MirroredTokenPauseUpdated(mirroredToken, isPaused);
}
function setSendRouter(address newRouter) external onlyAdmin {
require(newRouter != address(0), "CWMultiTokenBridgeL2: zero router");
sendRouter = IRouterClient(newRouter);
emit SendRouterUpdated(newRouter);
}
function setReceiveRouter(address newRouter) external onlyAdmin {
require(newRouter != address(0), "CWMultiTokenBridgeL2: 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), "CWMultiTokenBridgeL2: zero admin");
admin = newAdmin;
emit AdminChanged(newAdmin);
}
function calculateFee(
address mirroredToken,
uint64 destinationChainSelector,
address recipient,
uint256 amount
) external view returns (uint256) {
address canonicalToken = mirroredToCanonical[mirroredToken];
require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured");
DestinationConfig memory dest = destinations[destinationChainSelector];
require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled");
return sendRouter.getFee(destinationChainSelector, _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount));
}
function circulatingSupply(address mirroredToken) external view returns (uint256) {
uint256 totalMinted = mintedTotal[mirroredToken];
uint256 totalBurned = burnedTotal[mirroredToken];
return totalMinted > totalBurned ? totalMinted - totalBurned : 0;
}
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyReceiveRouter {
require(!processed[message.messageId], "CWMultiTokenBridgeL2: replayed");
(address canonicalToken, address recipient, uint256 amount) =
abi.decode(message.data, (address, address, uint256));
require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient");
require(amount > 0, "CWMultiTokenBridgeL2: zero amount");
address mirroredToken = canonicalToMirrored[canonicalToken];
require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured");
require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused");
DestinationConfig memory peer = destinations[message.sourceChainSelector];
require(peer.enabled, "CWMultiTokenBridgeL2: peer disabled");
require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL2: peer mismatch");
processed[message.messageId] = true;
ICWMintBurnToken(mirroredToken).mint(recipient, amount);
mintedTotal[mirroredToken] += amount;
emit Minted(canonicalToken, mirroredToken, recipient, amount);
emit SupplyAccountingUpdated(
canonicalToken,
mirroredToken,
mintedTotal[mirroredToken],
burnedTotal[mirroredToken],
IERC20(mirroredToken).totalSupply()
);
}
function burnAndSend(
address mirroredToken,
uint64 destinationChainSelector,
address recipient,
uint256 amount
) external payable returns (bytes32 messageId) {
require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient");
require(amount > 0, "CWMultiTokenBridgeL2: zero amount");
address canonicalToken = mirroredToCanonical[mirroredToken];
require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured");
require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused");
DestinationConfig memory dest = destinations[destinationChainSelector];
require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled");
ICWMintBurnToken(mirroredToken).burnFrom(msg.sender, amount);
burnedTotal[mirroredToken] += amount;
emit Burned(canonicalToken, mirroredToken, msg.sender, amount);
emit SupplyAccountingUpdated(
canonicalToken,
mirroredToken,
mintedTotal[mirroredToken],
burnedTotal[mirroredToken],
IERC20(mirroredToken).totalSupply()
);
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, mirroredToken, destinationChainSelector, recipient, amount);
_refundNativeExcess(fee);
return messageId;
}
function withdrawToken(address token, address to, uint256 amount) external onlyAdmin {
IERC20(token).safeTransfer(to, amount);
}
function withdrawNative(address payable to, uint256 amount) external onlyAdmin {
(bool ok, ) = to.call{value: amount}("");
require(ok, "CWMultiTokenBridgeL2: 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, "CWMultiTokenBridgeL2: 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, "CWMultiTokenBridgeL2: refund failed");
}
function _decodeSender(bytes memory senderData) internal pure returns (address senderAddress) {
if (senderData.length == 0) {
return address(0);
}
senderAddress = abi.decode(senderData, (address));
}
}