Files
smom-dbis-138/contracts/bridge/AlltraCustomBridge.sol
2026-03-02 12:14:09 -08:00

129 lines
4.0 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/IAlltraTransport.sol";
/**
* @title AlltraCustomBridge
* @notice Custom transport for 138 <-> ALL Mainnet (651940). Locks tokens and emits event; no CCIP.
* @dev Deploy at same address on 138 and 651940 via CREATE2. On 138: lock + emit LockForAlltra.
* Off-chain relayer or contract on 651940 completes mint/unlock. On 651940: implement
* unlockOrMint (called by relayer or same contract) to complete the flow.
*/
contract AlltraCustomBridge is IAlltraTransport, AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
uint256 public constant ALL_MAINNET_CHAIN_ID = 651940;
mapping(bytes32 => LockRecord) public locks;
mapping(bytes32 => bool) public releasedOnAlltra; // on 651940: prevent double release
mapping(address => uint256) public nonces;
bool private _hasRelayer;
struct LockRecord {
address sender;
address token;
uint256 amount;
address recipient;
uint256 createdAt;
bool released;
}
event LockForAlltra(
bytes32 indexed requestId,
address indexed sender,
address indexed token,
uint256 amount,
address recipient,
uint256 sourceChainId
);
event UnlockOnAlltra(
bytes32 indexed requestId,
address indexed recipient,
address indexed token,
uint256 amount
);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(RELAYER_ROLE, admin);
_hasRelayer = true;
}
/**
* @notice Lock tokens and emit event for relay to ALL Mainnet. Does not use CCIP.
*/
function lockAndRelay(
address token,
uint256 amount,
address recipient
) external payable override nonReentrant 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
});
emit LockForAlltra(requestId, msg.sender, token, amount, recipient, block.chainid);
return requestId;
}
function isConfigured() external view override returns (bool) {
return _hasRelayer;
}
/**
* @notice On ALL Mainnet (651940): release tokens to recipient after relay proof.
* @dev Only RELAYER_ROLE; in production, verify merkle proof or signature from source chain.
* Uses releasedOnAlltra[requestId] to prevent double release on this chain.
*/
function releaseOnAlltra(
bytes32 requestId,
address token,
uint256 amount,
address recipient
) external onlyRole(RELAYER_ROLE) nonReentrant {
require(!releasedOnAlltra[requestId], "already released");
releasedOnAlltra[requestId] = true;
if (token == address(0)) {
(bool sent,) = payable(recipient).call{value: amount}("");
require(sent, "transfer failed");
} else {
IERC20(token).safeTransfer(recipient, amount);
}
emit UnlockOnAlltra(requestId, recipient, token, amount);
}
receive() external payable {}
}