// Source: contracts/gateway/AxelarAmplifierGateway.sol pragma solidity ^0.8.0; // SPDX-License-Identifier: MIT // File contracts/interfaces/IAxelarGateway.sol /** * @title IAxelarGateway * @dev Interface for the Axelar Gateway that supports general message passing and contract call execution. */ interface IAxelarGateway { /** * @notice Emitted when a contract call is made through the gateway. * @dev Logs the attempt to call a contract on another chain. * @param sender The address of the sender who initiated the contract call. * @param destinationChain The name of the destination chain. * @param destinationContractAddress The address of the contract on the destination chain. * @param payloadHash The keccak256 hash of the sent payload data. * @param payload The payload data used for the contract call. */ event ContractCall( address indexed sender, string destinationChain, string destinationContractAddress, bytes32 indexed payloadHash, bytes payload ); /** * @notice Sends a contract call to another chain. * @dev Initiates a cross-chain contract call through the gateway to the specified destination chain and contract. * @param destinationChain The name of the destination chain. * @param contractAddress The address of the contract on the destination chain. * @param payload The payload data to be used in the contract call. */ function callContract( string calldata destinationChain, string calldata contractAddress, bytes calldata payload ) external; /** * @notice Checks if a contract call is approved. * @dev Determines whether a given contract call, identified by the commandId and payloadHash, is approved. * @param commandId The identifier of the command to check. * @param sourceChain The name of the source chain. * @param sourceAddress The address of the sender on the source chain. * @param contractAddress The address of the contract where the call will be executed. * @param payloadHash The keccak256 hash of the payload data. * @return True if the contract call is approved, false otherwise. */ function isContractCallApproved( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, address contractAddress, bytes32 payloadHash ) external view returns (bool); /** * @notice Validates and approves a contract call. * @dev Validates the given contract call information and marks it as approved if valid. * @param commandId The identifier of the command to validate. * @param sourceChain The name of the source chain. * @param sourceAddress The address of the sender on the source chain. * @param payloadHash The keccak256 hash of the payload data. * @return True if the contract call is validated and approved, false otherwise. */ function validateContractCall( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, bytes32 payloadHash ) external returns (bool); /** * @notice Checks if a command has been executed. * @dev Determines whether a command, identified by the commandId, has been executed. * @param commandId The identifier of the command to check. * @return True if the command has been executed, false otherwise. */ function isCommandExecuted(bytes32 commandId) external view returns (bool); } // File contracts/interfaces/IBaseAmplifierGateway.sol /** * @title IBaseAmplifierGateway * @dev Interface for the Base Axelar Amplifier Gateway that supports cross-chain messaging. */ interface IBaseAmplifierGateway is IAxelarGateway { /**********\ |* Errors *| \**********/ error InvalidMessages(); /** * @notice Emitted when a cross-chain message is approved. * @param commandId The identifier of the command to execute. * @param sourceChain The name of the source chain from whence the command came. * @param messageId The message id for the message. * @param sourceAddress The address of the sender on the source chain. * @param contractAddress The address of the contract where the call will be executed. * @param payloadHash The keccak256 hash of the approved payload data. */ event MessageApproved( bytes32 indexed commandId, string sourceChain, string messageId, string sourceAddress, address indexed contractAddress, bytes32 indexed payloadHash ); /** * @notice Emitted when a message has been executed. * @dev Logs the execution of an approved message. * `sourceChain` and `messageId` aren't included in the event due to backwards compatibility with `validateContractCall`. * @param commandId The commandId for the message that was executed. */ event MessageExecuted(bytes32 indexed commandId); /** * @notice Checks if a message is approved. * @dev Determines whether a given message, identified by the sourceChain and messageId, is approved. * @param sourceChain The name of the source chain. * @param messageId The unique identifier of the message. * @param sourceAddress The address of the sender on the source chain. * @param contractAddress The address of the contract where the call will be executed. * @param payloadHash The keccak256 hash of the payload data. * @return True if the contract call is approved, false otherwise. */ function isMessageApproved( string calldata sourceChain, string calldata messageId, string calldata sourceAddress, address contractAddress, bytes32 payloadHash ) external view returns (bool); /** * @notice Checks if a message is executed. * @dev Determines whether a given message, identified by the sourceChain and messageId is executed. * @param sourceChain The name of the source chain. * @param messageId The unique identifier of the message. * @return True if the message is executed, false otherwise. */ function isMessageExecuted(string calldata sourceChain, string calldata messageId) external view returns (bool); /** * @notice Validates if a message is approved. If message was in approved status, status is updated to executed to avoid replay. * @param sourceChain The name of the source chain. * @param messageId The unique identifier of the message. * @param sourceAddress The address of the sender on the source chain. * @param payloadHash The keccak256 hash of the payload data. * @return valid True if the message is approved, false otherwise. */ function validateMessage( string calldata sourceChain, string calldata messageId, string calldata sourceAddress, bytes32 payloadHash ) external returns (bool valid); /** * @notice Compute the commandId for a message. * @param sourceChain The name of the source chain as registered on Axelar. * @param messageId The unique message id for the message. * @return The commandId for the message. */ function messageToCommandId(string calldata sourceChain, string calldata messageId) external pure returns (bytes32); } // File contracts/types/AmplifierGatewayTypes.sol /** * @notice This enum represents the different types of commands that can be processed by the Axelar Amplifier Gateway */ enum CommandType { ApproveMessages, RotateSigners } /** * @notice This struct represents a message that is to be processed by the Amplifier Gateway * @param sourceChain The chain from which the message originated * @param messageId The unique identifier for the message * @param sourceAddress The address from which the message originated * @param contractAddress The address of the contract that the message is intended for * @param payloadHash The hash of the payload that is to be processed */ struct Message { string sourceChain; string messageId; string sourceAddress; address contractAddress; bytes32 payloadHash; } // File contracts/interfaces/IPausable.sol /** * @title Pausable * @notice This contract provides a mechanism to halt the execution of specific functions * if a pause condition is activated. */ interface IPausable { event Paused(address indexed account); event Unpaused(address indexed account); error Pause(); error NotPaused(); /** * @notice Check if the contract is paused * @return paused A boolean representing the pause status. True if paused, false otherwise. */ function paused() external view returns (bool); } // File contracts/utils/Pausable.sol /** * @title Pausable * @notice This contract provides a mechanism to halt the execution of specific functions * if a pause condition is activated. */ contract Pausable is IPausable { // uint256(keccak256('paused')) - 1 uint256 internal constant PAUSE_SLOT = 0xee35723ac350a69d2a92d3703f17439cbaadf2f093a21ba5bf5f1a53eb2a14d8; /** * @notice A modifier that throws a Paused custom error if the contract is paused * @dev This modifier should be used with functions that can be paused */ modifier whenNotPaused() { if (paused()) revert Pause(); _; } modifier whenPaused() { if (!paused()) revert NotPaused(); _; } /** * @notice Check if the contract is paused * @return paused_ A boolean representing the pause status. True if paused, false otherwise. */ function paused() public view returns (bool paused_) { assembly { paused_ := sload(PAUSE_SLOT) } } /** * @notice Pauses the contract * @dev This function should be callable by the owner/governance. */ function _pause() internal { _setPaused(true); emit Paused(msg.sender); } /** * @notice Unpauses the contract * @dev This function should be callable by the owner/governance. */ function _unpause() internal { _setPaused(false); emit Unpaused(msg.sender); } /** * @notice Sets the pause status of the contract * @dev This is an internal function, meaning it can only be called from within the contract itself * or from derived contracts. * @param paused_ The new pause status */ function _setPaused(bool paused_) internal { assembly { sstore(PAUSE_SLOT, paused_) } } } // File contracts/gateway/BaseAmplifierGateway.sol abstract contract BaseAmplifierGateway is Pausable, IBaseAmplifierGateway { /// @dev This slot contains the storage for this contract in an upgrade-compatible manner /// keccak256('BaseAmplifierGateway.Slot') - 1; bytes32 internal constant BASE_AMPLIFIER_GATEWAY_SLOT = 0x978b1ab9e384397ce0aab28eec0e3c25603b3210984045ad0e0f0a50d88cfc55; /// @dev While paused, only the address returned by `_pauseBypassSender` may consume messages. /// Default is address(0), meaning no bypass; derived contracts override to expose an owner / /// governance identity. modifier whenNotPausedExceptForBypass() { if (paused() && msg.sender != _pauseBypassSender()) revert Pause(); _; } /// @dev Returns the address allowed to consume messages while the gateway is paused. /// Returning address(0) means no caller can bypass pause. Override in derived contracts to /// designate a privileged identity (e.g. the upgrade owner / governance contract). function _pauseBypassSender() internal view virtual returns (address) { return address(0); } /// @dev Message can be in one of three states: non-existent, approved, or executed /// Non-existent: The message has not been seen before. Equal to 0 /// Approved: The message has been seen and approved. Equal to keccak256 of the message data /// Executed: The message has been seen and executed. Equal to 1 bytes32 internal constant MESSAGE_NONEXISTENT = 0; bytes32 internal constant MESSAGE_EXECUTED = bytes32(uint256(1)); /// @dev Storage for this contract /// @param messages Mapping of commandId to message status struct BaseAmplifierGatewayStorage { mapping(bytes32 => bytes32) messages; } /******************\ |* Public Methods *| \******************/ /** * @notice Sends a message to the specified destination chain and address with a given payload. * This function is the entry point for general message passing between chains. * @param destinationChain The chain where the destination contract exists. A registered chain name on Axelar must be used here * @param destinationContractAddress The address of the contract to call on the destination chain * @param payload The payload to be sent to the destination contract */ function callContract( string calldata destinationChain, string calldata destinationContractAddress, bytes calldata payload ) external whenNotPaused { emit ContractCall(msg.sender, destinationChain, destinationContractAddress, keccak256(payload), payload); } /** * @notice Checks if a message is approved. * @dev Determines whether a given message, identified by the sourceChain and messageId, is approved. * @param sourceChain The name of the source chain. * @param messageId The unique identifier of the message. * @param sourceAddress The address of the sender on the source chain. * @param contractAddress The address of the contract where the call will be executed. * @param payloadHash The keccak256 hash of the payload data. * @return True if the contract call is approved, false otherwise. */ function isMessageApproved( string calldata sourceChain, string calldata messageId, string calldata sourceAddress, address contractAddress, bytes32 payloadHash ) external view override returns (bool) { bytes32 commandId = messageToCommandId(sourceChain, messageId); return _isMessageApproved(commandId, sourceChain, sourceAddress, contractAddress, payloadHash); } /** * @notice Checks if a message is executed. * @dev Determines whether a given message, identified by the sourceChain and messageId is executed. * @param sourceChain The name of the source chain. * @param messageId The unique identifier of the message. * @return True if the message is executed, false otherwise. */ function isMessageExecuted(string calldata sourceChain, string calldata messageId) external view returns (bool) { return _baseAmplifierGatewayStorage().messages[messageToCommandId(sourceChain, messageId)] == MESSAGE_EXECUTED; } /** * @notice Validates if a message is approved. If message was in approved status, status is updated to executed to avoid replay. * @param sourceChain The name of the source chain. * @param messageId The unique identifier of the message. * @param sourceAddress The address of the sender on the source chain. * @param payloadHash The keccak256 hash of the payload data. * @return valid True if the message is approved, false otherwise. */ function validateMessage( string calldata sourceChain, string calldata messageId, string calldata sourceAddress, bytes32 payloadHash ) external override whenNotPausedExceptForBypass returns (bool valid) { bytes32 commandId = messageToCommandId(sourceChain, messageId); valid = _validateMessage(commandId, sourceChain, sourceAddress, payloadHash); } /** * @notice Compute the commandId for a message. * @param sourceChain The name of the source chain as registered on Axelar. * @param messageId The unique message id for the message. * @return The commandId for the message. */ function messageToCommandId(string calldata sourceChain, string calldata messageId) public pure returns (bytes32) { // Axelar doesn't allow `sourceChain` to contain '_', hence this encoding is umambiguous return keccak256(bytes(string.concat(sourceChain, '_', messageId))); } /*************************\ |* Legacy Public Methods *| \*************************/ /// @dev The below methods are available for backwards compatibility with the original AxelarExecutable /// Other implementations can skip these methods. function isCommandExecuted(bytes32 commandId) public view override returns (bool) { return _baseAmplifierGatewayStorage().messages[commandId] != MESSAGE_NONEXISTENT; } function isContractCallApproved( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, address contractAddress, bytes32 payloadHash ) external view override returns (bool) { return _isMessageApproved(commandId, sourceChain, sourceAddress, contractAddress, payloadHash); } function validateContractCall( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, bytes32 payloadHash ) external override whenNotPausedExceptForBypass returns (bool valid) { valid = _validateMessage(commandId, sourceChain, sourceAddress, payloadHash); } /*************************\ |* Integration Functions *| \*************************/ /** * @notice Approves an array of messages. * @param messages The array of messages to verify. */ function _approveMessages(Message[] calldata messages) internal { uint256 length = messages.length; if (length == 0) revert InvalidMessages(); for (uint256 i; i < length; ++i) { // Ignores message if it has already been approved before _approveMessage(messages[i]); } } /**********************\ |* Internal Functions *| \**********************/ function _isMessageApproved( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, address contractAddress, bytes32 payloadHash ) internal view returns (bool) { bytes32 messageHash = _messageHash(commandId, sourceChain, sourceAddress, contractAddress, payloadHash); return _baseAmplifierGatewayStorage().messages[commandId] == messageHash; } /** * @dev For backwards compatibility with `validateContractCall`, `commandId` is used here instead of `messageId`. * @return valid True if message is valid */ function _validateMessage( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, bytes32 payloadHash ) internal returns (bool valid) { bytes32 messageHash = _messageHash(commandId, sourceChain, sourceAddress, msg.sender, payloadHash); valid = _baseAmplifierGatewayStorage().messages[commandId] == messageHash; if (valid) { _baseAmplifierGatewayStorage().messages[commandId] = MESSAGE_EXECUTED; emit MessageExecuted(commandId); } } /** * @dev Approves a message if it hasn't been approved before. The message status is set to approved. */ function _approveMessage(Message calldata message) internal { // For other implementations, `sourceChain` and `messageId` tuple could be used as the mapping key directly. bytes32 commandId = messageToCommandId(message.sourceChain, message.messageId); // Ignore if message has already been approved/executed if (_baseAmplifierGatewayStorage().messages[commandId] != MESSAGE_NONEXISTENT) { return; } bytes32 messageHash = _messageHash( commandId, message.sourceChain, message.sourceAddress, message.contractAddress, message.payloadHash ); _baseAmplifierGatewayStorage().messages[commandId] = messageHash; emit MessageApproved( commandId, message.sourceChain, message.messageId, message.sourceAddress, message.contractAddress, message.payloadHash ); } /********************\ |* Pure Key Getters *| \********************/ /** * @dev For backwards compatibility with `validateContractCall`, `commandId` is used here instead of `messageId`. * @return bytes32 the message hash */ function _messageHash( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, address contractAddress, bytes32 payloadHash ) internal pure returns (bytes32) { return keccak256(abi.encode(commandId, sourceChain, sourceAddress, contractAddress, payloadHash)); } /** * @notice Gets the specific storage location for preventing upgrade collisions * @return slot containing the storage struct */ function _baseAmplifierGatewayStorage() private pure returns (BaseAmplifierGatewayStorage storage slot) { assembly { slot.slot := BASE_AMPLIFIER_GATEWAY_SLOT } } } // File contracts/interfaces/IBaseWeightedMultisig.sol interface IBaseWeightedMultisig { error InvalidSigners(); error InvalidThreshold(); error MalformedSignatures(); error LowSignaturesWeight(); error InvalidWeights(); error DuplicateSigners(bytes32 signersHash); error RedundantSignaturesProvided(uint256 required, uint256 provided); error InsufficientRotationDelay(uint256 minimumRotationDelay, uint256 lastRotationTimestamp, uint256 timeElapsed); event SignersRotated(uint256 indexed epoch, bytes32 indexed signersHash, bytes signers); /** * @dev This function returns the old signers retention period * @return uint256 The old signers retention period */ function previousSignersRetention() external view returns (uint256); /** * @dev This function returns the current signers epoch * @return uint256 The current signers epoch */ function epoch() external view returns (uint256); /** * @dev Returns the hash for a given signers epoch * @param signerEpoch The epoch to get the hash for * @return The hash for the given epoch */ function signersHashByEpoch(uint256 signerEpoch) external view returns (bytes32); /** * @dev Returns the epoch for a given hash * @param signersHash The hash to get the epoch for * @return The epoch for the given hash */ function epochBySignersHash(bytes32 signersHash) external view returns (uint256); /** * @notice This function returns the timestamp for the last signer rotation * @return uint256 The last rotation timestamp */ function lastRotationTimestamp() external view returns (uint256); /** * @notice This function returns the time elapsed (in secs) since the last rotation * @return uint256 The time since the last rotation */ function timeSinceRotation() external view returns (uint256); /** * @notice Compute the message hash that is signed by the weighted signers * @param signersHash The hash of the weighted signers that sign off on the data * @param dataHash The hash of the data * @return The message hash to be signed */ function messageHashToSign(bytes32 signersHash, bytes32 dataHash) external view returns (bytes32); } // File contracts/libs/ECDSA.sol /** * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. * * These functions can be used to verify that a message was signed by the holder * of the private keys of a given address. */ library ECDSA { error InvalidSignatureLength(); error InvalidS(); error InvalidV(); error InvalidSignature(); /** * @dev Returns the address that signed a hashed message (`hash`) with * `signature`. This address can then be used for verification purposes. * * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: * this function rejects them by requiring the `s` value to be in the lower * half order, and the `v` value to be either 27 or 28. * * IMPORTANT: `hash` _must_ be the result of a hash operation for the * verification to be secure: it is possible to craft signatures that * recover to arbitrary addresses for non-hashed data. A safe way to ensure * this is by receiving a hash of the original message (which may otherwise * be too long), and then calling {toEthSignedMessageHash} on it. */ function recover(bytes32 hash, bytes memory signature) internal pure returns (address signer) { // Check the signature length if (signature.length != 65) revert InvalidSignatureLength(); // Divide the signature in r, s and v variables bytes32 r; bytes32 s; uint8 v; // ecrecover takes the signature parameters, and the only way to get them // currently is to use assembly. // solhint-disable-next-line no-inline-assembly assembly { r := mload(add(signature, 0x20)) s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most // signatures from current libraries generate a unique signature with an s-value in the lower half order. // // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept // these malleable signatures as well. if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); if (v != 27 && v != 28) revert InvalidV(); signer = ecrecover(hash, v, r, s); // If the signature is valid (and not malleable), return the signer address if (signer == address(0)) revert InvalidSignature(); } } // File contracts/types/WeightedMultisigTypes.sol /** * @notice This struct represents the weighted signer * @param signer The address of the weighted signer * @param weight The weight of the weighted singer */ struct WeightedSigner { address signer; uint128 weight; } /** * @notice This struct represents the weighted signers payload * @param signers The list of weighted signers * @param threshold The threshold for the weighted signers * @param nonce The nonce to distinguish different weighted signer sets */ struct WeightedSigners { WeightedSigner[] signers; uint128 threshold; bytes32 nonce; } /** * @notice This struct represents a proof for a message from the weighted signers * @param signers The weighted signers * @param signatures The list of signatures */ struct Proof { WeightedSigners signers; bytes[] signatures; } // File contracts/governance/BaseWeightedMultisig.sol /** @title BaseWeightedMultisig Contract @notice Base contract to build a weighted multisig verification */ abstract contract BaseWeightedMultisig is IBaseWeightedMultisig { // keccak256('BaseWeightedMultisig.Slot') - 1; bytes32 internal constant BASE_WEIGHTED_MULTISIG_SLOT = 0x457f3fc26bf430b020fe76358b1bfaba57e1657ace718da6437cda9934eabfe8; struct BaseWeightedMultisigStorage { uint256 epoch; uint256 lastRotationTimestamp; mapping(uint256 => bytes32) signersHashByEpoch; mapping(bytes32 => uint256) epochBySignersHash; } /// @dev Previous signers retention. 0 means only the current signers are valid uint256 public immutable previousSignersRetention; /// @dev The domain separator for the signer proof bytes32 public immutable domainSeparator; /// @dev The minimum delay required between rotations uint256 public immutable minimumRotationDelay; /** * @dev Initializes the contract. * @dev Ownership of this contract should be transferred to the Gateway contract after deployment. * @param previousSignersRetention_ The number of previous signers to retain * @param domainSeparator_ The domain separator for the signer proof * @param minimumRotationDelay_ The minimum delay required between rotations */ constructor( uint256 previousSignersRetention_, bytes32 domainSeparator_, uint256 minimumRotationDelay_ ) { previousSignersRetention = previousSignersRetention_; domainSeparator = domainSeparator_; minimumRotationDelay = minimumRotationDelay_; } /**********************\ |* External Functions *| \**********************/ /** * @notice This function returns the current signers epoch * @return uint256 The current signers epoch */ function epoch() external view returns (uint256) { return _baseWeightedMultisigStorage().epoch; } /** * @notice This function returns the signers hash for a given epoch * @param signerEpoch The given epoch * @return bytes32 The signers hash for the given epoch */ function signersHashByEpoch(uint256 signerEpoch) external view returns (bytes32) { return _baseWeightedMultisigStorage().signersHashByEpoch[signerEpoch]; } /** * @notice This function returns the epoch for a given signers hash * @param signersHash The signers hash * @return uint256 The epoch for the given signers hash */ function epochBySignersHash(bytes32 signersHash) external view returns (uint256) { return _baseWeightedMultisigStorage().epochBySignersHash[signersHash]; } /** * @notice This function returns the timestamp for the last signer rotation * @return uint256 The last rotation timestamp */ function lastRotationTimestamp() external view returns (uint256) { return _baseWeightedMultisigStorage().lastRotationTimestamp; } /** * @notice This function returns the time elapsed (in secs) since the last rotation * @return uint256 The time since the last rotation */ function timeSinceRotation() external view returns (uint256) { return block.timestamp - _baseWeightedMultisigStorage().lastRotationTimestamp; } /*************************\ |* Integration Functions *| \*************************/ /** * @notice This function takes dataHash and proof data and reverts if proof is invalid * @param dataHash The hash of the message that was signed * @param proof The multisig proof data * @return isLatestSigners True if the proof is from the latest signer set * @dev The proof data should have signers, weights, threshold and signatures encoded * The proof is only valid if the signers weight crosses the threshold and there are no redundant signatures * The signers and signatures should be sorted by signer address in ascending order * Example: abi.encode([0x11..., 0x22..., 0x33...], [1, 1, 1], 2, [signature1, signature3]) */ function _validateProof(bytes32 dataHash, Proof calldata proof) internal view returns (bool isLatestSigners) { BaseWeightedMultisigStorage storage slot = _baseWeightedMultisigStorage(); WeightedSigners calldata signers = proof.signers; bytes32 signersHash = keccak256(abi.encode(signers)); uint256 signerEpoch = slot.epochBySignersHash[signersHash]; uint256 currentEpoch = slot.epoch; isLatestSigners = signerEpoch == currentEpoch; if (signerEpoch == 0 || currentEpoch - signerEpoch > previousSignersRetention) revert InvalidSigners(); bytes32 messageHash = messageHashToSign(signersHash, dataHash); _validateSignatures(messageHash, signers, proof.signatures); } /** * @notice This function rotates the current signers with a new set of signers * @dev Rotation to repeated signers is not allowed. * While the individual signer addresses and weights can be repeated, the nonce must be different. * @param newSigners The new weighted signers data * @param enforceRotationDelay If true, the minimum rotation delay will be enforced * @dev The signers should be sorted by signer address in ascending order */ function _rotateSigners(WeightedSigners memory newSigners, bool enforceRotationDelay) internal { BaseWeightedMultisigStorage storage slot = _baseWeightedMultisigStorage(); _validateSigners(newSigners); _updateRotationTimestamp(enforceRotationDelay); bytes memory newSignersData = abi.encode(newSigners); bytes32 newSignersHash = keccak256(newSignersData); // assign the next epoch to the new signers uint256 newEpoch = slot.epoch + 1; slot.epoch = newEpoch; slot.signersHashByEpoch[newEpoch] = newSignersHash; // signers must be distinct, since nonce should guarantee uniqueness even if signers are repeated if (slot.epochBySignersHash[newSignersHash] != 0) revert DuplicateSigners(newSignersHash); slot.epochBySignersHash[newSignersHash] = newEpoch; emit SignersRotated(newEpoch, newSignersHash, newSignersData); } /**********************\ |* Internal Functions *| \**********************/ /** * @dev Updates the last rotation timestamp, and enforces the minimum rotation delay if specified */ function _updateRotationTimestamp(bool enforceRotationDelay) internal { uint256 lastRotationTimestamp_ = _baseWeightedMultisigStorage().lastRotationTimestamp; uint256 currentTimestamp = block.timestamp; if (enforceRotationDelay && (currentTimestamp - lastRotationTimestamp_) < minimumRotationDelay) { revert InsufficientRotationDelay( minimumRotationDelay, lastRotationTimestamp_, currentTimestamp - lastRotationTimestamp_ ); } _baseWeightedMultisigStorage().lastRotationTimestamp = currentTimestamp; } /** * @notice This function takes messageHash and proof data and reverts if proof is invalid * @param messageHash The hash of the message that was signed * @param weightedSigners The weighted signers data * @param signatures The sorted signatures data * @dev The signers and signatures should be sorted by signer address in ascending order */ function _validateSignatures( bytes32 messageHash, WeightedSigners calldata weightedSigners, bytes[] calldata signatures ) internal pure { WeightedSigner[] calldata signers = weightedSigners.signers; uint256 signersLength = signers.length; uint256 signaturesLength = signatures.length; uint256 signerIndex; uint256 totalWeight; // looking for signers within signers // this requires both signers and signatures to be sorted // having it sorted allows us to avoid the full inner loop to find a match for (uint256 i; i < signaturesLength; ++i) { address recoveredSigner = ECDSA.recover(messageHash, signatures[i]); // looping through remaining signers to find a match for (; signerIndex < signersLength && recoveredSigner != signers[signerIndex].signer; ++signerIndex) {} // checking if we are out of signers if (signerIndex == signersLength) revert MalformedSignatures(); // accumulating signatures weight totalWeight = totalWeight + signers[signerIndex].weight; // weight needs to reach threshold if (totalWeight >= weightedSigners.threshold) { // validate the proof if there are no redundant signatures if (i + 1 == signaturesLength) return; revert RedundantSignaturesProvided(i + 1, signaturesLength); } // increasing signers index if match was found ++signerIndex; } // if weight sum below threshold revert LowSignaturesWeight(); } /** * @notice Compute the message hash that is signed by the weighted signers * @dev Returns an Ethereum Signed Message, created from `domainSeparator`, `signersHash`, and `dataHash`. * This replicates the behavior of the * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] * JSON-RPC method. * * See {recover}. * * @param signersHash The hash of the weighted signers that sign off on the data * @param dataHash The hash of the data * @return The message hash to be signed */ function messageHashToSign(bytes32 signersHash, bytes32 dataHash) public view returns (bytes32) { // 96 is the length of the trailing bytes return keccak256(bytes.concat('\x19Ethereum Signed Message:\n96', domainSeparator, signersHash, dataHash)); } /** * @notice This function checks if the provided signers are valid, i.e sorted and contain no duplicates, with valid weights and threshold * @dev If signers are invalid, the method will revert * @param weightedSigners The weighted signers */ function _validateSigners(WeightedSigners memory weightedSigners) internal pure { WeightedSigner[] memory signers = weightedSigners.signers; uint256 length = signers.length; uint256 totalWeight; if (length == 0) revert InvalidSigners(); // since signers need to be in strictly increasing order, // this prevents address(0) from being a valid signer address prevSigner = address(0); for (uint256 i = 0; i < length; ++i) { WeightedSigner memory weightedSigner = signers[i]; address currSigner = weightedSigner.signer; if (prevSigner >= currSigner) { revert InvalidSigners(); } prevSigner = currSigner; uint256 weight = weightedSigner.weight; if (weight == 0) revert InvalidWeights(); totalWeight = totalWeight + weight; } uint128 threshold = weightedSigners.threshold; if (threshold == 0 || totalWeight < threshold) revert InvalidThreshold(); } /** * @notice Gets the specific storage location for preventing upgrade collisions * @return slot containing the BaseWeightedMultisigStorage struct */ function _baseWeightedMultisigStorage() internal pure returns (BaseWeightedMultisigStorage storage slot) { assembly { slot.slot := BASE_WEIGHTED_MULTISIG_SLOT } } } // File contracts/interfaces/IContractIdentifier.sol // General interface for upgradable contracts interface IContractIdentifier { /** * @notice Returns the contract ID. It can be used as a check during upgrades. * @dev Meant to be overridden in derived contracts. * @return bytes32 The contract ID */ function contractId() external pure returns (bytes32); } // File contracts/interfaces/IImplementation.sol interface IImplementation is IContractIdentifier { error NotProxy(); function setup(bytes calldata data) external; } // File contracts/interfaces/IOwnable.sol /** * @title IOwnable Interface * @notice IOwnable is an interface that abstracts the implementation of a * contract with ownership control features. It's commonly used in upgradable * contracts and includes the functionality to get current owner, transfer * ownership, and propose and accept ownership. */ interface IOwnable { error NotOwner(); error InvalidOwner(); error InvalidOwnerAddress(); event OwnershipTransferStarted(address indexed newOwner); event OwnershipTransferred(address indexed newOwner); /** * @notice Returns the current owner of the contract. * @return address The address of the current owner */ function owner() external view returns (address); /** * @notice Returns the address of the pending owner of the contract. * @return address The address of the pending owner */ function pendingOwner() external view returns (address); /** * @notice Transfers ownership of the contract to a new address * @param newOwner The address to transfer ownership to */ function transferOwnership(address newOwner) external; /** * @notice Proposes to transfer the contract's ownership to a new address. * The new owner needs to accept the ownership explicitly. * @param newOwner The address to transfer ownership to */ function proposeOwnership(address newOwner) external; /** * @notice Transfers ownership to the pending owner. * @dev Can only be called by the pending owner */ function acceptOwnership() external; } // File contracts/interfaces/IUpgradable.sol // General interface for upgradable contracts interface IUpgradable is IOwnable, IImplementation { error InvalidCodeHash(); error InvalidImplementation(); error SetupFailed(); event Upgraded(address indexed newImplementation); function implementation() external view returns (address); function upgrade( address newImplementation, bytes32 newImplementationCodeHash, bytes calldata params ) external; } // File contracts/interfaces/IAxelarAmplifierGateway.sol /** * @title IAxelarAmplifierGateway * @dev Interface for the Axelar Gateway that supports general message passing. */ interface IAxelarAmplifierGateway is IPausable, IBaseAmplifierGateway, IBaseWeightedMultisig, IUpgradable { error NotLatestSigners(); error InvalidSender(address sender); event OperatorshipTransferred(address newOperator); /** * @notice Approves an array of messages, signed by the Axelar signers. * @param messages The array of messages to verify. * @param proof The proof signed by the Axelar signers for this command. */ function approveMessages(Message[] calldata messages, Proof calldata proof) external; /** * @notice Update the signer data for the auth module, signed by the Axelar signers. * @param newSigners The data for the new signers. * @param proof The proof signed by the Axelar signers for this command. */ function rotateSigners(WeightedSigners memory newSigners, Proof calldata proof) external; /** * @notice This function takes dataHash and proof and reverts if proof is invalid * @param dataHash The hash of the data being signed * @param proof The proof from Axelar signers * @return isLatestSigners True if provided signers are the current ones */ function validateProof(bytes32 dataHash, Proof calldata proof) external view returns (bool isLatestSigners); /** * @notice Returns the address of the gateway operator. * @return The address of the operator. */ function operator() external view returns (address); /** * @notice Transfer the operatorship to a new address. * @param newOperator The address of the new operator. */ function transferOperatorship(address newOperator) external; /** * @notice Pauses or unpauses the gateway. Callable by the operator (emergency EOA) * or the owner. Owner-controlled functions remain callable while paused so governance * proposals can always go through. * @param isPaused True to pause, false to unpause. */ function setPauseStatus(bool isPaused) external; } // File contracts/upgradable/Implementation.sol /** * @title Implementation * @notice This contract serves as a base for other contracts and enforces a proxy-first access restriction. * @dev Derived contracts must implement the setup function. */ abstract contract Implementation is IImplementation { address private immutable implementationAddress; /** * @dev Contract constructor that sets the implementation address to the address of this contract. */ constructor() { implementationAddress = address(this); } /** * @dev Modifier to require the caller to be the proxy contract. * Reverts if the caller is the current contract (i.e., the implementation contract itself). */ modifier onlyProxy() { if (implementationAddress == address(this)) revert NotProxy(); _; } /** * @notice Initializes contract parameters. * This function is intended to be overridden by derived contracts. * The overriding function must have the onlyProxy modifier. * @param params The parameters to be used for initialization */ function setup(bytes calldata params) external virtual; } // File contracts/utils/Ownable.sol /** * @title Ownable * @notice A contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * The owner account is set through ownership transfer. This module makes * it possible to transfer the ownership of the contract to a new account in one * step, as well as to an interim pending owner. In the second flow the ownership does not * change until the pending owner accepts the ownership transfer. */ abstract contract Ownable is IOwnable { // keccak256('owner') bytes32 internal constant _OWNER_SLOT = 0x02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c0; // keccak256('ownership-transfer') bytes32 internal constant _OWNERSHIP_TRANSFER_SLOT = 0x9855384122b55936fbfb8ca5120e63c6537a1ac40caf6ae33502b3c5da8c87d1; /** * @notice Initializes the contract by transferring ownership to the owner parameter. * @param _owner Address to set as the initial owner of the contract */ constructor(address _owner) { _transferOwnership(_owner); } /** * @notice Modifier that throws an error if called by any account other than the owner. */ modifier onlyOwner() { if (owner() != msg.sender) revert NotOwner(); _; } /** * @notice Returns the current owner of the contract. * @return owner_ The current owner of the contract */ function owner() public view returns (address owner_) { assembly { owner_ := sload(_OWNER_SLOT) } } /** * @notice Returns the pending owner of the contract. * @return owner_ The pending owner of the contract */ function pendingOwner() public view returns (address owner_) { assembly { owner_ := sload(_OWNERSHIP_TRANSFER_SLOT) } } /** * @notice Transfers ownership of the contract to a new account `newOwner`. * @dev Can only be called by the current owner. * @param newOwner The address to transfer ownership to */ function transferOwnership(address newOwner) external virtual onlyOwner { _transferOwnership(newOwner); } /** * @notice Propose to transfer ownership of the contract to a new account `newOwner`. * @dev Can only be called by the current owner. The ownership does not change * until the new owner accepts the ownership transfer. * @param newOwner The address to transfer ownership to */ function proposeOwnership(address newOwner) external virtual onlyOwner { if (newOwner == address(0)) revert InvalidOwnerAddress(); emit OwnershipTransferStarted(newOwner); assembly { sstore(_OWNERSHIP_TRANSFER_SLOT, newOwner) } } /** * @notice Accepts ownership of the contract. * @dev Can only be called by the pending owner */ function acceptOwnership() external virtual { address newOwner = pendingOwner(); if (newOwner != msg.sender) revert InvalidOwner(); _transferOwnership(newOwner); } /** * @notice Internal function to transfer ownership of the contract to a new account `newOwner`. * @dev Called in the constructor to set the initial owner. * @param newOwner The address to transfer ownership to */ function _transferOwnership(address newOwner) internal virtual { if (newOwner == address(0)) revert InvalidOwnerAddress(); emit OwnershipTransferred(newOwner); assembly { sstore(_OWNER_SLOT, newOwner) sstore(_OWNERSHIP_TRANSFER_SLOT, 0) } } } // File contracts/upgradable/Upgradable.sol /** * @title Upgradable Contract * @notice This contract provides an interface for upgradable smart contracts and includes the functionality to perform upgrades. */ abstract contract Upgradable is Ownable, Implementation, IUpgradable { // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; /** * @notice Constructor sets the implementation address to the address of the contract itself * @dev This is used in the onlyProxy modifier to prevent certain functions from being called directly * on the implementation contract itself. * @dev The owner is initially set as address(1) because the actual owner is set within the proxy. It is not * set as the zero address because Ownable is designed to throw an error for ownership transfers to the zero address. */ constructor() Ownable(address(1)) {} /** * @notice Returns the address of the current implementation * @return implementation_ Address of the current implementation */ function implementation() public view returns (address implementation_) { assembly { implementation_ := sload(_IMPLEMENTATION_SLOT) } } /** * @notice Upgrades the contract to a new implementation * @param newImplementation The address of the new implementation contract * @param newImplementationCodeHash The codehash of the new implementation contract * @param params Optional setup parameters for the new implementation contract * @dev This function is only callable by the owner. */ function upgrade( address newImplementation, bytes32 newImplementationCodeHash, bytes calldata params ) external override onlyOwner { if (IUpgradable(newImplementation).contractId() != IUpgradable(implementation()).contractId()) revert InvalidImplementation(); if (newImplementationCodeHash != newImplementation.codehash) revert InvalidCodeHash(); assembly { sstore(_IMPLEMENTATION_SLOT, newImplementation) } emit Upgraded(newImplementation); if (params.length > 0) { // slither-disable-next-line controlled-delegatecall (bool success, ) = newImplementation.delegatecall(abi.encodeWithSelector(this.setup.selector, params)); if (!success) revert SetupFailed(); } } /** * @notice Sets up the contract with initial data * @param data Initialization data for the contract * @dev This function is only callable by the proxy contract. */ function setup(bytes calldata data) external override(IImplementation, Implementation) onlyProxy { _setup(data); } /** * @notice Internal function to set up the contract with initial data * @param data Initialization data for the contract * @dev This function should be implemented in derived contracts. */ function _setup(bytes calldata data) internal virtual; } // File contracts/gateway/AxelarAmplifierGateway.sol /** * @title AxelarAmplifierGateway * @notice AxelarAmplifierGateway is the contract that allows apps on EVM chains * to send and receive cross-chain messages via the Axelar Amplifier protocol. * It handles cross-chain message passing (implemented by BaseAmplifierGateway), * and signer rotation (implemented by BaseWeightedMultisig). */ contract AxelarAmplifierGateway is BaseAmplifierGateway, BaseWeightedMultisig, Upgradable, IAxelarAmplifierGateway { /// @dev This slot contains the storage for this contract in an upgrade-compatible manner /// keccak256('AxelarAmplifierGateway.Slot') - 1; bytes32 internal constant AXELAR_AMPLIFIER_GATEWAY_SLOT = 0xca458dc12368669a3b8c292bc21c1b887ab1aa386fa3fcc1ed972afd74a330ca; struct AxelarAmplifierGatewayStorage { address operator; } /** * @dev Initializes the contract. * @param previousSignersRetention_ The number of previous signers to retain * @param domainSeparator_ The domain separator for the signer proof * @param minimumRotationDelay_ The minimum delay required between rotations */ constructor( uint256 previousSignersRetention_, bytes32 domainSeparator_, uint256 minimumRotationDelay_ ) BaseWeightedMultisig(previousSignersRetention_, domainSeparator_, minimumRotationDelay_) {} modifier onlyOperatorOrOwner() { address sender = msg.sender; if (sender != _axelarAmplifierGatewayStorage().operator && sender != owner()) revert InvalidSender(sender); _; } /// @dev Owner (EOA today, governance contract once `transferOwnership` is called) bypasses /// `whenNotPausedExceptForBypass` so it can keep consuming messages while the gateway is paused. function _pauseBypassSender() internal view override returns (address) { return owner(); } /*****************\ |* Upgradability *| \*****************/ /** * @notice Internal function to set up the contract with initial data. This function is also called during upgrades. * @dev The setup data consists of an optional new operator, and a list of signers to rotate too. * @param data Initialization data for the contract * @dev This function should be implemented in derived contracts. */ function _setup(bytes calldata data) internal override { (address operator_, WeightedSigners[] memory signers) = abi.decode(data, (address, WeightedSigners[])); // operator is an optional parameter. The gateway owner can set it later via `transferOperatorship` if needed. // This also simplifies setup for upgrades so if the current operator doesn't need to be changed, then it can be skipped, instead of having to specify the current operator again. if (operator_ != address(0)) { _transferOperatorship(operator_); } for (uint256 i = 0; i < signers.length; i++) { _rotateSigners(signers[i], false); } } /**********************\ |* External Functions *| \**********************/ function contractId() external pure returns (bytes32) { return keccak256('axelar-amplifier-gateway'); } /** * @notice Approves an array of messages, signed by the Axelar signers. * @param messages The array of messages to verify. * @param proof The proof signed by the Axelar signers for this command. */ function approveMessages(Message[] calldata messages, Proof calldata proof) external { bytes32 dataHash = keccak256(abi.encode(CommandType.ApproveMessages, messages)); _validateProof(dataHash, proof); _approveMessages(messages); } /** * @notice Rotate the weighted signers, signed off by the latest Axelar signers. * @dev The minimum rotation delay is enforced by default, unless the caller is the gateway operator. * The gateway operator allows recovery in case of an incorrect/malicious rotation, while still requiring a valid proof from a recent signer set. * Rotation to duplicate signers is rejected. * @param newSigners The data for the new signers. * @param proof The proof signed by the Axelar verifiers for this command. */ function rotateSigners(WeightedSigners memory newSigners, Proof calldata proof) external { bytes32 dataHash = keccak256(abi.encode(CommandType.RotateSigners, newSigners)); bool enforceRotationDelay = msg.sender != _axelarAmplifierGatewayStorage().operator; bool isLatestSigners = _validateProof(dataHash, proof); if (enforceRotationDelay && !isLatestSigners) { revert NotLatestSigners(); } // If newSigners is a repeat signer set, this will revert _rotateSigners(newSigners, enforceRotationDelay); } /** * @notice Pauses or unpauses the gateway. Callable by the operator (emergency EOA) * or the owner (governance contract or EOA). * @dev Pause freezes `callContract` and gates `validateMessage` / `validateContractCall` * to the owner only; `approveMessages` and `rotateSigners` stay open. Admin functions * remain callable so governance can always recover. * @param isPaused True to pause, false to unpause. */ function setPauseStatus(bool isPaused) external onlyOperatorOrOwner { if (isPaused) { _pause(); } else { _unpause(); } } /** * @notice This function takes dataHash and proof and reverts if proof is invalid * @param dataHash The hash of the data being signed * @param proof The proof from Axelar signers * @return isLatestSigners True if provided signers are the current ones */ function validateProof(bytes32 dataHash, Proof calldata proof) external view returns (bool isLatestSigners) { isLatestSigners = _validateProof(dataHash, proof); } /** * @notice Returns the address of the gateway operator. * @return The address of the operator. */ function operator() external view returns (address) { return _axelarAmplifierGatewayStorage().operator; } /** * @notice Transfer the operatorship to a new address. * @dev The owner or current operator can set the operator to address 0. * @param newOperator The address of the new operator. */ function transferOperatorship(address newOperator) external onlyOperatorOrOwner { _transferOperatorship(newOperator); } /**********************\ |* Internal Functions *| \**********************/ function _transferOperatorship(address newOperator) internal { _axelarAmplifierGatewayStorage().operator = newOperator; emit OperatorshipTransferred(newOperator); } /** * @notice Gets the specific storage location for preventing upgrade collisions * @return slot containing the storage struct */ function _axelarAmplifierGatewayStorage() private pure returns (AxelarAmplifierGatewayStorage storage slot) { assembly { slot.slot := AXELAR_AMPLIFIER_GATEWAY_SLOT } } }