Description:
Proxy contract enabling upgradeable smart contract patterns. Delegates calls to an implementation contract.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/upgrade/Gateway202509.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import "../Gateway.sol";
// New Gateway logic contract with an fee initializer
contract Gateway202509 is Gateway {
constructor(address beefyClient, address agentExecutor) Gateway(beefyClient, agentExecutor) {}
// Override parent initializer to prevent re-initialization of storage.
function initialize(bytes calldata) external override {
// Ensure that arbitrary users cannot initialize storage in this logic contract.
if (ERC1967.load() == address(0)) {
revert Unauthorized();
}
AssetsStorage.Layout storage assets = AssetsStorage.layout();
assets.foreignTokenDecimals = 10;
assets.maxDestinationFee = 20_000_000_000; // 2 DOT
}
}
"
},
"src/Gateway.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {MerkleProof} from "openzeppelin/utils/cryptography/MerkleProof.sol";
import {Verification} from "./Verification.sol";
import {Initializer} from "./Initializer.sol";
import {AgentExecutor} from "./AgentExecutor.sol";
import {IGatewayBase} from "./interfaces/IGatewayBase.sol";
import {
OperatingMode,
ParaID,
Channel,
ChannelID,
MultiAddress,
InboundMessageV1,
CommandV1,
InboundMessageV2,
CommandV2,
CommandKind,
CallsV1,
HandlersV1,
CallsV2,
HandlersV2,
IGatewayV1,
IGatewayV2
} from "./Types.sol";
import {Network} from "./v2/Types.sol";
import {Upgrade} from "./Upgrade.sol";
import {IInitializable} from "./interfaces/IInitializable.sol";
import {IUpgradable} from "./interfaces/IUpgradable.sol";
import {ERC1967} from "./utils/ERC1967.sol";
import {Address} from "./utils/Address.sol";
import {SafeNativeTransfer} from "./utils/SafeTransfer.sol";
import {Math} from "./utils/Math.sol";
import {ScaleCodec} from "./utils/ScaleCodec.sol";
import {Functions} from "./Functions.sol";
import {Constants} from "./Constants.sol";
import {CoreStorage} from "./storage/CoreStorage.sol";
import {PricingStorage} from "./storage/PricingStorage.sol";
import {AssetsStorage} from "./storage/AssetsStorage.sol";
import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol";
contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgradable {
using Address for address;
using SafeNativeTransfer for address payable;
// Address of the code to be run within `Agent.sol` using delegatecall
address public immutable AGENT_EXECUTOR;
// Consensus client for Polkadot
address public immutable BEEFY_CLIENT;
// Message handlers can only be dispatched by the gateway itself
modifier onlySelf() {
if (msg.sender != address(this)) {
revert IGatewayBase.Unauthorized();
}
_;
}
// Makes functions nonreentrant
modifier nonreentrant() {
assembly {
if tload(0) { revert(0, 0) }
// Set the flag to mark the function is currently executing.
tstore(0, 1)
}
_;
// Unlocks the guard, making the pattern composable.
// After the function exits, it can be called again, even in the same transaction.
assembly {
tstore(0, 0)
}
}
constructor(address beefyClient, address agentExecutor) {
BEEFY_CLIENT = beefyClient;
AGENT_EXECUTOR = agentExecutor;
}
/*
* _________
* \_ ___ \ ____ _____ _____ ____ ____
* / \ \/ / _ \ / \ / \ / _ \ / \
* \ \____( <_> )| Y Y \| Y Y \( <_> )| | \
* \______ / \____/ |__|_| /|__|_| / \____/ |___| /
* \/ \/ \/ \/
*/
// Verify that a message commitment is considered finalized by our BEEFY light client.
function _verifyCommitment(bytes32 commitment, Verification.Proof calldata proof, bool isV2)
internal
view
virtual
returns (bool)
{
return Verification.verifyCommitment(
BEEFY_CLIENT,
ScaleCodec.encodeU32(uint32(ParaID.unwrap(Constants.BRIDGE_HUB_PARA_ID))),
commitment,
proof,
isV2
);
}
/*
* _____ __________ .___ ____
* / _ \ \______ \| | ___ __/_ |
* / /_\ \ | ___/| | \ \/ / | |
* / | \ | | | | \ / | |
* \____|__ / |____| |___| \_/ |___|
* \/
*/
/**
* APIv1 Constants
*/
// Gas used for:
// 1. Mapping a command id to an implementation function
// 2. Calling implementation function
uint256 constant DISPATCH_OVERHEAD_GAS_V1 = 10_000;
/**
* APIv1 External API
*/
/// @dev Submit a message from Polkadot for verification and dispatch
/// @param message A message produced by the OutboundQueue pallet on BridgeHub
/// @param leafProof A message proof used to verify that the message is in the merkle tree
/// committed by the OutboundQueue pallet.
/// @param headerProof A proof that the commitment is included in parachain header that was
/// finalized by BEEFY.
function submitV1(
InboundMessageV1 calldata message,
bytes32[] calldata leafProof,
Verification.Proof calldata headerProof
) external nonreentrant {
uint256 startGas = gasleft();
Channel storage channel = Functions.ensureChannel(message.channelID);
// Ensure this message is not being replayed
if (message.nonce != channel.inboundNonce + 1) {
revert IGatewayBase.InvalidNonce();
}
// Increment nonce for origin.
// This also prevents the re-entrancy case in which a malicious party tries to re-enter by
// calling `submitInbound` again with the same (message, leafProof, headerProof) arguments.
channel.inboundNonce++;
// Produce the commitment (message root) by applying the leaf proof to the message leaf
bytes32 leafHash = keccak256(abi.encode(message));
bytes32 commitment = MerkleProof.processProof(leafProof, leafHash);
// Verify that the commitment is included in a parachain header finalized by BEEFY.
if (!_verifyCommitment(commitment, headerProof, false)) {
revert IGatewayBase.InvalidProof();
}
// Make sure relayers provide enough gas so that inner message dispatch
// does not run out of gas.
uint256 maxDispatchGas = message.maxDispatchGas;
if (gasleft() * 63 / 64 < maxDispatchGas + DISPATCH_OVERHEAD_GAS_V1) {
revert IGatewayBase.NotEnoughGas();
}
bool success = true;
// Dispatch message to a handler
if (message.command == CommandV1.AgentExecute) {
try Gateway(this).v1_handleAgentExecute{gas: maxDispatchGas}(message.params) {}
catch {
success = false;
}
} else if (message.command == CommandV1.SetOperatingMode) {
try Gateway(this).v1_handleSetOperatingMode{gas: maxDispatchGas}(message.params) {}
catch {
success = false;
}
} else if (message.command == CommandV1.Upgrade) {
try Gateway(this).v1_handleUpgrade{gas: maxDispatchGas}(message.params) {}
catch {
success = false;
}
} else if (message.command == CommandV1.SetTokenTransferFees) {
try Gateway(this).v1_handleSetTokenTransferFees{gas: maxDispatchGas}(message.params) {}
catch {
success = false;
}
} else if (message.command == CommandV1.SetPricingParameters) {
try Gateway(this).v1_handleSetPricingParameters{gas: maxDispatchGas}(message.params) {}
catch {
success = false;
}
} else if (message.command == CommandV1.UnlockNativeToken) {
try Gateway(this).v1_handleUnlockNativeToken{gas: maxDispatchGas}(message.params) {}
catch {
success = false;
}
} else if (message.command == CommandV1.RegisterForeignToken) {
try Gateway(this).v1_handleRegisterForeignToken{gas: maxDispatchGas}(message.params) {}
catch {
success = false;
}
} else if (message.command == CommandV1.MintForeignToken) {
try Gateway(this).v1_handleMintForeignToken{gas: maxDispatchGas}(
message.channelID, message.params
) {} catch {
success = false;
}
} else {
success = false;
}
// Calculate a gas refund, capped to protect against huge spikes in `tx.gasprice`
// that could drain funds unnecessarily. During these spikes, relayers should back off.
uint256 gasUsed = v1_transactionBaseGas() + (startGas - gasleft());
uint256 refund = gasUsed * Math.min(tx.gasprice, message.maxFeePerGas);
// Add the reward to the refund amount. If the sum is more than the funds available
// in the gateway, then reduce the total amount
uint256 amount = Math.min(refund + message.reward, address(this).balance);
// Do the payment if there funds available in the gateway
if (amount > Functions.dustThreshold()) {
payable(msg.sender).safeNativeTransfer(amount);
}
emit IGatewayV1.InboundMessageDispatched(
message.channelID, message.nonce, message.id, success
);
}
function operatingMode()
external
view
override(IGatewayV1, IGatewayV2)
returns (OperatingMode)
{
return CoreStorage.layout().mode;
}
function channelOperatingModeOf(ChannelID channelID) external view returns (OperatingMode) {
return CallsV1.channelOperatingModeOf(channelID);
}
function channelNoncesOf(ChannelID channelID) external view returns (uint64, uint64) {
return CallsV1.channelNoncesOf(channelID);
}
function agentOf(bytes32 agentID)
external
view
override(IGatewayV1, IGatewayV2)
returns (address)
{
return Functions.ensureAgent(agentID);
}
function pricingParameters() external view returns (UD60x18, uint128) {
return CallsV1.pricingParameters();
}
function implementation() public view returns (address) {
return ERC1967.load();
}
function isTokenRegistered(address token)
external
view
override(IGatewayV1, IGatewayV2)
returns (bool)
{
return CallsV1.isTokenRegistered(token);
}
function depositEther() external payable {
emit Deposited(msg.sender, msg.value);
}
function queryForeignTokenID(address token) external view returns (bytes32) {
return AssetsStorage.layout().tokenRegistry[token].foreignID;
}
// Total fee for registering a token
function quoteRegisterTokenFee() external view returns (uint256) {
return CallsV1.quoteRegisterTokenFee();
}
// Register an Ethereum-native token in the gateway and on AssetHub
function registerToken(address token) external payable nonreentrant {
CallsV1.registerToken(token);
}
// Total fee for sending a token
function quoteSendTokenFee(address token, ParaID destinationChain, uint128 destinationFee)
external
view
returns (uint256)
{
return CallsV1.quoteSendTokenFee(token, destinationChain, destinationFee);
}
// Transfer ERC20 tokens to a Polkadot parachain
function sendToken(
address token,
ParaID destinationChain,
MultiAddress calldata destinationAddress,
uint128 destinationFee,
uint128 amount
) external payable nonreentrant {
CallsV1.sendToken(
token, msg.sender, destinationChain, destinationAddress, destinationFee, amount
);
}
// @dev Get token address by tokenID
function tokenAddressOf(bytes32 tokenID) external view returns (address) {
return CallsV1.tokenAddressOf(tokenID);
}
/**
* APIv1 Inbound Message Handlers
*/
// Execute code within an agent
function v1_handleAgentExecute(bytes calldata data) external onlySelf {
HandlersV1.agentExecute(AGENT_EXECUTOR, data);
}
/// @dev Perform an upgrade of the gateway
function v1_handleUpgrade(bytes calldata data) external onlySelf {
HandlersV1.upgrade(data);
}
// @dev Set the operating mode of the gateway
function v1_handleSetOperatingMode(bytes calldata data) external onlySelf {
HandlersV1.setOperatingMode(data);
}
// @dev Set token fees of the gateway
function v1_handleSetTokenTransferFees(bytes calldata data) external onlySelf {
HandlersV1.setTokenTransferFees(data);
}
// @dev Set pricing params of the gateway
function v1_handleSetPricingParameters(bytes calldata data) external onlySelf {
HandlersV1.setPricingParameters(data);
}
// @dev Transfer Ethereum native token back from polkadot
function v1_handleUnlockNativeToken(bytes calldata data) external onlySelf {
HandlersV1.unlockNativeToken(AGENT_EXECUTOR, data);
}
// @dev Register a new fungible Polkadot token for an agent
function v1_handleRegisterForeignToken(bytes calldata data) external onlySelf {
HandlersV1.registerForeignToken(data);
}
// @dev Mint foreign token from polkadot
function v1_handleMintForeignToken(ChannelID channelID, bytes calldata data)
external
onlySelf
{
HandlersV1.mintForeignToken(channelID, data);
}
/**
* APIv1 Internal functions
*/
// Best-effort attempt at estimating the base gas use of `submitInbound` transaction, outside
// the block of code that is metered.
// This includes:
// * Cost paid for every transaction: 21000 gas
// * Cost of calldata: Zero byte = 4 gas, Non-zero byte = 16 gas
// * Cost of code inside submitInitial that is not metered: 14_698
//
// The major cost of calldata are the merkle proofs, which should dominate anything else
// (including the message payload) Since the merkle proofs are hashes, they are much more
// likely to be composed of more non-zero bytes than zero bytes.
//
// Reference: Ethereum Yellow Paper
function v1_transactionBaseGas() internal pure returns (uint256) {
return 21_000 + 14_698 + (msg.data.length * 16);
}
/*
* _____ __________ .___ ________
* / _ \ \______ \| | ___ __\_____ \
* / /_\ \ | ___/| | \ \/ / / ____/§
* / | \ | | | | \ / / \
* \____|__ / |____| |___| \_/ \_______ \
* \/ \/
*/
/// Overhead in selecting the dispatch handler for an arbitrary command
uint256 internal constant DISPATCH_OVERHEAD_GAS_V2 = 24_000;
function v2_submit(
InboundMessageV2 calldata message,
bytes32[] calldata leafProof,
Verification.Proof calldata headerProof,
bytes32 rewardAddress
) external nonreentrant {
CoreStorage.Layout storage $ = CoreStorage.layout();
if ($.inboundNonce.get(message.nonce)) {
revert IGatewayBase.InvalidNonce();
}
bytes32 leafHash = keccak256(abi.encode(message));
$.inboundNonce.set(message.nonce);
// Produce the commitment (message root) by applying the leaf proof to the message leaf
bytes32 commitment = MerkleProof.processProof(leafProof, leafHash);
// Verify that the commitment is included in a parachain header finalized by BEEFY.
if (!_verifyCommitment(commitment, headerProof, true)) {
revert IGatewayBase.InvalidProof();
}
// Dispatch the message payload. The boolean returned indicates whether all commands succeeded.
bool success = v2_dispatch(message);
// Emit the event with a success value "true" if all commands successfully executed, otherwise "false"
// if all or some of the commands failed.
emit IGatewayV2.InboundMessageDispatched(
message.nonce, message.topic, success, rewardAddress
);
}
function v2_outboundNonce() external view returns (uint64) {
return CallsV2.outboundNonce();
}
function v2_isDispatched(uint64 nonce) external view returns (bool) {
return CoreStorage.layout().inboundNonce.get(nonce);
}
// See docs for `IGateway.v2_sendMessage`
function v2_sendMessage(
bytes calldata xcm,
bytes[] calldata assets,
bytes calldata claimer,
uint128 executionFee,
uint128 relayerFee
) external payable nonreentrant {
CallsV2.sendMessage(xcm, assets, claimer, executionFee, relayerFee);
}
// See docs for `IGateway.v2_registerToken`
function v2_registerToken(
address token,
uint8 network,
uint128 executionFee,
uint128 relayerFee
) external payable nonreentrant {
require(network == uint8(Network.Polkadot), IGatewayV2.InvalidNetwork());
CallsV2.registerToken(token, Network(network), executionFee, relayerFee);
}
// See docs for `IGateway.v2_createAgent`
function v2_createAgent(bytes32 id) external {
CallsV2.createAgent(id);
}
/**
* APIv2 Message Handlers
*/
// Perform an upgrade of the gateway
function v2_handleUpgrade(bytes calldata data) external onlySelf {
HandlersV2.upgrade(data);
}
// Set the operating mode of the gateway
function v2_handleSetOperatingMode(bytes calldata data) external onlySelf {
HandlersV2.setOperatingMode(data);
}
// Unlock Native token
function v2_handleUnlockNativeToken(bytes calldata data) external onlySelf {
HandlersV2.unlockNativeToken(AGENT_EXECUTOR, data);
}
// Mint foreign token from polkadot
function v2_handleRegisterForeignToken(bytes calldata data) external onlySelf {
HandlersV2.registerForeignToken(data);
}
// Mint foreign token from polkadot
function v2_handleMintForeignToken(bytes calldata data) external onlySelf {
HandlersV2.mintForeignToken(data);
}
// Call an arbitrary contract function
function v2_handleCallContract(bytes32 origin, bytes calldata data) external onlySelf {
HandlersV2.callContract(origin, AGENT_EXECUTOR, data);
}
/**
* APIv2 Internal functions
*/
// Internal helper to dispatch a single command
function _dispatchCommand(CommandV2 calldata command, bytes32 origin)
internal
returns (bool)
{
// check that there is enough gas available to forward to the command handler
if (gasleft() * 63 / 64 < command.gas + DISPATCH_OVERHEAD_GAS_V2) {
revert IGatewayV2.InsufficientGasLimit();
}
if (command.kind == CommandKind.Upgrade) {
try Gateway(this).v2_handleUpgrade{gas: command.gas}(command.payload) {}
catch {
return false;
}
} else if (command.kind == CommandKind.SetOperatingMode) {
try Gateway(this).v2_handleSetOperatingMode{gas: command.gas}(command.payload) {}
catch {
return false;
}
} else if (command.kind == CommandKind.UnlockNativeToken) {
try Gateway(this).v2_handleUnlockNativeToken{gas: command.gas}(command.payload) {}
catch {
return false;
}
} else if (command.kind == CommandKind.RegisterForeignToken) {
try Gateway(this).v2_handleRegisterForeignToken{gas: command.gas}(command.payload) {}
catch {
return false;
}
} else if (command.kind == CommandKind.MintForeignToken) {
try Gateway(this).v2_handleMintForeignToken{gas: command.gas}(command.payload) {}
catch {
return false;
}
} else if (command.kind == CommandKind.CallContract) {
try Gateway(this).v2_handleCallContract{gas: command.gas}(origin, command.payload) {}
catch {
return false;
}
} else {
// Unknown command
return false;
}
return true;
}
// Dispatch all the commands within the batch of commands in the message payload. Each command is processed
// independently, such that failures emit a `CommandFailed` event without stopping execution of subsequent commands.
function v2_dispatch(InboundMessageV2 calldata message) internal returns (bool) {
bool allCommandsSucceeded = true;
for (uint256 i = 0; i < message.commands.length; i++) {
if (!_dispatchCommand(message.commands[i], message.origin)) {
emit IGatewayV2.CommandFailed(message.nonce, i);
allCommandsSucceeded = false;
}
}
return allCommandsSucceeded;
}
/**
* Upgrades
*/
/// Initialize storage within the `GatewayProxy` contract using this initializer.
///
/// This initializer cannot be called externally via the proxy as the function selector
/// is overshadowed in the proxy.
///
/// This implementation is only intended to initialize storage for initial deployments
/// of the `GatewayProxy` contract to transient or long-lived testnets.
///
/// The `GatewayProxy` deployed to Ethereum mainnet already has its storage initialized.
/// When its logic contract needs to upgraded, a new logic contract should be developed
/// that inherits from this base `Gateway` contract. Particularly, the `initialize` function
/// must be overridden to ensure selective initialization of storage fields relevant
/// to the upgrade.
///
/// ```solidity
/// contract Gateway202508 is Gateway {
/// function initialize(bytes calldata data) external override {
/// if (ERC1967.load() == address(0)) {
/// revert Unauthorized();
/// }
/// # Initialization routines here...
/// }
/// }
/// ```
///
function initialize(bytes calldata data) external virtual {
Initializer.initialize(data);
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.2) (utils/cryptography/MerkleProof.sol)
pragma solidity ^0.8.0;
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* The tree and the proofs can be generated using our
* https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
* You will find a quickstart guide in the readme.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, or use a hash function other than keccak256 for hashing leaves.
* This is because the concatenation of a sorted pair of internal nodes in
* the merkle tree could be reinterpreted as a leaf value.
* OpenZeppelin's JavaScript library generates merkle trees that are safe
* against this attack out of the box.
*/
library MerkleProof {
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*/
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
/**
* @dev Calldata version of {verify}
*
* _Available since v4.7._
*/
function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProofCalldata(proof, leaf) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leafs & pre-images are assumed to be sorted.
*
* _Available since v4.4._
*/
function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Calldata version of {processProof}
*
* _Available since v4.7._
*/
function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Returns true if the `leaves` can be simultaneously proven to be a part of a merkle tree defined by
* `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
*
* CAUTION: Not all merkle trees admit multiproofs. See {processMultiProof} for details.
*
* _Available since v4.7._
*/
function multiProofVerify(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProof(proof, proofFlags, leaves) == root;
}
/**
* @dev Calldata version of {multiProofVerify}
*
* CAUTION: Not all merkle trees admit multiproofs. See {processMultiProof} for details.
*
* _Available since v4.7._
*/
function multiProofVerifyCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProofCalldata(proof, proofFlags, leaves) == root;
}
/**
* @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
* respectively.
*
* CAUTION: Not all merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*
* _Available since v4.7._
*/
function processMultiProof(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofLen = proof.length;
uint256 totalHashes = proofFlags.length;
// Check proof validity.
require(leavesLen + proofLen - 1 == totalHashes, "MerkleProof: invalid multiproof");
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < totalHashes; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = _hashPair(a, b);
}
if (totalHashes > 0) {
require(proofPos == proofLen, "MerkleProof: invalid multiproof");
unchecked {
return hashes[totalHashes - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Calldata version of {processMultiProof}.
*
* CAUTION: Not all merkle trees admit multiproofs. See {processMultiProof} for details.
*
* _Available since v4.7._
*/
function processMultiProofCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofLen = proof.length;
uint256 totalHashes = proofFlags.length;
// Check proof validity.
require(leavesLen + proofLen - 1 == totalHashes, "MerkleProof: invalid multiproof");
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < totalHashes; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = _hashPair(a, b);
}
if (totalHashes > 0) {
require(proofPos == proofLen, "MerkleProof: invalid multiproof");
unchecked {
return hashes[totalHashes - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
}
function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, a)
mstore(0x20, b)
value := keccak256(0x00, 0x40)
}
}
}
"
},
"src/Verification.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {SubstrateMerkleProof} from "./utils/SubstrateMerkleProof.sol";
import {BeefyClient} from "./BeefyClient.sol";
import {ScaleCodec} from "./utils/ScaleCodec.sol";
import {SubstrateTypes} from "./SubstrateTypes.sol";
library Verification {
/// @dev Merkle proof for parachain header finalized by BEEFY
/// Reference: https://github.com/paritytech/polkadot/blob/09b61286da11921a3dda0a8e4015ceb9ef9cffca/runtime/rococo/src/lib.rs#L1312
struct HeadProof {
// The leaf index of the parachain being proven
uint256 pos;
// The number of leaves in the merkle tree
uint256 width;
// The proof items
bytes32[] proof;
}
/// @dev An MMRLeaf without the `leaf_extra` field.
/// Reference: https://github.com/paritytech/substrate/blob/14e0a0b628f9154c5a2c870062c3aac7df8983ed/primitives/consensus/beefy/src/mmr.rs#L52
struct MMRLeafPartial {
uint8 version;
uint32 parentNumber;
bytes32 parentHash;
uint64 nextAuthoritySetID;
uint32 nextAuthoritySetLen;
bytes32 nextAuthoritySetRoot;
}
/// @dev Parachain header
/// References:
/// * https://paritytech.github.io/substrate/master/sp_runtime/generic/struct.Header.html
/// * https://github.com/paritytech/substrate/blob/14e0a0b628f9154c5a2c870062c3aac7df8983ed/primitives/runtime/src/generic/header.rs#L41
struct ParachainHeader {
bytes32 parentHash;
uint256 number;
bytes32 stateRoot;
bytes32 extrinsicsRoot;
DigestItem[] digestItems;
}
/// @dev Represents a digest item within a parachain header.
/// References:
/// * https://paritytech.github.io/substrate/master/sp_runtime/generic/enum.DigestItem.html
/// * https://github.com/paritytech/substrate/blob/14e0a0b628f9154c5a2c870062c3aac7df8983ed/primitives/runtime/src/generic/digest.rs#L75
struct DigestItem {
uint256 kind;
bytes4 consensusEngineID;
bytes data;
}
/// @dev A chain of proofs
struct Proof {
// The parachain header containing the message commitment as a digest item
ParachainHeader header;
// The proof used to generate a merkle root of parachain heads
HeadProof headProof;
// The MMR leaf to be proven
MMRLeafPartial leafPartial;
// The MMR leaf prove
bytes32[] leafProof;
// The order in which proof items should be combined
uint256 leafProofOrder;
}
error InvalidParachainHeader();
/// @dev IDs of enum variants of DigestItem
/// Reference: https://github.com/paritytech/substrate/blob/14e0a0b628f9154c5a2c870062c3aac7df8983ed/primitives/runtime/src/generic/digest.rs#L201
uint256 public constant DIGEST_ITEM_OTHER = 0;
uint256 public constant DIGEST_ITEM_CONSENSUS = 4;
uint256 public constant DIGEST_ITEM_SEAL = 5;
uint256 public constant DIGEST_ITEM_PRERUNTIME = 6;
uint256 public constant DIGEST_ITEM_RUNTIME_ENVIRONMENT_UPDATED = 8;
/// @dev Enum variant ID for CustomDigestItem::Snowbridge
bytes1 public constant DIGEST_ITEM_OTHER_SNOWBRIDGE = 0x00;
/// @dev Enum variant ID for CustomDigestItem::SnowbridgeV2
bytes1 public constant DIGEST_ITEM_OTHER_SNOWBRIDGE_V2 = 0x01;
/// @dev Verify the message commitment by applying several proofs
///
/// 1. First check that the commitment is included in the digest items of the parachain header
/// 2. Generate the root of the parachain heads merkle tree
/// 3. Construct an MMR leaf containing the parachain heads root.
/// 4. Verify that the MMR leaf is included in the MMR maintained by the BEEFY light client.
///
/// Background info:
///
/// In the Polkadot relay chain, for every block:
/// 1. A merkle root of finalized parachain headers is constructed:
/// https://github.com/paritytech/polkadot/blob/09b61286da11921a3dda0a8e4015ceb9ef9cffca/runtime/rococo/src/lib.rs#L1312.
/// 2. An MMR leaf is produced, containing this parachain headers root, and is then inserted into the
/// MMR maintained by the `merkle-mountain-range` pallet:
/// https://github.com/paritytech/substrate/tree/master/frame/merkle-mountain-range
///
/// @param beefyClient The address of the BEEFY light client
/// @param encodedParaID The SCALE-encoded parachain ID of BridgeHub
/// @param commitment The message commitment root expected to be contained within the
/// digest of BridgeHub parachain header.
/// @param proof The chain of proofs described above
function verifyCommitment(
address beefyClient,
bytes4 encodedParaID,
bytes32 commitment,
Proof calldata proof,
bool isV2
) external view returns (bool) {
// Verify that parachain header contains the commitment
if (!isCommitmentInHeaderDigest(commitment, proof.header, isV2)) {
return false;
}
if (proof.headProof.pos >= proof.headProof.width) {
return false;
}
// Compute the merkle leaf hash of our parachain
bytes32 parachainHeadHash = createParachainHeaderMerkleLeaf(encodedParaID, proof.header);
// Compute the merkle root hash of all parachain heads
bytes32 parachainHeadsRoot = SubstrateMerkleProof.computeRoot(
parachainHeadHash, proof.headProof.pos, proof.headProof.width, proof.headProof.proof
);
bytes32 leafHash = createMMRLeaf(proof.leafPartial, parachainHeadsRoot);
// Verify that the MMR leaf is part of the MMR maintained by the BEEFY light client
return BeefyClient(beefyClient).verifyMMRLeafProof(
leafHash, proof.leafProof, proof.leafProofOrder
);
}
// Verify that a message commitment is in the header digest
function isCommitmentInHeaderDigest(
bytes32 commitment,
ParachainHeader calldata header,
bool isV2
) internal pure returns (bool) {
bytes1 digestItemOtherKind = isV2 ? DIGEST_ITEM_OTHER_SNOWBRIDGE_V2 : DIGEST_ITEM_OTHER_SNOWBRIDGE;
for (uint256 i = 0; i < header.digestItems.length; i++) {
// First check if the digest item is of the correct kind (DIGEST_ITEM_OTHER)
// and has the correct length (33 bytes)
if (header.digestItems[i].kind == DIGEST_ITEM_OTHER &&
header.digestItems[i].data.length == 33 &&
header.digestItems[i].data[0] == digestItemOtherKind &&
commitment == bytes32(header.digestItems[i].data[1:])
) {
return true;
}
}
return false;
}
// SCALE-Encodes: Vec<DigestItem>
// Reference: https://github.com/paritytech/substrate/blob/14e0a0b628f9154c5a2c870062c3aac7df8983ed/primitives/runtime/src/generic/digest.rs#L40
function encodeDigestItems(DigestItem[] calldata digestItems)
internal
pure
returns (bytes memory)
{
// encode all digest items into a buffer
bytes memory accum = hex"";
for (uint256 i = 0; i < digestItems.length; i++) {
accum = bytes.concat(accum, encodeDigestItem(digestItems[i]));
}
// Encode number of digest items, followed by encoded digest items
return bytes.concat(ScaleCodec.checkedEncodeCompactU32(digestItems.length), accum);
}
function encodeDigestItem(DigestItem calldata digestItem)
internal
pure
returns (bytes memory)
{
if (
digestItem.kind == DIGEST_ITEM_PRERUNTIME || digestItem.kind == DIGEST_ITEM_CONSENSUS
|| digestItem.kind == DIGEST_ITEM_SEAL
) {
return bytes.concat(
bytes1(uint8(digestItem.kind)),
digestItem.consensusEngineID,
ScaleCodec.checkedEncodeCompactU32(digestItem.data.length),
digestItem.data
);
} else if (digestItem.kind == DIGEST_ITEM_OTHER) {
return bytes.concat(
bytes1(uint8(DIGEST_ITEM_OTHER)),
ScaleCodec.checkedEncodeCompactU32(digestItem.data.length),
digestItem.data
);
} else if (digestItem.kind == DIGEST_ITEM_RUNTIME_ENVIRONMENT_UPDATED) {
return bytes.concat(bytes1(uint8(DIGEST_ITEM_RUNTIME_ENVIRONMENT_UPDATED)));
} else {
revert InvalidParachainHeader();
}
}
// Creates a keccak hash of a SCALE-encoded parachain header
function createParachainHeaderMerkleLeaf(bytes4 encodedParaID, ParachainHeader calldata header)
internal
pure
returns (bytes32)
{
// Hash of encoded parachain header merkle leaf
return keccak256(createParachainHeader(encodedParaID, header));
}
function createParachainHeader(bytes4 encodedParaID, ParachainHeader calldata header)
internal
pure
returns (bytes memory)
{
bytes memory encodedHeader = bytes.concat(
// H256
header.parentHash,
// Compact unsigned int
ScaleCodec.checkedEncodeCompactU32(header.number),
// H256
header.stateRoot,
// H256
header.extrinsicsRoot,
// Vec<DigestItem>
encodeDigestItems(header.digestItems)
);
return bytes.concat(
// u32
encodedParaID,
// length of encoded header
ScaleCodec.checkedEncodeCompactU32(encodedHeader.length),
encodedHeader
);
}
// SCALE-encode: MMRLeaf
// Reference: https://github.com/paritytech/substrate/blob/14e0a0b628f9154c5a2c870062c3aac7df8983ed/primitives/consensus/beefy/src/mmr.rs#L52
function createMMRLeaf(MMRLeafPartial memory leaf, bytes32 parachainHeadsRoot)
internal
pure
returns (bytes32)
{
bytes memory encodedLeaf = bytes.concat(
ScaleCodec.encodeU8(leaf.version),
ScaleCodec.encodeU32(leaf.parentNumber),
leaf.parentHash,
ScaleCodec.encodeU64(leaf.nextAuthoritySetID),
ScaleCodec.encodeU32(leaf.nextAuthoritySetLen),
leaf.nextAuthoritySetRoot,
parachainHeadsRoot
);
return keccak256(encodedLeaf);
}
}
"
},
"src/Initializer.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {AgentExecutor} from "./AgentExecutor.sol";
import {Agent} from "./Agent.sol";
import {OperatingMode, ParaID, TokenInfo, Channel, ChannelID} from "./Types.sol";
import {ERC1967} from "./utils/ERC1967.sol";
import {CoreStorage} from "./storage/CoreStorage.sol";
import {PricingStorage} from "./storage/PricingStorage.sol";
import {AssetsStorage} from "./storage/AssetsStorage.sol";
import {Constants} from "./Constants.sol";
import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol";
library Initializer {
error Unauthorized();
// Initial configuration for bridge
struct Config {
OperatingMode mode;
/// @dev The fee charged to users for submitting outbound messages (DOT)
uint128 deliveryCost;
/// @dev The ETH/DOT exchange rate
UD60x18 exchangeRate;
/// @dev The extra fee charged for registering tokens (DOT)
uint128 assetHubCreateAssetFee;
/// @dev The extra fee charged for sending tokens (DOT)
uint128 assetHubReserveTransferFee;
/// @dev extra fee to discourage spamming
uint256 registerTokenFee;
/// @dev Fee multiplier
UD60x18 multiplier;
uint8 foreignTokenDecimals;
uint128 maxDestinationFee;
}
function initialize(bytes calldata data) external {
// Prevent initialization of storage in implementation contract
if (ERC1967.load() == address(0)) {
revert Unauthorized();
}
CoreStorage.Layout storage core = CoreStorage.layout();
Config memory config = abi.decode(data, (Config));
core.mode = config.mode;
// Initialize agent for BridgeHub
address bridgeHubAgent = address(new Agent(Constants.BRIDGE_HUB_AGENT_ID));
core.agents[Constants.BRIDGE_HUB_AGENT_ID] = bridgeHubAgent;
// Initialize channel for primary governance track
core.channels[Constants.PRIMARY_GOVERNANCE_CHANNEL_ID] = Channel({
mode: OperatingMode.Normal,
agent: bridgeHubAgent,
inboundNonce: 0,
outboundNonce: 0
});
// Initialize channel for secondary governance track
core.channels[Constants.SECONDARY_GOVERNANCE_CHANNEL_ID] = Channel({
mode: OperatingMode.Normal,
agent: bridgeHubAgent,
inboundNonce: 0,
outboundNonce: 0
});
// Initialize agent for for AssetHub
address assetHubAgent = address(new Agent(Constants.ASSET_HUB_AGENT_ID));
core.agents[Constants.ASSET_HUB_AGENT_ID] = assetHubAgent;
// Initialize channel for AssetHub
core.channels[Constants.ASSET_HUB_PARA_ID.into()] = Channel({
mode: OperatingMode.Normal,
agent: assetHubAgent,
inboundNonce: 0,
outboundNonce: 0
});
// Initialize pricing storage
PricingStorage.Layout storage pricing = PricingStorage.layout();
pricing.exchangeRate = config.exchangeRate;
pricing.deliveryCost = config.deliveryCost;
pricing.multiplier = config.multiplier;
// Initialize assets storage
AssetsStorage.Layout storage assets = AssetsStorage.layout();
assets.assetHubParaID = Constants.ASSET_HUB_PARA_ID;
assets.assetHubAgent = assetHubAgent;
assets.registerTokenFee = config.registerTokenFee;
assets.assetHubCreateAssetFee = config.assetHubCreateAssetFee;
assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee;
assets.foreignTokenDecimals = config.foreignTokenDecimals;
assets.maxDestinationFee = config.maxDestinationFee;
TokenInfo storage etherTokenInfo = assets.tokenRegistry[address(0)];
etherTokenInfo.isRegistered = true;
}
}
"
},
"src/AgentExecutor.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {IERC20} from "./interfaces/IERC20.sol";
import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol";
import {Call} from "./utils/Call.sol";
/// @title Code which will run within an `Agent` using `delegatecall`.
/// @dev This is a singleton contract, meaning that all agents will execute the same code.
contract AgentExecutor {
using SafeTokenTransfer for IERC20;
using SafeNativeTransfer for address payable;
// Transfer ether to `recipient`.
function transferEther(address payable recipient, uint256 amount) external {
recipient.safeNativeTransfer(amount);
}
// Transfer ERC20 to `recipient`.
function transferToken(address token, address recipient, uint128 amount) external {
IERC20(token).safeTransfer(recipient, amount);
}
// Call contract with Ether value
function callContract(address target, bytes memory data, uint256 value) external {
bool success = Call.safeCall(target, data, value);
if (!success) {
revert();
}
}
function deposit() external payable {}
}
"
},
"src/interfaces/IGatewayBase.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {OperatingMode} from "../types/Common.sol";
/// Base interface for Gateway
interface IGatewayBase {
error InvalidToken();
error InvalidAmount();
error InvalidDestination();
error TokenNotRegistered();
error Unsupported();
error InvalidDestinationFee();
error AgentDoesNotExist();
error TokenAlreadyRegistered();
error TokenMintFailed();
error TokenTransferFailed();
error InvalidProof();
error InvalidNonce();
error NotEnoughGas();
error InsufficientEther();
error Unauthorized();
error Disabled();
error AgentExecutionFailed(bytes returndata);
error InvalidAgentExecutionPayload();
error InvalidConstructorParams();
error AlreadyInitialized();
// Emitted when the operating mode is changed
event OperatingModeChanged(OperatingMode mode);
// Emitted when foreign token from polkadot registered
event ForeignTokenRegistered(bytes32 indexed tokenID, address token);
/// @dev Emitted when a command is sent to register a new wrapped token on AssetHub
event TokenRegistrationSent(address token);
}
"
},
"src/Types.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {TokenInfo, OperatingMode} from "./types/Common.sol";
import {UD60x18} from "prb/math/src/UD60x18.sol";
import {
ParaID,
ChannelID,
Channel,
InboundMessage as InboundMessageV1,
Command as CommandV1,
MultiAddress
} from "./v1/Types.sol";
import {CallsV1} from "./v1/Calls.sol";
import {HandlersV1} from "./v1/Handlers.sol";
import {IGatewayV1} from "./v1/IGateway.sol";
import {
InboundMessage as InboundMessageV2, Command as CommandV2, CommandKind
} from "./v2/Types.sol";
import {CallsV2} from "./v2/Calls.sol";
import {HandlersV2} from "./v2/Handlers.sol";
import {IGatewayV2} from "./v2/IGateway.sol";
"
},
"src/v2/Types.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {OperatingMode} from "./../types/Common.sol";
// Inbound message from a Polkadot parachain (via BridgeHub)
struct InboundMessage {
// origin
bytes32 origin;
// Message nonce
uint64 nonce;
// XCM Topic
bytes32 topic;
// Commands
Command[] commands;
}
struct Command {
uint8 kind;
uint64 gas;
bytes payload;
}
// Command IDs
library CommandKind {
// Upgrade the Gateway implementation contract
uint8 constant Upgrade = 0;
// Set the operating mode for outbound messaging in the Gateway
uint8 constant SetOperatingMode = 1;
// Unlock native ERC20 and transfer to recipient
uint8 constant UnlockNativeToken = 2;
// Register a foreign token
uint8 constant RegisterForeignToken = 3;
// Mint foreign tokens
uint8 constant MintForeignToken = 4;
// Call an arbitrary solidity contract
uint8 constant CallContract = 5;
}
// Payload for outbound messages destined for Polkadot
struct Payload {
// Sender of the message
address origin;
// Asset transfer instructions
Asset[] assets;
// XCM Message
Xcm xcm;
// SCALE-encoded location of claimer who can claim trapped assets on AssetHub
bytes claimer;
// Ether value not reserved for fees
uint128 value;
// Ether value reserved for execution fees on AH
uint128 executionFee;
// Ether value reserved for relayer incentives
uint128 relayerFee;
}
struct Xcm {
// Variant ID
uint8 kind;
// ABI-encoded xcm variant
bytes data;
}
library XcmKind {
// SCALE-encoded raw bytes for `VersionedXcm`
uint8 constant Raw = 0;
// Create a new asset in the ForeignAssets pallet of AH
uint8 constant CreateAsset = 1;
}
// Format of ABI-encoded Xcm.data when Xcm.kind == XcmKind.CreateAsset
struct AsCreateAsset {
// Token address
address token;
// Polkadot network to create the asset in
uint8 network;
}
function makeRawXCM(bytes memory xcm) pure returns (Xcm memory) {
return Xcm({kind: XcmKind.Raw, data: xcm});
}
function makeCreateAssetXCM(address token, Network network) pure returns (Xcm memory) {
return Xcm({
kind: XcmKind.CreateAsset,
data: abi.encode(AsCreateAsset({token: token, network: uint8(network)}))
});
}
struct Asset {
uint8 kind;
bytes data;
}
library AssetKind {
uint8 constant NativeTokenERC20 = 0;
uint8 constant ForeignTokenERC20 = 1;
}
// Format of Asset.data when Asset.kind == AssetKind.NativeTokenERC20
struct AsNativeTokenERC20 {
address token;
uint128 amount;
}
// Format of Asset.data when Asset.kind == AssetKind.ForeignTokenERC20
struct AsForeignTokenERC20 {
bytes32 foreignID;
uint128 amount;
}
function makeNativeAsset(address token, uint128 amount) pure returns (Asset memory) {
return Asset({
kind: AssetKind.NativeTokenERC20,
data: abi.encode(AsNativeTokenERC20({token: token, amount: amount}))
});
}
function makeForeignAsset(bytes32 foreignID, uint128 amount) pure returns (Asset memory) {
return Asset({
kind: AssetKind.ForeignTokenERC20,
data: abi.encode(AsForeignTokenERC20({foreignID: foreignID, amount: amount}))
});
}
// V2 Command Params
// Payload for Upgrade
struct UpgradeParams {
// The address of the implementation contract
address impl;
// Codehash of the new implementation contract.
bytes32 implCodeHash;
// Parameters used to upgrade storage of the gateway
bytes initParams;
}
// Payload for SetOperatingMode instruction
struct SetOperatingModeParams {
/// The new operating mode
OperatingMode mode;
}
// Payload for NativeTokenUnlock instruction
struct UnlockNativeTokenParams {
// Token address
address token;
// Recipient address
address recipient;
// Amount to unlock
uint128 amount;
}
// Payload for RegisterForeignToken
struct RegisterForeignTokenParams {
/// @dev The token ID (hash of stable location id of token)
bytes32 foreignTokenID;
/// @dev The name of the token
string name;
/// @dev The symbol of the token
string symbol;
/// @dev The decimal of the token
uint8 decimals;
}
// Payload for MintForeignTokenParams instruction
struct MintForeignTokenParams {
// Foreign token ID
bytes32 foreignTokenID;
// Recipient address
address recipient;
// Amount to mint
uint128 amount;
}
// Payload for CallContractParams instruction
struct CallContractParams {
// target contract
address target;
// Call data
bytes data;
// Ether value
uint256 value;
}
enum Network {
Polkadot
}
"
},
"src/Upgrade.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {ERC1967} from "./utils/ERC1967.sol";
import {Call} from "./utils/Call.sol";
import {Address} from "./utils/Address.sol";
import {IInitializable} from "./interfaces/IInitializable.sol";
import {IUpgradable} from "./interfaces/IUpgradable.sol";
/// @dev Upgrades implementation contract
library Upgrade {
using Address for address;
function upgrade(address impl, bytes32 implCodeHash, bytes memory initializerParams)
external
{
// Verify that the implementation is actually a contract
if (!impl.isContract()) {
revert IUpgradable.InvalidContract();
}
// As a sanity check, ensure that the codehash of implementation contract
// matches the codehash in the upgrade proposal
if (impl.codehash != implCodeHash) {
revert IUpgradable.InvalidCodeHash();
}
// Update the proxy with the address of the new implementation
ERC1967.store(impl);
// Call the initializer
(bool success, bytes memory returndata) =
impl.delegatecall(abi.encodeCall(IInitializable.initialize, initializerParams));
Call.verifyResult(success, returndata);
emit IUpgradable.Upgraded(impl);
}
}
"
},
"src/interfaces/IInitializable.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
/**
* @title Initialization of gateway logic contracts
*/
interface IInitializable {
function initialize(bytes calldata data) external;
}
"
},
"src/interfaces/IUpgradable.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
interface IUpgradable {
// The new implementation address is a not a contract
error InvalidContract();
// The supplied codehash does not match the new implementation codehash
error InvalidCodeHash();
// The implementation contract was upgraded
event Upgraded(address indexed implementation);
function implementation() external view returns (address);
}
"
},
"src/utils/ERC1967.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
/// @title Minimal implementation of ERC1967 storage slot
library ERC1967 {
// bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
bytes32 public constant _IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
function load() internal view returns (address implementation) {
assembly {
implementation := sload(_IMPLEMENTATION_SLOT)
}
}
function store(address implementation) internal {
assembly {
sstore(_IMPLEMENTATION_SLOT, implementation)
}
}
}
"
},
"src/utils/Address.sol": {
"content": "// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023 Axelar Network
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
library Address {
// Checks whether `account` is a contract
function isContract(address account) internal view returns (bool) {
// https://eips.ethereum.org/EIPS/eip-1052
// keccak256('') == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
return account.codehash != bytes32(0)
&& account.codehash != 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
}
}
"
},
"src/utils/SafeTransfer.sol": {
"content": "// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023 Axelar Network
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pragma solidity 0.8.28;
import {IERC20} from "../interfaces/IERC20.sol";
error TokenTransferFailed();
error NativeTransferFailed();
library SafeTokenCall {
function safeCall(IERC20 token, bytes memory callData) internal {
(bool success, bytes memory returnData) = address(token).call(callData);
bool transferred =
success && (returnData.length == uint256(0) || abi.decode(returnData, (bool)));
if (!transferred || address(token).code.length == 0) {
revert TokenTransferFailed();
}
}
}
library SafeTokenTransfer {
function safeTransfer(IERC20 token, address receiver, uint256 amount) internal {
SafeTokenCall.safeCall(token, abi.encodeCall(IERC20.transfer, (receiver, amount)));
}
}
library SafeTokenTransferFrom {
function safeTransferFrom(IERC20 token, address from, address to, uint256 amount) internal {
SafeTokenCall.safeCall(token, abi.encodeCall(IERC20.transferFrom, (from, to, amount)));
}
}
library SafeNativeTransfer {
function safeNativeTransfer(address payable receiver, uint256 amount) internal {
bool success;
assembly {
success := call(gas(), receiver, amount, 0, 0, 0, 0)
}
if (!success) {
revert NativeTransferFailed();
}
}
}
"
},
"src/utils/Math.sol": {
"content": "// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023 OpenZeppelin
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
// Code from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol
pragma solidity 0.8.28;
/**
* @dev Standard math utilities missing in the Solidity language.
*/
library Math {
enum Rounding {
Floor, // Toward negative infinity
Ceil, // Toward positive infinity
Trunc, // Toward zero
Expand // Away from zero
}
/**
* @dev Returns the largest of two numbers.
*/
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
/**
* @dev Return the log in base 2 of a positive value rounded towards zero.
* Returns 0 if given 0.
*/
function log2(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 128;
}
if (value >> 64 > 0) {
value >>= 64;
result += 64;
}
if (value >> 32 > 0) {
value >>= 32;
result += 32;
}
if (value >> 16 > 0) {
value >>= 16;
result += 16;
}
if (value >> 8 > 0) {
value >>= 8;
result += 8;
}
if (value >> 4 > 0) {
value >>= 4;
result += 4;
}
if (value >> 2 > 0) {
value >>= 2;
result += 2;
}
if (value >> 1 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log2(value);
return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0);
}
}
/**
* @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.
*/
function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
return uint8(rounding) % 2 == 1;
}
/**
* @dev Safely adds two unsigned 16-bit integers, preventing overflow by saturating to max uint16.
*/
function saturatingAdd(uint16 a, uint16 b) internal pure returns (uint16) {
unchecked {
uint16 c =
Submitted on: 2025-09-24 17:02:19
Comments
Log in to comment.
No comments yet.