Merge branch 'dev'
This commit is contained in:
commit
a911a861f9
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
|
@ -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"
|
||||
|
|
|
@ -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 fetch_job = {
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
if let Some(account) = cache.accounts.get(address) {
|
||||
return Ok(account.clone());
|
||||
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,6 +220,7 @@ impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
|
|||
discriminator: [u8; 8],
|
||||
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
|
||||
let cache_key = (*program, discriminator);
|
||||
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
|
||||
|
@ -158,16 +228,37 @@ impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
|
|||
.map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone()))
|
||||
.collect::<Vec<_>>());
|
||||
}
|
||||
let accounts = self
|
||||
|
||||
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, discriminator)
|
||||
.await?;
|
||||
.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());
|
||||
}
|
||||
Ok(accounts)
|
||||
}
|
||||
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
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in New Issue