You are reading Palm development version documentation and some displayed features may not be available in the stable release. You can switch to stable version using the version box at screen bottom.

Updated on June 1, 2022

# Make your contracts bridge compatible

In our previous post we went through how the bridge operates, this article explains what you need to do to integrate your token contracts with the bridge.

Let’s go through these simple steps together:

### Integrating token contracts with the bridge

#### Original contracts vs Synthetic contracts

In the context of the Palm network’s bridge, an original contract sits where tokens are primarily minted. A synthetic contract is deployed on the chain where tokens will be transferred via the bridge.

Deploying both original and synthetic contracts ensures that tokens can be transferred back and forth between the original and destination chains.

Here are the changes you will need to make to your contracts for them to be bridge-compatible: The below specifications apply to ERC-721 contracts. Further specifications will be provided for ERC-1155 or ERC-20 contracts in future.

##### Original contract
1. Needs to include the Enumerable extension from Open Zeppelin libraries. This is to support enumerability of all the token ids in the contract as well as all token ids owned by each account, so that the bridge UI can help the user to select the correct token from their account.

Aside from Enumerable, any custom implementation of ERC-721 is allowed: bulk minting, token ID auto-increment, etc…

##### Synthetic contract
1. Also needs to add the Enumerable extension from Open Zeppelin libraries.

2. Needs to have a custom mint() function.

In order to mint a replica of the original token on the targeted chain, the synthetic contract must be able to mint tokens that have the same IDs and URIs as the original.

Custom mint() function example:
  1 2 3 4 5 6 7 8 9 10 11 /** * @dev Mints the specified token id to the recipient addresses * @dev The unused string parameter exists to support the API used by ChainBridge. * @dev Mint interface: function mint(address to, uint256 tokenId, string calldata _data) public where _data is the tokenUri * @param tokenId tokenId to be minted * @param recipient Address that will receive the tokens */ function mint(address recipient, uint256 tokenId, string calldata tokenUri) external onlyMinters { _mint(recipient, tokenId); _setTokenURI(tokenId, tokenUri); } 
3. Needs to grant the bridge minting permission.

The bridge’s handler will need access to the synthetic contract’s mint() function.

We recommend using role-based access controls to do this, and it also helps to avoid granting full admin functions to the bridge address.

You can set granular rights that only set controls on the mint()function.

Granular mint() rights example:
 1 2 3 4 5 6 7 /** * @dev Throws if called by any account other than minters. Implemented using the underlying AccessControl methods. */ modifier onlyMinters() { require(hasRole(MINTER_ROLE, _msgSender()), "Caller does not have the MINTER_ROLE"); _; } 
4. Needs to have a burn() function.

Same as for the mint() function described above, the bridge will need to be granted permission to the burn() function in order to burn synthetic tokens when transferring them back to the original chain.

5. Needs to give the bridge burning permission.

Same as point 5. but for the burn() function.

If you would like to put all those bits into context, here’s a contract example that applies for both original and synthetic contracts:

ERC-721 contract example:
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 // SPDX-License-Identifier: MIT pragma solidity 0.8.6; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol"; import "./ERC2981.sol"; contract NFT is AccessControl, ERC2981, ERC721Enumerable, ERC721Burnable, ERC721Pausable { event RoyaltyWalletChanged(address indexed previousWallet, address indexed newWallet); event RoyaltyFeeChanged(uint256 previousFee, uint256 newFee); event BaseURIChanged(string previousURI, string newURI); bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); uint256 public constant ROYALTY_FEE_DENOMINATOR = 100000; uint256 public royaltyFee; address public royaltyWallet; string private _baseTokenURI; /** * @param _name ERC721 token name * @param _symbol ERC721 token symbol * @param _uri Base token uri * @param _royaltyWallet Wallet where royalties should be sent * @param _royaltyFee Fee numerator to be used for fees */ constructor( string memory _name, string memory _symbol, string memory _uri, address _royaltyWallet, uint256 _royaltyFee ) ERC721(_name, _symbol) { _setBaseTokenURI(_uri); _setRoyaltyWallet(_royaltyWallet); _setRoyaltyFee(_royaltyFee); _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); _setupRole(OWNER_ROLE, msg.sender); _setupRole(MINTER_ROLE, msg.sender); } /** * @dev Throws if called by any account other than owners. Implemented using the underlying AccessControl methods. */ modifier onlyOwners() { require(hasRole(OWNER_ROLE, _msgSender()), "Caller does not have the OWNER_ROLE"); _; } /** * @dev Throws if called by any account other than minters. Implemented using the underlying AccessControl methods. */ modifier onlyMinters() { require(hasRole(MINTER_ROLE, _msgSender()), "Caller does not have the MINTER_ROLE"); _; } /** * @dev Mints the specified token ids to the recipient addresses * @param recipient Address that will receive the tokens * @param tokenIds Array of tokenIds to be minted */ function mint(address recipient, uint256[] calldata tokenIds) external onlyMinters { for (uint256 i = 0; i < tokenIds.length; i++) { _mint(recipient, tokenIds[i]); } } /** * @dev Mints the specified token id to the recipient addresses * @dev The unused string parameter exists to support the API used by ChainBridge. * @param recipient Address that will receive the tokens * @param tokenId tokenId to be minted */ function mint(address recipient, uint256 tokenId, string calldata) external onlyMinters { _mint(recipient, tokenId); } /** * @dev Pauses token transfers */ function pause() external onlyOwners { _pause(); } /** * @dev Unpauses token transfers */ function unpause() external onlyOwners { _unpause(); } /** * @dev Sets the base token URI * @param uri Base token URI */ function setBaseTokenURI(string calldata uri) external onlyOwners { _setBaseTokenURI(uri); } /** * @dev Sets the wallet to which royalties should be sent * @param _royaltyWallet Address that should receive the royalties */ function setRoyaltyWallet(address _royaltyWallet) external onlyOwners { _setRoyaltyWallet(_royaltyWallet); } /** * @dev Sets the fee percentage for royalties * @param _royaltyFee Basis points to compute royalty percentage */ function setRoyaltyFee(uint256 _royaltyFee) external onlyOwners { _setRoyaltyFee(_royaltyFee); } /** * @dev Function defined by ERC2981, which provides information about fees. * @param value Price being paid for the token (in base units) */ function royaltyInfo( uint256, // tokenId is not used in this case as all tokens take the same fee uint256 value ) external view override returns ( address, // receiver uint256 // royaltyAmount ) { return (royaltyWallet, (value * royaltyFee) / ROYALTY_FEE_DENOMINATOR); } /** * @dev For each existing tokenId, it returns the URI where metadata is stored * @param tokenId Token id */ function tokenURI(uint256 tokenId) public view override returns (string memory) { string memory uri = super.tokenURI(tokenId); return bytes(uri).length > 0 ? string(abi.encodePacked(uri, ".json")) : ""; } function supportsInterface(bytes4 interfaceId) public view override(AccessControl, ERC2981, ERC721, ERC721Enumerable) returns (bool) { return super.supportsInterface(interfaceId); } function _beforeTokenTransfer( address from, address to, uint256 tokenId ) internal override(ERC721, ERC721Enumerable, ERC721Pausable) { super._beforeTokenTransfer(from, to, tokenId); } function _setBaseTokenURI(string memory newURI) internal { emit BaseURIChanged(_baseTokenURI, newURI); _baseTokenURI = newURI; } function _setRoyaltyWallet(address _royaltyWallet) internal { require(_royaltyWallet != address(0), "INVALID_WALLET"); emit RoyaltyWalletChanged(royaltyWallet, _royaltyWallet); royaltyWallet = _royaltyWallet; } function _setRoyaltyFee(uint256 _royaltyFee) internal { require(_royaltyFee <= ROYALTY_FEE_DENOMINATOR, "INVALID_FEE"); emit RoyaltyFeeChanged(royaltyFee, _royaltyFee); royaltyFee = _royaltyFee; } function _baseURI() internal view override returns (string memory) { return _baseTokenURI; } } 

Code GitHub repository

You can use the Palm Testnet bridge to live-test your contracts’ integration with the bridge. All you need is to deploy them to Palm testnet and Rinkeby (Rinkeby is one of Ethereum’s testnets). For ERC-721 contracts, you will need to initialise it using the constructor, and grant the bridge’s ERC-721 handler contract MINTER_ROLE (see the address reference for relevant contract addresses). Your original and synthetic can be exactly the same as in production.