Technical Details

Token Transfer

In the case of transfers from Layer 1 to Layer 2, you need to call a relayer that is registered with the getRelayer() function in order to directly send your assets to the relayer. Ensure that you add the networkCode of the destination network to the last four digits of the transfer amount. Here's a list of things users must check when transferring assets, as failing to do so could result in a failure to slash:

  • The transfer amount is within the range set by the relayer.

  • The relayer supports the networkCode specified.

  • The transaction is a Type2 transaction (EIP-1559 transaction) and does not specify an AccessList.

  • In the case of ERC-20 tokens, transfer them by calling the transfer() function, not the transferFrom() or any other function.

To initiate transfers from Layer 2 to Layer 1 or between Layer 2 networks, you can execute the newTrade() function. Additionally, you can see if the Layer 2 network supports transfers to another Layer 2 network by checking the availableNetwork.

Sending Specific ERC-20 Tokens

ERC-20 tokens are diverse. While most ERC-20 transactions revert when they fail, there might be specific ERC-20 tokens that return false instead of reverting in the event of a transaction failure. Some tokens may have a native fee or rebase mechanism, leading to a discrepancy between the specified amount in the transfer function and the actual transferred amount. There could be tokens that adhere to the ERC-20 standard but have the logic within the transfer function manipulated to carry out malicious attacks.

Currently, Pheasant Network does not have plans to support such peripheral ERC-20 tokens. However, as trustless bridges, other relayers will be able to provide liquidity for any ERC-20 tokens once we open up the role to third parties. While we are planning to ban high-risk tokens on the client and provide warning messages, please exercise caution when handling minor ERC-20 tokens.

Fees

Layer 1 → Layer 2

Users must pay fees, which are determined by the relayers in advance. These fees are categorized into three levels: High, Medium, and Low. The specific fee amounts are subject to change based on the gas costs during the transaction. To access the fee settings, you can utilize the feeList(tokenTypeIndex) function.

Layer 2 → Layer 1 / Layer 2 → Layer 2

Users have the option to set the _fee parameter when invoking the newTrade() function. It is important for users to carefully choose an appropriate fee rate since relayers have the authority to reject requests with excessively low fees if they deem it financially unfavorable. Currently, the fee is automatically set to an appropriate rate on the client to ensure seamless transactions.

Network code

Pheasant Network assigns a network code to each supported network. The codes are managed in the availableNetwork and slashableNetwork array and primarily utilized for token transfers.

  • availableNetwork : Array of network codes eligible for transfers

  • slashableNetwork : Array of network codes eligible for slashing

If the network's code is present in the availableNetwork array but not in the slashableNetwork array, it indicates that a user can send assets to the network, but the network does not support the slashing feature.

There are multiple reasons why the slashing feature may not be supported in certain networks. Some networks may lack secure methods for communication with external networks, while others may have fundamentally different structures compared to Ethereum. Although we could choose to exclude these networks, we have decided to provide bridges for them, even in the absence of the slashing mechanism. This decision is motivated by our desire to contribute to the growth and success of their ecosystems from the early stages of development.

We have plans to update the protocol for networks that currently lack the slashing mechanism, enabling slashing once their communication standards between Layer 1 and Layer 2 are established and the validation mechanism for blockhashes is implemented. Until these updates are implemented, users will need to place trust in the relayers to act honestly.

Update functions

Relayers are given the ability to modify variables associated with token transfers. However, to maintain the security and integrity of the protocol and prevent relayers from acting against the best interests of the protocol, a waiting period must be observed before any changes made by relayers take effect.

To modify variables, relayers start by invoking the executeXXXUpdate() function. They can then finalize the update by calling the finalizeXXXUpdate() function after waiting for the UPDATE_PERIOD to elapse.

The finalizeXXXUpdate() function can be invoked by anyone, not just the relayer who initiated the update process. This is because restricting the authority to complete the update process solely to the relayer in the pre-finalization state would essentially be equivalent to allowing them to immediately update the variable.

The following are the variables relayers can update:

  • PheasantNetworkBridgeChild.sol

    • disputeManager / bondManager

    • tradeThresold / tradeMinimumAmount

    • withdrawalBlockPeriod

    • tradableBondRatio

    • avaiableNetwork / slashableNetwork / nativeIsNotETH

    • feeList

  • CoreChildCheckpointManager.sol

    • rootCheckpointManager

Owner

The protocol grants the power to deactivate contracts to a stakeholder known as the owner. Given that most Layer 2 networks are undergoing continuous updates and their specifications have not been finalized, the Pheasant network may encounter unforeseen vulnerabilities. In such cases, the owner must exercise their authority to implement emergency measures and update the contracts.

At present, the authority of the owner and relayers is distinct. However, we are considering transferring the owner's power to an address, such as 0xdead..., which would result in the relinquishment of the owner's authority.

Bond

Relayers are required to deposit a specific amount of assets as a bond, which can be calculated using the formula: tradeThreshold * tradableBondRatio / 100. The tradeThreshold represents the maximum amount a user can send in a single transaction, and tradableBondRatio is the value used to adjust the bond amount. Both tradeThreshold and tradableBondRatio can be modified by relayers.

In networks with an active slashing mechanism, the default value for tradableBondRatio is set to 220. This value is carefully determined, taking into account the gas fees required for distributing rewards to both the disputer and the user in cases of fraudulent transactions.

After requesting a bond withdrawal, both disputers and users are required to wait for a designated period of time before they can proceed with the bond withdrawal process. The duration of this waiting period is determined by UPDATE_PERIOD, which is currently set to three hours. The purpose of this delay is to prevent a malicious relayer from immediately withdrawing the bond after executing a fraudulent bridge transaction and evading consequences. By imposing this waiting period, it ensures that disputers and users have adequate time to take necessary actions for slashing the bond if fraudulent activity is detected.

Slash

The slashing mechanism varies depending on the source and destination networks.

  • For transactions from Layer 1 to Layer 2: If a certain period of time passes without the responsible relayer successfully transferring the funds deposited by a user, the user or a disputer can execute the slashUpwardTrade() function to slash the relayer's bond.

  • For transactions from Layer 2 to Layer 1 and from Layer 2 to Layer 2: If a relayer has submitted invalid evidence, the user or a disputer can execute the slash() function to slash the relayer's bond.

Here are the details:

Layer 1 → Layer 2

Before calling slashUpwardTrade(), the user needs to send a blockhash from Layer 1 to Layer 2 using CheckpointManager which should contain the transaction of the asset they transferred to the relayer. The slashUpwardTrade() function can only be executed once the blockhash is successfully stored in Layer 2, which may take a few minutes. If the evidence provided as an argument to slashUpwardTrade() is accurate, the function will initiate the slashing process based on the information provided in the evidence.

Layer 2 → Layer 1 / Layer 2 → Layer 2

Prior to invoking the slash() function, a disputer needs to send from Layer 1 to Layer 2 a blockhash of the block number contained in the evidence submitted by the relayer. Once the blockhash from Layer 1 is successfully saved in Layer 2, the slash() function will be executed. The disputer doesn't need to generate the evidence themselves. Instead, they can retrieve the required evidence from the EventLog, which records the relayer's execution of the withdraw() function.

Networks Not Adopting ETH as a Native Token

In networks where the native token is not ETH, such as Polygon PoS, a wrapped form of ETH (WETH) is utilized since the network is not specifically designed to handle ETH natively. Due to the differing contract logic in these networks, they override the functions related to token transfers by inheriting PheasantNetwork.sol to adapt and accommodate these specific network requirements.

nativeIsNotETH

The same slashing mechanism used in bridges connecting Layer 2 to Layer 1 or Layer 2 to Layer 2 networks is also applied to bridges linking a network where the native token is not ETH and another Layer 2 network. However, in the case of Polygon, which utilizes WETH instead of ETH as a native token, it is necessary to set the nativeIsNotETH parameter in the safeCheckEvidenceExceptBlockHash() function. This parameter allows for the customization of slashing conditions that are specific to networks where the native token is not ETH.

BridgeDisputeManager

Evidence

The evidence consists of several data components that are essential for verifying the inclusion of a specific transaction in a given block. Here are the contents of the evidence:

struct Evidence {
		uint256 blockNumber;
		bytes32 blockHash;
		bytes[] txReceiptProof;
		bytes[] txProof;
		bytes transaction;
		uint8[] path;
		bytes txReceipt;
		bytes[] rawTx;
		bytes[] rawBlockHeader;
}
  • blockNumber: The block number that stores the transaction to be verified

  • blockHash: The block hash of the block that stores the transaction to be verified.

  • txReceiptProof: The Merkle Patricia Trie proof of the transaction receipt for the transaction to be verified.

  • txProof: The Merkle Patricia Trie proof of the transaction to be verified.

  • transaction: The transaction encoded using RLP encoding.

  • path: The transaction index of the transaction to be verified.

  • txReceipt: The transaction receipt encoded using RLP encoding.

  • rawTx: The raw data for the transaction to be verified

  • rawBlockHeader: The raw data for the block header of the block that stores the transaction to be verified.

Basic Concept

Upon slashing, the protocol checks whether the bridge transaction executed by the relayer corresponds to the transfer made by the user and whether it is included in the correct blockhash.

Main function

// PheasantNetworkBridgeChild.sol

function isValidEvidence(Types.Trade memory _trade, Types.Evidence calldata _evidence) public view returns (bool) {
		return safeCheckEvidenceExceptBlockHash(_trade, _evidence)
			&& disputeManager.verifyBlockHash(_evidence.blockHash, _trade.destCode, _evidence.blockNumber);
}

function safeCheckEvidenceExceptBlockHash(Types.Trade memory _trade, Types.Evidence calldata _evidence) public view returns (bool) {
		bool isValidTx = false;

		uint256 destCode = _trade.destCode;
		uint8 tokenTypeIndex = _trade.tokenTypeIndex;
		bool isETHTrade = (tokenTypeIndex == Lib_DefaultValues.ETH_TOKEN_INDEX);
		bool isUpward = (destCode == networkCode);

		try disputeManager.checkEvidenceExceptBlockHash(
				isUpward ? true : (nativeIsNotETH[destCode] == uint(Bool.FALSE)) && (isETHTrade), // isNativeTokenCheck
				_trade.amount - _trade.fee, // trade amount
				isUpward ? networkCode : 0, // networkCheckCode
				isUpward ? _trade.relayer : _trade.to, // receiver
				tokenAddress[destCode][tokenTypeIndex], // tokenAddress
				_evidence
		) returns (bool result) {
				isValidTx = result;
		} catch {
				isValidTx = false; // if error, return false
		}

		return isValidTx;
}

// BridgeDisputeManager.sol

function checkEvidenceExceptBlockHash(
		bool _isNativeTokenCheck,
		uint256 _tradeAmount,
		uint256 _networkCheckCode,
		address _receiver,
		address _tokenAddress,
		Types.Evidence calldata _evidence
) public view returns (bool) {
		bool isValidTx = false;

		if(_isNativeTokenCheck) {
				isValidTx = checkTransferTx(_evidence.transaction, _receiver, _tradeAmount, _networkCheckCode);
		} else {
				isValidTx = checkERC20TransferTx(_evidence.transaction, _tokenAddress, _receiver, _tradeAmount, _networkCheckCode)
						&& verifyReceipt(_evidence.txReceipt)
						&& verifyProof(keccak256(_evidence.txReceipt), _evidence.txReceiptProof, _evidence.rawBlockHeader[BLOCKHEADER_RECEIPTROOT_INDEX], _evidence.path);
		}

		return isValidTx
				&& verifyBlockHeader(_evidence.blockHash, _evidence.rawBlockHeader)
				&& verifyProof(keccak256(_evidence.transaction), _evidence.txProof, _evidence.rawBlockHeader[BLOCKHEADER_TRANSACTIONROOT_INDEX], _evidence.path)
				&& verifyRawTx(_evidence.transaction, _evidence.rawTx);
}

function verifyBlockHash(bytes32 _blockHash, uint _destCode, uint _blockNumber) public view returns (bool){
		require(checkpointManager.getBlockHash(_destCode, _blockNumber) != bytes32(0), "Relay blockhash first");
		return checkpointManager.getBlockHash(_destCode, _blockNumber) == _blockHash;
}

The isValidEvidence() function is called to verify the evidence. The conditions for slashing a bond are as follows:

  • For transactions from Layer 1 to Layer 2: The checkEvidenceExceptBlockHash() and verifyBlockHash() functions return true. This means that the submitted evidence is correct and the blockhash in the evidence matches the blockhash for the transaction obtained from Layer 1.

  • For transactions from Layer 2 to Layer 1 and Layer 2 to Layer 2: The checkEvidenceExceptBlockHash() and verifyBlockHash() functions return false or trigger a revert. This indicates that the submitted evidence is incorrect and the blockhash in the evidence conflicts with the blockhash for the transaction obtained from Layer 1.

checkEvidenceExceptBlockHash()

function checkEvidenceExceptBlockHash(
		bool _isNativeTokenCheck,
		uint256 _tradeAmount,
		uint256 _networkCheckCode,
		address _receiver,
		address _tokenAddress,
		Types.Evidence calldata _evidence
) public view returns (bool)

This function verifies the inclusion of a valid transaction in the evidence and checks the consistency of the blockhash with other data within it. The specific functions to be invoked depend on whether users are transferring ETH or ERC-20 tokens.

  • For ETH transfers: The checkTransferTx() function will be called to verify the transaction.

  • For ERC-20 token transfers: Three functions, namely checkERC20TransferTx(), verifyReceipt(), and verifyProof(), will be invoked. This is because the protocol needs to ensure the successful execution of the transfer.

checkTransferTx()

function checkTransferTx(bytes calldata transaction, address recipient, uint256 amount, uint256 networkCode) external pure returns (bool);

This function decodes the transaction to extract information such as the recipient address and the amount sent.

  • For Layer 1 to Layer 2 transfers: Set the value of _networkCheckCode to the network code of the destination network. The last four digits added to the transfer amount represent the network code.

  • For Layer 2 to Layer 1 or Layer 2 to Layer 2 transfers: Set the value of _networkCheckCode to 0.

checkERC20TransferTx()

function checkERC20TransferTx(bytes calldata transaction, address tokenAddress, address recipient, uint256 amount, uint256 networkCode) external pure returns (bool);

This function verifies the validity of the amount and recipient address in the input data of the transaction.

  • For Layer 1 to Layer 2 transfers: Set the value of _networkCheckCode to the network code of the destination network.

  • For Layer 2 to Layer 1 or Layer 2 to Layer 2 transfers: Set the value of _networkCheckCode to 0.

verifyReceipt()

function verifyReceipt(bytes calldata txReceipt) external pure returns (bool);

This function checks whether the transaction was successful by validating the receipt included in the evidence.

verifyBlockHeader()

function verifyBlockHeader(bytes32 blockHash, bytes[] calldata blockHeaderRaw) external pure returns (bool);

This function verifies whether the hash value of rawBlockHeader in the evidence matches the blockhash contained in the same evidence.

verifyProof()

function verifyProof(bytes32 txHash, bytes[] memory proof, bytes memory bytesRoot, uint8[] memory path) external pure returns (bool);

This function verifies the Merkle Patricia Trie proof by:

  • Decoding the proof recursively to check if the endpoint of the path matches the transaction concerned.

  • Checking whether the node is a leaf, extension, or branch by examining the node's number of elements and nibble.

  • Verifying that the Merkle root matches the endpoint of the proof.

verifyRawTx()

function verifyRawTx(bytes memory transaction, bytes[] calldata txRaw) external pure returns (bool);

This function verifies that the hash value of rawTransaction in the evidence matches the transaction included in the evidence.

Last updated