5 Commits

Author SHA1 Message Date
hdbg
21b9d698fa docs: add EVM Policy Engine section
Some checks failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
2026-03-11 13:30:53 +01:00
hdbg
bb24cc19aa fix(evm::engine): added shared settings check in vet_transaction 2026-03-10 20:16:57 +01:00
hdbg
cc6afacb66 feat(evm): add find_all_grants to Policy trait with shared auto_type queries 2026-03-10 19:55:53 +01:00
hdbg
ed06471b40 feat(protobuf): EVM grants and signing definitions 2026-03-10 18:48:07 +01:00
hdbg
c14c4d27b3 housekeeping: linter 2026-03-10 16:54:23 +01:00
8 changed files with 571 additions and 29 deletions

View File

@@ -27,6 +27,82 @@ This document covers concrete technology choices and dependencies. For the archi
---
## EVM Policy Engine
### Overview
The EVM engine classifies incoming transactions, enforces grant constraints, and records executions. It is the sole path through which a wallet key is used for signing.
The central abstraction is the `Policy` trait. Each implementation handles one semantic transaction category and owns its own database tables for grant storage and transaction logging.
### Transaction Evaluation Flow
`Engine::evaluate_transaction` runs the following steps in order:
1. **Classify** — Each registered policy's `analyze(context)` inspects the transaction fields (`chain`, `to`, `value`, `calldata`). The first one returning `Some(meaning)` wins. If none match, the transaction is rejected as `UnsupportedTransactionType`.
2. **Find grant**`Policy::try_find_grant` queries for a non-revoked grant covering this wallet, client, chain, and target address.
3. **Check shared constraints**`check_shared_constraints` runs in the engine before any policy-specific logic. It enforces the validity window, gas fee caps, and transaction count rate limit (see below).
4. **Evaluate**`Policy::evaluate` checks the decoded meaning against the grant's policy-specific constraints and returns any violations.
5. **Record** — If `RunKind::Execution` and there are no violations, the engine writes to `evm_transaction_log` and calls `Policy::record_transaction` for any policy-specific logging (e.g., token transfer volume).
### Policy Trait
| Method | Purpose |
|---|---|
| `analyze` | Pure — classifies a transaction into a typed `Meaning`, or `None` if this policy doesn't apply |
| `evaluate` | Checks the `Meaning` against a `Grant`; returns a list of `EvalViolation`s |
| `create_grant` | Inserts policy-specific rows; returns the specific grant ID |
| `try_find_grant` | Finds a matching non-revoked grant for the given `EvalContext` |
| `find_all_grants` | Returns all non-revoked grants (used for listing) |
| `record_transaction` | Persists policy-specific data after execution |
`analyze` and `evaluate` are intentionally separate: classification is pure and cheap, while evaluation may involve DB queries (e.g., fetching past transfer volume).
### Registered Policies
**EtherTransfer** — plain ETH transfers (empty calldata)
- Grant requires: allowlist of recipient addresses + one volumetric rate limit (max ETH over a time window)
- Violations: recipient not in allowlist, cumulative ETH volume exceeded
**TokenTransfer** — ERC-20 `transfer(address,uint256)` calls
- Recognised by ABI-decoding the `transfer(address,uint256)` selector against a static registry of known token contracts (`arbiter_tokens_registry`)
- Grant requires: token contract address, optional recipient restriction, zero or more volumetric rate limits
- Violations: recipient mismatch, any volumetric limit exceeded
### Grant Model
Every grant has two layers:
- **Shared (`evm_basic_grant`)** — wallet, chain, validity period, gas fee caps, transaction count rate limit. One row per grant regardless of type.
- **Specific** — policy-owned tables (`evm_ether_transfer_grant`, `evm_token_transfer_grant`, etc.) holding type-specific configuration.
`find_all_grants` uses a `#[diesel::auto_type]` base join between the specific and shared tables, then batch-loads related rows (targets, volume limits) in two additional queries to avoid N+1.
The engine exposes `list_all_grants` which collects across all policy types into `Vec<Grant<SpecificGrant>>` via a blanket `From<Grant<S>> for Grant<SpecificGrant>` conversion.
### Shared Constraints (enforced by the engine)
These are checked centrally in `check_shared_constraints` before policy evaluation:
| Constraint | Fields | Behaviour |
|---|---|---|
| Validity window | `valid_from`, `valid_until` | Emits `InvalidTime` if current time is outside the range |
| Gas fee cap | `max_gas_fee_per_gas`, `max_priority_fee_per_gas` | Emits `GasLimitExceeded` if either cap is breached |
| Tx count rate limit | `rate_limit` (`count` + `window`) | Counts rows in `evm_transaction_log` within the window; emits `RateLimitExceeded` if at or above the limit |
---
### Known Limitations
- **Only EIP-1559 transactions are supported.** Legacy and EIP-2930 types are rejected outright.
- **No opaque-calldata (unknown contract) grant type.** The architecture describes a category for unrecognised contracts, but no policy implements it yet. Any transaction that is not a plain ETH transfer or a known ERC-20 transfer is unconditionally rejected.
- **Token registry is static.** Tokens are recognised only if they appear in the hard-coded `arbiter_tokens_registry` crate. There is no mechanism to register additional contracts at runtime.
- **Nonce management is not implemented.** The architecture lists nonce deduplication as a core responsibility, but no nonce tracking or enforcement exists yet.
---
## Memory Protection
The unsealed root key must be held in a hardened memory cell resistant to dumps, page swaps, and hibernation.

View File

@@ -2,6 +2,8 @@ syntax = "proto3";
package arbiter.client;
import "evm.proto";
message AuthChallengeRequest {
bytes pubkey = 1;
}
@@ -21,6 +23,8 @@ message ClientRequest {
oneof payload {
AuthChallengeRequest auth_challenge_request = 1;
AuthChallengeSolution auth_challenge_solution = 2;
arbiter.evm.EvmSignTransactionRequest evm_sign_transaction = 3;
arbiter.evm.EvmAnalyzeTransactionRequest evm_analyze_transaction = 4;
}
}
@@ -28,5 +32,7 @@ message ClientResponse {
oneof payload {
AuthChallenge auth_challenge = 1;
AuthOk auth_ok = 2;
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3;
arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4;
}
}

View File

@@ -2,6 +2,9 @@ syntax = "proto3";
package arbiter.evm;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
enum EvmError {
EVM_ERROR_UNSPECIFIED = 0;
EVM_ERROR_VAULT_SEALED = 1;
@@ -29,3 +32,185 @@ message WalletListResponse {
EvmError error = 2;
}
}
// --- Grant types ---
message TransactionRateLimit {
uint32 count = 1;
int64 window_secs = 2;
}
message VolumeRateLimit {
bytes max_volume = 1; // U256 as big-endian bytes
int64 window_secs = 2;
}
message SharedSettings {
int32 wallet_id = 1;
uint64 chain_id = 2;
optional google.protobuf.Timestamp valid_from = 3;
optional google.protobuf.Timestamp valid_until = 4;
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 TransactionRateLimit rate_limit = 7;
}
message EtherTransferSettings {
repeated bytes targets = 1; // list of 20-byte Ethereum addresses
VolumeRateLimit limit = 2;
}
message TokenTransferSettings {
bytes token_contract = 1; // 20-byte Ethereum address
optional bytes target = 2; // 20-byte Ethereum address; absent means any recipient allowed
repeated VolumeRateLimit volume_limits = 3;
}
message SpecificGrant {
oneof grant {
EtherTransferSettings ether_transfer = 1;
TokenTransferSettings token_transfer = 2;
}
}
message EtherTransferMeaning {
bytes to = 1; // 20-byte Ethereum address
bytes value = 2; // U256 as big-endian bytes
}
message TokenInfo {
string symbol = 1;
bytes address = 2; // 20-byte Ethereum address
uint64 chain_id = 3;
}
// Mirror of token_transfers::Meaning
message TokenTransferMeaning {
TokenInfo token = 1;
bytes to = 2; // 20-byte Ethereum address
bytes value = 3; // U256 as big-endian bytes
}
// Mirror of policies::SpecificMeaning
message SpecificMeaning {
oneof meaning {
EtherTransferMeaning ether_transfer = 1;
TokenTransferMeaning token_transfer = 2;
}
}
// --- Eval error types ---
message GasLimitExceededViolation {
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
}
message EvalViolation {
oneof kind {
bytes invalid_target = 1; // 20-byte Ethereum address
GasLimitExceededViolation gas_limit_exceeded = 2;
google.protobuf.Empty rate_limit_exceeded = 3;
google.protobuf.Empty volumetric_limit_exceeded = 4;
google.protobuf.Empty invalid_time = 5;
google.protobuf.Empty invalid_transaction_type = 6;
}
}
// Transaction was classified but no grant covers it
message NoMatchingGrantError {
SpecificMeaning meaning = 1;
}
// Transaction was classified and a grant was found, but constraints were violated
message PolicyViolationsError {
SpecificMeaning meaning = 1;
repeated EvalViolation violations = 2;
}
// top-level error returned when transaction evaluation fails
message TransactionEvalError {
oneof kind {
google.protobuf.Empty contract_creation_not_supported = 1;
google.protobuf.Empty unsupported_transaction_type = 2;
NoMatchingGrantError no_matching_grant = 3;
PolicyViolationsError policy_violations = 4;
}
}
// --- UserAgent grant management ---
message EvmGrantCreateRequest {
int32 client_id = 1;
SharedSettings shared = 2;
SpecificGrant specific = 3;
}
message EvmGrantCreateResponse {
oneof result {
int32 grant_id = 1;
EvmError error = 2;
}
}
message EvmGrantDeleteRequest {
int32 grant_id = 1;
}
message EvmGrantDeleteResponse {
oneof result {
google.protobuf.Empty ok = 1;
EvmError error = 2;
}
}
// Basic grant info returned in grant listings
message GrantEntry {
int32 id = 1;
int32 client_id = 2;
SharedSettings shared = 3;
SpecificGrant specific = 4;
}
message EvmGrantListRequest {
optional int32 wallet_id = 1;
}
message EvmGrantListResponse {
oneof result {
EvmGrantList grants = 1;
EvmError error = 2;
}
}
message EvmGrantList {
repeated GrantEntry grants = 1;
}
// --- Client transaction operations ---
message EvmSignTransactionRequest {
bytes wallet_address = 1; // 20-byte Ethereum address
bytes rlp_transaction = 2; // RLP-encoded EIP-1559 transaction (unsigned)
}
// oneof because signing and evaluation happen atomically — a signing failure
// is always either an eval error or an internal error, never a partial success
message EvmSignTransactionResponse {
oneof result {
bytes signature = 1; // 65-byte signature: r[32] || s[32] || v[1]
TransactionEvalError eval_error = 2;
EvmError error = 3;
}
}
message EvmAnalyzeTransactionRequest {
bytes wallet_address = 1; // 20-byte Ethereum address
bytes rlp_transaction = 2; // RLP-encoded EIP-1559 transaction
}
message EvmAnalyzeTransactionResponse {
oneof result {
SpecificMeaning meaning = 1;
TransactionEvalError eval_error = 2;
EvmError error = 3;
}
}

View File

@@ -58,6 +58,9 @@ message UserAgentRequest {
google.protobuf.Empty query_vault_state = 5;
google.protobuf.Empty evm_wallet_create = 6;
google.protobuf.Empty evm_wallet_list = 7;
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
}
}
message UserAgentResponse {
@@ -69,5 +72,8 @@ message UserAgentResponse {
VaultState vault_state = 5;
arbiter.evm.WalletCreateResponse evm_wallet_create = 6;
arbiter.evm.WalletListResponse evm_wallet_list = 7;
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
}
}

View File

@@ -1,22 +1,23 @@
pub mod abi;
pub mod safe_signer;
use alloy::{consensus::TxEip1559, primitives::TxKind, signers::Signature};
use alloy::{consensus::TxEip1559, primitives::{TxKind, U256}};
use chrono::Utc;
use diesel::{QueryResult, insert_into};
use diesel::{QueryResult, insert_into, sqlite::Sqlite};
use diesel_async::{AsyncConnection, RunQueryDsl};
use crate::{
db::{
self,
models::{
EvmBasicGrant, EvmTransactionLog, NewEvmBasicGrant, NewEvmTransactionLog,
EvmBasicGrant, NewEvmBasicGrant, NewEvmTransactionLog,
SqliteTimestamp,
},
schema::{self, evm_transaction_log},
},
evm::policies::{
EvalContext, EvalViolation, FullGrant, Policy, SpecificMeaning,
DatabaseID, EvalContext, EvalViolation, FullGrant, Grant, Policy, SharedGrantSettings,
SpecificGrant, SpecificMeaning,
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
},
};
@@ -87,6 +88,17 @@ pub enum CreationError {
Database(#[from] diesel::result::Error),
}
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum ListGrantsError {
#[error("Database connection pool error")]
#[diagnostic(code(arbiter_server::evm::list_grants_error::pool))]
Pool(#[from] db::PoolError),
#[error("Database returned error")]
#[diagnostic(code(arbiter_server::evm::list_grants_error::database))]
Database(#[from] diesel::result::Error),
}
/// Controls whether a transaction should be executed or only validated
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunKind {
@@ -96,6 +108,54 @@ pub enum RunKind {
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
pub struct Engine {
db: db::DatabasePool,
@@ -114,7 +174,11 @@ impl Engine {
.await?
.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() {
return Err(PolicyError::Violations(violations));
} else if run_kind == RunKind::Execution {
@@ -201,6 +265,27 @@ impl Engine {
Ok(id)
}
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListGrantsError> {
let mut conn = self.db.get().await?;
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();
grants.extend(
EtherTransfer::find_all_grants(&mut conn)
.await?
.into_iter()
.map(Grant::from),
);
grants.extend(
TokenTransfer::find_all_grants(&mut conn)
.await?
.into_iter()
.map(Grant::from),
);
Ok(grants)
}
pub async fn evaluate_transaction(
&self,
wallet_id: i32,
@@ -215,9 +300,11 @@ impl Engine {
wallet_id,
client_id,
chain: transaction.chain_id,
to: to,
to,
value: transaction.value,
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) {

View File

@@ -27,6 +27,10 @@ pub struct EvalContext {
pub to: Address,
pub value: U256,
pub calldata: Bytes,
// Gas pricing (EIP-1559)
pub max_fee_per_gas: u128,
pub max_priority_fee_per_gas: u128,
}
#[derive(Debug, Error, Diagnostic)]
@@ -97,6 +101,11 @@ pub trait Policy: Sized {
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> impl Future<Output = QueryResult<Option<Grant<Self::Settings>>>> + Send;
// Return all non-revoked grants, eagerly loading policy-specific settings
fn find_all_grants(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> impl Future<Output = QueryResult<Vec<Grant<Self::Settings>>>> + Send;
// Records, updates or deletes rate limits
// In other words, records grant-specific things after transaction is executed
fn record_transaction(
@@ -192,6 +201,19 @@ pub enum SpecificGrant {
TokenTransfer(token_transfers::Settings),
}
/// Blanket conversion from a typed `Grant<S>` into `Grant<SpecificGrant>`.
/// Lets the engine collect across all policies into one `Vec<Grant<SpecificGrant>>`.
impl<S: Into<SpecificGrant>> From<Grant<S>> for Grant<SpecificGrant> {
fn from(g: Grant<S>) -> Self {
Grant {
id: g.id,
shared_grant_id: g.shared_grant_id,
shared: g.shared,
settings: g.settings.into(),
}
}
}
pub struct FullGrant<PolicyGrant> {
pub basic: SharedGrantSettings,
pub specific: PolicyGrant,

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fmt::Display;
use alloy::primitives::{Address, U256};
@@ -11,7 +12,7 @@ use crate::db::models::{
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
NewEvmEtherTransferLimit, SqliteTimestamp,
};
use crate::db::schema::{evm_ether_transfer_limit, evm_transaction_log};
use crate::db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log};
use crate::evm::policies::{
Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
};
@@ -23,6 +24,14 @@ use crate::{
evm::{policies::Policy, utils},
};
#[diesel::auto_type]
fn grant_join() -> _ {
evm_ether_transfer_grant::table.inner_join(
evm_basic_grant::table
.on(evm_ether_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
}
use super::{DatabaseID, EvalContext, EvalViolation};
// Plain ether transfer
@@ -183,27 +192,16 @@ impl Policy for EtherTransfer {
context: &EvalContext,
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> diesel::result::QueryResult<Option<Grant<Self::Settings>>> {
use crate::db::schema::{
evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target,
};
let target_bytes = context.to.to_vec();
// Find a grant where:
// 1. The basic grant's wallet_id and client_id match the context
// 2. Any of the grant's targets match the context's `to` address
let grant: Option<(EvmBasicGrant, EvmEtherTransferGrant)> = evm_ether_transfer_grant::table
.inner_join(
evm_basic_grant::table
.on(evm_ether_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
.inner_join(
evm_ether_transfer_grant_target::table
.on(evm_ether_transfer_grant::id.eq(evm_ether_transfer_grant_target::grant_id)),
)
let grant: Option<(EvmBasicGrant, EvmEtherTransferGrant)> = grant_join()
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
.filter(evm_basic_grant::client_id.eq(context.client_id))
.filter(evm_ether_transfer_grant_target::address.eq(&target_bytes))
.filter(evm_basic_grant::revoked_at.is_null())
.select((
EvmBasicGrant::as_select(),
EvmEtherTransferGrant::as_select(),
@@ -266,4 +264,75 @@ impl Policy for EtherTransfer {
Ok(())
}
async fn find_all_grants(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<Vec<Grant<Self::Settings>>> {
let grants: Vec<(EvmBasicGrant, EvmEtherTransferGrant)> = grant_join()
.filter(evm_basic_grant::revoked_at.is_null())
.select((EvmBasicGrant::as_select(), EvmEtherTransferGrant::as_select()))
.load(conn)
.await?;
if grants.is_empty() {
return Ok(Vec::new());
}
let grant_ids: Vec<i32> = grants.iter().map(|(_, g)| g.id).collect();
let limit_ids: Vec<i32> = grants.iter().map(|(_, g)| g.limit_id).collect();
let all_targets: Vec<EvmEtherTransferGrantTarget> = evm_ether_transfer_grant_target::table
.filter(evm_ether_transfer_grant_target::grant_id.eq_any(&grant_ids))
.select(EvmEtherTransferGrantTarget::as_select())
.load(conn)
.await?;
let all_limits: Vec<EvmEtherTransferLimit> = evm_ether_transfer_limit::table
.filter(evm_ether_transfer_limit::id.eq_any(&limit_ids))
.select(EvmEtherTransferLimit::as_select())
.load(conn)
.await?;
let mut targets_by_grant: HashMap<i32, Vec<EvmEtherTransferGrantTarget>> = HashMap::new();
for target in all_targets {
targets_by_grant.entry(target.grant_id).or_default().push(target);
}
let limits_by_id: HashMap<i32, EvmEtherTransferLimit> =
all_limits.into_iter().map(|l| (l.id, l)).collect();
grants
.into_iter()
.map(|(basic, specific)| {
let targets: Vec<Address> = targets_by_grant
.get(&specific.id)
.map(|v| v.as_slice())
.unwrap_or_default()
.iter()
.filter_map(|t| {
let arr: [u8; 20] = t.address.clone().try_into().ok()?;
Some(Address::from(arr))
})
.collect();
let limit = limits_by_id
.get(&specific.limit_id)
.ok_or(diesel::result::Error::NotFound)?;
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),
},
},
})
})
.collect()
}
}

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use alloy::{
primitives::{Address, U256},
sol_types::SolCall,
@@ -14,16 +16,26 @@ use crate::db::models::{
NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, SqliteTimestamp,
};
use crate::db::schema::{
evm_token_transfer_grant, evm_token_transfer_log, evm_token_transfer_volume_limit,
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},
policies::{
Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
},
utils,
};
use super::{DatabaseID, EvalContext, EvalViolation};
#[diesel::auto_type]
fn grant_join() -> _ {
evm_token_transfer_grant::table.inner_join(
evm_basic_grant::table.on(evm_token_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Meaning {
token: &'static TokenInfo,
@@ -192,15 +204,10 @@ impl Policy for TokenTransfer {
context: &EvalContext,
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<Option<Grant<Self::Settings>>> {
use crate::db::schema::{evm_basic_grant, evm_token_transfer_grant};
let token_contract_bytes = context.to.to_vec();
let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = evm_token_transfer_grant::table
.inner_join(
evm_basic_grant::table
.on(evm_token_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
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::client_id.eq(context.client_id))
.filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes))
@@ -288,4 +295,88 @@ impl Policy for TokenTransfer {
Ok(())
}
async fn find_all_grants(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<Vec<Grant<Self::Settings>>> {
let grants: Vec<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
.filter(evm_basic_grant::revoked_at.is_null())
.select((
EvmBasicGrant::as_select(),
EvmTokenTransferGrant::as_select(),
))
.load(conn)
.await?;
if grants.is_empty() {
return Ok(Vec::new());
}
let grant_ids: Vec<i32> = grants.iter().map(|(_, g)| g.id).collect();
let all_volume_limits: Vec<EvmTokenTransferVolumeLimit> =
evm_token_transfer_volume_limit::table
.filter(evm_token_transfer_volume_limit::grant_id.eq_any(&grant_ids))
.select(EvmTokenTransferVolumeLimit::as_select())
.load(conn)
.await?;
let mut limits_by_grant: HashMap<i32, Vec<EvmTokenTransferVolumeLimit>> = HashMap::new();
for limit in all_volume_limits {
limits_by_grant
.entry(limit.grant_id)
.or_default()
.push(limit);
}
grants
.into_iter()
.map(|(basic, specific)| {
let volume_limits: Vec<VolumeRateLimit> = limits_by_grant
.get(&specific.id)
.map(|v| v.as_slice())
.unwrap_or_default()
.iter()
.map(|row| {
Ok(VolumeRateLimit {
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),
})
})
.collect::<QueryResult<Vec<_>>>()?;
let token_contract: [u8; 20] =
specific.token_contract.clone().try_into().map_err(|_| {
diesel::result::Error::DeserializationError(
"Invalid token contract address length".into(),
)
})?;
let target: Option<Address> = match &specific.receiver {
None => None,
Some(bytes) => {
let arr: [u8; 20] = bytes.clone().try_into().map_err(|_| {
diesel::result::Error::DeserializationError(
"Invalid receiver address length".into(),
)
})?;
Some(Address::from(arr))
}
};
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,
},
})
})
.collect()
}
}