Skip to content

Commit ed5db97

Browse files
pepebndcLeoPatOZnikeshnazareth
authored
ERC20, ERC721 and ERC1155 Bridge contracts and tests (#146)
Co-authored-by: Leo <[email protected]> Co-authored-by: Nikesh Nazareth <[email protected]>
1 parent b72fff2 commit ed5db97

20 files changed

+2405
-8
lines changed

src/protocol/BridgedERC1155.sol

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {BridgedTokenBase} from "./BridgedTokenBase.sol";
5+
import {IMintableERC1155} from "./IMintable.sol";
6+
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
7+
8+
/// @title BridgedERC1155
9+
/// @notice An ERC1155 token that represents a bridged token from another chain
10+
/// @dev Only the bridge contract can mint and burn tokens
11+
/// @dev Implements the ERC1155Metadata_URI interface, whether or not the original token supports it
12+
contract BridgedERC1155 is ERC1155, BridgedTokenBase, IMintableERC1155 {
13+
/// @dev Mapping from token ID to custom token URI
14+
mapping(uint256 => string) private _tokenURIs;
15+
16+
constructor(string memory uri_, address _originalToken) ERC1155(uri_) BridgedTokenBase(_originalToken) {}
17+
18+
/// @inheritdoc IMintableERC1155
19+
function mint(address to, uint256 id, uint256 amount, bytes memory data) external onlyOwner {
20+
_mint(to, id, amount, data);
21+
}
22+
23+
/// @dev Mints tokens with custom URI
24+
/// @param to Address to mint the tokens to
25+
/// @param id Token ID to mint
26+
/// @param amount Amount to mint
27+
/// @param tokenURI_ Custom URI for this token
28+
/// @param data Additional data
29+
function mintWithURI(address to, uint256 id, uint256 amount, string memory tokenURI_, bytes memory data)
30+
external
31+
onlyOwner
32+
{
33+
_mint(to, id, amount, data);
34+
_tokenURIs[id] = tokenURI_;
35+
}
36+
37+
/// @inheritdoc IMintableERC1155
38+
function burn(uint256 id, uint256 amount) external onlyOwner {
39+
_burn(msg.sender, id, amount);
40+
}
41+
42+
/// @dev See {IERC1155MetadataURI-uri}.
43+
function uri(uint256 tokenId) public view virtual override returns (string memory) {
44+
string memory _tokenURI = _tokenURIs[tokenId];
45+
46+
if (bytes(_tokenURI).length > 0) {
47+
return _tokenURI;
48+
}
49+
50+
return super.uri(tokenId);
51+
}
52+
}

src/protocol/BridgedERC20.sol

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {BridgedTokenBase} from "./BridgedTokenBase.sol";
5+
import {IMintableERC20} from "./IMintable.sol";
6+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7+
8+
/// @title BridgedERC20
9+
/// @notice An ERC20 token that represents a bridged token from another chain
10+
/// @dev Only the bridge contract can mint and burn tokens
11+
/// @dev Implements the optional metadata functions, whether or not the original token supports them
12+
contract BridgedERC20 is ERC20, BridgedTokenBase, IMintableERC20 {
13+
uint8 private immutable _decimals;
14+
15+
constructor(string memory name, string memory symbol, uint8 decimals_, address _originalToken)
16+
ERC20(name, symbol)
17+
BridgedTokenBase(_originalToken)
18+
{
19+
_decimals = decimals_;
20+
}
21+
22+
/// @inheritdoc IMintableERC20
23+
function mint(address to, uint256 amount) external onlyOwner {
24+
_mint(to, amount);
25+
}
26+
27+
/// @inheritdoc IMintableERC20
28+
function burn(uint256 amount) external onlyOwner {
29+
_burn(msg.sender, amount);
30+
}
31+
32+
function decimals() public view virtual override returns (uint8) {
33+
return _decimals;
34+
}
35+
}

src/protocol/BridgedERC721.sol

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {BridgedTokenBase} from "./BridgedTokenBase.sol";
5+
import {IMintableERC721} from "./IMintable.sol";
6+
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
7+
8+
/// @title BridgedERC721
9+
/// @notice An ERC721 token that represents a bridged token from another chain
10+
/// @dev Only the bridge contract can mint and burn tokens
11+
/// @dev Implements the ERC721Metadata interface, whether or not the original token supports it
12+
contract BridgedERC721 is ERC721, BridgedTokenBase, IMintableERC721 {
13+
/// @dev Mapping from token ID to custom token URI
14+
mapping(uint256 => string) private _tokenURIs;
15+
16+
constructor(string memory name, string memory symbol, address _originalToken)
17+
ERC721(name, symbol)
18+
BridgedTokenBase(_originalToken)
19+
{}
20+
21+
/// @inheritdoc IMintableERC721
22+
function mint(address to, uint256 tokenId, string memory tokenURI_) external onlyOwner {
23+
_safeMint(to, tokenId);
24+
_tokenURIs[tokenId] = tokenURI_;
25+
}
26+
27+
/// @inheritdoc IMintableERC721
28+
function burn(uint256 tokenId) external onlyOwner {
29+
_burn(tokenId);
30+
delete _tokenURIs[tokenId];
31+
}
32+
33+
/// @dev See {IERC721Metadata-tokenURI}.
34+
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
35+
_requireOwned(tokenId);
36+
return _tokenURIs[tokenId];
37+
}
38+
}

src/protocol/BridgedTokenBase.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5+
6+
/// @title BridgedTokenBase
7+
/// @notice Common base contract for all bridged tokens
8+
/// @dev Provides ownership control and original token tracking. Uses OpenZeppelin's Ownable for access control.
9+
abstract contract BridgedTokenBase is Ownable {
10+
/// @notice Address of the original token on the source chain
11+
address public immutable originalToken;
12+
13+
/// @dev Constructor sets the deployer (bridge) as owner and stores original token address
14+
/// @param _originalToken Address of the original token on the source chain
15+
constructor(address _originalToken) Ownable(msg.sender) {
16+
originalToken = _originalToken;
17+
}
18+
}

src/protocol/ERC1155Bridge.sol

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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

Comments
 (0)