fix(evm::engine): added shared settings check in vet_transaction
This commit is contained in:
@@ -50,19 +50,19 @@ message SharedSettings {
|
|||||||
uint64 chain_id = 2;
|
uint64 chain_id = 2;
|
||||||
optional google.protobuf.Timestamp valid_from = 3;
|
optional google.protobuf.Timestamp valid_from = 3;
|
||||||
optional google.protobuf.Timestamp valid_until = 4;
|
optional google.protobuf.Timestamp valid_until = 4;
|
||||||
optional bytes max_gas_fee_per_gas = 5; // U256 as big-endian bytes
|
optional bytes max_gas_fee_per_gas = 5; // U256 as big-endian bytes
|
||||||
optional bytes max_priority_fee_per_gas = 6; // U256 as big-endian bytes
|
optional bytes max_priority_fee_per_gas = 6; // U256 as big-endian bytes
|
||||||
optional TransactionRateLimit rate_limit = 7;
|
optional TransactionRateLimit rate_limit = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EtherTransferSettings {
|
message EtherTransferSettings {
|
||||||
repeated bytes targets = 1; // list of 20-byte Ethereum addresses
|
repeated bytes targets = 1; // list of 20-byte Ethereum addresses
|
||||||
VolumeRateLimit limit = 2;
|
VolumeRateLimit limit = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TokenTransferSettings {
|
message TokenTransferSettings {
|
||||||
bytes token_contract = 1; // 20-byte Ethereum address
|
bytes token_contract = 1; // 20-byte Ethereum address
|
||||||
optional bytes target = 2; // 20-byte Ethereum address; absent means any recipient allowed
|
optional bytes target = 2; // 20-byte Ethereum address; absent means any recipient allowed
|
||||||
repeated VolumeRateLimit volume_limits = 3;
|
repeated VolumeRateLimit volume_limits = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,20 +74,20 @@ message SpecificGrant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message EtherTransferMeaning {
|
message EtherTransferMeaning {
|
||||||
bytes to = 1; // 20-byte Ethereum address
|
bytes to = 1; // 20-byte Ethereum address
|
||||||
bytes value = 2; // U256 as big-endian bytes
|
bytes value = 2; // U256 as big-endian bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
message TokenInfo {
|
message TokenInfo {
|
||||||
string symbol = 1;
|
string symbol = 1;
|
||||||
bytes address = 2; // 20-byte Ethereum address
|
bytes address = 2; // 20-byte Ethereum address
|
||||||
uint64 chain_id = 3;
|
uint64 chain_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror of token_transfers::Meaning
|
// Mirror of token_transfers::Meaning
|
||||||
message TokenTransferMeaning {
|
message TokenTransferMeaning {
|
||||||
TokenInfo token = 1;
|
TokenInfo token = 1;
|
||||||
bytes to = 2; // 20-byte Ethereum address
|
bytes to = 2; // 20-byte Ethereum address
|
||||||
bytes value = 3; // U256 as big-endian bytes
|
bytes value = 3; // U256 as big-endian bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +101,13 @@ message SpecificMeaning {
|
|||||||
|
|
||||||
// --- Eval error types ---
|
// --- Eval error types ---
|
||||||
message GasLimitExceededViolation {
|
message GasLimitExceededViolation {
|
||||||
optional bytes max_gas_fee_per_gas = 1; // U256 as big-endian bytes
|
optional bytes max_gas_fee_per_gas = 1; // U256 as big-endian bytes
|
||||||
optional bytes max_priority_fee_per_gas = 2; // U256 as big-endian bytes
|
optional bytes max_priority_fee_per_gas = 2; // U256 as big-endian bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvalViolation {
|
message EvalViolation {
|
||||||
oneof kind {
|
oneof kind {
|
||||||
bytes invalid_target = 1; // 20-byte Ethereum address
|
bytes invalid_target = 1; // 20-byte Ethereum address
|
||||||
GasLimitExceededViolation gas_limit_exceeded = 2;
|
GasLimitExceededViolation gas_limit_exceeded = 2;
|
||||||
google.protobuf.Empty rate_limit_exceeded = 3;
|
google.protobuf.Empty rate_limit_exceeded = 3;
|
||||||
google.protobuf.Empty volumetric_limit_exceeded = 4;
|
google.protobuf.Empty volumetric_limit_exceeded = 4;
|
||||||
@@ -167,6 +167,7 @@ message GrantEntry {
|
|||||||
int32 id = 1;
|
int32 id = 1;
|
||||||
int32 client_id = 2;
|
int32 client_id = 2;
|
||||||
SharedSettings shared = 3;
|
SharedSettings shared = 3;
|
||||||
|
SpecificGrant specific = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvmGrantListRequest {
|
message EvmGrantListRequest {
|
||||||
@@ -187,7 +188,7 @@ message EvmGrantList {
|
|||||||
// --- Client transaction operations ---
|
// --- Client transaction operations ---
|
||||||
|
|
||||||
message EvmSignTransactionRequest {
|
message EvmSignTransactionRequest {
|
||||||
bytes wallet_address = 1; // 20-byte Ethereum address
|
bytes wallet_address = 1; // 20-byte Ethereum address
|
||||||
bytes rlp_transaction = 2; // RLP-encoded EIP-1559 transaction (unsigned)
|
bytes rlp_transaction = 2; // RLP-encoded EIP-1559 transaction (unsigned)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,14 +196,14 @@ message EvmSignTransactionRequest {
|
|||||||
// is always either an eval error or an internal error, never a partial success
|
// is always either an eval error or an internal error, never a partial success
|
||||||
message EvmSignTransactionResponse {
|
message EvmSignTransactionResponse {
|
||||||
oneof result {
|
oneof result {
|
||||||
bytes signature = 1; // 65-byte signature: r[32] || s[32] || v[1]
|
bytes signature = 1; // 65-byte signature: r[32] || s[32] || v[1]
|
||||||
TransactionEvalError eval_error = 2;
|
TransactionEvalError eval_error = 2;
|
||||||
EvmError error = 3;
|
EvmError error = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvmAnalyzeTransactionRequest {
|
message EvmAnalyzeTransactionRequest {
|
||||||
bytes wallet_address = 1; // 20-byte Ethereum address
|
bytes wallet_address = 1; // 20-byte Ethereum address
|
||||||
bytes rlp_transaction = 2; // RLP-encoded EIP-1559 transaction
|
bytes rlp_transaction = 2; // RLP-encoded EIP-1559 transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
pub mod abi;
|
pub mod abi;
|
||||||
pub mod safe_signer;
|
pub mod safe_signer;
|
||||||
|
|
||||||
use alloy::{consensus::TxEip1559, primitives::TxKind};
|
use alloy::{consensus::TxEip1559, primitives::{TxKind, U256}};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::{QueryResult, insert_into};
|
use diesel::{QueryResult, insert_into, sqlite::Sqlite};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -16,7 +16,8 @@ use crate::{
|
|||||||
schema::{self, evm_transaction_log},
|
schema::{self, evm_transaction_log},
|
||||||
},
|
},
|
||||||
evm::policies::{
|
evm::policies::{
|
||||||
EvalContext, EvalViolation, FullGrant, Grant, Policy, SpecificGrant, SpecificMeaning,
|
DatabaseID, EvalContext, EvalViolation, FullGrant, Grant, Policy, SharedGrantSettings,
|
||||||
|
SpecificGrant, SpecificMeaning,
|
||||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -107,6 +108,54 @@ pub enum RunKind {
|
|||||||
CheckOnly,
|
CheckOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_shared_constraints(
|
||||||
|
context: &EvalContext,
|
||||||
|
shared: &SharedGrantSettings,
|
||||||
|
shared_grant_id: DatabaseID,
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
) -> QueryResult<Vec<EvalViolation>> {
|
||||||
|
let mut violations = Vec::new();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
// Validity window
|
||||||
|
if shared.valid_from.map_or(false, |t| now < t)
|
||||||
|
|| shared.valid_until.map_or(false, |t| now > t)
|
||||||
|
{
|
||||||
|
violations.push(EvalViolation::InvalidTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gas fee caps
|
||||||
|
let fee_exceeded = shared
|
||||||
|
.max_gas_fee_per_gas
|
||||||
|
.map_or(false, |cap| U256::from(context.max_fee_per_gas) > cap);
|
||||||
|
let priority_exceeded = shared
|
||||||
|
.max_priority_fee_per_gas
|
||||||
|
.map_or(false, |cap| U256::from(context.max_priority_fee_per_gas) > cap);
|
||||||
|
if fee_exceeded || priority_exceeded {
|
||||||
|
violations.push(EvalViolation::GasLimitExceeded {
|
||||||
|
max_gas_fee_per_gas: shared.max_gas_fee_per_gas,
|
||||||
|
max_priority_fee_per_gas: shared.max_priority_fee_per_gas,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction count rate limit
|
||||||
|
if let Some(rate_limit) = &shared.rate_limit {
|
||||||
|
let window_start = SqliteTimestamp(now - rate_limit.window);
|
||||||
|
let count: i64 = evm_transaction_log::table
|
||||||
|
.filter(evm_transaction_log::grant_id.eq(shared_grant_id))
|
||||||
|
.filter(evm_transaction_log::signed_at.ge(window_start))
|
||||||
|
.count()
|
||||||
|
.get_result(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if count >= rate_limit.count as i64 {
|
||||||
|
violations.push(EvalViolation::RateLimitExceeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(violations)
|
||||||
|
}
|
||||||
|
|
||||||
// Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed
|
// Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
db: db::DatabasePool,
|
db: db::DatabasePool,
|
||||||
@@ -125,7 +174,11 @@ impl Engine {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||||
|
|
||||||
let violations = P::evaluate(&context, meaning, &grant, &mut conn).await?;
|
let mut violations =
|
||||||
|
check_shared_constraints(&context, &grant.shared, grant.shared_grant_id, &mut conn)
|
||||||
|
.await?;
|
||||||
|
violations.extend(P::evaluate(&context, meaning, &grant, &mut conn).await?);
|
||||||
|
|
||||||
if !violations.is_empty() {
|
if !violations.is_empty() {
|
||||||
return Err(PolicyError::Violations(violations));
|
return Err(PolicyError::Violations(violations));
|
||||||
} else if run_kind == RunKind::Execution {
|
} else if run_kind == RunKind::Execution {
|
||||||
@@ -247,9 +300,11 @@ impl Engine {
|
|||||||
wallet_id,
|
wallet_id,
|
||||||
client_id,
|
client_id,
|
||||||
chain: transaction.chain_id,
|
chain: transaction.chain_id,
|
||||||
to: to,
|
to,
|
||||||
value: transaction.value,
|
value: transaction.value,
|
||||||
calldata: transaction.input.clone(),
|
calldata: transaction.input.clone(),
|
||||||
|
max_fee_per_gas: transaction.max_fee_per_gas,
|
||||||
|
max_priority_fee_per_gas: transaction.max_priority_fee_per_gas,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(meaning) = EtherTransfer::analyze(&context) {
|
if let Some(meaning) = EtherTransfer::analyze(&context) {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ pub struct EvalContext {
|
|||||||
pub to: Address,
|
pub to: Address,
|
||||||
pub value: U256,
|
pub value: U256,
|
||||||
pub calldata: Bytes,
|
pub calldata: Bytes,
|
||||||
|
|
||||||
|
// Gas pricing (EIP-1559)
|
||||||
|
pub max_fee_per_gas: u128,
|
||||||
|
pub max_priority_fee_per_gas: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, Diagnostic)]
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ impl Policy for EtherTransfer {
|
|||||||
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
|
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
|
||||||
.filter(evm_basic_grant::client_id.eq(context.client_id))
|
.filter(evm_basic_grant::client_id.eq(context.client_id))
|
||||||
.filter(evm_ether_transfer_grant_target::address.eq(&target_bytes))
|
.filter(evm_ether_transfer_grant_target::address.eq(&target_bytes))
|
||||||
|
.filter(evm_basic_grant::revoked_at.is_null())
|
||||||
.select((
|
.select((
|
||||||
EvmBasicGrant::as_select(),
|
EvmBasicGrant::as_select(),
|
||||||
EvmEtherTransferGrant::as_select(),
|
EvmEtherTransferGrant::as_select(),
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ use crate::db::schema::{
|
|||||||
};
|
};
|
||||||
use crate::evm::{
|
use crate::evm::{
|
||||||
abi::IERC20::transferCall,
|
abi::IERC20::transferCall,
|
||||||
policies::{Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit},
|
policies::{
|
||||||
|
Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
||||||
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,8 +32,7 @@ use super::{DatabaseID, EvalContext, EvalViolation};
|
|||||||
#[diesel::auto_type]
|
#[diesel::auto_type]
|
||||||
fn grant_join() -> _ {
|
fn grant_join() -> _ {
|
||||||
evm_token_transfer_grant::table.inner_join(
|
evm_token_transfer_grant::table.inner_join(
|
||||||
evm_basic_grant::table
|
evm_basic_grant::table.on(evm_token_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
|
||||||
.on(evm_token_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +207,7 @@ impl Policy for TokenTransfer {
|
|||||||
let token_contract_bytes = context.to.to_vec();
|
let token_contract_bytes = context.to.to_vec();
|
||||||
|
|
||||||
let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
|
let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
|
||||||
|
.filter(evm_basic_grant::revoked_at.is_null())
|
||||||
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
|
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
|
||||||
.filter(evm_basic_grant::client_id.eq(context.client_id))
|
.filter(evm_basic_grant::client_id.eq(context.client_id))
|
||||||
.filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes))
|
.filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes))
|
||||||
@@ -299,7 +301,10 @@ impl Policy for TokenTransfer {
|
|||||||
) -> QueryResult<Vec<Grant<Self::Settings>>> {
|
) -> QueryResult<Vec<Grant<Self::Settings>>> {
|
||||||
let grants: Vec<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
|
let grants: Vec<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
|
||||||
.filter(evm_basic_grant::revoked_at.is_null())
|
.filter(evm_basic_grant::revoked_at.is_null())
|
||||||
.select((EvmBasicGrant::as_select(), EvmTokenTransferGrant::as_select()))
|
.select((
|
||||||
|
EvmBasicGrant::as_select(),
|
||||||
|
EvmTokenTransferGrant::as_select(),
|
||||||
|
))
|
||||||
.load(conn)
|
.load(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -318,7 +323,10 @@ impl Policy for TokenTransfer {
|
|||||||
|
|
||||||
let mut limits_by_grant: HashMap<i32, Vec<EvmTokenTransferVolumeLimit>> = HashMap::new();
|
let mut limits_by_grant: HashMap<i32, Vec<EvmTokenTransferVolumeLimit>> = HashMap::new();
|
||||||
for limit in all_volume_limits {
|
for limit in all_volume_limits {
|
||||||
limits_by_grant.entry(limit.grant_id).or_default().push(limit);
|
limits_by_grant
|
||||||
|
.entry(limit.grant_id)
|
||||||
|
.or_default()
|
||||||
|
.push(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
grants
|
grants
|
||||||
@@ -331,20 +339,20 @@ impl Policy for TokenTransfer {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
Ok(VolumeRateLimit {
|
Ok(VolumeRateLimit {
|
||||||
max_volume: utils::try_bytes_to_u256(&row.max_volume)
|
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(|e| {
|
||||||
.map_err(|e| diesel::result::Error::DeserializationError(Box::new(e)))?,
|
diesel::result::Error::DeserializationError(Box::new(e))
|
||||||
|
})?,
|
||||||
window: Duration::seconds(row.window_secs as i64),
|
window: Duration::seconds(row.window_secs as i64),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<QueryResult<Vec<_>>>()?;
|
.collect::<QueryResult<Vec<_>>>()?;
|
||||||
|
|
||||||
let token_contract: [u8; 20] = specific
|
let token_contract: [u8; 20] =
|
||||||
.token_contract
|
specific.token_contract.clone().try_into().map_err(|_| {
|
||||||
.clone()
|
diesel::result::Error::DeserializationError(
|
||||||
.try_into()
|
"Invalid token contract address length".into(),
|
||||||
.map_err(|_| diesel::result::Error::DeserializationError(
|
)
|
||||||
"Invalid token contract address length".into(),
|
})?;
|
||||||
))?;
|
|
||||||
|
|
||||||
let target: Option<Address> = match &specific.receiver {
|
let target: Option<Address> = match &specific.receiver {
|
||||||
None => None,
|
None => None,
|
||||||
|
|||||||
Reference in New Issue
Block a user