Skip to content

Commit

Permalink
feat(amplifier)!: amplifier gateway v2 (#154)
Browse files Browse the repository at this point in the history
* feat(auth)!: amplifier compatible auth contract

* fix tests

* rename types file

* rename storage function

* rename hashMessage

* switch to auth contract

* cache variable

* optimize validation

* add gas profiling test

* refactor

* fix interchain multisig tests

* fix amplifier gateway tests

* owner test

* lint

* rename auth contract and refactor auth interface

* fix tests

* rename multisig test

* add simplified multisig test

* codecov issue

* revert

* interchain multisig coverage

* feat(amplifier)!: amplifier gateway v2

* comment out amplifier gateway test

* prettier

* slither

* remove test imports

* fix command id derivation

* fix tests

* more test improvements

* fix signer indexing and add more tests

* _validateProof now accepts Proof type

* pin slither version

* fix arg

* pin node version

* add randomized signer rotation test

* prettier

* remove outer named tuple

* check command execution

* add remaining test coverage

* reorder command execution for consistency

* more reordering
  • Loading branch information
milapsheth authored Apr 10, 2024
1 parent aa421a9 commit 4b4ca7d
Show file tree
Hide file tree
Showing 7 changed files with 763 additions and 220 deletions.
235 changes: 148 additions & 87 deletions contracts/gateway/AxelarAmplifierGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ pragma solidity ^0.8.0;

import { IAxelarAmplifierGateway } from '../interfaces/IAxelarAmplifierGateway.sol';
import { IAxelarAmplifierGatewayAuth } from '../interfaces/IAxelarAmplifierGatewayAuth.sol';
import { CommandType, Message } from '../types/AmplifierGatewayTypes.sol';

contract AxelarAmplifierGateway is IAxelarAmplifierGateway {
/// @dev This slot contains all the storage for this contract in an upgrade-compatible manner
// keccak256('AxelarAmplifierGateway.Slot') - 1;
bytes32 internal constant AXELAR_AMPLIFIER_GATEWAY_SLOT =
0xca458dc12368669a3b8c292bc21c1b887ab1aa386fa3fcc1ed972afd74a330ca;
Expand All @@ -15,9 +17,6 @@ contract AxelarAmplifierGateway is IAxelarAmplifierGateway {
mapping(bytes32 => bool) approvals;
}

bytes32 internal constant SELECTOR_APPROVE_CONTRACT_CALL = keccak256('approveContractCall');
bytes32 internal constant SELECTOR_TRANSFER_OPERATORSHIP = keccak256('transferOperatorship');

IAxelarAmplifierGatewayAuth public immutable authModule;

constructor(address authModule_) {
Expand Down Expand Up @@ -45,14 +44,18 @@ contract AxelarAmplifierGateway is IAxelarAmplifierGateway {
address contractAddress,
bytes32 payloadHash
) external view override returns (bool) {
bytes32 key = _getIsContractCallApprovedKey(
commandId,
sourceChain,
sourceAddress,
contractAddress,
payloadHash
);
return _axelarAmplifierGatewayStorage().approvals[key];
return _isContractCallApproved(commandId, sourceChain, sourceAddress, contractAddress, payloadHash);
}

function isMessageApproved(
string calldata messageId,
string calldata sourceChain,
string calldata sourceAddress,
address contractAddress,
bytes32 payloadHash
) external view override returns (bool) {
bytes32 commandId = messageToCommandId(sourceChain, messageId);
return _isContractCallApproved(commandId, sourceChain, sourceAddress, contractAddress, payloadHash);
}

function validateContractCall(
Expand All @@ -61,119 +64,177 @@ contract AxelarAmplifierGateway is IAxelarAmplifierGateway {
string calldata sourceAddress,
bytes32 payloadHash
) external override returns (bool valid) {
bytes32 key = _getIsContractCallApprovedKey(commandId, sourceChain, sourceAddress, msg.sender, payloadHash);
valid = _axelarAmplifierGatewayStorage().approvals[key];

if (valid) {
delete _axelarAmplifierGatewayStorage().approvals[key];

emit ContractCallExecuted(commandId);
}
valid = _validateContractCall(commandId, sourceChain, sourceAddress, payloadHash);
}

/***********\
|* Getters *|
\***********/
function validateMessage(
string calldata messageId,
string calldata sourceChain,
string calldata sourceAddress,
bytes32 payloadHash
) external override returns (bool valid) {
bytes32 commandId = messageToCommandId(sourceChain, messageId);
valid = _validateContractCall(commandId, sourceChain, sourceAddress, payloadHash);
}

function isCommandExecuted(bytes32 commandId) public view override returns (bool) {
return _axelarAmplifierGatewayStorage().commands[commandId];
return _storage().commands[commandId];
}

/**
* @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 prevents `sourceChain` to contain '_',
// hence we can use it as a separator with abi.encodePacked to avoid ambiguous encodings
return keccak256(abi.encodePacked(CommandType.ApproveMessages, sourceChain, '_', messageId));
}

/**********************\
|* External Functions *|
\**********************/

function execute(bytes calldata batch) external {
(bytes memory data, bytes memory proof) = abi.decode(batch, (bytes, bytes));

bytes32 messageHash = keccak256(data);

// returns true for current operators
bool allowOperatorshipTransfer = authModule.validateProof(messageHash, proof);

uint256 chainId;
bytes32[] memory commandIds;
string[] memory commands;
bytes[] memory params;

(chainId, commandIds, commands, params) = abi.decode(data, (uint256, bytes32[], string[], bytes[]));

if (chainId != block.chainid) revert InvalidChainId();
/**
* @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, bytes calldata proof) external {
bytes32 dataHash = _computeDataHash(CommandType.ApproveMessages, abi.encode(messages));

uint256 commandsLength = commandIds.length;
_verifyProof(dataHash, proof);

if (commandsLength != commands.length || commandsLength != params.length) revert InvalidCommands();
uint256 length = messages.length;
if (length == 0) revert InvalidMessages();

for (uint256 i; i < commandsLength; ++i) {
bytes32 commandId = commandIds[i];
for (uint256 i; i < length; ++i) {
Message calldata message = messages[i];
bytes32 commandId = messageToCommandId(message.sourceChain, message.messageId);

// Ignore if commandId is already executed
// Ignore if message has already been approved
if (isCommandExecuted(commandId)) {
continue;
}

bytes32 commandHash = keccak256(abi.encodePacked(commands[i]));

if (commandHash == SELECTOR_APPROVE_CONTRACT_CALL) {
_approveContractCall(params[i], commandId);
} else if (commandHash == SELECTOR_TRANSFER_OPERATORSHIP) {
if (!allowOperatorshipTransfer) {
continue;
}
_commandExecuted(commandId);

allowOperatorshipTransfer = false;
_approveMessage(commandId, message);
}
}

_transferOperatorship(params[i]);
} else {
revert InvalidCommand(commandHash);
}
/**
* @notice Update the signer data for the auth module.
* @param newSignersData The data for the new signers.
* @param proof The proof signed by the Axelar verifiers for this command.
*/
function rotateSigners(bytes calldata newSignersData, bytes calldata proof) external {
bytes32 dataHash = _computeDataHash(CommandType.RotateSigners, newSignersData);
bytes32 commandId = dataHash;

_axelarAmplifierGatewayStorage().commands[commandId] = true;
if (isCommandExecuted(commandId)) {
revert CommandAlreadyExecuted(commandId);
}

// slither-disable-next-line reentrancy-events
emit Executed(commandId);
bool isLatestSigners = _verifyProof(dataHash, proof);
if (!isLatestSigners) {
revert NotLatestSigners();
}

_commandExecuted(commandId);

authModule.rotateSigners(newSignersData);

// slither-disable-next-line reentrancy-events
emit SignersRotated(newSignersData);
}

/**********************\
|* Internal Functions *|
\**********************/

function _approveContractCall(bytes memory params, bytes32 commandId) internal {
(
string memory sourceChain,
string memory sourceAddress,
address contractAddress,
bytes32 payloadHash,
bytes32 sourceTxHash,
uint256 sourceEventIndex
) = abi.decode(params, (string, string, address, bytes32, bytes32, uint256));
/**
* @dev This function computes the data hash that is used for signing
* @param commandType The type of command
* @param data The data that is being signed
* @return The hash of the data
*/
function _computeDataHash(CommandType commandType, bytes memory data) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(commandType, data));
}

/**
* @dev This function is used to mark a command as executed
*/
function _commandExecuted(bytes32 commandId) internal {
_storage().commands[commandId] = true;

emit Executed(commandId);
}

/**
* @dev This function verifies the proof data for a given data hash
* @param dataHash The hash of the data that was signed
* @param proof The data containing signers with signatures
* @return isLatestSigners True if the proof is signed by the latest signers
*/
function _verifyProof(bytes32 dataHash, bytes calldata proof) internal view returns (bool isLatestSigners) {
return authModule.validateProof(dataHash, proof);
}

function _isContractCallApproved(
bytes32 commandId,
string calldata sourceChain,
string calldata sourceAddress,
address contractAddress,
bytes32 payloadHash
) internal view returns (bool) {
bytes32 key = _getIsContractCallApprovedKey(
commandId,
sourceChain,
sourceAddress,
contractAddress,
payloadHash
);
_axelarAmplifierGatewayStorage().approvals[key] = true;
return _storage().approvals[key];
}

emit ContractCallApproved(
commandId,
sourceChain,
sourceAddress,
contractAddress,
payloadHash,
sourceTxHash,
sourceEventIndex
);
function _validateContractCall(
bytes32 commandId,
string calldata sourceChain,
string calldata sourceAddress,
bytes32 payloadHash
) internal returns (bool valid) {
bytes32 key = _getIsContractCallApprovedKey(commandId, sourceChain, sourceAddress, msg.sender, payloadHash);
valid = _storage().approvals[key];

if (valid) {
delete _storage().approvals[key];

emit ContractCallExecuted(commandId);
}
}

function _transferOperatorship(bytes memory newOperatorsData) internal {
authModule.rotateSigners(newOperatorsData);
function _approveMessage(bytes32 commandId, Message calldata message) internal {
bytes32 key = _getIsContractCallApprovedKey(
commandId,
message.sourceChain,
message.sourceAddress,
message.contractAddress,
message.payloadHash
);
_storage().approvals[key] = true;

// slither-disable-next-line reentrancy-events
emit OperatorshipTransferred(newOperatorsData);
emit ContractCallApproved(
commandId,
message.messageId,
message.sourceChain,
message.sourceAddress,
message.contractAddress,
message.payloadHash
);
}

/********************\
Expand All @@ -182,8 +243,8 @@ contract AxelarAmplifierGateway is IAxelarAmplifierGateway {

function _getIsContractCallApprovedKey(
bytes32 commandId,
string memory sourceChain,
string memory sourceAddress,
string calldata sourceChain,
string calldata sourceAddress,
address contractAddress,
bytes32 payloadHash
) internal pure returns (bytes32) {
Expand All @@ -192,9 +253,9 @@ contract AxelarAmplifierGateway is IAxelarAmplifierGateway {

/**
* @notice Gets the specific storage location for preventing upgrade collisions
* @return slot containing the WeightedMultisigStorage struct
* @return slot containing the AxelarAmplifierGatewayStorage struct
*/
function _axelarAmplifierGatewayStorage() private pure returns (AxelarAmplifierGatewayStorage storage slot) {
function _storage() private pure returns (AxelarAmplifierGatewayStorage storage slot) {
assembly {
slot.slot := AXELAR_AMPLIFIER_GATEWAY_SLOT
}
Expand Down
Loading

0 comments on commit 4b4ca7d

Please sign in to comment.