Merge branch 'dev'

This commit is contained in:
microwavedcola1 2023-01-05 20:14:26 +01:00
commit a911a861f9
11 changed files with 220 additions and 39 deletions

View File

@ -22,7 +22,7 @@ on:
env:
CARGO_TERM_COLOR: always
SOLANA_VERSION: '1.14.9'
RUST_TOOLCHAIN: '1.60.0'
RUST_TOOLCHAIN: '1.65.0'
LOG_PROGRAM: 'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD'
defaults:
@ -59,7 +59,8 @@ jobs:
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy -- --deny=warnings --allow=clippy::style --allow=clippy::complexity
# The --allow args are due to clippy scanning anchor
run: cargo clippy --no-deps -- --deny=warnings --allow=clippy::style --allow=clippy::complexity --allow=clippy::manual-retain --allow=clippy::crate-in-macro-def --allow=clippy::result-large-err
tests:
name: Run tests

26
Cargo.lock generated
View File

@ -428,6 +428,12 @@ dependencies = [
"event-listener",
]
[[package]]
name = "async-once-cell"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61305cacf1d0c5c9d3ee283d22f8f1f8c743a18ceb44a1b102bd53476c141de"
[[package]]
name = "async-stream"
version = "0.2.1"
@ -1118,6 +1124,7 @@ dependencies = [
"anchor-lang",
"anchor-spl",
"anyhow",
"async-once-cell",
"async-trait",
"base64 0.13.1",
"bincode",
@ -1445,6 +1452,17 @@ version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "default-env"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f753eb82d29277e79efc625e84aecacfd4851ee50e05a8573a4740239a77bfd3"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"syn 0.15.44",
]
[[package]]
name = "der"
version = "0.5.1"
@ -3114,6 +3132,7 @@ dependencies = [
"borsh",
"bytemuck",
"checked_math",
"default-env",
"derivative",
"env_logger 0.9.3",
"fixed",
@ -3132,6 +3151,7 @@ dependencies = [
"solana-program",
"solana-program-test",
"solana-sdk",
"solana-security-txt",
"spl-associated-token-account",
"spl-token",
"static_assertions",
@ -6207,6 +6227,12 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "solana-security-txt"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e0461f3afb29d8591300b3dd09b5472b3772d65688a2826ad960b8c0d5fa605"
[[package]]
name = "solana-send-transaction-service"
version = "1.14.10"

31
SECURITY.md Normal file
View File

@ -0,0 +1,31 @@
# Important Notice
Please **DO NOT** create a GitHub issue to report a security problem. Instead, please send an email to hello@blockworks.foundation with a detailed description of the attack vector and security risk you have identified.
# Bug Bounty Overview
Mango Markets offers bug bounties for Mango Markets' on-chain program code; UI only bugs are omitted.
|Severity|Description|Bug Bounty|
|-----------|--------------|-------------|
|Critical|Bugs that freeze user funds or drain the contract's holdings or involve theft of funds without user signatures|10% of the value of the hack up to $1,000,000|
|High|Bugs that could temporarily freeze user funds or incorrectly assign value to user funds|$10,000 to $50,000 per bug, assessed on a case by case basis|
|Medium/Low|Bugs that don't threaten user funds|$1,000 to $5,000 per bug, assessed on a case by case basis|
The severity guidelines are based on [Immunefi's classification system](https://immunefi.com/severity-updated/).
Note that these are simply guidelines for the severity of the bugs. Each bug bounty submission will be evaluated on a case-by-case basis.
## Submission
Please email hello@blockworks.foundation with a detailed description of the attack vector. For critical and moderate bugs, we require a proof of concept done on a privately deployed mainnet contract. We will reach out in 1 business day with additional questions or next steps on the bug bounty.
## Bug Bounty Payment
Bug bounties will be paid in USDC or locked MNGO, after a DAO vote. The Mango DAO has never refused a valid bug bounty so far.
## Invalid Bug Bounties
The following are out of scope for the bug bounty:
1. Attacks that the reporter has already exploited themselves, leading to damage.
2. Attacks requiring access to leaked keys/credentials.
3. Attacks requiring access to privileged addresses (governance, admin).
4. Incorrect data supplied by third party oracles (this does not exclude oracle manipulation/flash loan attacks).
5. Lack of liquidity.
6. Third party, off-chain bot errors (for instance bugs with an arbitrage bot running on the smart contracts).
7. Best practice critiques.
8. Sybil attacks.

View File

@ -11,6 +11,7 @@ anchor-client = { path = "../anchor/client" }
anchor-lang = { path = "../anchor/lang" }
anchor-spl = { path = "../anchor/spl" }
anyhow = "1.0"
async-once-cell = { version = "0.4.2", features = ["unpin"] }
async-trait = "0.1.52"
fixed = { version = "=1.11.0", features = ["serde", "borsh"] }
fixed-macro = "^1.1.1"

View File

@ -1,6 +1,9 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use async_once_cell::unpin::Lazy;
use anyhow::Context;
use anchor_client::ClientError;
@ -12,7 +15,7 @@ use solana_sdk::pubkey::Pubkey;
use mango_v4::state::MangoAccountValue;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
pub trait AccountFetcher: Sync + Send {
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
async fn fetch_raw_account_lookup_table(
@ -54,7 +57,7 @@ pub struct RpcAccountFetcher {
pub rpc: RpcClientAsync,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl AccountFetcher for RpcAccountFetcher {
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
self.rpc
@ -99,9 +102,44 @@ impl AccountFetcher for RpcAccountFetcher {
}
}
struct CoalescedAsyncJob<Key, Output> {
jobs: HashMap<Key, Arc<Lazy<Output>>>,
}
impl<Key, Output> Default for CoalescedAsyncJob<Key, Output> {
fn default() -> Self {
Self {
jobs: Default::default(),
}
}
}
impl<Key: std::cmp::Eq + std::hash::Hash, Output: 'static> CoalescedAsyncJob<Key, Output> {
/// Either returns the job for `key` or registers a new job for it
fn run_coalesced<F: std::future::Future<Output = Output> + Send + 'static>(
&mut self,
key: Key,
fut: F,
) -> Arc<Lazy<Output>> {
self.jobs
.entry(key)
.or_insert_with(|| Arc::new(Lazy::new(Box::pin(fut))))
.clone()
}
fn remove(&mut self, key: &Key) {
self.jobs.remove(key);
}
}
#[derive(Default)]
struct AccountCache {
accounts: HashMap<Pubkey, AccountSharedData>,
keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec<Pubkey>>,
account_jobs: CoalescedAsyncJob<Pubkey, anyhow::Result<AccountSharedData>>,
program_accounts_jobs:
CoalescedAsyncJob<(Pubkey, [u8; 8]), anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
}
impl AccountCache {
@ -112,18 +150,24 @@ impl AccountCache {
}
pub struct CachedAccountFetcher<T: AccountFetcher> {
fetcher: T,
cache: Mutex<AccountCache>,
fetcher: Arc<T>,
cache: Arc<Mutex<AccountCache>>,
}
impl<T: AccountFetcher> Clone for CachedAccountFetcher<T> {
fn clone(&self) -> Self {
Self {
fetcher: self.fetcher.clone(),
cache: self.cache.clone(),
}
}
}
impl<T: AccountFetcher> CachedAccountFetcher<T> {
pub fn new(fetcher: T) -> Self {
pub fn new(fetcher: Arc<T>) -> Self {
Self {
fetcher,
cache: Mutex::new(AccountCache {
accounts: HashMap::new(),
keys_for_program_and_discriminator: HashMap::new(),
}),
cache: Arc::new(Mutex::new(AccountCache::default())),
}
}
@ -133,16 +177,41 @@ impl<T: AccountFetcher> CachedAccountFetcher<T> {
}
}
#[async_trait::async_trait(?Send)]
impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
#[async_trait::async_trait]
impl<T: AccountFetcher + 'static> AccountFetcher for CachedAccountFetcher<T> {
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
let mut cache = self.cache.lock().unwrap();
if let Some(account) = cache.accounts.get(address) {
return Ok(account.clone());
let fetch_job = {
let mut cache = self.cache.lock().unwrap();
if let Some(acc) = cache.accounts.get(address) {
return Ok(acc.clone());
}
// Start or fetch a reference to the fetch + cache update job
let self_copy = self.clone();
let address_copy = address.clone();
cache.account_jobs.run_coalesced(*address, async move {
let result = self_copy.fetcher.fetch_raw_account(&address_copy).await;
let mut cache = self_copy.cache.lock().unwrap();
// remove the job from the job list, so it can be redone if it errored
cache.account_jobs.remove(&address_copy);
// store a successful fetch
if let Ok(account) = result.as_ref() {
cache.accounts.insert(address_copy, account.clone());
}
result
})
};
match fetch_job.get().await {
Ok(v) => Ok(v.clone()),
// Can't clone the stored error, so need to stringize it
Err(err) => Err(anyhow::format_err!(
"fetch error in CachedAccountFetcher: {:?}",
err
)),
}
let account = self.fetcher.fetch_raw_account(address).await?;
cache.accounts.insert(*address, account.clone());
Ok(account)
}
async fn fetch_program_accounts(
@ -151,23 +220,45 @@ impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let cache_key = (*program, discriminator);
let mut cache = self.cache.lock().unwrap();
if let Some(accounts) = cache.keys_for_program_and_discriminator.get(&cache_key) {
return Ok(accounts
.iter()
.map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone()))
.collect::<Vec<_>>());
let fetch_job = {
let mut cache = self.cache.lock().unwrap();
if let Some(accounts) = cache.keys_for_program_and_discriminator.get(&cache_key) {
return Ok(accounts
.iter()
.map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone()))
.collect::<Vec<_>>());
}
let self_copy = self.clone();
let program_copy = program.clone();
cache
.program_accounts_jobs
.run_coalesced(cache_key.clone(), async move {
let result = self_copy
.fetcher
.fetch_program_accounts(&program_copy, discriminator)
.await;
let mut cache = self_copy.cache.lock().unwrap();
cache.program_accounts_jobs.remove(&cache_key);
if let Ok(accounts) = result.as_ref() {
cache
.keys_for_program_and_discriminator
.insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect());
for (pk, acc) in accounts.iter() {
cache.accounts.insert(*pk, acc.clone());
}
}
result
})
};
match fetch_job.get().await {
Ok(v) => Ok(v.clone()),
// Can't clone the stored error, so need to stringize it
Err(err) => Err(anyhow::format_err!(
"fetch error in CachedAccountFetcher: {:?}",
err
)),
}
let accounts = self
.fetcher
.fetch_program_accounts(program, discriminator)
.await?;
cache
.keys_for_program_and_discriminator
.insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect());
for (pk, acc) in accounts.iter() {
cache.accounts.insert(*pk, acc.clone());
}
Ok(accounts)
}
}

View File

@ -141,7 +141,7 @@ impl AccountFetcher {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl crate::AccountFetcher for AccountFetcher {
async fn fetch_raw_account(
&self,

View File

@ -220,7 +220,9 @@ impl MangoClient {
owner: Keypair,
) -> anyhow::Result<Self> {
let rpc = client.rpc_async();
let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc }));
let account_fetcher = Arc::new(CachedAccountFetcher::new(Arc::new(RpcAccountFetcher {
rpc,
})));
let mango_account =
account_fetcher_fetch_mango_account(&*account_fetcher, &account).await?;
let group = mango_account.fixed.group;

View File

@ -28,6 +28,7 @@ bincode = "1.3.3"
borsh = { version = "0.9.3", features = ["const-generics"] }
bytemuck = { version = "^1.7.2", features = ["min_const_generics"] }
checked_math = { path = "../../lib/checked_math" }
default-env = "0.1.1"
derivative = "2.2.0"
fixed = { version = "=1.11.0", features = ["serde", "borsh"] } # todo: higher versions don't work
fixed-macro = "^1.1.1"
@ -38,6 +39,7 @@ serum_dex = { version = "0.5.6", git = "https://github.com/blockworks-foundation
solana-address-lookup-table-program = "~1.14.9"
solana-program = "~1.14.9"
solana-sdk = { version = "~1.14.9", default-features = false, optional = true }
solana-security-txt = "1.1.0"
static_assertions = "1.1"
switchboard-program = ">=0.2.0"
switchboard-v2 = "0.1.17"

View File

@ -84,7 +84,7 @@ pub fn token_register_trustless(
oracle: ctx.accounts.oracle.key(),
oracle_config: OracleConfig {
conf_filter: I80F48::from_num(0.10),
max_staleness_slots: -1,
max_staleness_slots: 600,
reserved: [0; 72],
},
stable_price_model: StablePriceModel::default(),

View File

@ -760,3 +760,17 @@ impl anchor_lang::Id for Mango {
ID
}
}
#[cfg(not(feature = "no-entrypoint"))]
use {default_env::default_env, solana_security_txt::security_txt};
#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
name: "Mango v4",
project_url: "https://mango.markets",
contacts: "email:hello@blockworks.foundation,link:https://docs.mango.markets/mango-markets/bug-bounty,discord:https://discord.gg/mangomarkets",
policy: "https://github.com/blockworks-foundation/mango-v4/blob/main/SECURITY.md",
preferred_languages: "en",
source_code: "https://github.com/blockworks-foundation/mango-v4",
source_revision: default_env!("GITHUB_SHA", "Unknown source revision"),
source_release: default_env!("GITHUB_REF_NAME", "Unknown source release")
}

View File

@ -36,6 +36,7 @@ const MAINNET_MINTS = new Map([
['SOL', 'So11111111111111111111111111111111111111112'], // 4 Wrapped SOL
['MSOL', 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So'], // 5
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], // 6
['BONK', 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263'], // 7
]);
const MAINNET_ORACLES = new Map([
// USDC - stub oracle
@ -46,6 +47,7 @@ const MAINNET_ORACLES = new Map([
['MSOL', 'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'],
['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'],
['BTC', 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'],
['BONK', '4SZ1qb4MtSUrZcoeaeQ3BDzVCyqxw3VwSFpPiMTmn4GE'],
]);
// External markets are matched with those in https://github.com/openbook-dex/openbook-ts/blob/master/packages/serum/src/markets.json
@ -322,6 +324,17 @@ async function registerTokens() {
'MNGO',
);
console.log(`Registering BONK...`);
const bonkMainnetMint = new PublicKey(MAINNET_MINTS.get('BONK')!);
const bonkMainnetOracle = new PublicKey(MAINNET_ORACLES.get('BONK')!);
await client.tokenRegisterTrustless(
group,
bonkMainnetMint,
bonkMainnetOracle,
7,
'BONK',
);
// log tokens/banks
await group.reloadAll(client);
for (const bank of await Array.from(group.banksMapByMint.values())