Merge branch 'main' into security-hash-revoke_at
This commit is contained in:
@@ -45,7 +45,7 @@ sol! {
|
||||
|
||||
sol! {
|
||||
/// Permit2 — Uniswap's canonical token approval manager.
|
||||
/// Replaces per-contract ERC-20 approve() with a single approval hub.
|
||||
/// Replaces per-contract ERC-20 `approve()` with a single approval hub.
|
||||
#[derive(Debug)]
|
||||
interface IPermit2 {
|
||||
struct TokenPermissions {
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
pub mod abi;
|
||||
pub mod safe_signer;
|
||||
|
||||
use alloy::primitives::Address;
|
||||
use alloy::{
|
||||
consensus::TxEip1559,
|
||||
primitives::{TxKind, U256},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use diesel::{
|
||||
ExpressionMethods as _, OptionalExtension, QueryDsl as _, QueryResult, SelectableHelper,
|
||||
insert_into, sqlite::Sqlite, update,
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use kameo::actor::ActorRef;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::KeyHolder,
|
||||
actors::vault::Vault,
|
||||
crypto::integrity,
|
||||
db::{
|
||||
self, DatabaseError,
|
||||
@@ -33,6 +20,19 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
consensus::TxEip1559,
|
||||
primitives::{Address, TxKind, U256},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use diesel::{
|
||||
ExpressionMethods as _, OptionalExtension, QueryDsl as _, QueryResult, SelectableHelper,
|
||||
insert_into, sqlite::Sqlite, update,
|
||||
};
|
||||
|
||||
pub mod abi;
|
||||
pub mod safe_signer;
|
||||
|
||||
pub mod policies;
|
||||
mod utils;
|
||||
|
||||
@@ -40,7 +40,7 @@ mod utils;
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PolicyError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] crate::db::DatabaseError),
|
||||
Database(#[from] DatabaseError),
|
||||
#[error("Transaction violates policy: {0:?}")]
|
||||
Violations(Vec<EvalViolation>),
|
||||
#[error("No matching grant found")]
|
||||
@@ -72,7 +72,7 @@ pub enum AnalyzeError {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ListError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] crate::db::DatabaseError),
|
||||
Database(#[from] DatabaseError),
|
||||
|
||||
#[error("Integrity verification failed for grant")]
|
||||
Integrity(#[from] integrity::Error),
|
||||
@@ -133,7 +133,7 @@ async fn check_shared_constraints(
|
||||
.get_result(conn)
|
||||
.await?;
|
||||
|
||||
if count >= rate_limit.count as i64 {
|
||||
if count >= rate_limit.count.into() {
|
||||
violations.push(EvalViolation::RateLimitExceeded);
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ async fn check_shared_constraints(
|
||||
// Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed
|
||||
pub struct Engine {
|
||||
db: db::DatabasePool,
|
||||
keyholder: ActorRef<KeyHolder>,
|
||||
vault: ActorRef<Vault>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -164,7 +164,7 @@ impl Engine {
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||
|
||||
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||
integrity::verify_entity(&mut conn, &self.vault, &grant.settings, grant.id).await?;
|
||||
|
||||
let mut violations = check_shared_constraints(
|
||||
&context,
|
||||
@@ -185,24 +185,22 @@ impl Engine {
|
||||
}
|
||||
|
||||
if run_kind == RunKind::Execution {
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
let log_id: i32 = insert_into(evm_transaction_log::table)
|
||||
.values(&NewEvmTransactionLog {
|
||||
grant_id: grant.common_settings_id,
|
||||
wallet_access_id: context.target.id,
|
||||
chain_id: context.chain as i32,
|
||||
eth_value: utils::u256_to_bytes(context.value).to_vec(),
|
||||
signed_at: Utc::now().into(),
|
||||
})
|
||||
.returning(evm_transaction_log::id)
|
||||
.get_result(conn)
|
||||
.await?;
|
||||
conn.transaction(async |conn| {
|
||||
let log_id: i32 = insert_into(evm_transaction_log::table)
|
||||
.values(&NewEvmTransactionLog {
|
||||
grant_id: grant.common_settings_id,
|
||||
wallet_access_id: context.target.id,
|
||||
chain_id: context.chain.into(),
|
||||
eth_value: utils::u256_to_bytes(context.value).to_vec(),
|
||||
signed_at: Utc::now().into(),
|
||||
})
|
||||
.returning(evm_transaction_log::id)
|
||||
.get_result(&mut *conn)
|
||||
.await?;
|
||||
|
||||
P::record_transaction(&context, meaning, log_id, &grant, conn).await?;
|
||||
P::record_transaction(&context, meaning, log_id, &grant, &mut *conn).await?;
|
||||
|
||||
QueryResult::Ok(())
|
||||
})
|
||||
QueryResult::Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
@@ -213,8 +211,8 @@ impl Engine {
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(db: db::DatabasePool, keyholder: ActorRef<KeyHolder>) -> Self {
|
||||
Self { db, keyholder }
|
||||
pub const fn new(db: db::DatabasePool, vault: ActorRef<Vault>) -> Self {
|
||||
Self { db, vault }
|
||||
}
|
||||
|
||||
pub async fn create_grant<P: Policy>(
|
||||
@@ -225,51 +223,55 @@ impl Engine {
|
||||
P::Settings: Clone,
|
||||
{
|
||||
let mut conn = self.db.get().await?;
|
||||
let keyholder = self.keyholder.clone();
|
||||
let vault = self.vault.clone();
|
||||
|
||||
let id = conn
|
||||
.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
use schema::evm_basic_grant;
|
||||
.transaction(async |conn| {
|
||||
use schema::evm_basic_grant;
|
||||
|
||||
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
|
||||
.values(&NewEvmBasicGrant {
|
||||
chain_id: full_grant.shared.chain as i32,
|
||||
wallet_access_id: full_grant.shared.wallet_access_id,
|
||||
valid_from: full_grant.shared.valid_from.map(SqliteTimestamp),
|
||||
valid_until: full_grant.shared.valid_until.map(SqliteTimestamp),
|
||||
max_gas_fee_per_gas: full_grant
|
||||
.shared
|
||||
.max_gas_fee_per_gas
|
||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||
max_priority_fee_per_gas: full_grant
|
||||
.shared
|
||||
.max_priority_fee_per_gas
|
||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||
rate_limit_count: full_grant
|
||||
.shared
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|rl| rl.count as i32),
|
||||
rate_limit_window_secs: full_grant
|
||||
.shared
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|rl| rl.window.num_seconds() as i32),
|
||||
revoked_at: None,
|
||||
})
|
||||
.returning(evm_basic_grant::all_columns)
|
||||
.get_result(conn)
|
||||
.await?;
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::as_conversions,
|
||||
reason = "fixme! #86"
|
||||
)]
|
||||
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
|
||||
.values(&NewEvmBasicGrant {
|
||||
chain_id: full_grant.shared.chain.into(),
|
||||
wallet_access_id: full_grant.shared.wallet_access_id,
|
||||
valid_from: full_grant.shared.valid_from.map(SqliteTimestamp),
|
||||
valid_until: full_grant.shared.valid_until.map(SqliteTimestamp),
|
||||
max_gas_fee_per_gas: full_grant
|
||||
.shared
|
||||
.max_gas_fee_per_gas
|
||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||
max_priority_fee_per_gas: full_grant
|
||||
.shared
|
||||
.max_priority_fee_per_gas
|
||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||
rate_limit_count: full_grant
|
||||
.shared
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|rl| rl.count as i32),
|
||||
rate_limit_window_secs: full_grant
|
||||
.shared
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|rl| rl.window.num_seconds() as i32),
|
||||
revoked_at: None,
|
||||
})
|
||||
.returning(evm_basic_grant::all_columns)
|
||||
.get_result(&mut *conn)
|
||||
.await?;
|
||||
|
||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||
P::create_grant(&basic_grant, &full_grant.specific, &mut *conn).await?;
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
integrity::sign_entity(&mut *conn, &vault, &full_grant, basic_grant.id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
QueryResult::Ok(basic_grant.id)
|
||||
})
|
||||
QueryResult::Ok(basic_grant.id)
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -281,143 +283,141 @@ impl Engine {
|
||||
basic_grant_id: i32,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
let keyholder = self.keyholder.clone();
|
||||
let vault = self.vault.clone();
|
||||
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
use crate::db::schema::{
|
||||
evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target,
|
||||
evm_ether_transfer_limit, evm_token_transfer_grant,
|
||||
evm_token_transfer_volume_limit,
|
||||
conn.transaction(async move |conn| {
|
||||
use crate::db::schema::{
|
||||
evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target,
|
||||
evm_ether_transfer_limit, evm_token_transfer_grant,
|
||||
evm_token_transfer_volume_limit,
|
||||
};
|
||||
|
||||
update(evm_basic_grant::table)
|
||||
.filter(evm_basic_grant::id.eq(basic_grant_id))
|
||||
.set(evm_basic_grant::revoked_at.eq(SqliteTimestamp(Utc::now())))
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
let basic_grant: EvmBasicGrant = evm_basic_grant::table
|
||||
.filter(evm_basic_grant::id.eq(basic_grant_id))
|
||||
.select(EvmBasicGrant::as_select())
|
||||
.first(&mut *conn)
|
||||
.await?;
|
||||
|
||||
let shared = SharedGrantSettings::try_from_model(basic_grant)?;
|
||||
|
||||
if let Some(ether_grant) = evm_ether_transfer_grant::table
|
||||
.filter(evm_ether_transfer_grant::basic_grant_id.eq(basic_grant_id))
|
||||
.select(EvmEtherTransferGrant::as_select())
|
||||
.first(&mut *conn)
|
||||
.await
|
||||
.optional()?
|
||||
{
|
||||
let target_rows: Vec<EvmEtherTransferGrantTarget> =
|
||||
evm_ether_transfer_grant_target::table
|
||||
.filter(evm_ether_transfer_grant_target::grant_id.eq(ether_grant.id))
|
||||
.select(EvmEtherTransferGrantTarget::as_select())
|
||||
.load(&mut *conn)
|
||||
.await?;
|
||||
let targets: Vec<Address> = target_rows
|
||||
.into_iter()
|
||||
.filter_map(|target| {
|
||||
let arr: [u8; 20] = target.address.try_into().ok()?;
|
||||
Some(Address::from(arr))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let limit: EvmEtherTransferLimit = evm_ether_transfer_limit::table
|
||||
.filter(evm_ether_transfer_limit::id.eq(ether_grant.limit_id))
|
||||
.select(EvmEtherTransferLimit::as_select())
|
||||
.first(&mut *conn)
|
||||
.await?;
|
||||
|
||||
let settings = CombinedSettings {
|
||||
shared: shared.clone(),
|
||||
specific: policies::ether_transfer::Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|
||||
|err| {
|
||||
diesel::result::Error::DeserializationError(Box::new(err))
|
||||
},
|
||||
)?,
|
||||
window: chrono::Duration::seconds(limit.window_secs.into()),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
update(evm_basic_grant::table)
|
||||
.filter(evm_basic_grant::id.eq(basic_grant_id))
|
||||
.set(evm_basic_grant::revoked_at.eq(SqliteTimestamp(Utc::now())))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
|
||||
let basic_grant: EvmBasicGrant = evm_basic_grant::table
|
||||
.filter(evm_basic_grant::id.eq(basic_grant_id))
|
||||
.select(EvmBasicGrant::as_select())
|
||||
.first(conn)
|
||||
.await?;
|
||||
|
||||
let shared = SharedGrantSettings::try_from_model(basic_grant)?;
|
||||
|
||||
if let Some(ether_grant) = evm_ether_transfer_grant::table
|
||||
.filter(evm_ether_transfer_grant::basic_grant_id.eq(basic_grant_id))
|
||||
.select(EvmEtherTransferGrant::as_select())
|
||||
.first(conn)
|
||||
integrity::sign_entity(&mut *conn, &vault, &settings, basic_grant_id)
|
||||
.await
|
||||
.optional()?
|
||||
{
|
||||
let target_rows: Vec<EvmEtherTransferGrantTarget> =
|
||||
evm_ether_transfer_grant_target::table
|
||||
.filter(evm_ether_transfer_grant_target::grant_id.eq(ether_grant.id))
|
||||
.select(EvmEtherTransferGrantTarget::as_select())
|
||||
.load(conn)
|
||||
.await?;
|
||||
let targets: Vec<Address> = target_rows
|
||||
.into_iter()
|
||||
.filter_map(|target| {
|
||||
let arr: [u8; 20] = target.address.try_into().ok()?;
|
||||
Some(Address::from(arr))
|
||||
})
|
||||
.collect();
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
let limit: EvmEtherTransferLimit = evm_ether_transfer_limit::table
|
||||
.filter(evm_ether_transfer_limit::id.eq(ether_grant.limit_id))
|
||||
.select(EvmEtherTransferLimit::as_select())
|
||||
.first(conn)
|
||||
return QueryResult::Ok(());
|
||||
}
|
||||
|
||||
if let Some(token_grant) = evm_token_transfer_grant::table
|
||||
.filter(evm_token_transfer_grant::basic_grant_id.eq(basic_grant_id))
|
||||
.select(EvmTokenTransferGrant::as_select())
|
||||
.first(&mut *conn)
|
||||
.await
|
||||
.optional()?
|
||||
{
|
||||
let volume_limit_rows: Vec<EvmTokenTransferVolumeLimit> =
|
||||
evm_token_transfer_volume_limit::table
|
||||
.filter(evm_token_transfer_volume_limit::grant_id.eq(token_grant.id))
|
||||
.select(EvmTokenTransferVolumeLimit::as_select())
|
||||
.load(&mut *conn)
|
||||
.await?;
|
||||
|
||||
let settings = CombinedSettings {
|
||||
shared: shared.clone(),
|
||||
specific: crate::evm::policies::ether_transfer::Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|
||||
|err| {
|
||||
diesel::result::Error::DeserializationError(Box::new(err))
|
||||
},
|
||||
)?,
|
||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &settings, basic_grant_id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
return QueryResult::Ok(());
|
||||
}
|
||||
|
||||
if let Some(token_grant) = evm_token_transfer_grant::table
|
||||
.filter(evm_token_transfer_grant::basic_grant_id.eq(basic_grant_id))
|
||||
.select(EvmTokenTransferGrant::as_select())
|
||||
.first(conn)
|
||||
.await
|
||||
.optional()?
|
||||
{
|
||||
let volume_limit_rows: Vec<EvmTokenTransferVolumeLimit> =
|
||||
evm_token_transfer_volume_limit::table
|
||||
.filter(evm_token_transfer_volume_limit::grant_id.eq(token_grant.id))
|
||||
.select(EvmTokenTransferVolumeLimit::as_select())
|
||||
.load(conn)
|
||||
.await?;
|
||||
let volume_limits: Vec<VolumeRateLimit> = volume_limit_rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
Ok(VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(
|
||||
|err| {
|
||||
diesel::result::Error::DeserializationError(Box::new(err))
|
||||
},
|
||||
)?,
|
||||
window: chrono::Duration::seconds(row.window_secs as i64),
|
||||
})
|
||||
let volume_limits: Vec<VolumeRateLimit> = volume_limit_rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
Ok(VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(
|
||||
|err| {
|
||||
diesel::result::Error::DeserializationError(Box::new(err))
|
||||
},
|
||||
)?,
|
||||
window: chrono::Duration::seconds(row.window_secs.into()),
|
||||
})
|
||||
.collect::<QueryResult<Vec<_>>>()?;
|
||||
})
|
||||
.collect::<QueryResult<Vec<_>>>()?;
|
||||
|
||||
let target: Option<Address> = match token_grant.receiver {
|
||||
None => None,
|
||||
Some(bytes) => {
|
||||
let arr: [u8; 20] = bytes.try_into().map_err(|_| {
|
||||
diesel::result::Error::DeserializationError(
|
||||
"Invalid receiver address length".into(),
|
||||
)
|
||||
})?;
|
||||
Some(Address::from(arr))
|
||||
}
|
||||
};
|
||||
|
||||
let token_contract: [u8; 20] =
|
||||
token_grant.token_contract.clone().try_into().map_err(|_| {
|
||||
let target: Option<Address> = match token_grant.receiver {
|
||||
None => None,
|
||||
Some(bytes) => {
|
||||
let arr: [u8; 20] = bytes.try_into().map_err(|_| {
|
||||
diesel::result::Error::DeserializationError(
|
||||
"Invalid token contract address length".into(),
|
||||
"Invalid receiver address length".into(),
|
||||
)
|
||||
})?;
|
||||
Some(Address::from(arr))
|
||||
}
|
||||
};
|
||||
|
||||
let settings = CombinedSettings {
|
||||
shared,
|
||||
specific: crate::evm::policies::token_transfers::Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
},
|
||||
};
|
||||
let token_contract: [u8; 20] =
|
||||
token_grant.token_contract.clone().try_into().map_err(|_| {
|
||||
diesel::result::Error::DeserializationError(
|
||||
"Invalid token contract address length".into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &settings, basic_grant_id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
let settings = CombinedSettings {
|
||||
shared,
|
||||
specific: policies::token_transfers::Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
},
|
||||
};
|
||||
|
||||
return QueryResult::Ok(());
|
||||
}
|
||||
integrity::sign_entity(&mut *conn, &vault, &settings, basic_grant_id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
Err(diesel::result::Error::NotFound)
|
||||
})
|
||||
return QueryResult::Ok(());
|
||||
}
|
||||
|
||||
Err(diesel::result::Error::NotFound)
|
||||
})
|
||||
.await
|
||||
.map_err(DatabaseError::from)
|
||||
@@ -436,7 +436,7 @@ impl Engine {
|
||||
|
||||
// Verify integrity of all grants before returning any results
|
||||
for grant in &all_grants {
|
||||
integrity::verify_entity(conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||
integrity::verify_entity(conn, &self.vault, &grant.settings, grant.id).await?;
|
||||
}
|
||||
|
||||
Ok(all_grants.into_iter().map(|g| Grant {
|
||||
@@ -466,7 +466,7 @@ impl Engine {
|
||||
let TxKind::Call(to) = transaction.to else {
|
||||
return Err(VetError::ContractCreationNotSupported);
|
||||
};
|
||||
let context = policies::EvalContext {
|
||||
let context = EvalContext {
|
||||
target,
|
||||
chain: transaction.chain_id,
|
||||
to,
|
||||
@@ -509,7 +509,7 @@ mod tests {
|
||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::actors::keyholder::{Bootstrap, KeyHolder};
|
||||
use crate::actors::{GlobalActors, vault::{Bootstrap, Vault}};
|
||||
use crate::crypto::integrity;
|
||||
use crate::db::{
|
||||
self, DatabaseConnection,
|
||||
@@ -564,10 +564,16 @@ mod tests {
|
||||
conn: &mut DatabaseConnection,
|
||||
shared: &SharedGrantSettings,
|
||||
) -> EvmBasicGrant {
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::as_conversions,
|
||||
reason = "fixme! #86"
|
||||
)]
|
||||
insert_into(evm_basic_grant::table)
|
||||
.values(NewEvmBasicGrant {
|
||||
wallet_access_id: shared.wallet_access_id,
|
||||
chain_id: shared.chain as i32,
|
||||
chain_id: shared.chain.into(),
|
||||
valid_from: shared.valid_from.map(SqliteTimestamp),
|
||||
valid_until: shared.valid_until.map(SqliteTimestamp),
|
||||
max_gas_fee_per_gas: shared
|
||||
@@ -731,7 +737,7 @@ mod tests {
|
||||
.values(NewEvmTransactionLog {
|
||||
grant_id: basic_grant.id,
|
||||
wallet_access_id: WALLET_ACCESS_ID,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
eth_value: super::utils::u256_to_bytes(U256::ZERO).to_vec(),
|
||||
signed_at: SqliteTimestamp(Utc::now()),
|
||||
})
|
||||
@@ -757,8 +763,12 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
|
||||
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
|
||||
let actor = Vault::spawn(
|
||||
Vault::new(db.clone(), GlobalActors::spawn_message_bus())
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
actor
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
||||
@@ -774,8 +784,8 @@ mod tests {
|
||||
use diesel::ExpressionMethods as _;
|
||||
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let engine = super::Engine::new(db.clone(), keyholder.clone());
|
||||
let vault = bootstrapped_vault(&db).await;
|
||||
let engine = super::Engine::new(db.clone(), vault.clone());
|
||||
|
||||
let full_grant = CombinedSettings {
|
||||
shared: SharedGrantSettings {
|
||||
@@ -788,7 +798,7 @@ mod tests {
|
||||
max_priority_fee_per_gas: None,
|
||||
rate_limit: None,
|
||||
},
|
||||
specific: crate::evm::policies::ether_transfer::Settings {
|
||||
specific: super::policies::ether_transfer::Settings {
|
||||
target: vec![RECIPIENT],
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: U256::from(100u64),
|
||||
@@ -828,7 +838,7 @@ mod tests {
|
||||
max_priority_fee_per_gas: 1,
|
||||
};
|
||||
|
||||
let grant = crate::evm::policies::ether_transfer::EtherTransfer::try_find_grant(
|
||||
let grant = EtherTransfer::try_find_grant(
|
||||
&context, &mut conn,
|
||||
)
|
||||
.await
|
||||
@@ -836,11 +846,11 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
integrity::verify_entity(&mut conn, &keyholder, &grant.settings, grant.id).await;
|
||||
integrity::verify_entity(&mut conn, &vault, &grant.settings, grant.id).await;
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(crate::crypto::integrity::Error::MacMismatch { .. })
|
||||
Err(integrity::Error::MacMismatch { .. })
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::fmt::Display;
|
||||
use crate::{
|
||||
crypto::integrity::v1::Integrable,
|
||||
db::models::{EvmBasicGrant, EvmWalletAccess},
|
||||
evm::utils,
|
||||
};
|
||||
|
||||
use alloy::primitives::{Address, Bytes, ChainId, U256};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
@@ -6,15 +10,9 @@ use diesel::{
|
||||
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
crypto::integrity::v1::Integrable,
|
||||
db::models::{self, EvmBasicGrant, EvmWalletAccess},
|
||||
evm::utils,
|
||||
};
|
||||
|
||||
pub mod ether_transfer;
|
||||
pub mod token_transfers;
|
||||
|
||||
@@ -87,10 +85,10 @@ pub trait Policy: Sized {
|
||||
|
||||
// Create a new grant in the database based on the provided grant details, and return its ID
|
||||
fn create_grant(
|
||||
basic: &models::EvmBasicGrant,
|
||||
basic: &EvmBasicGrant,
|
||||
grant: &Self::Settings,
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> impl std::future::Future<Output = QueryResult<DatabaseID>> + Send;
|
||||
) -> impl Future<Output = QueryResult<DatabaseID>> + Send;
|
||||
|
||||
// Try to find an existing grant that matches the transaction context, and return its details if found
|
||||
// Additionally, return ID of basic grant for shared-logic checks like rate limits and validity periods
|
||||
@@ -158,7 +156,7 @@ impl SharedGrantSettings {
|
||||
pub(crate) fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
|
||||
Ok(Self {
|
||||
wallet_access_id: model.wallet_access_id,
|
||||
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
|
||||
chain: model.chain_id.into(),
|
||||
valid_from: model.valid_from.map(Into::into),
|
||||
valid_until: model.valid_until.map(Into::into),
|
||||
revoked_at: model.revoked_at.map(Into::into),
|
||||
@@ -170,10 +168,11 @@ impl SharedGrantSettings {
|
||||
.max_priority_fee_per_gas
|
||||
.map(|b| utils::try_bytes_to_u256(&b))
|
||||
.transpose()?,
|
||||
#[expect(clippy::cast_sign_loss, clippy::as_conversions, reason = "fixme! #86")]
|
||||
rate_limit: match (model.rate_limit_count, model.rate_limit_window_secs) {
|
||||
(Some(count), Some(window_secs)) => Some(TransactionRateLimit {
|
||||
count: count as u32,
|
||||
window: Duration::seconds(window_secs as i64),
|
||||
window: Duration::seconds(window_secs.into()),
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
@@ -183,7 +182,7 @@ impl SharedGrantSettings {
|
||||
pub async fn query_by_id(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
id: i32,
|
||||
) -> diesel::result::QueryResult<Self> {
|
||||
) -> QueryResult<Self> {
|
||||
use crate::db::schema::evm_basic_grant;
|
||||
|
||||
let basic_grant: EvmBasicGrant = evm_basic_grant::table
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use alloy::primitives::{Address, U256};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use diesel::dsl::{auto_type, insert_into};
|
||||
use diesel::sqlite::Sqlite;
|
||||
use diesel::{ExpressionMethods, JoinOnDsl, prelude::*};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
|
||||
use crate::crypto::integrity::v1::Integrable;
|
||||
use crate::db::models::{
|
||||
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
|
||||
NewEvmEtherTransferLimit, SqliteTimestamp,
|
||||
};
|
||||
use crate::db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log};
|
||||
use crate::evm::policies::{
|
||||
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
||||
};
|
||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||
use crate::{
|
||||
crypto::integrity::v1::Integrable,
|
||||
db::models::{
|
||||
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
|
||||
NewEvmEtherTransferLimit, SqliteTimestamp,
|
||||
},
|
||||
db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log},
|
||||
db::{
|
||||
models::{self, NewEvmEtherTransferGrant, NewEvmEtherTransferGrantTarget},
|
||||
models::{NewEvmEtherTransferGrant, NewEvmEtherTransferGrantTarget},
|
||||
schema::{evm_ether_transfer_grant, evm_ether_transfer_grant_target},
|
||||
},
|
||||
evm::policies::{
|
||||
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||
VolumeRateLimit,
|
||||
},
|
||||
evm::{policies::Policy, utils},
|
||||
};
|
||||
|
||||
use alloy::primitives::{Address, U256};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use diesel::{
|
||||
dsl::{auto_type, insert_into},
|
||||
prelude::*,
|
||||
sqlite::Sqlite,
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
#[auto_type]
|
||||
fn grant_join() -> _ {
|
||||
evm_ether_transfer_grant::table.inner_join(
|
||||
@@ -32,8 +34,6 @@ fn grant_join() -> _ {
|
||||
)
|
||||
}
|
||||
|
||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||
|
||||
// Plain ether transfer
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Meaning {
|
||||
@@ -46,8 +46,8 @@ impl Display for Meaning {
|
||||
}
|
||||
}
|
||||
impl From<Meaning> for SpecificMeaning {
|
||||
fn from(val: Meaning) -> SpecificMeaning {
|
||||
SpecificMeaning::EtherTransfer(val)
|
||||
fn from(val: Meaning) -> Self {
|
||||
Self::EtherTransfer(val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ impl Integrable for Settings {
|
||||
}
|
||||
|
||||
impl From<Settings> for SpecificGrant {
|
||||
fn from(val: Settings) -> SpecificGrant {
|
||||
SpecificGrant::EtherTransfer(val)
|
||||
fn from(val: Settings) -> Self {
|
||||
Self::EtherTransfer(val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,7 @@ async fn query_relevant_past_transaction(
|
||||
) -> QueryResult<Vec<(U256, DateTime<Utc>)>> {
|
||||
let past_transactions: Vec<(Vec<u8>, SqliteTimestamp)> = evm_transaction_log::table
|
||||
.filter(evm_transaction_log::grant_id.eq(grant_id))
|
||||
.filter(
|
||||
evm_transaction_log::signed_at.ge(SqliteTimestamp(chrono::Utc::now() - longest_window)),
|
||||
)
|
||||
.filter(evm_transaction_log::signed_at.ge(SqliteTimestamp(Utc::now() - longest_window)))
|
||||
.select((
|
||||
evm_transaction_log::eth_value,
|
||||
evm_transaction_log::signed_at,
|
||||
@@ -103,7 +101,7 @@ async fn check_rate_limits(
|
||||
|
||||
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
|
||||
|
||||
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
||||
let window_start = Utc::now() - grant.settings.specific.limit.window;
|
||||
let prospective_cumulative_volume: U256 = past_transaction
|
||||
.iter()
|
||||
.filter(|(_, timestamp)| timestamp >= &window_start)
|
||||
@@ -153,10 +151,15 @@ impl Policy for EtherTransfer {
|
||||
}
|
||||
|
||||
async fn create_grant(
|
||||
basic: &models::EvmBasicGrant,
|
||||
basic: &EvmBasicGrant,
|
||||
grant: &Self::Settings,
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> diesel::result::QueryResult<DatabaseID> {
|
||||
) -> QueryResult<DatabaseID> {
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::as_conversions,
|
||||
reason = "fixme! #86"
|
||||
)]
|
||||
let limit_id: i32 = insert_into(evm_ether_transfer_limit::table)
|
||||
.values(NewEvmEtherTransferLimit {
|
||||
window_secs: grant.limit.window.num_seconds() as i32,
|
||||
@@ -191,7 +194,7 @@ impl Policy for EtherTransfer {
|
||||
async fn try_find_grant(
|
||||
context: &EvalContext,
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> diesel::result::QueryResult<Option<Grant<Self::Settings>>> {
|
||||
) -> QueryResult<Option<Grant<Self::Settings>>> {
|
||||
let target_bytes = context.to.to_vec();
|
||||
|
||||
// Find a grant where:
|
||||
@@ -245,7 +248,7 @@ impl Policy for EtherTransfer {
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume)
|
||||
.map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?,
|
||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||
window: Duration::seconds(limit.window_secs.into()),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -265,7 +268,7 @@ impl Policy for EtherTransfer {
|
||||
_log_id: i32,
|
||||
_grant: &Grant<Self::Settings>,
|
||||
_conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> diesel::result::QueryResult<()> {
|
||||
) -> QueryResult<()> {
|
||||
// Basic log is sufficient
|
||||
|
||||
Ok(())
|
||||
@@ -318,7 +321,7 @@ impl Policy for EtherTransfer {
|
||||
.map(|(basic, specific)| {
|
||||
let targets: Vec<Address> = targets_by_grant
|
||||
.get(&specific.id)
|
||||
.map(|v| v.as_slice())
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
@@ -342,7 +345,7 @@ impl Policy for EtherTransfer {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|
||||
|e| diesel::result::Error::DeserializationError(Box::new(e)),
|
||||
)?,
|
||||
window: Duration::seconds(limit.window_secs as i64),
|
||||
window: Duration::seconds(limit.window_secs.into()),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
use super::{EtherTransfer, Settings};
|
||||
use crate::{
|
||||
db::{
|
||||
self, DatabaseConnection,
|
||||
models::{
|
||||
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
|
||||
},
|
||||
schema::{evm_basic_grant, evm_transaction_log},
|
||||
},
|
||||
evm::{
|
||||
policies::{
|
||||
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||
VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
},
|
||||
};
|
||||
|
||||
use alloy::primitives::{Address, Bytes, U256, address};
|
||||
use chrono::{Duration, Utc};
|
||||
use diesel::{SelectableHelper, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
use crate::db::{
|
||||
self, DatabaseConnection,
|
||||
models::{
|
||||
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
|
||||
},
|
||||
schema::{evm_basic_grant, evm_transaction_log},
|
||||
};
|
||||
use crate::evm::{
|
||||
policies::{
|
||||
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||
VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
|
||||
use super::{EtherTransfer, Settings};
|
||||
|
||||
const WALLET_ACCESS_ID: i32 = 1;
|
||||
const CHAIN_ID: u64 = 1;
|
||||
const CHAIN_ID: alloy::primitives::ChainId = 1;
|
||||
|
||||
const ALLOWED: Address = address!("1111111111111111111111111111111111111111");
|
||||
const OTHER: Address = address!("2222222222222222222222222222222222222222");
|
||||
@@ -47,7 +48,7 @@ async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicG
|
||||
insert_into(evm_basic_grant::table)
|
||||
.values(NewEvmBasicGrant {
|
||||
wallet_access_id: WALLET_ACCESS_ID,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
max_gas_fee_per_gas: None,
|
||||
@@ -161,7 +162,7 @@ async fn evaluate_passes_when_volume_within_limit() {
|
||||
.values(NewEvmTransactionLog {
|
||||
grant_id,
|
||||
wallet_access_id: WALLET_ACCESS_ID,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
eth_value: utils::u256_to_bytes(U256::from(500u64)).to_vec(),
|
||||
signed_at: SqliteTimestamp(Utc::now()),
|
||||
})
|
||||
@@ -203,7 +204,7 @@ async fn evaluate_rejects_volume_over_limit() {
|
||||
.values(NewEvmTransactionLog {
|
||||
grant_id,
|
||||
wallet_access_id: WALLET_ACCESS_ID,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
|
||||
signed_at: SqliteTimestamp(Utc::now()),
|
||||
})
|
||||
@@ -246,7 +247,7 @@ async fn evaluate_passes_at_exactly_volume_limit() {
|
||||
.values(NewEvmTransactionLog {
|
||||
grant_id,
|
||||
wallet_access_id: WALLET_ACCESS_ID,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
eth_value: utils::u256_to_bytes(U256::from(900u64)).to_vec(),
|
||||
signed_at: SqliteTimestamp(Utc::now()),
|
||||
})
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::schema::{
|
||||
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
|
||||
evm_token_transfer_volume_limit,
|
||||
};
|
||||
use crate::evm::{
|
||||
abi::IERC20::transferCall,
|
||||
policies::{
|
||||
Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||
use crate::{
|
||||
crypto::integrity::Integrable,
|
||||
db::models::{
|
||||
@@ -18,20 +6,33 @@ use crate::{
|
||||
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
|
||||
SqliteTimestamp,
|
||||
},
|
||||
db::schema::{
|
||||
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
|
||||
evm_token_transfer_volume_limit,
|
||||
},
|
||||
evm::policies::CombinedSettings,
|
||||
evm::{
|
||||
abi::IERC20::transferCall,
|
||||
policies::{
|
||||
Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
},
|
||||
};
|
||||
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
|
||||
|
||||
use alloy::{
|
||||
primitives::{Address, U256},
|
||||
sol_types::SolCall,
|
||||
};
|
||||
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use diesel::dsl::{auto_type, insert_into};
|
||||
use diesel::sqlite::Sqlite;
|
||||
use diesel::{ExpressionMethods, prelude::*};
|
||||
use diesel::{
|
||||
dsl::{auto_type, insert_into},
|
||||
prelude::*,
|
||||
sqlite::Sqlite,
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
|
||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[auto_type]
|
||||
fn grant_join() -> _ {
|
||||
@@ -56,8 +57,8 @@ impl std::fmt::Display for Meaning {
|
||||
}
|
||||
}
|
||||
impl From<Meaning> for SpecificMeaning {
|
||||
fn from(val: Meaning) -> SpecificMeaning {
|
||||
SpecificMeaning::TokenTransfer(val)
|
||||
fn from(val: Meaning) -> Self {
|
||||
Self::TokenTransfer(val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +74,8 @@ impl Integrable for Settings {
|
||||
}
|
||||
|
||||
impl From<Settings> for SpecificGrant {
|
||||
fn from(val: Settings) -> SpecificGrant {
|
||||
SpecificGrant::TokenTransfer(val)
|
||||
fn from(val: Settings) -> Self {
|
||||
Self::TokenTransfer(val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +86,7 @@ async fn query_relevant_past_transfers(
|
||||
) -> QueryResult<Vec<(U256, DateTime<Utc>)>> {
|
||||
let past_logs: Vec<(Vec<u8>, SqliteTimestamp)> = evm_token_transfer_log::table
|
||||
.filter(evm_token_transfer_log::grant_id.eq(grant_id))
|
||||
.filter(
|
||||
evm_token_transfer_log::created_at
|
||||
.ge(SqliteTimestamp(chrono::Utc::now() - longest_window)),
|
||||
)
|
||||
.filter(evm_token_transfer_log::created_at.ge(SqliteTimestamp(Utc::now() - longest_window)))
|
||||
.select((
|
||||
evm_token_transfer_log::value,
|
||||
evm_token_transfer_log::created_at,
|
||||
@@ -128,7 +126,7 @@ async fn check_volume_rate_limits(
|
||||
let past_transfers = query_relevant_past_transfers(grant.id, longest_window, db).await?;
|
||||
|
||||
for limit in &grant.settings.specific.volume_limits {
|
||||
let window_start = chrono::Utc::now() - limit.window;
|
||||
let window_start = Utc::now() - limit.window;
|
||||
let prospective_cumulative_volume: U256 = past_transfers
|
||||
.iter()
|
||||
.filter(|(_, timestamp)| timestamp >= &window_start)
|
||||
@@ -204,6 +202,11 @@ impl Policy for TokenTransfer {
|
||||
.await?;
|
||||
|
||||
for limit in &grant.volume_limits {
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::as_conversions,
|
||||
reason = "fixme! #86"
|
||||
)]
|
||||
insert_into(evm_token_transfer_volume_limit::table)
|
||||
.values(NewEvmTokenTransferVolumeLimit {
|
||||
grant_id,
|
||||
@@ -253,7 +256,7 @@ impl Policy for TokenTransfer {
|
||||
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(|err| {
|
||||
diesel::result::Error::DeserializationError(Box::new(err))
|
||||
})?,
|
||||
window: Duration::seconds(row.window_secs as i64),
|
||||
window: Duration::seconds(row.window_secs.into()),
|
||||
})
|
||||
})
|
||||
.collect::<QueryResult<Vec<_>>>()?;
|
||||
@@ -303,7 +306,7 @@ impl Policy for TokenTransfer {
|
||||
.values(NewEvmTokenTransferLog {
|
||||
grant_id: grant.id,
|
||||
log_id,
|
||||
chain_id: context.chain as i32,
|
||||
chain_id: context.chain.into(),
|
||||
token_contract: context.to.to_vec(),
|
||||
recipient_address: meaning.to.to_vec(),
|
||||
value: utils::u256_to_bytes(meaning.value).to_vec(),
|
||||
@@ -352,7 +355,7 @@ impl Policy for TokenTransfer {
|
||||
.map(|(basic, specific)| {
|
||||
let volume_limits: Vec<VolumeRateLimit> = limits_by_grant
|
||||
.get(&specific.id)
|
||||
.map(|v| v.as_slice())
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|row| {
|
||||
@@ -360,7 +363,7 @@ impl Policy for TokenTransfer {
|
||||
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(|e| {
|
||||
diesel::result::Error::DeserializationError(Box::new(e))
|
||||
})?,
|
||||
window: Duration::seconds(row.window_secs as i64),
|
||||
window: Duration::seconds(row.window_secs.into()),
|
||||
})
|
||||
})
|
||||
.collect::<QueryResult<Vec<_>>>()?;
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
use alloy::primitives::{Address, Bytes, U256, address};
|
||||
use alloy::sol_types::SolCall;
|
||||
use super::{Settings, TokenTransfer};
|
||||
use crate::{
|
||||
db::{
|
||||
self, DatabaseConnection,
|
||||
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp},
|
||||
schema::evm_basic_grant,
|
||||
},
|
||||
evm::{
|
||||
abi::IERC20::transferCall,
|
||||
policies::{
|
||||
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||
VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
},
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
primitives::{Address, Bytes, U256, address},
|
||||
sol_types::SolCall,
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use diesel::{SelectableHelper, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
use crate::db::{
|
||||
self, DatabaseConnection,
|
||||
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp},
|
||||
schema::evm_basic_grant,
|
||||
};
|
||||
use crate::evm::{
|
||||
abi::IERC20::transferCall,
|
||||
policies::{
|
||||
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||
VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
|
||||
use super::{Settings, TokenTransfer};
|
||||
|
||||
// DAI on Ethereum mainnet — present in the static token registry
|
||||
const CHAIN_ID: u64 = 1;
|
||||
const DAI: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F");
|
||||
@@ -59,7 +62,7 @@ async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicG
|
||||
insert_into(evm_basic_grant::table)
|
||||
.values(NewEvmBasicGrant {
|
||||
wallet_access_id: WALLET_ACCESS_ID,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
max_gas_fee_per_gas: None,
|
||||
@@ -239,12 +242,11 @@ async fn evaluate_passes_volume_at_exact_limit() {
|
||||
.unwrap();
|
||||
|
||||
// Record a past transfer of 900, with current transfer 100 => exactly 1000 limit
|
||||
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
||||
insert_into(evm_token_transfer_log::table)
|
||||
.values(NewEvmTokenTransferLog {
|
||||
insert_into(db::schema::evm_token_transfer_log::table)
|
||||
.values(db::models::NewEvmTokenTransferLog {
|
||||
grant_id,
|
||||
log_id: 0,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
token_contract: DAI.to_vec(),
|
||||
recipient_address: RECIPIENT.to_vec(),
|
||||
value: utils::u256_to_bytes(U256::from(900u64)).to_vec(),
|
||||
@@ -284,12 +286,11 @@ async fn evaluate_rejects_volume_over_limit() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
||||
insert_into(evm_token_transfer_log::table)
|
||||
.values(NewEvmTokenTransferLog {
|
||||
insert_into(db::schema::evm_token_transfer_log::table)
|
||||
.values(db::models::NewEvmTokenTransferLog {
|
||||
grant_id,
|
||||
log_id: 0,
|
||||
chain_id: CHAIN_ID as i32,
|
||||
chain_id: CHAIN_ID.into(),
|
||||
token_contract: DAI.to_vec(),
|
||||
recipient_address: RECIPIENT.to_vec(),
|
||||
value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Mutex;
|
||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||
|
||||
use alloy::{
|
||||
consensus::SignableTransaction,
|
||||
@@ -6,9 +6,9 @@ use alloy::{
|
||||
primitives::{Address, B256, ChainId, Signature},
|
||||
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
|
||||
};
|
||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||
use async_trait::async_trait;
|
||||
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// An Ethereum signer that stores its secp256k1 secret key inside a
|
||||
/// hardware-protected [`MemSafe`] cell.
|
||||
@@ -82,8 +82,8 @@ impl SafeSigner {
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(clippy::significant_drop_tightening, reason = "false positive")]
|
||||
fn sign_hash_inner(&self, hash: &B256) -> Result<Signature> {
|
||||
#[allow(clippy::expect_used)]
|
||||
let mut cell = self.key.lock().expect("SafeSigner mutex poisoned");
|
||||
let reader = cell.read();
|
||||
let sig: (ecdsa::Signature, RecoveryId) = reader.sign_prehash(hash.as_ref())?;
|
||||
@@ -96,7 +96,6 @@ impl SafeSigner {
|
||||
{
|
||||
return Err(Error::TransactionChainIdMismatch {
|
||||
signer: chain_id,
|
||||
#[allow(clippy::expect_used)]
|
||||
tx: tx.chain_id().expect("Chain ID is guaranteed to be set"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@ use alloy::primitives::U256;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Expected {expected} bytes but got {actual} bytes")]
|
||||
pub struct LengthError {
|
||||
pub expected: usize,
|
||||
pub actual: usize,
|
||||
pub(super) struct LengthError {
|
||||
pub(super) expected: usize,
|
||||
pub(super) actual: usize,
|
||||
}
|
||||
|
||||
pub fn u256_to_bytes(value: U256) -> [u8; 32] {
|
||||
pub const fn u256_to_bytes(value: U256) -> [u8; 32] {
|
||||
value.to_le_bytes()
|
||||
}
|
||||
pub fn bytes_to_u256(bytes: &[u8]) -> Option<U256> {
|
||||
pub(super) fn bytes_to_u256(bytes: &[u8]) -> Option<U256> {
|
||||
let bytes: [u8; 32] = bytes.try_into().ok()?;
|
||||
Some(U256::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub fn try_bytes_to_u256(bytes: &[u8]) -> diesel::result::QueryResult<U256> {
|
||||
pub(super) fn try_bytes_to_u256(bytes: &[u8]) -> diesel::result::QueryResult<U256> {
|
||||
let bytes: [u8; 32] = bytes.try_into().map_err(|_| {
|
||||
diesel::result::Error::DeserializationError(Box::new(LengthError {
|
||||
expected: 32,
|
||||
|
||||
Reference in New Issue
Block a user