- 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
359 lines
15 KiB
Solidity
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;
|
|
}
|
|
}
|