Skip to content

Commit

Permalink
Store hash of execution data (#103)
Browse files Browse the repository at this point in the history
* Store hash of execution data

Reduces storage gas cost, but requires caller to pass in full execution
data.

Fixes #35.

* Rename and refactor to address comments

* Run forge fmt

* Nit fixes to address comment
  • Loading branch information
Dominator008 committed Oct 11, 2023
1 parent 0575e02 commit 5dab5e2
Show file tree
Hide file tree
Showing 16 changed files with 344 additions and 149 deletions.
39 changes: 24 additions & 15 deletions src/MultiBridgeMessageReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import "./libraries/Message.sol";
/// @dev The contract only accepts messages from trusted bridge receiver adapters, each of which implements the
/// IMessageReceiverAdapter interface.
contract MultiBridgeMessageReceiver is IMultiBridgeMessageReceiver, ExecutorAware {
using MessageLibrary for MessageLibrary.Message;
using MessageLibrary for MessageLibrary.MessageExecutionParams;

/// @notice the id of the source chain that this contract can receive messages from
uint256 public immutable srcChainId;
/// @notice the global access control contract
Expand All @@ -42,8 +45,8 @@ contract MultiBridgeMessageReceiver is IMultiBridgeMessageReceiver, ExecutorAwar
/// @notice count of bridge adapters that have delivered each message
mapping(bytes32 msgId => uint256 deliveryCount) public msgDeliveryCount;

/// @notice the data required for executing a message
mapping(bytes32 msgId => ExecutionData execData) public msgExecData;
/// @notice the hash of the params required for executing a message
mapping(bytes32 msgId => bytes32 execParamsHash) public msgExecParamsHash;

/// @notice whether a message has been sent to the governance timelock for execution
mapping(bytes32 msgId => bool scheduled) public isExecutionScheduled;
Expand Down Expand Up @@ -114,7 +117,7 @@ contract MultiBridgeMessageReceiver is IMultiBridgeMessageReceiver, ExecutorAwar

/// this msgId is totally different with each adapters' internal msgId(which is their internal nonce essentially)
/// although each adapters' internal msgId is attached at the end of calldata, it's not useful to MultiBridgeMessageReceiver.sol.
bytes32 msgId = MessageLibrary.computeMsgId(_message);
bytes32 msgId = _message.computeMsgId();

if (msgDeliveries[msgId][msg.sender]) {
revert Error.DUPLICATE_MESSAGE_DELIVERY_BY_ADAPTER();
Expand All @@ -127,29 +130,33 @@ contract MultiBridgeMessageReceiver is IMultiBridgeMessageReceiver, ExecutorAwar

msgDeliveries[msgId][msg.sender] = true;

/// increment quorum
/// increment vote count for a message
++msgDeliveryCount[msgId];

/// stores the execution data required
ExecutionData memory prevStored = msgExecData[msgId];
/// stores the hash of the execution params required
bytes32 prevStoredHash = msgExecParamsHash[msgId];

/// stores the message if the amb is the first one delivering the message
if (prevStored.target == address(0)) {
msgExecData[msgId] = ExecutionData(
_message.target, _message.callData, _message.nativeValue, _message.nonce, _message.expiration
);
if (prevStoredHash == bytes32(0)) {
msgExecParamsHash[msgId] = _message.computeExecutionParamsHash();
}

string memory bridgeName = IMessageReceiverAdapter(msg.sender).name();
emit BridgeMessageReceived(msgId, bridgeName, _message.nonce, msg.sender);
}

/// @inheritdoc IMultiBridgeMessageReceiver
function scheduleMessageExecution(bytes32 _msgId) external override {
ExecutionData memory _execData = msgExecData[_msgId];
function scheduleMessageExecution(bytes32 _msgId, MessageLibrary.MessageExecutionParams calldata _execParams)
external
override
{
bytes32 execParamsHash = msgExecParamsHash[_msgId];
if (_execParams.computeExecutionParamsHash() != execParamsHash) {
revert Error.EXEC_PARAMS_HASH_MISMATCH();
}

/// @dev validates if msg execution is not beyond expiration
if (block.timestamp > _execData.expiration) {
if (block.timestamp > _execParams.expiration) {
revert Error.MSG_EXECUTION_PASSED_DEADLINE();
}

Expand All @@ -167,10 +174,12 @@ contract MultiBridgeMessageReceiver is IMultiBridgeMessageReceiver, ExecutorAwar

/// @dev queues the action on timelock for execution
IGovernanceTimelock(governanceTimelock).scheduleTransaction(
_execData.target, _execData.value, _execData.callData
_execParams.target, _execParams.value, _execParams.callData
);

emit MessageExecutionScheduled(_msgId, _execData.target, _execData.value, _execData.nonce, _execData.callData);
emit MessageExecutionScheduled(
_msgId, _execParams.target, _execParams.value, _execParams.nonce, _execParams.callData
);
}

/// @notice update the governance timelock contract.
Expand Down
4 changes: 3 additions & 1 deletion src/MultiBridgeMessageSender.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import "./libraries/Error.sol";
/// Both of these are configured in the Global Access Control contract. In the case of Uniswap, both the authorised caller
/// and owner should be set to the Uniswap V2 Timelock contract on Ethereum.
contract MultiBridgeMessageSender {
using MessageLibrary for MessageLibrary.Message;

/*/////////////////////////////////////////////////////////////////
STRUCTS
////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -268,7 +270,7 @@ contract MultiBridgeMessageSender {
_args.nativeValue,
block.timestamp + _args.expiration
);
bytes32 msgId = MessageLibrary.computeMsgId(message);
bytes32 msgId = message.computeMsgId();
(bool[] memory adapterSuccess, uint256 successCount) =
_dispatchMessages(adapters, mmaReceiver, _args.dstChainId, message, _args.fees);

Expand Down
18 changes: 3 additions & 15 deletions src/interfaces/IMultiBridgeMessageReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,6 @@ import "../libraries/Message.sol";

/// @notice interface for the multi-bridge message receiver
interface IMultiBridgeMessageReceiver {
/// @notice encapsulates data that is relevant to a message's intended transaction execution.
struct ExecutionData {
// target contract address on the destination chain
address target;
// data to pass to target by low-level call
bytes callData;
// value to pass to target by low-level call
uint256 value;
// nonce of the message
uint256 nonce;
// expiration timestamp for the message beyond which it cannot be executed
uint256 expiration;
}

/// @notice emitted when a message has been received from a single bridge.
/// @param msgId is the unique identifier of the message
/// @param bridgeName is the name of the bridge from which the message was received
Expand Down Expand Up @@ -60,7 +46,9 @@ interface IMultiBridgeMessageReceiver {

/// @notice Sends a message, that has achieved quorum and has not yet expired, to the governance timelock for eventual execution.
/// @param _msgId is the unique identifier of the message
function scheduleMessageExecution(bytes32 _msgId) external;
/// @param _execParams are the params for message execution
function scheduleMessageExecution(bytes32 _msgId, MessageLibrary.MessageExecutionParams calldata _execParams)
external;

/// @notice adds or removes bridge receiver adapters.
/// @param _receiverAdapters the list of receiver adapters to add or remove
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/Error.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ library Error {
/// @dev is thrown if refund address is zero (or) invalid
error INVALID_REFUND_ADDRESS();

/// @dev is thrown if execution params do not match the stored hash
error EXEC_PARAMS_HASH_MISMATCH();

/*/////////////////////////////////////////////////////////////////
ADAPTER ERRORS
////////////////////////////////////////////////////////////////*/
Expand Down
34 changes: 34 additions & 0 deletions src/libraries/Message.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ library MessageLibrary {
uint256 expiration;
}

/// @notice encapsulates data that is relevant to a message's intended transaction execution.
struct MessageExecutionParams {
// target contract address on the destination chain
address target;
// data to pass to target by low-level call
bytes callData;
// value to pass to target by low-level call
uint256 value;
// nonce of the message
uint256 nonce;
// expiration timestamp for the message beyond which it cannot be executed
uint256 expiration;
}

/// @notice computes the message id (32 byte hash of the encoded message parameters)
/// @param _message is the cross-chain message
function computeMsgId(Message memory _message) internal pure returns (bytes32) {
Expand All @@ -37,4 +51,24 @@ library MessageLibrary {
)
);
}

function extractExecutionParams(Message memory _message) internal pure returns (MessageExecutionParams memory) {
return MessageExecutionParams({
target: _message.target,
callData: _message.callData,
value: _message.nativeValue,
nonce: _message.nonce,
expiration: _message.expiration
});
}

function computeExecutionParamsHash(MessageExecutionParams memory _params) internal pure returns (bytes32) {
return keccak256(
abi.encodePacked(_params.target, _params.callData, _params.value, _params.nonce, _params.expiration)
);
}

function computeExecutionParamsHash(Message memory _message) internal pure returns (bytes32) {
return computeExecutionParamsHash(extractExecutionParams(_message));
}
}
24 changes: 20 additions & 4 deletions test/integration-tests/GracePeriodExpiry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import "test/contracts-mock/MockUniswapReceiver.sol";

import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol";
import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol";
import "src/libraries/Message.sol";
import {Error} from "src/libraries/Error.sol";
import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol";

Expand Down Expand Up @@ -40,11 +41,17 @@ contract GracePeriodExpiryTest is Setup {
0.01 ether,
wormholeFee
);
MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]).remoteCall{value: 2 ether}(

bytes memory callData = abi.encode(MockUniswapReceiver.setValue.selector, "");
uint256 nativeValue = 0;
uint256 expiration = block.timestamp + EXPIRATION_CONSTANT;
MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]);
uint256 nonce = sender.nonce() + 1;
sender.remoteCall{value: 2 ether}(
DST_CHAIN_ID,
address(target),
abi.encode(MockUniswapReceiver.setValue.selector, ""),
0,
callData,
nativeValue,
EXPIRATION_CONSTANT,
refundAddress,
fees,
Expand All @@ -63,7 +70,16 @@ contract GracePeriodExpiryTest is Setup {
vm.selectFork(fork[DST_CHAIN_ID]);
vm.recordLogs();
/// schedule the message for execution by moving it to governance timelock contract
MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution(msgId);
MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution(
msgId,
MessageLibrary.MessageExecutionParams({
target: address(target),
callData: callData,
value: nativeValue,
nonce: nonce,
expiration: expiration
})
);
(uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) =
_getExecParams(vm.getRecordedLogs());

Expand Down
24 changes: 20 additions & 4 deletions test/integration-tests/MultiMessageAggregation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import "test/contracts-mock/MockUniswapReceiver.sol";

import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol";
import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol";
import "src/libraries/Message.sol";
import {Error} from "src/libraries/Error.sol";
import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol";

Expand Down Expand Up @@ -40,11 +41,17 @@ contract MultiBridgeMessageAggregationTest is Setup {
0.01 ether,
wormholeFee
);
MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]).remoteCall{value: 2 ether}(

bytes memory callData = abi.encode(MockUniswapReceiver.setValue.selector, "");
uint256 nativeValue = 0;
uint256 expiration = block.timestamp + EXPIRATION_CONSTANT;
MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]);
uint256 nonce = sender.nonce() + 1;
sender.remoteCall{value: 2 ether}(
DST_CHAIN_ID,
address(target),
abi.encode(MockUniswapReceiver.setValue.selector, ""),
0,
callData,
nativeValue,
EXPIRATION_CONSTANT,
refundAddress,
fees,
Expand All @@ -63,7 +70,16 @@ contract MultiBridgeMessageAggregationTest is Setup {
vm.selectFork(fork[DST_CHAIN_ID]);
vm.recordLogs();
/// schedule the message for execution by moving it to governance timelock contract
MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution(msgId);
MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution(
msgId,
MessageLibrary.MessageExecutionParams({
target: address(target),
callData: callData,
value: nativeValue,
nonce: nonce,
expiration: expiration
})
);
(uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) =
_getExecParams(vm.getRecordedLogs());

Expand Down
44 changes: 33 additions & 11 deletions test/integration-tests/RemoteAdapterAdd.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "test/Setup.t.sol";

import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol";
import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol";
import "src/libraries/Message.sol";
import {Error} from "src/libraries/Error.sol";
import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol";

Expand Down Expand Up @@ -51,7 +52,7 @@ contract RemoteAdapterAdd is Setup {
_adapterAdd(adaptersToAdd, operation);
}

function _adapterAdd(address[] memory adaptersToAdd, bool[] memory operation) internal {
function _adapterAdd(address[] memory adaptersToAdd, bool[] memory operation) private {
vm.selectFork(fork[ETHEREUM_CHAIN_ID]);
vm.startPrank(caller);

Expand All @@ -65,10 +66,28 @@ contract RemoteAdapterAdd is Setup {
0.01 ether,
wormholeFee
);
MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]).remoteCall{value: 2 ether}(

_sendAndExecuteMessage(adaptersToAdd, operation, fees);

for (uint256 j; j < adaptersToAdd.length; ++j) {
bool isTrusted = MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")])
.isTrustedExecutor(adaptersToAdd[j]);
assert(isTrusted);
}
}

function _sendAndExecuteMessage(address[] memory adaptersToAdd, bool[] memory operation, uint256[] memory fees)
private
{
MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]);
uint256 nonce = sender.nonce() + 1;
bytes memory callData =
abi.encodeWithSelector(MultiBridgeMessageReceiver.updateReceiverAdapters.selector, adaptersToAdd, operation);
uint256 expiration = block.timestamp + EXPIRATION_CONSTANT;
sender.remoteCall{value: 2 ether}(
DST_CHAIN_ID,
address(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]),
abi.encodeWithSelector(MultiBridgeMessageReceiver.updateReceiverAdapters.selector, adaptersToAdd, operation),
contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")],
callData,
0,
EXPIRATION_CONSTANT,
refundAddress,
Expand All @@ -88,7 +107,16 @@ contract RemoteAdapterAdd is Setup {
vm.selectFork(fork[DST_CHAIN_ID]);
vm.recordLogs();
/// schedule the message for execution by moving it to governance timelock contract
MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution(msgId);
MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution(
msgId,
MessageLibrary.MessageExecutionParams({
target: contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")],
callData: callData,
value: 0,
nonce: nonce,
expiration: expiration
})
);
(uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) =
_getExecParams(vm.getRecordedLogs());

Expand All @@ -97,11 +125,5 @@ contract RemoteAdapterAdd is Setup {
GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction(
txId, finalTarget, value, data, eta
);

for (uint256 j; j < adaptersToAdd.length; ++j) {
bool isTrusted = MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")])
.isTrustedExecutor(adaptersToAdd[j]);
assert(isTrusted);
}
}
}
Loading

0 comments on commit 5dab5e2

Please sign in to comment.