|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.28; |
| 3 | + |
| 4 | +import {BridgedERC1155} from "./BridgedERC1155.sol"; |
| 5 | +import {IERC1155Bridge} from "./IERC1155Bridge.sol"; |
| 6 | +import {ISignalService} from "./ISignalService.sol"; |
| 7 | + |
| 8 | +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; |
| 9 | + |
| 10 | +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; |
| 11 | +import {IERC1155MetadataURI} from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; |
| 12 | +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; |
| 13 | +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; |
| 14 | +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; |
| 15 | + |
| 16 | +/// @title ERC1155Bridge |
| 17 | +/// @notice A decentralized bridge for ERC1155 tokens that allows anyone to bridge any ERC1155 token |
| 18 | +/// @dev Uses a permissionless flow to initialize the counterpart token on the destination chain. |
| 19 | +contract ERC1155Bridge is IERC1155Bridge, ReentrancyGuardTransient, IERC1155Receiver, ERC165 { |
| 20 | + /// @dev Signal type constants to differentiate signal categories |
| 21 | + bytes32 private constant TOKEN_DESCRIPTION_SIGNAL_PREFIX = keccak256("ERC1155_TOKEN_DESCRIPTION"); |
| 22 | + bytes32 private constant DEPOSIT_SIGNAL_PREFIX = keccak256("ERC1155_DEPOSIT"); |
| 23 | + |
| 24 | + mapping(bytes32 id => bool processed) private _processed; |
| 25 | + mapping(address originalToken => address counterpartToken) private _counterpartTokens; |
| 26 | + mapping(address token => bool isBridgedToken) private _isBridgedTokens; |
| 27 | + |
| 28 | + /// Incremental nonce to generate unique deposit IDs. |
| 29 | + uint256 private _globalDepositNonce; |
| 30 | + |
| 31 | + ISignalService public immutable signalService; |
| 32 | + |
| 33 | + /// @dev Trusted source of commitments in the `CommitmentStore` that the bridge will use to validate withdrawals |
| 34 | + /// @dev This is the Anchor on L2 and the Checkpoint Tracker on the L1 |
| 35 | + address public immutable trustedCommitmentPublisher; |
| 36 | + |
| 37 | + /// @dev The counterpart bridge contract on the other chain. |
| 38 | + /// This is used to locate deposit signals inside the other chain's state root. |
| 39 | + /// WARN: This address has no significance (and may be untrustworthy) on this chain. |
| 40 | + address public immutable counterpart; |
| 41 | + |
| 42 | + constructor(address _signalService, address _trustedCommitmentPublisher, address _counterpart) { |
| 43 | + require(_signalService != address(0), "Empty signal service"); |
| 44 | + require(_trustedCommitmentPublisher != address(0), "Empty trusted publisher"); |
| 45 | + require(_counterpart != address(0), "Empty counterpart"); |
| 46 | + |
| 47 | + signalService = ISignalService(_signalService); |
| 48 | + trustedCommitmentPublisher = _trustedCommitmentPublisher; |
| 49 | + counterpart = _counterpart; |
| 50 | + } |
| 51 | + |
| 52 | + /// @inheritdoc IERC1155Receiver |
| 53 | + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) { |
| 54 | + return IERC1155Receiver.onERC1155Received.selector; |
| 55 | + } |
| 56 | + |
| 57 | + /// @inheritdoc IERC1155Receiver |
| 58 | + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) |
| 59 | + external |
| 60 | + pure |
| 61 | + returns (bytes4) |
| 62 | + { |
| 63 | + return IERC1155Receiver.onERC1155BatchReceived.selector; |
| 64 | + } |
| 65 | + |
| 66 | + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { |
| 67 | + return interfaceId == type(IERC1155Receiver).interfaceId || interfaceId == type(IERC1155Bridge).interfaceId |
| 68 | + || super.supportsInterface(interfaceId); |
| 69 | + } |
| 70 | + |
| 71 | + /// @inheritdoc IERC1155Bridge |
| 72 | + function processed(bytes32 id) public view returns (bool) { |
| 73 | + return _processed[id]; |
| 74 | + } |
| 75 | + |
| 76 | + /// @inheritdoc IERC1155Bridge |
| 77 | + function getCounterpartToken(address originalToken) public view returns (address) { |
| 78 | + return _counterpartTokens[originalToken]; |
| 79 | + } |
| 80 | + |
| 81 | + /// @inheritdoc IERC1155Bridge |
| 82 | + function getTokenDescriptionId(TokenDescription memory tokenDesc) public pure returns (bytes32 id) { |
| 83 | + return _generateTokenDescriptionId(tokenDesc); |
| 84 | + } |
| 85 | + |
| 86 | + /// @inheritdoc IERC1155Bridge |
| 87 | + function getDepositId(ERC1155Deposit memory erc1155Deposit) public pure returns (bytes32 id) { |
| 88 | + return _generateDepositId(erc1155Deposit); |
| 89 | + } |
| 90 | + |
| 91 | + /// @inheritdoc IERC1155Bridge |
| 92 | + function recordTokenDescription(address token) external returns (bytes32 id) { |
| 93 | + require(token != address(0), "Invalid token address"); |
| 94 | + |
| 95 | + string memory uri; |
| 96 | + try IERC1155MetadataURI(token).uri(0) returns (string memory tokenUri) { |
| 97 | + uri = tokenUri; |
| 98 | + } catch {} |
| 99 | + |
| 100 | + TokenDescription memory tokenDesc = TokenDescription({originalToken: token, uri: uri}); |
| 101 | + |
| 102 | + id = _generateTokenDescriptionId(tokenDesc); |
| 103 | + |
| 104 | + signalService.sendSignal(id); |
| 105 | + |
| 106 | + emit TokenDescriptionRecorded(id, tokenDesc); |
| 107 | + } |
| 108 | + |
| 109 | + /// @inheritdoc IERC1155Bridge |
| 110 | + function deployCounterpartToken(TokenDescription memory tokenDesc, uint256 height, bytes memory proof) |
| 111 | + external |
| 112 | + returns (address deployedToken) |
| 113 | + { |
| 114 | + bytes32 id = _generateTokenDescriptionId(tokenDesc); |
| 115 | + require(!_processed[id], CounterpartTokenAlreadyDeployed()); |
| 116 | + require( |
| 117 | + _counterpartTokens[tokenDesc.originalToken] == address(0), |
| 118 | + "Counterpart token already exists for this original token" |
| 119 | + ); |
| 120 | + |
| 121 | + signalService.verifySignal(height, trustedCommitmentPublisher, counterpart, id, proof); |
| 122 | + |
| 123 | + deployedToken = address(new BridgedERC1155(tokenDesc.uri, tokenDesc.originalToken)); |
| 124 | + _counterpartTokens[tokenDesc.originalToken] = deployedToken; |
| 125 | + _isBridgedTokens[deployedToken] = true; |
| 126 | + _processed[id] = true; |
| 127 | + |
| 128 | + emit CounterpartTokenDeployed(id, tokenDesc, deployedToken); |
| 129 | + } |
| 130 | + |
| 131 | + /// @inheritdoc IERC1155Bridge |
| 132 | + function deposit(address to, address localToken, uint256 tokenId, uint256 amount, address canceler) |
| 133 | + external |
| 134 | + nonReentrant |
| 135 | + returns (bytes32 id) |
| 136 | + { |
| 137 | + bool isBridged = _isBridgedToken(localToken); |
| 138 | + address originalToken = isBridged ? BridgedERC1155(localToken).originalToken() : localToken; |
| 139 | + |
| 140 | + string memory tokenURI_; |
| 141 | + try IERC1155MetadataURI(localToken).uri(tokenId) returns (string memory uri) { |
| 142 | + tokenURI_ = uri; |
| 143 | + } catch {} |
| 144 | + |
| 145 | + ERC1155Deposit memory erc1155Deposit = ERC1155Deposit({ |
| 146 | + nonce: _globalDepositNonce, |
| 147 | + from: msg.sender, |
| 148 | + to: to, |
| 149 | + originalToken: originalToken, |
| 150 | + tokenId: tokenId, |
| 151 | + amount: amount, |
| 152 | + tokenURI: tokenURI_, |
| 153 | + canceler: canceler |
| 154 | + }); |
| 155 | + |
| 156 | + id = _generateDepositId(erc1155Deposit); |
| 157 | + unchecked { |
| 158 | + ++_globalDepositNonce; |
| 159 | + } |
| 160 | + |
| 161 | + IERC1155(localToken).safeTransferFrom(msg.sender, address(this), tokenId, amount, ""); |
| 162 | + if (isBridged) { |
| 163 | + BridgedERC1155(localToken).burn(tokenId, amount); |
| 164 | + } |
| 165 | + |
| 166 | + signalService.sendSignal(id); |
| 167 | + emit DepositMade(id, erc1155Deposit, localToken); |
| 168 | + } |
| 169 | + |
| 170 | + /// @inheritdoc IERC1155Bridge |
| 171 | + function claimDeposit(ERC1155Deposit memory erc1155Deposit, uint256 height, bytes memory proof) |
| 172 | + external |
| 173 | + nonReentrant |
| 174 | + { |
| 175 | + bytes32 id = _claimDeposit(erc1155Deposit, erc1155Deposit.to, height, proof); |
| 176 | + emit DepositClaimed(id, erc1155Deposit); |
| 177 | + } |
| 178 | + |
| 179 | + /// @inheritdoc IERC1155Bridge |
| 180 | + function cancelDeposit(ERC1155Deposit memory erc1155Deposit, address claimee, uint256 height, bytes memory proof) |
| 181 | + external |
| 182 | + nonReentrant |
| 183 | + { |
| 184 | + require(msg.sender == erc1155Deposit.canceler, OnlyCanceler()); |
| 185 | + |
| 186 | + bytes32 id = _claimDeposit(erc1155Deposit, claimee, height, proof); |
| 187 | + |
| 188 | + emit DepositCancelled(id, claimee); |
| 189 | + } |
| 190 | + |
| 191 | + function _claimDeposit(ERC1155Deposit memory erc1155Deposit, address to, uint256 height, bytes memory proof) |
| 192 | + internal |
| 193 | + returns (bytes32 id) |
| 194 | + { |
| 195 | + id = _generateDepositId(erc1155Deposit); |
| 196 | + require(!processed(id), AlreadyClaimed()); |
| 197 | + |
| 198 | + signalService.verifySignal(height, trustedCommitmentPublisher, counterpart, id, proof); |
| 199 | + |
| 200 | + _processed[id] = true; |
| 201 | + _sendERC1155(erc1155Deposit, to); |
| 202 | + } |
| 203 | + |
| 204 | + /// @dev Function to transfer ERC1155 to the receiver. |
| 205 | + /// @param erc1155Deposit The deposit information containing the original token address and tokenId |
| 206 | + /// @param to Address to send the tokens to |
| 207 | + function _sendERC1155(ERC1155Deposit memory erc1155Deposit, address to) internal { |
| 208 | + address deployedToken = _counterpartTokens[erc1155Deposit.originalToken]; |
| 209 | + |
| 210 | + if (deployedToken != address(0)) { |
| 211 | + if (bytes(erc1155Deposit.tokenURI).length > 0) { |
| 212 | + BridgedERC1155(deployedToken).mintWithURI( |
| 213 | + to, erc1155Deposit.tokenId, erc1155Deposit.amount, erc1155Deposit.tokenURI, "" |
| 214 | + ); |
| 215 | + } else { |
| 216 | + BridgedERC1155(deployedToken).mint(to, erc1155Deposit.tokenId, erc1155Deposit.amount, ""); |
| 217 | + } |
| 218 | + } else { |
| 219 | + IERC1155(erc1155Deposit.originalToken).safeTransferFrom( |
| 220 | + address(this), to, erc1155Deposit.tokenId, erc1155Deposit.amount, "" |
| 221 | + ); |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + /// @dev Checks if a token is a bridged token deployed by this bridge. |
| 226 | + /// @param token The token address to check |
| 227 | + /// @return true if the token is a bridged token deployed by this bridge |
| 228 | + function _isBridgedToken(address token) internal view returns (bool) { |
| 229 | + return _isBridgedTokens[token]; |
| 230 | + } |
| 231 | + |
| 232 | + /// @dev Generates a unique ID for a token description. |
| 233 | + /// @param tokenDesc Token description to generate an ID for |
| 234 | + function _generateTokenDescriptionId(TokenDescription memory tokenDesc) internal pure returns (bytes32) { |
| 235 | + return keccak256(abi.encode(TOKEN_DESCRIPTION_SIGNAL_PREFIX, tokenDesc)); |
| 236 | + } |
| 237 | + |
| 238 | + /// @dev Generates a unique ID for a deposit. |
| 239 | + /// @param erc1155Deposit Deposit to generate an ID for |
| 240 | + function _generateDepositId(ERC1155Deposit memory erc1155Deposit) internal pure returns (bytes32) { |
| 241 | + return keccak256(abi.encode(DEPOSIT_SIGNAL_PREFIX, erc1155Deposit)); |
| 242 | + } |
| 243 | +} |
0 commit comments