├── .gitignore ├── Cargo.toml ├── README.md ├── abis └── token_vault.json └── src ├── lib.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fractional" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ethers = { version = "0.5.1", features = ["abigen"] } 10 | ethers-flashbots = "0.4.0" 11 | ethers-fireblocks = "0.1.1" 12 | ethers-structopt = { git = "https://github.com/gakonst/ethers-structopt" } 13 | 14 | once_cell = "1.8.0" 15 | serde_json = "1.0.67" 16 | structopt = "0.3.22" 17 | tokio = { version = "1.10.1", features = ["macros"] } 18 | color-eyre = "0.5.11" 19 | url = "2.2.2" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fractional-rs 2 | 3 | CLI & Utilities for [fractional.art](https://fractional.art) 4 | 5 | ## CLI Usage 6 | 7 | The CLI uses Flashbots' relay to submit the transactions. No bribe is required as 8 | you pay via the fee. It also supports [Fireblocks](fireblocks.com) API (requires vaults 9 | with the RAW feature on). 10 | 11 | ``` 12 | $ target/debug/fractional -h 13 | fractional 0.1.0 14 | 15 | USAGE: 16 | fractional [OPTIONS] --eth.url 17 | 18 | FLAGS: 19 | -h, --help Prints help information 20 | -V, --version Prints version information 21 | 22 | OPTIONS: 23 | --fireblocks.key Your fireblocks API key [env: FIREBLOCKS_API_KEY=] 24 | --flashbots.bribe The amount to be sent to the miner 25 | --flashbots.bribe_receiver 26 | The address that will receive the bribe. Ideally it should be a smart contract with a block.coinbase 27 | transfer 28 | --eth.hd_index your index in the standard hd path [default: 0] 29 | --eth.mnemonic Path to your mnemonic file 30 | --eth.private_key Your private key string 31 | --fireblocks.secret 32 | Path to your fireblocks.key file generated during api account creation [env: FIREBLOCKS_API_SECRET_PATH=] 33 | 34 | -u, --eth.url The tracing / archival node's URL 35 | --fireblocks.vault 36 | The fireblocks vault which will be used for authorizing transactions [env: FIREBLOCKS_VAULT=] 37 | 38 | 39 | ARGS: 40 | the fractional vault you're calling 41 | your bid (in wei) 42 | ``` 43 | 44 | ## Deployments 45 | 46 | ### Mainnet 47 | [Vault Factory](https://etherscan.io/address/0x85aa7f78bdb2de8f3e0c0010d99ad5853ffcfc63) 48 | 49 | [Token Vault](https://etherscan.io/address/0x7b0fce54574d9746414d11367f54c9ab94e53dca) 50 | 51 | [Settings](https://etherscan.io/address/0xE0FC79183a22106229B84ECDd55cA017A07eddCa) 52 | 53 | [Index ERC721 Factory](https://etherscan.io/address/0xde771104c0c44123d22d39bb716339cd0c3333a1) 54 | 55 | ### Rinkeby 56 | 57 | [Vault Factory](https://rinkeby.etherscan.io/address/0x458556c097251f52ca89cB81316B4113aC734BD1) 58 | 59 | [Token Vault](https://rinkeby.etherscan.io/address/0x825f25f908db46daEA42bd536d25f8633667f62b) 60 | 61 | [Settings](https://rinkeby.etherscan.io/address/0x1C0857f8642D704ecB213A752A3f68E51913A779) 62 | 63 | [Index ERC721 Factory](https://rinkeby.etherscan.io/address/0xee727b734aC43fc391b67caFd18e5DD4Dc939668) 64 | -------------------------------------------------------------------------------- /abis/token_vault.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_settings","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"buyer","type":"address"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"}],"name":"Bid","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Cash","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"}],"name":"PriceUpdate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"redeemer","type":"address"}],"name":"Redeem","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"buyer","type":"address"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"}],"name":"Start","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"buyer","type":"address"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"}],"name":"Won","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"auctionEnd","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"auctionLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"auctionState","outputs":[{"internalType":"enum TokenVault.State","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"bid","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"cash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"claimFees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"curator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"end","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"id","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_curator","type":"address"},{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_id","type":"uint256"},{"internalType":"uint256","name":"_supply","type":"uint256"},{"internalType":"uint256","name":"_listPrice","type":"uint256"},{"internalType":"uint256","name":"_fee","type":"uint256"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_curator","type":"address"}],"name":"kickCurator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lastClaimed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"livePrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"redeem","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"}],"name":"removeReserve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"reservePrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"reserveTotal","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"settings","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"start","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_length","type":"uint256"}],"name":"updateAuctionLength","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_curator","type":"address"}],"name":"updateCurator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_fee","type":"uint256"}],"name":"updateFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_new","type":"uint256"}],"name":"updateUserPrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"userPrices","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vaultClosed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"votingTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"weth","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"winning","outputs":[{"internalType":"address payable","name":"","type":"address"}],"stateMutability":"view","type":"function"}] 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::Result; 2 | use ethers::prelude::*; 3 | use once_cell::sync::Lazy; 4 | 5 | pub static TOKEN_VAULT: Lazy
= Lazy::new(|| { 6 | "0x7b0fce54574d9746414d11367f54c9ab94e53dca" 7 | .parse() 8 | .unwrap() 9 | }); 10 | 11 | use ethers::contract::abigen; 12 | abigen!(TokenVault, "./abis/token_vault.json"); 13 | 14 | use ethers_structopt::{EthereumOpts, FlashBotsOpts}; 15 | use structopt::StructOpt; 16 | 17 | #[derive(StructOpt, Clone)] 18 | pub struct VaultOpts { 19 | #[structopt(help = "the fractional vault you're calling")] 20 | pub vault: Address, 21 | 22 | #[structopt(help = "your bid (in wei)")] 23 | pub amount: U256, 24 | 25 | #[structopt(flatten)] 26 | pub eth: EthereumOpts, 27 | 28 | #[structopt(flatten)] 29 | pub flashbots: FlashBotsOpts, 30 | 31 | #[structopt(flatten)] 32 | pub fireblocks: FireblocksOpts, 33 | } 34 | 35 | #[derive(StructOpt, Clone)] 36 | pub struct FireblocksOpts { 37 | #[structopt( 38 | long = "fireblocks.secret", 39 | help = "Path to your fireblocks.key file generated during api account creation", 40 | env = "FIREBLOCKS_API_SECRET_PATH" 41 | )] 42 | secret: Option, 43 | 44 | #[structopt( 45 | long = "fireblocks.key", 46 | help = "Your fireblocks API key", 47 | env = "FIREBLOCKS_API_KEY", 48 | requires = "secret" 49 | )] 50 | api_key: Option, 51 | 52 | #[structopt( 53 | long = "fireblocks.vault", 54 | help = "The fireblocks vault which will be used for authorizing transactions", 55 | env = "FIREBLOCKS_VAULT", 56 | requires = "secret" 57 | )] 58 | vault_id: Option, 59 | } 60 | 61 | use ethers_fireblocks::{Config, FireblocksSigner}; 62 | impl FireblocksOpts { 63 | pub async fn signer(&self, chain_id: u64) -> Result> { 64 | let (secret, api_key, vault_id) = match (&self.secret, &self.api_key, &self.vault_id) { 65 | (Some(a), Some(b), Some(c)) => (a, b, c), 66 | (None, None, None) => return Ok(None), 67 | _ => unreachable!("Did you set all Fireblocks options?"), 68 | }; 69 | let cfg = Config::new(secret, &api_key, &vault_id, chain_id)?; 70 | let signer = FireblocksSigner::new(cfg).await; 71 | Ok(Some(signer)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::Result; 2 | use ethers::prelude::*; 3 | use ethers_flashbots::FlashbotsMiddleware; 4 | use fractional::{TokenVault, VaultOpts, TOKEN_VAULT}; 5 | use std::sync::Arc; 6 | use structopt::StructOpt; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | let opts = VaultOpts::from_args(); 11 | 12 | let provider = opts.eth.provider()?; 13 | 14 | if let Some(bribe) = opts.flashbots.bribe { 15 | println!( 16 | "Using Flashbots. Bribe {:?}. Bribe Receiver {:?}", 17 | bribe, opts.flashbots.bribe_receiver 18 | ); 19 | 20 | let bundle_signer = LocalWallet::new(&mut ethers::core::rand::thread_rng()); 21 | let provider = FlashbotsMiddleware::new( 22 | provider, 23 | url::Url::parse("https://relay.flashbots.net")?, 24 | bundle_signer, 25 | ); 26 | 27 | bid(provider, opts).await?; 28 | } else { 29 | bid(provider, opts).await?; 30 | }; 31 | 32 | Ok(()) 33 | } 34 | 35 | async fn bid(provider: M, opts: VaultOpts) -> Result<()> { 36 | let chain_id = provider.get_chainid().await?.as_u64(); 37 | if let Some(signer) = opts.fireblocks.signer(chain_id).await? { 38 | // will just sign transactions with the fireblocks signer, and submit them 39 | // via Flashbots as usual 40 | let provider = Arc::new(SignerMiddleware::new(provider, signer)); 41 | let fractional = TokenVault::new(*TOKEN_VAULT, provider.clone()); 42 | 43 | // TODO: We should be able to erase the type of the provider so they can 44 | // be easily composed. 45 | let call = fractional.bid().value(opts.amount); 46 | let res = call.send().await?; 47 | println!("Submitted tx: {:?}", *res); 48 | let receipt = res.await?; 49 | println!("Got receipt: {:?}", receipt); 50 | } else { 51 | let signer = opts.eth.signer()?.with_chain_id(chain_id); 52 | let provider = Arc::new(SignerMiddleware::new(provider, signer)); 53 | let fractional = TokenVault::new(*TOKEN_VAULT, provider.clone()); 54 | 55 | let call = fractional.bid().value(opts.amount); 56 | let res = call.send().await?; 57 | println!("Submitted tx: {:?}", *res); 58 | let receipt = res.await?; 59 | println!("Got receipt: {:?}", receipt); 60 | }; 61 | 62 | Ok(()) 63 | } 64 | --------------------------------------------------------------------------------