- 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
305 lines
12 KiB
Solidity
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));
|
|
}
|
|
}
|