Skip to content

Commit

Permalink
Add ERC20 Premints (#48)
Browse files Browse the repository at this point in the history
Needed to use macros for a lot of things. The main problem is that we need to use separate namespaces for V2 and ERC20 ABI types, because we need to tweak the abi.json and change the names of some types. That's required for signatures to work correctly, but they can't both have the same names.

Experimented with a generic based solution, but ultimately went for the macros because of that.
  • Loading branch information
ligustah authored May 3, 2024
1 parent f4c1e27 commit d21f180
Show file tree
Hide file tree
Showing 19 changed files with 1,867 additions and 377 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ env:

jobs:
build_and_test:
runs-on: rust_oss_runner
runs-on: ubuntu-latest
environment: Testing

steps:
Expand Down
1 change: 0 additions & 1 deletion src/chain.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::chain_list::{ChainListProvider, CHAINS};
use crate::controller::{ControllerCommands, ControllerInterface};
use crate::premints::zora_premint_v2::types::PREMINT_FACTORY_ADDR;
use crate::types::{InclusionClaim, Premint, PremintTypes};
use alloy::primitives::{address, Address, Bytes, TxKind};
use alloy::providers::Provider;
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use clap::Parser;
use mintpool::api;
use mintpool::metrics::init_metrics_and_logging;
use mintpool::premints::zora_premint_v2::types::ZoraPremintV2;
use mintpool::premints::zora_premint::v2::V2;
use mintpool::rules::RulesEngine;
use mintpool::run::{start_p2p_services, start_watch_chain};
use mintpool::stdin::watch_stdin;
Expand All @@ -23,7 +23,7 @@ async fn main() -> eyre::Result<()> {
let router = api::router_with_defaults(&config).merge(metrics_router);
api::start_api(&config, ctl.clone(), router, true).await?;

start_watch_chain::<ZoraPremintV2>(&config, ctl.clone()).await;
start_watch_chain::<V2>(&config, ctl.clone()).await;
tracing::info!(monotonic_counter.chains_watched = 1, "Watching chain");
if config.interactive {
watch_stdin(ctl.clone()).await;
Expand Down
2 changes: 1 addition & 1 deletion src/premints/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pub mod metadata;
pub mod zora_premint_v2;
pub mod zora_premint;
20 changes: 20 additions & 0 deletions src/premints/zora_premint/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use alloy::primitives::{address, Address};
use alloy::sol;
use serde::{Deserialize, Serialize};

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

// we need to use separate namespaces for each premint version,
// because they all need to have the type names for the signatures
// to calculate correctly
sol! {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
IZoraPremintERC20V1,
"src/premints/zora_premint/zora1155PremintExecutor_erc20_1.json"
}

sol! {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
IZoraPremintV2,
"src/premints/zora_premint/zora1155PremintExecutor_v2.json"
}
70 changes: 70 additions & 0 deletions src/premints/zora_premint/erc20v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::{implement_zora_premint_traits, typed_rule};
use alloy::primitives::Address;

use crate::premints::zora_premint::contract::IZoraPremintERC20V1;
use crate::premints::zora_premint::contract::IZoraPremintV2::IZoraPremintV2Errors;

use crate::rules::Rule;
use crate::storage::Reader;
use crate::types::PremintTypes;

impl Default for IZoraPremintERC20V1::ContractCreationConfig {
fn default() -> Self {
Self {
contractAdmin: Default::default(),
contractURI: Default::default(),
contractName: Default::default(),
}
}
}

impl Default for IZoraPremintERC20V1::TokenCreationConfig {
fn default() -> Self {
Self {
tokenURI: Default::default(),
maxSupply: Default::default(),
maxTokensPerAddress: Default::default(),
pricePerToken: Default::default(),
mintStart: Default::default(),
mintDuration: Default::default(),
royaltyBPS: Default::default(),
fixedPriceMinter: Default::default(),
royaltyMintSchedule: Default::default(),
royaltyRecipient: Default::default(),
}
}
}

impl Default for IZoraPremintERC20V1::CreatorAttribution {
fn default() -> Self {
Self {
tokenConfig: Default::default(),
uid: Default::default(),
version: Default::default(),
deleted: Default::default(),
}
}
}

implement_zora_premint_traits!(
IZoraPremintERC20V1,
ERC20V1,
"zora_premint_erc20v1",
"ERC20_1"
);

pub fn all_v2_rules<T: Reader>() -> Vec<Box<dyn Rule<T>>> {
vec![
typed_rule!(
PremintTypes::ZoraERC20V1,
ERC20V1::is_authorized_to_create_premint
),
typed_rule!(PremintTypes::ZoraERC20V1, ERC20V1::is_valid_signature),
typed_rule!(PremintTypes::ZoraERC20V1, ERC20V1::is_chain_supported),
typed_rule!(PremintTypes::ZoraERC20V1, ERC20V1::not_minted),
typed_rule!(
PremintTypes::ZoraERC20V1,
ERC20V1::premint_version_supported
),
]
}
5 changes: 5 additions & 0 deletions src/premints/zora_premint/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod contract;
pub mod erc20v1;
pub mod rules;
pub mod types;
pub mod v2;
137 changes: 137 additions & 0 deletions src/premints/zora_premint/rules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use crate::premints::zora_premint::v2::V2;
use crate::rules::Rule;
use crate::storage::Reader;
use crate::typed_rule;
use crate::types::PremintTypes;
#[macro_export]
macro_rules! zora_premint_rules {
($namespace:tt, $typ:ty, $version:literal) => {
pub async fn is_authorized_to_create_premint<T: $crate::storage::Reader>(
premint: &$typ,
context: &$crate::rules::RuleContext<T>,
) -> eyre::Result<$crate::rules::Evaluation> {
let rpc = match context.rpc {
None => return $crate::ignore!("Rule requires RPC call"),
Some(ref rpc) => rpc,
};

let call = $namespace::isAuthorizedToCreatePremintCall {
contractAddress: premint.collection_address,
signer: premint.collection.contractAdmin,
premintContractConfigContractAdmin: premint.collection.contractAdmin,
};

let result = $crate::chain::view_contract_call(
call,
rpc,
$crate::premints::zora_premint::contract::PREMINT_FACTORY_ADDR,
)
.await?;

match result.isAuthorized {
true => Ok($crate::rules::Evaluation::Accept),
false => $crate::reject!("Unauthorized to create premint"),
}
}

pub async fn not_minted<T: $crate::storage::Reader>(
premint: &$typ,
context: &$crate::rules::RuleContext<T>,
) -> eyre::Result<$crate::rules::Evaluation> {
let rpc = match context.rpc {
None => return $crate::ignore!("Rule requires RPC provider"),
Some(ref rpc) => rpc,
};

let call = $namespace::premintStatusCall {
contractAddress: premint.collection_address,
uid: premint.premint.uid,
};

let result = $crate::chain::view_contract_call(
call,
rpc,
$crate::premints::zora_premint::contract::PREMINT_FACTORY_ADDR,
)
.await?;

match result.contractCreated && !result.tokenIdForPremint.is_zero() {
false => Ok($crate::rules::Evaluation::Accept),
true => $crate::reject!("Premint already minted"),
}
}

pub async fn premint_version_supported<T: $crate::storage::Reader>(
premint: &$typ,
context: &$crate::rules::RuleContext<T>,
) -> eyre::Result<$crate::rules::Evaluation> {
let rpc = match context.rpc {
None => return $crate::ignore!("Rule requires RPC provider"),
Some(ref rpc) => rpc,
};

let call = $namespace::supportedPremintSignatureVersionsCall {
contractAddress: premint.collection_address,
};

let result = $crate::chain::view_contract_call(
call,
rpc,
$crate::premints::zora_premint::contract::PREMINT_FACTORY_ADDR,
)
.await?;

match result.versions.contains(&$version.to_string()) {
true => Ok($crate::rules::Evaluation::Accept),
false => $crate::reject!(concat!(
"Premint version ",
$version,
" not supported by contract"
)),
}
}

// * 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<T: $crate::storage::Reader>(
premint: &$typ,
_context: &$crate::rules::RuleContext<T>,
) -> eyre::Result<$crate::rules::Evaluation> {
// * 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: alloy::signers::Signature =
core::str::FromStr::from_str(premint.signature.as_str())?;

let domain = premint.eip712_domain();
let hash = alloy::sol_types::SolStruct::eip712_signing_hash(&premint.premint, &domain);
let signer: alloy::primitives::Address =
signature.recover_address_from_prehash(&hash)?;

if signer != premint.collection.contractAdmin {
$crate::reject!(
"Invalid signature for contract admin {} vs recovered {}",
premint.collection.contractAdmin,
signer
)
} else {
Ok($crate::rules::Evaluation::Accept)
}
}

pub async fn is_chain_supported<T: $crate::storage::Reader>(
premint: &$typ,
_context: &$crate::rules::RuleContext<T>,
) -> eyre::Result<$crate::rules::Evaluation> {
let supported_chains: Vec<u64> = vec![7777777, 999999999, 8453];
let chain_id = premint.chain_id;

match supported_chains.contains(&chain_id) {
true => Ok($crate::rules::Evaluation::Accept),
false => $crate::reject!("Chain not supported"),
}
}
};
}
Loading

0 comments on commit d21f180

Please sign in to comment.