Skip to content

Commit

Permalink
Merge pull request #8 from ourzora/andre/back-2426-rules-engine
Browse files Browse the repository at this point in the history
Andre/back 2426 rules engine
  • Loading branch information
ligustah authored Apr 3, 2024
2 parents 7f4c854 + 6cfc3ec commit 3c19251
Show file tree
Hide file tree
Showing 14 changed files with 652 additions and 287 deletions.
305 changes: 173 additions & 132 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
const-hex = "1.11.3"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
libp2p = { version = "0.53.2", features = [
Expand Down Expand Up @@ -34,6 +35,7 @@ sqlx = { version = "0.7.4", features = [
] }
once_cell = "1.19.0"
rand = "0.8.5"
runit = "0.1.0"
futures-util = "0.3"

alloy = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b661f54df0ac213f18639", features = [
Expand All @@ -43,16 +45,24 @@ alloy = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b
"pubsub",
] }


alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b661f54df0ac213f18639", features = [
"pubsub",
"ws",
] }
alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b661f54df0ac213f18639", features = [
"pubsub",
] }
alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b661f54df0ac213f18639", features = [
"eip712",
]}
alloy-signer-wallet = {git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b661f54df0ac213f18639" }

alloy-sol-types = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
alloy-core = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
alloy-sol-types = { git = "https://github.com/alloy-rs/core", rev = "525a233" , features = ["eip712-serde"]}
alloy-primitives = { git = "https://github.com/alloy-rs/core", rev = "525a233", features = ["serde"] }
alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
futures = "0.3.30"


[patch.crates-io]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod config;
pub mod controller;
pub mod p2p;
pub mod premints;
pub mod rules;
pub mod run;
pub mod stdin;
pub mod storage;
Expand Down
23 changes: 23 additions & 0 deletions src/premints/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ERC721Metadata {
pub name: String,
pub description: String,
pub image: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ERC1155Metadata {
pub name: String,
pub description: String,
pub image: String,
pub decimals: u64,
pub properties: Map<String, Value>,
}

enum TokenMetadata {
ERC721(ERC721Metadata),
ERC1155(ERC1155Metadata),
}
3 changes: 2 additions & 1 deletion src/premints/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod zora_v2;
pub mod metadata;
pub mod zora_premint_v2;
2 changes: 2 additions & 0 deletions src/premints/zora_premint_v2/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod rules;
pub mod types;
83 changes: 83 additions & 0 deletions src/premints/zora_premint_v2/rules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::str::FromStr;

use alloy_primitives::Signature;
use alloy_sol_types::SolStruct;

use crate::premints::zora_premint_v2::types::ZoraPremintV2;
use crate::rules::RuleContext;
use crate::types::Premint;

// create premint v2 rule implementations here

// TODO: is there any rust sugar to make this more concise?
// as it stands, it's not defined as an async function, so can't use async stuff
pub async fn is_authorized_to_create_premint<T: Premint>(premint: &T) -> eyre::Result<bool> {
// * if contract exists, check if the signer is the contract admin
// * if contract does not exist, check if the signer is the proposed contract admin
// * this logic exists as a function on the premint executor contract
Ok(true)
}

// * signatureIsValid ( this can be performed entirely offline )
// * check if the signature is valid
// * check if the signature is equal to the proposed contract admin

pub async fn is_valid_signature(
premint: ZoraPremintV2,
context: RuleContext,
) -> eyre::Result<bool> {
// * if contract exists, check if the signer is the contract admin
// * if contract does not exist, check if the signer is the proposed contract admin

let signature = Signature::from_str(premint.signature.as_str())?;

let domain = premint.eip712_domain();
let hash = premint.premint.eip712_signing_hash(&domain);
let signer = signature.recover_address_from_prehash(&hash)?;

Ok(signer == premint.collection.contractAdmin)
}

#[cfg(test)]
mod test {
use std::str::FromStr;

use super::*;

const PREMINT_JSON: &str = r#"
{
"collection": {
"contractAdmin": "0xa771209423284bace9a24a06d166a11196724b53",
"contractURI": "ipfs://bafkreic4fnavhtymee7makmk7wp257nloh5y5ysc2fcwa5rpg6v6f3jhly",
"contractName": "Karate sketch"
},
"premint": {
"tokenConfig": {
"tokenURI": "ipfs://bafkreier5h4a6btu24fsitbjdvpyak7moi6wkp33wlqmx2kfwgpq2lvx4y",
"maxSupply": 18446744073709551615,
"maxTokensPerAddress": 0,
"pricePerToken": 0,
"mintStart": 1702541688,
"mintDuration": 2592000,
"royaltyBPS": 500,
"fixedPriceMinter": "0x04e2516a2c207e84a1839755675dfd8ef6302f0a",
"payoutRecipient": "0xa771209423284bace9a24a06d166a11196724b53",
"createReferral": "0x0000000000000000000000000000000000000000"
},
"uid": 2,
"version": 1,
"deleted": false
},
"collectionAddress": "0x42e108d1ed954b0adbd53ea118ba7614622d10d0",
"chainId": 7777777,
"signature": "0x894405d100900e6823385ca881c91d5ca7137a326f0c7d27edfd2907d9669cea55626bbd807a36cea815eceeac6634f45cfec54d7157c35f496b999e7b9451de1c"
}"#;

#[tokio::test]
async fn test_is_valid_signature() {
let premint: ZoraPremintV2 = serde_json::from_str(PREMINT_JSON).unwrap();
assert!(is_valid_signature(premint, RuleContext {})
.await
.expect("failed to check signature"));
}
}
177 changes: 177 additions & 0 deletions src/premints/zora_premint_v2/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use std::borrow::Cow;

use alloy::rpc::types::eth::{Filter, Log, Transaction};
use alloy::sol_types::private::U256;
use alloy_primitives::{address, Address};
use alloy_signer::Signer;
use alloy_signer_wallet::LocalWallet;
use alloy_sol_macro::sol;
use alloy_sol_types::{eip712_domain, Eip712Domain, SolEvent};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::types::{InclusionClaim, Premint, PremintMetadata, PremintName};

sol! {
event PremintedV2(
address indexed contractAddress,
uint256 indexed tokenId,
bool indexed createdNewContract,
uint32 uid,
address minter,
uint256 quantityMinted
);

#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
struct ContractCreationConfig {
// Creator/admin of the created contract. Must match the account that signed the message
address contractAdmin;
// Metadata URI for the created contract
string contractURI;
// Name of the created contract
string contractName;
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
struct TokenCreationConfig {
// Metadata URI for the created token
string tokenURI;
// Max supply of the created token
uint256 maxSupply;
// Max tokens that can be minted for an address, 0 if unlimited
uint64 maxTokensPerAddress;
// Price per token in eth wei. 0 for a free mint.
uint96 pricePerToken;
// The start time of the mint, 0 for immediate. Prevents signatures from being used until the start time.
uint64 mintStart;
// The duration of the mint, starting from the first mint of this token. 0 for infinite
uint64 mintDuration;
// RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales.
uint32 royaltyBPS;
// This is the address that will be set on the `royaltyRecipient` for the created token on the 1155 contract,
// which is the address that receives creator rewards and secondary royalties for the token,
// and on the `fundsRecipient` on the ZoraCreatorFixedPriceSaleStrategy contract for the token,
// which is the address that receives paid mint funds for the token.
address payoutRecipient;
// Fixed price minter address
address fixedPriceMinter;
// create referral
address createReferral;
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
struct CreatorAttribution {
// The config for the token to be created
TokenCreationConfig tokenConfig;
// Unique id of the token, used to ensure that multiple signatures can't be used to create the same intended token.
// only one signature per token id, scoped to the contract hash can be executed.
uint32 uid;
// Version of this premint, scoped to the uid and contract. Not used for logic in the contract, but used externally to track the newest version
uint32 version;
// If executing this signature results in preventing any signature with this uid from being minted.
bool deleted;
}
}

// renaming solidity type not implemented yet in alloy-sol-types,
// so we alias the generated type.
pub type ZoraPremintConfigV2 = CreatorAttribution;

// modelled after the PremintRequest API type
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ZoraPremintV2 {
pub collection: ContractCreationConfig,
pub premint: ZoraPremintConfigV2,
pub collection_address: Address,
pub chain_id: U256,
pub signature: String,
}

pub static PREMINT_FACTORY_ADDR: Address = address!("7777773606e7e46C8Ba8B98C08f5cD218e31d340");

impl ZoraPremintV2 {
pub fn eip712_domain(&self) -> Eip712Domain {
Eip712Domain {
name: Some(Cow::from("Preminter")),
version: Some(Cow::from("2")),
chain_id: Some(self.chain_id),
verifying_contract: Some(self.collection_address),
salt: None,
}
}
}

#[async_trait]
impl Premint for ZoraPremintV2 {
fn metadata(&self) -> PremintMetadata {
PremintMetadata {
id: self.premint.uid.to_string(),
kind: Self::kind_id(),
signer: self.collection.contractAdmin,
chain_id: self.chain_id,
collection_address: Address::default(), // TODO: source this
token_id: U256::from(self.premint.uid),
uri: self.premint.tokenConfig.tokenURI.clone(),
}
}

fn guid(&self) -> String {
format!(
"{:?}:{:?}:{:?}:{:?}",
self.chain_id, self.collection_address, self.premint.uid, self.premint.version
)
}

fn check_filter(chain_id: u64) -> Option<Filter> {
let supported_chains = [7777777, 8423]; // TODO: add the rest here and enable testnet mode
if !supported_chains.contains(&chain_id) {
return None;
}
Some(
Filter::new()
.address(PREMINT_FACTORY_ADDR)
.event(PremintedV2::SIGNATURE),
)
}

fn map_claim(chain_id: u64, log: Log) -> eyre::Result<InclusionClaim> {
let event = PremintedV2::decode_raw_log(&log.topics, &log.data, true)?;

Ok(InclusionClaim {
premint_id: event.uid.to_string(),
chain_id,
tx_hash: log.transaction_hash.unwrap_or_default(),
log_index: log.log_index.unwrap_or(U256::from(0)).to(),
kind: "zora_premint_v2".to_string(),
})
}

async fn verify_claim(chain_id: u64, tx: Transaction, log: Log, claim: InclusionClaim) -> bool {
let event = PremintedV2::decode_raw_log(&log.topics, &log.data, true);
match event {
Ok(event) => {
let conditions = vec![
log.address == PREMINT_FACTORY_ADDR,
log.transaction_hash.unwrap_or_default() == tx.hash,
claim.tx_hash == tx.hash,
claim.log_index == log.log_index.unwrap_or_default().to::<u64>(),
claim.premint_id == event.uid.to_string(),
claim.kind == *"zora_premint_v2",
claim.chain_id == chain_id,
];

// confirm all conditions are true
conditions.into_iter().all(|x| x)
}
Err(e) => {
tracing::debug!("Failed to parse log: {}", e);
false
}
}
}

fn kind_id() -> PremintName {
PremintName("zora_premint_v2".to_string())
}
}
Loading

0 comments on commit 3c19251

Please sign in to comment.