# Horizen Documentation > Developer documentation for Horizen — an EVM-identical L3 on Base (Ethereum L2) using the OP Stack. Horizen adds compliant, verifiable privacy via VELA, a confidential execution coprocessor powered by Trusted Execution Environments (TEEs). Deploy standard Solidity contracts with Foundry or Hardhat (same tooling as Base/Ethereum), or build privacy-preserving apps with VELA. Mainnet chain ID: 26514, RPC https://horizen.calderachain.xyz/http. Testnet chain ID: 2651420, RPC https://horizen-testnet.rpc.caldera.xyz/http. ZEN is the native governance token (Base ERC-20: 0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229). Tutorials cover: ERC-20 and NFT deployment, price-triggered escrow with Stork oracle, bridging assets via Stargate LayerZero OFT (ZEN OFT Adapter on Base 0x57da2D504bf8b83Ef304759d9f2648522D7a9280, Horizen EID 30399) and native OP Stack bridge (L1StandardBridge on Base 0xf4a6cc4171fda694439f856d912777aa6ab05369), Goldsky subgraph indexing, PureFi compliance gating, and Safe multisig setup. Governance: Horizen DAO with ZenIP proposal and voting process. This file contains all documentation content in a single document following the llmstxt.org standard. ## About Decentralized governance is a cornerstone of the Horizen ecosystem. For a project like Horizen it is necessary to have a framework enabling community members to govern the project. For Horizen, Horizen DAO is that framework: a means through which network participants can effectuate changes to influence the future of Horizen. DAO stands for “Decentralized Autonomous Organization”, and has emerged as the standard for decentralized governance in the blockchain space (and beyond). While every structure entails tradeoffs, the Horizen community decided on the DAO model as it has been used successfully for a number of leading projects. Through the Horizen DAO, any member of the Horizen community can have a say over the direction of the project. This is a privilege and responsibility to which community members are expected to accord appropriate seriousness. This section lays out the basics of Horizen DAO and explains how to participate in governance. Look around, familiarize yourself with the guidelines and processes, and get involved! --- ## Guiding Values A strong social layer that builds trust and maintains integrity across the Horizen ecosystem is crucial. Community building is essential in growing blockchain adoption, and communities are built on shared values. The Horizen DAO’s guiding values are as follows: - Transparency: The Horizen DAO seeks to be open and honest to its mission of decentralized governance. Just as the blockchain is powered by open-source software with publicly visible transactions, decisions, and activities undertaken by the Horizen community through this DAO should be clearly grounded, articulated and understandable to all. - Accountability: Members of the community are accountable to each other. The mechanisms outlined in this Constitution crystallize the ways in which this will be achieved. - Security: Security is at the heart of the Horizen ecosystem and must be taken seriously by all community members and tokenholders. Any changes to the Horizen protocol should weigh security considerations heavily. - Community Involvement: The Horizen ecosystem and the Horizen DAO will only be as good as members of the community make it. Everyone has different skills to contribute and is encouraged to do so to ensure the success of the project. - Continuous Improvement: The Horizen DAO should never rest on its laurels. There are always things the community can do better, where community members should take an active role in proposing and implementing such improvements. - Social Responsibility: You, as a community member and tokenholder, are here because you want to help build the future of the internet – and of the world. As such, each of us must be responsible stewards of that mission and act with integrity in all that we do. --- ## Constitution ## 1. Introduction This Horizen DAO Constitution (the “Constitution”) sets forth the set of binding rules, procedures, processes, direction, and ethos for the Horizen DAO and The Horizen Foundation (the "Foundation"). Unless otherwise defined in this Constitution, defined terms in this Constitution shall have the meaning ascribed to those terms in Bylaws. Decentralized governance stands as a cornerstone of the Horizen ecosystem, which entrusts every Tokenholder with a pivotal role from introducing ideas to making formal proposals and carrying out implementation. As valuable contributors, your engagement is as much a privilege as it is a responsibility. When reviewing or commenting on ideas or proposals, remind yourself that a fellow member of the Horizen community took the time to research, write, and share their idea with you. Be respectful and constructive. Tokenholders are collaborators, and your collaboration underpins the Horizen DAO’s and ecosystem’s success. Similarly, when you put forward an idea or comment, be mindful of its impact and significance on community members and the larger mission of the Horizen DAO and ecosystem. Ensure that the ideas you share or the proposals you submit are well researched, thought through, and are presented clearly in a way that allows the idea or proposal to stand-up to this community’s scrutiny. The Foundation, a Cayman Islands foundation company, is responsible for furthering the growth of the Horizen ecosystem and will function as a steward of the Horizen community. Acting through its board of directors, and subject to the Bylaws and Foundation Articles ("Foundation Governing Documents"), the Foundation may: - Facilitate the administration of Horizen DAO governance; - Disburse treasury assets to fund community-approved initiatives, enter into contracts with service providers, or otherwise further its purpose of growing the Horizen ecosystem; - Amend this Constitution; and - Undertake other actions conducive to its stewardship role. The Foundation will always undertake these responsibilities in a manner consistent with Horizen DAO’s guiding principles and values, this Constitution and the Foundation Governing Documents. ## 2. Guiding Values A strong social layer that builds trust and maintains integrity across the Horizen ecosystem is crucial. Community building is essential in growing blockchain adoption, and communities are built on shared values. The Horizen DAO’s guiding values are as follows: - Transparency: The Horizen DAO seeks to be open and honest to its mission of decentralized governance. Just as the blockchain is powered by open-source software with publicly visible transactions, decisions and activities undertaken by the Horizen community through this DAO should be clearly grounded, articulated and understandable to all. - Accountability: Members of the community are accountable to each other. The mechanisms outlined in this Constitution crystallize the ways in which this will be achieved. - Security: Security is at the heart of the Horizen ecosystem and must be taken seriously by all community members and tokenholders. Any changes to the Horizen blockchain should weigh security considerations heavily. - Community Involvement: The Horizen ecosystem and the Horizen DAO will only be as good as members of the community make it. Everyone has different skills to contribute and is encouraged to do so to ensure the success of the project. - Continuous Improvement: The Horizen DAO should never rest on its laurels. There are always things the community can do better, and community members should take an active role in proposing and implementing such improvements. - Social Responsibility: You, as a community member and Tokenholder, are here because you want to help build the future of the internet – and of the world. As such, each of us must be responsible stewards of that mission and act with integrity in all that we do. ### For Reference - The "Horizen DAO" or "DAO" means, collectively, the decentralized community of individuals that own the $ZEN token. - The "Total Circulating Supply" means all of the $ZEN tokens currently in circulation. - The "ZenIP Process" means the rules and procedures for submitting and voting on ZenIPs, detailed further below in this Constitution. - The "Bylaws" means the bylaws of the Foundation as adopted by the Foundation in accordance with the Foundation Articles (as amended from time to time). A copy of the Bylaws is available here: [Foundational Documents](/governance/reference/foundational_docs). - The "Foundation Articles" means the Memorandum and Articles of Association of the Foundation (as may be amended from time to time). A copy of the Foundation Articles is available here: [Foundational Documents](/governance/reference/foundational_docs). - "Technical ZenIP" means any ZenIP which requires a technical implementation or upgrade to the Horizen blockchain, or which requires the modification of the Foundation Governing Documents or this Constitution. - "Non-Technical ZenIP" means any other ZenIP which does not require a technical upgrade to the Horizen blockchain, including but not limited to making grants, proposing arrangements with third parties and conducting Foundation governance. ## 3. Chain Governance This Constitution describes the decision-making framework for governance for Horizen Currently, $ZEN is the governance token for Horizen. This Constitution lays out a dual-track process for implementing improvement proposals. Technical proposals follow a “rough consensus” model, where implementation happens when a majority of network participants, on the applicable chain, choose to run the new software version. This is the same model currently used by certain other blockchain networks, most notably Bitcoin and Ethereum. Non-Technical proposals, meanwhile, are enacted via a directive to the Foundation by means of a Tokenholder Vote. Further details regarding the process and voting thresholds are outlined below. All DAO-approved proposals are reviewed by the Special Council for adherence to the mission of the Horizen DAO, this Constitution, the Foundation Governing Documents, and applicable law, before a ZenIP or EONIP is formally adopted. The responsibilities of the Special Council are described below and in the Foundation Governing Documents. Following a DAO-approved proposal, the Foundation is responsible for implementing the actions described in each such proposal. ## 4. DAO Voting As detailed in this Constitution and further in the Foundation Governing Documents, Tokenholders have the authority to propose and vote on ZenIPs, subject to, and in accordance with, either the ZenIP Process or EONIP Process (as applicable): - Appointing or removing members of the Special Council, and other DAO working groups, as those come into being, from time to time. - Creating new DAO Committees from time to time. - Remove individuals or organizations from the role of director or supervisor of the Foundation in accordance with the Foundation Articles (provided that the Foundation may not, at any time, be left with no directors or no supervisor). - Provide consent to any proposed changes to the Foundation Governing Documents, which would adversely affect in a material way the rights or powers conferred on the Tokenholders under the Foundation Governing Documents. - Approve the use, implementation, enhancement, improvement, management and licensing of the Horizen DAO-Governed IP; - Approve any other action in accordance with successful ZenIPs or the Foundation Governing Documents. ## 5. ZenIP Processes **a. Before Submitting a Proposal** Before you can submit a ZenIP, you must first check to see whether there have been past discussions about your proposal idea, what issues were raised, any reasons a past ZenIP may have been rejected or not welcomed by the community during a vote, and whether any other facts and circumstances have changed that would make your proposal idea proposal infeasible or unwarranted. To do this, you must check past discussions on Discourse ("Discourse") and the Horizen Improvement Proposals Discord channel and confirm that either (1) your idea is novel and has not been proposed before (“Option 1”) or (2) if your idea has been proposed before, the idea you plan to propose is substantially different such that the likelihood of success during a vote by the community is high (“Option 2”). This is the initial research phase. To submit a Non-Technical ZenIP you must hold at least 25,000 $ZEN. To submit a Technical ZenIP you must hold at least 100,000 $ZEN. Once you have completed the initial research phase, and your proposal idea meets either Option 1 or Option 2 above, you can post your idea to the Horizen Discourse following this template: - The Title: ZenIP Proposal Idea: [The Title for Your Proposal]; - A single topic sentence that describes your idea clearly; - Why you are proposing your idea and what problem you are trying to fix; - The outcome if your proposal is successfully passed during a vote; **b. Modifications to a ZenIP Idea** When you have posted your proposal idea, the Discourse moderator will then confirm whether your proposal idea conforms to the DAO’s approved guidelines before the proposal will be made public on Discourse. If you wish to change parts of your proposal idea, you may only do so in the comments to the published proposal idea on Discourse and your comments will be flagged as “official” changes – you will not have the opportunity to modify your proposal idea once it has been submitted to the Discourse moderator for publication. **c. Community Feedback** Once your proposal idea has been approved for publication by the Discourse moderator, community members will provide their feedback and reactions to your proposal idea via the comment section. This is a required process before your proposal idea is formally presented as a draft as outlined in step D. Community members have a seven (7) day window to provide their feedback on your proposal idea, including their reactions to your ideas or modifications made in the comments. You should use this as an opportunity to engage with the community, consider their feedback, and begin to anticipate the ways in which the community may (or may not) welcome your proposal before it is put towards a vote. **d. ZenIP Draft Creation** **i. ZenIP Draft** Once your proposal has completed the seven (7) day community review and feedback window your proposal idea will move to the draft stage. Your proposal must follow the below format: You must then draft your ZenIP according to a specific format and structure as outlined below: - Preamble -- Headers containing metadata about the ZenIP (see below). The License field of the preamble indicates the licensing terms, which MUST be acceptable according to the ZenIP licensing requirements which can be found [here for Horizen](https://github.com/HorizenOfficial/zen/blob/main/COPYING). - Terminology -- Definitions of technical or non-obvious terms used in the document. - Abstract -- A short (~200 word) description of the technical issue being addressed. - Motivation -- The motivation is critical for ZenIPs that want to change the Horizen blockchain. It should clearly explain why the current state of the blockchain is inadequate to address the problem that the ZenIP solves. - Specification -- The technical or non-technical specifications should describe the interface and semantics of any new feature. The specifications should be detailed enough to allow competing, interoperable implementations of the blockchain. - Rationale -- The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work. The rationale should provide evidence of consensus within the community and discuss important objections or concerns raised during discussion. - Security and privacy considerations -- If applicable, security and privacy considerations should be explicitly described, particularly if the ZenIP makes explicit trade-offs or assumptions. - Reference implementation -- If applicable, literal code implementing the ZenIP's specification, and/or a link to the reference implementation of the ZenIP's specification (only applicable to Technical ZenIPs ). The reference implementation must be completed before any ZenIP is given status “Implemented”, but it generally need not be completed before the ZenIP is accepted into “Proposed” format and structure. **e. ZenIP Draft Review and Moderator Feedback** Once your ZenIP draft has been posted, the moderator(s) will ensure that your draft correctly follows the template. The moderator(s) will also have the opportunity to request additional information from you via private messaging on Discourse. If you do not respond to the moderator’s request for feedback or additional information within 30 days from receipt of the moderator’s request, your ZenIP draft will be automatically rejected. If your draft ZenIP conforms to this Constitution and the ZenIP format, the moderator will assign your draft a proposal number. **f. Administrative Review** DAO and ecosystem. ZenIPs play a significant role in the system of decentralization. As such, all ZenIPs must undergo administrative review by the Special Council, which is made up of 7 members from the Horizen community. For more information on the Special Council, including the Horizen DAO’s ability to elect Special Council members, please see further below in this Constitution. The Special Council will determine whether additional information or clarification is needed before your ZenIP can be moved to a vote. During this review, the Special Council may tag your ZenIP “return for clarification” for the following reasons: - Cost to implement is unclear or unable to be calculated; - Proposal would use more than 10% of the DAO’s public treasury; - Proposal conflicts with another proposal; Your proposal may also be tagged as “return for reconsideration,” for reasons including but not limited to: - Proposal violates or threatens the mission or values of the Horizen DAO, whether financially or through a proposed significant deviation from the mission of the Foundation; - Proposal has the potential to harm the Horizen DAO or the Foundation, whether financially or through a proposed significant deviation from the mission of the Foundation; - Proposal might violate law or otherwise contradict advice of counsel for the Foundation. If a proposal is suspected to violate the law or otherwise contradict the advice of counsel for the Foundation, the Foundation directors will confer with external counsel to confirm; - There is reasonable suspicion of fraud or other misleading information in the ZenIP. There is a maximum of three (3) revisions permitted following tags and comments from the Special Council. If your ZenIP draft fails to satisfy the requirements after three (3) turns of revisions and comments, it will be rejected, and you will have to resubmit your proposal from the ZenIP Idea stage. If additional information is not needed, your ZenIP will proceed to the voting stage. During the period, the Special Council meets at least once a month to review draft proposals that are being elevated to the voting stage. This process is in place to ensure the continued security of the Horizen DAO, Foundation, and community. **g. Vote** Now that your ZenIP has been approved for voting, it will go to a live Snapshot vote. Voting can be accessed at [Snapshot](https://snapshot.org/#/horizenfoundation.eth). As a $ZEN tokenholder, you can participate in this Snapshot voting. ZenIPs are divided into two main categories, that each have their own requirements for adoption subject to a vote, in addition to the above requirements for drafting and submitting the proposals before they are put to a vote. **i. Non-Technical ZenIP** - Majority: The proposal must receive more votes in favor than against to pass; and - Quorum: There must be at least 3% of total circulating supply of $ZEN participating in the vote. **ii. Technical ZenIP** - Majority: Of the total votable $ZEN tokens participating in the vote, the proposal must receive at least 67% votes in favor of the proposal to pass; and - Quorum: There must be at least 5% of total circulating supply of $ZEN participating in the vote. Voting remains open for a period of 72 hours. At this time, anyone who meets the qualifications to vote may vote once for every $ZEN contained in the wallet they are voting from. Following 72 hours, voting closes, and no more votes may be cast. **iii. Tie Votes or Conflicts** If there is a tie in votes for Non-Technical ZenIP, then such ZenIP will be sent back to a community discussion phase for community members to discuss their differences or comprehension of the effects and purposes of the proposal and then put to a revote. A ZenIP can only be sent back to the community discussion phase three (3) times before the proposal is automatically rejected and will need to be resubmitted starting from the ZenIP Idea Stage. **h. Cooldown Period** A ZenIP that passes must undergo a final review to ensure the proposal does not violate the Foundation Governing Documents, any laws, or otherwise jeopardizes the safety and security of the Horizen DAO or Foundation. The Foundation Director(s) will review a ZenIP within 30 days from its passing. If the Foundation Director(s) are satisfied that no issues exist with implementing the approved ZenIP, it moves towards implementation. **i. Implementation** All ZenIPs that satisfy their voting approval thresholds, as described above, in addition to the other requirements specified in this Constitution, may be implemented in the following ways: - Technical ZenIP - Upon approval of any Technical ZenIP by the Horizen DAO, the Foundation will merge the approved ZenIP into the Horizen Github repository for release as part of a new software version. - Ultimate implementation of a Technical ZenIP which seeks to implement technical upgrades or changes will depend on the decision of a majority of network participants (miners and node operators) to run the new software version. - Non-Technical ZenIP - The Foundation and the DAO administrator will assist in implementing approved Non-Technical ZenIPs, including hiring or engaging service providers, coordinating documentation and contract negotiations and all other actions incidental to implementing the approved Non-Technical ZenIPs. - The Horizen DAO administrator and/or project management team will assist with the above implementations but are not responsible for this on their own. ## 6. Special Council **a. Overview** The Special Council is tasked with serving as a steward for the Horizen DAO and providing oversight of the Foundation with security of the Horizen DAO and ecosystem as its paramount focus. These activities may include holding emergency operational meetings to discuss any security threats to the Horizen DAO, any protocol utilizing the $ZEN token, the Tokenholders, or the Foundation. The Special Council serves as a line of defense before a ZenIP is put up for a final vote by the Tokenholders. The Special Council replaces the Horizen Community Council (HCC). The Horizen Community Council was established to represent $ZEN tokenholders within the Horizen ecosystem. That role is now embodied within the safety function of the Special Council. **b. Composition and Elections** Initially, the Special Council will be made up of seven (7) seats. Those seats will be filled by seven (7) individuals, initially appointed by the Foundation Director(s), who will serve a term that begins on the date this Constitution is effective, until the first election cycles (“Initial Term”), as detailed below. Thereafter, Tokenholders can nominate and elect Special Council members to serve a standard Term of one year (“Standard Term”), unless, subject to one of the conditions below, a Special Council member is removed before the end of their Standard Term. To stagger elections and optimize continuity of the Special Council, committee seats are randomly assigned to either a September cohort consisting of four (4) seats or a March cohort consisting of three (3) seats. Together, these cohorts will be subject to the Election Cycles discussed below. Beginning in 2025 and continuing every year thereafter unless otherwise modified pursuant to this Constitution: - Elections for the March cohort will commence on February 15 at 12:00 UTC of the relevant year and continue for seven (7) days until the election has been completed. - Elections for the September cohort will commence on August 15 at 12:00 UTC of the relevant year and continue for seven (7) days until the election has been completed. - An election is deemed to have been completed if, within the election period, Tokenholders had the opportunity to nominate or vote to elect an eligible individual to the Special Council, even if no nominations were made or no individual(s) elected. Tokenholders may, at any time during the Initial or Standard Term, vote to remove a Special Council member through, and subject to, a Non-Technical ZenIP Process. If, at any time, one or several Special Council members are removed such that a vacancy exists on the Special Council, including where all seven seats are vacant, the Foundation Director(s) shall, acting in the best interests of the Foundation, have the authority to nominate and appoint replacement members to the Special Council who shall serve a Standard Term until such time as they are removed by tokenholders subject to the Non-Technical ZenIP Process or are replaced in the immediately next election. The initial Special Council Members are: - Johncarlo Maddalena - Domenico Minniti - Benjamin Charbit - Herve Larren - Jemma Xu - Elias Ahonen - Leila Salieva **c. Responsibilities** Special Council members serve as a DAO committee and are tasked with serving as a steward for the Horizen DAO and providing oversight of the Foundation on behalf of the Horizen DAO. This includes calling emergency operational meetings as needed to discuss and address security threats to the Horizen DAO, any protocol utilizing the Token, the Tokenholders, or the Foundation, along with ensuring that they review any ZenIPs or EONIPs prior to voting by the Horizen DAO. **d. Qualifications** Given the responsibilities of Special Council members, candidates for the role of Special Council should have demonstrated most, if not all, of the following competencies: - Experience in the web3 industry and a demonstrated interest in the Horizen ecosystem. - Background in community engagement and community safety, with an emphasis on decentralized governance systems and communities. - An entrepreneurial mindset. - Excellent communication and writing skills. No more than two (2) Special Council members at a time should be a current employee of, owner of, or holder of a beneficial interest in any common entity. The Foundation’s grant recipients shall be excluded from the foregoing limitation, provided that the grant award is the sole contractual relationship between any such entity and the Foundation. Additionally, at all times, no less than five (5) of the Special Council members shall be non-US persons. No candidate with conflicts of interest that would prevent him or her from acting in the best interests of the Horizen DAO and/or the Foundation should be elected to the Special Council. Potential conflicts of interest could be, but are not limited to, affiliations with direct Horizen blockchain or EON blockchain competitors, or proven histories of exploiting projects and/or others. **e. Governance of Special Council** Following the conclusion of the Initial Term, Tokenholders may, subject to a Tokenholder Vote and the ZenIP Process, alter the number of seats that constitute the Special Council, provided that number of seats shall never be less than three (3) or larger than seven (7) and must always be an odd number to avoid tie votes. A quorum of the Special Council shall exist: - When the Special Council has three (3) total seats, two (2) members are present; - When the Special Council has five (5) total seats, three (3) members are present; - When the Special Council has seven (7) total seats, five (5) members are present. The Horizen DAO may approve and implement a ZenIP to change the rules governing future Special Council elections, but the ZenIP Process may not be used to intervene in an ongoing election. Special Council members may only be removed prior to the end of their terms under two conditions: 1. At least 10% of the total circulating supply of $ZEN participates in a vote for the removal of a Special Council member and at least 5/6th (83.33%) of all votes are "in favor" of removal; or All but one of the then current Special Council members, excluding the member at issue, vote in favor of removal. 2. The seat(s) of any Special Council member(s) who has been removed prior to the end of their respective term(s) shall remain unfilled until the next election, unless prior to an upcoming election, a replacement member is appointed by a vote of at least two-thirds (2/3rds) of the then-sitting Special Council members. If a vacant seat is filled by a vote of at least two-thirds (2/3rds) of the then-sitting Special Council members, then such member occupying that seat shall be up for reelection at the next election. ## 7. Committees To streamline the decentralized system of governance for the Horizen DAO the operation of the Foundation may be facilitated by committees. The members on these committees will serve to enhance decentralized governance and efficiency through administrative functions within the Foundation but are not fiduciaries. Tokenholders will have the authority to vary and/or create new committees within the Horizen DAO, from time to time, in accordance with this Constitution and/or the Foundation Governing Documents, to further these efforts at the DAO. --- ## Communication Channels - Discord: [Discord](https://discord.com/invite/z8eebsj7Sv) is a social media and messaging application that serves as the main communications platform for Horizen. Anyone is free to join the official Horizen server and engage in discussion with other community members across any of the channels. - Discourse: [Discourse](https://horizen.discourse.group/) is the first stop for proposals. It is an open forum for governance-related discussions. It is where $ZEN tokenholders can create ZenIPs, view current and past proposals, and comment on proposals. Members of the Horizen community must register for an account before contributing and engaging with posts. - Snapshot: [Snapshot](https://snapshot.org/#/horizenfoundation.eth) is an off-chain voting interface that allows the community to vote on ZenIPs that have reached the voting stage. Voting power is based on the amount of $ZEN held. --- ## Proposal Categories The improvement proposal process is the key tool for Horizen DAO’s community-led governance. Currently, $ZEN is the governance token for Horizen. Improvement proposals for each are called ZenIPs. Any $ZEN tokenholder can vote on improvement proposals, and those who hold the requisite amount of $ZEN may put forth proposals to the community. There are two categories of each type of proposal: technical and non-technical. A Technical ZenIP requires a technical implementation or upgrade to the Horizen blockchain or which requires the modification of the Horizen Foundation’s Governing Documents or the Constitution of the Horizen DAO. A Non-Technical ZenIP is any other proposal which does not require a technical upgrade to the Horizen blockchain; such a proposal may relate to making grants, proposing arrangements with third parties, or conducting governance of the Horizen Foundation. To submit a Technical ZenIP, you must hold at least 100,000 $ZEN. To submit a Non-Technical ZenIP, you must hold at least 25,000 $ZEN. --- ## Voting Process Once an ZenIP has been approved for voting, it will proceed to a live Snapshot vote. As a $ZEN tokenholder, you can participate in this Snapshot voting. The two categories of ZenIPs/EONIPs have different requirements for vote quorums and majorities: - [Technical ZenIP](https://snapshot.org/#/horizenfoundationtechnical.eth/create) - There must be at least 5% of the circulating supply of $ZEN participating in the vote. The proposal must receive at least 67% of votes in favor in order to pass. - [Non-Technical ZenIP](https://snapshot.org/#/horizenfoundationnontechnical.eth/create) - There must be at least 3% of the circulating supply of $ZEN participating in the vote. The proposal must receive more votes in favor than against in order to pass. Voting remains open for a period of 72 hours. At this time, anyone who meets the qualifications to vote may vote once for every $ZEN contained in the wallet they are voting from. Following 72 hours, voting closes, and no more votes may be cast. --- ## Quick Voting Guide This quick guide is meant to help you go over the simple steps to vote on ZENIPs. ## FAQ How much voting power do I have? Your voting power is a sum of the following criteria: - $ZEN in your wallet. --- ## Foundational Documents [**Transparency Report**](/transparency) **Foundation Bylaws** Download PDF **Foundation Memorandum of Association** Download PDF **Foundation Articles of Association** Download PDF --- ## Submit or Vote on a Proposal - **Submit a proposal** — [Discourse](https://horizen.discourse.group/) - **Vote on a proposal** — [Snapshot](https://snapshot.org/#/horizenfoundation.eth) --- ## Join the Discussion - **Discord** — [Join the Horizen community](https://discord.com/invite/z8eebsj7Sv) --- ## Network & Contract Addresses All addresses below are sourced directly from official Horizen and integration partner documentation. Always verify addresses before interacting with any contract on mainnet. ## Token Contracts ### ZEN | Network | Type | Address | |---|---|---| | Mainnet Base | ERC-20 | [`0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229`](https://basescan.org/address/0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229) | | Mainnet Base | OFT Adapter (LayerZero) | [`0x57da2D504bf8b83Ef304759d9f2648522D7a9280`](https://basescan.org/address/0x57da2D504bf8b83Ef304759d9f2648522D7a9280) | | Mainnet Horizen | OFT (LayerZero) | [`0x57da2D504bf8b83Ef304759d9f2648522D7a9280`](https://explorer.horizen.io/address/0x57da2D504bf8b83Ef304759d9f2648522D7a9280) | | Testnet Base | ERC-20 (tZEN) | [`0x107fdE93838e3404934877935993782F977324BB`](https://sepolia.basescan.org/address/0x107fdE93838e3404934877935993782F977324BB) | | Testnet Base | OFT Adapter (LayerZero) | [`0x2ead4B0beBD8e54F9B7cC1007DF4c44a27b9a339`](https://sepolia.basescan.org/address/0x2ead4B0beBD8e54F9B7cC1007DF4c44a27b9a339) | | Testnet Horizen | OFT (LayerZero) | [`0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87`](https://explorer-testnet.horizen.io/address/0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87) | ### USDC / USDC.e | Network | Type | Address | |---|---|---| | Mainnet Base | USDC ERC-20 | [`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`](https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) | | Mainnet Base | Lock Contract (LayerZero) | [`0x27a16dc786820B16E5c9028b75B99F6f604b5d26`](https://basescan.org/address/0x27a16dc786820B16E5c9028b75B99F6f604b5d26) | | Mainnet Horizen | USDC.e ERC-20 | [`0xDF7108f8B10F9b9eC1aba01CCa057268cbf86B6c`](https://explorer.horizen.io/address/0xDF7108f8B10F9b9eC1aba01CCa057268cbf86B6c) | | Mainnet Horizen | OFT (LayerZero) | [`0x3a1293Bdb83bBbDd5Ebf4fAc96605aD2021BbC0f`](https://explorer.horizen.io/address/0x3a1293Bdb83bBbDd5Ebf4fAc96605aD2021BbC0f) | ### cbBTC | Network | Type | Address | |---|---|---| | Mainnet Base | ERC-20 | [`0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf`](https://basescan.org/address/0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf) | | Mainnet Base | OFT Adapter (LayerZero) | [`0x68fb5BB8330C0b9d907F50f278143873276ee056`](https://basescan.org/address/0x68fb5BB8330C0b9d907F50f278143873276ee056) | | Mainnet Horizen | OFT (LayerZero) | [`0x68fb5BB8330C0b9d907F50f278143873276ee056`](https://explorer.horizen.io/address/0x68fb5BB8330C0b9d907F50f278143873276ee056) | | Testnet Base | ERC-20 | [`0xcbb7c0006f23900c38eb856149f799620fcb8a4a`](https://sepolia.basescan.org/address/0xcbb7c0006f23900c38eb856149f799620fcb8a4a) | | Testnet Base | OFT Adapter (LayerZero) | [`0x5dE29d14E72feb79967596F3Ae57A9BfBA192769`](https://sepolia.basescan.org/address/0x5dE29d14E72feb79967596F3Ae57A9BfBA192769) | | Testnet Horizen | OFT (LayerZero) | [`0x06DA6bDD2aB23447af5162ab0975edDA7E8d3747`](https://explorer-testnet.horizen.io/address/0x06DA6bDD2aB23447af5162ab0975edDA7E8d3747) | ## Migration Contracts (Base Mainnet) These contracts handle the ZEN token migration from the legacy Horizen mainchain and EON chain. Relevant for integrations that verify ZEN token provenance or check unclaimed migration balances. | Contract | Address | |---|---| | **ZenToken** - Official ZEN ERC-20 | [`0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229`](https://basescan.org/address/0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229) | | **EONBackupVault** - EON balance distribution | [`0x1Cc689233837A0b96e1f176d49FC08462f70C47F`](https://basescan.org/address/0x1Cc689233837A0b96e1f176d49FC08462f70C47F) | | **ZendBackupVault** - ZEND manual claiming | [`0x1Ee188bDf19eBF04B73Ab6FFcec2a864cd4774F2`](https://basescan.org/address/0x1Ee188bDf19eBF04B73Ab6FFcec2a864cd4774F2) | Source code: [github.com/HorizenOfficial/horizen-migration](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/contracts) ## Oracle Contracts: Stork Verified directly from [docs.stork.network/resources/contract-addresses/evm](https://docs.stork.network/resources/contract-addresses/evm). | Network | Address | |---|---| | **Mainnet Horizen** | [`0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62`](https://explorer.horizen.io/address/0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62) | | **Testnet Horizen** | [`0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62`](https://explorer-testnet.horizen.io/address/0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62) | > Both mainnet and testnet share the same Stork contract address on Horizen. ## Multisig: Den / Safe | Resource | URL | |---|---| | Den on Horizen | `https://safe.horizen.io/welcome` | --- ## JSON-RPC Endpoints Horizen is fully EVM-compatible and supports the standard Ethereum JSON-RPC API. Use these endpoints to connect wallets, submit transactions, query state, and interact with deployed contracts. ## Mainnet | Parameter | Value | |---|---| | **Chain ID** | `26514` | | **RPC (HTTPS)** | `https://horizen.calderachain.xyz/http` | | **RPC (WebSocket)** | `wss://horizen.calderachain.xyz/ws` | | **Currency Symbol** | `ETH` | | **Block Explorer** | `https://explorer.horizen.io/` | | **Bridge** | `https://hub.horizen.io/` | ## Testnet (Base Sepolia) | Parameter | Value | |---|---| | **Chain ID** | `2651420` | | **RPC (HTTPS)** | `https://horizen-testnet.rpc.caldera.xyz/http` | | **RPC (WebSocket)** | `wss://horizen-testnet.rpc.caldera.xyz/ws` | | **Currency Symbol** | `ETH` | | **Block Explorer** | `https://explorer-testnet.horizen.io/` | | **Faucet** | `https://hub-testnet.horizen.io/` | ## Supported JSON-RPC Methods Horizen supports the full standard Ethereum JSON-RPC specification. The most commonly used methods are listed below. **Chain & Network** | Method | Description | |---|---| | `eth_chainId` | Returns the current chain ID | | `net_version` | Returns the network ID | | `eth_blockNumber` | Returns the latest block number | | `eth_gasPrice` | Returns the current gas price in wei | **Accounts & Balances** | Method | Description | |---|---| | `eth_accounts` | Returns a list of addresses owned by the client | | `eth_getBalance` | Returns the ETH balance of an address | | `eth_getTransactionCount` | Returns the nonce of an address | **Blocks** | Method | Description | |---|---| | `eth_getBlockByHash` | Returns block data by hash | | `eth_getBlockByNumber` | Returns block data by block number | | `eth_getBlockTransactionCountByHash` | Returns number of transactions in a block by hash | | `eth_getBlockTransactionCountByNumber` | Returns number of transactions in a block by number | **Transactions** | Method | Description | |---|---| | `eth_sendRawTransaction` | Submits a signed transaction | | `eth_getTransactionByHash` | Returns transaction data by hash | | `eth_getTransactionReceipt` | Returns receipt for a mined transaction | | `eth_estimateGas` | Estimates gas required for a transaction | | `eth_call` | Executes a call without creating a transaction | **Logs & Events** | Method | Description | |---|---| | `eth_getLogs` | Returns logs matching a filter | | `eth_newFilter` | Creates a new filter for log events | | `eth_getFilterLogs` | Returns logs for an existing filter | | `eth_uninstallFilter` | Removes a filter | **Example for Querying Chain ID:** ```bash curl -X POST https://horizen.calderachain.xyz/http \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' ``` ```json { "jsonrpc": "2.0", "id": 1, "result": "0x6792" } ``` > `0x6792` is `26514` in decimal — Horizen Mainnet's Chain ID. **Example for Querying ETH Balance:** ```bash curl -X POST https://horizen.calderachain.xyz/http \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0xYourAddress", "latest"], "id": 1 }' ``` **Example for WebSocket subscription (ethers.js):** ```javascript import { ethers } from "ethers"; const provider = new ethers.WebSocketProvider( "wss://horizen.calderachain.xyz/ws" ); provider.on("block", (blockNumber) => { console.log("New block:", blockNumber); }); ``` --- ## Supported Wallets & Tools ## Wallets Any EVM-compatible wallet works on Horizen Chain. Add the network using the RPC configuration above. | Wallet | Type | Notes | |---|---|---| | **MetaMask** | Browser / Mobile | Add Horizen manually via Settings → Networks | | **Coinbase Wallet** | Browser / Mobile | Add custom network via Settings | | **Rabby** | Browser | Add manually if not auto-detected | | **Ledger** | Hardware | Use with MetaMask or Rabby as the interface | | **Safe (via Den)** | Smart Contract Multisig | [https://safe.horizen.io](https://safe.horizen.io) | **Adding Horizen Mainnet to MetaMask:** ``` Network Name: Horizen Mainnet RPC URL: https://horizen.calderachain.xyz/http Chain ID: 26514 Currency Symbol: ETH Block Explorer: https://explorer.horizen.io/ ``` **Adding Horizen Testnet to MetaMask:** ``` Network Name: Horizen Testnet RPC URL: https://horizen-testnet.rpc.caldera.xyz/http Chain ID: 2651420 Currency Symbol: ETH Block Explorer: https://explorer-testnet.horizen.io/ ``` ## Developer Tools | Tool | Type | Notes | |---|---|---| | **Foundry** | Smart contract development | Fully supported | | **Hardhat** | Smart contract development | Fully supported | | **ethers.js** | JavaScript library | Works out of the box with Horizen RPC | | **viem** | TypeScript library | Works out of the box with Horizen RPC | | **wagmi** | React hooks library | Works out of the box with Horizen RPC | | **Goldsky** | Subgraph indexing | Chain slug: `horizen-testnet` | | **Stork** | Oracle | Contract: `0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62` | **Connecting ethers.js to Horizen:** ```javascript import { ethers } from "ethers"; // HTTPS provider — Mainnet const provider = new ethers.JsonRpcProvider( "https://horizen.calderachain.xyz/http" ); // WebSocket provider — Mainnet const wsProvider = new ethers.WebSocketProvider( "wss://horizen.calderachain.xyz/ws" ); // Verify connection const network = await provider.getNetwork(); console.log("Chain ID:", network.chainId); // 26514n ``` **Connecting viem to Horizen:** ```typescript import { createPublicClient, http, defineChain } from "viem"; const horizen = defineChain({ id: 26514, name: "Horizen Mainnet", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://horizen.calderachain.xyz/http"], webSocket: ["wss://horizen.calderachain.xyz/ws"], }, }, blockExplorers: { default: { name: "Horizen Explorer", url: "https://explorer.horizen.io", }, }, }); const client = createPublicClient({ chain: horizen, transport: http(), }); const blockNumber = await client.getBlockNumber(); console.log("Current block:", blockNumber); ``` ## Block Explorers | Network | Explorer | API Base URL | |---|---|---| | Mainnet | [https://explorer.horizen.io/](https://explorer.horizen.io/) | `https://explorer.horizen.io/api` | | Testnet | [https://explorer-testnet.horizen.io/](https://explorer-testnet.horizen.io/) | `https://explorer-testnet.horizen.io/api` | Both explorers are powered by Blockscout and support the full Blockscout REST API — query transactions, blocks, addresses, token transfers, and verified contracts programmatically. Blockscout API docs: [docs.blockscout.com/for-users/api](https://docs.blockscout.com/for-users/api) ## Bridge & Faucet | Resource | Network | URL | |---|---|---| | **Bridge** | Mainnet | `https://hub.horizen.io/` | | **Bridge** | Testnet | `https://hub-testnet.horizen.io/` | | **ZEN Bridge (Stargate)** | Base → Horizen | `https://stargate.finance/?srcChain=base&srcToken=0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229&dstChain=horizen&dstToken=0x57da2D504bf8b83Ef304759d9f2648522D7a9280` | | **Faucet** | Testnet | `https://hub-testnet.horizen.io/` | ## Quick Reference — All URLs | Resource | URL | |---|---| | Mainnet RPC | `https://horizen.calderachain.xyz/http` | | Mainnet WebSocket | `wss://horizen.calderachain.xyz/ws` | | Testnet RPC | `https://horizen-testnet.rpc.caldera.xyz/http` | | Testnet WebSocket | `wss://horizen-testnet.rpc.caldera.xyz/ws` | | Mainnet Explorer | `https://explorer.horizen.io/` | | Testnet Explorer | `https://explorer-testnet.horizen.io/` | | Mainnet Bridge | `https://hub.horizen.io/` | | Testnet Bridge / Faucet | `https://hub-testnet.horizen.io/` | | ZEN Bridge (Stargate) | `https://stargate.finance/?srcChain=base&srcToken=0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229&dstChain=horizen&dstToken=0x57da2D504bf8b83Ef304759d9f2648522D7a9280` | | Multisig (Den) | `https://safe.horizen.io/welcome` | | Official Docs | `https://docs.horizen.io` | | GitHub | `https://github.com/HorizenOfficial` | | Stork Oracle Docs | `https://docs.stork.network` | | Goldsky Docs | `https://docs.goldsky.com` | | Den Docs | `https://docs.onchainden.com` | --- ## Bridge Assets via Stargate Stargate is a cross-chain liquidity protocol built on [LayerZero](https://layerzero.network). It is the primary bridge for ZEN, USDC.e, and cbBTC on Horizen, supporting transfers across 80+ chains. ZEN and cbBTC use the LayerZero OFT (Omnichain Fungible Token) standard — burned on the source chain and minted on the destination. USDC uses Stargate's lock/mint model - USDC is locked on Base and USDC.e is minted on Horizen. ## Supported Tokens The following tokens are supported on Horizen via Stargate: | Token | Horizen Contract | Bridge Mechanism | |-------|-----------------|------| | ZEN | `0x57da2D504bf8b83Ef304759d9f2648522D7a9280` | OFT (burn/mint) | | USDC.e | `0x3a1293Bdb83bBbDd5Ebf4fAc96605aD2021BbC0f` | Lock/Mint | | cbBTC | `0x68fb5BB8330C0b9d907F50f278143873276ee056` | OFT (burn/mint) | :::note ZEN on Horizen and Base share the same OFT contract address (`0x57da2D504bf8b83Ef304759d9f2648522D7a9280`) — this is expected behavior for the OFT standard. ::: ## Bridge via the Stargate UI ### Prerequisites - A wallet with funds on your source chain (e.g., ETH for gas on Base or Horizen) - The token you want to bridge ### Steps 1. Go to [stargate.finance](https://stargate.finance) 2. Connect your wallet 3. On the **Transfer** page, select your source chain and token 4. Select **Horizen** as the destination chain (or vice versa) 5. Enter the amount to bridge 6. Review the estimated fee and receive amount 7. Click **Transfer** and confirm the transaction in your wallet Stargate offers two transfer modes: - **Simple** - sends assets to your same connected wallet address on the destination chain. Use this for most cases. - **Advanced** - lets you specify a custom destination address. Use this if you're bridging to a different wallet. ### Fees Stargate charges a protocol fee of **0.06%** per transfer. You will also pay gas on the source chain. No gas is required on the destination chain. ### Transfer time Most transfers complete in **under 2 minutes**, depending on source chain finality. ## Slippage Stargate uses unified liquidity pools. For stable assets like USDC, slippage is typically negligible. You can configure slippage tolerance in **Advanced Settings** on the transfer page. - **Low slippage (0.1%)** — protects against price changes but may cause the transaction to fail in low-liquidity conditions - **High slippage (1–3%)** — more likely to succeed but you may receive slightly fewer tokens For ZEN and cbBTC (OFT standard), slippage does not apply — the burn/mint mechanism guarantees you receive the exact amount minus the protocol fee. ## LayerZero OFT Contract Addresses For developers integrating token bridging directly, here are the full contract references: ### Mainnet | Token | Chain | OFT Contract | |-------|-------|-------------| | ZEN | Base | OFT Adapter: `0x57da2D504bf8b83Ef304759d9f2648522D7a9280` | | ZEN | Horizen | OFT: `0x57da2D504bf8b83Ef304759d9f2648522D7a9280` | | USDC | Base | Lock contract: `0x27a16dc786820B16E5c9028b75B99F6f604b5d26` | | USDC.e | Horizen | OFT: `0x3a1293Bdb83bBbDd5Ebf4fAc96605aD2021BbC0f` | | cbBTC | Base | OFT Adapter: `0x68fb5BB8330C0b9d907F50f278143873276ee056` | | cbBTC | Horizen | OFT: `0x68fb5BB8330C0b9d907F50f278143873276ee056` | ### Testnet (Base Sepolia ↔ Horizen Testnet) | Token | Chain | OFT Contract | |-------|-------|-------------| | tZEN | Base Sepolia | OFT Adapter: `0x2ead4B0beBD8e54F9B7cC1007DF4c44a27b9a339` | | tZEN | Horizen Testnet | OFT: `0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87` | | cbBTC | Base Sepolia | OFT Adapter: `0x5dE29d14E72feb79967596F3Ae57A9BfBA192769` | | cbBTC | Horizen Testnet | OFT: `0x06DA6bDD2aB23447af5162ab0975edDA7E8d3747` | --- ## Bridge Assets via Native Bridge This page covers step-by-step instructions for using the Horizen native bridge. :::note Before You Start - You need a Web3 wallet (MetaMask, Rabby, or Coinbase Wallet) - **Deposits:** Ensure your wallet has ETH on **Base** (mainnet) or **Base Sepolia** (testnet) - **Withdrawals:** Ensure your wallet has ETH on **Horizen** and enough ETH to cover gas on Base when you return to claim - Always test with a small amount first ::: ## Deposit: Base → Horizen Use this flow to move ETH from Base into Horizen Chain. The estimated time is a few minutes after Base confirmation ### Steps **1. Open the bridge** Go to `https://hub.horizen.io/` (mainnet) or `https://hub-testnet.horizen.io/` (testnet) in your browser. **2. Connect your wallet** Click **Connect Wallet** and approve the connection in your wallet. Make sure your wallet is set to **Base** (mainnet) or **Base Sepolia** (testnet) before connecting. **3. Set the direction** In the **From** field, select **Base**. In the **To** field, select **Horizen**. **4. Enter the amount** Enter the amount of ETH you want to bridge. Leave enough ETH in your Base wallet to cover the transaction gas fee. **5. Confirm the transaction** Click **Confirm** and approve the transaction in your wallet. The transaction is submitted to Base. **6. Wait for arrival** Your ETH will arrive on Horizen within a few minutes. You can verify the balance by checking your wallet address on the Horizen explorer: - Mainnet: `https://explorer.horizen.io/` - Testnet: `https://explorer-testnet.horizen.io/` :::note If your wallet is not yet configured for Horizen, add the network first. See [Network Configuration →](/horizen-chain/network/mainnet) for the RPC details. ::: ## Withdrawal: Horizen → Base Use this flow to move ETH from Horizen Chain back to Base. :::warning Read This Before You Withdraw Withdrawals take a minimum of **7 days** to complete due to the optimistic rollup challenge window. This is the security mechanism of the bridge. You will need to return to the bridge after 7 days to complete a second transaction (the Claim) on Base. **Funds are not automatically sent to your Base wallet.** ::: **Estimated time:** 7 days minimum from initiation to receipt on Base ### Step 1 - Initiate the Withdrawal on Horizen **1. Open the bridge** Go to `https://hub.horizen.io/` (mainnet) or `https://hub-testnet.horizen.io/` (testnet). **2. Connect your wallet** Click **Connect Wallet** and approve the connection. Make sure your wallet is set to **Horizen** (mainnet) or **Horizen Testnet**. **3. Set the direction** In the **From** field, select **Horizen**. In the **To** field, select **Base**. **4. Enter the amount** Enter the amount of ETH you want to withdraw. Leave enough ETH in your Horizen wallet to cover the gas fee for this transaction. **5. Confirm the transaction** Click **Confirm** and approve the transaction in your wallet. This submits the withdrawal transaction on Horizen. Once confirmed, the 7-day challenge window begins. :::note Keep a record of your transaction hash. You can look up the transaction on the Horizen explorer to confirm it was included. ::: --- ### Step 2 - Claim on Base (after 7 days) After the 7-day challenge window has passed, you must return to the bridge to release your funds on Base. The bridge will show a **Claim** button for any pending withdrawal that is ready. **1. Return to the bridge** Go to `https://hub.horizen.io/` and connect the same wallet you used to initiate the withdrawal. **2. Locate your pending withdrawal** The bridge will display your pending withdrawal with a **Claim** button once the 7-day window has elapsed. **3. Switch to Base** Switch your wallet to **Base** (mainnet) before claiming. The claim transaction is executed on Base. **4. Submit the claim** Click **Claim** and approve the transaction in your wallet. This calls the `OptimismPortal` contract on Base, which releases your ETH. **5. Confirm receipt** Your ETH will arrive in your Base wallet once the claim transaction is confirmed. You can verify this on [Basescan](https://basescan.org). ## Troubleshooting **My deposit hasn't arrived after 10 minutes** Check the transaction status on the Horizen block explorer using your wallet address. If the deposit transaction is confirmed on Base but has not appeared on Horizen, wait a few more minutes — the Sequencer may be slightly delayed. If the issue persists, check the [Horizen Discord](https://discord.gg/horizen) for any known network issues. **I can't see my Claim button after 7 days** Make sure you are connected with the same wallet address used to initiate the withdrawal, and that your wallet is set to **Base**. If the claim button is still not visible, the 7-day window may not have fully elapsed — check the initiation transaction timestamp on the explorer. **I accidentally closed the browser during the withdrawal** Your withdrawal is recorded on-chain. Return to `https://hub.horizen.io/`, reconnect the same wallet, and the pending withdrawal will be visible. You do not need to re-initiate anything. **I sent ETH to the bridge contract address directly** Do not interact with bridge contracts directly unless you are using the official bridge UI or the OP Stack SDK. Direct contract calls require specific parameters and incorrect usage can result in lost funds. If you have done this, contact the Horizen team immediately via [Discord](https://discord.gg/horizen). --- ## How Bridging Works on Horizen ## The Native Bridge Horizen supports two bridges: the **native OP Stack bridge** and the **Stargate bridge**. This page covers how the native bridge works — it is the canonical, most trust-minimized way to move ETH between Base and Horizen. For bridging ZEN, USDC, cbBTC, or other chains, see [Bridge Assets via Stargate](./bridge-assets-stargate.md). | Network | Bridge URL | | --- | --- | | Mainnet | `https://hub.horizen.io/` | | Testnet | `https://hub-testnet.horizen.io/` | The native bridge relies entirely on the OP Stack's optimistic rollup security model — the same model that secures Base itself. No third-party protocol is involved in the bridging path. ## Deposit and Withdrawal Mechanics Moving assets in each direction behaves very differently. Understanding this before you bridge will prevent surprises. ### Deposits: Base → Horizen Deposits are straightforward and fast. When you deposit from Base to Horizen: 1. Your ETH (or supported asset) is locked in the bridge contract on Base 2. An equivalent amount is minted on Horizen 3. Funds arrive on Horizen **within a few minutes** There is no challenge window for deposits. Once the transaction is confirmed on Base and the Horizen Sequencer processes it, your funds are available. Set direction: Base → Horizen Enter amount and confirm Deposit confirmed ### Withdrawals: Horizen → Base Withdrawals are a two-step process and take significantly longer due to the **7-day challenge window** that is a core property of the optimistic rollup model. :::warning 7-Day Withdrawal Window Withdrawals from Horizen to Base take a minimum of **7 days** to complete. It is the security mechanism of the optimistic rollup model. Plan accordingly. Do not initiate a withdrawal if you need your funds on Base sooner than 7 days from now. ::: :::note Two Transactions Required A withdrawal requires **two separate transactions**: - Transaction 1: Initiate the withdrawal on Horizen (costs gas on Horizen) - Transaction 2: Claim the funds on Base after 7 days (costs gas on Base) You must return to `https://hub.horizen.io/` after the 7-day window to complete the claim. Funds are not automatically sent to your Base wallet. ::: Step 1 — Initiate: set direction Horizen → Base, enter amount Step 2 — Claim: return after 7 days and submit the claim on Base ## What the Bridge Supports The native bridge currently supports **ETH** between Base and Horizen. :::note Additional assets may be available through the bridge UI. Always verify the asset list directly at `https://hub.horizen.io/` — this documentation reflects the state at the time of writing and the bridge may be updated independently. ::: ## Trust Model The native bridge inherits the full security of the OP Stack and Base: - **Deposits** are secured by Base's smart contracts and finality guarantees - **Withdrawals** are secured by the 7-day optimistic challenge window — any invalid state can be disputed during this period before funds are released - **No third party** is involved in the bridging path — there is no relayer, liquidity provider, or off-chain validator between you and the contracts This makes the native bridge the most trust-minimized option available. The tradeoff is the 7-day withdrawal window. --- ## Compliance On Horizen, compliance logic is code instead of a platform guardrail. Since Horizen is EVM-identical, you can implement compliance at whatever layer matches your requirement: encode rules directly in Solidity, enforce policies over private data using confidential computation, or integrate a purpose-built AML/KYC protocol. ## Contract-Level Rules The most direct approach is to encode rules into your contract. Access control lists, role-based modifiers, jurisdiction flags, and per-address limits are all standard Solidity patterns that require no external dependencies or off-chain components. ```solidity mapping(address => bool) public allowlisted; modifier onlyAllowlisted() { require(allowlisted[msg.sender], "address not allowlisted"); _; } ``` This pattern is appropriate when your compliance logic is fully deterministic, you own and control the rule set, and access decisions do not depend on external data (such as AML risk scores maintained by a third party). ## Confidential Enforcement Some compliance requirements involve private data: verifying a user meets a financial threshold without disclosing the amount, enforcing policy on encrypted inputs, or producing auditable proof that a rule executed correctly without exposing the underlying state. For these cases, the compliance logic can run inside a TEE using VELA. The compliance logic is your code. VELA provides a hardware-isolated execution environment and a cryptographic attestation proving the rules ran correctly on the specified input. No operator, cloud provider, or external party can access the data during execution. Authorized verifiers like auditors and regulators can confirm compliance through the attestation without seeing the underlying state. → [What is VELA?](/vela/introduction) ## Third-Party AML/KYC Screening For AML screening against external risk databases, Horizen supports integration with PureFi. The model is off-chain screening with synchronous on-chain enforcement: PureFi's issuer runs the AML check off-chain and returns a signed payload; your contract calls the PureFi Verifier with that payload as a blocking call. It reverts if the check failed, so your business logic executes only if the address passed screening. This pattern is for use cases where compliance requires validation against maintained AML datasets that your contract cannot hold or update on its own. → [PureFi Integration](/horizen-chain/integrations/purefi) --- ## Using Foundry [**Foundry**](https://www.getfoundry.sh/) is a fast, Rust-based development toolkit for Ethereum. It handles everything from compilation and testing to deployment and on-chain interaction via the command line. ## Install Foundry Foundry runs natively on **macOS** and **Linux**. On **Windows**, you must use [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install) — Foundry does not support PowerShell or CMD. Make sure `curl` and `git` are installed before proceeding. ```bash curl -L https://foundry.paradigm.xyz | bash foundryup ``` Verify the installation: ```bash forge --version ``` ## Create a new project ```bash forge init hello_horizen && cd hello_horizen ``` This scaffolds a new Foundry project with a sample `Counter.sol` contract at `src/Counter.sol`, a test file, and a default `foundry.toml` config. ## Configure Horizen in foundry.toml Open `foundry.toml` and add the Horizen Testnet as a named network: ```toml [rpc_endpoints] horizen_testnet = "https://horizen-testnet.rpc.caldera.xyz/http" horizen_mainnet = "https://horizen.calderachain.xyz/http" ``` This lets you reference networks by name in scripts and commands instead of pasting the full RPC URL each time. ## Compile your contracts ```bash forge build ``` ## Deploy to Horizen Testnet ```bash forge create src/Counter.sol:Counter \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --private-key ``` On success, the output will include the deployed contract address: ``` Deployer: 0xYourWalletAddress Deployed to: 0xYourContractAddress Transaction hash: 0xYourTxHash ``` ## Deploy to Horizen Mainnet Swap the RPC URL to the mainnet endpoint and make sure your wallet has mainnet ETH: ```bash forge create src/Counter.sol:Counter \ --rpc-url https://horizen.calderachain.xyz/http \ --private-key ``` ## Using a .env file (recommended) Avoid exposing your private key in terminal history by storing it in a `.env` file: ``` # .env PRIVATE_KEY=your_private_key_here RPC_URL=https://horizen-testnet.rpc.caldera.xyz/http ``` Then load it in your deploy command: ```bash source .env forge create src/Counter.sol:Counter \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY ``` Add `.env` to your `.gitignore` — never commit private keys to a repository. ## Run tests ```bash forge test ``` To run tests against a live fork of Horizen: ```bash forge test --fork-url https://horizen-testnet.rpc.caldera.xyz/http ``` For contract verification after deployment, see [Verify a Contract](/horizen-chain/deploy-contracts/verify-contract). --- ## Using Hardhat [**Hardhat**](https://hardhat.org/) is a flexible JavaScript/TypeScript development environment for Ethereum. It is well suited for teams that prefer a Node.js-based workflow, plugin ecosystem, and scripted deployments. ## Initialize a new project Hardhat requires **Node.js (v18+)** and **npm**. It works on **macOS**, **Linux**, and **Windows**. On Windows, we recommend using [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install) for a smoother experience, though native CMD/PowerShell is also supported. ```bash mkdir hardhat-tutorial cd hardhat-tutorial npx hardhat init ``` Follow the prompts to create a TypeScript project. Hardhat will scaffold the project with a sample contract, test, and ignition deployment module. ## Install dependencies ```bash npm install ``` ## Configure Horizen in hardhat.config.ts Open `hardhat.config.ts` and add Horizen Testnet and Mainnet as named networks: ```typescript import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { solidity: "0.8.28", networks: { horizen_testnet: { type: "http", url: "https://horizen-testnet.rpc.caldera.xyz/http", accounts: [""], chainId: 2651420, }, horizen_mainnet: { type: "http", url: "https://horizen.calderachain.xyz/http", accounts: [""], chainId: 26514, }, }, }; export default config; ``` Use environment variables for your private key in production. See the `.env` pattern in the Foundry section above — it works identically with `process.env.PRIVATE_KEY` in Hardhat configs. ## Compile your contracts ```bash npx hardhat compile ``` ## Deploy to Horizen Testnet Hardhat uses Ignition for deployments. Your deployment module lives at `ignition/modules/`. Deploy with: ```bash npx hardhat ignition deploy ignition/modules/Counter.ts --network horizen_testnet ``` ## Deploy to Horizen Mainnet ```bash npx hardhat ignition deploy ignition/modules/Counter.ts --network horizen_mainnet ``` ## Run tests ```bash npx hardhat test ``` For contract verification after deployment, see [Verify a Contract](/horizen-chain/deploy-contracts/verify-contract). --- ## Verify a Contract Verifying your contract publishes the source code to the block explorer, allowing anyone to read and audit it. ## Verify using Foundry Foundry's `forge verify-contract` command supports Blockscout-based explorers directly. **Testnet:** ```bash forge verify-contract \ src/Counter.sol:Counter \ --verifier blockscout \ --verifier-url https://explorer-testnet.horizen.io/api/ ``` **Mainnet:** ```bash forge verify-contract \ src/Counter.sol:Counter \ --verifier blockscout \ --verifier-url https://explorer.horizen.io/api/ ``` If your contract uses a specific compiler version or optimization settings, add them explicitly: ```bash forge verify-contract \ src/Counter.sol:Counter \ --verifier blockscout \ --verifier-url https://explorer.horizen.io/api/ \ --compiler-version 0.8.28 \ --num-optimization-runs 200 ``` ## Verify using Hardhat Install the Hardhat Verify plugin if it is not already included: ```bash npm install --save-dev @nomicfoundation/hardhat-verify ``` Add the Blockscout verifier configuration to `hardhat.config.ts`: ```typescript import "@nomicfoundation/hardhat-verify"; const config: HardhatUserConfig = { // ...existing config... etherscan: { apiKey: { horizen_testnet: "placeholder", // Blockscout does not require an API key horizen_mainnet: "placeholder", }, customChains: [ { network: "horizen_testnet", chainId: 2651420, urls: { apiURL: "https://explorer-testnet.horizen.io/api", browserURL: "https://explorer-testnet.horizen.io", }, }, { network: "horizen_mainnet", chainId: 26514, urls: { apiURL: "https://explorer.horizen.io/api", browserURL: "https://explorer.horizen.io", }, }, ], }, }; ``` Then verify: **Testnet** ```bash npx hardhat verify --network horizen_testnet ``` **Mainnet** ```bash npx hardhat verify --network horizen_mainnet ``` ## Verify manually via the block explorer If you prefer a UI: 1. Go to your contract address on the explorer. 2. Click the Contract tab. 3. Select Verify & Publish. 4. Choose your verification method: Solidity (Single file), Solidity (Standard JSON input), or Solidity (Multi-part files). 5. Fill in the compiler version and optimization settings that match your build exactly. 6. Submit — the explorer will compile and match the bytecode. Once verified, your contract's source code, ABI, and read/write methods will be publicly visible on the explorer under the Contract tab. --- ## Subgraph Indexing (Goldsky) Goldsky is a data indexing provider that makes it straightforward to extract, transform, and serve on-chain data as queryable APIs — powering dashboards, analytics, and application backends without building custom indexing infrastructure. Goldsky offers two core products: - **Subgraphs** — define event-driven indexing logic, query via GraphQL - **Mirror** — real-time data replication pipelines that stream on-chain data directly into your own databases or data warehouses **Horizen Testnet** is available on Goldsky with the chain slug: **`horizen-testnet`** ## Getting Started **Install the Goldsky CLI:** ```bash curl https://goldsky.com | sh ``` **Authenticate:** ```bash goldsky login ``` ## Deploy via CLI (Subgraph Config Files) This is the standard approach if you are already familiar with subgraph development. You define your indexing logic locally across three files: - `subgraph.yaml` — defines data sources, event handlers, and the network - `schema.graphql` — defines the entities your subgraph will index - `src/mappings.ts` — AssemblyScript handlers that transform raw events into entities **subgraph.yaml — targeting Horizen Testnet:** ```yaml specVersion: 0.0.5 schema: file: ./schema.graphql dataSources: - kind: ethereum name: MyContract network: horizen-testnet source: address: "0xYourContractAddress" abi: MyContract startBlock: 0 mapping: kind: ethereum/events apiVersion: 0.0.7 language: wasm/assemblyscript entities: - MyEntity abis: - name: MyContract file: ./abis/MyContract.json eventHandlers: - event: MyEvent(indexed address,uint256) handler: handleMyEvent file: ./src/mappings.ts ``` **Deploy to Goldsky:** ```bash goldsky subgraph deploy my-subgraph/1.0.0 --path . ``` Full step-by-step guide: [docs.goldsky.com/subgraphs/deploying-subgraphs](https://docs.goldsky.com/subgraphs/deploying-subgraphs) ## Deploy via Instant Subgraphs (No Config Required) If you want to get up and running immediately without writing indexing logic, Goldsky can auto-generate the subgraph configuration from a contract address and ABI. This is the fastest way to start querying contract events as a GraphQL API. ```bash goldsky subgraph deploy my-subgraph/1.0.0 \ --from-abi \ --address \ --network horizen-testnet \ --startBlock ``` Goldsky will generate the `subgraph.yaml`, `schema.graphql`, and mapping files automatically and deploy in one step. ## Mirror — Real-Time Data Pipelines Goldsky Mirror lets you stream raw on-chain data — blocks, transactions, logs, traces — directly into your own infrastructure in real time. This is useful for analytics databases, alerting systems, and any use case where you need the full raw chain data rather than event-driven entity indexing. **Create a pipeline interactively:** ```bash goldsky pipeline create my-horizen-pipeline ``` This launches a guided CLI flow where you select: - Data source type (subgraph, chain-level dataset) - Filters to apply - Destination sink (PostgreSQL, ClickHouse, Kafka, webhooks, and more) **Create a pipeline from a definition file:** ```bash goldsky pipeline create my-horizen-pipeline \ --definition-path ./pipeline.json ``` Definition files are useful for complex pipelines with multiple sources or sinks. ## Reference Links | Resource | URL | |---|---| | Goldsky Documentation | `https://docs.goldsky.com` | | Deploying Subgraphs Guide | `https://docs.goldsky.com/subgraphs/deploying-subgraphs` | | Instant Subgraphs Guide | `https://docs.goldsky.com/subgraphs/instant-subgraphs` | | Mirror Pipelines Guide | `https://docs.goldsky.com/mirror/introduction` | --- ## Multisig Wallets (Den) Den is the multisig wallet interface for Horizen Chain. It is a **self-custodial, multi-signature wallet** built on top of **Safe contracts** — the most widely audited and trusted smart contract wallet infrastructure in the EVM ecosystem, securing billions in assets across hundreds of protocols. Den is available for Horizen at: **`https://safe.horizen.io/welcome`** With Den on Horizen you can: - Create and manage new Safe multisig wallets on Horizen Chain - Import and use existing Safe wallets - Create, simulate, and execute transactions - Batch multiple transactions into a single execution - Manage signers and approval thresholds ## Creating a New Multisig Wallet 1. Go to `https://safe.horizen.io/welcome` and connect your wallet 2. Click **Create new Safe** 3. Give your Safe a name 4. Add the signer addresses and set the required approval threshold (e.g. 2-of-3) 5. Review the setup and deploy — this submits a contract deployment transaction on Horizen 6. Once confirmed, your multisig is live and ready to use Full guide: [docs.onchainden.com/set-up-den/creating-and-managing-safes](https://docs.onchainden.com/set-up-den/creating-and-managing-safes) ## Using an Existing Safe If you already have a Safe deployed on another EVM network, you can import it into Den on Horizen using the same address (EVM addresses are consistent across chains). Guide: [docs.onchainden.com/set-up-den/using-existing-safes](https://docs.onchainden.com/set-up-den/using-existing-safes) ## Creating Transactions 1. Open your Safe in Den at `https://safe.horizen.io` 2. Click **New Transaction** 3. Choose the transaction type: send assets, contract interaction, or raw transaction 4. Fill in the details and click **Create** 5. Share with co-signers — each required signer approves via their wallet 6. Once the threshold is reached, any signer can execute the transaction on-chain Guide: [docs.onchainden.com/creating-transactions/getting-started](https://docs.onchainden.com/creating-transactions/getting-started) ## Simulating Transactions Before executing a multisig transaction on-chain, Den lets you simulate it to preview exactly what state changes will occur — token transfers, contract state changes, and any potential reverts — without spending gas. Guide: [docs.onchainden.com/creating-transactions/simulations](https://docs.onchainden.com/creating-transactions/simulations) ## Batching Transactions Den supports batching multiple transactions into a single on-chain execution. This is particularly useful for DeFi operations (e.g. approve + swap in one step) or protocol management actions that need to be atomic. Guide: [docs.onchainden.com/creating-transactions/batching](https://docs.onchainden.com/creating-transactions/batching) ## Reference Links | Resource | URL | |---|---| | Den on Horizen | `https://safe.horizen.io/welcome` | | Den Documentation | `https://docs.onchainden.com` | | Create & Manage Safes | `https://docs.onchainden.com/set-up-den/creating-and-managing-safes` | | Using Existing Safes | `https://docs.onchainden.com/set-up-den/using-existing-safes` | | Creating Transactions | `https://docs.onchainden.com/creating-transactions/getting-started` | | Simulating Transactions | `https://docs.onchainden.com/creating-transactions/simulations` | | Batching Transactions | `https://docs.onchainden.com/creating-transactions/batching` | --- ## Compliance Gating (PureFi) PureFi is a compliance layer that combines off-chain AML checking with on-chain verification gating. Rather than running AML logic inside your contract, PureFi's off-chain issuer validates a signed request and returns a cryptographic payload — your contract then calls the PureFi Verifier with that payload, which either reverts (compliance check failed) or returns cleanly so your business logic can proceed. The critical mental model: **your contract is not monitoring logs**. It is making a synchronous call to the PureFi Verifier and blocking on the result. ## How PureFi Works The full integration flow, from user action to business logic execution: 1. Your frontend or backend builds a verification request — `package type`, `rule ID`, `from` (user wallet), `to` (your contract) 2. The user's wallet signs the request payload via EIP-712 3. Your frontend/backend submits the signed payload to the PureFi issuer 4. The issuer runs the AML check off-chain — if it passes, it returns a `_purefidata` bytes string 5. Your contract receives `_purefidata` and calls `verifier.validatePayload(_purefidata)` — the call reverts if validation fails 6. Only after a clean return from the Verifier does your contract execute its business logic (mint, swap, access grant, etc.) ## Step 1: Install the PureFi Solidity SDK **Foundry:** ```bash forge install purefiprotocol/sdk-solidity-v5 ``` Then add to `remappings.txt`: ``` @purefi-sdk-solidity-v5/=lib/sdk-solidity-v5/src/ ``` **npm:** ```bash npm i @purefi/sdk-solidity-v5 ``` ## Step 2: Implement the Verifier in Your Contract Import the two core interfaces: ```solidity import {IPureFiVerifier} from "@purefi-sdk-solidity-v5/interfaces/IPureFiVerifier.sol"; import {PureFiDataLibrary} from "@purefi-sdk-solidity-v5/libraries/PureFiDataLibrary.sol"; ``` The recommended pattern is to extend the base receiver and override it with the Horizen mainnet deployment: ```solidity abstract contract PureFiSdkVerifiedReceiverBase { using PureFiDataLibrary for bytes; IPureFiVerifier public immutable verifier; uint256 public immutable expectedChainId; constructor(address verifier_, uint256 expectedChainId_) { verifier = IPureFiVerifier(verifier_); expectedChainId = expectedChainId_; } function submitPureFiPackage(bytes calldata purefiData) external returns (bytes32 purefiDataHash) { if (block.chainid != expectedChainId) revert WrongChain(expectedChainId, block.chainid); verifier.validatePayload(purefiData); bytes calldata package_ = purefiData.getPackage(); purefiDataHash = keccak256(purefiData); lastFrom = package_.getFrom(); lastTo = package_.getTo(); lastRule = package_.getRule(); lastPackageType = package_.getPackageType(); } } ``` Extend this for Horizen mainnet with the hardcoded verifier proxy address and chain ID: ```solidity contract PureFiSdkVerifiedReceiverHorizenMainnet is PureFiSdkVerifiedReceiverBase { address public constant PUREFI_VERIFIER = 0x681Edd4906e2a0a277E2A6c394A4595f83e1329c; uint256 public constant HORIZEN_MAINNET_CHAIN_ID = 26514; constructor() PureFiSdkVerifiedReceiverBase(PUREFI_VERIFIER, HORIZEN_MAINNET_CHAIN_ID) {} } ``` :::warning `verifier.validatePayload(purefiData)` is a synchronous external call. If the Verifier reverts for any reason — invalid payload, failed compliance check, wrong chain — the entire transaction reverts and none of the subsequent code in your contract executes. ::: :::note How the proxy Verifier works The Verifier address is a proxy. When your contract calls it, the proxy `delegatecall`s to the implementation contract, which performs signature and payload verification. On success, it returns to the proxy, which returns to your contract. There is no log-watching — this is a real blocking call with a synchronous result. ::: ## Step 3: Build the Off-chain Integration Flow In production, your frontend or backend is responsible for steps 1–3 of the flow. Playground (covered below) lets you run through these manually before wiring them into your app. **1. Construct the payload** Build the request with these fields: | Field | Description | |---|---| | `package type` | Determined by your PureFi subscription configuration | | `rule ID` | The compliance rule to evaluate | | `from` | The user wallet address being checked | | `to` | Your target contract address | **2. Request user signature** Prompt the user's connected wallet to sign the payload via EIP-712. The signature is included in the body sent to the issuer. **3. Call the PureFi issuer** Submit the payload and signature to the issuer endpoint. If the AML check passes, the issuer returns a `_purefidata` bytes string. If it fails, no `_purefidata` is returned and you should not attempt to submit a transaction. ## Step 4: Submit the Transaction Pass the `_purefidata` bytes string returned by the issuer as the argument to your contract's verification method: ```solidity // Your contract method that accepts the purefiData function yourMethod(bytes calldata purefiData, /* ...other args */ ) external { verifier.validatePayload(purefiData); // business logic only runs if the line above doesn't revert } ``` From your frontend, call this method with `purefiData` as the argument after receiving it from the issuer. ## Testing with Playground PureFi's Playground lets you manually run the complete flow — construct a payload, sign it, call the issuer, and submit the resulting `_purefidata` to your contract — without writing any frontend code. Use it to verify your integration before wiring up your own app. **Playground is a debugging and learning tool, not a production entry point.** ### Payload Constructor Set the package type, rule ID, `from` (your test wallet), and `to` (your deployed contract address) to generate the payload for the issuer. ### Signature Process Connect your browser wallet. The chain ID must match the target network. After confirming, the wallet produces the EIP-712 signature included in the next step. ### Verification Process Select the issuer environment and submit. A successful response returns the `_purefidata` string you will use on-chain. ### Transaction Builder Paste your contract address, ABI, and select the method that accepts `_purefidata`. Fill in the returned `_purefidata` as the parameter and submit the transaction. ## Horizen Mainnet Deployment | | Value | |---|---| | SDK version | `@purefi/sdk-solidity-v5@5.2.0` | | Verifier proxy | `0x681Edd4906e2a0a277E2A6c394A4595f83e1329c` | | Verifier implementation | `0x61C468B554B6F0b0842242F7Df079bb392EE0555` | | Chain ID | `26514` | Register your deployed contract against your PureFi subscription in the [PureFi Dashboard](https://dashboard.purefi.io/) before testing. ## Debugging: Success and Failure Indicators **Signs the integration is working:** - The issuer returns a non-empty `_purefidata` string - `validatePayload(...)` does not revert - Your business logic executes after the verification call - Your contract emits its own success event - The subscription usage count in the Dashboard decrements as expected **Signs something is wrong:** - The issuer returns no data or an error — the compliance check failed; do not submit a transaction - `validatePayload(...)` reverts — the payload is invalid, stale, or was submitted to the wrong chain/contract - The transaction fails — check that your contract address is registered in the Dashboard and that you are using the correct verifier proxy address for Horizen mainnet --- ## Oracles (Stork) Stork is the primary oracle integration for Horizen Chain. It is a **pull oracle** that delivers price data and other off-chain data feeds at sub-second latency, designed for use cases like perpetuals, lending protocols, and any application that requires fast, verifiable market data. Unlike push oracles (which maintain feeds on-chain at all times), Stork operates on a **consumer-driven model**: feeds are not posted to the chain continuously. Instead, your application fetches the latest signed data off-chain and pushes it on-chain exactly when needed. This makes it highly cost-efficient: you only pay for the data updates your protocol actually uses. ## How Stork Works on Horizen ## Contract Addresses | Network | Stork Contract Address | |---|---| | Mainnet Horizen | [`0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62`](https://explorer.horizen.io/address/0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62) | | Testnet Horizen | [`0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62`](https://explorer-testnet.horizen.io/address/0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62) | Both mainnet and testnet share the same Stork contract address on Horizen. Source: [docs.stork.network/resources/contract-addresses/evm](https://docs.stork.network/resources/contract-addresses/evm). ## Integration Steps ### Step 1: Fetch Data via the Stork REST API Before pushing anything on-chain, fetch the latest signed price data from Stork's off-chain API. Each response contains a signed payload ready to be submitted directly to the on-chain contract. **API endpoint:** ``` GET https://rest.jp.stork-oracle.network/v1/prices/latest?assets= ``` Full REST API reference: [docs.stork.network/api-reference/rest-api](https://docs.stork.network/api-reference/rest-api) Available asset IDs (e.g. `BTCUSD`, `ETHUSD`) are listed in the [Stork Asset ID Registry](https://docs.stork.network/resources/asset-id-registry). ### Step 2: Push Data On-Chain Once you have the signed payload, submit it to the Stork contract on Horizen using `updateTemporalNumericValuesV1`. This verifies the aggregator signature and stores the price on-chain. ```solidity interface IStork { function updateTemporalNumericValuesV1( StorkStructs.TemporalNumericValueInput[] calldata updateData ) external payable; function getUpdateFeeV1( StorkStructs.TemporalNumericValueInput[] calldata updateData ) external view returns (uint feeAmount); } ``` **Important:** Always call `getUpdateFeeV1` first to determine the required fee, then pass that value as `msg.value` when calling `updateTemporalNumericValuesV1`. Submitting without sufficient fee will revert with `InsufficientFee`. ```solidity // Get required fee uint fee = stork.getUpdateFeeV1(updateData); // Push signed data on-chain stork.updateTemporalNumericValuesV1{value: fee}(updateData); ``` > **Tip:** If your protocol uses multiple price feeds, batch them in a single `updateTemporalNumericValuesV1` call. This saves gas and ensures all prices are updated atomically in the same block. ### Step 3: Read Data On-Chain Once a feed is updated on-chain, your smart contract can read it using `getTemporalNumericValueV1`. This function includes an automatic staleness check — it reverts with `StaleValue` if the stored price is older than the chain's configured freshness threshold. ```solidity interface IStork { function getTemporalNumericValueV1( bytes32 id ) external view returns (StorkStructs.TemporalNumericValue memory value); } struct TemporalNumericValue { // Nanosecond-precision Unix timestamp of the price update uint64 timestampNs; // Price scaled to 18 decimal places int192 quantizedValue; } ``` **Example — reading ETH/USD price in a Solidity contract:** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IStork { struct TemporalNumericValue { uint64 timestampNs; int192 quantizedValue; } function getTemporalNumericValueV1( bytes32 id ) external view returns (TemporalNumericValue memory value); } contract PriceConsumer { IStork public immutable stork; // ETHUSD feed ID (verify from Stork Asset Registry) bytes32 public constant ETH_USD_ID = 0x59102b37de83bdda9f38ac8254e596f0d9ac61d2035c07936675e87342817160; // Stork contract on Horizen mainnet: 0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62 constructor(address _stork) { stork = IStork(_stork); } function getEthPrice() external view returns (int192) { IStork.TemporalNumericValue memory val = stork.getTemporalNumericValueV1(ETH_USD_ID); return val.quantizedValue; // 18 decimal places } } ``` For view functions where you want to implement custom staleness logic, use `getTemporalNumericValueUnsafeV1` instead — it returns the stored value without reverting on staleness, allowing you to implement your own freshness checks. ### Reference Links | Resource | URL | |---|---| | Stork Documentation | `https://docs.stork.network` | | Asset ID Registry | `https://docs.stork.network/resources/asset-id-registry` | | REST API Reference | `https://docs.stork.network/api-reference/rest-api` | | EVM Contract API | `https://docs.stork.network/api-reference/contract-apis/evm` | | EVM SDK Example | `https://github.com/Stork-Oracle/stork-external/tree/main/chains/evm/examples/stork` | --- ## Block Explorer Horizen provides a full block explorer for both mainnet and testnet, powered by Caldera. | Network | Explorer URL | | --- | --- | | Mainnet | https://explorer.horizen.io/ | | Testnet | https://explorer-testnet.horizen.io/ | ## What you can do with the explorer - Search transactions by hash, block number, or address - View contract deployments and verified source code - Inspect token transfers (ERC-20, ERC-721, ERC-1155) - Monitor wallet balances and transaction history - Track internal transactions and event logs --- ## Faucet The testnet faucet provides free test ETH for development and testing on Horizen Testnet. Faucet URL: [https://hub-testnet.horizen.io/](https://hub-testnet.horizen.io/) ## To claim testnet ETH - Connect your wallet on the faucet page. - Ensure your wallet is set to Horizen Testnet (Chain ID: 2651420). - Submit a claim. The funds arrive within a few seconds. Testnet ETH has no real value and is only valid on the Horizen Testnet network. If you need additional testnet ETH beyond faucet limits, you can bridge Base Sepolia ETH to Horizen Testnet via the testnet bridge. --- ## Mainnet Configuration {/* import AddToWalletButton from '@site/src/components/AddToWalletButton'; */} Add the following network to your wallet or development environment to connect to Horizen Mainnet. ## Horizen Mainnet (Base) | Parameter | Value | | --- | --- | | Network Name | Horizen Mainnet | | Chain ID | 26514 | | RPC URL (HTTPS) | `https://horizen.calderachain.xyz/http` | | RPC URL (WebSocket) | `wss://horizen.calderachain.xyz/ws` | | Currency Symbol | ETH | | Block Explorer | https://explorer.horizen.io/ | | Mainnet Hub | https://hub.horizen.io/ | {/* */} ## Adding to MetaMask manually: 1. Open MetaMask → Networks → Add Network 2. Click Add a network manually 3. Fill in the values from the table above 4. Click Save - Horizen Mainnet is now available in your wallet --- ## Testnet Configuration The Horizen Testnet runs on Base Sepolia and is the recommended environment for all development and testing before deploying to mainnet. ## Horizen Testnet (Base Sepolia) | Parameter | Value | | --- | --- | | Network Name | Horizen Testnet | | Chain ID | 2651420 | | RPC URL (HTTPS) | `https://horizen-testnet.rpc.caldera.xyz/http` | | RPC URL (WebSocket) | `wss://horizen-testnet.rpc.caldera.xyz/ws` | | Currency Symbol | ETH | | Block Explorer | https://explorer-testnet.horizen.io/ | | Faucet | https://hub-testnet.horizen.io/| ## Adding to MetaMask manually: 1. Open MetaMask → Networks → Add Network 2. Click Add a network manually 3. Fill in the values from the table above 4. Click Save - Horizen Testnet is now available in your wallet --- ## What is Horizen Chain? **Horizen** is an EVM-compatible Layer-3 blockchain built on top of [Base](https://www.base.org/), an Ethereum Layer-2 powered by the OP (Optimism) Stack. Built for private onchain finance and compliance-forward blockchain app development, Horizen aims to bring regulatory-compliant, auditable confidentiality to where Ethereum's liquidity and activity already live. Unlike chains that require learning a new language or committing to a fixed privacy model, Horizen lets you ship standard Solidity and choose your confidentiality primitive per use case, or skip it entirely. There is no mandated privacy stack and nothing new to adopt before your first deployment. Horizen Chain is built on top of the Ethereum stack, inheriting Base's security, composability, and liquidity, while making confidential execution possible through a variety of privacy primitives and tools including [**Vela**](https://vela.horizenlabs.io/), the trusted execution environment (TEE) product from Horizen Labs, and [**zkVerify**](https://zkverify.io), the zero-knowledge proof verification and attestation protocol also by Horizen Labs. ## The result is a chain where: - Contracts can execute with confidentiality as outputs are cryptographically sealed and attested by app-level privacy integrations - Compliance and auditability are programmable at the application level (developers implement approaches such as viewing keys or verifiable audit logs) - Developers build with familiar EVM tooling (Hardhat, Foundry, ethers.js, wagmi, etc) - Liquidity moves freely between Horizen and Base via native and LayerZero bridging Horizen is the capital coordination and execution layer for a new class of privacy-first onchain applications spanning DeFi, payments, AI agents, and compliance-aware institutional finance - all composable with Base and the broader Ethereum ecosystem. --- ## The L3 Architecture Horizen Chain is an OP Stack rollup that settles directly onto Base, which in turn settles onto Ethereum. ## The Layered Model **Ethereum - Security & Final Settlement**: The root of trust. Ethereum provides the cryptographic finality that everything above it inherits. **Base - Scalable Execution & Data Availability**: Horizen's settlement surface. Transaction data and state commitments from Horizen are published directly to Base's native data-availability layer. **Horizen Chain - Capital Coordination & Execution**: An EVM-compatible rollup using the OP Stack. Horizen inherits Base's scalability and sequencing infrastructure, with opt-in confidential execution available through [Vela](https://vela.horizenlabs.io/). **Vela - Confidential Coprocessor**: An emerging confidential coprocessor by Horizen Labs that sits alongside Horizen Chain. Applications offload sensitive computation to TEE enclaves, receive cryptographically attested results back, and anchor those results on-chain. --- ## Horizen's Architecture Horizen is an EVM-compatible L3 built on [Base](https://www.base.org/) using the [OP Stack](https://docs.optimism.io/stack/getting-started) - an open-source, modular rollup framework from the Optimism Collective. By settling on Base, Horizen inherits Ethereum's security and Base's low fees and high throughput while execution on Horizen adds confidentiality and compliance features integrated at the application level. The OP Stack architecture is composed of modular layers: Data Availability (DA), Sequencing, Derivation, Execution, and Settlement. These layers work together to form a cohesive optimistic rollup. Below, we detail the primary components relevant to Horizen's implementation. ## Sequencer The Sequencer is the central actor responsible for ordering and processing user transactions on Horizen. A dedicated node collects transactions from users, executes them locally to compute the new state, and prepares batches for submission. ### Functionality: - Aggregates transactions into blocks. - Computes the resulting state transitions using the Execution Engine. - Compresses transaction data for efficiency. - Publishes batches to the Data Availability layer. - Ensures soft finality on Horizen, where transactions are considered "safe" shortly after inclusion but require settlement on Base for full finalization. ## Batcher The Batcher is a specialized component that handles the compression and submission of transaction batches to the DA layer. ### Functionality: - Collects sequenced transactions and compresses them using techniques like zlib or custom OP Stack compression to reduce gas costs. - Submits batches as calldata to the Batch Inbox contract on Base L2. - Manages channel framing: Batches are grouped into channels, which are submitted when full or timed out. - Supports EIP-4844 blob transactions if configured, further reducing costs by offloading data to blobs on Ethereum L1 (via Base). ## Proposer The Proposer submits commitments to Horizen's state (output roots) to the settlement layer on Base. ### Functionality: - Monitors the Sequencer's output and derives the L3 state root after batch processing. - Submits proposals to the L2OutputOracle contract on Base. - Proposals include the output root, block number, and other metadata. - Only "safe" (finalized on Horizen) blocks are proposed, with submissions triggered by withdrawals or periodic intervals. ## Derivation Layer The Derivation layer processes raw data from the DA layer to generate inputs for the Execution Engine. ### Functionality: - Fetches batches and deposit events from Base (e.g., via the Batch Inbox and Deposit contracts). - Reconstructs the transaction list and applies it to the current state. - Handles L2 (Base) attributes, such as gas fees and block metadata. - Ensures the chain derives correctly from Base blocks, maintaining synchronization. ## Rollup Module The core of the derivation pipeline, responsible for parsing sequencer batches and L2-originated deposits (e.g., bridge transactions from Base to Horizen). The derivation pipeline feeds into the Engine API, allowing any node to independently sync and verify the chain by replaying data from Base. ## Execution Engine Horizen uses a near-vanilla Ethereum Virtual Machine (EVM) for state transitions. It processes derived inputs to execute transactions and update the state trie. ### Functionality: - Supports all Ethereum opcodes, with minor OP Stack extensions (e.g., L2 data fee for batches) - Maintains full compatibility with Ethereum tools and smart contracts ## Fault Proofs and Settlement Settlement verifies and finalizes Horizen's state on Base, enabling secure withdrawals and cross-chain interactions. ### Optimistic Model - State proposals from the Proposer are assumed correct unless challenged during the dispute window (7 days in standard configuration). - **Fault Proof System**: Initial proposals can be disputed by a multisig of trusted parties. If a threshold attests to an invalid state, the proposal is rejected. - Anyone can submit a fault proof using on-chain verification games. This involves interactive disputes resolved by bisecting state transitions until the fault is pinpointed. ## Withdrawal Process - Users initiate withdrawals on Horizen, which are included in batches. - After proposal and the challenge window, funds are released on Base via the OptimismPortal contract. - **Security**: Relies on the economic incentive for honest challengers and the immutability of DA on Base. ## Understanding the workflow - **Transaction Submission**: Users send transactions to the Horizen Sequencer (via RPC endpoints). - **Sequencing and Batching**: The Sequencer orders transactions, executes them locally, and passes data to the Batcher. The Batcher compresses and submits batches to Base (as calldata or blobs). - **Data Availability**: Batches are recorded on Ethereum blobs, making them immutable and retrievable. - **Derivation**: Full nodes (including verifiers) fetch data from Base, derive the transaction list, and feed it to the Execution Engine to update the state. - **Proposal**: The Proposer submits the computed output root to the L2OutputOracle on Base. - **Settlement and Challenges**: The proposal enters a 7-day challenge window. If no valid dispute, the state is finalized. Disputes trigger fault proof games. - **Withdrawals**: For cross-layer transfers, users prove message inclusion after finalization, claiming funds on Base. --- ## Privacy Tools Horizen gives you two complementary confidentiality primitives that operate at different layers, in addition to supporting any other EVM-compatible privacy implementation integrated by developers at the app level. Vela keeps inputs and computation private during execution using a TEE-based approach. zkVerify uses zero-knowledge proofs to prove that computation ran correctly and posts attestations onchain for dApp interoperability. These tools can be used independently or together — what you adopt depends on what your application needs to keep private and how it needs to demonstrate correctness. ## Vela - Confidential Execution [Vela](https://vela.horizenlabs.io/) is a TEE-based confidential execution solution by Horizen Labs. Application logic runs inside TEE hardware enclaves where data is encrypted in memory, inaccessible to any external observer including the host machine and cloud provider. Every computation produces a cryptographic attestation proving the code ran correctly inside a genuine enclave, without revealing the underlying data or intermediate state. **Use Vela when:** - Your application processes sensitive inputs that must stay private during computation — confidential balances, sealed bids, private order books, encrypted AI inference - You need cryptographic proof that specific logic executed correctly without exposing what it executed on - Compliance or audit requirements demand verifiable rule enforcement without raw data disclosure → [What is Vela?](/vela/introduction) ## zkVerify - On-Chain ZK Proof Verification zkVerify is a purpose-built L1 for verifying ZK proofs — a separate protocol that integrates with Horizen and any other EVM-compatible chain. Applications generate proofs off-chain using a ZK proving system, submit them to zkVerify, and receive on-chain verification results consumable by Horizen contracts, without deploying a custom verifier contract or paying native chain gas costs for proof verification. **Use zkVerify when:** - Your application already generates ZK proofs (from a ZK circuit, prover library, or ZK rollup) and needs cheap, fast on-chain verification - You want to avoid deploying and maintaining chain-specific verifier contracts - You need to aggregate or batch proof verification across multiple applications → [zkVerify Documentation](https://docs.zkverify.io) --- ## cbBTC on Horizen ## What is cbBTC? cbBTC is **Coinbase Wrapped BTC** — an ERC-20 token issued by Coinbase, backed 1:1 by Bitcoin held in Coinbase's custody. It brings Bitcoin liquidity to EVM chains, making BTC usable in smart contracts, DeFi protocols, and any application that accepts ERC-20 tokens. cbBTC on Horizen is bridged from Base via the **LayerZero OFT (Omnichain Fungible Token) standard**. When cbBTC is bridged to Horizen, the cbBTC on Base is locked in the OFT Adapter contract and an equivalent amount is minted on Horizen. When bridging back, the Horizen cbBTC is burned and the corresponding amount is released on Base. ## Contract Addresses ### Mainnet | Network | Type | Address | | --- | --- | --- | | Base | cbBTC ERC-20 (Coinbase) | [`0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf`](https://basescan.org/token/0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf) | | Base | OFT Adapter (LayerZero) | [`0x68fb5BB8330C0b9d907F50f278143873276ee056`](https://basescan.org/address/0x68fb5BB8330C0b9d907F50f278143873276ee056) | | Horizen | cbBTC ERC-20 / OFT | [`0x68fb5BB8330C0b9d907F50f278143873276ee056`](https://explorer.horizen.io/token/0x68fb5BB8330C0b9d907F50f278143873276ee056) | ### Testnet | Network | Type | Address | | --- | --- | --- | | Base Sepolia | cbBTC ERC-20 | [`0xcbb7c0006f23900c38eb856149f799620fcb8a4a`](https://sepolia.basescan.org/token/0xcbb7c0006f23900c38eb856149f799620fcb8a4a) | | Base Sepolia | OFT Adapter (LayerZero) | [`0x5dE29d14E72feb79967596F3Ae57A9BfBA192769`](https://sepolia.basescan.org/address/0x5dE29d14E72feb79967596F3Ae57A9BfBA192769) | | Horizen Testnet | cbBTC ERC-20 / OFT | [`0x06DA6bDD2aB23447af5162ab0975edDA7E8d3747`](https://explorer.horizen.io/token/0x06DA6bDD2aB23447af5162ab0975edDA7E8d3747) | :::note On Horizen, the ERC-20 and OFT contract are the **same address** — `0x68fb5BB8330C0b9d907F50f278143873276ee056` on mainnet and `0x06DA6bDD2aB23447af5162ab0975edDA7E8d3747` on testnet. Use these addresses for all on-chain interactions including balance queries, transfers, and approvals. ::: ## Trust Model & Custody cbBTC's backing is fully custodial. The BTC underlying every cbBTC token is held by Coinbase in secure custody — not in a decentralised smart contract. Coinbase issues, mints, and burns cbBTC, and provides regular attestations confirming 1:1 BTC reserves. ## Integrating cbBTC in your dApp cbBTC is a standard ERC-20 token and is fully compatible with all EVM tooling on Horizen. There is one critical integration detail that differs from most ERC-20 tokens: :::warning Critical — Decimals cbBTC uses **8 decimals**, matching Bitcoin's native precision — not the 18 decimals used by ETH and most ERC-20 tokens. Any arithmetic, pricing, or display logic that assumes 18 decimals will produce completely wrong results. Always use `8` explicitly or read `decimals()` from the contract. ::: ### Reading cbBTC balance (ethers.js) ```javascript import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider( "https://horizen.calderachain.xyz/http" ); const CBBTC_ADDRESS = "0x68fb5BB8330C0b9d907F50f278143873276ee056"; const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", ]; const cbbtc = new ethers.Contract(CBBTC_ADDRESS, ERC20_ABI, provider); const balance = await cbbtc.balanceOf("0xYourAddress"); const decimals = await cbbtc.decimals(); // 8, not 18 const formatted = ethers.formatUnits(balance, decimals); console.log(`cbBTC balance: ${formatted}`); ``` ### Using cbBTC in a Solidity contract ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IERC20 { function balanceOf(address account) external view returns (uint256); function transferFrom( address from, address to, uint256 amount ) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function decimals() external view returns (uint8); } contract BTCCollateral { // cbBTC on Horizen Mainnet — 8 decimals IERC20 public constant CBBTC = IERC20(0x68fb5BB8330C0b9d907F50f278143873276ee056); // 1 cbBTC = 1e8 base units (8 decimals, not 1e18) uint256 public constant ONE_CBBTC = 1e8; function deposit(uint256 amount) external { // amount is in cbBTC base units (8 decimals) // e.g. 0.5 BTC = 50_000_000 require(amount >= ONE_CBBTC / 100, "Minimum 0.01 cbBTC"); CBBTC.transferFrom(msg.sender, address(this), amount); } function getBalance(address user) external view returns (uint256) { // Returns amount in 8 decimal base units return CBBTC.balanceOf(user); } } ``` ## Bridging cbBTC For step-by-step instructions on bridging cbBTC from Base to Horizen and back, see the [Bridge Assets](/horizen-chain/bridging/bridge-assets) section. --- ## Gas on Horizen (ETH) Gas on Horizen Chain is paid in ETH, which is the same as on Base and Ethereum mainnet. There is no separate gas token. If you've deployed on Base, the fee mechanics on Horizen are identical. ## How gas fees work on an L3 Every transaction on Horizen has two fee components: - **L3 execution fee** — the cost of running your transaction on Horizen itself. Calculated the same way as any EVM chain: `gas used × gas price`. - **L1 data fee** — a small additional fee covering the cost of publishing your transaction data to Base. The OP Stack calculates and appends this automatically — you do not set it manually. It appears as a separate line item in the transaction receipt. In practice, fees on Horizen are very low. As an L3 on Base, Horizen benefits from Base's already-low data costs, with the L1 data fee typically being a small fraction of the total transaction cost. ## Getting ETH on Horizen 1. **Testnet:** Use the faucet at [https://hub-testnet.horizen.io/](https://hub-testnet.horizen.io/). 2. **Mainnet:** Bridge ETH from Base to Horizen via [https://hub.horizen.io/](https://hub.horizen.io/). --- ## USDC on Horizen ## What is USDC.e? USDC.e is the bridged form of USDC available on Horizen Chain. It is deployed via **Stargate Hydra** — Stargate's Bridging-as-a-Service model- and backed 1:1 by USDC locked in Stargate's liquidity pool on Base. When a user bridges USDC from any Stargate-connected chain to Horizen, their USDC is locked in Stargate's pool on the origin chain and an equivalent amount of USDC.e is minted on Horizen. When leaving Horizen, USDC.e is burned and native USDC is released on any Stargate-supported destination chain — not necessarily Base. The `.e` suffix denotes that this is a bridged, pre-native representation of USDC. It is not issued by Circle, is not directly redeemable through Circle, and does not have access to Circle products such as Circle Mint or CCTP. These are properties of native USDC. USDC.e's value is entirely backed by USDC locked in Stargate's pool. ## Circle's Bridged USDC Standard USDC.e on Horizen is deployed in conformance with **[Circle's Bridged USDC Standard](https://www.circle.com/bridged-usdc)** — Circle's official specification for deploying bridged USDC on EVM chains in a way that preserves a defined, seamless upgrade path to native issuance. The standard requires the bridged USDC contract to be deployed with bytecode identical to Circle's native USDC contracts on other EVM chains. This allows Circle to trustlessly verify the contract, and if both Horizen and Circle agree, take ownership of the contract and upgrade it to native USDC in place. **What this means practically:** - The **contract address does not change** on upgrade to native USDC. Any dApp integrating `0xDF7108f8B10F9b9eC1aba01CCa057268cbf86B6c` today will automatically be using native USDC after an upgrade — zero code changes, zero liquidity migration. - **Existing holders require no action.** All supply, holders, and on-chain integrations are retained exactly as-is through the upgrade. - **The upgrade is Circle's option, not a guarantee.** Circle evaluates supply size, growth rate, number of holders, and supported applications when prioritising native issuance for any chain. :::note This is the same upgrade path that World Chain recently completed — USDC.e was upgraded in-place to native USDC with the contract address unchanged and no developer action required. ::: ## Contract Addresses :::warning There are no official testnet USDC.e addresses for Horizen at this time. USDC.e is available on **mainnet only**. ::: | Network | Type | Address | | --- | --- | --- | | Mainnet Base | USDC ERC-20 (Circle) | [`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`](https://basescan.org/token/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) | | Mainnet Base | Stargate Pool / Lock Contract | [`0x27a16dc786820B16E5c9028b75B99F6f604b5d26`](https://basescan.org/address/0x27a16dc786820B16E5c9028b75B99F6f604b5d26) | | Mainnet Horizen | **USDC.e ERC-20** | [`0xDF7108f8B10F9b9eC1aba01CCa057268cbf86B6c`](https://explorer.horizen.io/token/0xDF7108f8B10F9b9eC1aba01CCa057268cbf86B6c) | | Mainnet Horizen | OFT Contract (LayerZero) | [`0x3a1293Bdb83bBbDd5Ebf4fAc96605aD2021BbC0f`](https://explorer.horizen.io/address/0x3a1293Bdb83bBbDd5Ebf4fAc96605aD2021BbC0f) | :::note The ERC-20 contract (`0xDF7108...`) and the OFT contract (`0x3a1293...`) are **separate addresses** on Horizen — unlike ZEN and cbBTC where they share the same address. When integrating USDC.e into your dApp, always use the **ERC-20 address**. The OFT contract is the bridge mechanism, not the token itself. ::: ## Integrating USDC.e in your dApp USDC.e is a standard ERC-20 token and is fully compatible with all EVM tooling, DeFi primitives, and wallet interfaces on Horizen. :::warning Critical — Decimals USDC.e uses **6 decimals**, not 18. This is consistent with USDC on all other EVM chains. Any arithmetic, pricing, or display logic that assumes 18 decimals will produce completely wrong results. Always use `6` explicitly or read `decimals()` from the contract. ::: ### Reading USDC.e balance (ethers.js) ```javascript import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider( "https://horizen.calderachain.xyz/http" ); const USDC_E_ADDRESS = "0xDF7108f8B10F9b9eC1aba01CCa057268cbf86B6c"; const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", ]; const usdce = new ethers.Contract(USDC_E_ADDRESS, ERC20_ABI, provider); const balance = await usdce.balanceOf("0xYourAddress"); const decimals = await usdce.decimals(); // 6 const formatted = ethers.formatUnits(balance, decimals); console.log(`USDC.e balance: ${formatted}`); ``` ### Using USDC.e in a Solidity contract ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IERC20 { function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function transferFrom( address from, address to, uint256 amount ) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function decimals() external view returns (uint8); } contract MyHorizenApp { // USDC.e on Horizen Mainnet — 6 decimals IERC20 public constant USDC_E = IERC20(0xDF7108f8B10F9b9eC1aba01CCa057268cbf86B6c); // 1 USDC.e = 1_000_000 base units (6 decimals, not 1e18) uint256 public constant ONE_USDC_E = 1_000_000; function getUsdcBalance(address user) external view returns (uint256) { // Returns amount in 6 decimal base units return USDC_E.balanceOf(user); } function pay(address recipient, uint256 usdcAmount) external { // usdcAmount in base units e.g. 5 USDC = 5_000_000 require(usdcAmount >= ONE_USDC_E, "Minimum 1 USDC.e"); USDC_E.transferFrom(msg.sender, recipient, usdcAmount); } } ``` ## Bridging USDC.e For step-by-step instructions on bridging USDC from Base to Horizen and back, see the [Bridge Assets](/horizen-chain/bridging/bridge-assets) section. --- ## ZEN Token ZEN is the coordination, value-routing, and governance token of the Horizen ecosystem. It is a standard ERC-20 token that lives on Base and bridges to Horizen Chain via LayerZero's OFT (Omnichain Fungible Token) framework. Note: ZEN is not used to pay gas fees. ## Contract Addresses ### ZEN (Mainnet) | Network | Contract | Address | | --- | --- | --- | | Mainnet Base | ERC-20 | [0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229](https://basescan.org/address/0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229) | | Mainnet Base | OFT Adapter | [0x57da2D504bf8b83Ef304759d9f2648522D7a9280](https://basescan.org/address/0x57da2D504bf8b83Ef304759d9f2648522D7a9280) | | Mainnet Horizen | ERC-20 | [0x57da2D504bf8b83Ef304759d9f2648522D7a9280](https://explorer.horizen.io/token/0x57da2D504bf8b83Ef304759d9f2648522D7a9280) | | Mainnet Horizen | OFT | [0x57da2D504bf8b83Ef304759d9f2648522D7a9280](https://explorer.horizen.io/token/0x57da2D504bf8b83Ef304759d9f2648522D7a9280) | ### tZEN (Testnet) | Network | Contract | Address | | --- | --- | --- | | Testnet Base | ERC-20 | [0x107fdE93838e3404934877935993782F977324BB](https://sepolia.basescan.org/address/0x107fdE93838e3404934877935993782F977324BB) | | Testnet Base | OFT Adapter | [0x2ead4B0beBD8e54F9B7cC1007DF4c44a27b9a339](https://sepolia.basescan.org/address/0x2ead4B0beBD8e54F9B7cC1007DF4c44a27b9a339) | | Testnet Horizen | ERC-20 | [0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87](https://explorer.horizen.io/token/0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87) | | Testnet Horizen | OFT | [0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87](https://explorer.horizen.io/token/0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87) | --- ## Horizen Hub ## Horizen Links | Product | Link | | --- | --- | | Github | https://github.com/HorizenOfficial/horizen | | Horizen Block Explorer | https://explorer.horizen.io/ | | Horizen Mainnet Hub | https://hub.horizen.io/ | | Horizen Testnet Hub | https://hub-testnet.horizen.io/ | | Monitoring | todo | | Documentation | https://docs.horizen.io | ## Recommended Wallets | Wallet | Link | | --- | --- | | Metamask | https://metamask.io/ | --- ## Migration overview Horizen migrated all the $ZEN balances from both the old Horizen mainchain and the EON EVM chain to an [ERC-20 smart contract](https://basescan.org/address/0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229) on Base (Ethereum L2 Rollup). Both old chains will be discontinued, and all coin transfers are now managed on Base, via ERC-20 smart contract calls. **Migration was successfully completed on July 23, 2025** ## What has been migrated? - **EON** — All EOA (Externally owned accounts) balances have been automatically migrated to the same address (EON and Base share the same address format, and users can use the same wallet keys). The amounts staked by forgers or delegators have also been migrated and moved to the same address in the new chain. Smart contracts, ZEN balances locked in smart contracts and stakes delegated by smart contracts have **not** been migrated. - **ZEND Mainchain** — The migration covered all $ZEN funds locked in UTXOs of type PayToPubKeyHash (single address) or PayToScriptHash Multisig (multisig address). Note: we estimated that 99% of current UTXOs are part of these groups. A simple manual claim of the funds will be required because the address format on the two chains is different (Bitcoin-format in the old Horizen chain, Ethereum format on Base). An automatic migration like the one from EON has not been performed here, because the on-chain UTXO structure does not track the original key/address owning it, making it impossible to map between old and new addresses. ## Overview of the process 1. A migration point has been fixed on both old chains. When reached, Horizen Labs performed dumps of all the relevant data on both chains. 2. A set of smart contracts have been deployed on BASE to handle the restore: - The official [ERC-20 smart contract](https://basescan.org/token/0xf43eb8de897fbc7f2502483b2bef7bb9ea179229) - An [EONBackupVault](https://basescan.org/address/0x1Cc689233837A0b96e1f176d49FC08462f70C47F), used to store the EON balances and automatically mint them to BASE - A [ZENDBackupVault](https://basescan.org/address/0x1Ee188bDf19eBF04B73Ab6FFcec2a864cd4774F2), used to store the ZEND balances and expose methods for manual claiming Horizen Labs was responsible for deploying the contracts and loading the dump data. 3. A migration check procedure will allow third parties to challenge the fairness of the loaded data. 4. ZEND owners will be able to claim funds via on-chain calls to the ZENDBackupVault contract. The next sections of this website will detail all these steps. --- ## Preparing old chain nodes A proper version of ZEND and EON has been released to support the migration. In particular: - **EON 1.5** - Hardfork configured to stop backward transfers to the mainchain - Dump support via command-line - **ZEND 6** - Hardfork configured to stop forward transfers to EON and sidechain creation - Stop of users transactions mining --- ## Migration starting points The activation of the EON 1.5 hard-fork marked the start of the migration process. As usual with EON, the hard-fork has been triggered at a specific consensus epoch, with millisecond precision. ZEND also activated a hard-fork at a specific height. ## Final block hash determination The rules below uniquely identify the final block hash of both chains: this marks the block at which the balances have been migrated, and **any transaction recorded after this will have no value**. - For ZEND Mainchain, the blockhash at the hardfork height has been marked as the final block hash. - For EON, the block including the reference to the above mainchain block has been considered the final block. Before starting the migration process, both of them have been confirmed by *more than 100 following blocks* on mainchain, making *infeasebale* a block revert before the migration point. Here are the final confirmed hashes: | | | | -------- | ------- | | **ZEND Mainchain final block hash:** | 000000000059963d5021a9c29167878916e476a249ca988dd828bac4a8a3351a | | **ZEND Mainchain final block height:** | 1807300 | | **EON final block hash:** | d3e837c2939917f8a676f9a4b626c1024718636740732db05fc6de811a8e32aa | | **EON final block height:** | 3573401 | ## Useful commands to get the block hashes ### For ZEND To obtain the hash of the block at a specific height: ```bash zen-cli getblockhash ``` In case of testnet, the command is: ```bash zen-cli -testnet getblockhash ``` ### For EON To obtain the block that references a specific ZEND block by height: ```bash curl -sX POST 'http://127.0.0.1:9085/mainchain/blockReferenceInfoBy' -H 'Content-Type: application/json' -H 'accept: application/json' -d '{"height":1654690, "format": true}' ``` The result will be in this format: - The field *mainchainHeaderSidechainBlockId* is the EON block hash referencing the mainchain block. - The field *hash* is the ZEND hash (double check it is equals to the ZEND getblockhash result) ```json { "result" : { "blockReferenceInfo" : { "mainchainHeaderSidechainBlockId" : "ae4cea03e6920679775e57236f27dc541ad900d9741bb2b71a46074748ff3062", "mainchainReferenceDataSidechainBlockId" : "ae4cea03e6920679775e57236f27dc541ad900d9741bb2b71a46074748ff3062", "hash" : "000218ca034fc86b54b2417a376656c90a5ee7e5412d015a588758f8dd521d3c", "parentHash" : "0003199e4fe1db486924ceaa8325a2a3884a894276632aa7a36dbf5b8e46332e", "height" : 1654690 } } } ``` --- ## Dump execution In the previous step, the network has reached the migration heights, and the final block hashes/heights of both chains are now revealed. This section describes how the data, which includes all the balances to be migrated, was generated. We will need to interact with a node of each chain: - Fully synced ZEND Mainchain node - Fully synced EON Chain node with dump support enabled: - To enable dump support, the following fragment must be present in the config file (*important*: to generate a valid state dump, the fragment must be added *BEFORE* starting to sync the chain): ```text evmStateDump { enabled = true } ``` - If using Docker and the Docker image zencash/evmapp:1.5.0 , you can configure the following env property: ```bash SCNODE_EVM_STATE_DUMP_ENABLED=true ``` This will set automatically the previous property in the container. ## How to obtain the dump data 1. Execute a dump of ZEND balances at that specified height. ZEND is shipped with a dumper command line utility to do this: ```bash zen-cli stop dumper -H MC_MIGRATION_HEIGHT > utxos.csv ``` Please note the following: - The stop command is needed because ZEND must not be running while performing the dump - If executing a dump of the testnet you must add the flag: -t - MC_MIGRATION_HEIGHT must be **not too old in the past** compared to the latest tip: maximum supported height is **tip-100** 2. Execute a dump of EON State. Execute the following call on the EON node: ```bash curl --request POST 'http://127.0.0.1:9085/ethv1' -H 'Content-Type: application/json' -H 'accept: application/json' -d '{ "jsonrpc":"2.0", "method":"zen_dump", "params":["0xbda76ab769c4e158f8e8add81bdf17c9d919fb54cd5e32f1c83cebdfc3dc363c","/zendata/eon.dump"], "id":1 }' ``` - First parameter of the method must be replaced by EON_MIGRATION_HASH - Second parameter is the local-path of the output dump. ## How to create the restore artifacts 3. Download and follow the README instructions of [this folder](https://github.com/HorizenOfficial/horizen-migration/tree/dev/dump-scripts) to execute the *create_restore_artifacts.sh* bash script. The script will process the following data: - two previous full dumps - the list of the staked ZEN at the specific dump height, obtained by querying a running EON node - a list of ["auto mapped" addresses](https://github.com/HorizenOfficial/horizen-migration/tree/dev/dump-scripts/automappings): they are hardcoded mappings that comes from offchain agreements between selected partners (centralized exchanges) and HorizenLabs. The script will perform the following: - For ZEND: - transform the addresses in Base58 decoded format (is easier to handle in the solidity code), without chain prefix - transform the balances in "wei" format (1 ZEN = 1 with 18 zeros) - exclude the automapped addresses - order the addresses alphabetically - For EON: - filters out the smart contracts addresses and the stakes belonging to smart contracts - merge the EON staked ZEN to EOA balances - filters out addresses with 0 balance and no stakes - filters out the 0x0000000000000000000000000000000000000000 account - transforms the balances in "wei" format (1 ZEN = 1 with 18 zeros) - include the automapped addresses - orders the addresses alphabetically The result of the script will be two restore artifacts: - a *zend.json* file, containing a key-value json data structure like this: ```json { "0xabf1FF91cECD9990B3f29363B62B87FD76f55F4A": 10001500000000000000000, "0x448ae34180D03AD7da48975d6Fd7B297bb871E26": 2082100000000000000, "0x144e0FE5e69893577107a15a7c76bABd59f0A279": 100000000000000000 } ``` The *keys* represent the ZEND address in a Base58check decoded format, without the first 2 bytes chain prefix (so 20 bytes in total), prepended with 0x. The *values* represent the ZEND balance, in "wei format" (1 ZEN = 1 with 18 zeros). - an *eon.json* file, containing a key-value json data structure like this: ```json { "0xBa2290AEaAE3e1ea336431911C97a67Ebff46528": 1500000000000000000, "0xFEB3DE3D4A6F49bbF643c44E64dfd3e46D3E0F04": 821003000000000000, "0x2a085Ca4E931938Aa383C88026b0566cFce1A34b": 45500000000000000 } ``` The *keys* represent the EON address in the hex form with “0x” prefix. The *values* represent the EON balance, in “wei format” (1 ZEN = 1 with 18 zeros). These artifacts will be the ones used for the data loading and migration check steps. ## Final restore artifacts For transparency, the restore artifacs are also available on Github at the following url: [https://github.com/HorizenOfficial/horizen-migration/tree/main/snapshots/mainnet](https://github.com/HorizenOfficial/horizen-migration/tree/main/snapshots/mainnet) **Everyone is encouraged to verify and confirm this data**, by adding a signature on the signature/ subfolder: [see here for more detailed instructions on how to participate](https://github.com/HorizenOfficial/horizen-migration/blob/main/snapshots/README.md). --- ## Data loading In this step all the balances obtained in the previous dump step have been loaded into the vault smart contracts. This operation has been performed by firing a batch of transactions, by an authorized Horizen admin (its address was whitelisted in the vault contracts). Detailed instructions are in the [README.md](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/README.md) file of the **horizen-migration** Git repository, together with the scripts to be executed (they use the Hardhat framework). ## Migration data cumulative hash The concept of "cumulative hash" is used as a "fingerprint" of the dump data. Assuming we have a list of dump tuples composed by [address,balance], ordered by address, we define it with the following pseudo-code rule: ```text cumulative_hash = "0x0000000000000000000000000000000000000000000000000000000000000000" for each dump tuple: overall_hash = keccak-256-hash(overall_hash, tuple.address, tuple.value) ``` In the data loading process, it has been: - first calculated off-chain - fed into the smart contract - Inside the solidity code: - recalculated during the batch data loading - compared with the initial one, to check the correctness of the loading and to receive confirmation that the loading has been completed (claiming methods will be enabled only at this point) Furthermore, in the [migration check step](./06-migration-check.md), the same hash can be recomputed off-chain by any third-party, and compared with the one stored in the smart contract. --- ## Migration check The data loading process has been successfully completed by Horizen, but anyone can check that the migrated data correctly reflects the old chains state by following the steps described in this section. ## How it works We have already described in previous sections the concept of dumps, migration data, and cumulative hash. The verification process will require to take a new dump, recalculating the hash locally with the same algorithm, and compare it with the one stored in the vault smart contracts. ### Instructions 1. Execute the dumps and create the restore artifacts with the procedure already [described here](./04-dump-execution.md) (you will need a fully-synched mainchain node and a fully-synched EON node). Alternatively, you can download the certified artifacts from here: [https://github.com/HorizenOfficial/horizen-migration/tree/main/snapshots/mainnet](https://github.com/HorizenOfficial/horizen-migration/tree/main/snapshots/mainnet) 2. Download the Github repository [horizen-migration-check](https://github.com/HorizenOfficial/horizen-migration-check) and follow the README instructions to recalculate the hash from the restore artifacts and compare it with the on-chain one. --- ## Claiming mainchain ZEN This section covers the steps to follow to claim old mainchain ZEN balances after the migration. There is no time window in place for claiming tokens. ## Prerequisites Before going through the claim process you will need to have some ETH on Base L2 to process the claim. Ensure you have at least **0.000004 ETH per claim** to cover the necessary transaction costs. If you have multiple claims, the total amount in your wallet must be sufficient to cover all of them. ### Option 1: Use a Centralized Exchange The most straightforward way to get ETH on Base is by using a centralized exchange like Coinbase or Binance. 1. Buy ETH on your preferred exchange. 2. Withdraw it to your Base wallet address (e.g., MetaMask). - If using Coinbase or Binance, select "Base" as the destination network. - For other exchanges, you may need to first withdraw to Ethereum and then bridge to Base using a third-party bridge (see Option 2). ### Option 2: Bridge ETH from Ethereum or Other Chains If your exchange doesn't support direct Base withdrawals, or if you already have ETH on another network (like Ethereum Mainnet, Arbitrum, or Optimism), you can bridge ETH to Base using [Superbridge](https://superbridge.app/base). 1. Connect your wallet (e.g., MetaMask). 2. Choose the source network and Base as the destination. 3. Enter the amount and confirm the bridge transaction. 4. ETH will typically arrive on Base within a few minutes. ### Option 3: Buy ETH Using Credit Card Some services allow you to buy ETH directly on Base using a debit card, credit card, or other local payment methods. Popular onramps include [MoonPay](https://www.moonpay.com/) or [Onramp](https://onramp.money/). Note that these services usually require a minimum purchase of $20-$25. Once ETH is obtained, bridge it to Base using Option 2. To confirm that you've received ETH on Base, check your wallet or visit https://basescan.org. ## Who needs to execute the manual claim? Only ZEN balances coming from the old ZEND Mainchain will need to be manually claimed. If you only had funds on EON Chain, your balances will be automatically moved to the same address in the new chain, and no manual operation will be needed. ## Why is the manual claim needed? A simple manual claim of the funds is required because the address format on the two chains is different (Bitcoin-format in the old Horizen chain, Ethereum format in the new Horizen chain). The on-chain UTXO structure does not track the original key/address owning it, making it impossible to automatically map between old and new addresses. ## How to claim The old mainchain is a Bitcoin-like chain, where funds are locked in multiple cryptographic "boxes" called UTXO. To unlock funds, you will need to generate a signature of a specific message with the same private key able to "unlock" the corresponding UTXOs. Two additional methods are developed to allow a "direct" claim that doesn't need any signed message. The claim will then be performed on-chain, by calling a method on the official Horizen migration contract. Here the details about the method call: ### Pay-to-pub-key hash Claim This is the most common format of UTXO, used for example by the Horizen Sphere Wallet and many other non-custodial wallets. The Solidity method to claim this kind of UTXOs is the following: ```solidity function claimP2PKH(address destAddress, bytes memory hexSignature, PubKey calldata pubKey) public ``` #### Parameters - **destAddress** — Destination address on Base of the funds to be claimed. This can be generated by any keypair (using the same previous private-key is not mandatory), and can be different from the tx caller. - **hexSignature** — ECDSA/Secp256k1 Signature generated with the private key associated with the UTXOs to claim, of the following message: `”ZENCLAIM” + destAddress` (destAddress here is represented in the hex form according to EIP-55, with “0x” prefix. No space has to be inserted after “ZENCLAIM” string). - **pubKey** — Public key associated to the same private key used for the signature and owning the UTXOs. Must be always sent in uncompressed form: the PubKey data structure is composed of two bytes32 fields, that represents two components x and y (first 32 bytes and second 32 bytes). #### Events emitted After a successful claim the following event will be emitted: ```solidity event Claimed(address destAddress, bytes20 zenAddress, uint256 amount) ``` ### Pay-to-script-hash Multisig Claim This kind of UTXO is associated to multi-signature wallets. Note: we do not support any other type of Pay-to-script-hash different from the Multisig one. The Solidity method to claim this kind of UTXOs is the following: ```solidity function claimP2SH(address destAddress, bytes[] memory hexSignatures, bytes memory script, PubKey[] calldata pubKeys) ``` #### Parameters - **destAddress** — Destination address on Base of the funds to be claimed. This can be generated by any keypair (using the same previous private key is not mandatory), and can be different from the tx caller. - **hexSignatures[]** — List of ZEND ECDSA/Secp256k1 Signatures generated with the private keys associated to the multisig address, of the following message: `”ZENCLAIM” + zen_multisig_address + destAddress` - zen_multisig_address: hex representation of zen multisig address derived from this script (base58check decoded representation without chain prefix (leading 2 bytes removed, so in total 20 bytes) prepended with “0x” prefix) - destAddress: destination address represented in the hex form according to EIP-55, with “0x” prefix. - No space has to be inserted between the three parts of the string. - The minimum amount of valid signatures defined in the multisig script must be satisfied (example: 3 out of 5). - The list size has always to be equal to the total number of signatures accepted by the script, and in case a signature is not present the corresponding element should be set to a 0 bytes array. - For example: if the scripts accepts 2 out of 3 signatures, and we have only A and C signatures, the hexSignatures list will be: `[SigA, 0, SigC]` - **script** — Full UTXO redeem script. - **pubKeys** — List of public keys accepted by the script. Must be always sent in uncompressed form: the PubKey data structure is composed of two bytes32 fields, that represents two components x and y (first 32 bytes and second 32 bytes). The array length and position of elements must correspond to those defined in the script and to the array of signatures. If a key signature is not present, also the corresponding pubKeys X and Y must both be *bytes32(0)* #### Events emitted After a successful claim the following event will be emitted: ```solidity event Claimed(address destAddress, bytes20 zenAddress, uint256 amount) ``` ### Direct Claim - 1st method This method allows a direct claim of special UTXOs generated deterministically from a BaseAddress. This is a special usecase for users that can't generate a signed message, and requires they create this special UTXO in the old mainchain before the migration. This method can be invoked by anyone (not mandatory the sender of the claim tx to be the same destination address). The Solidity method to execute this claim is the following: ```solidity function claimDirect(address baseDestAddress) public ``` #### Parameters - **baseDestAddress** — Destination address on Base of the funds to be claimed. Any Base address is a valid *baseDestAddress*, and that address could claim the funds for the Zend Address generated as such: 1) Calculate SHA256 hash of the baseDestAddress hex 2) Calculate Ripemd160 hash of the output from step 1 3) Concatenate prefix: `0x2089` for ZEND Mainnet or `0x2098` for ZEND testnet 4) Encode it in Base 58 The procedure could be resumed by the formula: `base58.encode(‘0x2089’ + Ripemd160(SHA256(baseDestAddress hex)))` An example Javascript implementation is the following: ```javascript const createHash = require('create-hash') const bs58check = require('bs58check') const prefix = '2089' const baseDestAddress = //Base address in string form without '0x' prefix const ZENDTransferAddress = bs58check.encode( Buffer.from( prefix + createHash('rmd160').update( createHash('sha256').update( Buffer.from(baseDestAddress, 'hex') ).digest() ).digest('hex'), 'hex') ) console.log(ZENDTransferAddress) ``` The owner of an arbitrary Zend address should migrate its funds on the Zend address generated in this way **BEFORE** the Zend Migration. As example, we consider a Zend address owner preparing for the migration that wants to use the direct claim: 1) He generates a Base wallet and gets its address, for example: `0x6ebacd4a2a48728e98aAAA101C59f2e0c57fA987` 2) He executes the code above with parameter `baseDestAddress = 6ebacd4a2a48728e98aAAA101C59f2e0c57fA987`. The output is `zncwpByDSdYjCw3HipRY8MS5dRRsxSR7AGU` 3) Before the Zend Migration, he sends a transaction to move the ZEN from his original address to the generated one (`zncwpByDSdYjCw3HipRY8MS5dRRsxSR7AGU`) 4) After the migration, he (or anyone else) can invoke the method `claimDirect(0x6ebacd4a2a48728e98aAAA101C59f2e0c57fA987)` on the migration Smart Contract. 5) The ZEN balance will be restored as ZEN ERC-20 token balance on Base chain on the address `0x6ebacd4a2a48728e98aAAA101C59f2e0c57fA987` #### Events emitted After a successful claim the following event will be emitted: ```solidity event Claimed(address destAddress, bytes20 zenAddress, uint256 amount) ``` ### Direct Claim - 2nd method This method allows a direct claim of special UTXOs generated deterministically from a BaseAddress. This is a special usecase for users that can't generate a signed message, and requires they create this special UTXO in the old mainchain before the migration. It is similar to the previous 'Direct Claim - 1st method', but here the UTXO is a P2SH 1-of-2 multisig, on which one of the public keys is calculated from the beneficiary Base address. The other public key is a real one, so it is possible for the user to remain in control of their funds on Zend using this owned key. This method can be invoked by anyone (not mandatory the sender of the claim tx to be the same destination address). The Solidity method to execute this claim is the following: ```solidity function claimDirectMultisig(bytes memory script, address baseDestAddress) public ``` #### Parameters - **script** — P2SH redeem Script of the 1-of-2 multisig address to claim - **baseDestAddress** — Destination address on Base of the funds to be claimed. Any Base address is a valid *baseDestAddress*, and that address could claim the funds for the multisig Zend Address generated as such: 1) Calculate SHA256 hash of the baseDestAddress hex 2) Create a 1-of-2 multisig address using as **second** public key the hash calculated at Step 1 with "02" as prefix An example Javascript implementation for the multisig Zend Address calculation is the following: ```javascript const zencashjs = require('zencashjs') const bs58check = require('bs58check') const createHash = require('create-hash') const baseDestAddress = //Base address in string form without '0x' prefix const directMultisigPubKey1 = //Insert any owned public key const directMultisigPubKey2 = "02"+createHash('sha256').update(Buffer.from(baseDestAddress, 'hex')).digest('hex') multisigScript = zencashjs.address.mkMultiSigRedeemScript([directMultisigPubKey1, directMultisigPubKey2], 1, 2); const zenDirectMultisigAddress = zencashjs.address.multiSigRSToAddress(multisigScript); console.log(multisigScript) console.log(zenDirectMultisigAddress) ``` The owner of an arbitrary Zend address should migrate its funds on the multisig Zend address generated in this way before the migration, then invoke `claimDirectMultisig` method on claim smart contract with `multisigScript` and `baseDestAddress` as parameters. #### Events emitted After a successful claim the following event will be emitted: ```solidity event Claimed(address destAddress, bytes20 zenAddress, uint256 amount) ``` ## Security and Audits **Important:** Only use the official claim page: https://www.horizen.io/zenclaim. Be on the lookout for scams or nefarious actors sending you to other websites which may look like Horizen's. Always verify the URL when claiming. The smart contracts have been audited by two independent firms and the claim tools have also been fully audited: - **Smart Contract Code Audit (Cantina):** https://cantina.xyz/portfolio/1586d855-a063-4449-918b-39c2a038b9bb - **Smart Contract Code Review (Halborn):** https://www.halborn.com/audits/the-horizen-foundation/horizen-migration---code-review-0aa462 - **Claim Tools Audit (Cantina):** https://cantina.xyz/portfolio/f3d1defb-1686-41ea-b602-0a03e6b824b2 --- ## Smart contracts Here a more technical deep dive of the smart contracts used for the migration. Their solidity code is publicly available [in this repository](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/contracts). ## ZenToken (ERC-20 official ZEN contract) - Official ERC-20 contract representing ZEN. - Has a maximum capped supply of 21 Millions of ZEN (the same as the old Manchain) - Accepts in the constructor: - The token name and symbol - The address of the EONBackupVault and ZendBackupVault contracts - The address who will receive the remaining portion of Zen after the migration (Horizen foundation and HorizenDAO) - Minting authority is granted only to the vault smart contracts - Exposes a "callback" method **notifyMintingDone**: is called by the vault smart contracts when the minting has been completed. When both of them have completed the process, the contract will mint the remaining supply with the rules determined by the [ZenIP 42409](https://snapshot.box/#/s:horizenfoundationtechnical.eth/proposal/0x3a0ce870c5a894f4468f72d9fde843e9f25e8268890a44ebcc1cb0d5dbbe89cf) . | | | | --- | --- | | **Address on BASE mainnet** | [0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229](https://basescan.org/address/0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229) | | **Solidity source code** | [ZenToken.sol](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/contracts/ZenToken.sol) | ## EONBackupVault - Contract used to store the EON balances and automatically distribute them. - Accepts in the constructor the whitelisted admin address: is the only one able to call contract's write methods. - Methods **setERC20** and **setCumulativeHashCheckpoint** have to be called before the loading: the first one sets the reference to the ERC-20, the second sets the expected cumulative hash after the loading will be completed. - The data loading is done in batches, by calling the method **batchInsert**. Logic to recalculate the cumulative hash is present in the method. - After all the data has been loaded, multiple calls to the **distribute** method will mint the amounts to the payee | | | | --- | --- | | **Address on BASE mainnet** | [0x1Cc689233837A0b96e1f176d49FC08462f70C47F](https://basescan.org/address/0x1Cc689233837A0b96e1f176d49FC08462f70C47F) | | **Solidity source code** | [EONBackupVault.sol](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/contracts/EONBackupVault.sol) | ## ZendBackupVault - Contract used to store the ZEND balances and allow their manual claiming. - Accepts in the constructor the whitelisted admin address and the message string constants to be used (concatenated with the token symbol) for the claiming signature's message prefix. (For mainnet this will correspond to "ZENCLAIM") - Methods **setERC20** and **setCumulativeHashCheckpoint** must be called before the loading: the first one sets the reference to the ERC-20, and the second sets the expected cumulative hash after the loading is completed. - The data loading is done in batches, by calling the method **batchInsert**. During the loading, the contract will mint the corresponding ZEN values to itself. - Manual claiming is possible through the methods **claimP2PKH** and **claimP2SH**. They will be enabled only once the expected cumulative hash will be reached. After each successful claim, the corresponding ZEN amount will be transferred to the payee (this means that the total balance of the contract will correspond to the unclaimed total value at any given time) | | | | --- | --- | | **Address on BASE mainnet** | [0x1Ee188bDf19eBF04B73Ab6FFcec2a864cd4774F2](https://basescan.org/address/0x1Ee188bDf19eBF04B73Ab6FFcec2a864cd4774F2) | | **Solidity source code** | [ZendBackupVault.sol](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/contracts/ZendBackupVault.sol) | ## LinearTokenVesting contract - A contract that locks an amount of ERC-20 tokens and release them to a predefined beneficiary. The vesting is linear, and the number of intervals and time between each interval will be configurable. An admin address set in the constructor has the rights to modify the beneficiary or the vesting parameters (will be subject to offchain DAO voting). | | | | --- | --- | | **Solidity source code** | [LinearTokenVesting.sol](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/contracts/LinearTokenVesting.sol) | ## ZenMigrationFactory contract - Responsible to deploy all the previous contracts and set the correct references between them. - Method **deployMigrationContracts** will perform the task. Accepted parameters will be the token name and symbol, the claim message string constant and the beneficiaries of the remaining supply after the migration (Horizen foundation and HorizenDAO) | | | | --- | --- | | **Solidity source code** | [ZenMigrationFactory.sol](https://github.com/HorizenOfficial/horizen-migration/blob/main/erc20-migration/contracts/ZenMigrationFactory.sol) | **The diagram below represents the sequence of the main contract's calls:** --- ## Tools for Claiming Process This guide explains how to sign a message and complete the ZEN token claim process using official Horizen tools. ## Overview of Tools **Step 1: Sign a Message** Use one of the following tools to generate a signed message: - [Sphere Wallet](#sphere-wallet) - [Ledger Signing Tool](#ledger-signing-tool) - [Private Key Signing Tool](#private-key-signing-tool) - [CLI Tool](#cli-tool) **Step 2: Submit the Claim** Use either to submit the claim: - [Claim Page](#claim-page) - [CLI Tool](#cli-tool-1) ## Sign Message Before claiming, you must generate a valid signature using the message format: ```text "ZENCLAIM" + destinationAddress ``` For example: ```text ZENCLAIM0x1B9aCc8d2c9e20aC2e78904e6f123f2D22Dd2A8w ``` This section outlines how to do this using the three available tools. ### Sphere Wallet If you have your seed phrase, you can use [Sphere Testnet](https://github.com/HorizenOfficial/Sphere_by_Horizen_Testnet/releases/tag/desktop-v1.13.0-testnet) to sign a message. 1. Open Sphere and import your seed phrase (if not already imported). 2. Verify that your wallet addresses and balances are correct. 3. To generate a signature, click on this icon in your Sphere wallet and enter this message: `"ZENCLAIM" + destinationAddress`; Example: `ZENCLAIM0x1B9aCc8d2c9e20aC2e78904e6f123f2D22Dd2A8w`. ![Sign a message with Sphere](/img/migration-tools/sphere-1.png) 4. Click **Create Signature**. This will be used in the claim process. ### Ledger Signing Tool If your funds are stored on a Ledger hardware wallet, use the [Ledger Signing Tool](https://github.com/HorizenOfficial/horizen-migration-ledger-signing-tool/releases/latest). > **Note**: For security, we recommend downloading the tool and running it offline. Download and extract the static files [here](https://github.com/HorizenOfficial/horizen-migration-ledger-signing-tool/releases/latest), then open `index.html` locally. **Prerequisites** - Install both the Bitcoin and Horizen apps on your Ledger device. - Ensure the Horizen app version is v2.4.1 or higher. **Signing Instructions** 1. **Connect Your Ledger Device** Connect your Ledger device and open the **Horizen** app. Ensure the device is unlocked and displays "Application is ready" on the screen. 2. **Launch the Ledger Signing Tool** Open the Ledger Signing Tool and click **Connect**. > Make sure your Ledger device is unlocked, and the Horizen app is open. The Ledger screen will show "Application is ready". ![Connect Ledger](/img/migration-tools/ledger-1.png) 3. **Enter the Destination Address** Enter the **destination address**. This is the EVM address that will receive the migrated ZEN tokens. The "Message to Sign" will auto-populate. ![Enter destination address](/img/migration-tools/ledger-2.png) 4. **Locate and Adjust the Derivation Path** Enter the derivation path for the **ZEN address being claimed from**. To find this: - Open the Ledger Live app - Go to the Horizen account to claim from - Click **Edit Account → Advanced** - Note the `freshAddressPath` ![Find accounts](/img/migration-tools/ledger-3.png) ![Edit account](/img/migration-tools/ledger-4.png) ![Find derivation path](/img/migration-tools/ledger-5.png) #### About Derivation Paths Ledger uses the following format for HD wallet derivation: ```text m / purpose' / coin_type' / account' / change / address_index ``` For **Horizen**, the derivation path is: ```text m / 44' / 121' / account' / change / address_index ``` - `change` is: - `0` → receiving address - `1` → change address - `address_index` is the index of the address under that account #### Understanding `freshAddressPath` Ledger shows the **next unused address** as the `freshAddressPath`. To find the **last used** address: - Subtract `1` from the `address_index`. > **Example** If the `freshAddressPath` is `m/44'/121'/0'/0/5` Then the last used receiving address is `m/44'/121'/0'/0/4` #### Important: Check All Possible Addresses To ensure **no funds are left behind**: 1. **Scan backwards** from the `freshAddressPath`, checking each: - `address_index` (e.g., 4, 3, 2, 1, 0) - for both `change = 0` and `change = 1` 2. This means you should check all paths like: ```text m/44'/121'/0'/0/4 m/44'/121'/0'/1/4 m/44'/121'/0'/0/3 m/44'/121'/0'/1/3 ... ``` This ensures you catch both receiving and change addresses that may have ZEN balances. 5. **Verify the ZEN Address** Paste each derived ZEN address into the [Horizen Explorer](https://explorer.horizen.io/) to check the balances. ![Copy ZEN address](/img/migration-tools/ledger-6.png) 6. **Sign the Message** Click **Sign Message** and confirm the message on your Ledger device. Copy the generated signature. ### Private Key Signing Tool If you have direct access to your private key, use the [Private Key Signing Tool](https://github.com/HorizenOfficial/horizen-migration-signing-tool-private-key/releases/latest). If you only have your seed phrase, you'll need to derive your private key using a tool such as [Ian Coleman's BIP39 tool](https://github.com/iancoleman/bip39/releases/tag/0.5.6). For security, **always use this tool offline** by downloading the `bip39-standalone.html` file from the official GitHub release. After downloading, open the file in a web browser with your internet connections disabled. Be sure to select the **Coin** to "ZEN - Horizen" in the dropdown. > **Note**: For security, we recommend downloading the tool and running it offline. Download and extract the static files [here](https://github.com/HorizenOfficial/horizen-migration-signing-tool-private-key/releases/latest), then open `index.html` locally. ![Private Key Signing Tool](/img/migration-tools/private-key-1.png) 1. Enter your **private key** and confirm the ZEN address is correct. 2. Enter the **destination EVM address**. The "Message to Sign" will auto-populate. 3. Click **Sign Message** to generate and copy the signed message. ### CLI Tool The CLI tool provides functionality for signing and verifying messages. It also supports claiming tokens from ZEN addresses, both standard transparent and multisignature addresses (see [below](#cli-tool-1)). The CLI can be used directly from the command line or imported as a module into a Node.js project. #### Available Commands - **`signmessage`** Sign a message with a ZEN private key. - **`verifymessage`** Verify a signed message against a ZEN address. For detailed usage examples and other supported commands, refer to the [GitHub README](https://github.com/HorizenOfficial/horizen-migration-cli/tree/1.0.0-ZENCLAIM). ## Claim Process Once you have a valid signature, use the claim interface to submit your request. ### Claim Page You can claim ZEN directly through the official web interface: - Mainnet Claim Page: [https://horizen.io/zenclaim](https://horizen.io/zenclaim) 1. **Connect Wallet** Click Connect Wallet and choose your provider (e.g., MetaMask). Make sure you're connected to Base Mainnet. ![Connect MetaMask](/img/migration-tools/metamask.png) **Instructions for connecting to Base Sepolia Testnet (if not already set up)** Click on the "Add Custom Network" from the network dropdown and enter in the following credentials ```text Network Name: Base Mainnet RPC URL: https://mainnet.base.org Chain ID: 8453 Symbol: ETH Block Explorer URL: https://basescan.org ``` 2. **Import Token** Make sure to import either tZEN (if on testnet) or ZEN (on mainnet) so that the tokens appear in Metamask. Under the tokens tab select the "Import Tokens" button and enter the following for the appropriate environment. ```text Base Mainnet Contract: 0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229 Symbol: ZEN ``` ![Import ZEN token](/img/migration-tools/import-token.png) 3. **Enter ZEN Address** Input your Horizen Transparent Address (e.g., from Sphere). The interface will display your available ZEN balance. Click **Next**. ![Enter ZEN address](/img/migration-tools/claim-1.png) 4. **Paste Signature and Destination Address** - Paste the **signed message** (see the options above) into the signature field - Enter the **destination address** address where you want to claim the $ZEN tokens. Note that this should NOT be an exchange deposit address. You can enter the same connected wallet address here. ![Enter signature and destination address](/img/migration-tools/claim-2.png) 5. **Submit Claim** Click **Claim** to initiate the transfer of $ZEN from the Horizen chain to the Base Mainnet. ### CLI Tool The CLI tool provides functionality for claiming tokens from ZEN addresses, both standard transparent and multisignature addresses. It also supports signing and verifying messages (see [above](#cli-tool)). The CLI can be used directly from the command line or imported as a module into a Node.js project. #### Available Commands - **`claimzenaddress`** Claim tokens from a transparent ZEN address. - **`claimzenmultisigaddress`** Claim tokens from a multisignature ZEN address. For detailed usage examples and other supported commands, refer to the [GitHub README](https://github.com/HorizenOfficial/horizen-migration-cli/tree/1.0.0-ZENCLAIM). --- ## Troubleshooting & FAQ ### I've copied my address from Sphere wallet but I get an error saying I'm using a testnet address There are two versions of Sphere, one for testnet, and one for mainnet. Make sure you are using the production version of Sphere when copying addresses. ### I've followed all the steps but I'm not able to process the transaction, or an insufficient funds error is showing up in my wallet Make sure you have ETH in your wallet on Base L2. You need at least **0.000004 ETH per claim**. See the [Prerequisites](/migration/claiming-zen#prerequisites) section for instructions on getting ETH on Base. ### It's saying I need to connect to the Base network but I don't know how You may need to add the network to your wallet manually. Using MetaMask as an example: Click on the **Add Custom Network** button at the bottom of the network dropdown on the top left of MetaMask. Enter the following credentials for Base Mainnet: ```text Network Name: Base Mainnet RPC URL: https://mainnet.base.org Chain ID: 8453 Symbol: ETH Block Explorer URL: https://basescan.org ``` ### The claim portal is saying I've entered an incorrect signature - Message format should not have any commas, apostrophes, or quotations. - Use the appropriate claim prefix. - Message format is: `ZENCLAIM` + destination address (no space between them). - Make sure the signature was made from the same Horizen wallet address you created the signature with. - The destination address entered must match the one you used to sign the message. - Double check to make sure the wallet addresses entered have no typos. ### I don't have an EVM address yet or am not able to connect my wallet Make sure that you have a wallet extension installed on your browser/device. A common EVM wallet to use is [MetaMask](https://metamask.io/). ### I'm seeing a message about interacting with the wallet for the first time, what should I do? If you see a message about a first-time interaction, this is normal. Just click the **Got It** button and proceed. ### I have a wallet installed but I don't know how to connect it On the claim portal page there is a **Connect Wallet** button. Click this and follow the steps. Make sure that popups are not blocked as this is how wallets connect to a dApp. Once connected you should see your wallet address displayed where the button was. ### I've finished my claim but ZEN is not showing up in my wallet Make sure to import the ZEN token into your wallet. In MetaMask, go to **Import Tokens** and add the ZEN ERC-20 contract address on Base. See the [Claim Page](/migration/migration-tools#claim-page) section in the migration tools guide. ### I added up my Mainchain ZEND balance and EON balance, but the total doesn't exactly match what I see in MetaMask. Why? MetaMask may truncate decimal values when displaying token balances. This can cause your displayed ZEN balance to appear slightly different than what you expected. For the most accurate result, check the precise token amounts on the block explorers: - For your ZEND Mainchain balance: https://explorer.horizen.io - For your EON balance: https://eon-explorer.horizenlabs.io/ - For your final Base ZEN balance (after claiming): https://basescan.org When doing your addition, make sure to use the exact numbers shown on the explorers — not the rounded balances in MetaMask. --- ## Start Here - Prerequisites This guide will explain how to claim ZEN from the Horizen ZEND mainchain to Base L2 chain. Importantly, there is no time window in place for claiming tokens. ## Get ETH First Before going through the claim process you will need to have some ETH on Base L2 to process the claim. Please ensure you have at least **0.000004 ETH per claim** to cover the necessary transaction costs. If you have multiple claims, the total amount in your wallet must be sufficient to cover all of them. **Getting ETH on Base (Required to Claim ZEN)** #### Option 1: Use a Centralized Exchange (Recommended Path) The most straightforward way to get ETH on Base is by using a centralized exchange like Coinbase, Binance. 1. Buy ETH on your preferred exchange. 2. Withdraw it to your Base wallet address (e.g., MetaMask). - If using Coinbase and Binance, select "Base" as the destination network. - For other exchanges, users may need to first withdraw to Ethereum and then bridge to Base using a third-party bridge. See below for bridging instructions. #### Option 2: Bridge ETH from Ethereum or Other Chains to Base If your exchange doesn’t support direct Base withdrawals, or if you already have ETH on another network (like Ethereum Mainnet, Arbitrum, or Optimism), you can bridge ETH to Base using a third-party bridge. For bridging ETH from Ethereum to Base, the recommendation is to use [Superbridge](https://superbridge.app/base). 1. Connect your wallet (e.g., MetaMask). 2. Choose the source network and Base as the destination. 3. Enter the amount and confirm the bridge transaction. 4. ETH will typically arrive on Base within a few minutes. #### Option 3: Buy ETH Using Credit Card or Local Payment Some services allow you to buy ETH directly on Base using a debit card, credit card, or other local payment methods. Popular onramps include [MoonPay](https://www.moonpay.com/) or [Onramp](https://onramp.money/). Note that these services usually require a minimum purchase of $20-$25. Once ETH is obtained, make sure to follow the instructions (in Option 2) to bridge ETH over to Base. #### Final Tip To confirm that you’ve received ETH on Base, check your wallet (e.g., MetaMask) or visit https://basescan.org. Once you see ETH in your Base wallet, you’re ready to claim your ZEN. ## Claim Process Claiming is a two step process, first you will sign a message with your old Horizen wallet, then you will submit a claim with your new wallet on Base. **Step 1: Create a Signature** **Step 2: Submit the Claim** Proceed to [Step 1: Create a Signature](/migration/mainnet-migration-instructions/create-a-signature) --- ## Step 1: Create a Signature The process for creating a signature **varies depending on your wallet**. Messages have to use the format shown below. Signing messages and claiming requires providing an EVM compatible destination address. This will be the Base address where ZEN will be sent to after claiming. **Note that this should NOT be an exchange deposit address**. Mainnet Message Format ``` "ZENCLAIM" + destinationAddress ``` For example, if your destination address is `0x1B9aCc8d2c9e20aC2e78904e6f123f2D22Dd2A8w` then your message format will be the following: ``` Mainnet message example: ZENCLAIM0x1B9aCc8d2c9e20aC2e78904e6f123f2D22Dd2A8w ``` Save your message format as it will be used when generating a signature. Instructions for claiming will vary depending on where and how you are holding ZEN. Choose from one of the bullet points below: - **[Sphere Wallet Users](/migration/mainnet-migration-instructions/sphere-wallet-users)** - **[Ledger Wallet Users](/migration/mainnet-migration-instructions/ledger-wallet-users)** - **[Other Wallet Users](/migration/mainnet-migration-instructions/other-wallet-users)** For users who manage ZEN with other wallets not listed above. Note that users need access to their private keys in order to use this tool. - **[Super Users - CLI Tool](/migration/mainnet-migration-instructions/cli)** For users or organizations who manage multiple wallet addresses, have access to private keys, and need to generate multiple signatures. This tool can also be used to claim from multiple wallets. --- ## Sphere Wallet Users If you have your seed phrase, you can use [Sphere](https://github.com/HorizenOfficial/Sphere_by_Horizen_Private/releases/latest) to sign a message. 1. Open Sphere and import your seed phrase (if not already imported). 2. Verify that your wallet addresses and balances are correct. 3. To generate a signature, click on this icon in your Sphere address and enter the message in the “Message to be signed” box as shown below. ![Sign a message with Sphere](/img/migration-tools/sphere-1.png) 4. Click **Create Signature**. This will generate a signature for you, save this as it will be used in the claim process. > **Note:** _if you have several addresses with balances to claim in your wallet, you need to repeat the procedure for every address._ 5. Proceed to the [Claim Page](/migration/mainnet-migration-instructions/claim-page). --- ## Ledger Wallet Users If your funds are stored on a Ledger hardware wallet, use the [Ledger Signing Tool](https://github.com/HorizenOfficial/horizen-migration-ledger-signing-tool/releases/latest). > **Note**: For security, we recommend downloading the tool and running it offline. Download and extract the static files [here](https://github.com/HorizenOfficial/horizen-migration-ledger-signing-tool/releases/latest), then open `index.html` locally. **Google Chrome is the recommended browser for this tool.** **Prerequisites** - Install both the Bitcoin and Horizen apps on your Ledger device. - Ensure the Horizen app version is v2.4.1 or higher. **Signing Instructions** 1. **Connect Your Ledger Device** Connect your Ledger device and open the **Horizen** app. Ensure the device is unlocked and displays "Application is ready" on the screen. 2. **Launch the Ledger Signing Tool** Open the Ledger Signing Tool and click **Connect**. Make sure your Ledger device is unlocked, and the Horizen app is open. The Ledger screen will show "Application is ready". ![Connect Ledger](/img/migration-tools/ledger-1.png) 3. **Enter the Destination Address** Enter the **destination address**. The destination address is where ZEN will be sent to, make sure you are the owner of this address. The "Message to Sign" will auto-populate. ![Enter destination address](/img/migration-tools/ledger-2.png) 4. **Locate and Adjust the Derivation Path** Enter the derivation path for the **ZEN address being claimed from**. To find this: - Open the Ledger Live app - Go to the Horizen account to claim from - Click **Edit Account → Advanced** - Note the `freshAddressPath` ![Find accounts](/img/migration-tools/ledger-3.png) ![Edit account](/img/migration-tools/ledger-4.png) ![Find derivation path](/img/migration-tools/ledger-5.png) --- #### About Derivation Paths Ledger uses the following format for HD wallet derivation: ``` m / purpose' / coin_type' / account' / change / address_index ``` For **Horizen**, the derivation path is: ``` m / 44' / 121' / account' / change / address_index ``` - `change` is: - `0` → receiving address - `1` → change address - `address_index` is the index of the address under that account --- #### Understanding `freshAddressPath` Ledger shows the **next unused address** as the `freshAddressPath`. To find the **last used** address: - Subtract `1` from the `address_index`. > **Example** > If the `freshAddressPath` is `m/44'/121'/0'/0/5` > Then the last used receiving address is `m/44'/121'/0'/0/4` --- #### Important: Check All Possible Addresses To ensure **no funds are left behind**: 1. **Scan backwards** from the `freshAddressPath`, checking each: - `address_index` (e.g., 4, 3, 2, 1, 0) - for both `change = 0` and `change = 1` 2. This means you should check all paths like: ``` m/44'/121'/0'/0/4 m/44'/121'/0'/1/4 m/44'/121'/0'/0/3 m/44'/121'/0'/1/3 ... ``` This ensures you catch both receiving and change addresses that may have ZEN balances. 3. **Verify the ZEN Address** Paste each derived ZEN address into the [Horizen Explorer](https://explorer.horizen.io/) to check the balances. ![Copy ZEN address](/img/migration-tools/ledger-6.png) 4. **Sign the Message** Click **Sign Message** and confirm the message on your Ledger device. Copy the generated signature and save it for the next step in the process. 5. Proceed to the [Claim Page](/migration/mainnet-migration-instructions/claim-page). --- ## Other Wallet Users If you have direct access to your private key, use the [Private Key Signing Tool](https://github.com/HorizenOfficial/horizen-migration-signing-tool-private-key/releases/latest). If you only have your seed phrase, you'll need to derive your private key using a tool such as [Ian Coleman's BIP39 tool](https://github.com/iancoleman/bip39/releases/tag/0.5.6). For security, **always use this tool offline** by downloading the `bip39-standalone.html` file from the official GitHub release. After downloading, open the file in a web browser with your internet connections disabled. Be sure to select the **Coin** to "ZEN - Horizen" in the dropdown. > **Note**: For security, we recommend downloading the tool and running it offline. Download and extract the static files [here](https://github.com/HorizenOfficial/horizen-migration-signing-tool-private-key/releases/latest), then open `index.html` locally. 1. Enter your **private key** and confirm the ZEN address is correct. 2. Enter the **destination EVM address**. The destination address is where ZEN will be sent to, make sure you are the owner of this address. The "Message to Sign" will auto-populate. 3. Click **Sign Message** to generate and copy the signed message. 4. Proceed to the [Claim Page](/migration/mainnet-migration-instructions/claim-page). --- ## Super Users - CLI Tool The CLI tool provides functionality for signing and verifying messages. It also supports claiming tokens from ZEN addresses, both standard transparent and multisignature addresses. The CLI can be used directly from the command line or imported as a module into a Node.js project. ## Available Commands - **`signmessage`** Sign a message with a ZEN private key. - **`verifymessage`** Verify a signed message against a ZEN address. - **`claimzenaddress`** Submit a claim. - **`claimmultisigaddress`** Submit a claim for a multisig address. For detailed usage examples and other supported commands, refer to the [GitHub README](https://github.com/HorizenOfficial/horizen-migration-cli/tree/1.0.0-ZENCLAIM). --- ## Step 2: Submit the Claim You can claim ZEN directly through the official web interface: https://horizen.io/zenclaim ## 1. Connect Wallet Click Connect Wallet and choose your provider (e.g., MetaMask). Make sure you're connected to Base Mainnet. ## 2. Import Token Make sure to import ZEN so that the token appears in MetaMask. Under the tokens tab select the "Import Tokens" button and enter the following: ``` Base Mainnet Contract: 0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229 Symbol: ZEN ``` ## 3. Enter ZEN Address Input your Horizen Transparent Address (e.g., from Sphere). The interface will display your available ZEN balance. Click **Next**. ![Enter ZEN address](/img/migration-tools/claim-1.png) ## 4. Paste Signature and Destination Address - The signed message should have already been created. If you haven’t done this yet, please go to the [Create a Signature](/migration/mainnet-migration-instructions/create-a-signature) section to generate a signature. - Paste the **signed message** into the signature field. - **Enter the same destination address used in the message signing step.** ![Enter signature and destination address](/img/migration-tools/claim-2.png) ## 5. Submit Claim Click **Claim ZEN** to initiate the transfer of $ZEN from the Horizen chain to Base Mainnet. --- ## Security and Audits **Important:** Make sure to only use the official claim page from Horizen’s website: https://www.horizen.io/zenclaim. Be on the lookout for scams or nefarious actors sending you to other websites which may look like Horizen’s. Be sure to always check the URL when claiming. The smart contracts have been audited by two independent firms and the website has also been fully audited. Audit reports are available below: - **Smart Contract Code Audit:** Link to summary, full download link in upper right corner: https://cantina.xyz/portfolio/1586d855-a063-4449-918b-39c2a038b9bb - **Smart Contract Code Review:** https://www.halborn.com/audits/the-horizen-foundation/horizen-migration---code-review-0aa462 - **Claim Tools Audit:** Link to summary, full download link in upper right corner: https://cantina.xyz/portfolio/f3d1defb-1686-41ea-b602-0a03e6b824b2 --- ## Troubleshooting & FAQ(Mainnet-migration-instructions) **1. I’ve copied my address from Sphere wallet but I get an error saying I’m using a testnet address?** There are two versions of Sphere, one for testnet, and one for mainnet. Make sure you are using the production version of Sphere when copying addresses. **2. I’ve followed all the steps but I’m not able to process the transaction, or an insufficient funds error is showing up in my wallet?** You may see a message like the following Make sure you have ETH in your wallet on Base L2. Instructions for getting ETH are in the intro section of this document: [Get ETH First](/migration/mainnet-migration-instructions/mainnet-claim#get-eth-first) **3. It’s saying I need to connect to the Base network but I don’t know how.** You may need to add the network to the list of connected networks in your wallet. Usually this shows up automatically for common networks like Base, but here's instructions on how to add it manually. Using Metamask as the example wallet (Network credentials are the same for other wallets). Click on the **Add Custom Network** button at the bottom of the dropdown on the top left of Metmask wallet. Enter the following credentials for Base Mainnet network. ``` Network Name: Base Mainnet RPC URL: https://mainnet.base.org Chain ID: 8453 Symbol: ETH Block Explorer URL: https://basescan.org ``` **4. The claim portal is saying I’ve entered an incorrect signature.** - Message format should - Not have any commas, apostrophes, or quotations. - Use the appropriate claim prefix. - Message format is as described here: [Create a Message](/migration/mainnet-migration-instructions/create-a-signature) - Make sure the signature was made from the same Horizen wallet address you created the signature with. - Importantly you need to enter the same destination address as the one you used to sign the message. - Double check to make sure the wallet addresses entered have no typos. **5. I don’t have an EVM address yet or am not able to connect my wallet.** Make sure that you have a wallet extension installed on your browser/device. A common EVM wallet to use is [MetaMask](https://metamask.io/). **6. I'm seeing a message about interacting with the wallet for the first time, what should I do?** If you see the message below it is common and not to worry, just click the **Got It** button **7. I have a wallet installed but I don’t know how to connect it.** On the claim portal page there is a Connect Wallet button. Click this and follow the steps. Make sure that popups are not blocked as this is how wallets connect to a dApp. ![Claim page connect wallet button](/img/migration-tools/connect-1.png) Once connected you should see your wallet address show up where the button is. ![Claim page address displayed](/img/migration-tools/connect-2.png) **8. I’ve finished my claim but ZEN is not showing up in my wallet.** Make sure to import the token as described in step 2 of the [Claim Page](/migration/mainnet-migration-instructions/claim-page#2-import-token). **9. I added up my Mainchain ZEND balance and EON balance, but the total doesn’t exactly match what I see in MetaMask. Why?** MetaMask may truncate decimal values when displaying token balances. This can cause your displayed ZEN balance to appear slightly different than what you expected based on your own calculations. For the most accurate result, check the precise token amounts on the block explorers: - For your ZEND Mainchain balance: https://explorer.horizen.io - For your EON balance: https://eon-explorer.horizenlabs.io/ - For your final Base ZEN balance (after claiming): https://basescan.org When doing your addition, make sure to use the exact numbers shown on the explorers - not the rounded balances in MetaMask. ![Check balance on BaseScan](/img/migration-tools/basescan-balance.png) --- ## Transparency Report ## Introduction The formation of The Horizen Foundation, a Cayman Islands foundation company (the "The Horizen Foundation "), marked the beginning of the process for launching the Horizen DAO (the “DAO”). As detailed in the Constitution of the Horizen DAO (the "Constitution"), The Horizen Foundation exists as a steward of the Horizen community as the legal entity through which the Horizen DAO can govern and make decisions for the ecosystem. The formation of The Horizen Foundation was undertaken in accordance with ZenIP 42203, in which the community directed the DAO and foundation to be established. This Transparency Report (this "Report") details the steps taken in advance of creating and configuring the Horizen DAO to comply with relevant legal registration and operational requirements. It is important for the community to understand what those steps were and the costs that were associated with undertaking them. This report identifies the activities taken by The Horizen Foundation on behalf of Horizen DAO in pursuit of the ZenIP 42203 directive. Members of the Horizen community can implement changes of both a technical and non-technical nature through the governance process outlined below and described in further detail in the Constitution. By way of the governance structure that has been created in pursuit of ZenIP 42203, and which is noted in this Transparency Report, the community is empowered to interact with and modify it via governance proposals. ## Foundation Governance Under Cayman Islands law, The Horizen Foundation must have at least one director and one supervisor at all times. All directors have a fiduciary duty to The Horizen Foundation and are responsible for its management and operations, including entering into contracts and agreements on behalf of The Horizen Foundation. At formation, The Horizen Foundation incorporated with one professional director provided by a Cayman Islands governance and corporate services firm, Leeward, which serves as supervisor. Per the Constitution and accompanying governance documentation, members of the Horizen DAO have the ability to remove directors and supervisors via the ZenIP process (provided that The Horizen Foundation may not, at any time, be left without a director or a supervisor). ## Initial Set-up and Operating Costs The Horizen Foundation incurred initial expenses for preparing, investigating and establishing the Horizen DAO and its decentralized system of governance (the “Pre-Launch Costs”). This undertaking involved meticulous deliberation from independent advisors, legal structuring, and technical proficiency to create a system designed to be secure, mitigate known risks, and provide a technically robust operational framework. The goal was to empower the DAO to govern itself on a dependable platform and to facilitate the secure transition of the Horizen mainchain and Horizen EON networks from the Zen Blockchain Foundation to the Horizen DAO considering both technical and legal considerations. As of the date of this Report, the aggregate Pre-Launch Costs incurred by The Horizen Foundation are as follows:\* Cost Category Amount Local Counsel (Cayman Islands) $20,359 Regulatory and Corporate Counsel $165,292.35 DAO Administrator, Secretary, Supervisor, Registered Office $61,875 Director Expenses + Filing Fees $8,435 Development and Management Support Services $45,878 TOTAL $301,839.35 \*costs subject to change based on invoices not yet received as of the launch date. At the DAO’s launch, the responsibility for governing the Horizen and EON networks was formally transferred to the DAO. In support of the DAO, The Horizen Foundation has undertaken the financial responsibilities associated with day-to-day operations, blockchain functionality, infrastructure maintenance, service agreements, and ongoing enhancements for the Horizen and EON ecosystems. Furthermore, the Horizen community's portion of the block subsidy (i.e., 20% of the total block subsidy), previously directed to a multisig wallet maintained on behalf of the Zen Blockchain Foundation (the **“ZBF Block Subsidy Wallet”**) and then directed to the ZBF Treasury wallet (the **“ZBF Treasury”**), is now directed to a multisig wallet maintained on behalf of the Horizen DAO and The Horizen Foundation (the **“Horizen DAO Block Subsidy Wallet”**) which will then be directed to the Horizen DAO Treasury wallet (the **“Horizen DAO Treasury Wallet”**). Each of the signers is subject to the terms of a legally enforceable Multi-Signature Participation Agreement with The Horizen Foundation. To sustain the vital work of The Horizen Foundation, support the continued operation of core infrastructure and advancement of the Horizen and EON networks and provide for the DAO’s general operating costs, it is imperative to ensure sufficient funding for the Horizen DAO using the DAO’s portion of its block subsidy. A list of estimated annual DAO operational costs incurred by The Horizen Foundation is set out below: Cost Category Annual Cost DAO Personnel & Consultants $151,248 USD ZEN Ambassadors $13,950 USD Mainchain Development Services $1,730,000 USD Avg.(historical range: $1,200,000 - $2,280,000) Infrastructure Maintenance $400,000 USD Avg.(historical range: $40,000 - $860,000) Ecosystem Services Support with community management, project management, ecosystem adoption, and grant program implementation $151,472 USD Avg.(historical range: $2,400 - $258,750) Professional Fees (legal & tax) $80,000 USD Software Subscriptions $196,306 USD Server Fees $234,850 USD Director Fees $25,000 USD Supervisor Fees $30,000 USD DAO Secretary & Registered Office Fees $6,800 USD Special Council Consideration 17,500 $ZEN DAO Administrator Fees $333,750 USD D&O Insurance $30,000 USD To that end, the Horizen DAO’s portion of the block subsidy shall be allocated as follows: - 25% to be retained in the Horizen DAO treasury for use by the community in accordance with the ZenIP and EONIP proposal processes. - 75% to be allocated to an administrative budget for The Horizen Foundation to satisfy the DAO’s operational costs. The Zen Blockchain Foundation (**"ZBF"**) has longstanding relationships with service providers who were engaged at the direction of the community. The services performed by these providers were, and remain, necessary for the continued operation of the Horizen blockchain and broader ecosystem. The Horizen Foundation recognizes that Horizen would not be here today without the services and expertise from those providers, and also recognizes the need to expeditiously repay the costs that were incurred in creating and configuring the Horizen DAO. With that critical objective in mind, until such time that the outstanding Pre-Launch Costs are satisfied in full, 95% of the DAO’s portion of the block subsidy shall be allocated to the administrative budget to fund operational costs that are essential for the continued functioning of Horizen (including administration, engineering and infrastructure as well as The Horizen Foundation operational costs), and will expeditiously repay the outstanding debts that were incurred at the direction of ZBF. To the extent there are insufficient funds in any payment period to satisfy amounts owed to service providers in full, the amount of any such unpaid expenses will be added to the startup cost balance and accrue interest accordingly. This repayment strategy will enable The Horizen ecosystem and its community to efficiently and effectively establish a sustainable and unimpeded Horizen DAO Treasury. ## DAO Governance The Governance Structure was established in line with the Constitution of the Horizen DAO. ### Summary of governance powers #### 1. $ZEN Tokenholder - The Horizen DAO is made up of the $ZEN tokenholders, who play a pivotal role in its system of decentralized governance with the overarching goal of fostering a trustless, transparent, and verifiable Horizen ecosystem. Given that Horizen aims to serve as a public resource, it is only fitting that those benefiting from this public good should be the ones steering its governance. - $ZEN token holders have the ability to propose and vote on ZenIPs and EONIPs with respect to the blockchains and ecosystems governed by the Horizen DAO. - Sections 3 and 4 of the Constitution, and The Horizen Foundation Governing Documents set forth the details relating to the governance by the token holders, as well as their specific authorities and voting powers. #### 2. Special Council **Overview:** The **"Special Council"** is a DAO committee, initially made up of 7 Special Council members, to provide oversight of The Horizen Foundation on behalf of the Horizen DAO as further detailed in Section 6 of the Constitution. The Special Council’s oversight responsibilities include convening emergency operational meetings when necessary to deliberate and mitigate security risks affecting the Horizen DAO related to any protocols utilizing the $ZEN token, tokenholders, or The Horizen Foundation. Additional responsibilities will include community and ecosystem protection functions by overseeing the review of ZenIPs and EONIPs before they are put forth for a vote by the Horizen DAO. The initial Special Council members, split by cohort, are as follows: ##### March Cohort: - Brian Rose - Marwan Alzarouni - Benjamin Charbit ##### September Cohort: - Herve Larren - Jemma Xu - Elias Ahonen - Leila Salieva **Compensation:** - Special Council members are each paid an annual compensation of 2,500 $ZEN tokens, to vest on a monthly basis, for their time, expertise, and efforts in serving the community. **Special Council Elections:** - Section 6(b) of the Constitution describes the Special Council election process. The DAO holds the authority to designate and choose individuals for the Special Council, and further retains the capability to modify the Special Council's authority, whether by expansion or reduction, via a Technical ZenIP. #### 3. Horizen DAO Voting Procedure Section 5 of the Constitution describes the details of the ZenIP and EONIP processes and voting procedures, as well as the different categories of ZenIPs and EONIPs. There are a few governance channels and tools available, each serving its own purpose. - [Discord](https://horizen.io/invite/discord): is a social media and messaging application that serves as the main communications platform for Horizen. Anyone is free to join the official Horizen server and engage in discussion with other community members across any of the channels. This is where community members can first propose and socialize ideas before beginning the formal proposal process. - [Discourse](http://forum.horizen.io): is an open forum for governance related discussions. It is where tokenholders can create ZenIPs and EONIPs, view current and past proposals, and comment on proposals. Members of the Horizen community must register for an account before contributing and engaging with posts. - [Snapshot](https://snapshot.org/#/horizenfoundation.eth): is an off-chain voting interface that allows the community to vote on ZenIPs and EONIPs based on the amount of $ZEN they hold, and delegate voting power to a different delegate. Because Snapshot is not compatible with Horizen’s mainchain, it is integrated with EON, which is Ethereum-compatible. This means that $ZEN tokenholders will need to “link” their mainchain $ZEN address with an EON address in order to capture their full voting power for proposals in Snapshot. More information on this process can be found [here](https://eon.horizen.io/governance/dao). The process of voting, choice of any of the tools, and length of voting can be changed by the community through a Technical ZenIP. --- ## Bridge Assets from Base to Horizen Horizen runs two bridges. They are different products with different mechanics, and picking the wrong one is the most common way to lose an afternoon. | | [Stargate](https://stargate.finance/) (LayerZero OFT) | [Native bridge](https://hub.horizen.io) | |---|---|---| | Assets | ZEN, USDC, cbBTC | ETH | | Mechanism | Burn on source, mint on destination | OP Stack lock and mint via Base | | Routes | Horizen to 80+ chains | Horizen and Base only | | Deposit time | Typically under 2 minutes | A few minutes after Base finality | | Withdrawal | Same mechanism both directions | Challenge period applies (7 days) | | Fees | 0.06% protocol fee plus source gas | Source gas only | The detail that trips people up: ETH, the gas token on Horizen, moves only through the native bridge. It is not a Stargate asset here. Your users' first onboarding transaction is a native bridge deposit, and so is yours. This tutorial does both routes programmatically on mainnet (Base to Horizen). There is no contract to write. You are calling contracts that are already deployed, which makes this a scripting exercise, not a Foundry project. ## Prerequisites - Foundry installed (`cast` in your PATH) - Node 18+ with `viem` and `@layerzerolabs/lz-v2-utilities` if you want the TypeScript route - A wallet funded with Base ETH (for gas on both routes) - ZEN on Base for the Stargate route — acquire via any DEX on Base (Uniswap, Aerodrome) using the ZEN ERC-20 address below - RPC endpoints: Base at `https://mainnet.base.org`, Horizen Mainnet at `https://horizen.calderachain.xyz/http` ## Contract reference Mainnet, Base to Horizen: | Token | Chain | Contract | |---|---|---| | ZEN | Base | ERC-20 `0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229` · OFT Adapter `0x57da2D504bf8b83Ef304759d9f2648522D7a9280` | | ZEN | Horizen | OFT `0x57da2D504bf8b83Ef304759d9f2648522D7a9280` | | USDC | Base | Lock contract `0x27a16dc786820b16e5c9028b75b99f6f604b5d26` | | USDC | Horizen | OFT `0x3a1293Bdb83bBbDd5Ebf4fAc96605aD2021BbC0f` | | cbBTC | Base | OFT Adapter `0x68fb5BB8330C0b9d907F50f278143873276ee056` | | cbBTC | Horizen | OFT `0x68fb5BB8330C0b9d907F50f278143873276ee056` | | ETH (native bridge) | Base | L1StandardBridge `0xf4a6cc4171fda694439f856d912777aa6ab05369` | :::note ZEN's Base OFT Adapter and Horizen OFT share the same address — that is how this deployment is wired. ::: Testnet, Base Sepolia to Horizen Testnet: | Token | Chain | Contract | |---|---|---| | tZEN | Base Sepolia | OFT Adapter `0x2ead4B0beBD8e54F9B7cC1007DF4c44a27b9a339` | | tZEN | Horizen Testnet | OFT `0xb06EC4ce262D8dbDc24Fac87479A49A7DC4cFb87` | | cbBTC | Base Sepolia | OFT Adapter `0x5dE29d14E72feb79967596F3Ae57A9BfBA192769` | | cbBTC | Horizen Testnet | OFT `0x06DA6bDD2aB23447af5162ab0975edDA7E8d3747` | :::caution Testnet tokens tZEN on Base Sepolia has no public faucet. Request an allocation in the [Horizen Discord](https://discord.gg/horizen-334085157441110017) (`#developer` channel). For the native bridge on testnet, get the L1StandardBridge address from a real deposit transaction on [hub-testnet.horizen.io](https://hub-testnet.horizen.io). ::: LayerZero endpoint IDs — LayerZero's own routing identifiers, unrelated to EVM chain IDs: | Chain | EID | |---|---| | Base | 30184 | | Base Sepolia | 40245 | | Horizen Mainnet | 30399 | | Horizen Testnet | 40435 | ## Route 1: Bridge ZEN over Stargate If you want the UI path instead, [Stargate](https://stargate.finance/) handles this route without code. This tutorial covers the programmatic path for integrations and automation. An OFT transfer is one contract call: `send()` on the source-chain OFT contract, preceded by a fee quote from `quoteSend()`. You pay the LayerZero message fee in native gas through `msg.value`. Everything routes through a single struct: ```solidity struct SendParam { uint32 dstEid; // destination endpoint ID, not the chain ID bytes32 to; // recipient address, left-padded to 32 bytes uint256 amountLD; // amount in the token's local decimals uint256 minAmountLD; // equal to amountLD for burn/mint OFTs bytes extraOptions; // executor options, 0x when enforced options exist bytes composeMsg; // 0x for a plain transfer bytes oftCmd; // 0x for a plain transfer } ``` ### Step 1: Approve the adapter Going Base to Horizen, the adapter pulls ZEN from your wallet, so it needs allowance. Going the other way, the Horizen OFT is the token itself and burns directly, no approval needed. This asymmetry is inherent to the adapter pattern. ```bash export ADAPTER=0x57da2D504bf8b83Ef304759d9f2648522D7a9280 export ZEN=0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229 export RPC_BASE=https://mainnet.base.org cast send $ZEN "approve(address,uint256)" $ADAPTER 1ether \ --rpc-url $RPC_BASE --private-key $PK ``` ### Step 2: Quote the fee ```bash export DST_EID=30399 export RECIPIENT_B32=0x000000000000000000000000 cast call $ADAPTER \ "quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))" \ "($DST_EID,$RECIPIENT_B32,1000000000000000000,1000000000000000000,0x,0x,0x)" false \ --rpc-url $RPC_BASE ``` This returns `(nativeFee, lzTokenFee)`. The native fee covers DVN verification and executor delivery on the destination chain. You pay it once, on the source chain. No gas is needed on Horizen. ### Step 3: Send ```bash cast send $ADAPTER \ "send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)" \ "($DST_EID,$RECIPIENT_B32,1000000000000000000,1000000000000000000,0x,0x,0x)" \ "($NATIVE_FEE,0)" $YOUR_ADDRESS \ --value $NATIVE_FEE \ --rpc-url $RPC_BASE --private-key $PK ``` The last argument is the refund address for any fee overpayment. ### Step 4: Track and Verify Paste the transaction hash into [LayerZero Scan](https://layerzeroscan.com). The message moves through Inflight, Confirming, and Delivered. Delivered means the executor has landed the mint on Horizen. Confirm the balance yourself rather than trusting the UI: ```bash cast call 0x57da2D504bf8b83Ef304759d9f2648522D7a9280 \ "balanceOf(address)(uint256)" $YOUR_ADDRESS \ --rpc-url https://horizen.calderachain.xyz/http ``` ### The same flow in Viem ```typescript import { createWalletClient, createPublicClient, http, parseEther, pad } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { base } from 'viem/chains' import { Options } from '@layerzerolabs/lz-v2-utilities' import { oftAbi } from './abi/oft' // IOFT quoteSend + send fragments from @layerzerolabs/oft-evm const ADAPTER = '0x57da2D504bf8b83Ef304759d9f2648522D7a9280' const DST_EID = 30399 const account = privateKeyToAccount(process.env.PK as `0x${string}`) const publicClient = createPublicClient({ chain: base, transport: http() }) const walletClient = createWalletClient({ account, chain: base, transport: http() }) // If quoteSend reverts with empty options, the pathway has no enforced options. // Build explicit executor options instead of guessing at gas on the destination: const extraOptions = Options.newOptions() .addExecutorLzReceiveOption(200_000, 0) .toHex() as `0x${string}` const sendParam = { dstEid: DST_EID, to: pad(account.address, { size: 32 }), amountLD: parseEther('1'), minAmountLD: parseEther('1'), extraOptions, composeMsg: '0x' as const, oftCmd: '0x' as const, } const [nativeFee, lzTokenFee] = await publicClient.readContract({ address: ADAPTER, abi: oftAbi, functionName: 'quoteSend', args: [sendParam, false], }) as [bigint, bigint] const hash = await walletClient.writeContract({ address: ADAPTER, abi: oftAbi, functionName: 'send', args: [sendParam, { nativeFee, lzTokenFee }, account.address], value: nativeFee, }) console.log(`sent: https://layerzeroscan.com/tx/${hash}`) ``` :::tip `dstEid` is a LayerZero endpoint ID, not an EVM chain ID. The two numbering schemes are unrelated. Passing a chain ID here is the single most common OFT integration bug, and the failure mode is a revert at quote time if you are lucky, or a misrouted message if you are not. ::: ## Route 2: Bridge ETH over the Native Bridge ETH moves through Horizen's native OP Stack bridge. Deposits are a single transaction on Base. Withdrawals follow the standard OP Stack two-step: initiate on Horizen, wait out the challenge period, finalize on Base. If you want the UI path, [hub.horizen.io](https://hub.horizen.io) handles both deposits and the prove/finalize withdrawal flow. ### Deposit, Base to Horizen ```bash export L1_STANDARD_BRIDGE=0xf4a6cc4171fda694439f856d912777aa6ab05369 cast send $L1_STANDARD_BRIDGE \ "bridgeETH(uint32,bytes)" 200000 0x \ --value 0.01ether \ --rpc-url $RPC_BASE --private-key $PK ``` The first argument is the minimum gas limit for the deposit transaction on Horizen. 200k is comfortable headroom for a plain ETH transfer. ETH lands after Base finality (typically a few minutes). Verify: ```bash cast balance $YOUR_ADDRESS --rpc-url https://horizen.calderachain.xyz/http ``` ### Withdrawal, Horizen to Base Withdrawals are subject to a 7-day challenge period before they can be finalized on Base. The bridge hub at [hub.horizen.io](https://hub.horizen.io/) handles the prove and finalize steps for you, and for most workflows that is the right tool. Budget for the challenge period in anything user-facing you build on top of this: it is a property of the rollup's security model, not a UI limitation. :::note Challenge period 7 days is the standard OP Stack default. Caldera can configure this differently per deployment. Verify the current value with the Horizen team or check the `FINALIZATION_PERIOD_SECONDS` setting on the L2OutputOracle contract before publishing a hard deadline to users. ::: ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `quoteSend` reverts | Empty `extraOptions` on a pathway without enforced options | Build explicit options with `addExecutorLzReceiveOption(200000, 0)` | | `send` reverts on allowance | Adapter not approved, Base to Horizen direction only | Approve the adapter for the ERC-20 first | | Stuck at Inflight on LayerZero Scan | DVN verification pending | Wait it out; past 10 minutes on testnet, check the pathway config on LayerZero Scan | | Reverts or misroutes with a valid-looking `dstEid` | EVM chain ID used instead of the LayerZero EID | Use the EID table above; the schemes are unrelated | | ETH transfer attempted through Stargate | ETH is not a Stargate asset on Horizen | Use the native bridge | ## Further reading - [How Bridging Works](/horizen-chain/bridging/how-bridging-works): the reference page covering the UI path, fees, and slippage - [LayerZero OFT standard](https://docs.layerzero.network/v2/developers/evm/oft/native-transfer) - [LayerZero deployments: Horizen](https://docs.layerzero.network/v2/deployments/chains/horizen) - [Native bridge hub](https://hub.horizen.io) - [Horizen block explorer](https://explorer.horizen.io) --- ## Deploy an ERC-20 Token on Horizen Horizen runs a near-vanilla EVM where all standard Ethereum opcodes are supported, with minor OP Stack extensions for L2 data fees. This tutorial deploys a production-grade ERC-20 using [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts/), with both **Foundry** and **Hardhat** paths. It also covers contract verification on the Horizen Explorer. ## Architecture Context Before touching code, understand the fee model. Horizen is an OP Stack L3 settling on Base (L2), which settles on Ethereum (L1). Your transaction fee has two components: - **L2 execution fee** — gas consumed by your transaction × the Horizen base fee - **L1 data fee** — the cost of publishing your transaction's calldata as a batch to Base The L1 data fee is automatically appended by the OP Stack. It's typically small but non-zero, and it fluctuates with Base's gas price. Your ETH on Horizen is native bridged ETH — the same asset as on Base. ## Network Reference For full network details (RPC endpoints, chain IDs, explorer, faucet), see: - [Mainnet Configuration](/horizen-chain/network/mainnet) - [Testnet Configuration](/horizen-chain/network/testnet) ## Step 1: Write the Contract Write a minimal but complete ERC-20 contract with an owner-controlled mint function and a fixed max supply: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol"; import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /// @title HorizenToken /// @notice A capped, burnable ERC-20 with owner-controlled minting. contract HorizenToken is ERC20Capped, ERC20Burnable, Ownable { constructor( string memory name, string memory symbol, uint256 cap, address initialOwner ) ERC20(name, symbol) ERC20Capped(cap) Ownable(initialOwner) {} /// @notice Mint tokens to an address. Caller must be the owner. /// @param to Recipient address /// @param amount Amount in the token's smallest unit (wei equivalent) function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } /// @dev Required override — ERC20Capped hooks into _update, not _mint. function _update( address from, address to, uint256 value ) internal override(ERC20, ERC20Capped) { super._update(from, to, value); } } ``` A few design decisions worth noting: - `ERC20Capped` enforces a maximum supply at the `_update` level — it's impossible to exceed the cap regardless of how many times `mint` is called. - The `_update` override is mandatory in OZ v5.x. The v4.x pattern using `_beforeTokenTransfer` no longer works. - Deploying with `initialOwner` as a constructor argument (rather than hardcoding `msg.sender`) makes the contract significantly easier to test and safer to deploy via a script where `msg.sender` is a hot deployment key you may not want as permanent owner. ## Path A — Foundry ### Install Foundry ```bash curl -L https://foundry.paradigm.xyz | bash foundryup ``` ### Initialize the project ```bash forge init horizen-token && cd horizen-token ``` ### Install OpenZeppelin Contracts ```bash forge install OpenZeppelin/openzeppelin-contracts --no-commit ``` Add the remapping so Forge can resolve the import path: ```bash echo '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/' >> remappings.txt ``` ### Place the contract Save the Solidity above to `src/HorizenToken.sol`. ### Test it locally first ```solidity // test/HorizenToken.t.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {Test} from "forge-std/Test.sol"; import {HorizenToken} from "../src/HorizenToken.sol"; contract HorizenTokenTest is Test { HorizenToken token; address owner = address(0xBEEF); address user = address(0xCAFE); uint256 constant CAP = 1_000_000 ether; // 1M tokens function setUp() public { token = new HorizenToken("Horizen Token", "HZN", CAP, owner); } function test_MintUnderCap() public { vm.prank(owner); token.mint(user, 500_000 ether); assertEq(token.balanceOf(user), 500_000 ether); assertEq(token.totalSupply(), 500_000 ether); } function test_MintOverCapReverts() public { vm.prank(owner); vm.expectRevert(); token.mint(user, CAP + 1); } function test_OnlyOwnerCanMint() public { vm.prank(user); vm.expectRevert(); token.mint(user, 1 ether); } function test_Burn() public { vm.prank(owner); token.mint(user, 100 ether); vm.prank(user); token.burn(50 ether); assertEq(token.balanceOf(user), 50 ether); } } ``` ```bash forge test -vvv ``` Don't deploy until tests pass. Gas costs ETH on testnet; time costs more. ### Build ```bash forge build ``` ### Deploy to Horizen Testnet ```bash forge create src/HorizenToken.sol:HorizenToken \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --private-key $PRIVATE_KEY \ --constructor-args "Horizen Token" "HZN" 1000000000000000000000000 $OWNER_ADDRESS ``` Constructor args breakdown: - `"Horizen Token"` — token name - `"HZN"` — symbol - `1000000000000000000000000` — cap: 1,000,000 tokens × 1e18 (must be passed as a raw `uint256` wei value, not a decimal) - `$OWNER_ADDRESS` — your intended owner; if you want `msg.sender`, pass `$(cast wallet address --private-key $PRIVATE_KEY)` Forge prints the deployed address on success: ``` Deployer: 0x... Deployed to: 0x... Transaction hash: 0x... ``` ### Deploy with a script (recommended for production) Rather than passing private keys on the command line, use a Forge script with a hardware wallet or environment variable: ```solidity // script/Deploy.s.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {Script, console} from "forge-std/Script.sol"; import {HorizenToken} from "../src/HorizenToken.sol"; contract DeployScript is Script { function run() external { uint256 deployerKey = vm.envUint("PRIVATE_KEY"); address owner = vm.envAddress("OWNER_ADDRESS"); vm.startBroadcast(deployerKey); HorizenToken token = new HorizenToken( "Horizen Token", "HZN", 1_000_000 ether, owner ); console.log("HorizenToken deployed at:", address(token)); console.log("Owner:", token.owner()); console.log("Cap:", token.cap()); vm.stopBroadcast(); } } ``` ```bash forge script script/Deploy.s.sol:DeployScript \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --broadcast \ --verify ``` The `--verify` flag submits source code to the explorer automatically after deployment. If verification fails (network timing), rerun with `--resume`. ## Path B — Hardhat ### Initialize the project ```bash mkdir horizen-token && cd horizen-token npm init -y npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox npm install @openzeppelin/contracts npx hardhat init ``` Select "TypeScript project" when prompted. ### Configure Hardhat (`hardhat.config.ts`) ```typescript import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { solidity: { version: "0.8.22", settings: { optimizer: { enabled: true, runs: 200 }, }, }, networks: { horizen: { type: "http", url: "https://horizen-testnet.rpc.caldera.xyz/http", accounts: [process.env.PRIVATE_KEY ?? ""], chainId: 2651420, }, "horizen-mainnet": { type: "http", url: "https://horizen.calderachain.xyz/http", accounts: [process.env.PRIVATE_KEY ?? ""], chainId: 26514, }, }, }; export default config; ``` ### Write the Ignition deploy module ```typescript // ignition/modules/HorizenToken.ts import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; import { parseEther } from "ethers"; export default buildModule("HorizenToken", (m) => { const owner = m.getParameter("owner", "0xYourOwnerAddress"); const token = m.contract("HorizenToken", [ "Horizen Token", "HZN", parseEther("1000000"), // 1M token cap owner, ]); return { token }; }); ``` ### Deploy ```bash npx hardhat compile npx hardhat ignition deploy ignition/modules/HorizenToken.ts \ --network horizen \ --parameters '{"owner": "0xYourOwnerAddress"}' ``` Ignition records deployment state in `ignition/deployments/` — rerunning the command is idempotent. The deployed address is in `ignition/deployments/chain-2651420/deployed_addresses.json`. --- ## Verifying on the Explorer Verification links your contract's source code to the on-chain bytecode, enabling the explorer to decode transactions and display the ABI. **With Foundry:** ```bash forge verify-contract \ src/HorizenToken.sol:HorizenToken \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --constructor-args $(cast abi-encode "constructor(string,string,uint256,address)" "Horizen Token" "HZN" 1000000000000000000000000 $OWNER_ADDRESS) ``` **With Hardhat:** ```bash npx hardhat verify --network horizen \ "Horizen Token" "HZN" "1000000000000000000000000" $OWNER_ADDRESS ``` > Horizen's explorer is powered by Blockscout. If `hardhat-verify` doesn't auto-detect the verifier, add `etherscan: { apiKey: { horizen: "any-non-empty-string" }, customChains: [{ ... }] }` to your Hardhat config pointing at the explorer's verification API. --- ## Interacting with the Deployed Contract Use `cast` (Foundry) for quick on-chain reads and writes: ```bash # Read name cast call $TOKEN "name()(string)" \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http # Read total supply cast call $TOKEN "totalSupply()(uint256)" \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http # Mint 100 tokens to an address (as owner) cast send $TOKEN "mint(address,uint256)" \ $RECIPIENT 100000000000000000000 \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --private-key $PRIVATE_KEY ``` --- ## Deploy an NFT Collection # Deploy an NFT Collection on Horizen Horizen runs a near-vanilla EVM, so every Ethereum opcode works, and the standard ERC-721 and ERC-1155 patterns apply without modification. The OP Stack adds a small L1 data fee on top of your execution gas, but nothing about the NFT contract or minting flow changes from what you'd write for Base or Ethereum mainnet. This tutorial builds a production-grade ERC-721 collection with: - On-chain supply cap - Public mint with per-wallet limit - Owner-controlled reveal (pre-reveal URI → post-reveal base URI) - `ERC721Enumerable` for on-chain enumeration (useful for indexers and dApps that need to list tokens by owner) - Withdrawal of mint proceeds ## Architecture Note NFT metadata URIs typically point to IPFS or a centralized server. Horizen has no built-in IPFS node or pinning service — you're responsible for hosting your metadata. [Pinata](https://pinata.cloud), [NFT.Storage](https://nft.storage), and [Thirdweb Storage](https://thirdweb.com/dashboard/infrastructure/storage) all work fine; there's nothing Horizen-specific here. What Horizen does give you: fast block times (OP Stack block every 2 seconds), low fees, and full EVM compatibility. ## Step 1: The Contract ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @title HorizenNFT /// @notice A capped ERC-721 collection with public mint, per-wallet limits, /// and a reveal mechanic. contract HorizenNFT is ERC721Enumerable, ERC721URIStorage, Ownable, ReentrancyGuard { uint256 public immutable MAX_SUPPLY; uint256 public mintPrice; uint256 public maxPerWallet; bool public revealed; string private _baseTokenURI; // post-reveal base URI string private _preRevealURI; // single URI for all tokens pre-reveal uint256 private _nextTokenId; // Per-wallet mint count tracking mapping(address => uint256) public mintedByWallet; event Revealed(string baseURI); event Minted(address indexed to, uint256 indexed tokenId); event Withdrawn(address indexed to, uint256 amount); error SoldOut(); error WalletLimitExceeded(); error InsufficientPayment(); error MintAmountZero(); error WithdrawFailed(); constructor( string memory name, string memory symbol, uint256 maxSupply, uint256 _mintPrice, uint256 _maxPerWallet, string memory preRevealURI, address initialOwner ) ERC721(name, symbol) Ownable(initialOwner) { MAX_SUPPLY = maxSupply; mintPrice = _mintPrice; maxPerWallet = _maxPerWallet; _preRevealURI = preRevealURI; } // ── Public Mint ───────────────────────────────────────────────────────── /// @notice Mint `amount` tokens. Reverts if supply, wallet cap, or payment /// conditions are not met. function mint(uint256 amount) external payable nonReentrant { if (amount == 0) revert MintAmountZero(); if (_nextTokenId + amount > MAX_SUPPLY) revert SoldOut(); if (mintedByWallet[msg.sender] + amount > maxPerWallet) revert WalletLimitExceeded(); if (msg.value < mintPrice * amount) revert InsufficientPayment(); mintedByWallet[msg.sender] += amount; for (uint256 i = 0; i < amount; i++) { uint256 tokenId = _nextTokenId++; _safeMint(msg.sender, tokenId); emit Minted(msg.sender, tokenId); } } // ── Owner Functions ────────────────────────────────────────────────────── /// @notice Mint directly to an address (reserve/team allocation). No fee. function ownerMint(address to, uint256 amount) external onlyOwner { if (_nextTokenId + amount > MAX_SUPPLY) revert SoldOut(); for (uint256 i = 0; i < amount; i++) { uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); } } /// @notice Reveal the collection. Sets the base URI for all tokens. /// Irreversible — once revealed, the pre-reveal URI is abandoned. function reveal(string calldata baseURI) external onlyOwner { _baseTokenURI = baseURI; revealed = true; emit Revealed(baseURI); } /// @notice Update the mint price. function setMintPrice(uint256 newPrice) external onlyOwner { mintPrice = newPrice; } /// @notice Update the per-wallet mint limit. function setMaxPerWallet(uint256 newMax) external onlyOwner { maxPerWallet = newMax; } /// @notice Withdraw all ETH from the contract to `to`. function withdraw(address payable to) external onlyOwner nonReentrant { uint256 balance = address(this).balance; (bool ok, ) = to.call{value: balance}(""); if (!ok) revert WithdrawFailed(); emit Withdrawn(to, balance); } // ── Metadata ───────────────────────────────────────────────────────────── /// @dev Pre-reveal: all tokens return the same placeholder URI. /// Post-reveal: baseURI + tokenId + ".json" function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) { _requireOwned(tokenId); if (!revealed) { return _preRevealURI; } return string(abi.encodePacked(_baseTokenURI, _toString(tokenId), ".json")); } function _baseURI() internal view override returns (string memory) { return _baseTokenURI; } // ── Required Overrides ──────────────────────────────────────────────────── // ERC721Enumerable and ERC721URIStorage both override _update and supportsInterface. // Solidity requires explicit resolution. function _update(address to, uint256 tokenId, address auth) internal override(ERC721, ERC721Enumerable) returns (address) { return super._update(to, tokenId, auth); } function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) { super._increaseBalance(account, value); } function supportsInterface(bytes4 interfaceId) public view override(ERC721Enumerable, ERC721URIStorage) returns (bool) { return super.supportsInterface(interfaceId); } // ── Helpers ─────────────────────────────────────────────────────────────── function totalMinted() external view returns (uint256) { return _nextTokenId; } function remainingSupply() external view returns (uint256) { return MAX_SUPPLY - _nextTokenId; } /// @dev Converts uint256 to string. Avoids importing full Strings library. function _toString(uint256 value) internal pure returns (string memory) { if (value == 0) return "0"; uint256 temp = value; uint256 digits; while (temp != 0) { digits++; temp /= 10; } bytes memory buffer = new bytes(digits); while (value != 0) { digits -= 1; buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); value /= 10; } return string(buffer); } } ``` ## Step 2: Foundry Setup ```bash forge init horizen-nft && cd horizen-nft forge install OpenZeppelin/openzeppelin-contracts --no-commit echo '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/' >> remappings.txt ``` Save the contract to `src/HorizenNFT.sol`. ### Tests ```solidity // test/HorizenNFT.t.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {Test} from "forge-std/Test.sol"; import {HorizenNFT} from "../src/HorizenNFT.sol"; contract HorizenNFTTest is Test { HorizenNFT nft; address owner = address(0xBEEF); address alice = address(0xA11CE); address bob = address(0xB0B); uint256 constant SUPPLY = 1000; uint256 constant PRICE = 0.01 ether; uint256 constant PER_WALLET = 5; function setUp() public { nft = new HorizenNFT( "Horizen NFT", "HNFT", SUPPLY, PRICE, PER_WALLET, "ipfs://QmPreReveal", owner ); vm.deal(alice, 10 ether); vm.deal(bob, 10 ether); } function test_MintSuccess() public { vm.prank(alice); nft.mint{value: PRICE * 2}(2); assertEq(nft.balanceOf(alice), 2); assertEq(nft.totalMinted(), 2); } function test_PreRevealURI() public { vm.prank(alice); nft.mint{value: PRICE}(1); assertEq(nft.tokenURI(0), "ipfs://QmPreReveal"); } function test_RevealUpdatesURI() public { vm.prank(alice); nft.mint{value: PRICE}(1); vm.prank(owner); nft.reveal("ipfs://QmBaseURI/"); assertEq(nft.tokenURI(0), "ipfs://QmBaseURI/0.json"); } function test_WalletLimitReverts() public { vm.prank(alice); nft.mint{value: PRICE * 5}(5); // Max vm.prank(alice); vm.expectRevert(HorizenNFT.WalletLimitExceeded.selector); nft.mint{value: PRICE}(1); // Over limit } function test_InsufficientPaymentReverts() public { vm.prank(alice); vm.expectRevert(HorizenNFT.InsufficientPayment.selector); nft.mint{value: PRICE - 1}(1); } function test_Withdraw() public { vm.prank(alice); nft.mint{value: PRICE * 3}(3); uint256 ownerBefore = owner.balance; vm.prank(owner); nft.withdraw(payable(owner)); assertEq(owner.balance, ownerBefore + PRICE * 3); assertEq(address(nft).balance, 0); } function test_SoldOutReverts() public { // Fill remaining supply via ownerMint (faster than individual mints) vm.prank(owner); nft.ownerMint(bob, SUPPLY); vm.prank(alice); vm.expectRevert(HorizenNFT.SoldOut.selector); nft.mint{value: PRICE}(1); } } ``` ```bash forge test -vvv ``` ### Deploy script ```solidity // script/Deploy.s.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {Script, console} from "forge-std/Script.sol"; import {HorizenNFT} from "../src/HorizenNFT.sol"; contract DeployScript is Script { function run() external { uint256 deployerKey = vm.envUint("PRIVATE_KEY"); address owner = vm.envAddress("OWNER_ADDRESS"); vm.startBroadcast(deployerKey); HorizenNFT nft = new HorizenNFT( "Horizen NFT", // name "HNFT", // symbol 10_000, // max supply 0.01 ether, // mint price 5, // max per wallet "ipfs://QmYourPreRevealCID",// pre-reveal URI — upload to IPFS first owner ); console.log("HorizenNFT deployed at:", address(nft)); vm.stopBroadcast(); } } ``` ```bash forge script script/Deploy.s.sol:DeployScript \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --broadcast ``` ## Step 3: Prepare Your Metadata Your metadata must be ready before you deploy or reveal. The standard structure for each token (`0.json`, `1.json`, etc.): ```json { "name": "Horizen NFT #0", "description": "A token in the Horizen NFT collection.", "image": "ipfs://QmYourImageCID/0.png", "attributes": [ { "trait_type": "Background", "value": "Blue" }, { "trait_type": "Rarity", "value": "Common" } ] } ``` Upload the entire `metadata/` folder to IPFS. The returned folder CID becomes your base URI: ``` ipfs://QmFolderCID/ ``` After upload, call `reveal`: ```bash cast send $NFT_ADDRESS "reveal(string)" "ipfs://QmFolderCID/" \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --private-key $PRIVATE_KEY ``` Verify: ```bash cast call $NFT_ADDRESS "tokenURI(uint256)(string)" 0 \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http # Expected: ipfs://QmFolderCID/0.json ``` ## Hardhat Path (Condensed) ```bash mkdir horizen-nft && cd horizen-nft npm init -y npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox npm install @openzeppelin/contracts npx hardhat init ``` `hardhat.config.ts`: ```typescript import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { solidity: "0.8.22", networks: { horizen: { type: "http", url: "https://horizen-testnet.rpc.caldera.xyz/http", accounts: [process.env.PRIVATE_KEY ?? ""], chainId: 2651420, }, }, }; export default config; ``` Ignition module: ```typescript // ignition/modules/HorizenNFT.ts import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; import { parseEther } from "ethers"; export default buildModule("HorizenNFT", (m) => { const nft = m.contract("HorizenNFT", [ "Horizen NFT", "HNFT", 10_000, parseEther("0.01"), 5, "ipfs://QmYourPreRevealCID", m.getParameter("owner"), ]); return { nft }; }); ``` ```bash npx hardhat compile npx hardhat ignition deploy ignition/modules/HorizenNFT.ts \ --network horizen \ --parameters '{"owner": "0xYourOwnerAddress"}' ``` --- ## Build a Price-Triggered ETH Escrow This tutorial builds an escrow contract that locks ETH and releases it to a recipient when ETH/USD crosses a price threshold, using the Stork pull oracle for on-chain price data. You'll write the contract, test it against a mock oracle, deploy it to testnet or mainnet, and build a server-side-safe Next.js frontend with Wagmi. ## Architecture Context Two concepts govern this dApp's design. **Pull oracle model.** Stork does not push prices on-chain automatically. Instead, callers fetch a signed price payload from the Stork REST API off-chain and include it as calldata in the transaction that needs the price. The contract passes that payload to `stork.updateTemporalNumericValuesV1()`, which verifies the publisher's ECDSA signature and writes the price. The contract then reads it back with `stork.getTemporalNumericValueV1()`. This means anyone can trigger the release, because anyone can fetch a fresh price and submit it. **Fee model.** Horizen L3 has two fee components: - **L2 execution fee** - gas used × base fee - **L1 data fee** - calldata batch cost, appended automatically by the OP Stack The Stork oracle itself also charges a small per-update fee (`singleUpdateFeeInWei`, query `getUpdateFeeV1` at runtime). On testnet this is 1 wei. Your `triggerCheck` call must forward at least that amount via `msg.value`. ## Network Reference For RPC endpoints, chain IDs, explorer, and faucet: - [Testnet Configuration](/horizen-chain/network/testnet) - [Mainnet Configuration](/horizen-chain/network/mainnet) ## Step 1: Define the Stork Interface Stork's on-chain contract is deployed at `0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62` on both Horizen testnet and mainnet. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IStork { struct TemporalNumericValue { uint64 timestampNs; int192 quantizedValue; // price scaled by 1e18 } struct TemporalNumericValueInput { TemporalNumericValue temporalNumericValue; bytes32 id; bytes32 publisherMerkleRoot; bytes32 valueComputeAlgHash; bytes32 r; bytes32 s; uint8 v; } function updateTemporalNumericValuesV1( TemporalNumericValueInput[] calldata updateData ) external payable; function getUpdateFeeV1( TemporalNumericValueInput[] calldata updateData ) external view returns (uint256 feeAmount); function getTemporalNumericValueV1(bytes32 id) external view returns (TemporalNumericValue memory value); } ``` Key facts about the Stork data model: - `quantizedValue` is `int192` scaled to 18 decimals. Example: $3,500 is `3500e18`. - `timestampNs` is a nanosecond Unix timestamp (`uint64`). It's a 19-digit integer that **exceeds float64 precision**. See [Step 5](#step-5-server-side-price-proxy) for why this matters in JavaScript. - The ETH/USD asset ID on all networks: `0x59102b37de83bdda9f38ac8254e596f0d9ac61d2035c07936675e87342817160` ## Step 2: Write the Contract ```solidity // src/TriggerVault.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract TriggerVault { bytes32 public constant ETH_USD_ID = 0x59102b37de83bdda9f38ac8254e596f0d9ac61d2035c07936675e87342817160; IStork public immutable stork; enum Direction { Above, Below } struct Escrow { address depositor; address recipient; uint256 amount; int256 targetPrice; // USD, 18 decimals Direction direction; bool active; } uint256 public nextId; mapping(uint256 => Escrow) public escrows; event EscrowCreated(uint256 indexed id, address indexed depositor, address indexed recipient, uint256 amount, int256 targetPrice, Direction direction); event EscrowReleased(uint256 indexed id, address indexed recipient, uint256 amount, int256 triggerPrice); event EscrowCancelled(uint256 indexed id, address indexed depositor, uint256 amount); error NotDepositor(); error EscrowNotActive(); error ConditionNotMet(); error TransferFailed(); error ZeroAmount(); error ZeroAddress(); constructor(address _stork) { stork = IStork(_stork); } function createEscrow( address recipient, int256 targetPrice, Direction direction ) external payable returns (uint256 id) { if (msg.value == 0) revert ZeroAmount(); if (recipient == address(0)) revert ZeroAddress(); id = nextId++; escrows[id] = Escrow({ depositor: msg.sender, recipient: recipient, amount: msg.value, targetPrice: targetPrice, direction: direction, active: true }); emit EscrowCreated(id, msg.sender, recipient, msg.value, targetPrice, direction); } /// @notice Push a fresh Stork price on-chain and release ETH if the condition is met. /// @param updateData Signed payload from GET /v1/prices/latest?assets=ETHUSD /// @dev msg.value must cover the Stork update fee (query getUpdateFeeV1; 1 wei on testnet). function triggerCheck( uint256 id, IStork.TemporalNumericValueInput[] calldata updateData ) external payable { Escrow storage e = escrows[id]; if (!e.active) revert EscrowNotActive(); stork.updateTemporalNumericValuesV1{value: msg.value}(updateData); IStork.TemporalNumericValue memory tv = stork.getTemporalNumericValueV1(ETH_USD_ID); int256 currentPrice = int256(tv.quantizedValue); bool conditionMet = e.direction == Direction.Above ? currentPrice >= e.targetPrice : currentPrice <= e.targetPrice; if (!conditionMet) revert ConditionNotMet(); e.active = false; uint256 amount = e.amount; emit EscrowReleased(id, e.recipient, amount, currentPrice); (bool ok,) = e.recipient.call{value: amount}(""); if (!ok) revert TransferFailed(); } function cancelEscrow(uint256 id) external { Escrow storage e = escrows[id]; if (!e.active) revert EscrowNotActive(); if (e.depositor != msg.sender) revert NotDepositor(); e.active = false; uint256 amount = e.amount; emit EscrowCancelled(id, msg.sender, amount); (bool ok,) = msg.sender.call{value: amount}(""); if (!ok) revert TransferFailed(); } function getEscrowsByDepositor(address depositor) external view returns (uint256[] memory ids) { uint256 count; for (uint256 i = 0; i < nextId; i++) { if (escrows[i].depositor == depositor) count++; } ids = new uint256[](count); uint256 j; for (uint256 i = 0; i < nextId; i++) { if (escrows[i].depositor == depositor) ids[j++] = i; } } } ``` Design decisions worth noting: - `triggerCheck` is **permissionless** so any account can call it. ETH always goes to `e.recipient`, not to the caller, so there's no griefing risk from third-party triggering. - `msg.value` is forwarded directly to `updateTemporalNumericValuesV1`. Do not hardcode 0 because the Stork fee is a runtime value and may change. - ETH transfer uses a low-level `call` rather than `transfer` to avoid gas-limit issues with contract recipients. ## Step 3: Test Locally with MockStork Foundry tests should never hit a live oracle. Use a minimal mock that implements the same interface. ```solidity // test/TriggerVault.t.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {TriggerVault, IStork} from "../src/TriggerVault.sol"; contract MockStork { int192 public price; function setPrice(int192 _price) external { price = _price; } function updateTemporalNumericValuesV1( IStork.TemporalNumericValueInput[] calldata ) external payable {} function getUpdateFeeV1( IStork.TemporalNumericValueInput[] calldata ) external pure returns (uint256) { return 0; } function getTemporalNumericValueV1(bytes32) external view returns (IStork.TemporalNumericValue memory) { return IStork.TemporalNumericValue({ timestampNs: uint64(block.timestamp * 1e9), quantizedValue: price }); } } contract TriggerVaultTest is Test { TriggerVault vault; MockStork stork; address depositor = makeAddr("depositor"); address recipient = makeAddr("recipient"); int256 constant TARGET = 3500e18; function setUp() public { stork = new MockStork(); vault = new TriggerVault(address(stork)); vm.deal(depositor, 10 ether); } function test_triggerReleasesWhenConditionMet() public { vm.prank(depositor); uint256 id = vault.createEscrow{value: 1 ether}( recipient, TARGET, TriggerVault.Direction.Above ); stork.setPrice(int192(3600e18)); vault.triggerCheck(id, new IStork.TemporalNumericValueInput[](0)); (,,,,,bool active) = vault.escrows(id); assertFalse(active); assertEq(recipient.balance, 1 ether); } function test_triggerRevertsWhenConditionNotMet() public { vm.prank(depositor); uint256 id = vault.createEscrow{value: 1 ether}( recipient, TARGET, TriggerVault.Direction.Above ); stork.setPrice(int192(3000e18)); vm.expectRevert(TriggerVault.ConditionNotMet.selector); vault.triggerCheck(id, new IStork.TemporalNumericValueInput[](0)); } function test_cancelReturnsETH() public { vm.prank(depositor); uint256 id = vault.createEscrow{value: 1 ether}(recipient, TARGET, TriggerVault.Direction.Above); vm.prank(depositor); vault.cancelEscrow(id); assertEq(depositor.balance, 10 ether); } function test_cancelRevertsForNonDepositor() public { vm.prank(depositor); uint256 id = vault.createEscrow{value: 1 ether}(recipient, TARGET, TriggerVault.Direction.Above); vm.expectRevert(TriggerVault.NotDepositor.selector); vault.cancelEscrow(id); } } ``` Run the suite: ```bash forge test -vvv ``` Don't deploy until all tests pass. Gas costs ETH on testnet but a broken oracle integration costs more. ## Step 4: Deploy Write a deploy script so private keys never appear on the command line. ```solidity // script/Deploy.s.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Script, console} from "forge-std/Script.sol"; import {TriggerVault} from "../src/TriggerVault.sol"; contract DeployScript is Script { // Stork oracle — same address on Horizen testnet and mainnet address constant STORK = 0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62; function run() external { uint256 pk = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(pk); TriggerVault vault = new TriggerVault(STORK); console.log("TriggerVault deployed at:", address(vault)); vm.stopBroadcast(); } } ``` Add both Horizen RPC aliases to `foundry.toml`: ```toml [rpc_endpoints] horizen_testnet = "https://horizen-testnet.rpc.caldera.xyz/http" horizen_mainnet = "https://horizen.calderachain.xyz/http" ``` Deploy to testnet: ```bash forge script script/Deploy.s.sol:DeployScript \ --rpc-url horizen_testnet \ --broadcast \ --private-key $PRIVATE_KEY ``` Deploy to mainnet: ```bash forge script script/Deploy.s.sol:DeployScript \ --rpc-url horizen_mainnet \ --broadcast \ --private-key $PRIVATE_KEY ``` Save the printed address. You'll set it as `NEXT_PUBLIC_CONTRACT_ADDRESS` in your frontend. ## Step 5: Server-Side Price Proxy The Stork REST API requires an API key. Never expose it client-side. Use a Next.js API route as a server-side proxy. ```typescript // app/api/stork-price/route.ts import { NextResponse } from "next/server"; const STORK_API = "https://rest.jp.stork-oracle.network/v1/prices/latest"; const ETH_USD_ASSET = "ETHUSD"; export async function GET() { const apiKey = process.env.STORK_API_KEY; if (!apiKey) { return NextResponse.json({ error: "STORK_API_KEY not set" }, { status: 500 }); } const res = await fetch(`${STORK_API}?assets=${ETH_USD_ASSET}`, { headers: { Authorization: `Basic ${apiKey}` }, cache: "no-store", }); if (!res.ok) { return NextResponse.json({ error: `Stork API error: ${res.status}` }, { status: res.status }); } // IMPORTANT: use res.text() + regex, not res.json(). // timestampNs is a 19-digit integer. JavaScript's JSON.parse converts it to // float64, silently rounding it by up to 215ns. The Stork oracle's ECDSA // signature is computed over the exact integer — a rounded value produces // a hash mismatch and the oracle reverts with InvalidSignature (0x8baa579f). const rawText = await res.text(); const safeText = rawText.replace(/:(\s*)(-?\d{16,})([,}\]])/g, `:$1"$2"$3`); const body = JSON.parse(safeText); const entry = body?.data?.[ETH_USD_ASSET]; if (!entry) { return NextResponse.json({ error: "No ETHUSD data in response" }, { status: 502 }); } const sp = entry.stork_signed_price; const updateData = [{ temporalNumericValue: { timestampNs: sp.timestamped_signature.timestamp, // string — preserved precision quantizedValue: sp.price, // string — preserved precision }, id: sp.encoded_asset_id as `0x${string}`, publisherMerkleRoot: sp.publisher_merkle_root as `0x${string}`, valueComputeAlgHash: (sp.calculation_alg.checksum.startsWith("0x") ? sp.calculation_alg.checksum : `0x${sp.calculation_alg.checksum}`) as `0x${string}`, r: sp.timestamped_signature.signature.r as `0x${string}`, s: sp.timestamped_signature.signature.s as `0x${string}`, v: Number(sp.timestamped_signature.signature.v), }]; return NextResponse.json({ updateData }); } ``` The precision requirement is non-negotiable: `timestampNs` and `quantizedValue` must survive JSON serialisation as strings and be converted to `BigInt` on the frontend. Passing rounded integers into `updateTemporalNumericValuesV1` causes `InvalidSignature`. The oracle hashes these exact values before recovering the publisher's signing address. ## Step 6: Frontend Integration Create `src/lib/contract.ts` with the addresses for your target network: ```typescript // src/lib/contract.ts export const TRIGGER_VAULT_ADDRESS = process.env .NEXT_PUBLIC_CONTRACT_ADDRESS as `0x${string}`; // Same on testnet and mainnet export const STORK_ADDRESS = "0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62" as const; ``` ### Creating an escrow ```typescript import { useWriteContract } from "wagmi"; import { parseEther } from "viem"; import { triggerVaultAbi } from "@/lib/abi"; import { TRIGGER_VAULT_ADDRESS } from "@/lib/contract"; const { writeContractAsync } = useWriteContract(); async function handleCreate( recipient: `0x${string}`, targetPriceUsd: number, // e.g. 3500 direction: 0 | 1 // 0 = Above, 1 = Below ) { await writeContractAsync({ address: TRIGGER_VAULT_ADDRESS, abi: triggerVaultAbi, functionName: "createEscrow", args: [ recipient, BigInt(Math.round(targetPriceUsd * 1e18)), // scale to 18 decimals direction, ], value: parseEther("0.01"), // ETH to lock in the escrow }); } ``` ### Reading escrows ```typescript import { useReadContract } from "wagmi"; import { triggerVaultAbi } from "@/lib/abi"; import { TRIGGER_VAULT_ADDRESS } from "@/lib/contract"; const { data: escrowIds } = useReadContract({ address: TRIGGER_VAULT_ADDRESS, abi: triggerVaultAbi, functionName: "getEscrowsByDepositor", args: [address], }); ``` ### Triggering an escrow Fetch the signed price, simulate the call to surface revert reasons early, then send it. ```typescript import { useAccount, useWriteContract, usePublicClient } from "wagmi"; import { triggerVaultAbi, storkAbi } from "@/lib/abi"; import { TRIGGER_VAULT_ADDRESS, STORK_ADDRESS } from "@/lib/contract"; // Shape of each item returned by /api/stork-price interface UpdateDataRaw { temporalNumericValue: { timestampNs: string; quantizedValue: string }; id: `0x${string}`; publisherMerkleRoot: `0x${string}`; valueComputeAlgHash: `0x${string}`; r: `0x${string}`; s: `0x${string}`; v: number; } const { address } = useAccount(); const { writeContractAsync } = useWriteContract(); const publicClient = usePublicClient(); async function handleTrigger(escrowId: bigint) { const res = await fetch("/api/stork-price"); const { updateData } = await res.json(); // Convert string fields to BigInt before passing to viem const updateDataTyped = (updateData as UpdateDataRaw[]).map((d) => ({ ...d, temporalNumericValue: { timestampNs: BigInt(d.temporalNumericValue.timestampNs), quantizedValue: BigInt(d.temporalNumericValue.quantizedValue), }, })); // Query the required Stork fee at runtime — do not hardcode const storkFee = await publicClient!.readContract({ address: STORK_ADDRESS, abi: storkAbi, functionName: "getUpdateFeeV1", args: [updateDataTyped], }) as bigint; const callParams = { address: TRIGGER_VAULT_ADDRESS, abi: triggerVaultAbi, functionName: "triggerCheck" as const, args: [escrowId, updateDataTyped] as const, value: storkFee, }; // Simulate first — surfaces ConditionNotMet, InvalidSignature, InsufficientFee // as readable errors before the wallet popup if (publicClient && address) { await publicClient.simulateContract({ ...callParams, account: address }); } await writeContractAsync(callParams); } ``` Three things to get right in the trigger call: - **`value: storkFee`** - must forward at least `getUpdateFeeV1()` wei to the Stork oracle. The snippet above queries it at runtime before the call; passing 0 reverts with `InsufficientFee (0x025dbdd4)`. - **BigInt conversion** - `timestampNs` and `quantizedValue` come back from the API route as strings. Convert them with `BigInt()` before passing to viem; viem encodes them as `uint64` and `int192` respectively. - **`simulateContract` before `writeContractAsync`** - catches `ConditionNotMet` and oracle errors before the wallet confirmation dialog, giving the user a readable error instead of a failed transaction. ### ABI reference Create `src/lib/abi.ts`. The struct components for `updateData` must match the Solidity interface defined in Step 1 exactly as `triggerCheck` and `getUpdateFeeV1` both take the same `TemporalNumericValueInput[]`. ```typescript // src/lib/abi.ts // Shared struct components for TemporalNumericValueInput const updateDataComponents = [ { name: "temporalNumericValue", type: "tuple", components: [ { name: "timestampNs", type: "uint64" }, { name: "quantizedValue", type: "int192" }, ], }, { name: "id", type: "bytes32" }, { name: "publisherMerkleRoot", type: "bytes32" }, { name: "valueComputeAlgHash", type: "bytes32" }, { name: "r", type: "bytes32" }, { name: "s", type: "bytes32" }, { name: "v", type: "uint8" }, ] as const; export const triggerVaultAbi = [ { name: "createEscrow", type: "function", inputs: [ { name: "recipient", type: "address" }, { name: "targetPrice", type: "int256" }, { name: "direction", type: "uint8" }, // 0 = Above, 1 = Below ], outputs: [{ name: "id", type: "uint256" }], stateMutability: "payable", }, { name: "triggerCheck", type: "function", inputs: [ { name: "id", type: "uint256" }, { name: "updateData", type: "tuple[]", components: updateDataComponents }, ], outputs: [], stateMutability: "payable", }, { name: "cancelEscrow", type: "function", inputs: [{ name: "id", type: "uint256" }], outputs: [], stateMutability: "nonpayable", }, { name: "getEscrowsByDepositor", type: "function", inputs: [{ name: "depositor", type: "address" }], outputs: [{ name: "ids", type: "uint256[]" }], stateMutability: "view", }, ] as const; export const storkAbi = [ { name: "getUpdateFeeV1", type: "function", inputs: [ { name: "updateData", type: "tuple[]", components: updateDataComponents }, ], outputs: [{ name: "feeAmount", type: "uint256" }], stateMutability: "view", }, ] as const; ``` ## Step 7: Verify on the Explorer Testnet: ```bash forge verify-contract $DEPLOYED_ADDRESS src/TriggerVault.sol:TriggerVault \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --constructor-args $(cast abi-encode "constructor(address)" $STORK_ADDRESS) ``` Mainnet: ```bash forge verify-contract $DEPLOYED_ADDRESS src/TriggerVault.sol:TriggerVault \ --rpc-url https://horizen.calderachain.xyz/http \ --constructor-args $(cast abi-encode "constructor(address)" $STORK_ADDRESS) ``` Horizen's explorer is powered by Blockscout. If verification doesn't auto-detect the verifier, it accepts any non-empty API key; add a `customChains` entry to your Hardhat config if using that path. ## Interacting On-Chain (cast) Set your RPC URL for the target network first: ```bash # Testnet export RPC_URL=https://horizen-testnet.rpc.caldera.xyz/http # Mainnet export RPC_URL=https://horizen.calderachain.xyz/http ``` ```bash export STORK=0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62 export ETH_USD_ID=0x59102b37de83bdda9f38ac8254e596f0d9ac61d2035c07936675e87342817160 # Read the latest ETH/USD price from the Stork oracle cast call $STORK "getTemporalNumericValueV1(bytes32)(uint64,int192)" $ETH_USD_ID \ --rpc-url $RPC_URL # Read a specific escrow cast call $VAULT "escrows(uint256)((address,address,uint256,int256,uint8,bool))" 0 \ --rpc-url $RPC_URL # Cancel an escrow (depositor only) cast send $VAULT "cancelEscrow(uint256)" 0 \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY ``` ## Event Indexing The contract emits three events suitable for indexing with Goldsky or any subgraph-compatible pipeline: | Event | Fields | |---|---| | `EscrowCreated` | `id`, `depositor`, `recipient`, `amount`, `targetPrice`, `direction` | | `EscrowReleased` | `id`, `recipient`, `amount`, `triggerPrice` | | `EscrowCancelled` | `id`, `depositor`, `amount` | All three are indexed with `id` as the primary key, making historical queries efficient without a full table scan. --- ## Connect a Price Feed with Stork Oracle Stork is a pull-based oracle protocol designed for ultra-low latency. Unlike push oracles that maintain a continuously updated on-chain price, Stork lets your contract or off-chain application fetch a price on demand, then verify it trustlessly on-chain using a signed payload. This tutorial covers the full data flow: 1. Fetch a signed price update from the Stork REST API 2. Push that price on-chain to the Stork contract 3. Read the verified price from your own smart contract ## How Stork Works on Horizen Stork operates as a **pull oracle**. Prices are not continuously pushed on-chain - instead, your application fetches a cryptographically signed price payload from Stork's API, then submits it to the Stork on-chain contract to verify and store it. Other contracts read from that stored value. This model means gas is only spent when a price is actually needed, and freshness is guaranteed by the cryptographic signature rather than a heartbeat. ## Prerequisites - A Stork API key - request one at [stork.network](https://www.stork.network/) or their developer portal - A deployed contract on Horizen (or you can test with an EOA and cast/ethers.js) - Foundry or Hardhat for contract interaction - Node.js ≥ 18 ## Step 1: Identify Your Asset ID Stork identifies price feeds using an **asset ID** - a human-readable string like `BTCUSD`, `ETHUSD`, or `ZENUSD`. You can browse available feeds via the Stork REST API: ```bash curl -u ":" \ "https://rest.jp.stork-oracles.com/v1/prices/latest?assets=BTCUSD,ETHUSD" ``` The response contains the latest signed price with its encoded asset ID: ```json { "data": { "BTCUSD": { "timestamp": 1718000000000000000, "asset_id": "BTCUSD", "signature_type": "evm", "trigger": "delta", "price": "67500000000000000000000", "stork_signed_price": { "public_key": "0x...", "encoded_asset_id": "0x4254435553440000000000000000000000000000000000000000000000000000", "price": "67500000000000000000000", "timestamped_signature": { "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "timestamp": 1718000000000000000, "msg_hash": "0x..." }, "publisher_merkle_root": "0x...", "calculation_alg": { "type": "median", "version": "v1", "checksum": "0x..." } } } } } ``` Note the `encoded_asset_id` - this is the `bytes32` identifier used in all on-chain calls. ## Step 2: The Stork Contract Interface The Stork contract on Horizen exposes two core functions: ```solidity // Push a price update on-chain (verifies the signature, stores the value) function updateTemporalNumericValuesV1( StorkStructs.TemporalNumericValueInput[] calldata updateData ) external payable; // Read the latest stored price for an asset function getTemporalNumericValueV1( bytes32 id ) external view returns (StorkStructs.TemporalNumericValue memory value); ``` The `TemporalNumericValue` struct returned by the getter: ```solidity struct TemporalNumericValue { uint256 timestampNs; // Nanosecond timestamp of the price int128 quantizedValue; // Price scaled to 18 decimal places } ``` > **Price representation:** Stork prices use 18 decimal places. A BTC price of $67,500 is represented as `67500 * 1e18 = 67500000000000000000000`. Divide by `1e18` in your application logic. ## Step 3: Push a Price Update (Off-Chain Script) Here's a complete Node.js script that fetches a fresh price from the Stork API and pushes it on-chain: ```typescript import { ethers } from "ethers"; // ── Config ────────────────────────────────────────────────────────────────── const RPC_URL = "https://horizen-testnet.rpc.caldera.xyz/http"; const PRIVATE_KEY = process.env.PRIVATE_KEY!; const STORK_API_KEY = process.env.STORK_API_KEY!; const STORK_CONTRACT = "0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62"; // Horizen Stork contract const ASSET = "BTCUSD"; // ── Minimal ABI ───────────────────────────────────────────────────────────── const STORK_ABI = [ "function updateTemporalNumericValuesV1((bytes32 id, (uint256 timestampNs, int128 quantizedValue) temporalNumericValue, bytes32 publisherMerkleRoot, bytes32 valueComputeAlgHash, bytes signature)[] calldata updateData) external payable", "function getTemporalNumericValueV1(bytes32 id) external view returns (uint256 timestampNs, int128 quantizedValue)", "function getUpdateFeeV1((bytes32 id, (uint256 timestampNs, int128 quantizedValue) temporalNumericValue, bytes32 publisherMerkleRoot, bytes32 valueComputeAlgHash, bytes signature)[] calldata updateData) external view returns (uint256 feeAmount)", ]; // ── Fetch from Stork API ───────────────────────────────────────────────────── async function fetchStorkPrice(asset: string) { const res = await fetch( `https://rest.jp.stork-oracles.com/v1/prices/latest?assets=${asset}`, { headers: { Authorization: "Basic " + Buffer.from(`${STORK_API_KEY}:`).toString("base64"), }, } ); if (!res.ok) throw new Error(`Stork API error: ${res.status}`); const json = await res.json(); return json.data[asset].stork_signed_price; } // ── Build update payload ───────────────────────────────────────────────────── function buildUpdateData(signedPrice: any) { const { r, s, v } = signedPrice.timestamped_signature.signature; // Pack the ECDSA signature into 65 bytes: r (32) + s (32) + v (1) const signature = ethers.concat([r, s, ethers.toBeArray(v)]); return { id: signedPrice.encoded_asset_id, temporalNumericValue: { timestampNs: BigInt(signedPrice.timestamped_signature.timestamp), quantizedValue: BigInt(signedPrice.price), }, publisherMerkleRoot: signedPrice.publisher_merkle_root, valueComputeAlgHash: signedPrice.calculation_alg.checksum, signature, }; } // ── Main ───────────────────────────────────────────────────────────────────── async function main() { const provider = new ethers.JsonRpcProvider(RPC_URL); const signer = new ethers.Wallet(PRIVATE_KEY, provider); const stork = new ethers.Contract(STORK_CONTRACT, STORK_ABI, signer); console.log(`Fetching ${ASSET} price from Stork...`); const signedPrice = await fetchStorkPrice(ASSET); const updateData = [buildUpdateData(signedPrice)]; // Query the required fee (some Stork deployments charge a small fee per update) const fee = await stork.getUpdateFeeV1(updateData); console.log(`Update fee: ${ethers.formatEther(fee)} ETH`); // Push the price on-chain const tx = await stork.updateTemporalNumericValuesV1(updateData, { value: fee }); console.log(`Transaction submitted: ${tx.hash}`); await tx.wait(); console.log("Price updated on-chain."); // Read it back to verify const stored = await stork.getTemporalNumericValueV1(signedPrice.encoded_asset_id); const humanPrice = Number(stored.quantizedValue) / 1e18; console.log(`Stored price: $${humanPrice.toFixed(2)}`); console.log(`Timestamp: ${new Date(Number(stored.timestampNs) / 1e6).toISOString()}`); } main().catch(console.error); ``` Run it: ```bash PRIVATE_KEY=0x... STORK_API_KEY=your_key npx ts-node push-price.ts ``` ## Step 4: Consume the Price in Your Smart Contract Now write a Solidity contract that reads the Stork price and uses it in business logic. This example is a minimal price-gated contract that only accepts deposits when the asset price is above a threshold: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /// @dev Minimal Stork interface - only what we need interface IStork { struct TemporalNumericValue { uint256 timestampNs; int128 quantizedValue; } function getTemporalNumericValueV1(bytes32 id) external view returns (TemporalNumericValue memory); } contract PriceGatedVault { IStork public immutable stork; // bytes32 asset IDs - use the encoded_asset_id from the Stork API bytes32 public constant BTC_USD = 0x4254435553440000000000000000000000000000000000000000000000000000; // Price staleness tolerance: reject prices older than 60 seconds uint256 public constant MAX_PRICE_AGE_NS = 60 * 1e9; // Minimum BTC price (in USD, 18 decimals) to allow deposits int128 public constant MIN_PRICE = 50_000 * int128(1e18); mapping(address => uint256) public deposits; event Deposited(address indexed user, uint256 amount, int128 btcPrice); constructor(address _stork) { stork = IStork(_stork); } function deposit() external payable { IStork.TemporalNumericValue memory price = stork.getTemporalNumericValueV1(BTC_USD); // Check price freshness require( block.timestamp * 1e9 - price.timestampNs < MAX_PRICE_AGE_NS, "PriceGatedVault: stale price" ); // Check price threshold require( price.quantizedValue >= MIN_PRICE, "PriceGatedVault: BTC price too low" ); deposits[msg.sender] += msg.value; emit Deposited(msg.sender, msg.value, price.quantizedValue); } function getPrice() external view returns (int128 price, uint256 timestampNs) { IStork.TemporalNumericValue memory val = stork.getTemporalNumericValueV1(BTC_USD); return (val.quantizedValue, val.timestampNs); } } ``` ### Deploy with Foundry ```bash forge create src/PriceGatedVault.sol:PriceGatedVault \ --constructor-args 0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62 \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http \ --private-key $PRIVATE_KEY ``` ### Verify the price read ```bash cast call "getPrice()(int128,uint256)" \ --rpc-url https://horizen-testnet.rpc.caldera.xyz/http ``` ## Step 5: Keeping Prices Fresh (Automation) For production apps, you need a process that periodically pushes price updates so they're never stale when your contract checks them. Options: **Option A - Simple cron job** Run the push script on a schedule (e.g., every 30 seconds): ```bash # crontab entry: every minute * * * * * /usr/bin/node /app/push-price.js >> /var/log/stork-push.log 2>&1 ``` **Option B - Event-driven push (recommended)** Only push when a price update is actually needed (e.g., just before a user transaction). Have your frontend call the push script via a backend API endpoint, then submit the user's transaction: ```typescript // Frontend: push price, then call contract async function depositWithFreshPrice() { // 1. Push fresh price via your backend await fetch("/api/push-price", { method: "POST" }); // 2. Submit deposit transaction const tx = await vaultContract.deposit({ value: ethers.parseEther("0.1") }); await tx.wait(); } ``` **Option C - In-transaction push** Have the user's transaction include both the price update and the contract call in a single multicall. This is the most trustless approach - the price is pushed atomically with its consumption: ```solidity function depositWithPriceUpdate( IStork.TemporalNumericValueInput[] calldata updateData ) external payable { // Push the price in the same transaction (user bears the update fee) stork.updateTemporalNumericValuesV1{value: stork.getUpdateFeeV1(updateData)}(updateData); // Now read and use the freshly updated price IStork.TemporalNumericValue memory price = stork.getTemporalNumericValueV1(BTC_USD); require(price.quantizedValue >= MIN_PRICE, "Price too low"); deposits[msg.sender] += msg.value; } ``` ## Encoding Asset IDs If you need to compute the `bytes32` asset ID for a feed programmatically: ```typescript import { ethers } from "ethers"; // Stork encodes asset IDs as right-padded ASCII bytes32 function encodeAssetId(asset: string): string { return ethers.encodeBytes32String(asset); } // e.g. "BTCUSD" → "0x4254435553440000000000000000000000000000000000000000000000000000" console.log(encodeAssetId("BTCUSD")); ``` --- ## Index Your Contract with Goldsky [Goldsky](https://goldsky.com) is a high-performance blockchain data indexing platform. It lets you extract on-chain events from your Horizen smart contracts, transform them into queryable entities, and serve them over a GraphQL API without running your own infrastructure. This tutorial walks through two paths: - **Instant Subgraph** — zero config, fastest way to get a GraphQL endpoint from a deployed contract - **Custom Subgraph via CLI** — full control over schema, mappings, and entity relationships By the end you'll have a live GraphQL endpoint indexing your Horizen contract's events. ## Prerequisites - A deployed contract on Horizen (mainnet or testnet). You'll need the contract address and ABI - [Node.js](https://nodejs.org) ≥ 18 - A [Goldsky account](https://app.goldsky.com) (free tier available) ## Step 1: Install the Goldsky CLI For macOS/Linux: ```bash curl https://goldsky.com | sh ``` For Windows (requires [Node.js](https://nodejs.org)): ```bash npm install -g @goldskycom/cli ``` Verify the installation: ```bash goldsky --version ``` Log in to your Goldsky account: ```bash goldsky login ``` When prompted, paste the API key from your [Goldsky Project Settings](https://app.goldsky.com) page. ## Path A — Instant Subgraph (Low-Code) The instant subgraph mode is the fastest path to a working GraphQL API. You provide a config file with your contract details; Goldsky auto-generates the subgraph manifest, schema, and event mappings. ### 1. Export your contract ABI If you deployed with Foundry, the ABI is in `out/.sol/.json`. Extract just the ABI array: ```bash cat out/MyToken.sol/MyToken.json | jq '.abi' > abi.json ``` With Hardhat: ```bash cat artifacts/contracts/MyToken.sol/MyToken.json | jq '.abi' > abi.json ``` Alternatively, find the ABI in the **Contract** tab of the [Horizen Testnet Explorer](https://explorer-testnet.horizen.io/) and save it to `abi.json`. ### 2. Create a config file Create a file named `horizen-config.json` in your project directory: ```json { "version": "1", "name": "my-token", "abis": { "mytoken": { "path": "./abi.json" } }, "instances": [ { "abi": "mytoken", "address": "0xYourContractAddress", "startBlock": 1500000, "chain": "horizen-testnet" } ] } ``` **Fields explained:** | Field | Description | |---|---| | `abis` | Maps a name to your local ABI file. The key (`mytoken`) is referenced by instances | | `instances[].address` | Your deployed contract address | | `instances[].startBlock` | Block number to begin indexing from — use your contract's deployment block | | `instances[].chain` | Goldsky chain slug — use `horizen-testnet` or `horizen` | > For multiple contracts or chains, add more entries to `instances`. Each additional chain produces a separate subgraph. ### 3. Deploy the instant subgraph ```bash goldsky subgraph deploy my-token/1.0.0 --from-abi horizen-config.json ``` Once deployed, Goldsky returns a GraphQL endpoint: ``` https://api.goldsky.com/api/public//subgraphs/my-token/1.0.0/gn ``` Every event emitted by your contract is now queryable. If your contract emits `Transfer(address indexed from, address indexed to, uint256 value)`, the generated schema will include a `Transfer` entity you can query immediately. ## Path B — Custom Subgraph via CLI For production-grade indexing — custom entity relationships, derived fields, aggregations — you define the subgraph manually. This is the same developer experience as The Graph Protocol. ### Project Structure ``` my-subgraph/ ├── subgraph.yaml # Manifest: chain, contract, event handlers ├── schema.graphql # Entity definitions ├── src/ │ └── mapping.ts # AssemblyScript event handlers ├── abis/ │ └── MyToken.json # Contract ABI └── package.json ``` ### 1. Scaffold the project Install the Graph CLI: ```bash npm install -g @graphprotocol/graph-cli ``` Initialize a new subgraph: ```bash graph init my-subgraph \ --protocol ethereum \ --network horizen-testnet \ --contract-address 0xYourContractAddress \ --abi ./abis/MyToken.json \ --index-events ``` ### 2. Define your schema (`schema.graphql`) ```graphql type Transfer @entity(immutable: true) { id: Bytes! from: Bytes! to: Bytes! value: BigInt! blockNumber: BigInt! blockTimestamp: BigInt! transactionHash: Bytes! } type TokenHolder @entity { id: Bytes! # holder address balance: BigInt! transferCount: BigInt! } ``` Entities marked `@entity(immutable: true)` are optimized for append-only data (ideal for events). Mutable entities like `TokenHolder` allow updates. ### 3. Configure the manifest (`subgraph.yaml`) ```yaml specVersion: 0.0.5 schema: file: ./schema.graphql dataSources: - kind: ethereum name: MyToken network: horizen-testnet source: address: "0xYourContractAddress" abi: MyToken startBlock: 1500000 # Replace with your contract's deployment block mapping: kind: ethereum/events apiVersion: 0.0.7 language: wasm/assemblyscript entities: - Transfer - TokenHolder abis: - name: MyToken file: ./abis/MyToken.json eventHandlers: - event: Transfer(indexed address,indexed address,uint256) handler: handleTransfer file: ./src/mapping.ts ``` > **Finding your start block:** Look up your contract deployment transaction on the [Horizen Testnet Explorer](https://explorer-testnet.horizen.io/) and use that block number. Indexing from block 0 works but is slow. ### 4. Write event handlers (`src/mapping.ts`) ```typescript import { Transfer as TransferEvent } from "../generated/MyToken/MyToken"; import { Transfer, TokenHolder } from "../generated/schema"; import { BigInt, Bytes } from "@graphprotocol/graph-ts"; export function handleTransfer(event: TransferEvent): void { // Persist the raw event let transfer = new Transfer( event.transaction.hash.concatI32(event.logIndex.toI32()) ); transfer.from = event.params.from; transfer.to = event.params.to; transfer.value = event.params.value; transfer.blockNumber = event.block.number; transfer.blockTimestamp = event.block.timestamp; transfer.transactionHash = event.transaction.hash; transfer.save(); // Update or create the recipient's balance let recipient = TokenHolder.load(event.params.to); if (recipient == null) { recipient = new TokenHolder(event.params.to); recipient.balance = BigInt.fromI32(0); recipient.transferCount = BigInt.fromI32(0); } recipient.balance = recipient.balance.plus(event.params.value); recipient.transferCount = recipient.transferCount.plus(BigInt.fromI32(1)); recipient.save(); // Update the sender's balance let sender = TokenHolder.load(event.params.from); if (sender != null) { sender.balance = sender.balance.minus(event.params.value); sender.save(); } } ``` ### 5. Generate types and build ```bash graph codegen && graph build ``` `graph codegen` generates TypeScript types from your schema and ABIs. Always run it after changing `schema.graphql` or adding new ABIs. ### 6. Deploy to Goldsky ```bash goldsky subgraph deploy my-token/1.0.0 --path . ``` Goldsky reads your local `subgraph.yaml` and deploys to their managed infrastructure. You'll get a GraphQL endpoint on success. ## Querying Your Subgraph Once deployed, test your endpoint with a GraphQL query: ```graphql { transfers( first: 10 orderBy: blockTimestamp orderDirection: desc ) { id from to value blockTimestamp transactionHash } tokenHolders( first: 5 orderBy: balance orderDirection: desc ) { id balance transferCount } } ``` You can run this directly in [Goldsky's GraphQL playground](https://app.goldsky.com) or integrate it into your frontend with any GraphQL client. ### Example: fetching with `fetch` in JavaScript ```javascript const SUBGRAPH_URL = "https://api.goldsky.com/api/public//subgraphs/my-token/1.0.0/gn"; async function getRecentTransfers() { const query = `{ transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) { from to value blockTimestamp } }`; const res = await fetch(SUBGRAPH_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }), }); const { data } = await res.json(); return data.transfers; } ``` ## Managing Subgraphs ```bash # List all your deployed subgraphs goldsky subgraph list # Check indexing status and current block goldsky subgraph inspect my-token/1.0.0 # Delete a subgraph version goldsky subgraph delete my-token/1.0.0 ``` To deploy a new version without downtime, increment the version: ```bash goldsky subgraph deploy my-token/1.1.0 --path . ``` Old versions remain queryable until explicitly deleted, allowing zero-downtime migrations. --- ## Add Compliance Gating with PureFi PureFi lets you gate any smart contract action (such as a mint, swap, deposit, or access grant) behind an off-chain AML check. The check happens outside the EVM; the result is delivered as a signed payload your contract verifies on-chain before allowing execution to continue. This tutorial walks through the full integration end-to-end: 1. Install the PureFi Solidity SDK 2. Write and deploy a verified receiver contract 3. Register your contract in the PureFi Dashboard 4. Build the off-chain verification flow (TypeScript) 5. Submit a compliance-gated transaction ## How PureFi Works on Horizen PureFi is not an on-chain oracle. It is a hybrid compliance system: the AML check runs off-chain through PureFi's issuer, and the result is a cryptographically signed bytes payload called `_purefidata`. Your contract passes that payload to the PureFi Verifier, which validates the signature and payload on-chain. If it passes, execution continues. If it fails, the entire transaction reverts. The full flow per user action: 1. Your frontend constructs a verification request (package type, rule ID, user wallet, your contract address) 2. The user's wallet signs the request via EIP-712 3. Your backend submits the signed request to the PureFi issuer 4. The issuer runs the AML check — on pass, it returns a `_purefidata` bytes string 5. Your frontend passes `_purefidata` to your contract method 6. Your contract calls `verifier.validatePayload(_purefidata)` — reverts on fail, continues on success The Verifier on Horizen is a proxy contract. Your contract makes a synchronous call to it; the proxy `delegatecall`s to the implementation, validates signature and payload, and returns. There are no logs to watch — the result of the call is the answer. ## Prerequisites - A PureFi subscription — set one up at [dashboard.purefi.io](https://dashboard.purefi.io/) - Your rule ID and package type from your PureFi subscription config - A deployed contract on Horizen Mainnet (or ready to deploy — Step 2 covers this) - Foundry or Hardhat for contract deployment - Node.js ≥ 18 and `ethers` v6 for the off-chain script ## Step 1: Install the PureFi Solidity SDK **Foundry:** ```bash forge install purefiprotocol/sdk-solidity-v5 ``` Add to `remappings.txt`: ``` @purefi-sdk-solidity-v5/=lib/sdk-solidity-v5/src/ ``` **npm (Hardhat or mixed projects):** ```bash npm i @purefi/sdk-solidity-v5 ``` ## Step 2: Write and Deploy Your Verified Receiver Contract The PureFi SDK provides a base abstract contract that handles chain ID validation and Verifier calls. Extend it for Horizen Mainnet with the hardcoded proxy address. Create `src/YourCompliantContract.sol`: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IPureFiVerifier} from "@purefi-sdk-solidity-v5/interfaces/IPureFiVerifier.sol"; import {PureFiDataLibrary} from "@purefi-sdk-solidity-v5/libraries/PureFiDataLibrary.sol"; /// @dev Base contract — handles Verifier calls and chain ID enforcement abstract contract PureFiSdkVerifiedReceiverBase { using PureFiDataLibrary for bytes; IPureFiVerifier public immutable verifier; uint256 public immutable expectedChainId; error WrongChain(uint256 expected, uint256 actual); constructor(address verifier_, uint256 expectedChainId_) { verifier = IPureFiVerifier(verifier_); expectedChainId = expectedChainId_; } function _validateAndUnpack(bytes calldata purefiData) internal returns (address from, address to, uint256 rule, uint8 packageType) { if (block.chainid != expectedChainId) revert WrongChain(expectedChainId, block.chainid); verifier.validatePayload(purefiData); bytes calldata package_ = purefiData.getPackage(); from = package_.getFrom(); to = package_.getTo(); rule = package_.getRule(); packageType = package_.getPackageType(); } } /// @dev Horizen Mainnet concrete implementation — hardcodes verifier proxy and chain ID abstract contract PureFiReceiverHorizenMainnet is PureFiSdkVerifiedReceiverBase { address public constant PUREFI_VERIFIER = 0x681Edd4906e2a0a277E2A6c394A4595f83e1329c; uint256 public constant HORIZEN_CHAIN_ID = 26514; constructor() PureFiSdkVerifiedReceiverBase(PUREFI_VERIFIER, HORIZEN_CHAIN_ID) {} } /// @dev Example: a mint function gated behind PureFi compliance verification contract CompliantMinter is PureFiReceiverHorizenMainnet { mapping(address => bool) public hasMinted; event Minted(address indexed user, bytes32 indexed purefiHash); // purefiData is the bytes string returned by the PureFi issuer after a passed AML check function mint(bytes calldata purefiData) external { (address from, , , ) = _validateAndUnpack(purefiData); // After _validateAndUnpack returns, the compliance check has passed on-chain. // The Verifier would have reverted the tx if it hadn't. require(from == msg.sender, "PureFi: payload not for caller"); require(!hasMinted[msg.sender], "Already minted"); hasMinted[msg.sender] = true; emit Minted(msg.sender, keccak256(purefiData)); // ... your actual mint logic here } } ``` Deploy with Foundry: ```bash forge create src/YourCompliantContract.sol:CompliantMinter \ --rpc-url https://horizen.calderachain.xyz/http \ --private-key $PRIVATE_KEY ``` Note the deployed contract address — you will need it in the next step. ## Step 3: Register Your Contract in the PureFi Dashboard Before any `_purefidata` will be issued for your contract, you must bind it to your subscription. 1. Open [dashboard.purefi.io](https://dashboard.purefi.io/) and connect your subscription wallet 2. Navigate to your subscription and find the **Contracts** or **Bind Contract** section 3. Enter your deployed contract address and confirm :::warning If your contract is not registered in the Dashboard, the issuer will not return `_purefidata` for transactions targeting it. Registering is required before any end-to-end test will work. ::: ## Step 4: Build the Off-chain Verification Flow In production, your frontend or backend handles the request construction, signing, and issuer call. Here is a complete Node.js/TypeScript script that runs all three steps and prints the `_purefidata` ready to submit on-chain. :::note The issuer endpoint URL and exact EIP-712 typed data structure are version-specific. Retrieve the correct values for your SDK version from [wiki.purefi.io/docs/category/solidity-sdk](https://wiki.purefi.io/docs/category/solidity-sdk) and replace the `PUREFI_ISSUER_URL` and `TYPES` constants below. ::: Create `scripts/get-purefidata.ts`: ```typescript import { ethers } from "ethers"; // ── Config ──────────────────────────────────────────────────────────────────── const RPC_URL = "https://horizen.calderachain.xyz/http"; const PRIVATE_KEY = process.env.PRIVATE_KEY!; // signing wallet (the "from" address) const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS!; // your deployed CompliantMinter // Get the correct issuer URL for your environment from: // https://wiki.purefi.io/docs/category/solidity-sdk const PUREFI_ISSUER_URL = process.env.PUREFI_ISSUER_URL!; // These come from your PureFi subscription configuration const PACKAGE_TYPE = Number(process.env.PACKAGE_TYPE!); const RULE_ID = Number(process.env.RULE_ID!); // ── EIP-712 domain & types ──────────────────────────────────────────────────── // Verify these match your SDK version in the PureFi wiki const DOMAIN = { name: "PureFi", version: "1", chainId: 26514, }; const TYPES = { PureFiPayload: [ { name: "packageType", type: "uint8" }, { name: "ruleId", type: "uint256" }, { name: "from", type: "address" }, { name: "to", type: "address" }, ], }; // ── Step 1: Construct the payload ───────────────────────────────────────────── function buildPayload(from: string, to: string) { return { packageType: PACKAGE_TYPE, ruleId: RULE_ID, from, to, }; } // ── Step 2: Sign with EIP-712 ───────────────────────────────────────────────── async function signPayload( signer: ethers.Wallet, payload: ReturnType ): Promise { return signer.signTypedData(DOMAIN, TYPES, payload); } // ── Step 3: Fetch _purefidata from the issuer ───────────────────────────────── async function fetchPurefiData( payload: ReturnType, signature: string ): Promise { const res = await fetch(PUREFI_ISSUER_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ payload, signature }), }); if (!res.ok) { // A non-2xx response means the AML check failed or the request was malformed. // Do NOT attempt to submit a transaction — there is no valid _purefidata. const body = await res.text(); throw new Error(`PureFi issuer rejected (${res.status}): ${body}`); } const json = await res.json(); return json.purefidata as string; } // ── Main ────────────────────────────────────────────────────────────────────── async function main() { const provider = new ethers.JsonRpcProvider(RPC_URL); const signer = new ethers.Wallet(PRIVATE_KEY, provider); const from = await signer.getAddress(); console.log(`From: ${from}`); console.log(`Contract: ${CONTRACT_ADDRESS}`); // 1. Build const payload = buildPayload(from, CONTRACT_ADDRESS); console.log("Payload:", payload); // 2. Sign const signature = await signPayload(signer, payload); console.log("Signature:", signature.slice(0, 20) + "..."); // 3. Get _purefidata console.log("Calling PureFi issuer..."); const purefiData = await fetchPurefiData(payload, signature); console.log("_purefidata:", purefiData.slice(0, 30) + "..."); console.log("\nFull _purefidata (pass this to your contract):"); console.log(purefiData); } main().catch(console.error); ``` Run it: ```bash PRIVATE_KEY=0x... \ CONTRACT_ADDRESS=0x... \ PUREFI_ISSUER_URL=https://issuer.purefi.io/... \ PACKAGE_TYPE=1 \ RULE_ID=777 \ npx ts-node scripts/get-purefidata.ts ``` On success, the script prints the `_purefidata` bytes string. Copy it — you submit it in the next step. ## Step 5: Submit the Compliance-Gated Transaction With `_purefidata` in hand, call your contract. This single transaction performs both the on-chain compliance verification and your business logic atomically. ```typescript import { ethers } from "ethers"; const RPC_URL = "https://horizen.calderachain.xyz/http"; const PRIVATE_KEY = process.env.PRIVATE_KEY!; const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS!; const PUREFI_DATA = process.env.PUREFI_DATA!; // output from Step 4 const ABI = [ "function mint(bytes calldata purefiData) external", ]; async function main() { const provider = new ethers.JsonRpcProvider(RPC_URL); const signer = new ethers.Wallet(PRIVATE_KEY, provider); const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer); console.log("Submitting compliance-gated transaction..."); const tx = await contract.mint(PUREFI_DATA); console.log("Transaction submitted:", tx.hash); const receipt = await tx.wait(); if (receipt.status === 1) { console.log("Success — compliance check passed and mint executed."); } else { console.log("Transaction reverted — validatePayload failed."); } } main().catch(console.error); ``` Run it: ```bash PRIVATE_KEY=0x... \ CONTRACT_ADDRESS=0x... \ PUREFI_DATA=0x... \ npx ts-node scripts/submit.ts ``` If `validatePayload` succeeds, your `Minted` event will be emitted and the transaction will confirm. If it reverts, check the failure indicators below. ## Testing with Playground Before wiring up your own frontend and backend, use PureFi's Playground to manually run through the full flow. Playground is accessible from your [PureFi Dashboard](https://dashboard.purefi.io/) and lets you generate `_purefidata` without writing any code — useful for verifying your contract is correctly registered and that the Verifier accepts calls to it. ### Payload Constructor Fill in your package type, rule ID, the test wallet address as `from`, and your deployed contract as `to`. This generates the payload submitted to the issuer. :::note Screenshot placeholder *[Screenshot: Payload Constructor UI — package type, rule ID, from/to fields]* ::: ### Signature Process Connect your browser wallet. The chain ID must be Horizen Mainnet (26514). Confirm the signing prompt to produce the EIP-712 signature. :::note Screenshot placeholder *[Screenshot: Signature step — wallet prompt and resulting EIP-712 data]* ::: ### Verification Process Select the issuer environment and submit. A successful AML check returns the `_purefidata` bytes string in the response panel. :::note Screenshot placeholder *[Screenshot: Verification step — issuer response with _purefidata]* ::: ### Transaction Builder Paste your contract address, ABI, select the `mint` method (or whichever method accepts `purefiData`), and paste `_purefidata` as the argument. Submit the transaction and confirm in your wallet. :::note Screenshot placeholder *[Screenshot: Transaction Builder — contract, method selector, and _purefidata field]* ::: ## Debugging: Success and Failure Indicators **The integration is working when:** - The issuer returns a non-empty `_purefidata` string - `validatePayload(...)` does not revert - Your business logic executes and emits its event - Subscription usage in the Dashboard decrements after each verified call **Something is wrong when:** - The issuer returns an error or empty response — the AML check failed; do not submit a transaction - `validatePayload(...)` reverts — check that you are using the Horizen Mainnet verifier proxy (`0x681Edd4906e2a0a277E2A6c394A4595f83e1329c`) and that `block.chainid` is `26514` - Transaction reverts with `WrongChain` — your contract is being called on the wrong network - The issuer refuses to issue `_purefidata` — confirm your contract address is registered in the Dashboard ## Horizen Mainnet Reference | | Value | |---|---| | RPC | `https://horizen.calderachain.xyz/http` | | Chain ID | `26514` | | PureFi Verifier proxy | `0x681Edd4906e2a0a277E2A6c394A4595f83e1329c` | | PureFi Verifier implementation | `0x61C468B554B6F0b0842242F7Df079bb392EE0555` | | SDK version | `@purefi/sdk-solidity-v5@5.2.0` | ## Reference Links | Resource | URL | |---|---| | PureFi Dashboard | `https://dashboard.purefi.io/` | | Solidity SDK Docs | `https://wiki.purefi.io/docs/category/solidity-sdk` | | SDK GitHub | `https://github.com/purefiprotocol/sdk-solidity-v5` | | Compliance Gating reference page | `/horizen-chain/integrations/purefi` | --- ## Set Up a Multisig on Horizen Multi-signature wallets require multiple private key holders to approve a transaction before it can be executed. They're the standard for treasury management, protocol upgrades, and any onchain operation where a single point of failure is unacceptable. On Horizen, multisig is provided through **[Den](https://onchainden.com/)** - a self-custodial multisig interface built on top of **Safe** contracts (formerly Gnosis Safe), the most battle-tested asset management infrastructure in the EVM ecosystem. Den adds workflow tooling: transaction simulation, batching, and team management on top of Safe's cryptographic guarantees. ## What You're Setting Up A Safe wallet is a smart contract, not an EOA. It lives on-chain at a deterministic address, holds assets, and executes transactions only when the required number of signers have approved. ## Prerequisites - At minimum 2 wallet addresses to act as signers (MetaMask, Ledger, hardware wallet, etc.) - Each signer needs a small amount of ETH on Horizen for gas - A browser with a Web3 wallet extension (MetaMask recommended) ## Step 1: Add Horizen to Your Wallet Before connecting to Den, add Horizen to your wallet. Follow the steps on the relevant network page: - [Mainnet Configuration](/horizen-chain/network/mainnet) - [Testnet Configuration](/horizen-chain/network/testnet) ## Step 2: Open Den and Connect Navigate to the **[Den dashboard for Horizen](https://app.onchainden.com)** and connect your wallet. Make sure your wallet is set to the correct Horizen network before connecting. Den will display your connected address and let you switch between your existing Safes or create a new one. ## Step 3: Create a New Safe Click **New Safe** and walk through the creation wizard. ### Choose Owners Add the wallet addresses of all intended signers. Each address you add is called an **owner**. Owners don't need to be connected during setup - you only need their addresses. ``` Owner 1: 0xAlice... (your own wallet, the deployer) Owner 2: 0xBob... (teammate) Owner 3: 0xCarol... (teammate or hardware wallet) ``` Best practices for owner selection: - Use hardware wallets (Ledger, Trezor) for high-value safes - Never reuse a hot wallet that's also used for other operations - Consider adding a cold storage address as one owner for disaster recovery ### Set the Threshold The threshold (M) determines how many of the N owners must sign before a transaction executes. | Scenario | Owners (N) | Threshold (M) | Rationale | |---|---|---|---| | Small team / startup | 3 | 2 | Tolerates 1 key loss; fast approvals | | Protocol treasury | 5 | 3 | Tolerates 2 key losses; balanced security | | High-value protocol | 7 | 5 | High security; slow but robust | > A 1-of-N setup provides no additional security over a single-signer wallet. A threshold of N-of-N means any single key loss permanently locks funds. Neither is recommended for production. ### Review and Deploy Den shows you the final configuration. Deploying the Safe is an on-chain transaction - you pay a one-time gas fee. The transaction creates a new Safe smart contract at a deterministic address on Horizen. Once confirmed, you'll see your Safe address in Den: ``` Safe address: 0xYourSafe... (on Horizen, Chain ID 26514) ``` **Save this address.** It's where you'll send assets. ## Step 4: Fund the Safe Send ETH (or any ERC-20 tokens) to your Safe address. The Safe holds funds like any other address - you can verify the balance on the [Horizen Explorer](https://explorer.horizen.io/). For testnet, use the [Horizen Testnet Faucet](https://hub-testnet.horizen.io/) to fund your signers' wallets, then send ETH to your Safe. ## Step 5: Create Your First Transaction In Den, navigate to your Safe and click **New Transaction**. Den supports three transaction types: ### Send Assets Transfer ETH or ERC-20 tokens to an address. Fill in: - **To:** destination address - **Token:** ETH or select an ERC-20 - **Amount:** value to transfer ### Contract Interaction Call any smart contract function. You'll need the contract address and ABI (or Den can decode verified contracts automatically). Example: calling `approve()` on a token contract: - **Contract:** `0xTokenAddress` - **Function:** `approve(address spender, uint256 amount)` - **Args:** your spender address, amount in wei ### Raw Transaction Paste raw calldata for custom interactions. Useful for interacting with unverified contracts or complex encoded payloads. ## Step 6: Simulate the Transaction Before collecting signatures, simulate the transaction. Den executes the transaction against a fork of the current chain state and reports: - Whether the transaction would succeed or revert - State changes (balance diffs, storage mutations) - Estimated gas usage - Any events emitted Simulation catches errors before signers spend time signing a broken transaction. **Always simulate before circulating for signatures.** ## Step 7: Collect Signatures Once the transaction is created, Den generates a shareable link. Distribute this to the other owners. Each owner: 1. Opens Den and connects their wallet 2. Navigates to the pending transaction 3. Reviews the transaction details (to address, calldata, value) 4. Signs with their wallet (this is an off-chain signature) Signatures are aggregated by Den until the threshold is met. > **Security reminder:** each owner should independently verify the transaction details before signing. Never sign based solely on a teammate's summary. Check the raw calldata yourself. ## Step 8: Execute the Transaction Once M signatures have been collected, any owner (or any funded address) can execute the transaction. Click **Execute** in Den. This bundles all signatures into a single on-chain transaction: ``` execTransaction( address to, uint256 value, bytes calldata data, uint8 operation, ...signatures ) ``` The Safe contract verifies that the signatures are valid and that the required threshold is met, then executes the inner call atomically. ## Step 9: Batch Multiple Transactions Batching lets you execute multiple operations in a single on-chain transaction, saving gas and ensuring atomicity (all operations succeed or all revert together). In Den, click **New Batch** and add multiple transaction steps: ``` Batch: "Monthly Treasury Operations" Step 1: Approve 10,000 USDC to DeFi protocol Step 2: Deposit 10,000 USDC into DeFi protocol Step 3: Send 1 ETH to development fund ``` Den encodes these as a `multiSend` call through Safe's `MultiSendCallOnly` contract - each step executes sequentially within a single transaction. ## Importing an Existing Safe If you already have a Safe deployed (e.g., from a previous deployment or another network), you can import it into Den: 1. In Den, click **Load Existing Safe** 2. Enter your Safe address 3. Verify the owner list and threshold match your expectations ## Safe Contract Addresses on Horizen Safe contracts are deployed at canonical addresses across EVM chains. Verify the current Safe deployment on Horizen at the [Safe deployments repository](https://github.com/safe-global/safe-deployments) or through the [Horizen Explorer](https://explorer.horizen.io/). The key contracts: - **SafeProxyFactory** - deploys new Safe instances - **Safe (Singleton)** - the implementation logic all proxies point to - **MultiSendCallOnly** - used for batched transactions - **CompatibilityFallbackHandler** - ERC-1271 signature support ## Security Best Practices **Key management** - Store at least one signing key on a hardware wallet - Never store private keys in plaintext or in version control - Use separate keys for your Safe owner role vs. your development hot wallet **Threshold hygiene** - Maintain a threshold that can tolerate key loss. With 3 owners and a 2-of-3 threshold, you can recover if one key is permanently lost - you just rotate it out using the remaining 2 - If a signer leaves your team, rotate them out of the Safe immediately using a transaction approved by the remaining signers **Transaction review** - Always decode raw calldata before signing. Tools like [Swiss Knife](https://calldata.swiss-knife.xyz/) or the Horizen Explorer's calldata decoder can help - Check the `to` address carefully - address poisoning attacks use addresses with similar prefixes/suffixes to your intended destination **Rotation procedure** Adding or removing an owner is itself a Safe transaction that requires M-of-N approval: 1. Create a transaction calling `addOwnerWithThreshold(newOwner, newThreshold)` or `removeOwner(prevOwner, owner, newThreshold)` 2. Collect required signatures 3. Execute on-chain ## Advanced: Interacting with the Safe Contract Directly For scripted or programmatic access, use the Safe SDK: ```typescript import Safe, { EthersAdapter } from "@safe-global/protocol-kit"; import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://horizen.calderachain.xyz/http"); const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }); const safe = await Safe.create({ ethAdapter, safeAddress: "0xYourSafeAddress", }); // Build a transaction const tx = await safe.createTransaction({ transactions: [ { to: "0xRecipient", value: ethers.parseEther("0.1").toString(), data: "0x", }, ], }); // Sign it with the current signer const signedTx = await safe.signTransaction(tx); // Execute (if threshold is already met after this signature) const result = await safe.executeTransaction(signedTx); console.log("Executed:", result.hash); ``` > The Safe Protocol Kit handles nonce management, signature aggregation, and gas estimation. Use it when building automation or CI/CD pipelines that interact with your Safe. --- ## Your First Confidential App import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; This guide walks you through deploying the VELA example application — a private transfer app — and running your first confidential transaction. By the end you will have deposited ETH into an encrypted account inside the TEE and verified your private balance. > **Prerequisites:** Complete [Local Environment Setup](./local-environment-setup.md) > and have `docker compose up` running before proceeding. ## What You're Deploying The example application (`vela-nova`) is a private account-based ledger running entirely inside the TEE. Balances, transfers, and transaction history are all encrypted — external observers see only attested state roots on-chain, not the underlying data. It supports four operations: `deposit`, `privatetransfer`, `withdraw`, and `deanonymize` (for authorized auditors). ## Step 1: Download the Artifacts Go to the [`vela-nova` v0.1.0 release page](https://github.com/HorizenOfficial/vela-nova/releases/tag/v0.1.0) and download two files: - `payment_app.wasm` — the compiled WASM module you'll deploy into the TEE - `novaw-linux` — the CLI wallet for interacting with the app Place both files in a `wallet/` folder. Make `novaw-linux` executable: ```bash chmod +x novaw-linux ``` > **Mac users:** `novaw-linux` is a Linux x86-64 binary and cannot run directly on Mac > (neither Intel nor Apple Silicon). All wallet commands must be run inside a Docker > container — see the Mac tabs in each step below. Running the binary directly will give > `exec format error`. ## Step 2: Configure the Wallet Copy the wallet config template: ```bash cp wallet.conf.template wallet.conf ``` Open `wallet.conf` and set the following values to connect to your local environment. The URLs differ depending on your OS: When `novaw-linux` runs inside Docker, `localhost` resolves to the container itself — not your Mac. Use `host.docker.internal` to reach services on your host machine: ```ini rpcUrl=http://host.docker.internal:8545 ProcessorAddress=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 TeeAuthenticatorAddress=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 AuthorityServiceURL=http://host.docker.internal:8081 SubgraphURL=http://host.docker.internal:8000/subgraphs/name/hcce ``` ```ini rpcUrl=http://localhost:8545 ProcessorAddress=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 TeeAuthenticatorAddress=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 AuthorityServiceURL=http://localhost:8081 SubgraphURL=http://localhost:8000/subgraphs/name/hcce ``` These are the deterministic contract addresses deployed by the local `deployer` service. They will be the same on every fresh environment. ## Step 3: Set Your Keys You need two keys: a secp256k1 key for signing on-chain transactions, and a P-521 key for private communication with the TEE. ### secp256k1 key Use one of the Anvil default account private keys. These accounts are pre-funded with 1000 ETH on the local chain. > **Important for `deployapp`:** The account must have `DEPLOYER_ROLE` on `ProcessorEndpoint`. > In the local dev environment, only **Anvil Account #0** has this role pre-granted. > Use Account #0's key when running `deployapp` — any other key will fail with a role error. > You can use other Anvil accounts for `registeruser`, `deposit`, and other operations. Anvil Account #0 (required for `deployapp`): ``` ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ``` Add it to `wallet.conf`: ```ini keySecp256k1=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ``` ### P-521 key Generate a fresh key pair: ```bash cd docker run --rm --platform linux/amd64 \ -v $(pwd):/wallet -w /wallet \ ubuntu:22.04 /wallet/novaw-linux generatekeys ``` ```bash cd ./novaw-linux generatekeys ``` Copy the printed `P521` value into `wallet.conf`: ```ini keyP521= ``` The P-521 key is used for ECDH-encrypted communication between your client and the TEE. The TEE uses your registered public key to encrypt all events it sends back to you — only your private key can decrypt them. ## Step 4: Deploy the WASM Application ```bash cd docker run --rm --platform linux/amd64 \ -v $(pwd):/wallet -w /wallet \ ubuntu:22.04 /wallet/novaw-linux deployapp \ --wasm /wallet/payment_app.wasm --max-value-fee "100 wei" ``` ```bash cd ./novaw-linux deployapp \ --wasm ./payment_app.wasm --max-value-fee "100 wei" ``` On success you will see: ``` Deploy app completed successfully. ApplicationID: ``` Copy the printed `ApplicationID` into `wallet.conf`: ```ini ApplicationID= ``` What happens under the hood: 1. The wallet uploads `payment_app.wasm` to the Authority Service (`POST /deploy/upload`) 2. An on-chain deploy request is submitted to `ProcessorEndpoint` 3. The Processor Manager picks up the request and forwards the WASM artifact to the Executor inside the TEE 4. The TEE verifies the WASM fingerprint (SHA-256) against the on-chain descriptor before loading the module 5. The application is assigned an `ApplicationID` ## Step 5: Register Your User Before you can interact with the app, register your P-521 public key on-chain. This tells the TEE which key to use when encrypting events back to you: ```bash docker run --rm --platform linux/amd64 -v $(pwd):/wallet -w /wallet \ ubuntu:22.04 /wallet/novaw-linux registeruser ``` ```bash ./novaw-linux registeruser ``` ## Step 6: Run Your First Private Transaction Check your public balance (starts at zero): ```bash docker run --rm --platform linux/amd64 -v $(pwd):/wallet -w /wallet \ ubuntu:22.04 /wallet/novaw-linux getpublicbalance ``` ```bash ./novaw-linux getpublicbalance ``` Deposit 1 ETH into your private account inside the TEE: ```bash docker run --rm --platform linux/amd64 -v $(pwd):/wallet -w /wallet \ ubuntu:22.04 /wallet/novaw-linux deposit -a "1 ETH" ``` ```bash ./novaw-linux deposit -a "1 ETH" ``` The deposit is submitted as an on-chain transaction. The Processor Manager detects it, routes it to the Executor, which credits your encrypted account inside the TEE and emits an encrypted event confirming the operation. Only your P-521 key can decrypt it. Verify your private balance: ```bash docker run --rm --platform linux/amd64 -v $(pwd):/wallet -w /wallet \ ubuntu:22.04 /wallet/novaw-linux getprivatebalance ``` ```bash ./novaw-linux getprivatebalance ``` If you see `1 ETH` reflected in your private balance, the full stack is working correctly. ## Troubleshooting **`wasm module is empty (code 11)`** The `manager` and `authorityservice` containers are not sharing a named volume, so the manager cannot find the uploaded WASM artifact. Apply the shared volume fix described in [Local Environment Setup](./local-environment-setup.md), then force-recreate both containers: ```bash cd /dockerfiles docker compose up -d --force-recreate manager authorityservice ``` Then retry the `deployapp` command. **`Insufficient funds for gas * price + value`** The secp256k1 key in `wallet.conf` is not an Anvil pre-funded account. Switch to Anvil Account #0's key (see Step 3). Freshly generated keys have 0 ETH and every on-chain transaction will fail. ## Full Command Reference ```bash # Mac — replace with any command below docker run --rm --platform linux/amd64 -v $(pwd):/wallet -w /wallet \ ubuntu:22.04 /wallet/novaw-linux # Linux — run directly ./novaw-linux # Commands generatekeys # Generate a P-521 key pair deployapp # Deploy a WASM application registeruser # Register your P-521 key on-chain getpublicbalance # Query on-chain balance deposit -a "1 ETH" # Deposit into private account getprivatebalance # Query encrypted TEE balance privatetransfer # Transfer between private accounts help # Full command reference ``` --- ## Local Environment Setup (Docker) The VELA local environment runs a complete stack on your machine via Docker Compose. It includes a local EVM chain, automatic smart contract deployment, a subgraph indexer, the Processor Manager, and the Authority Service — everything you need to develop and test a VASM application without touching a testnet. > **Note:** The TEE is emulated in this environment. No real AWS Nitro > Enclave is used. Only one WASM application deployment is supported > per environment (appId 1). ## Prerequisites - [Docker](https://docs.docker.com/get-docker/) with Docker Compose v2+ - Git No other local tooling is required to run the environment. You only need a language toolchain (Go + TinyGo) if you intend to build your own WASM module. ## Setup **1. Clone the starter kit** ```bash git clone https://github.com/HorizenOfficial/vela-starterkit cd vela-starterkit/dockerfiles ``` **2. Create your environment file** For local development, copy the pre-configured dev defaults: ```bash cp .env.dev .env ``` The `.env.dev` file has everything pre-configured for local use. You do not need to modify it to get started. **3. Start the stack** ```bash docker compose up ``` ## Service Startup Sequence Docker Compose brings up six services in a defined dependency chain: | Step | Service | What It Does | | --- | --- | --- | | 1 | `chain` | Starts Foundry Anvil — a local EVM dev chain | | 2 | `subgraph-postgres`, `subgraph-ipfs` | Starts Graph Node infrastructure | | 3 | `deployer` | Deploys all VELA smart contracts, writes addresses to a shared volume, then exits | | 4 | `subgraph-node` | Starts Graph Node, connects to the chain | | 5 | `subgraph-deployer` | Reads deployed contract addresses, generates the subgraph manifest, deploys it, then exits | | 6 | `manager`, `authorityservice` | Start and begin polling the chain and subgraph for requests | The full stack is ready when `manager` and `authorityservice` are running and healthy. This takes roughly 30–60 seconds on first boot. ## Connecting MetaMask To interact with the local chain from a browser wallet: | Field | Value | |---|---| | RPC URL | `http://localhost:8545` | | Chain ID | `31337` | | Currency | ETH | Anvil pre-funds a set of default accounts with 1000 ETH each. Their private keys are printed in the `chain` service logs on startup. ## Data Persistence and Volume Management All state is persisted across restarts in Docker volumes. | Volume | Contents | |---|---| | `vela-skit-chain-data` | Anvil chain data | | `vela-skit-manager-data` | Processor Manager LevelDB state | | `vela-skit-deploy-data` | Deployed contract addresses | | `vela-skit-manager-reports` | Deanonymization reports (shared with Authority Service) | | `vela-skit-logs` | Centralized logs | **Restart without data loss:** ```bash docker compose down && docker compose up ``` The deployer detects existing contracts and skips redeployment. **Full reset — start from scratch:** ```bash docker compose down docker volume rm dockerfiles_vela-skit-chain-data \ dockerfiles_vela-skit-deploy-data \ dockerfiles_vela-skit-manager-data docker compose up ``` **If you modify contracts:** rebuild the deployer image and delete both the chain and deploy volumes before restarting. --- ## Prerequisites & Installation Before working with VELA locally, make sure your development environment meets the following requirements. ## Requirements - **Docker** — VELA's local environment runs entirely in Docker containers. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) or Docker Engine for your platform. - **Git** — Required to clone the starter kit and related repositories. - **Node.js** (optional) — Needed if you plan to use the `vela-common-ts` TypeScript library for client-side interactions. - **Go** (optional) — Needed if you plan to use the `vela-common-go` library. ## Installation Clone the starter kit repository: ```bash git clone https://github.com/HorizenOfficial/vela-starterkit.git cd vela-starterkit ``` Follow the instructions in the repository README to bring up the local environment. The starter kit handles container orchestration, environment configuration, and sample application deployment. --- ## What is Vela? Vela is a TEE-based confidential execution solution by Horizen Labs, now in closed beta. It allows developers to run application logic inside Trusted Execution Environments (TEEs), where data is encrypted in memory and computations are cryptographically attested. No operator, cloud provider, or third party can access the data being processed. At the same time, regulators and auditors can verify compliance through cryptographic proof without requiring access to raw application state. ## Key Properties **Confidential execution** — Application logic runs inside a TEE. Data is encrypted in memory and inaccessible to the host machine, the cloud provider, or any external observer. **Cryptographic attestation** — Every computation produces a verifiable attestation that proves the code ran correctly inside a genuine enclave, without revealing the data itself. **Compliance without exposure** — Authorized parties (auditors, regulators) can verify that specific rules were followed using cryptographic proofs. They do not need access to the underlying data. **Chain-agnostic** — Vela is not limited to Horizen Chain. It is designed as a coprocessor that can serve applications across multiple EVM-compatible networks. ## How It Fits with Horizen Horizen Chain is an EVM-native L3 built on Base. Vela extends it by providing the confidential computation layer that the base chain does not offer. Together, they enable applications that are both publicly verifiable and privately executed. ## Current Status Vela is in active development and open for developer testing. The local development environment runs via Docker with an emulated TEE. For details on getting started, see the [Getting Started](/vela/getting-started/prerequisites) section. → [Vela website](https://vela.horizenlabs.io/) --- ## Limitations VELA is in active development. The following constraints apply to the current release and will be addressed in upcoming iterations. ## No Production Deployment VELA is not yet deployed to any testnet or mainnet environment. All development and testing happens locally using Docker. ## Emulated TEE The local development environment uses an emulated Trusted Execution Environment rather than real hardware enclaves. This means your application logic runs in an isolated container, but does not benefit from hardware-level attestation guarantees. If you have a working prototype and need access to a dedicated AWS Nitro Enclave instance on Horizen Chain, contact the team directly. ## Single WASM Application Per Environment Only one WebAssembly application can be deployed into a VELA environment at a time. Multi-app support is on the roadmap. ## What the Team is Working On The VELA team is actively building toward a production-ready release. Below is a summary of what is currently in progress. ### Multi-WASM Application Support Removing the single-app constraint so that multiple WebAssembly applications can be deployed within a single VELA environment. ### Self-Deployment on Testnet Enabling developers to deploy their own VELA applications to a shared testnet without needing to coordinate directly with the team. ### Shared Testnet Environment A single, persistent testnet environment accessible to all developers for testing and integration work. ### ERC-20 Token Support Adding native support for ERC-20 token interactions within VELA applications. ### Deanonymization Controls More granular controls for managing when and how data can be revealed to authorized parties such as auditors or regulators. --- The team shares updates as new features are deployed. If you have feedback or feature requests, reach out through [Discord](https://discord.gg/horizen) or open an issue on the [VELA repository](https://github.com/HorizenOfficial/vela). --- ## Zenrise Loyalty Program Terms of Use These Zenrise Loyalty Program Terms of Use, as may be amended from time to time (the “Terms”) apply to your access to and use of the websites, platform, software, technologies, features, and other online products and services provided or made available by **Horizen Foundation** a Cayman Islands foundation company ("Horizen," "we," "us," or “our”) in connection with the **Zenrise Loyalty Program** (the “Program”), including, but not limited to, the rewards dashboard, tracking systems, and associated services (collectively, the “Services”). For purposes of these Terms, “user”, “you”, and “your” means you as the user of the Services. If you use the Services on behalf of a company or other entity, then “you” includes you and that entity, and you represent and warrant that (a) you are an authorized representative of the entity with the authority to bind the entity to these Terms, and (b) you agree to these Terms on the entity’s behalf. **PLEASE READ THESE TERMS CAREFULLY AS THEY CONTAIN IMPORTANT INFORMATION AND AFFECT YOUR LEGAL RIGHTS. BY CLICKING TO ACCEPT AND/OR USING OUR SERVICES, YOU AGREE TO BE BOUND BY THESE TERMS AND ALL OF THE TERMS INCORPORATED HEREIN BY REFERENCE. IF YOU DO NOT AGREE TO THESE TERMS, YOU MAY NOT ACCESS OR USE THE SERVICES. YOUR PARTICIPATION IN THE LOYALTY PROGRAM IS ENTIRELY VOLUNTARY, BUT IF YOU ARE PARTICIPATING, YOU MUST STRICTLY ADHERE TO THE TERMS.** ## Definitions In addition to terms defined elsewhere in these Terms, the following capitalized terms will have the meanings set forth below: * “Disputes” has the meaning set forth in the “Dispute Resolution; Binding Arbitration” section. * “Horizen Parties” means Horizen, its affiliates, suppliers, licensors, and their respective officers, directors, employees, and agents. * “Program Dashboard” means the official web interface, dashboard, or application provided by Horizen for users to interact with the Program, track XP, and view Rewards. * “Program Period” means the period during which the Program is active, commencing on the date prescribed by Horizen and continuing until terminated by Horizen in its sole discretion. * “Rewards” means any benefits, digital items, goods, services, discounts, or other items of value made available for redemption of XP, as described in the Program Dashboard or any applicable Rewards Policy. * “Rewards Policy” means the then-current program rules, redemption terms, and policies applicable to Rewards, as published in the Program Dashboard or otherwise made available by Horizen, as may be updated from time to time. * “Services” means the websites, platform, software, technologies, features, and other online products and services provided or made available by Horizen in connection with the Program, including the Program Dashboard. * “XP” means the experience points or other similar digital units accumulated by users for participation in the Program. ## Privacy Policy Please refer to our Privacy Policy, as may be amended from time to time in accordance with its terms, available at https://www.horizenlabs.io/privacy (the "Privacy Policy"), for information about how we collect, use, and disclose information. You acknowledge and agree that your use of the Services is subject to our Privacy Policy, which is incorporated herein by reference. ## Changes to Terms Horizen may update the Terms at any time, at its sole discretion. If it does so, Horizen will deliver a notice either by posting the updated Terms on its website, on any applicable blog or forum used for sharing information or through other communications. It’s important that you review any and all updated Terms. If you continue to utilize the Services after Horizen has posted updated Terms, you agree to be bound by the updated Terms. If you don’t agree to be bound by the updated Terms, then you may not utilize the Services anymore. ## User Account and Responsibilities You agree to provide accurate, current, and complete information in connection with your account and to update such information as necessary. You are responsible for safeguarding your account credentials, and you agree not to disclose them to any third party. You are solely responsible for any and all activities or actions that occur under your account, whether or not you have authorized such activities or actions. You must immediately notify Horizen of any unauthorized use of your account. ## Ownership The Services, including their “look and feel” (e.g., text, graphics, images, logos, page headers, button icons, and scripts), proprietary content, information and other materials, and all content and other materials contained therein, including, without limitation, Horizen and Zenrise brands, logo and all designs, text, graphics, pictures, data, software, sound files, other files, and the selection and arrangement thereof are the proprietary property of us or our affiliates, licensors, or users, as applicable, and you agree not to take any action(s) inconsistent with such ownership interests. We and our affiliates, licensors, and users, as applicable, reserve all rights in connection with the Service and its content, including, without limitation, the exclusive right to create derivative works. You grant Horizen a perpetual, irrevocable, nonexclusive, royalty-free, worldwide, fully-paid, and sublicensable license to use, reproduce, modify, adapt, publish, translate, create derivative works from, distribute, and display any feedback, comments, or suggestions you provide in connection with the Services or Program, without any compensation or obligation to you. ## Duration of the Loyalty Program The Loyalty Program will commence on the date prescribed by Horizen and continue until terminated by Horizen in its sole discretion (“Program Period”). You acknowledge that you know the Loyalty Program is a dynamic initiative and as such, subject to changes in structure and duration. Notwithstanding any other information provided by Horizen regarding the Loyalty Program (including on its website, social media or through other communications), Horizen may change, discontinue, or terminate, temporarily or permanently, all or any part of the Loyalty Program, at any time and without notice, at its sole discretion. Upon termination of the Program or your account for any reason, all unredeemed XP and any rights to claim unfulfilled Rewards will be immediately forfeited and void, and you will have no further claims against Horizen with respect to such XP or Rewards, unless otherwise required by applicable law. Horizen may, in its sole discretion, cancel any pending or unfulfilled Rewards upon termination. ## Eligibility for Loyalty Program You may participate in the Program only if you meet the following criteria and continue to meet them throughout the Program Period: (a) you are 18 years or older and capable of forming a binding contract with Horizen; (b) you are not the subject of sanctions administered or enforced by any country or government (including but not limited to the United States, United Kingdom, European Union, or United Nations) or otherwise designated on any list of prohibited or restricted parties (including but not limited to the U.S. Treasury Department's List of Specially Designated Nationals and Blocked Persons) and you are not a citizen, organized, or resident in a country or territory that is the subject of comprehensive country-wide or territory-wide sanctions (including, as of the date of these Terms, Cuba, Iran, North Korea, Syria, and the Crimea, Donetsk, and Luhansk regions of Ukraine); and (c) your participation is not prohibited by any applicable law. Your participation, including the earning of XP and redemption of Rewards, is conditioned on your ongoing compliance with these Terms. Horizen reserves the right to require you to provide information for identity or eligibility verification (KYC) at any time, including as a condition to redeeming Rewards. Notwithstanding any information provided by Horizen regarding the Loyalty Program, Horizen may change or modify at any time the number of participants eligible to participate or the requirements of the Program and terminate any participant’s participation at any time. The Loyalty Program may operate in certain phases. Your selection or participation in any one phase does not imply that you will be selected for any other phases. ## Program XP (Experience Points) In your use of the Loyalty Program, you may accumulate “XP” (Experience Points), which are virtual items with no monetary value. XP does not constitute any currency or property of any type and confers no legal or equitable rights. Subject to these Terms, XP may be redeemable for rewards as specified in the Program dashboard or as otherwise communicated by Horizen, at the sole discretion of Horizen. XP are not transferable between users, and you may not attempt to sell, trade, or transfer any XP outside of the Program, or obtain any manner of credit using any XP. Any attempt to sell, trade, or transfer any XP will be null and void. We may, in our sole discretion, decide to delete, wipe or otherwise remove XP at any time without notice, including, without limitation, the modification of the presence, amounts, or any other conditions applicable to the XP, without any liability to you or other users. Horizen does not guarantee that XP will continue to be offered for a specific length of time. ## Rewards & Referral Conditions In the course of your engagement with the Loyalty Program, Horizen may, at its sole discretion, grant XP to participants who meet certain criteria. This includes the completion of designated activities, referrals, and challenges. ### Referral Rewards: You acknowledge that rewards for referring other users are subject to specific anti-sybil conditions. Referral XP is only unlocked when the referred user meets the specific activity thresholds defined in the Program dashboard (e.g. verifying social accounts and earning a minimum amount of XP). Horizen reserves the right to withhold, cancel, or revoke XP if we detect any signs of botting, sybil attacks, or manipulation of the referral system. ## Suspension and Forfeiture Notwithstanding any other provision of these Terms, Horizen reserves the right to suspend or terminate your account, freeze or forfeit any accumulated XP or Rewards, and/or claw back or require the return of any issued Rewards if Horizen, in its sole discretion, suspects or determines that you have engaged in fraud, abuse, policy violations, chargebacks, or any activity that is inconsistent with the spirit of the Program, or that an error has occurred in the crediting of XP or issuance of a Reward. All determinations by Horizen are final and binding. ## Reward Availability All Rewards are subject to availability. Horizen may, in its sole discretion, substitute any Reward for one of equal or greater value, limit quantities, impose per-user caps, and restrict availability by geographic location or due to partner availability. The redemption of Rewards may be subject to additional terms and conditions as set forth in the Rewards Policy. ## Prohibited Activities You agree not to engage in any of the following activities: * Use any automated means or form of scraping or data extraction to access, query or otherwise collect information from the Services; * Create multiple accounts, wallets, or social identities (Sybil attacks) to manipulate the XP system or leaderboard; * Use the Services in any manner that could interfere with, disrupt, negatively affect or inhibit other users from fully enjoying the Services, or that could damage, disable, overburden or impair the functioning of the Services; * Attempt to circumvent any content-filtering techniques we employ or attempt to access any feature or area of the Services that you are not authorized to access; * Develop, use, or deploy any software viruses or other harmful or malicious code in connection with the Services; * Exploit any bug, vulnerability, or other unintentional aspect of the Services to gain an unfair advantage; * Provide false, inaccurate, or misleading information to Horizen in connection with the Program; * Engage in any harassing, threatening, intimidating, predatory, or stalking conduct; * Violate any applicable laws, rules, or regulations in connection with your access to or use of the Services. ## WARRANTY DISCLAIMERS; RISKS HORIZEN AND ITS AFFILIATES MAKE NO REPRESENTATIONS OR WARRANTIES OF ANY KIND WITH RESPECT TO THE SERVICES AND HEREBY DISCLAIM ALL SUCH WARRANTIES. THE SERVICES ARE PROVIDED “AS IS” AND "AS AVAILABLE" WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND. WITHOUT LIMITING THE FOREGOING, HORIZEN EXPLICITLY DISCLAIMS ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT, ACCURACY, AND NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR TRADE USAGE. HORIZEN MAKES NO WARRANTY THAT THE SERVICES WILL MEET YOUR REQUIREMENTS OR BE AVAILABLE ON AN UNINTERRUPTED, SECURE, OR ERROR-FREE BASIS. HORIZEN MAKES NO WARRANTY REGARDING THE QUALITY, ACCURACY, TIMELINESS, TRUTHFULNESS, COMPLETENESS, OR RELIABILITY OF THE SERVICES OR ANY CONTENT OBTAINED THROUGH THE SERVICES. YOU ACKNOWLEDGE AND AGREE THAT: (A) THE LOYALTY PROGRAM MAY BE SUBJECT TO TECHNICAL UPDATES, CHANGES, OR DOWNTIME; (B) XP AND ANY REWARDS HAVE NO GUARANTEED VALUE AND MAY BE MODIFIED OR ELIMINATED AT ANY TIME; (C) THERE IS NO GUARANTEE THAT THE PROGRAM WILL CONTINUE FOR ANY SPECIFIC DURATION; AND (D) YOU ASSUME ALL RISKS ASSOCIATED WITH YOUR PARTICIPATION IN THE LOYALTY PROGRAM. NOTHING IN THESE TERMS CONSTITUTES LEGAL, TAX, FINANCIAL, OR INVESTMENT ADVICE, AND YOU SHOULD CONSULT YOUR OWN ADVISORS BEFORE PARTICIPATING IN THE PROGRAM. ## LIMITATION OF LIABILITY TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT WILL THE HORIZEN PARTIES BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF USE, LOSS OF PROFITS, LOSS OF DATA, LOSS OF GOODWILL, LOSS OF XP, LOSS OF REWARDS, OR ANY OTHER INTANGIBLE LOSSES ARISING OUT OF OR RELATED TO THESE TERMS OR YOUR ACCESS TO OR USE OF (OR INABILITY TO ACCESS OR USE) THE SERVICES OR THE LOYALTY PROGRAM, WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING NEGLIGENCE), STATUTE, OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT THE HORIZEN PARTIES HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGE. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE AGGREGATE LIABILITY OF THE HORIZEN PARTIES TO YOU FOR ALL CLAIMS ARISING OUT OF OR RELATED TO THESE TERMS OR THE SERVICES, WHETHER IN CONTRACT, TORT, OR OTHERWISE, SHALL NOT EXCEED ONE HUNDRED U.S. DOLLARS ($100.00). THE LIMITATIONS AND EXCLUSIONS IN THIS SECTION WILL NOT APPLY TO LIABILITY FOR: (A) DEATH OR PERSONAL INJURY CAUSED BY OUR NEGLIGENCE; (B) FRAUD OR FRAUDULENT MISREPRESENTATION; OR (C) ANY OTHER LIABILITY THAT CANNOT BE EXCLUDED OR LIMITED BY APPLICABLE LAW. EACH PROVISION OF THESE TERMS THAT PROVIDES FOR A LIMITATION OF LIABILITY, DISCLAIMER OF WARRANTIES, OR EXCLUSION OF DAMAGES IS INTENDED TO AND DOES ALLOCATE THE RISKS BETWEEN THE PARTIES UNDER THESE TERMS. THIS ALLOCATION IS AN ESSENTIAL ELEMENT OF THE BASIS OF THE BARGAIN BETWEEN THE PARTIES. You are solely responsible for determining and paying any and all taxes, duties, and other governmental charges and fees assessed or imposed on or with respect to any Rewards you receive. Horizen may require you to provide tax identification information (such as an IRS Form W-9 or equivalent) as a condition of redeeming certain Rewards and may report the value of Rewards to tax authorities as required by law. You represent and warrant that you will comply with all applicable laws (e.g., local, state, federal and other laws) when using the Services, including, without limitation, all applicable export control and trade sanctions laws and regulations. ## Indemnification To the fullest extent permitted by applicable law, you agree to indemnify, defend, and hold harmless the Horizen Parties from and against any and all claims, damages, liabilities, costs, and expenses (including reasonable attorneys' fees) arising from or related to: (a) your access to or use of the Services; (b) your violation of these Terms or any applicable law; (c) your violation of the rights of any third party; or (d) any fraud, negligence, or willful misconduct by you. ## General Terms These Terms, together with the Privacy Policy and any other documents expressly incorporated by reference, constitute the entire agreement between you and Horizen regarding the subject matter hereof and supersede all prior or contemporaneous agreements, understandings, and communications, whether written or oral. In the event of a conflict between these Terms and any Rewards Policy or other communication, these Terms will control. Nothing herein shall constitute an employment, consultancy, joint venture, or partnership relationship between you and Horizen. You may not assign or transfer these Terms or your rights hereunder, in whole or in part, by operation of law or otherwise, without our prior written consent. We may assign these Terms at any time without notice or consent. The failure to require performance of any provision will not affect our right to require performance at any other time after that, nor will a waiver by us of any breach or default of these Terms, or any provision of these Terms, be a waiver of any subsequent breach or default or a waiver of the provision itself. If any provision of the Terms is held invalid or unenforceable by an arbitrator or a court of competent jurisdiction, that provision will be enforced to the maximum extent permissible and the other provisions of the Terms will remain in full force and effect. The following sections will survive the expiration or termination of these Terms: Definitions, Ownership, Warranty Disclaimers; Risks, Limitation of Liability, Indemnification, Compliance and Taxes, General Terms, and Dispute Resolution; Binding Arbitration. Horizen will not be liable for any delay or failure to perform resulting from causes outside its reasonable control, including acts of God, war, terrorism, riots, embargoes, acts of civil or military authorities, fire, floods, accidents, strikes or shortages of transportation facilities, fuel, energy, labor or materials (a "Force Majeure Event"). The Services may contain links to third-party websites or services. We are not responsible for the content, products, or services on or available from those third parties. You acknowledge sole responsibility for and assume all risk arising from your use of any third-party resources. These Terms will be governed by and construed in accordance with the laws of the Cayman Islands, without regard to its conflict of law principles. Except as otherwise provided herein, any notices or other communications provided by Horizen under these Terms will be given by posting to the Services or via email to the address associated with your account. Notices to Horizen must be sent to legal@horizen.io and The Horizen Foundation 3119 9 Forum Lane, Camana Bay, P.O Box 144, Grand Cayman KY1-9006 DISPUTE RESOLUTION; BINDING ARBITRATION. PLEASE READ THIS SECTION CAREFULLY AS IT AFFECTS YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT. You and Horizen agree that any dispute, claim, or controversy arising out of or relating to these Terms or the breach, termination, enforcement, interpretation, or validity thereof, or the use of the Services or the Program (collectively, "Disputes") will be resolved by binding arbitration, except that each party retains the right: (i) to bring an individual action in small claims court if it qualifies; and (ii) to seek injunctive or other equitable relief in a court of competent jurisdiction to prevent the actual or threatened infringement, misappropriation, or violation of a party's copyrights, trademarks, trade secrets, patents, or other intellectual property rights. The arbitration will be administered by the Cayman Islands Arbitration Centre in accordance with its rules then in effect. The governing law of this arbitration agreement shall be the laws of the Cayman Islands. There will be one arbitrator, the arbitration will be held in Grand Cayman, Cayman Islands, and the language of the arbitration will be English. The parties agree that the arbitrator shall have exclusive authority to decide all issues relating to the interpretation, applicability, enforceability and scope of this arbitration agreement. The arbitrator's fees will be split between the parties or as otherwise determined by the arbitrator. The arbitrator may award costs and fees to the prevailing party as permitted by applicable law. The proceedings and the outcome of the arbitration will be confidential, except to the extent necessary to enforce the award or as required by law. YOU AND HORIZEN AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. Further, unless both you and Horizen agree otherwise, the arbitrator may not consolidate more than one person's claims, and may not otherwise preside over any form of a representative or class proceeding. If this specific class action waiver is found to be unenforceable, then the entirety of this "Dispute Resolution" section will be null and void. Notwithstanding any other provision of these Terms, to the extent that any mandatory consumer protection law of your habitual residence requires application of that jurisdiction's law or grants its courts jurisdiction, this arbitration agreement shall not preclude such rights.