Prevent invalid destination griefing for the relayer (#2703)

### Description

- Adds destinationDomain to the `GasPayment` event
- recording destination domain as `destination` while reading events for
the relayer

### Drive-by changes

- none

### Related issues

- fixes https://github.com/hyperlane-xyz/issues/issues/475

### Backward compatibility

No, change in event emitted and relayer indexing

### Testing

- Unit

---------

Signed-off-by: -f <kunalarora1729@gmail.com>
Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>
pull/2748/head
Kunal Arora 1 year ago committed by GitHub
parent 7312a6f6db
commit f0e4f2b89b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 129
      rust/agents/relayer/src/msg/gas_payment/mod.rs
  2. 2
      rust/agents/relayer/src/msg/gas_payment/policies/minimum.rs
  3. 1
      rust/agents/relayer/src/msg/gas_payment/policies/none.rs
  4. 1
      rust/agents/relayer/src/msg/gas_payment/policies/on_chain_fee_quoting.rs
  5. 279
      rust/chains/hyperlane-ethereum/abis/IInterchainGasPaymaster.abi.json
  6. 1
      rust/chains/hyperlane-ethereum/src/interchain_gas.rs
  7. 1
      rust/chains/hyperlane-sealevel/src/interchain_gas.rs
  8. 24
      rust/hyperlane-base/src/db/rocks/hyperlane_db.rs
  9. 3
      rust/hyperlane-base/src/db/rocks/storage_types.rs
  10. 27
      rust/hyperlane-core/src/traits/encode.rs
  11. 14
      rust/hyperlane-core/src/types/log_metadata.rs
  12. 16
      rust/hyperlane-core/src/types/mod.rs
  13. 7
      solidity/contracts/igps/InterchainGasPaymaster.sol
  14. 2
      solidity/contracts/interfaces/IInterchainGasPaymaster.sol
  15. 8
      solidity/test/igps/InterchainGasPaymaster.t.sol

@ -6,8 +6,8 @@ use tracing::{debug, error, trace};
use hyperlane_base::db::HyperlaneRocksDB;
use hyperlane_core::{
HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, TxCostEstimate, TxOutcome,
U256,
GasPaymentKey, HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment,
TxCostEstimate, TxOutcome, U256,
};
use crate::msg::gas_payment::policies::GasPaymentPolicyOnChainFeeQuoting;
@ -78,7 +78,13 @@ impl GasPaymentEnforcer {
tx_cost_estimate: &TxCostEstimate,
) -> Result<Option<U256>> {
let msg_id = message.id();
let current_payment = self.db.retrieve_gas_payment_by_message_id(msg_id)?;
let gas_payment_key = GasPaymentKey {
message_id: msg_id,
destination: message.destination,
};
let current_payment = self
.db
.retrieve_gas_payment_by_gas_payment_key(gas_payment_key)?;
let current_expenditure = self.db.retrieve_gas_expenditure_by_message_id(msg_id)?;
for (policy, whitelist) in &self.policies {
if !whitelist.msg_matches(message, true) {
@ -137,7 +143,10 @@ mod test {
use std::str::FromStr;
use hyperlane_base::db::{test_utils, HyperlaneRocksDB};
use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, TxCostEstimate, H160, H256, U256};
use hyperlane_core::{
HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, LogMeta, TxCostEstimate, H160,
H256, U256,
};
use crate::settings::{
matching_list::MatchingList, GasPaymentEnforcementConf, GasPaymentEnforcementPolicy,
@ -209,6 +218,118 @@ mod test {
.await;
}
#[tokio::test]
async fn test_different_destinations() {
#[allow(unused_must_use)]
test_utils::run_test_db(|db| async move {
let msg = HyperlaneMessage {
destination: 123,
..HyperlaneMessage::default()
};
let hyperlane_db = HyperlaneRocksDB::new(
&HyperlaneDomain::new_test_domain("test_different_destinations"),
db,
);
let enforcer = GasPaymentEnforcer::new(
vec![GasPaymentEnforcementConf {
policy: GasPaymentEnforcementPolicy::Minimum {
payment: U256::one(),
},
matching_list: MatchingList::default(),
}],
hyperlane_db.clone(),
);
let wrong_destination_payment = InterchainGasPayment {
message_id: msg.id(),
destination: 456,
payment: U256::one(),
gas_amount: U256::one(),
};
hyperlane_db.process_gas_payment(wrong_destination_payment, &LogMeta::random());
// Ensure if the gas payment was made to the incorrect destination, it does not meet
// the requirement
assert!(enforcer
.message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),)
.await
.unwrap()
.is_none());
let correct_destination_payment = InterchainGasPayment {
message_id: msg.id(),
destination: msg.destination,
payment: U256::one(),
gas_amount: U256::one(),
};
hyperlane_db.process_gas_payment(correct_destination_payment, &LogMeta::random());
// Ensure if the gas payment was made to the correct destination, it meets the
// requirement
assert!(enforcer
.message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),)
.await
.unwrap()
.is_some());
})
.await;
}
#[tokio::test]
async fn test_half_and_half_payment() {
#[allow(unused_must_use)]
test_utils::run_test_db(|db| async move {
let msg = HyperlaneMessage {
destination: 123,
..HyperlaneMessage::default()
};
let hyperlane_db = HyperlaneRocksDB::new(
&HyperlaneDomain::new_test_domain("test_half_and_half_payment"),
db,
);
let enforcer = GasPaymentEnforcer::new(
vec![GasPaymentEnforcementConf {
policy: GasPaymentEnforcementPolicy::Minimum {
payment: U256::from(2),
},
matching_list: MatchingList::default(),
}],
hyperlane_db.clone(),
);
let initial_payment = InterchainGasPayment {
message_id: msg.id(),
destination: msg.destination,
payment: U256::one(),
gas_amount: U256::one(),
};
hyperlane_db.process_gas_payment(initial_payment, &LogMeta::random());
// Ensure if only half gas payment was made, it does not meet the requirement
assert!(enforcer
.message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),)
.await
.unwrap()
.is_none());
let deficit_payment = InterchainGasPayment {
message_id: msg.id(),
destination: msg.destination,
payment: U256::one(),
gas_amount: U256::one(),
};
hyperlane_db.process_gas_payment(deficit_payment, &LogMeta::random());
// Ensure if the full gas payment was made, it meets the requirement
assert!(enforcer
.message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),)
.await
.unwrap()
.is_some());
})
.await;
}
#[tokio::test]
async fn test_non_empty_matching_list() {
test_utils::run_test_db(|db| async move {

@ -41,6 +41,7 @@ async fn test_gas_payment_policy_minimum() {
// If the payment is less than the minimum, returns false
let current_payment = InterchainGasPayment {
message_id: H256::zero(),
destination: message.destination,
payment: U256::from(999u32),
gas_amount: U256::zero(),
};
@ -70,6 +71,7 @@ async fn test_gas_payment_policy_minimum() {
// If the payment is at least the minimum, returns false
let current_payment = InterchainGasPayment {
message_id: H256::zero(),
destination: message.destination,
payment: U256::from(1000u32),
gas_amount: U256::zero(),
};

@ -33,6 +33,7 @@ async fn test_gas_payment_policy_none() {
let current_payment = InterchainGasPayment {
message_id: H256::zero(),
destination: message.destination,
payment: U256::zero(),
gas_amount: U256::zero(),
};

@ -70,6 +70,7 @@ mod test {
fn current_payment(gas_amount: impl Into<U256>) -> InterchainGasPayment {
InterchainGasPayment {
message_id: H256::zero(),
destination: 0,
payment: U256::zero(),
gas_amount: gas_amount.into(),
}

@ -1,4 +1,36 @@
[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "beneficiary",
"type": "address"
}
],
"name": "BeneficiarySet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint32",
"name": "remoteDomain",
"type": "uint32"
},
{
"indexed": false,
"internalType": "address",
"name": "gasOracle",
"type": "address"
}
],
"name": "GasOracleSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
@ -8,6 +40,12 @@
"name": "messageId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "uint32",
"name": "destinationDomain",
"type": "uint32"
},
{
"indexed": false,
"internalType": "uint256",
@ -24,6 +62,145 @@
"name": "GasPayment",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint8",
"name": "version",
"type": "uint8"
}
],
"name": "Initialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [],
"name": "beneficiary",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "claim",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "deployedBlock",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"name": "gasOracles",
"outputs": [
{
"internalType": "contract IGasOracle",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_destinationDomain",
"type": "uint32"
}
],
"name": "getExchangeRateAndGasPrice",
"outputs": [
{
"internalType": "uint128",
"name": "tokenExchangeRate",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "gasPrice",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
},
{
"internalType": "address",
"name": "_beneficiary",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
@ -52,6 +229,48 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "metadata",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "message",
"type": "bytes"
}
],
"name": "postDispatch",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "metadata",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "message",
"type": "bytes"
}
],
"name": "quoteDispatch",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
@ -75,5 +294,63 @@
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_beneficiary",
"type": "address"
}
],
"name": "setBeneficiary",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "uint32",
"name": "remoteDomain",
"type": "uint32"
},
{
"internalType": "address",
"name": "gasOracle",
"type": "address"
}
],
"internalType": "struct InterchainGasPaymaster.GasOracleConfig[]",
"name": "_configs",
"type": "tuple[]"
}
],
"name": "setGasOracles",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
]

@ -103,6 +103,7 @@ where
(
InterchainGasPayment {
message_id: H256::from(log.message_id),
destination: log.destination_domain,
payment: log.payment.into(),
gas_amount: log.gas_amount.into(),
},

@ -210,6 +210,7 @@ impl SealevelInterchainGasPaymasterIndexer {
let igp_payment = InterchainGasPayment {
message_id: gas_payment_account.message_id,
destination: gas_payment_account.destination_domain,
payment: gas_payment_account.payment.into(),
gas_amount: gas_payment_account.gas_amount.into(),
};

@ -8,7 +8,7 @@ use tokio::time::sleep;
use tracing::{debug, instrument, trace};
use hyperlane_core::{
HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneMessageStore,
GasPaymentKey, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneMessageStore,
HyperlaneWatermarkedLogStore, InterchainGasExpenditure, InterchainGasPayment,
InterchainGasPaymentMeta, LogMeta, H256,
};
@ -146,7 +146,7 @@ impl HyperlaneRocksDB {
self.store_processed_by_gas_payment_meta(&payment_meta, &true)?;
// Update the total gas payment for the message to include the payment
self.update_gas_payment_by_message_id(payment)?;
self.update_gas_payment_by_gas_payment_key(payment)?;
// Return true to indicate the gas payment was processed for the first time
Ok(true)
@ -160,12 +160,16 @@ impl HyperlaneRocksDB {
}
/// Update the total gas payment for a message to include gas_payment
fn update_gas_payment_by_message_id(&self, event: InterchainGasPayment) -> DbResult<()> {
let existing_payment = self.retrieve_gas_payment_by_message_id(event.message_id)?;
fn update_gas_payment_by_gas_payment_key(&self, event: InterchainGasPayment) -> DbResult<()> {
let gas_payment_key = GasPaymentKey {
message_id: event.message_id,
destination: event.destination,
};
let existing_payment = self.retrieve_gas_payment_by_gas_payment_key(gas_payment_key)?;
let total = existing_payment + event;
debug!(?event, new_total_gas_payment=?total, "Storing gas payment");
self.store_interchain_gas_payment_data_by_message_id(&total.message_id, &total.into())?;
self.store_interchain_gas_payment_data_by_gas_payment_key(&gas_payment_key, &total.into())?;
Ok(())
}
@ -190,14 +194,14 @@ impl HyperlaneRocksDB {
}
/// Retrieve the total gas payment for a message
pub fn retrieve_gas_payment_by_message_id(
pub fn retrieve_gas_payment_by_gas_payment_key(
&self,
message_id: H256,
gas_payment_key: GasPaymentKey,
) -> DbResult<InterchainGasPayment> {
Ok(self
.retrieve_interchain_gas_payment_data_by_message_id(&message_id)?
.retrieve_interchain_gas_payment_data_by_gas_payment_key(&gas_payment_key)?
.unwrap_or_default()
.complete(message_id))
.complete(gas_payment_key.message_id, gas_payment_key.destination))
}
/// Retrieve the total gas payment for a message
@ -315,7 +319,7 @@ make_store_and_retrieve!(pub(self), dispatched_block_number_by_nonce, MESSAGE_DI
make_store_and_retrieve!(pub, processed_by_nonce, NONCE_PROCESSED, u32, bool);
make_store_and_retrieve!(pub(self), processed_by_gas_payment_meta, GAS_PAYMENT_META_PROCESSED, InterchainGasPaymentMeta, bool);
make_store_and_retrieve!(pub(self), interchain_gas_expenditure_data_by_message_id, GAS_EXPENDITURE_FOR_MESSAGE_ID, H256, InterchainGasExpenditureData);
make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_message_id, GAS_PAYMENT_FOR_MESSAGE_ID, H256, InterchainGasPaymentData);
make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_gas_payment_key, GAS_PAYMENT_FOR_MESSAGE_ID, GasPaymentKey, InterchainGasPaymentData);
make_store_and_retrieve!(
pub,
pending_message_retry_count_by_message_id,

@ -31,9 +31,10 @@ impl Default for InterchainGasPaymentData {
}
impl InterchainGasPaymentData {
pub fn complete(self, message_id: H256) -> InterchainGasPayment {
pub fn complete(self, message_id: H256, destination: u32) -> InterchainGasPayment {
InterchainGasPayment {
message_id,
destination,
payment: self.payment,
gas_amount: self.gas_amount,
}

@ -1,6 +1,6 @@
use std::io::{Error, ErrorKind};
use crate::{HyperlaneProtocolError, H160, H256, H512, U256};
use crate::{GasPaymentKey, HyperlaneProtocolError, H160, H256, H512, U256};
/// Simple trait for types with a canonical encoding
pub trait Encode {
@ -179,3 +179,28 @@ impl Decode for bool {
}
}
}
impl Encode for GasPaymentKey {
fn write_to<W>(&self, writer: &mut W) -> std::io::Result<usize>
where
W: std::io::Write,
{
let mut written = 0;
written += self.message_id.write_to(writer)?;
written += self.destination.write_to(writer)?;
Ok(written)
}
}
impl Decode for GasPaymentKey {
fn read_from<R>(reader: &mut R) -> Result<Self, HyperlaneProtocolError>
where
R: std::io::Read,
Self: Sized,
{
Ok(Self {
message_id: H256::read_from(reader)?,
destination: u32::read_from(reader)?,
})
}
}

@ -67,3 +67,17 @@ impl Ord for LogMeta {
self.partial_cmp(other).unwrap()
}
}
impl LogMeta {
/// Create a new LogMeta with random transaction ID
pub fn random() -> Self {
Self {
address: H256::zero(),
block_number: 1,
block_hash: H256::zero(),
transaction_id: H512::random(),
transaction_index: 0,
log_index: U256::zero(),
}
}
}

@ -103,11 +103,22 @@ impl From<Signature> for ethers_core::types::Signature {
}
}
/// Key for the gas payment
#[derive(Debug, Copy, Clone)]
pub struct GasPaymentKey {
/// Id of the message
pub message_id: H256,
/// Destination domain paid for.
pub destination: u32,
}
/// A payment of a message's gas costs.
#[derive(Debug, Copy, Clone)]
pub struct InterchainGasPayment {
/// Id of the message
pub message_id: H256,
/// Destination domain paid for.
pub destination: u32,
/// Amount of native tokens paid.
pub payment: U256,
/// Amount of destination gas paid for.
@ -133,8 +144,13 @@ impl Add for InterchainGasPayment {
self.message_id, rhs.message_id,
"Cannot add interchain gas payments for different messages"
);
assert_eq!(
self.destination, rhs.destination,
"Cannot add interchain gas payments for different destinations"
);
Self {
message_id: self.message_id,
destination: self.destination,
payment: self.payment + rhs.payment,
gas_amount: self.gas_amount + rhs.gas_amount,
}

@ -184,7 +184,12 @@ contract InterchainGasPaymaster is
payable(_refundAddress).sendValue(_overpayment);
}
emit GasPayment(_messageId, _gasAmount, _requiredPayment);
emit GasPayment(
_messageId,
_destinationDomain,
_gasAmount,
_requiredPayment
);
}
/**

@ -10,11 +10,13 @@ interface IInterchainGasPaymaster {
/**
* @notice Emitted when a payment is made for a message's gas costs.
* @param messageId The ID of the message to pay for.
* @param destinationDomain The domain of the destination chain.
* @param gasAmount The amount of destination gas paid for.
* @param payment The amount of native tokens paid.
*/
event GasPayment(
bytes32 indexed messageId,
uint32 indexed destinationDomain,
uint256 gasAmount,
uint256 payment
);

@ -34,6 +34,7 @@ contract InterchainGasPaymasterTest is Test {
event GasPayment(
bytes32 indexed messageId,
uint32 indexed destinationDomain,
uint256 gasAmount,
uint256 payment
);
@ -176,7 +177,12 @@ contract InterchainGasPaymasterTest is Test {
uint256 _overpayment = 54321;
vm.expectEmit(true, true, false, true);
emit GasPayment(testMessageId, testGasAmount, _quote);
emit GasPayment(
testMessageId,
testDestinationDomain,
testGasAmount,
_quote
);
igp.payForGas{value: _quote + _overpayment}(
testMessageId,
testDestinationDomain,

Loading…
Cancel
Save