218 lines
7.3 KiB
Solidity
218 lines
7.3 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
import "./interfaces/IZedxionTransport.sol";
|
|
|
|
interface ICWMintBurnToken is IERC20 {
|
|
function mint(address to, uint256 amount) external;
|
|
function burnFrom(address from, uint256 amount) external;
|
|
}
|
|
|
|
/**
|
|
* @title ZedxionCustomBridge
|
|
* @notice GRU custom transport for Chain 138 ↔ ZEDXION (83872). No CCIP.
|
|
* @dev Deploy at same address on 138 and 83872 via CREATE2 (salt keccak256("ZedxionCustomBridge")).
|
|
* On 138: lock canonical c* and emit LockForZedxion.
|
|
* On 83872: relayer calls releaseOnZedxion to mint cW* or transfer escrowed assets.
|
|
* Reverse: lock on 83872 (LockFor138), relayer completes releaseOn138 on 138.
|
|
*/
|
|
contract ZedxionCustomBridge is IZedxionTransport, AccessControl, ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
|
|
uint256 public constant ZEDXION_CHAIN_ID = 83872;
|
|
uint256 public constant HUB_CHAIN_ID = 138;
|
|
|
|
mapping(bytes32 => LockRecord) public locks;
|
|
mapping(bytes32 => bool) public releasedOnDestination;
|
|
mapping(address => address) public canonicalToMirrored;
|
|
mapping(address => address) public mirroredToCanonical;
|
|
mapping(address => bool) public mintableMirroredToken;
|
|
mapping(address => uint256) public nonces;
|
|
bool private _hasRelayer;
|
|
|
|
struct LockRecord {
|
|
address sender;
|
|
address token;
|
|
uint256 amount;
|
|
address recipient;
|
|
uint256 createdAt;
|
|
bool released;
|
|
}
|
|
|
|
event LockForZedxion(
|
|
bytes32 indexed requestId,
|
|
address indexed sender,
|
|
address indexed token,
|
|
uint256 amount,
|
|
address recipient,
|
|
uint256 sourceChainId
|
|
);
|
|
|
|
event UnlockOnZedxion(
|
|
bytes32 indexed requestId,
|
|
address indexed recipient,
|
|
address indexed token,
|
|
uint256 amount
|
|
);
|
|
|
|
event LockFor138(
|
|
bytes32 indexed requestId,
|
|
address indexed sender,
|
|
address indexed token,
|
|
uint256 amount,
|
|
address recipient,
|
|
uint256 sourceChainId
|
|
);
|
|
|
|
event UnlockOn138(
|
|
bytes32 indexed requestId,
|
|
address indexed recipient,
|
|
address indexed token,
|
|
uint256 amount
|
|
);
|
|
|
|
event TokenPairConfigured(address indexed canonicalToken, address indexed mirroredToken);
|
|
|
|
constructor(address admin) {
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(RELAYER_ROLE, admin);
|
|
_hasRelayer = true;
|
|
}
|
|
|
|
function zedxionChainId() external pure override returns (uint256) {
|
|
return ZEDXION_CHAIN_ID;
|
|
}
|
|
|
|
function configureTokenPair(address canonicalToken, address mirroredToken) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
require(canonicalToken != address(0) && mirroredToken != address(0), "zero token");
|
|
canonicalToMirrored[canonicalToken] = mirroredToken;
|
|
mirroredToCanonical[mirroredToken] = canonicalToken;
|
|
emit TokenPairConfigured(canonicalToken, mirroredToken);
|
|
}
|
|
|
|
function setMintableMirroredToken(address mirroredToken, bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
mintableMirroredToken[mirroredToken] = enabled;
|
|
}
|
|
|
|
function bridgeToZedxion(
|
|
address token,
|
|
uint256 amount,
|
|
address recipientOn83872
|
|
) external payable override nonReentrant returns (bytes32 requestId) {
|
|
require(block.chainid == HUB_CHAIN_ID, "ZedxionCustomBridge: hub only");
|
|
return _lock(token, amount, recipientOn83872, ZEDXION_CHAIN_ID);
|
|
}
|
|
|
|
function bridgeFromZedxion(
|
|
address tokenOn83872,
|
|
uint256 amount,
|
|
address recipientOn138
|
|
) external payable override nonReentrant returns (bytes32 requestId) {
|
|
require(block.chainid == ZEDXION_CHAIN_ID, "ZedxionCustomBridge: zedxion only");
|
|
return _lock(tokenOn83872, amount, recipientOn138, HUB_CHAIN_ID);
|
|
}
|
|
|
|
function _lock(
|
|
address token,
|
|
uint256 amount,
|
|
address recipient,
|
|
uint256 destinationChainId
|
|
) internal returns (bytes32 requestId) {
|
|
require(recipient != address(0), "zero recipient");
|
|
require(amount > 0, "zero amount");
|
|
|
|
requestId = keccak256(abi.encodePacked(
|
|
msg.sender,
|
|
token,
|
|
amount,
|
|
recipient,
|
|
nonces[msg.sender]++,
|
|
block.chainid,
|
|
block.timestamp
|
|
));
|
|
|
|
if (token == address(0)) {
|
|
require(msg.value >= amount, "insufficient value");
|
|
} else {
|
|
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
|
|
}
|
|
|
|
locks[requestId] = LockRecord({
|
|
sender: msg.sender,
|
|
token: token,
|
|
amount: amount,
|
|
recipient: recipient,
|
|
createdAt: block.timestamp,
|
|
released: false
|
|
});
|
|
|
|
if (destinationChainId == ZEDXION_CHAIN_ID) {
|
|
emit LockForZedxion(requestId, msg.sender, token, amount, recipient, block.chainid);
|
|
emit ZedxionBridgeInitiated(requestId, msg.sender, token, amount, recipient, ZEDXION_CHAIN_ID);
|
|
} else {
|
|
emit LockFor138(requestId, msg.sender, token, amount, recipient, block.chainid);
|
|
emit ZedxionBridgeInitiated(requestId, msg.sender, token, amount, recipient, HUB_CHAIN_ID);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice On 83872: mint cW* or release escrow after 138 lock. On 138: release canonical c* after 83872 burn/lock.
|
|
*/
|
|
function releaseOnZedxion(
|
|
bytes32 requestId,
|
|
address token,
|
|
uint256 amount,
|
|
address recipient
|
|
) external onlyRole(RELAYER_ROLE) nonReentrant {
|
|
require(block.chainid == ZEDXION_CHAIN_ID, "ZedxionCustomBridge: zedxion only");
|
|
_release(requestId, token, amount, recipient, true);
|
|
}
|
|
|
|
function releaseOn138(
|
|
bytes32 requestId,
|
|
address token,
|
|
uint256 amount,
|
|
address recipient
|
|
) external onlyRole(RELAYER_ROLE) nonReentrant {
|
|
require(block.chainid == HUB_CHAIN_ID, "ZedxionCustomBridge: hub only");
|
|
_release(requestId, token, amount, recipient, false);
|
|
}
|
|
|
|
function _release(
|
|
bytes32 requestId,
|
|
address token,
|
|
uint256 amount,
|
|
address recipient,
|
|
bool onZedxion
|
|
) internal {
|
|
require(!releasedOnDestination[requestId], "already released");
|
|
require(recipient != address(0), "zero recipient");
|
|
require(amount > 0, "zero amount");
|
|
releasedOnDestination[requestId] = true;
|
|
|
|
if (token == address(0)) {
|
|
(bool sent,) = payable(recipient).call{value: amount}("");
|
|
require(sent, "transfer failed");
|
|
} else if (mintableMirroredToken[token]) {
|
|
ICWMintBurnToken(token).mint(recipient, amount);
|
|
} else {
|
|
IERC20(token).safeTransfer(recipient, amount);
|
|
}
|
|
|
|
emit ZedxionBridgeCompleted(requestId, bytes32(0));
|
|
|
|
if (onZedxion) {
|
|
emit UnlockOnZedxion(requestId, recipient, token, amount);
|
|
} else {
|
|
emit UnlockOn138(requestId, recipient, token, amount);
|
|
}
|
|
}
|
|
|
|
receive() external payable {}
|
|
}
|