Skip to content
This documentation is in beta, and we’re looking for feedback. See how you can contribute 🌟

Integrate With the Bridge

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

// 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](</howto/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