Website logo
Join the CommunityContact Us
Navigate through spaces
⌘K
Palm Network Developer Portal
The Palm Network
Submit an Article
Network Details
Validators
Network Update
Transition runbook
Testing
JSON-RPC API changes
Validators
Self-hosted nodes
Developer updates
FAQs
Support
Get Started
Get PALM Tokens
Gas Fees
Connect to Palm network
Run a Transaction Node
How To Tutorials
MetaMask Wallet Setup
Deploy NFT Contracts
Verifying NFT Contracts
Mint NFTs with Hardhat
Bridge
Use the Bridge
Integrate With the Bridge
Bridge Component Addresses
Deprecated
Use Supported Tools
Block Explorer
Moralis
Palm Safe
The Graph
Covalent API
Docs powered by Archbee
Bridge

Integrate With the Bridge

10min

Integrating Token Contracts With the Bridge

In the 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 so that token owners can transfer them between Palm and Ethereum networks.



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.

You might need to make some changes to your contracts for them to be bridge-compatible:

The below specifications applies to ERC-721 contracts. Tutorials for ERC-1155 or ERC-20 contracts will be provided in the future.



Original contract

  1. Add the Enumerable extension from Open Zeppelin libraries
    • Enumerability of all the token IDs in the contract
    • Support for all token IDs owned by each account

In addition to Enumerable, any custom implementation of ERC-721 is allowed: bulk minting, token ID auto-increment, etc.

Synthetic contract

  1. Add the Enumerable extension from Open Zeppelin libraries
  2. Create a custom mint() function.
  3. Grant Minting Permission to the Bridge
  4. Add a burn() function.
  5. Grant Burning Permission to the Bridge


Contract Example | Original and Synthetic Contracts



ERC-721 contract example
// 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 an exception if called by any account other than the owners.
    * Implemented using the underlying AccessControl methods.
    */
    modifier onlyOwners() {
        require(hasRole(OWNER_ROLE, _msgSender()), "Caller does not have the OWNER_ROLE");
        _;
    }

    /**
    * @dev Throws an exception 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;
    }
}



Testing your contracts

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 Goerli (Ethereum's main testnet).

For ERC-721 contracts, you will need to initialize it using the constructor, and grant the bridge's ERC-721 handler contract MINTER_ROLE. See the Bridge Component Addresses for relevant contract addresses.

Your original and synthetic can be exactly the same as in production.



How to Use the Bridge

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

If everything looks OK, you will grant minter and/or burner roles to the bridge's ERC721 handler contract address, and then we will register the contracts with the bridge so that tokens can be transferred by owners.

We strongly recommend testing your contracts with the testnet bridge before setting them up with the mainnet bridge.



🔥 Bridge Component Addresses



Updated 05 Dec 2023
Did this page help you?
PREVIOUS
Use the Bridge
NEXT
Bridge Component Addresses
Docs powered by Archbee
TABLE OF CONTENTS
Integrating Token Contracts With the Bridge
Original Contracts vs Synthetic Contracts
Original contract
Synthetic contract
Contract Example | Original and Synthetic Contracts
Testing your contracts
How to Use the Bridge
🔥 Bridge Component Addresses
Docs powered by Archbee