refactor(server): rework envelopes and integrity check
This commit is contained in:
@@ -12,6 +12,7 @@ use kameo::actor::ActorRef;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::KeyHolder,
|
||||
crypto::integrity,
|
||||
db::{
|
||||
self, DatabaseError,
|
||||
models::{
|
||||
@@ -20,11 +21,10 @@ use crate::{
|
||||
schema::{self, evm_transaction_log},
|
||||
},
|
||||
evm::policies::{
|
||||
DatabaseID, EvalContext, EvalViolation, FullGrant, Grant, Policy, SharedGrantSettings,
|
||||
DatabaseID, EvalContext, EvalViolation, Grant, Policy, CombinedSettings, SharedGrantSettings,
|
||||
SpecificGrant, SpecificMeaning, ether_transfer::EtherTransfer,
|
||||
token_transfers::TokenTransfer,
|
||||
},
|
||||
integrity,
|
||||
};
|
||||
|
||||
pub mod policies;
|
||||
@@ -63,6 +63,15 @@ pub enum AnalyzeError {
|
||||
UnsupportedTransactionType,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ListError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] crate::db::DatabaseError),
|
||||
|
||||
#[error("Integrity verification failed for grant")]
|
||||
Integrity(#[from] integrity::Error),
|
||||
}
|
||||
|
||||
/// Controls whether a transaction should be executed or only validated
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RunKind {
|
||||
@@ -141,18 +150,16 @@ impl Engine {
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||
|
||||
let signed_grant = integrity::evm::SignedEvmGrant::from_active_grant(&Grant {
|
||||
id: grant.id,
|
||||
shared_grant_id: grant.shared_grant_id,
|
||||
shared: grant.shared.clone(),
|
||||
settings: grant.settings.clone().into(),
|
||||
});
|
||||
integrity::verify_entity(&mut conn, &self.keyholder, &signed_grant).await?;
|
||||
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||
|
||||
let mut violations =
|
||||
check_shared_constraints(&context, &grant.shared, grant.shared_grant_id, &mut conn)
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
let mut violations = check_shared_constraints(
|
||||
&context,
|
||||
&grant.settings.shared,
|
||||
grant.common_settings_id,
|
||||
&mut conn,
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
violations.extend(
|
||||
P::evaluate(&context, meaning, &grant, &mut conn)
|
||||
.await
|
||||
@@ -162,13 +169,13 @@ impl Engine {
|
||||
if !violations.is_empty() {
|
||||
return Err(PolicyError::Violations(violations));
|
||||
}
|
||||
|
||||
|
||||
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.shared_grant_id,
|
||||
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(),
|
||||
@@ -198,7 +205,7 @@ impl Engine {
|
||||
|
||||
pub async fn create_grant<P: Policy>(
|
||||
&self,
|
||||
full_grant: FullGrant<P::Settings>,
|
||||
full_grant: CombinedSettings<P::Settings>,
|
||||
) -> Result<i32, DatabaseError>
|
||||
where
|
||||
P::Settings: Clone,
|
||||
@@ -213,25 +220,25 @@ impl Engine {
|
||||
|
||||
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
|
||||
.values(&NewEvmBasicGrant {
|
||||
chain_id: full_grant.basic.chain as i32,
|
||||
wallet_access_id: full_grant.basic.wallet_access_id,
|
||||
valid_from: full_grant.basic.valid_from.map(SqliteTimestamp),
|
||||
valid_until: full_grant.basic.valid_until.map(SqliteTimestamp),
|
||||
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
|
||||
.basic
|
||||
.shared
|
||||
.max_gas_fee_per_gas
|
||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||
max_priority_fee_per_gas: full_grant
|
||||
.basic
|
||||
.shared
|
||||
.max_priority_fee_per_gas
|
||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||
rate_limit_count: full_grant
|
||||
.basic
|
||||
.shared
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|rl| rl.count as i32),
|
||||
rate_limit_window_secs: full_grant
|
||||
.basic
|
||||
.shared
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|rl| rl.window.num_seconds() as i32),
|
||||
@@ -243,16 +250,14 @@ impl Engine {
|
||||
|
||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||
|
||||
let signed_grant = integrity::evm::SignedEvmGrant {
|
||||
basic_grant_id: basic_grant.id,
|
||||
shared: full_grant.basic.clone(),
|
||||
specific: full_grant.specific.clone().into(),
|
||||
revoked_at: basic_grant.revoked_at.map(Into::into),
|
||||
};
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &signed_grant)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
&keyholder,
|
||||
&full_grant,
|
||||
basic_grant.id,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
QueryResult::Ok(basic_grant.id)
|
||||
})
|
||||
@@ -262,43 +267,36 @@ impl Engine {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, DatabaseError> {
|
||||
let mut conn = self.db.get().await?;
|
||||
async fn list_one_kind<Kind: Policy, Y>(
|
||||
&self,
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> Result<impl Iterator<Item = Grant<Y>>, ListError>
|
||||
where
|
||||
Y: From<Kind::Settings>,
|
||||
{
|
||||
let all_grants = Kind::find_all_grants(conn)
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
|
||||
// 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?;
|
||||
}
|
||||
|
||||
Ok(all_grants.into_iter().map(|g| Grant {
|
||||
id: g.id,
|
||||
common_settings_id: g.common_settings_id,
|
||||
settings: g.settings.generalize(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
|
||||
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();
|
||||
|
||||
grants.extend(
|
||||
EtherTransfer::find_all_grants(&mut conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|g| Grant {
|
||||
id: g.id,
|
||||
shared_grant_id: g.shared_grant_id,
|
||||
shared: g.shared,
|
||||
settings: SpecificGrant::EtherTransfer(g.settings),
|
||||
}),
|
||||
);
|
||||
grants.extend(
|
||||
TokenTransfer::find_all_grants(&mut conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|g| Grant {
|
||||
id: g.id,
|
||||
shared_grant_id: g.shared_grant_id,
|
||||
shared: g.shared,
|
||||
settings: SpecificGrant::TokenTransfer(g.settings),
|
||||
}),
|
||||
);
|
||||
|
||||
for grant in &grants {
|
||||
let signed = integrity::evm::SignedEvmGrant::from_active_grant(grant);
|
||||
integrity::verify_entity(&mut conn, &self.keyholder, &signed)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
integrity::Error::Database(db_err) => db_err,
|
||||
_ => DatabaseError::Connection(diesel::result::Error::RollbackTransaction),
|
||||
})?;
|
||||
}
|
||||
grants.extend(self.list_one_kind::<EtherTransfer, _>(&mut conn).await?);
|
||||
grants.extend(self.list_one_kind::<TokenTransfer, _>(&mut conn).await?);
|
||||
|
||||
Ok(grants)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use diesel::{
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
db::models::{self, EvmBasicGrant, EvmWalletAccess},
|
||||
evm::utils,
|
||||
crypto::integrity::v1::Integrable, db::models::{self, EvmBasicGrant, EvmWalletAccess}, evm::utils
|
||||
};
|
||||
|
||||
pub mod ether_transfer;
|
||||
@@ -59,16 +59,15 @@ pub enum EvalViolation {
|
||||
|
||||
pub type DatabaseID = i32;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Grant<PolicySettings> {
|
||||
pub id: DatabaseID,
|
||||
pub shared_grant_id: DatabaseID, // ID of the basic grant for shared-logic checks like rate limits and validity periods
|
||||
pub shared: SharedGrantSettings,
|
||||
pub settings: PolicySettings,
|
||||
pub common_settings_id: DatabaseID, // ID of the basic grant for shared-logic checks like rate limits and validity periods
|
||||
pub settings: CombinedSettings<PolicySettings>,
|
||||
}
|
||||
|
||||
pub trait Policy: Sized {
|
||||
type Settings: Send + Sync + 'static + Into<SpecificGrant>;
|
||||
type Settings: Send + Sync + 'static + Into<SpecificGrant> + Integrable;
|
||||
type Meaning: Display + std::fmt::Debug + Send + Sync + 'static + Into<SpecificMeaning>;
|
||||
|
||||
fn analyze(context: &EvalContext) -> Option<Self::Meaning>;
|
||||
@@ -124,19 +123,19 @@ pub enum SpecificMeaning {
|
||||
TokenTransfer(token_transfers::Meaning),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
pub struct TransactionRateLimit {
|
||||
pub count: u32,
|
||||
pub window: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
pub struct VolumeRateLimit {
|
||||
pub max_volume: U256,
|
||||
pub window: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
pub struct SharedGrantSettings {
|
||||
pub wallet_access_id: i32,
|
||||
pub chain: ChainId,
|
||||
@@ -197,7 +196,23 @@ pub enum SpecificGrant {
|
||||
TokenTransfer(token_transfers::Settings),
|
||||
}
|
||||
|
||||
pub struct FullGrant<PolicyGrant> {
|
||||
pub basic: SharedGrantSettings,
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CombinedSettings<PolicyGrant> {
|
||||
pub shared: SharedGrantSettings,
|
||||
pub specific: PolicyGrant,
|
||||
}
|
||||
|
||||
impl<P> CombinedSettings<P> {
|
||||
pub fn generalize<Y: From<P>>(self) -> CombinedSettings<Y> {
|
||||
CombinedSettings {
|
||||
shared: self.shared,
|
||||
specific: self.specific.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Integrable> Integrable for CombinedSettings<P> {
|
||||
const KIND: &'static str = P::KIND;
|
||||
const VERSION: i32 = P::VERSION;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ 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::{
|
||||
Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
||||
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
||||
};
|
||||
use crate::{
|
||||
db::{
|
||||
@@ -51,11 +52,14 @@ impl From<Meaning> for SpecificMeaning {
|
||||
}
|
||||
|
||||
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct Settings {
|
||||
pub target: Vec<Address>,
|
||||
pub limit: VolumeRateLimit,
|
||||
}
|
||||
impl Integrable for Settings {
|
||||
const KIND: &'static str = "EtherTransfer";
|
||||
}
|
||||
|
||||
impl From<Settings> for SpecificGrant {
|
||||
fn from(val: Settings) -> SpecificGrant {
|
||||
@@ -95,17 +99,17 @@ async fn check_rate_limits(
|
||||
db: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> QueryResult<Vec<EvalViolation>> {
|
||||
let mut violations = Vec::new();
|
||||
let window = grant.settings.limit.window;
|
||||
let window = grant.settings.specific.limit.window;
|
||||
|
||||
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
|
||||
|
||||
let window_start = chrono::Utc::now() - grant.settings.limit.window;
|
||||
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
||||
let prospective_cumulative_volume: U256 = past_transaction
|
||||
.iter()
|
||||
.filter(|(_, timestamp)| timestamp >= &window_start)
|
||||
.fold(current_transfer_value, |acc, (value, _)| acc + *value);
|
||||
|
||||
if prospective_cumulative_volume > grant.settings.limit.max_volume {
|
||||
if prospective_cumulative_volume > grant.settings.specific.limit.max_volume {
|
||||
violations.push(EvalViolation::VolumetricLimitExceeded);
|
||||
}
|
||||
|
||||
@@ -138,7 +142,7 @@ impl Policy for EtherTransfer {
|
||||
let mut violations = Vec::new();
|
||||
|
||||
// Check if the target address is within the grant's allowed targets
|
||||
if !grant.settings.target.contains(&meaning.to) {
|
||||
if !grant.settings.specific.target.contains(&meaning.to) {
|
||||
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
|
||||
}
|
||||
|
||||
@@ -247,9 +251,11 @@ impl Policy for EtherTransfer {
|
||||
|
||||
Ok(Some(Grant {
|
||||
id: grant.id,
|
||||
shared_grant_id: grant.basic_grant_id,
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
settings,
|
||||
common_settings_id: grant.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
specific: settings,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -327,15 +333,17 @@ impl Policy for EtherTransfer {
|
||||
|
||||
Ok(Grant {
|
||||
id: specific.id,
|
||||
shared_grant_id: specific.basic_grant_id,
|
||||
shared: SharedGrantSettings::try_from_model(basic)?,
|
||||
settings: Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
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),
|
||||
common_settings_id: specific.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic)?,
|
||||
specific: Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -11,7 +11,10 @@ use crate::db::{
|
||||
schema::{evm_basic_grant, evm_transaction_log},
|
||||
};
|
||||
use crate::evm::{
|
||||
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
|
||||
policies::{
|
||||
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||
VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
|
||||
@@ -108,9 +111,11 @@ async fn evaluate_passes_for_allowed_target() {
|
||||
|
||||
let grant = Grant {
|
||||
id: 999,
|
||||
shared_grant_id: 999,
|
||||
shared: shared(),
|
||||
settings: make_settings(vec![ALLOWED], 1_000_000),
|
||||
common_settings_id: 999,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: make_settings(vec![ALLOWED], 1_000_000),
|
||||
},
|
||||
};
|
||||
let context = ctx(ALLOWED, U256::from(100u64));
|
||||
let m = EtherTransfer::analyze(&context).unwrap();
|
||||
@@ -127,9 +132,11 @@ async fn evaluate_rejects_disallowed_target() {
|
||||
|
||||
let grant = Grant {
|
||||
id: 999,
|
||||
shared_grant_id: 999,
|
||||
shared: shared(),
|
||||
settings: make_settings(vec![ALLOWED], 1_000_000),
|
||||
common_settings_id: 999,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: make_settings(vec![ALLOWED], 1_000_000),
|
||||
},
|
||||
};
|
||||
let context = ctx(OTHER, U256::from(100u64));
|
||||
let m = EtherTransfer::analyze(&context).unwrap();
|
||||
@@ -167,9 +174,11 @@ async fn evaluate_passes_when_volume_within_limit() {
|
||||
|
||||
let grant = Grant {
|
||||
id: grant_id,
|
||||
shared_grant_id: basic.id,
|
||||
shared: shared(),
|
||||
settings,
|
||||
common_settings_id: basic.id,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: settings,
|
||||
},
|
||||
};
|
||||
let context = ctx(ALLOWED, U256::from(100u64));
|
||||
let m = EtherTransfer::analyze(&context).unwrap();
|
||||
@@ -207,9 +216,11 @@ async fn evaluate_rejects_volume_over_limit() {
|
||||
|
||||
let grant = Grant {
|
||||
id: grant_id,
|
||||
shared_grant_id: basic.id,
|
||||
shared: shared(),
|
||||
settings,
|
||||
common_settings_id: basic.id,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: settings,
|
||||
},
|
||||
};
|
||||
let context = ctx(ALLOWED, U256::from(1u64));
|
||||
let m = EtherTransfer::analyze(&context).unwrap();
|
||||
@@ -248,9 +259,11 @@ async fn evaluate_passes_at_exactly_volume_limit() {
|
||||
|
||||
let grant = Grant {
|
||||
id: grant_id,
|
||||
shared_grant_id: basic.id,
|
||||
shared: shared(),
|
||||
settings,
|
||||
common_settings_id: basic.id,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: settings,
|
||||
},
|
||||
};
|
||||
let context = ctx(ALLOWED, U256::from(100u64));
|
||||
let m = EtherTransfer::analyze(&context).unwrap();
|
||||
@@ -282,8 +295,11 @@ async fn try_find_grant_roundtrip() {
|
||||
|
||||
assert!(found.is_some());
|
||||
let g = found.unwrap();
|
||||
assert_eq!(g.settings.target, vec![ALLOWED]);
|
||||
assert_eq!(g.settings.limit.max_volume, U256::from(1_000_000u64));
|
||||
assert_eq!(g.settings.specific.target, vec![ALLOWED]);
|
||||
assert_eq!(
|
||||
g.settings.specific.limit.max_volume,
|
||||
U256::from(1_000_000u64)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -347,7 +363,7 @@ async fn find_all_grants_excludes_revoked() {
|
||||
|
||||
let all = EtherTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||
assert_eq!(all.len(), 1);
|
||||
assert_eq!(all[0].settings.target, vec![ALLOWED]);
|
||||
assert_eq!(all[0].settings.specific.target, vec![ALLOWED]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -363,8 +379,11 @@ async fn find_all_grants_multiple_targets() {
|
||||
|
||||
let all = EtherTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||
assert_eq!(all.len(), 1);
|
||||
assert_eq!(all[0].settings.target.len(), 2);
|
||||
assert_eq!(all[0].settings.limit.max_volume, U256::from(1_000_000u64));
|
||||
assert_eq!(all[0].settings.specific.target.len(), 2);
|
||||
assert_eq!(
|
||||
all[0].settings.specific.limit.max_volume,
|
||||
U256::from(1_000_000u64)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -10,11 +10,8 @@ use diesel::dsl::{auto_type, insert_into};
|
||||
use diesel::sqlite::Sqlite;
|
||||
use diesel::{ExpressionMethods, prelude::*};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::db::models::{
|
||||
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit, NewEvmTokenTransferGrant,
|
||||
NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, SqliteTimestamp,
|
||||
};
|
||||
use crate::db::schema::{
|
||||
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
|
||||
evm_token_transfer_volume_limit,
|
||||
@@ -26,6 +23,15 @@ use crate::evm::{
|
||||
},
|
||||
utils,
|
||||
};
|
||||
use crate::{
|
||||
crypto::integrity::Integrable,
|
||||
db::models::{
|
||||
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
|
||||
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
|
||||
SqliteTimestamp,
|
||||
},
|
||||
evm::policies::CombinedSettings,
|
||||
};
|
||||
|
||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||
|
||||
@@ -38,9 +44,9 @@ fn grant_join() -> _ {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Meaning {
|
||||
pub(crate) token: &'static TokenInfo,
|
||||
pub(crate) to: Address,
|
||||
pub(crate) value: U256,
|
||||
pub token: &'static TokenInfo,
|
||||
pub to: Address,
|
||||
pub value: U256,
|
||||
}
|
||||
impl std::fmt::Display for Meaning {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -58,12 +64,15 @@ impl From<Meaning> for SpecificMeaning {
|
||||
}
|
||||
|
||||
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Settings {
|
||||
pub token_contract: Address,
|
||||
pub target: Option<Address>,
|
||||
pub volume_limits: Vec<VolumeRateLimit>,
|
||||
}
|
||||
impl Integrable for Settings {
|
||||
const KIND: &'static str = "TokenTransfer";
|
||||
}
|
||||
impl From<Settings> for SpecificGrant {
|
||||
fn from(val: Settings) -> SpecificGrant {
|
||||
SpecificGrant::TokenTransfer(val)
|
||||
@@ -106,13 +115,20 @@ async fn check_volume_rate_limits(
|
||||
) -> QueryResult<Vec<EvalViolation>> {
|
||||
let mut violations = Vec::new();
|
||||
|
||||
let Some(longest_window) = grant.settings.volume_limits.iter().map(|l| l.window).max() else {
|
||||
let Some(longest_window) = grant
|
||||
.settings
|
||||
.specific
|
||||
.volume_limits
|
||||
.iter()
|
||||
.map(|l| l.window)
|
||||
.max()
|
||||
else {
|
||||
return Ok(violations);
|
||||
};
|
||||
|
||||
let past_transfers = query_relevant_past_transfers(grant.id, longest_window, db).await?;
|
||||
|
||||
for limit in &grant.settings.volume_limits {
|
||||
for limit in &grant.settings.specific.volume_limits {
|
||||
let window_start = chrono::Utc::now() - limit.window;
|
||||
let prospective_cumulative_volume: U256 = past_transfers
|
||||
.iter()
|
||||
@@ -158,7 +174,7 @@ impl Policy for TokenTransfer {
|
||||
return Ok(violations);
|
||||
}
|
||||
|
||||
if let Some(allowed) = grant.settings.target
|
||||
if let Some(allowed) = grant.settings.specific.target
|
||||
&& allowed != meaning.to
|
||||
{
|
||||
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
|
||||
@@ -269,9 +285,11 @@ impl Policy for TokenTransfer {
|
||||
|
||||
Ok(Some(Grant {
|
||||
id: token_grant.id,
|
||||
shared_grant_id: token_grant.basic_grant_id,
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
settings,
|
||||
common_settings_id: token_grant.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
specific: settings,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -369,12 +387,14 @@ impl Policy for TokenTransfer {
|
||||
|
||||
Ok(Grant {
|
||||
id: specific.id,
|
||||
shared_grant_id: specific.basic_grant_id,
|
||||
shared: SharedGrantSettings::try_from_model(basic)?,
|
||||
settings: Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
common_settings_id: specific.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic)?,
|
||||
specific: Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,10 @@ use crate::db::{
|
||||
};
|
||||
use crate::evm::{
|
||||
abi::IERC20::transferCall,
|
||||
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
|
||||
policies::{
|
||||
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||
VolumeRateLimit,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
|
||||
@@ -134,9 +137,11 @@ async fn evaluate_rejects_nonzero_eth_value() {
|
||||
|
||||
let grant = Grant {
|
||||
id: 999,
|
||||
shared_grant_id: 999,
|
||||
shared: shared(),
|
||||
settings: make_settings(None, None),
|
||||
common_settings_id: 999,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: make_settings(None, None),
|
||||
},
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let mut context = ctx(DAI, calldata);
|
||||
@@ -163,9 +168,11 @@ async fn evaluate_passes_any_recipient_when_no_restriction() {
|
||||
|
||||
let grant = Grant {
|
||||
id: 999,
|
||||
shared_grant_id: 999,
|
||||
shared: shared(),
|
||||
settings: make_settings(None, None),
|
||||
common_settings_id: 999,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: make_settings(None, None),
|
||||
},
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
@@ -183,9 +190,11 @@ async fn evaluate_passes_matching_restricted_recipient() {
|
||||
|
||||
let grant = Grant {
|
||||
id: 999,
|
||||
shared_grant_id: 999,
|
||||
shared: shared(),
|
||||
settings: make_settings(Some(RECIPIENT), None),
|
||||
common_settings_id: 999,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: make_settings(Some(RECIPIENT), None),
|
||||
},
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
@@ -203,9 +212,11 @@ async fn evaluate_rejects_wrong_restricted_recipient() {
|
||||
|
||||
let grant = Grant {
|
||||
id: 999,
|
||||
shared_grant_id: 999,
|
||||
shared: shared(),
|
||||
settings: make_settings(Some(RECIPIENT), None),
|
||||
common_settings_id: 999,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: make_settings(Some(RECIPIENT), None),
|
||||
},
|
||||
};
|
||||
let calldata = transfer_calldata(OTHER, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
@@ -247,9 +258,11 @@ async fn evaluate_passes_volume_at_exact_limit() {
|
||||
|
||||
let grant = Grant {
|
||||
id: grant_id,
|
||||
shared_grant_id: basic.id,
|
||||
shared: shared(),
|
||||
settings,
|
||||
common_settings_id: basic.id,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: settings,
|
||||
},
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
@@ -290,9 +303,11 @@ async fn evaluate_rejects_volume_over_limit() {
|
||||
|
||||
let grant = Grant {
|
||||
id: grant_id,
|
||||
shared_grant_id: basic.id,
|
||||
shared: shared(),
|
||||
settings,
|
||||
common_settings_id: basic.id,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: settings,
|
||||
},
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
@@ -313,9 +328,11 @@ async fn evaluate_no_volume_limits_always_passes() {
|
||||
|
||||
let grant = Grant {
|
||||
id: 999,
|
||||
shared_grant_id: 999,
|
||||
shared: shared(),
|
||||
settings: make_settings(None, None), // no volume limits
|
||||
common_settings_id: 999,
|
||||
settings: CombinedSettings {
|
||||
shared: shared(),
|
||||
specific: make_settings(None, None), // no volume limits
|
||||
},
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
|
||||
let context = ctx(DAI, calldata);
|
||||
@@ -349,10 +366,13 @@ async fn try_find_grant_roundtrip() {
|
||||
|
||||
assert!(found.is_some());
|
||||
let g = found.unwrap();
|
||||
assert_eq!(g.settings.token_contract, DAI);
|
||||
assert_eq!(g.settings.target, Some(RECIPIENT));
|
||||
assert_eq!(g.settings.volume_limits.len(), 1);
|
||||
assert_eq!(g.settings.volume_limits[0].max_volume, U256::from(5_000u64));
|
||||
assert_eq!(g.settings.specific.token_contract, DAI);
|
||||
assert_eq!(g.settings.specific.target, Some(RECIPIENT));
|
||||
assert_eq!(g.settings.specific.volume_limits.len(), 1);
|
||||
assert_eq!(
|
||||
g.settings.specific.volume_limits[0].max_volume,
|
||||
U256::from(5_000u64)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -434,9 +454,9 @@ async fn find_all_grants_loads_volume_limits() {
|
||||
|
||||
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||
assert_eq!(all.len(), 1);
|
||||
assert_eq!(all[0].settings.volume_limits.len(), 1);
|
||||
assert_eq!(all[0].settings.specific.volume_limits.len(), 1);
|
||||
assert_eq!(
|
||||
all[0].settings.volume_limits[0].max_volume,
|
||||
all[0].settings.specific.volume_limits[0].max_volume,
|
||||
U256::from(9_999u64)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user