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 March 29, 2022

# Integrating with the Palm network bridge

The Palm Network enables NFT trading in a fast, cost-efficient, and eco-friendly manner. However, some users might decide to move their assets from the Palm Network to Ethereum in order to reach specific marketplaces. They can transfer their token(s) using the Palm network bridge.

As a developer you can support those users by ensuring your smart contracts are compatible with the Palm network bridge.

1. What is the Palm network Bridge.
2. How to integrate with the bridge.

## What is the Palm network Bridge?

The Palm network bridge connects the Palm network with Ethereum. It allows transferring assets such as ERC-20 and ERC-721 tokens back and forth between the Palm Network and Ethereum.

The bridge works by locking tokens that have already been minted on one side of the bridge and then minting an equivalent token on the other side, using what we call a “synthetic” version of the ERC-721 contract:

Users can send their tokens back to the original side: the bridge will burn the synthetic token and release the original token that will be transferred to the destination wallet address:

A fee is required for transferring assets from Palm to Ethereum (to cover gas costs of minting and a carbon offset). Moving assets back to Palm will top up the depositor’s account with a small amount of PALM tokens.

What does an end-user see when she uses the bridge?

The Palm network provides a user-friendly Dapp where users can initiate the transfer and give their approval to pay transfer fees (in DAI).

Now, let’s dive a bit deeper into how the bridge operates:

The Palm bridge runs on ChainBridge, a communication protocol where events on the source chain are used to send messages routed to the destination chain, where they will be submitted as transactions.

A few concepts specific to ChainBridge:

• Relayers — Off-chain servers that listen for particular events on the source chain and submit signed proposals to the destination chain.
• Bridge contracts — Delegate calls to the handler contracts for deposits, start a transaction on the source chain, and execute the proposals on the target chain.
• Handler contracts — Palm’s handler contracts send mint/burn transactions depending on the user input.
• Target contracts — On Palm, target contracts are ERC-20, ERC-721 and ERC-1155 on each side of the bridge.
• Deposit() function — Here, a deposit is simply the initiation of a transfer of a piece of data, often representing instructions to lock a token in the bridge. In the reverse direction, the deposit is an instruction to burn a token.
• Resource ID — Identifier for the transferring token’s smart contract. Resource ID is used to link the equivalent contracts on both sides of the bridge.
• Chain ID — Identifier of the chain, for example, Palm Network or Ethereum
• Calldata — Payload contained by an event/proposal. The calldata represents a function to be executed on the targeted chain. On Palm, calldata represent the mint() functions.

### Transfer flow

What actually happens when an end-user uses the bridge?

Here’s the workflow occurring when a user transfers an ERC-721 token from the Palm Network to Ethereum:

1. The user calls the deposit() function on Palm Network’s bridge contract. The user must provide the target chain, the resource ID, and the calldata, which represent a token transfer to be executed on Ethereum.
2. The ERC-721 handler’s deposit() function is called, which verifies the data provided by the user. The bridge then locks the token on the ERC-721 contract.
3. Proposal - Palm’s bridge contract then emits a Deposit event containing the data that will be executed on Ethereum. On ChainBridge, this type of event is called a proposal.
4. Once the bridge’s first relayer detects the event on Ethereum, it executes the proposal on Ethereum via the bridge. Effectively, the proposal delegates an executeDepositcall to the ERC-721 handler contract.
5. The ERC-721 handler’s executeDeposit function validates the parameters provided by the user and makes a call to the target ERC-721 contract to mint the token with the original ID (a custom mint function on the target contract is passed the token ID as part of the calldata to ensure this). The token is transferred to the recipient’s account on Ethereum.

## How to ensure your token contract works with the bridge?

All you need to do to integrate with the bridge is to prepare your token contracts so that the Bridge supports them.

### Making your token contracts bridge-compatible

#### 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 table below for relevant contract addresses). Your original and synthetic can be exactly the same as in production.

Once you have prepared your contracts for the bridge, feel free to contact us on discord to validate your contracts compatibility, they will be tested by our team on the testnet and then set for production.

 Chain Address Description Palm Mainnet 0xB3C62Aed3be8e0577D4724C40a01379dbf895C01 Bridge Contract - main bridge smart contract Palm Mainnet 0x97FAcbF880e47c27cafbA6bE3d677A50d536813e ERC-20 Handler - account given permissions to mint/burn ERC-20 tokens on Palm Network Palm Mainnet 0x317bc33A442AA0f6C8235cb2487f0Bb338eD27E4 ERC-721 Handler - account given permissions to mint/burn ERC-721 tokens on Palm Network Palm Mainnet 0x22887Af68E57A76692f2686020FF563aC873eA24 Primary relayer - transactions for assets minted/burned on Palm Network can be found here Palm Mainnet 0x2A84F0c208872184c9dfcd57B6cd7bF63BcF829E Secondary relayer - this account periodically sweeps for transactions that did not successfully complete by the primary relayer Palm Mainnet 0x8993D834b036913E25f35ed1Cbb288F26779e16a Tollbooth account that collects fees in DAI from bridge users. Dai fees are regularly transferred to the Ethereum network using the bridge, swapped for Ether, and added to the Ethereum Relayer accounts. This way, it can pay for gas fees incurred by the bridge when it mints a token on behalf of the user. Carbon offset fees are also collected in this account and withdrawn for direct payment to carbon offset projects. Ethereum Mainnet 0x7D0e63736aEb136aCd44C70D6e1A0f27fb897679 Bridge Contract - main bridge smart contract. Ethereum Mainnet 0xf4684EB75659Bec9C3c3b19f075a6fd5ABa34b87 ERC-20 Handler - account given permissions to mint/burn ERC-20 tokens on Ethereum. Ethereum Mainnet 0x4B4473093d98F0daA39e60406333B019c3A29D36 ERC-721 Handler - account given permissions to mint/burn ERC-721 tokens on Ethereum. Ethereum Mainnet 0x22887Af68E57A76692f2686020FF563aC873eA24 Primary relayer - transactions for assets minted/burned on Ethereum can be found here. Ethereum Mainnet 0x2A84F0c208872184c9dfcd57B6cd7bF63BcF829E Secondary relayer - this account periodically sweeps for transactions that did not successfully complete on the primary relayer.