Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Andre/back 2426 rules engine #8

Merged
merged 19 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading