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
This commit is contained in:
304
contracts/bridge/CWMultiTokenBridgeL2.sol
Normal file
304
contracts/bridge/CWMultiTokenBridgeL2.sol
Normal file
@@ -0,0 +1,304 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user